From 04638cead54b3e9dd659702a3a22fd117a10e00a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 08:32:28 +0200 Subject: [PATCH 001/576] chore(deps): bump solana-slot-hashes from 3.0.1 to 3.0.2 (#12603) * chore(deps): bump solana-slot-hashes from 3.0.1 to 3.0.2 Bumps [solana-slot-hashes](https://github.com/anza-xyz/solana-sdk) from 3.0.1 to 3.0.2. - [Release notes](https://github.com/anza-xyz/solana-sdk/releases) - [Commits](https://github.com/anza-xyz/solana-sdk/compare/borsh@v3.0.1...borsh@v3.0.2) --- updated-dependencies: - dependency-name: solana-slot-hashes dependency-version: 3.0.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Update all workspaces --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- dev-bins/Cargo.lock | 4 ++-- programs/sbf/Cargo.lock | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7956427ebf0..e6c090ad221 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10396,9 +10396,9 @@ dependencies = [ [[package]] name = "solana-slot-hashes" -version = "3.0.1" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2585f70191623887329dfb5078da3a00e15e3980ea67f42c2e10b07028419f43" +checksum = "0a57c158c35629f9e302ab385f16b15813f4927a31c27dda72f3df828bb08d93" dependencies = [ "serde", "serde_derive", diff --git a/Cargo.toml b/Cargo.toml index cd6cbc8c322..a0e20c4a903 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -478,7 +478,7 @@ solana-shred-version = "3.0.1" solana-signature = { version = "3.4.0", default-features = false } solana-signer = "3.0.0" solana-signer-store = "0.1.0" -solana-slot-hashes = "3.0.1" +solana-slot-hashes = "3.0.2" solana-slot-history = "3.0.1" solana-stable-layout = "3.0.1" solana-stake-interface = "3.1.0" diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 30469159172..000a37847be 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -8924,9 +8924,9 @@ dependencies = [ [[package]] name = "solana-slot-hashes" -version = "3.0.1" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2585f70191623887329dfb5078da3a00e15e3980ea67f42c2e10b07028419f43" +checksum = "0a57c158c35629f9e302ab385f16b15813f4927a31c27dda72f3df828bb08d93" dependencies = [ "serde", "serde_derive", diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index a436b6ecfaa..ae07cc7bcc1 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -9276,9 +9276,9 @@ dependencies = [ [[package]] name = "solana-slot-hashes" -version = "3.0.1" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2585f70191623887329dfb5078da3a00e15e3980ea67f42c2e10b07028419f43" +checksum = "0a57c158c35629f9e302ab385f16b15813f4927a31c27dda72f3df828bb08d93" dependencies = [ "serde", "serde_derive", From 49853a65e3a545439b5dca78f1f2f0604a617b32 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 09:12:09 +0200 Subject: [PATCH 002/576] chore(deps): bump solana-last-restart-slot from 3.0.0 to 3.0.1 in /dev-bins (#12611) * chore(deps): bump solana-last-restart-slot in /dev-bins Bumps [solana-last-restart-slot](https://github.com/anza-xyz/solana-sdk) from 3.0.0 to 3.0.1. - [Release notes](https://github.com/anza-xyz/solana-sdk/releases) - [Commits](https://github.com/anza-xyz/solana-sdk/compare/sdk@v3.0.0...borsh@v3.0.1) --- updated-dependencies: - dependency-name: solana-last-restart-slot dependency-version: 3.0.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Update all workspaces --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- dev-bins/Cargo.lock | 4 ++-- programs/sbf/Cargo.lock | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e6c090ad221..80c4f0a0f18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8752,9 +8752,9 @@ dependencies = [ [[package]] name = "solana-last-restart-slot" -version = "3.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcda154ec827f5fc1e4da0af3417951b7e9b8157540f81f936c4a8b1156134d0" +checksum = "426711c6564b790026e45cabec3c64b971864c48b6b2d83c0ebf52a118bb4cda" dependencies = [ "serde", "serde_derive", diff --git a/Cargo.toml b/Cargo.toml index a0e20c4a903..4eb1f750df8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -410,7 +410,7 @@ solana-instruction-error = "2.3.0" solana-instructions-sysvar = "3.0.0" solana-keccak-hasher = "3.1.0" solana-keypair = "3.1.2" -solana-last-restart-slot = "3.0.0" +solana-last-restart-slot = "3.0.1" solana-lattice-hash = { path = "lattice-hash", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-leader-schedule = { path = "leader-schedule", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-ledger = { path = "ledger", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 000a37847be..570035b7191 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -7575,9 +7575,9 @@ dependencies = [ [[package]] name = "solana-last-restart-slot" -version = "3.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcda154ec827f5fc1e4da0af3417951b7e9b8157540f81f936c4a8b1156134d0" +checksum = "426711c6564b790026e45cabec3c64b971864c48b6b2d83c0ebf52a118bb4cda" dependencies = [ "serde", "serde_derive", diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index ae07cc7bcc1..313579feee1 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -7239,9 +7239,9 @@ dependencies = [ [[package]] name = "solana-last-restart-slot" -version = "3.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcda154ec827f5fc1e4da0af3417951b7e9b8157540f81f936c4a8b1156134d0" +checksum = "426711c6564b790026e45cabec3c64b971864c48b6b2d83c0ebf52a118bb4cda" dependencies = [ "serde", "serde_derive", From eac5b9f045368f02a2d2b0059409c715afd04893 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 09:29:12 +0200 Subject: [PATCH 003/576] chore(deps): bump solana-epoch-rewards from 3.0.1 to 3.0.2 (#12605) * chore(deps): bump solana-epoch-rewards from 3.0.1 to 3.0.2 Bumps [solana-epoch-rewards](https://github.com/anza-xyz/solana-sdk) from 3.0.1 to 3.0.2. - [Release notes](https://github.com/anza-xyz/solana-sdk/releases) - [Commits](https://github.com/anza-xyz/solana-sdk/compare/borsh@v3.0.1...borsh@v3.0.2) --- updated-dependencies: - dependency-name: solana-epoch-rewards dependency-version: 3.0.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Update all workspaces --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- dev-bins/Cargo.lock | 4 ++-- programs/sbf/Cargo.lock | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 80c4f0a0f18..611c157c912 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8165,9 +8165,9 @@ dependencies = [ [[package]] name = "solana-epoch-rewards" -version = "3.0.1" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5e7b0ba210593ba8ddd39d6d234d81795d1671cebf3026baa10d5dc23ac42f0" +checksum = "1cddf2388b28291210d9aa60690740733cab527531f06ed153c4d388951e407c" dependencies = [ "serde", "serde_derive", diff --git a/Cargo.toml b/Cargo.toml index 4eb1f750df8..b9aa213ca67 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -385,7 +385,7 @@ solana-download-utils = { path = "download-utils", version = "=4.2.0-alpha.0", f solana-ed25519-program = "3.0.0" solana-entry = { path = "entry", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-epoch-info = "3.1.0" -solana-epoch-rewards = "3.0.1" +solana-epoch-rewards = "3.0.2" solana-epoch-rewards-hasher = "3.1.0" solana-epoch-schedule = "3.1.0" solana-faucet = { path = "faucet", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 570035b7191..13f4a26a8ba 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -7141,9 +7141,9 @@ dependencies = [ [[package]] name = "solana-epoch-rewards" -version = "3.0.1" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5e7b0ba210593ba8ddd39d6d234d81795d1671cebf3026baa10d5dc23ac42f0" +checksum = "1cddf2388b28291210d9aa60690740733cab527531f06ed153c4d388951e407c" dependencies = [ "serde", "serde_derive", diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index 313579feee1..8826c09dbf5 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -6824,9 +6824,9 @@ dependencies = [ [[package]] name = "solana-epoch-rewards" -version = "3.0.1" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5e7b0ba210593ba8ddd39d6d234d81795d1671cebf3026baa10d5dc23ac42f0" +checksum = "1cddf2388b28291210d9aa60690740733cab527531f06ed153c4d388951e407c" dependencies = [ "serde", "serde_derive", From ddb592b10e2adc9178cfc5d616be569cf049125e Mon Sep 17 00:00:00 2001 From: Brooks Date: Wed, 20 May 2026 07:01:05 -0400 Subject: [PATCH 004/576] Bank::get_slot_history() is fallible (#12598) --- core/src/replay_stage.rs | 4 +++- core/src/validator.rs | 4 +++- rpc-client-api/src/custom_error.rs | 8 ++++++++ rpc/src/rpc.rs | 4 +++- runtime/src/bank.rs | 4 ++-- runtime/src/snapshot_bank_utils.rs | 10 ++++------ 6 files changed, 23 insertions(+), 11 deletions(-) diff --git a/core/src/replay_stage.rs b/core/src/replay_stage.rs index 56edaea51c4..f552a192ce8 100644 --- a/core/src/replay_stage.rs +++ b/core/src/replay_stage.rs @@ -1768,7 +1768,9 @@ impl ReplayStage { ) -> Result { let tower = Tower::restore(tower_storage, node_pubkey).and_then(|restored_tower| { let root_bank = bank_forks.read().unwrap().root_bank(); - let slot_history = root_bank.get_slot_history(); + let slot_history = root_bank + .get_slot_history() + .expect("slot history must exist"); restored_tower.adjust_lockouts_after_replay(root_bank.slot(), &slot_history) }); match tower { diff --git a/core/src/validator.rs b/core/src/validator.rs index 9fcd10167bb..0b6545ccbd2 100644 --- a/core/src/validator.rs +++ b/core/src/validator.rs @@ -2059,7 +2059,9 @@ fn post_process_restored_tower( let restored_tower = restored_tower.and_then(|tower| { let root_bank = bank_forks.root_bank(); - let slot_history = root_bank.get_slot_history(); + let slot_history = root_bank + .get_slot_history() + .expect("slot history must exist"); // make sure tower isn't corrupted first before the following hard fork check let tower = tower.adjust_lockouts_after_replay(root_bank.slot(), &slot_history); diff --git a/rpc-client-api/src/custom_error.rs b/rpc-client-api/src/custom_error.rs index 620ae497268..7f39d6c7a43 100644 --- a/rpc-client-api/src/custom_error.rs +++ b/rpc-client-api/src/custom_error.rs @@ -29,6 +29,7 @@ pub const JSON_RPC_SERVER_ERROR_EPOCH_REWARDS_PERIOD_ACTIVE: i64 = -32017; pub const JSON_RPC_SERVER_ERROR_SLOT_NOT_EPOCH_BOUNDARY: i64 = -32018; pub const JSON_RPC_SERVER_ERROR_LONG_TERM_STORAGE_UNREACHABLE: i64 = -32019; pub const JSON_RPC_SERVER_ERROR_FILTER_TRANSACTION_NOT_FOUND: i64 = -32020; +pub const JSON_RPC_SERVER_ERROR_NO_SLOT_HISTORY: i64 = -32021; #[derive(Error, Debug)] #[allow(clippy::large_enum_variant)] @@ -83,6 +84,8 @@ pub enum RpcCustomError { LongTermStorageUnreachable, #[error("FilterTransactionNotFound")] FilterTransactionNotFound { signature: String }, + #[error("NoSlotHistory")] + NoSlotHistory, } #[derive(Debug, Serialize, Deserialize)] @@ -267,6 +270,11 @@ impl From for Error { message: format!("Transaction {signature} not found"), data: None, }, + RpcCustomError::NoSlotHistory => Self { + code: ErrorCode::ServerError(JSON_RPC_SERVER_ERROR_NO_SLOT_HISTORY), + message: "No slot history".to_string(), + data: None, + }, } } } diff --git a/rpc/src/rpc.rs b/rpc/src/rpc.rs index bbeef742949..52dc7167838 100644 --- a/rpc/src/rpc.rs +++ b/rpc/src/rpc.rs @@ -3136,7 +3136,9 @@ pub mod rpc_bank { } }; - let slot_history = bank.get_slot_history(); + let Some(slot_history) = bank.get_slot_history() else { + return Err(RpcCustomError::NoSlotHistory.into()); + }; if first_slot < slot_history.oldest() { return Err(Error::invalid_params(format!( "firstSlot, {}, is too small; min {}", diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index e88be995e7c..d7512be4b98 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -2491,8 +2491,8 @@ impl Bank { }); } - pub fn get_slot_history(&self) -> SlotHistory { - from_account(&self.get_account(&sysvar::slot_history::id()).unwrap()).unwrap() + pub fn get_slot_history(&self) -> Option { + from_account(&self.get_account(&sysvar::slot_history::id())?) } fn update_epoch_stakes( diff --git a/runtime/src/snapshot_bank_utils.rs b/runtime/src/snapshot_bank_utils.rs index 859304dab22..8347308f96c 100644 --- a/runtime/src/snapshot_bank_utils.rs +++ b/runtime/src/snapshot_bank_utils.rs @@ -527,12 +527,10 @@ fn verify_slot_deltas( ) -> std::result::Result<(), VerifySlotDeltasError> { let max_root_entries = bank.status_cache.read().unwrap().max_root_entries(); let info = verify_slot_deltas_structural(slot_deltas, bank.slot(), max_root_entries)?; - verify_slot_deltas_with_history( - &info.slots, - &bank.get_slot_history(), - bank.slot(), - max_root_entries, - ) + let slot_history = bank + .get_slot_history() + .expect("snapshot bank must have slot history"); + verify_slot_deltas_with_history(&info.slots, &slot_history, bank.slot(), max_root_entries) } /// Verify that the snapshot's slot deltas are not corrupt/invalid From 321c741c28cb7b4bbd25f91031e4512122a93534 Mon Sep 17 00:00:00 2001 From: Kamil Skalski Date: Wed, 20 May 2026 13:54:45 +0200 Subject: [PATCH 005/576] clippy(programs): gate asm feature declaration with solana target (#12592) cleanup(programs): gate feature declaration on solana target --- programs/sbf/rust/divide_by_zero/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/programs/sbf/rust/divide_by_zero/src/lib.rs b/programs/sbf/rust/divide_by_zero/src/lib.rs index ad0f9f6e4f9..a29ba207d9d 100644 --- a/programs/sbf/rust/divide_by_zero/src/lib.rs +++ b/programs/sbf/rust/divide_by_zero/src/lib.rs @@ -1,6 +1,6 @@ //! Example/test program to trigger vm error by dividing by zero -#![feature(asm_experimental_arch)] +#![cfg_attr(target_os = "solana", feature(asm_experimental_arch))] use { solana_account_info::AccountInfo, solana_program_error::ProgramResult, solana_pubkey::Pubkey, From 03c3122510964614fd356665b2d332a111d7e56d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 21:00:51 +0800 Subject: [PATCH 006/576] chore(deps): bump solana-curve25519 from 4.0.0 to 4.0.1 (#12612) * chore(deps): bump solana-curve25519 from 4.0.0 to 4.0.1 in /dev-bins Bumps [solana-curve25519](https://github.com/anza-xyz/solana-sdk) from 4.0.0 to 4.0.1. - [Release notes](https://github.com/anza-xyz/solana-sdk/releases) - [Commits](https://github.com/anza-xyz/solana-sdk/compare/sdk@v4.0.0...sdk@v4.0.1) --- updated-dependencies: - dependency-name: solana-curve25519 dependency-version: 4.0.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Update all workspaces --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 6 +++--- Cargo.toml | 2 +- dev-bins/Cargo.lock | 6 +++--- programs/sbf/Cargo.lock | 8 ++++---- programs/sbf/Cargo.toml | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 611c157c912..f65fd84d5c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8031,9 +8031,9 @@ dependencies = [ [[package]] name = "solana-curve25519" -version = "4.0.0" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ee66a3e25ed8d20ed6fcd1ac71735415ff1edaf33ff1ec2118826eba927bcc" +checksum = "14b4d2a4bf0d0b0a86c22111917e86e8bd39a7b31420fb2c7d73eb83761fc7af" dependencies = [ "bytemuck", "bytemuck_derive", @@ -10741,7 +10741,7 @@ dependencies = [ "solana-bn254", "solana-clock", "solana-cpi", - "solana-curve25519 4.0.0", + "solana-curve25519 4.0.1", "solana-epoch-rewards", "solana-epoch-schedule", "solana-fee-calculator", diff --git a/Cargo.toml b/Cargo.toml index b9aa213ca67..ee55051e909 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -379,7 +379,7 @@ solana-connection-cache = { path = "connection-cache", version = "=4.2.0-alpha.0 solana-core = { path = "core", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-cost-model = { path = "cost-model", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-cpi = "3.1.0" -solana-curve25519 = "4.0.0" +solana-curve25519 = "4.0.1" solana-derivation-path = "3.0.0" solana-download-utils = { path = "download-utils", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-ed25519-program = "3.0.0" diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 13f4a26a8ba..b8f4482a500 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -7034,9 +7034,9 @@ dependencies = [ [[package]] name = "solana-curve25519" -version = "4.0.0" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ee66a3e25ed8d20ed6fcd1ac71735415ff1edaf33ff1ec2118826eba927bcc" +checksum = "14b4d2a4bf0d0b0a86c22111917e86e8bd39a7b31420fb2c7d73eb83761fc7af" dependencies = [ "bytemuck", "bytemuck_derive", @@ -9243,7 +9243,7 @@ dependencies = [ "solana-bn254", "solana-clock", "solana-cpi", - "solana-curve25519 4.0.0", + "solana-curve25519 4.0.1", "solana-hash 4.3.0", "solana-hash-512", "solana-instruction", diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index 8826c09dbf5..dbe5b9b4a8c 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -6711,9 +6711,9 @@ dependencies = [ [[package]] name = "solana-curve25519" -version = "4.0.0" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ee66a3e25ed8d20ed6fcd1ac71735415ff1edaf33ff1ec2118826eba927bcc" +checksum = "14b4d2a4bf0d0b0a86c22111917e86e8bd39a7b31420fb2c7d73eb83761fc7af" dependencies = [ "bytemuck", "bytemuck_derive", @@ -8483,7 +8483,7 @@ dependencies = [ name = "solana-sbf-rust-curve25519" version = "4.2.0-alpha.0" dependencies = [ - "solana-curve25519 4.0.0", + "solana-curve25519 4.0.1", "solana-msg", "solana-program-entrypoint", ] @@ -9576,7 +9576,7 @@ dependencies = [ "solana-bn254", "solana-clock", "solana-cpi", - "solana-curve25519 4.0.0", + "solana-curve25519 4.0.1", "solana-hash 4.3.0", "solana-hash-512", "solana-instruction", diff --git a/programs/sbf/Cargo.toml b/programs/sbf/Cargo.toml index 5a03284e300..609f26a4dce 100644 --- a/programs/sbf/Cargo.toml +++ b/programs/sbf/Cargo.toml @@ -112,7 +112,7 @@ solana-clock = { version = "=3.0.1", features = ["serde", "sysvar"] } solana-compute-budget = { path = "../../compute-budget", version = "=4.2.0-alpha.0" } solana-compute-budget-instruction = { path = "../../compute-budget-instruction", version = "=4.2.0-alpha.0" } solana-cpi = "=3.1.0" -solana-curve25519 = "=4.0.0" +solana-curve25519 = "=4.0.1" solana-define-syscall = "=3.0.0" solana-fee = { path = "../../fee", version = "=4.2.0-alpha.0" } solana-hash = { version = "=4.3.0", features = ["bytemuck", "serde", "std"] } From eadfda362499055d9aae7b335f18ef771a1ca265 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Wed, 20 May 2026 15:52:42 +0100 Subject: [PATCH 007/576] Revise RPC API update instructions in `.mergify.yml` (#12609) * Revise RPC API update instructions in .mergify.yml Updated RPC API change instructions with new examples. * Remove unecessary whitespace changes --- .mergify.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.mergify.yml b/.mergify.yml index 6bf45241cdf..c06a16e455d 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -200,8 +200,8 @@ pull_request_rules: message: | If this PR represents a change to the public RPC API: - 1. Make sure it includes a complementary update to `rpc-client/` ([example](https://github.com/solana-labs/solana/pull/29558/files)) - 2. Open a follow-up PR to update the JavaScript client `@solana/kit` ([example](https://github.com/solana-labs/solana-web3.js/pull/2868/files)) + 1. Make sure it includes a complementary update to `rpc-client/` ([example](https://github.com/anza-xyz/agave/pull/2036/files)) + 2. Open a follow-up PR to update the JavaScript client `@solana/kit` ([example](https://github.com/anza-xyz/kit/pull/974/files)) Thank you for keeping the RPC clients in sync with the server API @{{author}}. - name: Reminder to add Firedancer team to changes in `programs/` From da1c5a034c009a5c1d5f491b165a904d8cb19a53 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 23:00:42 +0800 Subject: [PATCH 008/576] chore(deps): bump solana-serialize-utils from 3.1.1 to 3.1.2 (#12622) * chore(deps): bump solana-serialize-utils from 3.1.1 to 3.1.2 Bumps [solana-serialize-utils](https://github.com/anza-xyz/solana-sdk) from 3.1.1 to 3.1.2. - [Release notes](https://github.com/anza-xyz/solana-sdk/releases) - [Commits](https://github.com/anza-xyz/solana-sdk/compare/bn254@v3.1.1...bn254@v3.1.2) --- updated-dependencies: - dependency-name: solana-serialize-utils dependency-version: 3.1.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Update all workspaces --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- dev-bins/Cargo.lock | 4 ++-- programs/sbf/Cargo.lock | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f65fd84d5c4..59a488c957f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10300,9 +10300,9 @@ dependencies = [ [[package]] name = "solana-serialize-utils" -version = "3.1.1" +version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d7cc401931d178472358e6b78dc72d031dc08f752d7410f0e8bd259dd6f02fa" +checksum = "761357b0853c9623bf12c1d2314b3d6160a85b087b84c45224fb85766d22616b" dependencies = [ "solana-instruction-error", "solana-pubkey 4.2.0", diff --git a/Cargo.toml b/Cargo.toml index ee55051e909..5bf9a520e86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -470,7 +470,7 @@ solana-seed-phrase = "3.0.0" solana-send-transaction-service = { path = "send-transaction-service", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-serde = "3.0.0" solana-serde-varint = "3.0.1" -solana-serialize-utils = "3.1.1" +solana-serialize-utils = "3.1.2" solana-sha256-hasher = "3.1.0" solana-sha512-hasher = "1.0.1" solana-short-vec = "3.2.1" diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index b8f4482a500..e6c1bfbe4fd 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -8833,9 +8833,9 @@ dependencies = [ [[package]] name = "solana-serialize-utils" -version = "3.1.1" +version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d7cc401931d178472358e6b78dc72d031dc08f752d7410f0e8bd259dd6f02fa" +checksum = "761357b0853c9623bf12c1d2314b3d6160a85b087b84c45224fb85766d22616b" dependencies = [ "solana-instruction-error", "solana-pubkey 4.2.0", diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index dbe5b9b4a8c..e84f84ad23c 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -9185,9 +9185,9 @@ dependencies = [ [[package]] name = "solana-serialize-utils" -version = "3.1.1" +version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d7cc401931d178472358e6b78dc72d031dc08f752d7410f0e8bd259dd6f02fa" +checksum = "761357b0853c9623bf12c1d2314b3d6160a85b087b84c45224fb85766d22616b" dependencies = [ "solana-instruction-error", "solana-pubkey 4.2.0", From 1662ba839644020f5087292daa9bf4879edfbe67 Mon Sep 17 00:00:00 2001 From: Lucas Ste <38472950+LucasSte@users.noreply.github.com> Date: Wed, 20 May 2026 12:04:55 -0300 Subject: [PATCH 009/576] Remove redundant account owner check (#12607) Remove redundant check --- program-runtime/src/cpi.rs | 4 ++-- programs/sbf/tests/programs.rs | 12 ++++++------ transaction-context/src/instruction_accounts.rs | 4 ---- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/program-runtime/src/cpi.rs b/program-runtime/src/cpi.rs index bbd3f1d8029..d5cd9c2027b 100644 --- a/program-runtime/src/cpi.rs +++ b/program-runtime/src/cpi.rs @@ -2530,7 +2530,7 @@ mod tests { virtual_address_space_adjustments, account_data_direct_mapping, ), - Err(error) if error.downcast_ref::().unwrap() == &InstructionError::AccountDataSizeChanged + Err(error) if error.downcast_ref::().unwrap() == &InstructionError::ExternalAccountDataModified ); // truncating resize @@ -2548,7 +2548,7 @@ mod tests { virtual_address_space_adjustments, account_data_direct_mapping, ), - Err(error) if error.downcast_ref::().unwrap() == &InstructionError::AccountDataSizeChanged + Err(error) if error.downcast_ref::().unwrap() == &InstructionError::ExternalAccountDataModified ); } } diff --git a/programs/sbf/tests/programs.rs b/programs/sbf/tests/programs.rs index 4617c02fbb3..0dbbc5ca644 100644 --- a/programs/sbf/tests/programs.rs +++ b/programs/sbf/tests/programs.rs @@ -3039,7 +3039,7 @@ fn test_program_sbf_realloc() { ) .unwrap_err() .unwrap(), - TransactionError::InstructionError(0, InstructionError::AccountDataSizeChanged) + TransactionError::InstructionError(0, InstructionError::ExternalAccountDataModified) ); // realloc and assign to self via cpi @@ -3066,7 +3066,7 @@ fn test_program_sbf_realloc() { ) .unwrap_err() .unwrap(), - TransactionError::InstructionError(0, InstructionError::AccountDataSizeChanged) + TransactionError::InstructionError(0, InstructionError::ExternalAccountDataModified) ); // Assign to self and realloc via cpi @@ -3344,7 +3344,7 @@ fn test_program_sbf_realloc_invoke() { ) .unwrap_err() .unwrap(), - TransactionError::InstructionError(0, InstructionError::AccountDataSizeChanged) + TransactionError::InstructionError(0, InstructionError::ExternalAccountDataModified) ); // realloc and assign to self via system program @@ -3375,7 +3375,7 @@ fn test_program_sbf_realloc_invoke() { ) .unwrap_err() .unwrap(), - TransactionError::InstructionError(0, InstructionError::AccountDataSizeChanged) + TransactionError::InstructionError(0, InstructionError::ExternalAccountDataModified) ); // Assign to self and realloc via system program @@ -4328,7 +4328,7 @@ fn test_cpi_account_data_updates() { if virtual_address_space_adjustments { InstructionError::InvalidRealloc } else { - InstructionError::AccountDataSizeChanged + InstructionError::ExternalAccountDataModified } ) ); @@ -4371,7 +4371,7 @@ fn test_cpi_account_data_updates() { if virtual_address_space_adjustments && deprecated_callee { InstructionError::InvalidRealloc } else { - InstructionError::AccountDataSizeChanged + InstructionError::ExternalAccountDataModified } ) ); diff --git a/transaction-context/src/instruction_accounts.rs b/transaction-context/src/instruction_accounts.rs index 00ac4119f2d..a84788a26de 100644 --- a/transaction-context/src/instruction_accounts.rs +++ b/transaction-context/src/instruction_accounts.rs @@ -350,10 +350,6 @@ impl BorrowedInstructionAccount<'_, '_> { /// Returns an error if the account data can not be resized to the given length pub fn can_data_be_resized(&self, new_len: usize) -> Result<(), InstructionError> { let old_len = self.get_data().len(); - // Only the owner can change the length of the data - if new_len != old_len && !self.is_owned_by_current_program() { - return Err(InstructionError::AccountDataSizeChanged); - } self.transaction_context .accounts .can_data_be_resized(old_len, new_len)?; From 3abb37da9301a7d66f55ff81a5dd40cc7b110e31 Mon Sep 17 00:00:00 2001 From: Mykola Dzham Date: Wed, 20 May 2026 19:29:28 +0200 Subject: [PATCH 010/576] Update version bump procedure for the master (#12446) We have the new workflow now running `xtask` that can and should be also used to bump the edge version in the master. --- RELEASE.md | 30 ++++++------------------------ 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index e22e5c1b7fb..10ab7ae56f3 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -91,34 +91,16 @@ Then proceed with the usual "Miscellaneous Clean up" steps below. #### Steps -1. Check out the latest commit on `master` branch: - ``` - git fetch --all - git checkout upstream/master - ``` +1. Update master branch to the next release minor version: dispatch [Bump Version](https://github.com/anza-xyz/agave/actions/workflows/bump-version.yml) pipeline against `master` branch to create the version bump PR. `Version bump level` should be either `minor` or `major`. +1. Review version bump PR, get all required approvals and merge it. +1. Determine the last commit **right before the version bump commit**. 1. Determine the new branch name. The name should be "v" + the first 2 version fields - from Cargo.toml. For example, a Cargo.toml with version = "0.9.0" implies + from Cargo.toml **at the commit right before the version bump commit**. For example, a Cargo.toml with version = "0.9.0" implies the next branch name is "v0.9". 1. Create the new branch and push this branch to the `agave` repository: ``` - git checkout -b - git push -u origin - ``` - -Alternatively use the Github UI. - -### Update master branch to the next release minor version - -1. After the new branch has been created and pushed, update the Cargo.toml files on **master** to the next semantic version (e.g. 0.9.0 -> 0.10.0) with: - ``` - $ scripts/increment-cargo-version.sh minor - ``` -1. Push all the changed Cargo.toml and Cargo.lock files to the `master` branch with something like: - ``` - git co -b version_update - git ls-files -m | xargs git add - git commit -m 'Bump version to X.Y+1.0' - git push -u origin version_update + git checkout -b + git push -u upstream ``` 1. Confirm that your freshly cut release branch is shown as `BETA_CHANNEL` and the previous release branch as `STABLE_CHANNEL`: ``` From 0efe8bfc8c19cac3bc32e520db533c4cef2eb610 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Mei=C3=9Fner?= Date: Wed, 20 May 2026 13:35:28 -0400 Subject: [PATCH 011/576] Refactor - `filter_executable_program_accounts()` (#12589) * Moves ProgramCacheMatchCriteria from TransactionBatchProcessor::replenish_program_cache() into TransactionBatchProcessor::filter_executable_program_accounts(). * Removes &self parameter from TransactionBatchProcessor::filter_executable_program_accounts(). * Moves filter_executable_program_accounts() into program_loader.rs * Moves get_account_shared_data() from get_program_deployment_slot() into filter_executable_program_accounts(). * Merges get_account_shared_data() calls into one. * Directly construct the resulting Vec instead of going through a HashMap. * Creates ProgramToLoad struct for the result type of filter_executable_program_accounts(). * Turns ProgramToLoad::program_id into a reference. * Review feedback by Lucas. * Removes mut from program_cache_for_tx_batch parameter of filter_executable_program_accounts(). * Rewrites test_filter_executable_program_accounts(). --- program-runtime/src/loaded_programs.rs | 121 +++++++----- svm/src/program_loader.rs | 192 ++++++++++++++++-- svm/src/transaction_processor.rs | 261 ++++--------------------- svm/tests/concurrent_tests.rs | 28 +-- 4 files changed, 299 insertions(+), 303 deletions(-) diff --git a/program-runtime/src/loaded_programs.rs b/program-runtime/src/loaded_programs.rs index 183f98cc5a4..1f4f487deae 100644 --- a/program-runtime/src/loaded_programs.rs +++ b/program-runtime/src/loaded_programs.rs @@ -183,6 +183,17 @@ impl EpochBoundaryPreparation { } } +/// Input of ProgramCache::extract() +#[derive(Clone, PartialEq, Debug)] +pub struct ProgramToLoad<'a> { + /// The program address + pub program_id: &'a Pubkey, + /// Potentially filter out / ignore some entries during the start up / catch up phase + pub match_criteria: ProgramCacheMatchCriteria, + /// When the program account was last written to (might be after the deployment slot) + pub last_modification_slot: Slot, +} + #[derive(Debug)] pub(crate) enum IndexImplementation { /// Fork-graph aware index implementation @@ -341,6 +352,7 @@ impl ProgramCacheForTxBatch { } } +#[derive(Clone, PartialEq, Debug)] pub enum ProgramCacheMatchCriteria { DeployedOnOrAfterSlot(Slot), Tombstone, @@ -572,7 +584,7 @@ impl ProgramCache { /// and returns which program accounts the accounts DB needs to load. pub fn extract( &self, - search_for: &mut Vec<(Pubkey, ProgramCacheMatchCriteria, Slot)>, + search_for: &mut Vec, loaded_programs_for_tx_batch: &mut ProgramCacheForTxBatch, program_runtime_environment_for_execution: &ProgramRuntimeEnvironment, increment_usage_counter: bool, @@ -587,8 +599,8 @@ impl ProgramCache { entries, loading_entries, } => { - search_for.retain(|(key, match_criteria, _slot)| { - if let Some(second_level) = entries.get(key) { + search_for.retain(|program_to_load| { + if let Some(second_level) = entries.get(program_to_load.program_id) { let mut filter_by_deployment_slot = None; for entry in second_level.iter().rev() { let required_deployment_slot = @@ -625,7 +637,10 @@ impl ProgramCache { .or(Some(entry.deployment_slot)); continue; } - if !Self::matches_criteria(entry, match_criteria) { + if !Self::matches_criteria( + entry, + &program_to_load.match_criteria, + ) { break; } if let ProgramCacheEntryType::Unloaded(_environment) = @@ -656,20 +671,20 @@ impl ProgramCache { } loaded_programs_for_tx_batch .entries - .insert(*key, entry_to_return); + .insert(*program_to_load.program_id, entry_to_return); return false; } } } if cooperative_loading_task.is_none() { let mut loading_entries = loading_entries.lock().unwrap(); - let entry = loading_entries.entry(*key); + let entry = loading_entries.entry(*program_to_load.program_id); if let Entry::Vacant(entry) = entry { entry.insert(( loaded_programs_for_tx_batch.slot, thread::current().id(), )); - cooperative_loading_task = Some(*key); + cooperative_loading_task = Some(*program_to_load.program_id); } } true @@ -941,7 +956,7 @@ pub(crate) mod tests { crate::{ loaded_programs::{ BlockRelation, ForkGraph, ProgramCache, ProgramCacheForTxBatch, - ProgramCacheMatchCriteria, ProgramRuntimeEnvironment, + ProgramCacheMatchCriteria, ProgramRuntimeEnvironment, ProgramToLoad, get_mock_program_runtime_environment, }, program_cache_entry::{ @@ -1793,11 +1808,11 @@ pub(crate) mod tests { } } - fn get_entries_to_load( + fn get_entries_to_load<'a>( cache: &ProgramCache, loading_slot: Slot, - keys: &[Pubkey], - ) -> Vec<(Pubkey, ProgramCacheMatchCriteria, Slot)> { + keys: &'a [Pubkey], + ) -> Vec> { let fork_graph = cache.fork_graph.as_ref().unwrap().upgrade().unwrap(); let locked_fork_graph = fork_graph.read().unwrap(); let entries = cache.get_flattened_entries_for_tests(); @@ -1813,12 +1828,10 @@ pub(crate) mod tests { BlockRelation::Equal | BlockRelation::Ancestor, ) }) - .map(|(program_id, entry)| { - ( - *program_id, - ProgramCacheMatchCriteria::NoCriteria, - entry.deployment_slot, - ) + .map(|(_program_id, entry)| ProgramToLoad { + program_id: key, + match_criteria: ProgramCacheMatchCriteria::NoCriteria, + last_modification_slot: entry.deployment_slot, }) }) .collect() @@ -1839,11 +1852,11 @@ pub(crate) mod tests { } fn match_missing( - missing: &[(Pubkey, ProgramCacheMatchCriteria, Slot)], - program: &Pubkey, + missing: &[ProgramToLoad], + program_id: &Pubkey, expected_result: bool, ) -> bool { - missing.iter().any(|(key, _, _)| key == program) == expected_result + missing.iter().any(|entry| entry.program_id == program_id) == expected_result } #[test] @@ -1918,8 +1931,8 @@ pub(crate) mod tests { // 23 // Testing fork 0 - 10 - 20 - 22 with current slot at 22 - let mut missing = - get_entries_to_load(&cache, 22, &[program1, program2, program3, program4]); + let keys = &[program1, program2, program3, program4]; + let mut missing = get_entries_to_load(&cache, 22, keys); assert!(match_missing(&missing, &program2, false)); assert!(match_missing(&missing, &program3, false)); let mut extracted = ProgramCacheForTxBatch::new(22); @@ -1928,8 +1941,7 @@ pub(crate) mod tests { assert!(match_slot(&extracted, &program4, 0, 22)); // Testing fork 0 - 5 - 11 - 15 - 16 with current slot at 15 - let mut missing = - get_entries_to_load(&cache, 15, &[program1, program2, program3, program4]); + let mut missing = get_entries_to_load(&cache, 15, keys); assert!(match_missing(&missing, &program3, false)); let mut extracted = ProgramCacheForTxBatch::new(15); cache.extract(&mut missing, &mut extracted, &env, true, true); @@ -1944,8 +1956,7 @@ pub(crate) mod tests { assert_eq!(tombstone.deployment_slot, 15); // Testing the same fork above, but current slot is now 18 (equal to effective slot of program4). - let mut missing = - get_entries_to_load(&cache, 18, &[program1, program2, program3, program4]); + let mut missing = get_entries_to_load(&cache, 18, keys); assert!(match_missing(&missing, &program3, false)); let mut extracted = ProgramCacheForTxBatch::new(18); cache.extract(&mut missing, &mut extracted, &env, true, true); @@ -1955,8 +1966,7 @@ pub(crate) mod tests { assert!(match_slot(&extracted, &program4, 15, 18)); // Testing the same fork above, but current slot is now 23 (future slot than effective slot of program4). - let mut missing = - get_entries_to_load(&cache, 23, &[program1, program2, program3, program4]); + let mut missing = get_entries_to_load(&cache, 23, keys); assert!(match_missing(&missing, &program3, false)); let mut extracted = ProgramCacheForTxBatch::new(23); cache.extract(&mut missing, &mut extracted, &env, true, true); @@ -1966,8 +1976,7 @@ pub(crate) mod tests { assert!(match_slot(&extracted, &program4, 15, 23)); // Testing fork 0 - 5 - 11 - 15 - 16 with current slot at 11 - let mut missing = - get_entries_to_load(&cache, 11, &[program1, program2, program3, program4]); + let mut missing = get_entries_to_load(&cache, 11, keys); assert!(match_missing(&missing, &program3, false)); let mut extracted = ProgramCacheForTxBatch::new(11); cache.extract(&mut missing, &mut extracted, &env, true, true); @@ -1998,8 +2007,7 @@ pub(crate) mod tests { // 23 // Testing fork 11 - 15 - 16- 19 - 22 with root at 5 and current slot at 22 - let mut missing = - get_entries_to_load(&cache, 21, &[program1, program2, program3, program4]); + let mut missing = get_entries_to_load(&cache, 21, keys); assert!(match_missing(&missing, &program3, false)); let mut extracted = ProgramCacheForTxBatch::new(21); cache.extract(&mut missing, &mut extracted, &env, true, true); @@ -2009,8 +2017,7 @@ pub(crate) mod tests { assert!(match_slot(&extracted, &program4, 15, 21)); // Testing fork 0 - 5 - 11 - 25 - 27 with current slot at 27 - let mut missing = - get_entries_to_load(&cache, 27, &[program1, program2, program3, program4]); + let mut missing = get_entries_to_load(&cache, 27, keys); let mut extracted = ProgramCacheForTxBatch::new(27); cache.extract(&mut missing, &mut extracted, &env, true, true); assert!(match_slot(&extracted, &program1, 0, 27)); @@ -2036,8 +2043,7 @@ pub(crate) mod tests { // 23 // Testing fork 16, 19, 23, with root at 15, current slot at 23 - let mut missing = - get_entries_to_load(&cache, 23, &[program1, program2, program3, program4]); + let mut missing = get_entries_to_load(&cache, 23, keys); assert!(match_missing(&missing, &program3, false)); let mut extracted = ProgramCacheForTxBatch::new(23); cache.extract(&mut missing, &mut extracted, &env, true, true); @@ -2086,7 +2092,8 @@ pub(crate) mod tests { cache.assign_program(&env, program3, 25, new_test_entry(25, 26)); // Testing fork 0 - 5 - 11 - 15 - 16 - 19 - 21 - 23 with current slot at 19 - let mut missing = get_entries_to_load(&cache, 12, &[program1, program2, program3]); + let keys = &[program1, program2, program3]; + let mut missing = get_entries_to_load(&cache, 12, keys); assert!(match_missing(&missing, &program3, false)); let mut extracted = ProgramCacheForTxBatch::new(12); cache.extract(&mut missing, &mut extracted, &env, true, true); @@ -2094,9 +2101,11 @@ pub(crate) mod tests { assert!(match_slot(&extracted, &program2, 11, 12)); // Test the same fork, but request the program modified at a later slot than what's in the cache. - let mut missing = get_entries_to_load(&cache, 12, &[program1, program2, program3]); - missing.get_mut(0).unwrap().1 = ProgramCacheMatchCriteria::DeployedOnOrAfterSlot(5); - missing.get_mut(1).unwrap().1 = ProgramCacheMatchCriteria::DeployedOnOrAfterSlot(5); + let mut missing = get_entries_to_load(&cache, 12, keys); + missing.get_mut(0).unwrap().match_criteria = + ProgramCacheMatchCriteria::DeployedOnOrAfterSlot(5); + missing.get_mut(1).unwrap().match_criteria = + ProgramCacheMatchCriteria::DeployedOnOrAfterSlot(5); assert!(match_missing(&missing, &program3, false)); let mut extracted = ProgramCacheForTxBatch::new(12); cache.extract(&mut missing, &mut extracted, &env, true, true); @@ -2159,7 +2168,8 @@ pub(crate) mod tests { ); // Testing fork 0 - 5 - 11 - 15 - 16 - 19 - 21 - 23 with current slot at 19 - let mut missing = get_entries_to_load(&cache, 19, &[program1, program2, program3]); + let keys = &[program1, program2, program3]; + let mut missing = get_entries_to_load(&cache, 19, keys); assert!(match_missing(&missing, &program3, false)); let mut extracted = ProgramCacheForTxBatch::new(19); cache.extract(&mut missing, &mut extracted, &env, true, true); @@ -2167,7 +2177,7 @@ pub(crate) mod tests { assert!(match_slot(&extracted, &program2, 11, 19)); // Testing fork 0 - 5 - 11 - 25 - 27 with current slot at 27 - let mut missing = get_entries_to_load(&cache, 27, &[program1, program2, program3]); + let mut missing = get_entries_to_load(&cache, 27, keys); let mut extracted = ProgramCacheForTxBatch::new(27); cache.extract(&mut missing, &mut extracted, &env, true, true); assert!(match_slot(&extracted, &program1, 0, 27)); @@ -2175,7 +2185,7 @@ pub(crate) mod tests { assert!(match_missing(&missing, &program3, true)); // Testing fork 0 - 10 - 20 - 22 with current slot at 22 - let mut missing = get_entries_to_load(&cache, 22, &[program1, program2, program3]); + let mut missing = get_entries_to_load(&cache, 22, keys); assert!(match_missing(&missing, &program2, false)); let mut extracted = ProgramCacheForTxBatch::new(22); cache.extract(&mut missing, &mut extracted, &env, true, true); @@ -2218,13 +2228,14 @@ pub(crate) mod tests { cache.assign_program(&env, program1, 20, new_test_entry(20, 21)); // Testing fork 0 - 10 - 20 - 22 with current slot at 22 - let mut missing = get_entries_to_load(&cache, 22, &[program1]); + let keys = &[program1]; + let mut missing = get_entries_to_load(&cache, 22, keys); let mut extracted = ProgramCacheForTxBatch::new(22); cache.extract(&mut missing, &mut extracted, &env, true, true); assert!(match_slot(&extracted, &program1, 20, 22)); // Looking for a different environment - let mut missing = get_entries_to_load(&cache, 22, &[program1]); + let mut missing = get_entries_to_load(&cache, 22, keys); let mut extracted = ProgramCacheForTxBatch::new(22); cache.extract(&mut missing, &mut extracted, &other_env, true, true); assert!(match_missing(&missing, &program1, true)); @@ -2239,7 +2250,11 @@ pub(crate) mod tests { cache.set_fork_graph(Arc::downgrade(&fork_graph)); let program1 = Pubkey::new_unique(); - let mut missing = vec![(program1, ProgramCacheMatchCriteria::NoCriteria, 0)]; + let mut missing = vec![ProgramToLoad { + program_id: &program1, + match_criteria: ProgramCacheMatchCriteria::NoCriteria, + last_modification_slot: 0, + }]; let mut extracted = ProgramCacheForTxBatch::new(0); cache.extract(&mut missing, &mut extracted, &env, true, true); assert!(match_missing(&missing, &program1, true)); @@ -2319,7 +2334,8 @@ pub(crate) mod tests { cache.prune(10, None, &fork_graph.read().unwrap()); - let mut missing = get_entries_to_load(&cache, 20, &[program1]); + let keys = &[program1]; + let mut missing = get_entries_to_load(&cache, 20, keys); let mut extracted = ProgramCacheForTxBatch::new(20); cache.extract(&mut missing, &mut extracted, &env, true, true); @@ -2361,13 +2377,14 @@ pub(crate) mod tests { let program2 = Pubkey::new_unique(); cache.assign_program(&env, program2, 10, new_test_entry(10, 11)); - let mut missing = get_entries_to_load(&cache, 20, &[program1, program2]); + let keys = &[program1, program2]; + let mut missing = get_entries_to_load(&cache, 20, keys); let mut extracted = ProgramCacheForTxBatch::new(20); cache.extract(&mut missing, &mut extracted, &env, true, true); assert!(match_slot(&extracted, &program1, 0, 20)); assert!(match_slot(&extracted, &program2, 10, 20)); - let mut missing = get_entries_to_load(&cache, 6, &[program1, program2]); + let mut missing = get_entries_to_load(&cache, 6, keys); assert!(match_missing(&missing, &program2, false)); let mut extracted = ProgramCacheForTxBatch::new(6); cache.extract(&mut missing, &mut extracted, &env, true, true); @@ -2377,13 +2394,13 @@ pub(crate) mod tests { // On fork chaining from slot 5, the entry deployed at slot 0 will become visible. cache.prune_by_deployment_slot(5); - let mut missing = get_entries_to_load(&cache, 20, &[program1, program2]); + let mut missing = get_entries_to_load(&cache, 20, keys); let mut extracted = ProgramCacheForTxBatch::new(20); cache.extract(&mut missing, &mut extracted, &env, true, true); assert!(match_slot(&extracted, &program1, 0, 20)); assert!(match_slot(&extracted, &program2, 10, 20)); - let mut missing = get_entries_to_load(&cache, 6, &[program1, program2]); + let mut missing = get_entries_to_load(&cache, 6, keys); assert!(match_missing(&missing, &program2, false)); let mut extracted = ProgramCacheForTxBatch::new(6); cache.extract(&mut missing, &mut extracted, &env, true, true); @@ -2393,7 +2410,7 @@ pub(crate) mod tests { // As there is no other entry for program2, extract() will return it as missing. cache.prune_by_deployment_slot(10); - let mut missing = get_entries_to_load(&cache, 20, &[program1, program2]); + let mut missing = get_entries_to_load(&cache, 20, keys); assert!(match_missing(&missing, &program2, false)); let mut extracted = ProgramCacheForTxBatch::new(20); cache.extract(&mut missing, &mut extracted, &env, true, true); diff --git a/svm/src/program_loader.rs b/svm/src/program_loader.rs index edbba40abb2..def37cd9126 100644 --- a/svm/src/program_loader.rs +++ b/svm/src/program_loader.rs @@ -1,13 +1,17 @@ #[cfg(feature = "metrics")] use solana_program_runtime::program_metrics::LoadProgramMetrics; use { + crate::account_loader::PROGRAM_OWNERS, solana_account::{AccountSharedData, ReadableAccount, state_traits::StateMut}, solana_clock::Slot, solana_instruction::error::InstructionError, solana_loader_v3_interface::state::UpgradeableLoaderState, solana_loader_v4_interface::state::{LoaderV4State, LoaderV4Status}, solana_program_runtime::{ - loaded_programs::ProgramRuntimeEnvironment, + loaded_programs::{ + ProgramCacheForTxBatch, ProgramCacheMatchCriteria, ProgramRuntimeEnvironment, + ProgramToLoad, + }, program_cache_entry::{ DELAY_VISIBILITY_SLOT_OFFSET, ProgramCacheEntry, ProgramCacheEntryOwner, ProgramCacheEntryType, @@ -17,8 +21,10 @@ use { solana_sdk_ids::{bpf_loader, bpf_loader_deprecated, bpf_loader_upgradeable, loader_v4}, solana_svm_callback::TransactionProcessingCallback, solana_svm_timings::ExecuteTimings, + solana_svm_transaction::svm_message::SVMMessage, solana_svm_type_overrides::sync::Arc, solana_transaction_error::{TransactionError, TransactionResult}, + std::sync::atomic::Ordering, }; #[derive(Debug)] @@ -208,11 +214,8 @@ pub fn load_program_with_pubkey( /// Returns an error if the program's account state can not be found or parsed. pub(crate) fn get_program_deployment_slot( callbacks: &CB, - pubkey: &Pubkey, + program: &AccountSharedData, ) -> TransactionResult { - let (program, _slot) = callbacks - .get_account_shared_data(pubkey) - .ok_or(TransactionError::ProgramAccountNotFound)?; if bpf_loader_upgradeable::check_id(program.owner()) { if let Ok(UpgradeableLoaderState::Program { programdata_address, @@ -239,6 +242,40 @@ pub(crate) fn get_program_deployment_slot( } } +/// Appends to a set of executable program accounts (all accounts owned by any loader) +/// for transactions with a valid blockhash or nonce. +pub fn filter_executable_program_accounts<'a, CB: TransactionProcessingCallback>( + callbacks: &CB, + program_cache_for_tx_batch: &ProgramCacheForTxBatch, + tx: &'a impl SVMMessage, + check_program_deployment_slot: bool, +) -> Vec> { + let mut result = Vec::new(); + for account_key in tx.account_keys().iter() { + if let Some(cache_entry) = program_cache_for_tx_batch.find(account_key) { + cache_entry.stats.uses.fetch_add(1, Ordering::Relaxed); + } else if let Some((account, last_modification_slot)) = + callbacks.get_account_shared_data(account_key) + && PROGRAM_OWNERS.contains(account.owner()) + { + let match_criteria = if check_program_deployment_slot { + get_program_deployment_slot(callbacks, &account) + .map_or(ProgramCacheMatchCriteria::Tombstone, |slot| { + ProgramCacheMatchCriteria::DeployedOnOrAfterSlot(slot) + }) + } else { + ProgramCacheMatchCriteria::NoCriteria + }; + result.push(ProgramToLoad { + program_id: account_key, + match_criteria, + last_modification_slot, + }); + } + } + result +} + // Plucked from the now-removed Loader V4 program library. fn loader_v4_get_state(data: &[u8]) -> Result<&LoaderV4State, InstructionError> { unsafe { @@ -260,6 +297,9 @@ mod tests { super::*, crate::transaction_processor::TransactionBatchProcessor, solana_account::WritableAccount, + solana_hash::Hash, + solana_keypair::Keypair, + solana_message::compiled_instruction::CompiledInstruction, solana_program_runtime::{ loaded_programs::{ BlockRelation, ForkGraph, ProgramRuntimeEnvironment, @@ -267,8 +307,10 @@ mod tests { }, solana_sbpf::program::BuiltinProgram, }, - solana_sdk_ids::{bpf_loader, bpf_loader_upgradeable}, + solana_sdk_ids::{bpf_loader, bpf_loader_upgradeable, native_loader}, solana_svm_callback::InvokeContextCallback, + solana_svm_type_overrides::sync::atomic::AtomicU64, + solana_transaction::{Transaction, sanitized::SanitizedTransaction}, std::{ cell::RefCell, collections::HashMap, @@ -686,19 +728,18 @@ mod tests { #[test] fn test_program_modification_slot_account_not_found() { let mock_bank = MockBankCallback::default(); - let key = Pubkey::new_unique(); - let result = get_program_deployment_slot(&mock_bank, &key); - assert_eq!(result.err(), Some(TransactionError::ProgramAccountNotFound)); - let mut account_data = AccountSharedData::new(100, 100, &bpf_loader_upgradeable::id()); mock_bank .account_shared_data .borrow_mut() .insert(key, (account_data.clone(), 0)); - let result = get_program_deployment_slot(&mock_bank, &key); + let result = get_program_deployment_slot( + &mock_bank, + &mock_bank.get_account_shared_data(&key).unwrap().0, + ); assert_eq!(result.err(), Some(TransactionError::ProgramAccountNotFound)); let state = UpgradeableLoaderState::Program { @@ -710,7 +751,10 @@ mod tests { .borrow_mut() .insert(key, (account_data.clone(), 0)); - let result = get_program_deployment_slot(&mock_bank, &key); + let result = get_program_deployment_slot( + &mock_bank, + &mock_bank.get_account_shared_data(&key).unwrap().0, + ); assert_eq!(result.err(), Some(TransactionError::ProgramAccountNotFound)); } @@ -748,7 +792,10 @@ mod tests { .borrow_mut() .insert(key2, (account_data.clone(), 0)); - let result = get_program_deployment_slot(&mock_bank, &key1); + let result = get_program_deployment_slot( + &mock_bank, + &mock_bank.get_account_shared_data(&key1).unwrap().0, + ); assert_eq!(result.unwrap(), 77); account_data.set_owner(Pubkey::new_unique()); @@ -757,7 +804,124 @@ mod tests { .borrow_mut() .insert(key2, (account_data, 0)); - let result = get_program_deployment_slot(&mock_bank, &key2); + let result = get_program_deployment_slot( + &mock_bank, + &mock_bank.get_account_shared_data(&key2).unwrap().0, + ); assert_eq!(result.unwrap(), 0); } + + #[test] + fn test_filter_executable_program_accounts() { + let feepayer = Keypair::new(); + let loader_ids = [ + bpf_loader_deprecated::id(), + bpf_loader::id(), + bpf_loader_upgradeable::id(), + native_loader::id(), + ]; + let program_ids = [ + Pubkey::new_unique(), + Pubkey::new_unique(), + Pubkey::new_unique(), + Pubkey::new_unique(), + ]; + let account_ids = [ + Pubkey::new_unique(), + Pubkey::new_unique(), + Pubkey::new_unique(), + Pubkey::new_unique(), + ]; + + let mut loaded_programs_for_tx_batch = ProgramCacheForTxBatch::default(); + let mock_bank = MockBankCallback::default(); + for i in 0..3 { + loaded_programs_for_tx_batch.replenish( + loader_ids[i], + Arc::new(ProgramCacheEntry { + program: ProgramCacheEntryType::Builtin(BuiltinProgram::new_mock()), + account_owner: ProgramCacheEntryOwner::NativeLoader, + account_size: 0, + deployment_slot: 0, + effective_slot: 0, + stats: Arc::default(), + latest_access_slot: AtomicU64::default(), + }), + ); + mock_bank.account_shared_data.borrow_mut().insert( + loader_ids[i], + (AccountSharedData::new(1, 1, &program_ids[3]), 0), + ); + mock_bank.account_shared_data.borrow_mut().insert( + program_ids[i], + (AccountSharedData::new(1, 1, &loader_ids[i]), 0), + ); + mock_bank.account_shared_data.borrow_mut().insert( + account_ids[i], + (AccountSharedData::new(1, 1, &program_ids[i]), 0), + ); + } + + let tx = Transaction::new_with_compiled_instructions( + &[&feepayer], + &[program_ids[1], program_ids[2], loader_ids[2]], + Hash::new_unique(), + vec![ + account_ids[0], + account_ids[1], + account_ids[2], + account_ids[3], + ], + vec![ + CompiledInstruction::new(1, &(), vec![0, 1, 2, 3]), + CompiledInstruction::new(2, &(), vec![0, 1, 2, 3]), + CompiledInstruction::new(3, &(), vec![0, 1, 2, 3]), + ], + ); + let sanitized_tx = SanitizedTransaction::from_transaction_for_tests(tx); + + let missing_programs = filter_executable_program_accounts( + &mock_bank, + &loaded_programs_for_tx_batch, + &sanitized_tx, + false, + ); + assert_eq!( + missing_programs, + &[ + ProgramToLoad { + program_id: &program_ids[1], + match_criteria: ProgramCacheMatchCriteria::NoCriteria, + last_modification_slot: 0, + }, + ProgramToLoad { + program_id: &program_ids[2], + match_criteria: ProgramCacheMatchCriteria::NoCriteria, + last_modification_slot: 0, + }, + ] + ); + + let missing_programs = filter_executable_program_accounts( + &mock_bank, + &loaded_programs_for_tx_batch, + &sanitized_tx, + true, + ); + assert_eq!( + missing_programs, + &[ + ProgramToLoad { + program_id: &program_ids[1], + match_criteria: ProgramCacheMatchCriteria::DeployedOnOrAfterSlot(0), + last_modification_slot: 0, + }, + ProgramToLoad { + program_id: &program_ids[2], + match_criteria: ProgramCacheMatchCriteria::Tombstone, + last_modification_slot: 0, + }, + ] + ); + } } diff --git a/svm/src/transaction_processor.rs b/svm/src/transaction_processor.rs index c803fe86015..6dff7307d8d 100644 --- a/svm/src/transaction_processor.rs +++ b/svm/src/transaction_processor.rs @@ -1,14 +1,14 @@ use { crate::{ account_loader::{ - AccountLoader, CheckedTransactionDetails, LoadedTransaction, PROGRAM_OWNERS, - TransactionCheckResult, TransactionLoadResult, ValidatedTransactionDetails, - load_transaction, update_rent_exempt_status_for_account, validate_fee_payer, + AccountLoader, CheckedTransactionDetails, LoadedTransaction, TransactionCheckResult, + TransactionLoadResult, ValidatedTransactionDetails, load_transaction, + update_rent_exempt_status_for_account, validate_fee_payer, }, account_overrides::AccountOverrides, message_processor::process_message, nonce_info::NonceInfo, - program_loader::{get_program_deployment_slot, load_program_with_pubkey}, + program_loader::{filter_executable_program_accounts, load_program_with_pubkey}, rollback_accounts::RollbackAccounts, transaction_account_state_info::{ TransactionAccountStateInfo, get_uninitialized_accounts_size, verify_changes, @@ -44,6 +44,7 @@ use { loaded_programs::{ EpochBoundaryPreparation, ForkGraph, ProgramCache, ProgramCacheForTxBatch, ProgramCacheMatchCriteria, ProgramRuntimeEnvironment, ProgramRuntimeEnvironments, + ProgramToLoad, }, program_cache_entry::ProgramCacheEntry, solana_sbpf::{program::BuiltinProgram, vm::Config as VmConfig}, @@ -57,11 +58,11 @@ use { solana_svm_measure::{measure::Measure, measure_us}, solana_svm_timings::{ExecuteTimingType, ExecuteTimings}, solana_svm_transaction::{svm_message::SVMMessage, svm_transaction::SVMTransaction}, - solana_svm_type_overrides::sync::{Arc, RwLock, RwLockReadGuard, atomic::Ordering}, + solana_svm_type_overrides::sync::{Arc, RwLock, RwLockReadGuard}, solana_transaction_context::transaction::{ExecutionRecord, TransactionContext}, solana_transaction_error::{TransactionError, TransactionResult}, std::{ - collections::{HashMap, HashSet}, + collections::HashSet, fmt::{Debug, Formatter}, rc::Rc, }, @@ -316,9 +317,13 @@ impl TransactionBatchProcessor { // Pre-populate the builtin program cache from the global cache. // This is done once per block rather than once per batch. let mut builtin_program_cache = ProgramCacheForTxBatch::new(slot); - let mut search_for: Vec<(Pubkey, ProgramCacheMatchCriteria, Slot)> = builtin_program_ids + let mut search_for: Vec = builtin_program_ids .iter() - .map(|key| (*key, ProgramCacheMatchCriteria::NoCriteria, 0)) + .map(|program_id| ProgramToLoad { + program_id, + match_criteria: ProgramCacheMatchCriteria::NoCriteria, + last_modification_slot: 0, + }) .collect(); self.global_program_cache.read().unwrap().extract( &mut search_for, @@ -497,11 +502,12 @@ impl TransactionBatchProcessor { } }, TransactionLoadResult::Loaded(loaded_transaction) => { - let (program_accounts_set, filter_executable_us) = - measure_us!(self.filter_executable_program_accounts( + let (missing_programs, filter_executable_us) = + measure_us!(filter_executable_program_accounts( &account_loader, - &mut program_cache_for_tx_batch, + &program_cache_for_tx_batch, tx, + config.check_program_deployment_slot, )); execute_timings.saturating_add_in_place( ExecuteTimingType::FilterExecutableUs, @@ -511,13 +517,12 @@ impl TransactionBatchProcessor { let ((), program_cache_us) = measure_us!({ self.replenish_program_cache( &account_loader, - &program_accounts_set, + missing_programs, environment .program_runtime_environments .get_env_for_execution(), &mut program_cache_for_tx_batch, &mut execute_timings, - config.check_program_deployment_slot, config.limit_to_load_programs, true, // increment_usage_counter ); @@ -812,56 +817,17 @@ impl TransactionBatchProcessor { } } - /// Appends to a set of executable program accounts (all accounts owned by any loader) - /// for transactions with a valid blockhash or nonce. - fn filter_executable_program_accounts( - &self, - account_loader: &AccountLoader, - program_cache_for_tx_batch: &mut ProgramCacheForTxBatch, - tx: &impl SVMMessage, - ) -> HashMap { - let mut program_accounts_set = HashMap::default(); - for account_key in tx.account_keys().iter() { - if let Some(cache_entry) = program_cache_for_tx_batch.find(account_key) { - cache_entry.stats.uses.fetch_add(1, Ordering::Relaxed); - } else if let Some((account, last_modification_slot)) = - account_loader.get_account_shared_data(account_key) - && PROGRAM_OWNERS.contains(account.owner()) - { - program_accounts_set.insert(*account_key, last_modification_slot); - } - } - program_accounts_set - } - #[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))] fn replenish_program_cache( &self, account_loader: &AccountLoader, - program_accounts_set: &HashMap, + mut missing_programs: Vec, program_runtime_environment_for_execution: &ProgramRuntimeEnvironment, program_cache_for_tx_batch: &mut ProgramCacheForTxBatch, execute_timings: &mut ExecuteTimings, - check_program_deployment_slot: bool, limit_to_load_programs: bool, increment_usage_counter: bool, ) { - let mut missing_programs: Vec<(Pubkey, ProgramCacheMatchCriteria, Slot)> = - program_accounts_set - .iter() - .map(|(pubkey, last_modification_slot)| { - let match_criteria = if check_program_deployment_slot { - get_program_deployment_slot(account_loader, pubkey) - .map_or(ProgramCacheMatchCriteria::Tombstone, |slot| { - ProgramCacheMatchCriteria::DeployedOnOrAfterSlot(slot) - }) - } else { - ProgramCacheMatchCriteria::NoCriteria - }; - (*pubkey, match_criteria, *last_modification_slot) - }) - .collect(); - let mut count_hits_and_misses = true; loop { // Lock the global cache. @@ -1279,7 +1245,6 @@ mod tests { solana_fee_calculator::FeeCalculator, solana_fee_structure::FeeDetails, solana_hash::Hash, - solana_keypair::Keypair, solana_message::{LegacyMessage, Message, MessageHeader, SanitizedMessage}, solana_nonce as nonce, solana_program_runtime::{ @@ -1292,11 +1257,11 @@ mod tests { }, solana_rent::Rent, solana_sbpf::vm, - solana_sdk_ids::{bpf_loader, bpf_loader_upgradeable, system_program, sysvar}, + solana_sdk_ids::{bpf_loader, system_program, sysvar}, solana_signature::Signature, solana_svm_callback::{AccountState, InvokeContextCallback}, solana_system_interface::instruction as system_instruction, - solana_transaction::{Transaction, sanitized::SanitizedTransaction}, + solana_transaction::sanitized::SanitizedTransaction, solana_transaction_context::transaction::TransactionContext, solana_transaction_error::TransactionError, std::{borrow::Cow, collections::HashMap}, @@ -1732,18 +1697,18 @@ mod tests { batch_processor.program_runtime_environment_for_epoch(0); let key = Pubkey::new_unique(); - let mut account_set = HashMap::new(); - account_set.insert(key, 0); - let mut program_cache_for_tx_batch = ProgramCacheForTxBatch::new(batch_processor.slot); batch_processor.replenish_program_cache( &account_loader, - &account_set, + vec![ProgramToLoad { + program_id: &key, + match_criteria: ProgramCacheMatchCriteria::NoCriteria, + last_modification_slot: 0, + }], &program_runtime_environment_for_execution, &mut program_cache_for_tx_batch, &mut ExecuteTimings::default(), - false, true, true, ); @@ -1768,20 +1733,20 @@ mod tests { .insert(key, account_data); let account_loader = (&mock_bank).into(); - let mut account_set = HashMap::new(); - account_set.insert(key, 0); let mut loaded_missing = 0; - for limit_to_load_programs in [false, true] { let mut program_cache_for_tx_batch = ProgramCacheForTxBatch::new(batch_processor.slot); batch_processor.replenish_program_cache( &account_loader, - &account_set, + vec![ProgramToLoad { + program_id: &key, + match_criteria: ProgramCacheMatchCriteria::NoCriteria, + last_modification_slot: 0, + }], &program_runtime_environment_for_execution, &mut program_cache_for_tx_batch, &mut ExecuteTimings::default(), - false, limit_to_load_programs, true, ); @@ -1799,164 +1764,6 @@ mod tests { assert!(loaded_missing > 0); } - #[test] - fn test_filter_executable_program_accounts() { - let mock_bank = MockBankCallback::default(); - let key1 = Pubkey::new_unique(); - let owner1 = bpf_loader::id(); - let key2 = Pubkey::new_unique(); - let owner2 = bpf_loader_upgradeable::id(); - - let mut data1 = AccountSharedData::default(); - data1.set_owner(owner1); - data1.set_lamports(93); - mock_bank - .account_shared_data - .write() - .unwrap() - .insert(key1, data1); - - let mut data2 = AccountSharedData::default(); - data2.set_owner(owner2); - data2.set_lamports(90); - mock_bank - .account_shared_data - .write() - .unwrap() - .insert(key2, data2); - let account_loader = (&mock_bank).into(); - - let message = Message { - account_keys: vec![key1, key2], - header: MessageHeader::default(), - instructions: vec![CompiledInstruction { - program_id_index: 0, - accounts: vec![], - data: vec![], - }], - recent_blockhash: Hash::default(), - }; - - let sanitized_message = new_unchecked_sanitized_message(message); - - let sanitized_transaction = SanitizedTransaction::new_for_tests( - sanitized_message, - vec![Signature::new_unique()], - false, - ); - - let batch_processor = TransactionBatchProcessor::::default(); - let program_accounts_set = batch_processor.filter_executable_program_accounts( - &account_loader, - &mut ProgramCacheForTxBatch::default(), - &sanitized_transaction, - ); - - assert_eq!(program_accounts_set.len(), 2); - assert!(program_accounts_set.contains_key(&key1)); - assert!(program_accounts_set.contains_key(&key2)); - } - - #[test] - fn test_filter_executable_program_accounts_no_errors() { - let keypair1 = Keypair::new(); - let keypair2 = Keypair::new(); - - let non_program_pubkey1 = Pubkey::new_unique(); - let non_program_pubkey2 = Pubkey::new_unique(); - let program1_pubkey = bpf_loader::id(); - let program2_pubkey = bpf_loader_upgradeable::id(); - let account1_pubkey = Pubkey::new_unique(); - let account2_pubkey = Pubkey::new_unique(); - let account3_pubkey = Pubkey::new_unique(); - let account4_pubkey = Pubkey::new_unique(); - - let account5_pubkey = Pubkey::new_unique(); - - let bank = MockBankCallback::default(); - bank.account_shared_data.write().unwrap().insert( - non_program_pubkey1, - AccountSharedData::new(1, 10, &account5_pubkey), - ); - bank.account_shared_data.write().unwrap().insert( - non_program_pubkey2, - AccountSharedData::new(1, 10, &account5_pubkey), - ); - bank.account_shared_data.write().unwrap().insert( - program1_pubkey, - AccountSharedData::new(40, 1, &account5_pubkey), - ); - bank.account_shared_data.write().unwrap().insert( - program2_pubkey, - AccountSharedData::new(40, 1, &account5_pubkey), - ); - bank.account_shared_data.write().unwrap().insert( - account1_pubkey, - AccountSharedData::new(1, 10, &non_program_pubkey1), - ); - bank.account_shared_data.write().unwrap().insert( - account2_pubkey, - AccountSharedData::new(1, 10, &non_program_pubkey2), - ); - bank.account_shared_data.write().unwrap().insert( - account3_pubkey, - AccountSharedData::new(40, 1, &program1_pubkey), - ); - bank.account_shared_data.write().unwrap().insert( - account4_pubkey, - AccountSharedData::new(40, 1, &program2_pubkey), - ); - let account_loader = (&bank).into(); - - let tx1 = Transaction::new_with_compiled_instructions( - &[&keypair1], - &[non_program_pubkey1], - Hash::new_unique(), - vec![account1_pubkey, account2_pubkey, account3_pubkey], - vec![CompiledInstruction::new(1, &(), vec![0])], - ); - let sanitized_tx1 = SanitizedTransaction::from_transaction_for_tests(tx1); - - let tx2 = Transaction::new_with_compiled_instructions( - &[&keypair2], - &[non_program_pubkey2], - Hash::new_unique(), - vec![account4_pubkey, account3_pubkey, account2_pubkey], - vec![CompiledInstruction::new(1, &(), vec![0])], - ); - let sanitized_tx2 = SanitizedTransaction::from_transaction_for_tests(tx2); - - let batch_processor = TransactionBatchProcessor::::default(); - - let tx1_programs = batch_processor.filter_executable_program_accounts( - &account_loader, - &mut ProgramCacheForTxBatch::default(), - &sanitized_tx1, - ); - - assert_eq!(tx1_programs.len(), 1); - assert!( - tx1_programs.contains_key(&account3_pubkey), - "failed to find the program account", - ); - - let tx2_programs = batch_processor.filter_executable_program_accounts( - &account_loader, - &mut ProgramCacheForTxBatch::default(), - &sanitized_tx2, - ); - - assert_eq!(tx2_programs.len(), 2); - assert!( - tx2_programs.contains_key(&account3_pubkey), - "failed to find the program account", - ); - assert!( - tx2_programs.contains_key(&account4_pubkey), - "failed to find the program account", - ); - } - #[test] #[allow(deprecated)] fn test_sysvar_cache_initialization1() { @@ -2154,7 +1961,11 @@ mod tests { .write() .unwrap() .extract( - &mut vec![(key, ProgramCacheMatchCriteria::NoCriteria, 0)], + &mut vec![ProgramToLoad { + program_id: &key, + match_criteria: ProgramCacheMatchCriteria::NoCriteria, + last_modification_slot: 0, + }], &mut loaded_programs_for_tx_batch, &program_runtime_environment, true, diff --git a/svm/tests/concurrent_tests.rs b/svm/tests/concurrent_tests.rs index ac4b1c481ee..f71cb223ebc 100644 --- a/svm/tests/concurrent_tests.rs +++ b/svm/tests/concurrent_tests.rs @@ -10,11 +10,13 @@ use { thread, }, solana_account::{AccountSharedData, ReadableAccount, WritableAccount}, - solana_clock::Slot, solana_instruction::{AccountMeta, Instruction}, solana_program_runtime::{ execution_budget::SVMTransactionExecutionAndFeeBudgetLimits, - loaded_programs::{ProgramCacheForTxBatch, ProgramRuntimeEnvironments}, + loaded_programs::{ + ProgramCacheForTxBatch, ProgramCacheMatchCriteria, ProgramRuntimeEnvironments, + ProgramToLoad, + }, program_cache_entry::ProgramCacheEntryType, }, solana_pubkey::Pubkey, @@ -31,7 +33,7 @@ use { solana_svm_feature_set::SVMFeatureSet, solana_svm_timings::ExecuteTimings, solana_transaction::{Transaction, sanitized::SanitizedTransaction}, - std::collections::{HashMap, HashSet}, + std::collections::HashSet, }; mod mock_bank; @@ -42,15 +44,12 @@ fn program_cache_execution(threads: usize) { let mut mock_bank = MockBankCallback::default(); let fork_graph = Arc::new(RwLock::new(MockForkGraph {})); let batch_processor = TransactionBatchProcessor::new(5, 5, Arc::downgrade(&fork_graph), None); - - let programs = vec![ + let programs = [ deploy_program("hello-solana".to_string(), 0, &mut mock_bank), deploy_program("simple-transfer".to_string(), 0, &mut mock_bank), deploy_program("clock-sysvar".to_string(), 0, &mut mock_bank), ]; - let account_maps: HashMap = programs.iter().map(|key| (*key, 0)).collect(); - let ths: Vec<_> = (0..threads) .map(|_| { let local_bank = mock_bank.clone(); @@ -59,9 +58,15 @@ fn program_cache_execution(threads: usize) { batch_processor.slot, batch_processor.epoch, ); - let maps = account_maps.clone(); - let programs = programs.clone(); thread::spawn(move || { + let missing_programs: Vec = programs + .iter() + .map(|program_id| ProgramToLoad { + program_id, + match_criteria: ProgramCacheMatchCriteria::NoCriteria, + last_modification_slot: 0, + }) + .collect(); let feature_set = SVMFeatureSet::all_enabled(); let account_loader = AccountLoader::new_with_loaded_accounts_capacity( None, @@ -74,15 +79,14 @@ fn program_cache_execution(threads: usize) { processor.program_runtime_environment_for_epoch(processor.epoch); processor.replenish_program_cache( &account_loader, - &maps, + missing_programs, &program_runtime_environment_for_execution, &mut result, &mut ExecuteTimings::default(), - false, true, true, ); - for key in &programs { + for key in programs.iter() { let cache_entry = result.find(key); assert!(matches!( cache_entry.unwrap().program, From 5a48f30922a8f2d455bcb5062a23b4eaeeda7c80 Mon Sep 17 00:00:00 2001 From: Brennan Date: Wed, 20 May 2026 13:49:44 -0700 Subject: [PATCH 012/576] Broadcast Slot Blacklist (#12584) broadcast blacklist --- .../broadcast_stage/standard_broadcast_run.rs | 141 +++++++++++++++++- 1 file changed, 140 insertions(+), 1 deletion(-) diff --git a/turbine/src/broadcast_stage/standard_broadcast_run.rs b/turbine/src/broadcast_stage/standard_broadcast_run.rs index 5df7a2289ac..f40996a4855 100644 --- a/turbine/src/broadcast_stage/standard_broadcast_run.rs +++ b/turbine/src/broadcast_stage/standard_broadcast_run.rs @@ -24,9 +24,14 @@ use { solana_runtime::bank::Bank, solana_sha256_hasher::hashv, solana_time_utils::AtomicInterval, - std::{borrow::Cow, sync::RwLock}, + std::{borrow::Cow, collections::VecDeque, sync::RwLock}, }; +// Expect blacklist events to be extremely rare, so we can tightly bound the +// data structure. Need to be able to hold a full leader span, and we'll go +// ahead and overprovision a bit just in case. +const MAX_BROADCAST_BLACKLIST_SIZE: usize = 16; + #[derive(Clone)] pub struct StandardBroadcastRun { slot: Slot, @@ -59,6 +64,7 @@ pub struct StandardBroadcastRun { votor_event_sender: VotorEventSender, max_data_shreds_per_slot: u32, max_code_shreds_per_slot: u32, + broadcast_blacklist: VecDeque, } #[derive(Debug)] @@ -102,6 +108,22 @@ impl StandardBroadcastRun { votor_event_sender, max_data_shreds_per_slot: DEFAULT_MAX_DATA_SHREDS_PER_SLOT, max_code_shreds_per_slot: DEFAULT_MAX_CODE_SHREDS_PER_SLOT, + broadcast_blacklist: VecDeque::new(), + } + } + + fn is_broadcast_blacklisted(&self, slot: Slot) -> bool { + self.broadcast_blacklist.contains(&slot) + } + + fn blacklist_broadcast_slot(&mut self, slot: Slot) { + if self.is_broadcast_blacklisted(slot) { + return; + } + + self.broadcast_blacklist.push_back(slot); + while self.broadcast_blacklist.len() > MAX_BROADCAST_BLACKLIST_SIZE { + self.broadcast_blacklist.pop_front(); } } @@ -314,6 +336,15 @@ impl StandardBroadcastRun { last_tick_height, } = receive_results; + if self.is_broadcast_blacklisted(bank.slot()) { + return Ok(()); + } + + if self.is_broadcast_blacklisted(bank.parent_slot()) { + self.blacklist_broadcast_slot(bank.slot()); + return Err(Error::DuplicateSlotBroadcast(bank.slot())); + } + let mut to_shreds_time = Measure::start("broadcast_to_shreds"); self.max_data_shreds_per_slot = bank.max_data_shreds_per_slot(); self.max_code_shreds_per_slot = bank.max_code_shreds_per_slot(); @@ -346,6 +377,7 @@ impl StandardBroadcastRun { process_stats.num_extant_slots += 1; // This is a faulty situation that should not happen. // Refrain from generating shreds for the slot. + self.blacklist_broadcast_slot(bank.slot()); return Err(Error::DuplicateSlotBroadcast(bank.slot())); } @@ -638,6 +670,7 @@ mod test { solana_keypair::Keypair, solana_ledger::{ blockstore::Blockstore, + blockstore_meta::SlotMeta, genesis_utils::create_genesis_config, get_tmp_ledger_path, shred::{DATA_SHREDS_PER_FEC_BLOCK, max_ticks_per_n_shreds}, @@ -1017,6 +1050,112 @@ mod test { assert!(standard_broadcast_run.completed) } + #[test] + fn test_broadcast_blacklist() { + let num_shreds_per_slot = 2; + let ( + blockstore, + genesis_config, + _cluster_info, + parent_bank, + leader_keypair, + _socket, + _bank_forks, + ) = setup(num_shreds_per_slot); + let bank1 = new_child_bank(&parent_bank, 1); + + blockstore + .put_meta( + bank1.slot(), + &SlotMeta { + slot: bank1.slot(), + parent_slot: Some(bank1.parent_slot()), + received: 1, + ..SlotMeta::default() + }, + ) + .unwrap(); + + let (votor_event_sender, _votor_event_receiver) = unbounded(); + let mut standard_broadcast_run = StandardBroadcastRun::new( + 0, + Arc::new(MigrationStatus::default()), + votor_event_sender, + test_leader_schedule_cache(&bank1), + ); + let (bsend, brecv) = unbounded(); + let (ssend, srecv) = unbounded(); + + let ticks = create_ticks(1, 0, genesis_config.hash()); + let err = standard_broadcast_run + .process_receive_results( + &leader_keypair, + &blockstore, + &ssend, + &bsend, + ReceiveResults { + component: BlockComponent::EntryBatch(ticks.clone()), + bank: bank1.clone(), + last_tick_height: bank1.tick_height() + ticks.len() as u64, + }, + &mut ProcessShredsStats::default(), + ) + .unwrap_err(); + assert_matches!(err, Error::DuplicateSlotBroadcast(1)); + assert!(standard_broadcast_run.is_broadcast_blacklisted(1)); + + standard_broadcast_run + .process_receive_results( + &leader_keypair, + &blockstore, + &ssend, + &bsend, + ReceiveResults { + component: BlockComponent::EntryBatch(ticks.clone()), + bank: bank1.clone(), + last_tick_height: bank1.tick_height() + ticks.len() as u64, + }, + &mut ProcessShredsStats::default(), + ) + .unwrap(); + assert!(brecv.try_recv().is_err()); + assert!(srecv.try_recv().is_err()); + + let bank2 = Arc::new(Bank::new_from_parent( + bank1.clone(), + *bank1.leader(), + bank1.slot() + 1, + )); + let err = standard_broadcast_run + .process_receive_results( + &leader_keypair, + &blockstore, + &ssend, + &bsend, + ReceiveResults { + component: BlockComponent::EntryBatch(ticks.clone()), + bank: bank2, + last_tick_height: bank1.tick_height() + ticks.len() as u64, + }, + &mut ProcessShredsStats::default(), + ) + .unwrap_err(); + assert_matches!(err, Error::DuplicateSlotBroadcast(2)); + assert!(standard_broadcast_run.is_broadcast_blacklisted(2)); + + for slot in 3..=18 { + standard_broadcast_run.blacklist_broadcast_slot(slot); + } + assert_eq!( + standard_broadcast_run.broadcast_blacklist.len(), + MAX_BROADCAST_BLACKLIST_SIZE + ); + assert!(!standard_broadcast_run.is_broadcast_blacklisted(1)); + assert!(!standard_broadcast_run.is_broadcast_blacklisted(2)); + assert!(standard_broadcast_run.is_broadcast_blacklisted(3)); + assert!(standard_broadcast_run.is_broadcast_blacklisted(18)); + } + #[test] fn test_update_shred_limits_from_bank() { agave_logger::setup(); From b002d1d3af2c99f03282247be767e34ff7944e17 Mon Sep 17 00:00:00 2001 From: Joe C Date: Thu, 21 May 2026 05:41:09 +0800 Subject: [PATCH 013/576] runtime: gate `RewardCommission.commission_bps` on SIMD-0232 (#12588) * runtime: gate RewardCommission.commission_bps on SIMD-0232 Feature-gates the `commission_bps` field on `RewardCommission` (and the outward-facing `RewardInfo.commission_bps` it populates) on `custom_commission_collector` (SIMD-0232). The field becomes `Option` and is populated as `None` once the feature is active, dropped at the lowest level so the field can be deleted outright on feature cleanup. * runtime: gate PartitionedStakeReward.commission_bps on SIMD-0232 Feature-gates the `commission_bps` field on `PartitionedStakeReward` (and the outward-facing `StakeRewardInfo.commission_bps` it populates) on `custom_commission_collector` (SIMD-0232). The field becomes `Option` and is populated as `None` once the feature is active, dropped at the lowest level so the field can be deleted outright on feature cleanup. --- .../partitioned_epoch_rewards/calculation.rs | 109 +++++++++--------- .../partitioned_epoch_rewards/distribution.rs | 10 +- .../src/bank/partitioned_epoch_rewards/mod.rs | 11 +- 3 files changed, 66 insertions(+), 64 deletions(-) diff --git a/runtime/src/bank/partitioned_epoch_rewards/calculation.rs b/runtime/src/bank/partitioned_epoch_rewards/calculation.rs index edbe252816f..bb5f5456701 100644 --- a/runtime/src/bank/partitioned_epoch_rewards/calculation.rs +++ b/runtime/src/bank/partitioned_epoch_rewards/calculation.rs @@ -431,6 +431,7 @@ impl Bank { delay_commission_updates: bool, commission_rate_in_basis_points: bool, adjust_delegations_for_rent: bool, + custom_commission_collector: bool, ) -> Option { // curry closure to add the contextual stake_pubkey let reward_calc_tracer = reward_calc_tracer.as_ref().map(|outer| { @@ -475,10 +476,10 @@ impl Bank { stake_pubkey, stake, stake_reward: 0, - commission_bps: 0, + commission_bps: (!custom_commission_collector).then_some(0), }; let reward_commission = RewardCommission { - commission_bps: 0, + commission_bps: (!custom_commission_collector).then_some(0), commission_lamports: 0, }; return Some(DelegationRewards { @@ -540,10 +541,10 @@ impl Bank { stake_pubkey, stake, stake_reward, - commission_bps, + commission_bps: (!custom_commission_collector).then_some(commission_bps), }; let reward_commission = RewardCommission { - commission_bps, + commission_bps: (!custom_commission_collector).then_some(commission_bps), commission_lamports, }; Some(DelegationRewards { @@ -579,6 +580,7 @@ impl Bank { // Name intentionally doesn't match -- "adjust delegations for rent" is // part of relaxing post-exec min balance checks. let adjust_delegations_for_rent = feature_snapshot.relax_post_exec_min_balance_check; + let custom_commission_collector = feature_snapshot.custom_commission_collector; let mut measure_redeem_rewards = Measure::start("redeem-rewards"); // For N stake delegations, where N is >1,000,000, we produce: @@ -609,6 +611,7 @@ impl Bank { delay_commission_updates, commission_rate_in_basis_points, adjust_delegations_for_rent, + custom_commission_collector, ); let (stake_reward, maybe_reward_record) = match maybe_reward_record { @@ -847,7 +850,7 @@ impl Bank { reward_type: RewardType::Voting, lamports: *commission_lamports as i64, post_balance: commission_account.lamports(), - commission_bps: Some(*commission_bps), + commission_bps: *commission_bps, }, commission_account, )) @@ -1140,7 +1143,7 @@ mod tests { #[derive(Default)] struct VoteOperations { - expected_reward_commission: Option, + expect_reward: bool, // ops to perform before epoch ends create_with_balance: Option, delegate_stake_amount: Option, @@ -1191,7 +1194,7 @@ mod tests { reward_type: RewardType::Voting, lamports: rc.commission_lamports as i64, post_balance, - commission_bps: Some(rc.commission_bps), + commission_bps: rc.commission_bps, } }) } @@ -1320,7 +1323,6 @@ mod tests { Bank::new_from_parent_with_bank_forks(bank_forks, bank, SlotLeader::new_unique(), slot); for (vote_address, vote_op) in &op.vote_operations { - let expected_commission = vote_op.expected_reward_commission; let recalculated_vote_reward = recalculate_reward_commission_for_tests(&bank, vote_address); let vote_reward = bank @@ -1337,13 +1339,13 @@ mod tests { .lamports(); let vote_balance = bank.get_balance(vote_address); - if let Some(expected_commission) = &expected_commission { + if vote_op.expect_reward { let reward_lamports = vote_balance - prev_vote_balance; let expected_vote_reward = RewardInfo { reward_type: RewardType::Voting, lamports: reward_lamports as i64, post_balance: vote_balance, - commission_bps: Some(*expected_commission as u16 * 100), + commission_bps: None, }; assert_eq!( @@ -1422,27 +1424,23 @@ mod tests { // Check that if a vote account didn't exist two epochs ago (normal for // new vote accounts), that the reward commission falls back to the // commission from the end of the rewarded epoch. - bank = { - let expected_commission = if delay_commission_updates { 1 } else { 2 }; - apply_epoch_operations( - bank, - bank_forks.as_ref(), - EpochOperations { - epoch: 1, - vote_operations: vec![( - vote_address, - VoteOperations { - new_commission: Some(2), - earned_credits: Some(1000), - expected_reward_commission: Some(expected_commission), - ..VoteOperations::default() - }, - )], - }, - ) - }; + bank = apply_epoch_operations( + bank, + bank_forks.as_ref(), + EpochOperations { + epoch: 1, + vote_operations: vec![( + vote_address, + VoteOperations { + new_commission: Some(2), + earned_credits: Some(1000), + expect_reward: true, + ..VoteOperations::default() + }, + )], + }, + ); - let expected_commission = if delay_commission_updates { 1 } else { 3 }; apply_epoch_operations( bank, bank_forks.as_ref(), @@ -1453,7 +1451,7 @@ mod tests { VoteOperations { new_commission: Some(3), earned_credits: Some(1000), - expected_reward_commission: Some(expected_commission), + expect_reward: true, ..VoteOperations::default() }, )], @@ -1506,7 +1504,7 @@ mod tests { create_with_balance: Some(LAMPORTS_PER_SOL), new_commission: Some(1), earned_credits: Some(1000), - expected_reward_commission: Some(1), + expect_reward: true, ..VoteOperations::default() }, )], @@ -1526,7 +1524,7 @@ mod tests { VoteOperations { new_commission: Some(2), earned_credits: Some(1000), - expected_reward_commission: Some(1), + expect_reward: true, ..VoteOperations::default() }, )], @@ -1543,7 +1541,7 @@ mod tests { VoteOperations { new_commission: Some(3), earned_credits: Some(1000), - expected_reward_commission: Some(1), + expect_reward: true, ..VoteOperations::default() }, )], @@ -1661,7 +1659,7 @@ mod tests { VoteOperations { new_commission: Some(1), earned_credits: Some(1000), - expected_reward_commission: Some(0), + expect_reward: true, ..VoteOperations::default() }, )], @@ -1680,7 +1678,7 @@ mod tests { VoteOperations { new_commission: Some(2), earned_credits: Some(1000), - expected_reward_commission: Some(0), + expect_reward: true, ..VoteOperations::default() }, )], @@ -1696,7 +1694,7 @@ mod tests { genesis_vote_address, VoteOperations { earned_credits: Some(1000), - expected_reward_commission: Some(1), + expect_reward: true, ..VoteOperations::default() }, )], @@ -1712,7 +1710,7 @@ mod tests { genesis_vote_address, VoteOperations { earned_credits: Some(1000), - expected_reward_commission: Some(2), + expect_reward: true, ..VoteOperations::default() }, )], @@ -1728,7 +1726,7 @@ mod tests { genesis_vote_address, VoteOperations { earned_credits: Some(1000), - expected_reward_commission: Some(2), + expect_reward: true, ..VoteOperations::default() }, )], @@ -1792,9 +1790,8 @@ mod tests { assert_eq!(vote_rewards_accounts.len(), 1); let reward_commission = vote_rewards_accounts.get(vote_pubkey).unwrap(); let vote_rewards = 0; - let commission_bps = vote_state.inflation_rewards_commission_bps; assert_eq!(reward_commission.commission_lamports, vote_rewards); - assert_eq!(reward_commission.commission_bps, commission_bps); + assert_eq!(reward_commission.commission_bps, None); assert_eq!(stake_reward_calculation.stake_rewards.num_rewards(), 1); let expected_reward = { @@ -1807,7 +1804,7 @@ mod tests { stake, stake_pubkey, stake_reward, - commission_bps, + commission_bps: None, } }; assert_eq!( @@ -2246,7 +2243,7 @@ mod tests { accumulator1.add_reward( commission_pubkey_a, RewardCommission { - commission_bps: 1_000, + commission_bps: Some(1_000), commission_lamports: 50, }, 50, @@ -2254,7 +2251,7 @@ mod tests { accumulator1.add_reward( commission_pubkey_b, RewardCommission { - commission_bps: 1_000, + commission_bps: Some(1_000), commission_lamports: 50, }, 50, @@ -2262,7 +2259,7 @@ mod tests { accumulator2.add_reward( commission_pubkey_b, RewardCommission { - commission_bps: 1_000, + commission_bps: Some(1_000), commission_lamports: 30, }, 30, @@ -2270,7 +2267,7 @@ mod tests { accumulator2.add_reward( commission_pubkey_c, RewardCommission { - commission_bps: 1_000, + commission_bps: Some(1_000), commission_lamports: 50, }, 50, @@ -2282,28 +2279,28 @@ mod tests { .reward_commissions .get(&commission_pubkey_a) .unwrap(); - assert_eq!(reward_commission_a_1.commission_bps, 1_000); + assert_eq!(reward_commission_a_1.commission_bps, Some(1_000)); assert_eq!(reward_commission_a_1.commission_lamports, 50); let reward_commission_b_1 = accumulator1 .reward_commissions .get(&commission_pubkey_b) .unwrap(); - assert_eq!(reward_commission_b_1.commission_bps, 1_000); + assert_eq!(reward_commission_b_1.commission_bps, Some(1_000)); assert_eq!(reward_commission_b_1.commission_lamports, 50); let reward_commission_b_2 = accumulator2 .reward_commissions .get(&commission_pubkey_b) .unwrap(); - assert_eq!(reward_commission_b_2.commission_bps, 1_000); + assert_eq!(reward_commission_b_2.commission_bps, Some(1_000)); assert_eq!(reward_commission_b_2.commission_lamports, 30); let reward_commission_c_2 = accumulator2 .reward_commissions .get(&commission_pubkey_c) .unwrap(); - assert_eq!(reward_commission_c_2.commission_bps, 1_000); + assert_eq!(reward_commission_c_2.commission_bps, Some(1_000)); assert_eq!(reward_commission_c_2.commission_lamports, 50); let accumulator = accumulator1.accumulate_into_larger(accumulator2); @@ -2314,14 +2311,14 @@ mod tests { .reward_commissions .get(&commission_pubkey_a) .unwrap(); - assert_eq!(reward_commission_a.commission_bps, 1_000); + assert_eq!(reward_commission_a.commission_bps, Some(1_000)); assert_eq!(reward_commission_a.commission_lamports, 50); let reward_commission_b = accumulator .reward_commissions .get(&commission_pubkey_b) .unwrap(); - assert_eq!(reward_commission_b.commission_bps, 1_000); + assert_eq!(reward_commission_b.commission_bps, Some(1_000)); // sum of the reward commissions from both accumulators assert_eq!(reward_commission_b.commission_lamports, 80); @@ -2329,7 +2326,7 @@ mod tests { .reward_commissions .get(&commission_pubkey_c) .unwrap(); - assert_eq!(reward_commission_c.commission_bps, 1_000); + assert_eq!(reward_commission_c.commission_bps, Some(1_000)); assert_eq!(reward_commission_c.commission_lamports, 50); } @@ -2597,7 +2594,7 @@ mod tests { reward_commissions.insert( pubkey, RewardCommission { - commission_bps: 0, + commission_bps: Some(0), commission_lamports: 1, // enough to overflow }, ); @@ -2623,7 +2620,7 @@ mod tests { reward_commissions.insert( pubkey, RewardCommission { - commission_bps: 500, + commission_bps: Some(500), commission_lamports, }, ); @@ -2671,7 +2668,7 @@ mod tests { reward_commissions.insert( pubkey, RewardCommission { - commission_bps, + commission_bps: Some(commission_bps), commission_lamports, }, ); diff --git a/runtime/src/bank/partitioned_epoch_rewards/distribution.rs b/runtime/src/bank/partitioned_epoch_rewards/distribution.rs index 2d1f782d7eb..9c52e692dd0 100644 --- a/runtime/src/bank/partitioned_epoch_rewards/distribution.rs +++ b/runtime/src/bank/partitioned_epoch_rewards/distribution.rs @@ -262,7 +262,7 @@ impl Bank { reward_type, lamports: i64::try_from(partitioned_stake_reward.stake_reward).unwrap(), post_balance: account.lamports(), - commission_bps: Some(partitioned_stake_reward.commission_bps), + commission_bps: partitioned_stake_reward.commission_bps, }, stake_account: account, }) @@ -711,7 +711,7 @@ mod tests { stake_pubkey: nonexistent_account, stake: new_stake, stake_reward, - commission_bps, + commission_bps: Some(commission_bps), }; let stakes_cache = bank.stakes_cache.stakes(); let stakes_cache_accounts = stakes_cache.stake_delegations(); @@ -748,7 +748,7 @@ mod tests { stake_pubkey: overflowing_account, stake: new_stake, stake_reward, - commission_bps, + commission_bps: Some(commission_bps), }; let stakes_cache = bank.stakes_cache.stakes(); let stakes_cache_accounts = stakes_cache.stake_delegations(); @@ -795,7 +795,7 @@ mod tests { stake_pubkey: successful_account, stake: new_stake, stake_reward, - commission_bps, + commission_bps: Some(commission_bps), }; let stakes_cache = bank.stakes_cache.stakes(); let stakes_cache_accounts = stakes_cache.stake_delegations(); @@ -876,7 +876,7 @@ mod tests { stake_pubkey: deactivating_account, stake: deactivating_stake, stake_reward, - commission_bps, + commission_bps: Some(commission_bps), }; let stakes_cache = bank.stakes_cache.stakes(); let stakes_cache_accounts = stakes_cache.stake_delegations(); diff --git a/runtime/src/bank/partitioned_epoch_rewards/mod.rs b/runtime/src/bank/partitioned_epoch_rewards/mod.rs index 880b7026cc0..9b786cc378a 100644 --- a/runtime/src/bank/partitioned_epoch_rewards/mod.rs +++ b/runtime/src/bank/partitioned_epoch_rewards/mod.rs @@ -35,7 +35,10 @@ pub(crate) struct PartitionedStakeReward { pub stake_reward: u64, /// Reward commission in basis points (0-10,000 representing 0-100%) for /// recording reward info. - pub commission_bps: u16, + // + // Note: This field becomes always `None` once SIMD-0232 is activated. + // After full activation, it can be removed on feature cleanup. + pub commission_bps: Option, } /// A vector of stake rewards. @@ -150,7 +153,9 @@ pub(crate) enum EpochRewardPhase { #[derive(Debug)] pub(super) struct RewardCommission { - pub(super) commission_bps: u16, + // Note: This field becomes always `None` once SIMD-0232 is activated. + // After full activation, it can be removed on feature cleanup. + pub(super) commission_bps: Option, pub(super) commission_lamports: u64, } @@ -427,7 +432,7 @@ mod tests { stake_pubkey: stake_reward.stake_pubkey, stake, stake_reward: stake_reward.stake_reward_info.lamports as u64, - commission_bps: stake_reward.stake_reward_info.commission_bps.unwrap(), + commission_bps: stake_reward.stake_reward_info.commission_bps, }) } else { None From f5b9898023b2ea061f01bcfb4893628d98143aec Mon Sep 17 00:00:00 2001 From: cmoyes-jump Date: Wed, 20 May 2026 17:41:45 -0500 Subject: [PATCH 014/576] gossip: re-land FFI harness with pure-Agave accessors gatted on DCOU (#12563) * gossip: gate pure-Agave accessors on DCOU, use conformance for protosol/FFI * add comment about why conformance is needed --- gossip/Cargo.toml | 6 ++-- gossip/src/contact_info.rs | 5 +-- gossip/src/crds_data.rs | 10 +++--- gossip/src/crds_gossip_pull.rs | 5 +-- gossip/src/duplicate_shred.rs | 9 ++--- gossip/src/harness.rs | 59 ++++++++++++++++++++++++++++++++ gossip/src/lib.rs | 5 ++- gossip/src/protocol.rs | 2 +- gossip/tests/conformance/main.rs | 2 +- 9 files changed, 86 insertions(+), 17 deletions(-) create mode 100644 gossip/src/harness.rs diff --git a/gossip/Cargo.toml b/gossip/Cargo.toml index 3410a709662..fc9bd345e01 100644 --- a/gossip/Cargo.toml +++ b/gossip/Cargo.toml @@ -28,7 +28,9 @@ frozen-abi = [ "solana-vote-program/frozen-abi", ] agave-unstable-api = [] -dev-context-only-utils = ["dep:prost", "dep:protosol", "solana-bloom/dev-context-only-utils"] +dev-context-only-utils = ["solana-bloom/dev-context-only-utils"] +conformance = ["dep:prost", "dep:protosol", "dev-context-only-utils"] +dummy-for-ci-check = ["conformance"] [dependencies] agave-logger = { workspace = true } @@ -98,7 +100,7 @@ bs58 = { workspace = true } criterion = { workspace = true } num_cpus = { workspace = true } serial_test = { workspace = true } -solana-gossip = { path = ".", features = ["agave-unstable-api", "dev-context-only-utils"] } +solana-gossip = { path = ".", features = ["agave-unstable-api", "dev-context-only-utils", "conformance"] } solana-net-utils = { path = "../net-utils", features = ["agave-unstable-api", "dev-context-only-utils"] } solana-perf = { path = "../perf", features = ["agave-unstable-api", "dev-context-only-utils"] } solana-runtime = { path = "../runtime", features = ["agave-unstable-api", "dev-context-only-utils"] } diff --git a/gossip/src/contact_info.rs b/gossip/src/contact_info.rs index 0307a298091..3de9e865d1e 100644 --- a/gossip/src/contact_info.rs +++ b/gossip/src/contact_info.rs @@ -248,12 +248,13 @@ impl ContactInfo { &self.version } - #[cfg(any(test, feature = "dev-context-only-utils"))] + // Conformance-only accessors; unused under DCOU. + #[cfg(any(test, feature = "conformance"))] pub(crate) fn addrs(&self) -> &[IpAddr] { &self.addrs } - #[cfg(any(test, feature = "dev-context-only-utils"))] + #[cfg(any(test, feature = "conformance"))] pub(crate) fn sockets(&self) -> &[SocketEntry] { &self.sockets } diff --git a/gossip/src/crds_data.rs b/gossip/src/crds_data.rs index 56117a8ee3c..4ef1193d1d9 100644 --- a/gossip/src/crds_data.rs +++ b/gossip/src/crds_data.rs @@ -254,12 +254,13 @@ pub struct LowestSlot { } impl LowestSlot { - #[cfg(any(test, feature = "dev-context-only-utils"))] + // Conformance-only accessors; unused under DCOU. + #[cfg(any(test, feature = "conformance"))] pub(crate) fn wallclock(&self) -> u64 { self.wallclock } - #[cfg(any(test, feature = "dev-context-only-utils"))] + #[cfg(any(test, feature = "conformance"))] pub(crate) fn from(&self) -> &Pubkey { &self.from } @@ -365,12 +366,13 @@ impl Vote { &self.transaction } - #[cfg(any(test, feature = "dev-context-only-utils"))] + // Conformance-only accessors; unused under DCOU. + #[cfg(any(test, feature = "conformance"))] pub(crate) fn from(&self) -> &Pubkey { &self.from } - #[cfg(any(test, feature = "dev-context-only-utils"))] + #[cfg(any(test, feature = "conformance"))] pub(crate) fn wallclock(&self) -> u64 { self.wallclock } diff --git a/gossip/src/crds_gossip_pull.rs b/gossip/src/crds_gossip_pull.rs index e5132fe9c54..7a8772d8ef1 100644 --- a/gossip/src/crds_gossip_pull.rs +++ b/gossip/src/crds_gossip_pull.rs @@ -90,12 +90,13 @@ impl solana_sanitize::Sanitize for CrdsFilter { } impl CrdsFilter { - #[cfg(any(test, feature = "dev-context-only-utils"))] + // Conformance-only accessors; unused under DCOU. + #[cfg(any(test, feature = "conformance"))] pub(crate) fn mask(&self) -> u64 { self.mask } - #[cfg(any(test, feature = "dev-context-only-utils"))] + #[cfg(any(test, feature = "conformance"))] pub(crate) fn get_mask_bits(&self) -> u32 { self.mask_bits } diff --git a/gossip/src/duplicate_shred.rs b/gossip/src/duplicate_shred.rs index 14e2e408adc..b1ac6cc727a 100644 --- a/gossip/src/duplicate_shred.rs +++ b/gossip/src/duplicate_shred.rs @@ -52,25 +52,26 @@ impl DuplicateShred { self.chunk_index } - #[cfg(any(test, feature = "dev-context-only-utils"))] + // Conformance-only accessors; unused under DCOU. + #[cfg(any(test, feature = "conformance"))] #[inline] pub(crate) fn from(&self) -> &Pubkey { &self.from } - #[cfg(any(test, feature = "dev-context-only-utils"))] + #[cfg(any(test, feature = "conformance"))] #[inline] pub(crate) fn wallclock(&self) -> u64 { self.wallclock } - #[cfg(any(test, feature = "dev-context-only-utils"))] + #[cfg(any(test, feature = "conformance"))] #[inline] pub(crate) fn slot(&self) -> Slot { self.slot } - #[cfg(any(test, feature = "dev-context-only-utils"))] + #[cfg(any(test, feature = "conformance"))] #[inline] pub(crate) fn chunk(&self) -> &[u8] { &self.chunk diff --git a/gossip/src/harness.rs b/gossip/src/harness.rs new file mode 100644 index 00000000000..1dda7277552 --- /dev/null +++ b/gossip/src/harness.rs @@ -0,0 +1,59 @@ +use {crate::protocol::gossip_decode_to_effects, prost::Message, std::ffi::c_int}; + +/// Conformance harness entry point for gossip message decoding. +/// +/// Deserializes a binary-encoded gossip message from the input buffer, +/// decodes it into effects, and writes the Protobuf-encoded result into +/// the output buffer. +/// +/// # Parameters +/// - `out_ptr`: Pointer to the output buffer where the Protobuf-encoded +/// result will be written. +/// - `out_psz`: Pointer to a `u64` that on entry holds the capacity of the +/// output buffer and on successful return is updated to the number of +/// bytes actually written. +/// - `in_ptr`: Pointer to the input buffer containing the binary-encoded +/// gossip message. May be null when `in_sz` is 0. +/// - `in_sz`: Length of the input buffer in bytes. +/// +/// # Returns +/// `1` on success, `0` on failure (null pointers, zero-length input, or +/// output buffer too small). +/// +/// # Safety +/// - `out_ptr` must point to a writable buffer whose capacity is stored at +/// `*out_psz`. +/// - `out_psz` must be a valid, aligned pointer to a `u64`. +/// - `in_ptr` must point to a readable buffer of at least `in_sz` bytes +/// (or be null/ignored when `in_sz` is 0). +#[unsafe(no_mangle)] +pub unsafe extern "C" fn sol_compat_gossip_decode_v1( + out_ptr: *mut u8, + out_psz: *mut u64, + in_ptr: *const u8, + in_sz: u64, +) -> c_int { + if out_ptr.is_null() || out_psz.is_null() { + return 0; + } + + let in_slice = if in_sz == 0 { + &[] + } else if in_ptr.is_null() { + return 0; + } else { + unsafe { std::slice::from_raw_parts(in_ptr, in_sz as usize) } + }; + + let effects = gossip_decode_to_effects(in_slice); + let out_vec = effects.encode_to_vec(); + + let out_cap = unsafe { *out_psz } as usize; + if out_vec.len() > out_cap { + return 0; + } + let out_slice = unsafe { std::slice::from_raw_parts_mut(out_ptr, out_cap) }; + out_slice[..out_vec.len()].copy_from_slice(&out_vec); + unsafe { *out_psz = out_vec.len() as u64 }; + 1 +} diff --git a/gossip/src/lib.rs b/gossip/src/lib.rs index 056aa386144..87d60c50551 100644 --- a/gossip/src/lib.rs +++ b/gossip/src/lib.rs @@ -47,7 +47,10 @@ extern crate solana_frozen_abi_macro; #[macro_use] extern crate solana_metrics; -#[cfg(feature = "dev-context-only-utils")] +#[cfg(feature = "conformance")] pub use protocol::gossip_decode_to_effects; +#[cfg(feature = "conformance")] +mod harness; + mod wire_format_tests; diff --git a/gossip/src/protocol.rs b/gossip/src/protocol.rs index a667af5d80c..ac9da04f1ab 100644 --- a/gossip/src/protocol.rs +++ b/gossip/src/protocol.rs @@ -255,7 +255,7 @@ pub(crate) fn split_gossip_messages( }) } -#[cfg(feature = "dev-context-only-utils")] +#[cfg(feature = "conformance")] pub fn gossip_decode_to_effects(input: &[u8]) -> protosol::protos::GossipEffects { use { crate::{ diff --git a/gossip/tests/conformance/main.rs b/gossip/tests/conformance/main.rs index 970d3c9796d..1c24bfa73e8 100644 --- a/gossip/tests/conformance/main.rs +++ b/gossip/tests/conformance/main.rs @@ -1,4 +1,4 @@ -#![cfg(feature = "dev-context-only-utils")] +#![cfg(feature = "conformance")] //! Gossip conformance tests and fixture generation. From e8cab9155b2d20b1a67ea6d7626ca562a6386e7e Mon Sep 17 00:00:00 2001 From: Brennan Date: Wed, 20 May 2026 18:10:57 -0700 Subject: [PATCH 015/576] Dynamic ns per slot (#12645) dynamic ns per slot --- core/src/block_creation_loop.rs | 20 +++++++------------- runtime/src/bank.rs | 5 +++++ runtime/src/bank/tests.rs | 12 ++++++++++++ votor/src/common.rs | 9 --------- votor/src/event_handler.rs | 7 +++---- votor/src/timer_manager.rs | 11 ++++------- votor/src/timer_manager/timers.rs | 30 ++++++++++++------------------ 7 files changed, 43 insertions(+), 51 deletions(-) diff --git a/core/src/block_creation_loop.rs b/core/src/block_creation_loop.rs index 618bddb7c07..e79e00b173e 100644 --- a/core/src/block_creation_loop.rs +++ b/core/src/block_creation_loop.rs @@ -410,13 +410,10 @@ fn reset_poh_recorder(bank: &Arc, ctx: &LeaderContext) { .reset(bank.clone(), next_leader_slot); } -/// From the passed in bank (used for determining slot times) and leader block -/// index within the leader window, returns the duration after which we should -/// publish the final shred for the block with starting point being the start of -/// the leader window. -fn block_timeout(bank: &Bank, leader_block_index: usize) -> Duration { - Duration::from_nanos_u128(bank.ns_per_slot) - .saturating_mul((leader_block_index as u32).saturating_add(1)) +/// Returns the elapsed leader-window time at which `slot` must be completed. +fn block_timeout(bank: &Bank, slot: Slot) -> Duration { + Duration::from_nanos_u128(bank.ns_per_slot_at_slot(slot)) + .saturating_mul((leader_slot_index(slot) as u32).saturating_add(1)) } /// Select the freshest leader-window notification within one source. @@ -519,7 +516,7 @@ fn produce_block_footer( .get_nanosecond_clock() .unwrap_or_else(|| bank.clock().unix_timestamp.saturating_mul(1_000_000_000)); let parent_slot = parent_bank.slot(); - let ns_per_slot = u64::try_from(bank.ns_per_slot).unwrap_or(u64::MAX); + let ns_per_slot = u64::try_from(bank.ns_per_slot_at_slot(slot)).unwrap_or(u64::MAX); block_producer_time_nanos = skew_block_producer_time_nanos( parent_slot, @@ -571,7 +568,7 @@ fn produce_window( let mut slot = start_slot; while !ctx.exit.load(Ordering::Relaxed) && slot <= end_slot { - let timeout = block_timeout(&working_bank, leader_slot_index(slot)); + let timeout = block_timeout(&working_bank, slot); trace!( "{my_pubkey}: waiting for leader bank {slot} to finish, remaining time: {}ms", timeout.saturating_sub(block_timer.elapsed()).as_millis() @@ -1068,10 +1065,7 @@ fn start_leader_wait_for_parent_replay( ctx.my_pubkey ); let my_pubkey = ctx.my_pubkey; - let timeout = block_timeout( - &ctx.bank_forks.read().unwrap().root_bank(), - leader_slot_index(slot), - ); + let timeout = block_timeout(&ctx.bank_forks.read().unwrap().root_bank(), slot); let end_slot = last_of_consecutive_leader_slots(slot); let mut slot_delay_start = Measure::start("slot_delay"); diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index d7512be4b98..6f9d317f44c 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -2641,6 +2641,11 @@ impl Bank { self.epoch_schedule().get_slots_in_epoch(epoch) as f64 / self.slots_per_year } + /// Returns the slot duration to use for `slot`. + pub fn ns_per_slot_at_slot(&self, _slot: Slot) -> u128 { + self.ns_per_slot + } + pub fn max_processing_age(&self) -> usize { self.max_processing_age } diff --git a/runtime/src/bank/tests.rs b/runtime/src/bank/tests.rs index eafff8b5292..cdbf7605f0e 100644 --- a/runtime/src/bank/tests.rs +++ b/runtime/src/bank/tests.rs @@ -6580,6 +6580,18 @@ fn poh_estimate_offset(bank: &Bank) -> Duration { * Duration::from_nanos(bank.ns_per_slot as u64) } +#[test] +fn test_ns_per_slot() { + let (genesis_config, _) = create_genesis_config(1); + let bank = Bank::new_for_tests(&genesis_config); + + assert_eq!(bank.ns_per_slot_at_slot(bank.slot()), bank.ns_per_slot); + assert_eq!( + bank.ns_per_slot_at_slot(bank.slot().saturating_add(1)), + bank.ns_per_slot + ); +} + #[test] fn test_timestamp_slow() { fn max_allowable_delta_since_epoch(bank: &Bank, max_allowable_drift: u32) -> i64 { diff --git a/votor/src/common.rs b/votor/src/common.rs index 26c37bce27f..1749480f4b6 100644 --- a/votor/src/common.rs +++ b/votor/src/common.rs @@ -4,7 +4,6 @@ use { fraction::Fraction, vote::{Vote, VoteType}, }, - solana_clock::DEFAULT_MS_PER_SLOT, std::time::Duration, }; @@ -82,13 +81,5 @@ const DELTA_BLOCK_PROPAGATION: Duration = DELTA.checked_mul(3).unwrap(); /// validator triggering the parent ready event and for block propagation delay. pub(crate) const DELTA_TIMEOUT: Duration = DELTA.checked_add(DELTA_BLOCK_PROPAGATION).unwrap(); -/// Time budget we allow a leader to build and send their first slice after -/// their leader window starts. `TimeoutCrashedLeader` must therefore fire -/// no earlier than `DELTA_TIMEOUT + DELTA_FIRST_SLICE` from the start of the -/// window, otherwise we may declare a correct leader crashed. -/// -/// Conservatively initialized to the slot time. -pub(crate) const DELTA_FIRST_SLICE: Duration = Duration::from_millis(DEFAULT_MS_PER_SLOT); - /// Timeout for standstill detection mechanism. pub(crate) const DELTA_STANDSTILL: Duration = Duration::from_millis(10_000); diff --git a/votor/src/event_handler.rs b/votor/src/event_handler.rs index 2a4d8f74bd8..281a1ae8fdb 100644 --- a/votor/src/event_handler.rs +++ b/votor/src/event_handler.rs @@ -4,7 +4,6 @@ use { crate::{ commitment::{CommitmentType, update_commitment_cache}, - common::DELTA_FIRST_SLICE, consensus_metrics::ConsensusMetricsEvent, event::{ CompletedBlock, RepairEvent, RepairEventSender, SwitchBankEvent, SwitchBankEventSender, @@ -229,9 +228,9 @@ impl EventHandler { let should_set_timeouts = vctx.vote_history.add_parent_ready(slot, parent_block); Self::check_pending_blocks(my_pubkey, &mut local_context.pending_blocks, vctx, votes)?; if should_set_timeouts { - // TODO: configure delta_first_slice in bank - let delta_first_slice = DELTA_FIRST_SLICE; - let delta_block = Duration::from_nanos_u128(vctx.sharable_banks.root().ns_per_slot); + let root_bank = vctx.sharable_banks.root(); + let delta_block = Duration::from_nanos_u128(root_bank.ns_per_slot_at_slot(slot)); + let delta_first_slice = delta_block; timer_manager.write().set_timeouts( slot, local_context.standstill_slot, diff --git a/votor/src/timer_manager.rs b/votor/src/timer_manager.rs index 89a0930e6a8..251f8e65827 100644 --- a/votor/src/timer_manager.rs +++ b/votor/src/timer_manager.rs @@ -86,11 +86,8 @@ impl TimerManager { #[cfg(test)] mod tests { use { - super::*, - crate::{common::DELTA_FIRST_SLICE, event::VotorEvent}, - crossbeam_channel::unbounded, - solana_clock::DEFAULT_MS_PER_SLOT, - std::time::Duration, + super::*, crate::event::VotorEvent, crossbeam_channel::unbounded, + solana_clock::DEFAULT_MS_PER_SLOT, std::time::Duration, }; #[test] @@ -102,8 +99,8 @@ mod tests { exit.clone(), Arc::new(MigrationStatus::post_migration_status()), ); - let delta_first_slice = DELTA_FIRST_SLICE; let delta_block = Duration::from_millis(DEFAULT_MS_PER_SLOT); + let delta_first_slice = delta_block; let slot = 52; let start = Instant::now(); timer_manager.set_timeouts(slot, None, delta_first_slice, delta_block); @@ -125,7 +122,7 @@ mod tests { assert_eq!(s, slot); assert!( Instant::now().duration_since(start) - >= DELTA_TIMEOUT + DELTA_FIRST_SLICE + >= DELTA_TIMEOUT + delta_first_slice ); timeouts_received += 1; } diff --git a/votor/src/timer_manager/timers.rs b/votor/src/timer_manager/timers.rs index 7b94b5efabd..9306c1c15f0 100644 --- a/votor/src/timer_manager/timers.rs +++ b/votor/src/timer_manager/timers.rs @@ -32,7 +32,7 @@ fn calculate_timeout_multiplier(slot: Slot, standstill_slot: Option) -> f6 /// Encodes a basic state machine of the different stages involved in handling /// timeouts for a window of slots. enum TimerState { - /// Waiting for initial DELTA_TIMEOUT + DELTA_FIRST_SLICE stage. + /// Waiting for initial DELTA_TIMEOUT + delta_first_slice stage. WaitForFirstSlice { /// The slots in the window. Must not be empty. window: VecDeque, @@ -77,9 +77,9 @@ impl TimerState { (delta_timeout.as_secs_f64() * timeout_multiplier).min(MAX_TIMEOUT_SECS), ); - // A correct leader may take up to `DELTA_FIRST_SLICE` to send their first slice, + // A correct leader may take up to `delta_first_slice` to send their first slice, // so the earliest sound point to declare them crashed is - // `DELTA_FIRST_SLICE + delta_timeout` after their window starts. + // `delta_first_slice + delta_timeout` after their window starts. let timeout = now .checked_add(scaled_delta_timeout) .unwrap() @@ -264,9 +264,7 @@ impl Timers { #[cfg(test)] mod tests { use { - super::*, - crate::common::{DELTA_FIRST_SLICE, DELTA_TIMEOUT}, - crossbeam_channel::unbounded, + super::*, crate::common::DELTA_TIMEOUT, crossbeam_channel::unbounded, solana_clock::DEFAULT_MS_PER_SLOT, }; @@ -321,14 +319,9 @@ mod tests { let mut timers = Timers::new(one_micro, sender); assert!(timers.progress(now).is_none()); assert!(receiver.try_recv().unwrap_err().is_empty()); + let delta_block = Duration::from_millis(DEFAULT_MS_PER_SLOT); - timers.set_timeouts( - 0, - now, - None, - DELTA_FIRST_SLICE, - Duration::from_millis(DEFAULT_MS_PER_SLOT), - ); + timers.set_timeouts(0, now, None, delta_block, delta_block); while timers.progress(now).is_some() { now = now.checked_add(one_micro).unwrap(); } @@ -431,19 +424,20 @@ mod tests { // Use a large multiplier that would exceed MAX_TIMEOUT let multiplier = 1000000.0; let delta_block = Duration::from_millis(DEFAULT_MS_PER_SLOT); + let delta_first_slice = delta_block; let (mut timer_state, next_fire) = TimerState::new( 100, DELTA_TIMEOUT, - DELTA_FIRST_SLICE, + delta_first_slice, delta_block, now, multiplier, ); - // The first timeout should be capped at MAX_TIMEOUT (+ unscaled DELTA_FIRST_SLICE). + // The first timeout should be capped at MAX_TIMEOUT (+ unscaled delta_first_slice). let expected_first_fire = - now + Duration::from_secs(MAX_TIMEOUT_SECS as u64) + DELTA_FIRST_SLICE; + now + Duration::from_secs(MAX_TIMEOUT_SECS as u64) + delta_first_slice; assert_eq!(next_fire, expected_first_fire); // Progress the timer to get TimeoutCrashedLeader @@ -453,9 +447,9 @@ mod tests { )); // Slot 0's block deadline is `T + delta_block + scaled_delta_timeout`, so - // the gap from the (DELTA_FIRST_SLICE-shifted) first fire is `delta_block - DELTA_FIRST_SLICE`. + // the gap from the (delta_first_slice-shifted) first fire is `delta_block - delta_first_slice`. let next = timer_state.next_fire().unwrap(); let actual_delta = next - next_fire; - assert_eq!(actual_delta, delta_block - DELTA_FIRST_SLICE); + assert_eq!(actual_delta, delta_block - delta_first_slice); } } From 69d4559b32a650e818fb43ae9d04dd9f5baad167 Mon Sep 17 00:00:00 2001 From: Brennan Date: Wed, 20 May 2026 18:16:04 -0700 Subject: [PATCH 016/576] Dynamic PER stores (#12646) --- runtime/src/bank.rs | 19 +++++++++++++++++++ .../src/bank/partitioned_epoch_rewards/mod.rs | 14 +++++++++----- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 6f9d317f44c..c889026d92a 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -586,6 +586,7 @@ impl PartialEq for Bank { status_cache: _, blockhash_queue, max_processing_age, + partitioned_rewards_stake_account_stores_per_block, ancestors: _, hash, parent_hash, @@ -656,6 +657,8 @@ impl PartialEq for Bank { } = self; *blockhash_queue.read().unwrap() == *other.blockhash_queue.read().unwrap() && *max_processing_age == other.max_processing_age + && *partitioned_rewards_stake_account_stores_per_block + == other.partitioned_rewards_stake_account_stores_per_block && *hash.read().unwrap() == *other.hash.read().unwrap() && parent_hash == &other.parent_hash && parent_slot == &other.parent_slot @@ -811,6 +814,9 @@ pub struct Bank { /// Maximum age in slots a blockhash can be for a tx to be processed. max_processing_age: usize, + /// Number of stake accounts to store in each block during partitioned rewards. + partitioned_rewards_stake_account_stores_per_block: u64, + /// The set of parents including this bank pub ancestors: Ancestors, @@ -1134,11 +1140,16 @@ struct NewEpochBundle { impl Bank { fn default_with_accounts(accounts: Accounts) -> Self { + let partitioned_rewards_stake_account_stores_per_block = accounts + .accounts_db + .partitioned_epoch_rewards_config + .stake_account_stores_per_block; let mut bank = Self { rc: BankRc::new(accounts), status_cache: Arc::>::default(), blockhash_queue: RwLock::::default(), max_processing_age: MAX_PROCESSING_AGE, + partitioned_rewards_stake_account_stores_per_block, ancestors: Ancestors::default(), hash: RwLock::::default(), parent_hash: Hash::default(), @@ -1381,6 +1392,8 @@ impl Bank { epoch, blockhash_queue, max_processing_age: parent.max_processing_age, + partitioned_rewards_stake_account_stores_per_block: parent + .partitioned_rewards_stake_account_stores_per_block, // TODO: clean this up, so much special-case copying... hashes_per_tick: parent.hashes_per_tick, ticks_per_slot: parent.ticks_per_slot, @@ -2046,11 +2059,17 @@ impl Bank { let stakes_accounts_load_duration = now.elapsed(); let rent = Self::load_rent_from_account_for_snapshot_load(&bank_rc.accounts, &ancestors); + let partitioned_rewards_stake_account_stores_per_block = bank_rc + .accounts + .accounts_db + .partitioned_epoch_rewards_config + .stake_account_stores_per_block; let mut bank = Self { rc: bank_rc, status_cache: Arc::>::default(), blockhash_queue: RwLock::new(fields.blockhash_queue), max_processing_age: MAX_PROCESSING_AGE, + partitioned_rewards_stake_account_stores_per_block, ancestors, hash: RwLock::new(fields.hash), parent_hash: fields.parent_hash, diff --git a/runtime/src/bank/partitioned_epoch_rewards/mod.rs b/runtime/src/bank/partitioned_epoch_rewards/mod.rs index 9b786cc378a..5bf473a87c5 100644 --- a/runtime/src/bank/partitioned_epoch_rewards/mod.rs +++ b/runtime/src/bank/partitioned_epoch_rewards/mod.rs @@ -354,11 +354,7 @@ impl Bank { /// # stake accounts to store in one block during partitioned reward interval pub(super) fn partitioned_rewards_stake_account_stores_per_block(&self) -> u64 { - self.rc - .accounts - .accounts_db - .partitioned_epoch_rewards_config - .stake_account_stores_per_block + self.partitioned_rewards_stake_account_stores_per_block } /// Calculate the number of blocks required to distribute rewards to all stake accounts. @@ -695,6 +691,14 @@ mod tests { bank.partitioned_rewards_stake_account_stores_per_block(); assert_eq!(stake_account_stores_per_block, 10); + let (bank, bank_forks) = bank.wrap_with_bank_forks_for_tests(); + let bank = + Bank::new_from_parent_with_bank_forks(&bank_forks, bank, SlotLeader::default(), 1); + assert_eq!( + bank.partitioned_rewards_stake_account_stores_per_block(), + stake_account_stores_per_block + ); + let check_num_reward_distribution_blocks = |num_stakes: u64, expected_num_reward_distribution_blocks: u64| { // Given the short epoch, i.e. 32 slots, we should cap the number of reward distribution blocks to 32/10 = 3. From 84d51cb15a7a88ad20d45d912a25232e8daa88d8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 13:04:03 +0800 Subject: [PATCH 017/576] chore(deps): bump spl-token-interface from 2.0.0 to 3.0.0 (#12623) * chore(deps): bump spl-token-interface from 2.0.0 to 3.0.0 Bumps [spl-token-interface](https://github.com/solana-program/token) from 2.0.0 to 3.0.0. - [Release notes](https://github.com/solana-program/token/releases) - [Commits](https://github.com/solana-program/token/compare/interface@v2.0.0...interface@v3.0.0) --- updated-dependencies: - dependency-name: spl-token-interface dependency-version: 3.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] * Update all workspaces --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- dev-bins/Cargo.lock | 4 ++-- programs/sbf/Cargo.lock | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 59a488c957f..3be74351852 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11791,9 +11791,9 @@ dependencies = [ [[package]] name = "spl-token-interface" -version = "2.0.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c564ac05a7c8d8b12e988a37d82695b5ba4db376d07ea98bc4882c81f96c7f3" +checksum = "7143c4e676984047a400e2fbd7ac29a1659f2b1b52b897cfde19316b562e4589" dependencies = [ "arrayref", "bytemuck", diff --git a/Cargo.toml b/Cargo.toml index 5bf9a520e86..56952aadda5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -528,7 +528,7 @@ spl-pod = "0.7.3" spl-token-2022-interface = "2.1.0" spl-token-confidential-transfer-proof-extraction = "0.5.1" spl-token-group-interface = "0.7.2" -spl-token-interface = "2.0.0" +spl-token-interface = "3.0.0" spl-token-metadata-interface = "0.8.0" static_assertions = "1.1.0" stream-cancel = "0.8.2" diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index e6c1bfbe4fd..9c230a7036d 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -10141,9 +10141,9 @@ dependencies = [ [[package]] name = "spl-token-interface" -version = "2.0.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c564ac05a7c8d8b12e988a37d82695b5ba4db376d07ea98bc4882c81f96c7f3" +checksum = "7143c4e676984047a400e2fbd7ac29a1659f2b1b52b897cfde19316b562e4589" dependencies = [ "arrayref", "bytemuck", diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index e84f84ad23c..59e8cc9f761 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -10434,9 +10434,9 @@ dependencies = [ [[package]] name = "spl-token-interface" -version = "2.0.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c564ac05a7c8d8b12e988a37d82695b5ba4db376d07ea98bc4882c81f96c7f3" +checksum = "7143c4e676984047a400e2fbd7ac29a1659f2b1b52b897cfde19316b562e4589" dependencies = [ "arrayref", "bytemuck", From b687dc6a2902e116b8efe1719a49fa4d249f4fc8 Mon Sep 17 00:00:00 2001 From: Brooks Date: Thu, 21 May 2026 01:30:55 -0400 Subject: [PATCH 018/576] Renames fn to AccountsDb::do_load_for_tests() (#12626) --- accounts-db/src/accounts_db.rs | 12 +-- accounts-db/src/accounts_db/tests.rs | 136 +++++++++------------------ 2 files changed, 50 insertions(+), 98 deletions(-) diff --git a/accounts-db/src/accounts_db.rs b/accounts-db/src/accounts_db.rs index a344c4fbd97..81f1f5b6a09 100644 --- a/accounts-db/src/accounts_db.rs +++ b/accounts-db/src/accounts_db.rs @@ -6736,7 +6736,7 @@ impl AccountsDb { /// Note that this is non-deterministic if clean is running asynchronously. /// If a zero lamport account exists in the index, then Some is returned. /// Once it is cleaned from the index, None is returned. - fn load_without_fixed_root( + fn do_load_for_tests( &self, ancestors: &Ancestors, pubkey: &Pubkey, @@ -6751,13 +6751,13 @@ impl AccountsDb { pub fn assert_load_account(&self, slot: Slot, pubkey: Pubkey, expected_lamports: u64) { let ancestors = Ancestors::from(vec![slot]); - let (account, slot) = self.load_without_fixed_root(&ancestors, &pubkey).unwrap(); + let (account, slot) = self.do_load_for_tests(&ancestors, &pubkey).unwrap(); assert_eq!((account.lamports(), slot), (expected_lamports, slot)); } pub fn assert_not_load_account(&self, slot: Slot, pubkey: Pubkey) { let ancestors = Ancestors::from(vec![slot]); - let load = self.load_without_fixed_root(&ancestors, &pubkey); + let load = self.do_load_for_tests(&ancestors, &pubkey); assert!(load.is_none(), "{load:?}"); } @@ -6771,7 +6771,7 @@ impl AccountsDb { let ancestors = Ancestors::from(vec![slot]); for _ in 0..num { let idx = rng().random_range(0..num); - let account = self.load_without_fixed_root(&ancestors, &pubkeys[idx]); + let account = self.do_load_for_tests(&ancestors, &pubkeys[idx]); let account1 = Some(( AccountSharedData::new( (idx + count) as u64, @@ -6855,7 +6855,7 @@ impl AccountsDb { let account = AccountSharedData::new((t + 1) as u64, space, AccountSharedData::default().owner()); pubkeys.push(pubkey); - assert!(self.load_without_fixed_root(&ancestors, &pubkey).is_none()); + assert!(self.do_load_for_tests(&ancestors, &pubkey).is_none()); self.store_for_tests((slot, [(&pubkey, &account)].as_slice())); } for t in 0..num_vote { @@ -6864,7 +6864,7 @@ impl AccountsDb { AccountSharedData::new((num + t + 1) as u64, space, &solana_vote_program::id()); pubkeys.push(pubkey); let ancestors = Ancestors::from(vec![slot]); - assert!(self.load_without_fixed_root(&ancestors, &pubkey).is_none()); + assert!(self.do_load_for_tests(&ancestors, &pubkey).is_none()); self.store_for_tests((slot, [(&pubkey, &account)].as_slice())); } } diff --git a/accounts-db/src/accounts_db/tests.rs b/accounts-db/src/accounts_db/tests.rs index 01f5556687f..16b4056d134 100644 --- a/accounts-db/src/accounts_db/tests.rs +++ b/accounts-db/src/accounts_db/tests.rs @@ -338,10 +338,7 @@ define_accounts_db_test!(test_accountsdb_add_root, |db| { db.store_for_tests((0, [(&key, &account0)].as_slice())); db.add_root(0); let ancestors = Ancestors::from(vec![1]); - assert_eq!( - db.load_without_fixed_root(&ancestors, &key), - Some((account0, 0)) - ); + assert_eq!(db.do_load_for_tests(&ancestors, &key), Some((account0, 0))); }); define_accounts_db_test!(test_accountsdb_latest_ancestor, |db| { @@ -355,13 +352,13 @@ define_accounts_db_test!(test_accountsdb_latest_ancestor, |db| { let ancestors = Ancestors::from(vec![1]); assert_eq!( - &db.load_without_fixed_root(&ancestors, &key).unwrap().0, + &db.do_load_for_tests(&ancestors, &key).unwrap().0, &account1 ); let ancestors = Ancestors::from(vec![1, 0]); assert_eq!( - &db.load_without_fixed_root(&ancestors, &key).unwrap().0, + &db.do_load_for_tests(&ancestors, &key).unwrap().0, &account1 ); @@ -392,13 +389,13 @@ define_accounts_db_test!(test_accountsdb_latest_ancestor_with_root, |db| { let ancestors = Ancestors::from(vec![1]); assert_eq!( - &db.load_without_fixed_root(&ancestors, &key).unwrap().0, + &db.do_load_for_tests(&ancestors, &key).unwrap().0, &account1 ); let ancestors = Ancestors::from(vec![1, 0]); assert_eq!( - &db.load_without_fixed_root(&ancestors, &key).unwrap().0, + &db.do_load_for_tests(&ancestors, &key).unwrap().0, &account1 ); }); @@ -428,29 +425,23 @@ define_accounts_db_test!(test_accountsdb_root_one_slot, |db| { // at the Accounts level) let ancestors = Ancestors::from(vec![0, 1]); assert_eq!( - &db.load_without_fixed_root(&ancestors, &key).unwrap().0, + &db.do_load_for_tests(&ancestors, &key).unwrap().0, &account1 ); // we should see 1 token in slot 2 let ancestors = Ancestors::from(vec![0, 2]); assert_eq!( - &db.load_without_fixed_root(&ancestors, &key).unwrap().0, + &db.do_load_for_tests(&ancestors, &key).unwrap().0, &account0 ); db.add_root(0); let ancestors = Ancestors::from(vec![1]); - assert_eq!( - db.load_without_fixed_root(&ancestors, &key), - Some((account1, 1)) - ); + assert_eq!(db.do_load_for_tests(&ancestors, &key), Some((account1, 1))); let ancestors = Ancestors::from(vec![2]); - assert_eq!( - db.load_without_fixed_root(&ancestors, &key), - Some((account0, 0)) - ); // original value + assert_eq!(db.do_load_for_tests(&ancestors, &key), Some((account0, 0))); // original value }); define_accounts_db_test!(test_accountsdb_add_root_many, |db| { @@ -459,9 +450,7 @@ define_accounts_db_test!(test_accountsdb_add_root_many, |db| { for _ in 1..100 { let idx = rng().random_range(0..99); let ancestors = Ancestors::from(vec![0]); - let account = db - .load_without_fixed_root(&ancestors, &pubkeys[idx]) - .unwrap(); + let account = db.do_load_for_tests(&ancestors, &pubkeys[idx]).unwrap(); let default_account = AccountSharedData::from(Account { lamports: (idx + 1) as u64, ..Account::default() @@ -475,13 +464,9 @@ define_accounts_db_test!(test_accountsdb_add_root_many, |db| { for _ in 1..100 { let idx = rng().random_range(0..99); let ancestors = Ancestors::from(vec![0]); - let account0 = db - .load_without_fixed_root(&ancestors, &pubkeys[idx]) - .unwrap(); + let account0 = db.do_load_for_tests(&ancestors, &pubkeys[idx]).unwrap(); let ancestors = Ancestors::from(vec![1]); - let account1 = db - .load_without_fixed_root(&ancestors, &pubkeys[idx]) - .unwrap(); + let account1 = db.do_load_for_tests(&ancestors, &pubkeys[idx]).unwrap(); let default_account = AccountSharedData::from(Account { lamports: (idx + 1) as u64, ..Account::default() @@ -543,15 +528,9 @@ define_accounts_db_test!(test_accounts_unsquashed, |db0| { // masking accounts is done at the Accounts level, at accountsDB we see // original account let ancestors = Ancestors::from(vec![0, 1]); - assert_eq!( - db0.load_without_fixed_root(&ancestors, &key), - Some((account1, 1)) - ); + assert_eq!(db0.do_load_for_tests(&ancestors, &key), Some((account1, 1))); let ancestors = Ancestors::from(vec![0]); - assert_eq!( - db0.load_without_fixed_root(&ancestors, &key), - Some((account0, 0)) - ); + assert_eq!(db0.do_load_for_tests(&ancestors, &key), Some((account0, 0))); }); /// Test to verify that reclaiming old storages during flush works correctly. @@ -647,7 +626,7 @@ define_accounts_db_test!(test_remove_unrooted_slot_cached, |db| { // Purge the slot db.remove_unrooted_slots(&[(unrooted_slot, unrooted_bank_id)]); - assert!(db.load_without_fixed_root(&ancestors, &key).is_none()); + assert!(db.do_load_for_tests(&ancestors, &key).is_none()); assert!(db.accounts_cache.slot_cache(unrooted_slot).is_none()); assert!(db.storage.get_slot_storage_entry(unrooted_slot).is_none()); assert!(!db.contains(&key)); @@ -674,7 +653,7 @@ define_accounts_db_test!(test_remove_slot_snapshot_minimizer, |db| { // Purge the slot db.purge_slots_for_snapshot_minimizer([(&rooted_slot)].into_iter()); - assert!(db.load_without_fixed_root(&ancestors, &key).is_none()); + assert!(db.do_load_for_tests(&ancestors, &key).is_none()); assert!(db.accounts_cache.slot_cache(rooted_slot).is_none()); assert!(db.storage.get_slot_storage_entry(rooted_slot).is_none()); assert!(!db.contains(&key)); @@ -684,15 +663,14 @@ fn update_accounts(accounts: &AccountsDb, pubkeys: &[Pubkey], slot: Slot, range: for _ in 1..1000 { let idx = rng().random_range(0..range); let ancestors = Ancestors::from(vec![slot]); - if let Some((mut account, _)) = accounts.load_without_fixed_root(&ancestors, &pubkeys[idx]) - { + if let Some((mut account, _)) = accounts.do_load_for_tests(&ancestors, &pubkeys[idx]) { account.checked_add_lamports(1).unwrap(); accounts.store_for_tests((slot, [(&pubkeys[idx], &account)].as_slice())); if account.is_zero_lamport() { let ancestors = Ancestors::from(vec![slot]); assert!( accounts - .load_without_fixed_root(&ancestors, &pubkeys[idx]) + .do_load_for_tests(&ancestors, &pubkeys[idx]) .is_none() ); } else { @@ -713,7 +691,7 @@ fn test_account_one() { let mut pubkeys: Vec = vec![]; db.create_account(&mut pubkeys, 0, 1, 0, 0); let ancestors = Ancestors::from(vec![0]); - let account = db.load_without_fixed_root(&ancestors, &pubkeys[0]).unwrap(); + let account = db.do_load_for_tests(&ancestors, &pubkeys[0]).unwrap(); let default_account = AccountSharedData::from(Account { lamports: 1, ..Account::default() @@ -756,7 +734,7 @@ fn test_account_grow_many() { for (i, key) in keys.iter().enumerate() { assert_eq!( accounts - .load_without_fixed_root(&ancestors, key) + .do_load_for_tests(&ancestors, key) .unwrap() .0 .lamports(), @@ -805,17 +783,11 @@ fn test_account_grow() { } let ancestors = Ancestors::from(vec![0]); assert_eq!( - accounts - .load_without_fixed_root(&ancestors, &pubkey1) - .unwrap() - .0, + accounts.do_load_for_tests(&ancestors, &pubkey1).unwrap().0, account1 ); assert_eq!( - accounts - .load_without_fixed_root(&ancestors, &pubkey2) - .unwrap() - .0, + accounts.do_load_for_tests(&ancestors, &pubkey2).unwrap().0, account2 ); @@ -829,17 +801,11 @@ fn test_account_grow() { } let ancestors = Ancestors::from(vec![0]); assert_eq!( - accounts - .load_without_fixed_root(&ancestors, &pubkey1) - .unwrap() - .0, + accounts.do_load_for_tests(&ancestors, &pubkey1).unwrap().0, account1 ); assert_eq!( - accounts - .load_without_fixed_root(&ancestors, &pubkey2) - .unwrap() - .0, + accounts.do_load_for_tests(&ancestors, &pubkey2).unwrap().0, account2 ); if flush { @@ -1649,7 +1615,7 @@ fn test_cleanup_key_not_removed() { db.print_accounts_stats("post"); let ancestors = Ancestors::from(vec![2]); assert_eq!( - db.load_without_fixed_root(&ancestors, &key1) + db.do_load_for_tests(&ancestors, &key1) .unwrap() .0 .lamports(), @@ -1669,7 +1635,7 @@ fn test_store_large_account() { db.store_for_tests((0, [(&key, &account)].as_slice())); let ancestors = Ancestors::from(vec![0]); - let ret = db.load_without_fixed_root(&ancestors, &key).unwrap(); + let ret = db.do_load_for_tests(&ancestors, &key).unwrap(); assert_eq!(ret.0.data().len(), data_len); } @@ -2599,7 +2565,7 @@ fn test_wrapping_storage_id() { assert_eq!(slots - 1, db.next_id.load(Ordering::Acquire)); let ancestors = Ancestors::default(); keys.iter().for_each(|key| { - assert!(db.load_without_fixed_root(&ancestors, key).is_some()); + assert!(db.do_load_for_tests(&ancestors, key).is_some()); }); } @@ -2625,7 +2591,7 @@ fn test_reuse_storage_id() { }); let ancestors = Ancestors::default(); keys.iter().for_each(|key| { - assert!(db.load_without_fixed_root(&ancestors, key).is_some()); + assert!(db.do_load_for_tests(&ancestors, key).is_some()); }); } @@ -2646,7 +2612,7 @@ fn test_zero_lamport_new_root_not_cleaned() { // Should still be able to find zero lamport account in slot 1 assert_eq!( - db.load_without_fixed_root(&Ancestors::default(), &account_key), + db.do_load_for_tests(&Ancestors::default(), &account_key), Some((zero_lamport_account, 1)) ); } @@ -2660,26 +2626,23 @@ fn test_store_load_cached() { db.store_for_tests((slot, &[(&key, &account0)][..])); // Load with no ancestors and no root will return nothing - assert!( - db.load_without_fixed_root(&Ancestors::default(), &key) - .is_none() - ); + assert!(db.do_load_for_tests(&Ancestors::default(), &key).is_none()); // Load with ancestors not equal to `slot` will return nothing let ancestors = Ancestors::from(vec![slot + 1]); - assert!(db.load_without_fixed_root(&ancestors, &key).is_none()); + assert!(db.do_load_for_tests(&ancestors, &key).is_none()); // Load with ancestors equal to `slot` will return the account let ancestors = Ancestors::from(vec![slot]); assert_eq!( - db.load_without_fixed_root(&ancestors, &key), + db.do_load_for_tests(&ancestors, &key), Some((account0.clone(), slot)) ); // Adding root will return the account even without ancestors db.add_root(slot); assert_eq!( - db.load_without_fixed_root(&Ancestors::default(), &key), + db.do_load_for_tests(&Ancestors::default(), &key), Some((account0, slot)) ); } @@ -2698,7 +2661,7 @@ fn test_store_flush_load_cached() { db.flush_accounts_cache(true, None); let ancestors = Ancestors::from(vec![slot]); assert_eq!( - db.load_without_fixed_root(&ancestors, &key), + db.do_load_for_tests(&ancestors, &key), Some((account0.clone(), slot)) ); @@ -2706,7 +2669,7 @@ fn test_store_flush_load_cached() { db.add_root(slot); db.flush_accounts_cache(true, None); assert_eq!( - db.load_without_fixed_root(&Ancestors::default(), &key), + db.do_load_for_tests(&Ancestors::default(), &key), Some((account0, slot)) ); } @@ -2734,25 +2697,22 @@ fn test_flush_accounts_cache() { // Unrooted slot should be able to be fetched before the flush let ancestors = Ancestors::from(vec![unrooted_slot]); assert_eq!( - db.load_without_fixed_root(&ancestors, &unrooted_key), + db.do_load_for_tests(&ancestors, &unrooted_key), Some((account0.clone(), unrooted_slot)) ); db.flush_accounts_cache(true, None); // After the flush, the unrooted slot is still in the cache - assert!( - db.load_without_fixed_root(&ancestors, &unrooted_key) - .is_some() - ); + assert!(db.do_load_for_tests(&ancestors, &unrooted_key).is_some()); assert!(db.contains(&unrooted_key)); assert_eq!(db.accounts_cache.num_slots(), 1); assert!(db.accounts_cache.slot_cache(unrooted_slot).is_some()); assert_eq!( - db.load_without_fixed_root(&Ancestors::default(), &key5), + db.do_load_for_tests(&Ancestors::default(), &key5), Some((account0.clone(), root5)) ); assert_eq!( - db.load_without_fixed_root(&Ancestors::default(), &key6), + db.do_load_for_tests(&Ancestors::default(), &key6), Some((account0, root6)) ); } @@ -2827,7 +2787,7 @@ fn run_test_flush_accounts_cache_if_needed(num_roots: usize, num_unrooted: usize Ancestors::from(vec![slot]) }; assert_eq!( - db.load_without_fixed_root(&ancestors, &key), + db.do_load_for_tests(&ancestors, &key), Some((account0.clone(), slot)) ); } @@ -3461,11 +3421,7 @@ fn test_accounts_db_cache_clean_dead_slots() { // an older ancestor set let ancestors = Ancestors::from(vec![last_dead_slot]); for key in &keys { - assert!( - accounts_db - .load_without_fixed_root(&ancestors, key) - .is_some() - ); + assert!(accounts_db.do_load_for_tests(&ancestors, key).is_some()); } // If no `max_clean_root` is specified, cleaning should purge all flushed slots @@ -3481,11 +3437,7 @@ fn test_accounts_db_cache_clean_dead_slots() { // Dead slots have been purged, so these keys should not be findable in the database. for key in &keys { - assert!( - accounts_db - .load_without_fixed_root(&ancestors, key) - .is_none() - ); + assert!(accounts_db.do_load_for_tests(&ancestors, key).is_none()); } // Each slot should only have one entry in the storage, since all other accounts were // cleaned due to later updates @@ -6285,7 +6237,7 @@ fn test_write_accounts_to_cache_scenarios( ); // Verify results - let loaded = db.load_without_fixed_root(&ancestors, &key); + let loaded = db.do_load_for_tests(&ancestors, &key); match expected_lamports { Some(expected) => { assert!(loaded.is_some(), "Account should be loadable"); @@ -6454,7 +6406,7 @@ fn test_load_does_not_return_data_from_non_ancestor_root() { // Ancestors = {17, 19}: min_slot = 17. Slot 18 is rooted but not an // ancestor, so it must be excluded. Slot 16 <= 17, so it is returned. let ancestors = Ancestors::from(vec![17, 19]); - let (account, slot) = db.load_without_fixed_root(&ancestors, &pubkey).unwrap(); + let (account, slot) = db.do_load_for_tests(&ancestors, &pubkey).unwrap(); assert_eq!( slot, 16, From 513752b8faed3636f577639783a581a0b26737f9 Mon Sep 17 00:00:00 2001 From: Brennan Date: Thu, 21 May 2026 03:07:48 -0700 Subject: [PATCH 019/576] Dynamic shred limits (#12541) * dynamic shred limits * comments --- core/src/shred_fetch_stage.rs | 2 +- ledger/src/shred/filter.rs | 273 ++++++++++++++++++++++++---------- 2 files changed, 195 insertions(+), 80 deletions(-) diff --git a/core/src/shred_fetch_stage.rs b/core/src/shred_fetch_stage.rs index 9b71217bfdf..780463e5a37 100644 --- a/core/src/shred_fetch_stage.rs +++ b/core/src/shred_fetch_stage.rs @@ -63,7 +63,7 @@ impl ShredFetchStage { let mut shred_filter_ctx = ShredFilterContext::new_with_turbine_mode( sharable_banks.root(), shred_version, - turbine_mode, + Some(turbine_mode), ); let repair_keypair = repair_context.map(RepairContext::keypair); diff --git a/ledger/src/shred/filter.rs b/ledger/src/shred/filter.rs index 1ec10a07cf9..a78b3ad4dc3 100644 --- a/ledger/src/shred/filter.rs +++ b/ledger/src/shred/filter.rs @@ -3,9 +3,8 @@ use { super::{ - DATA_SHREDS_PER_FEC_BLOCK, Error, MAX_CODE_SHREDS_PER_SLOT, MAX_DATA_SHREDS_PER_SLOT, - Payload, ReedSolomonCache, Shred, ShredFetchStats, ShredFlags, ShredType, ShredVariant, - layout, merkle, + DATA_SHREDS_PER_FEC_BLOCK, Error, Payload, ReedSolomonCache, Shred, ShredFetchStats, + ShredFlags, ShredType, ShredVariant, layout, merkle, }, crate::blockstore, agave_feature_set::discard_unexpected_data_complete_shreds, @@ -25,6 +24,44 @@ use { }, }; +/// Per-slot shred index limits used by bank-aware shred filtering. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct ShredLimits { + /// Exclusive upper bound for data shred indexes in a slot. + max_data_shreds_per_slot: u32, + /// Exclusive upper bound for code shred indexes in a slot. + max_code_shreds_per_slot: u32, +} + +impl ShredLimits { + /// Slot-independent maximum cap used by tests. + #[cfg(test)] + const DEFAULT: Self = Self { + max_data_shreds_per_slot: super::MAX_DATA_SHREDS_PER_SLOT as u32, + max_code_shreds_per_slot: super::MAX_CODE_SHREDS_PER_SLOT as u32, + }; + + /// Creates shred limits with exclusive upper bounds for data and code indexes. + const fn new(max_data_shreds_per_slot: u32, max_code_shreds_per_slot: u32) -> Self { + Self { + max_data_shreds_per_slot, + max_code_shreds_per_slot, + } + } + + /// Returns true if `index` is below the data shred limit. + #[inline] + const fn is_data_index_in_bounds(self, index: u32) -> bool { + index < self.max_data_shreds_per_slot + } + + /// Returns true if `index` is below the code shred limit. + #[inline] + const fn is_code_index_in_bounds(self, index: u32) -> bool { + index < self.max_code_shreds_per_slot + } +} + /// Controls turbine and repair behavior for testing network partitions. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] #[repr(u8)] @@ -90,6 +127,36 @@ impl Default for TurbineMode { } } +/// Bank-backed shred limit lookup shared by fetch and recovery paths. +struct ShredLimitContext { + root_bank: Arc, + #[cfg(test)] + shred_limits_override: Option, +} + +impl ShredLimitContext { + fn new(root_bank: Arc) -> Self { + Self { + root_bank, + #[cfg(test)] + shred_limits_override: None, + } + } + + /// Returns the shred limits for `slot` as derived from the current root bank. + fn shred_limits(&self, slot: Slot) -> ShredLimits { + #[cfg(test)] + if let Some(shred_limits) = self.shred_limits_override { + return shred_limits; + } + + ShredLimits::new( + self.root_bank.max_data_shreds_per_slot_for_slot(slot), + self.root_bank.max_code_shreds_per_slot_for_slot(slot), + ) + } +} + pub struct ShredFilterContext { last_updated: Instant, @@ -111,60 +178,20 @@ pub struct ShredFilterContext { // but specify DATA_COMPLETE discard_unexpected_data_complete_shreds_feature_slot: Option, - // The shred limits per slot. At the moment these are hard coded - // In the future we could replace these with feature activation - // slots to case on what the limits should be - max_data_shreds_per_slot: u32, - max_code_shreds_per_slot: u32, + // The shred limits per slot, derived from the root bank for each shred slot. + shred_limit_context: ShredLimitContext, pub stats: ShredFetchStats, } impl ShredFilterContext { pub fn new(root_bank: Arc, shred_version: u16) -> Self { - Self::new_with_custom_shred_limits_and_turbine_mode( - root_bank, - shred_version, - MAX_DATA_SHREDS_PER_SLOT as u32, - MAX_CODE_SHREDS_PER_SLOT as u32, - None, - ) + Self::new_with_turbine_mode(root_bank, shred_version, None) } pub fn new_with_turbine_mode( root_bank: Arc, shred_version: u16, - turbine_mode: TurbineMode, - ) -> Self { - Self::new_with_custom_shred_limits_and_turbine_mode( - root_bank, - shred_version, - MAX_DATA_SHREDS_PER_SLOT as u32, - MAX_CODE_SHREDS_PER_SLOT as u32, - Some(turbine_mode), - ) - } - - pub fn new_with_custom_shred_limits( - root_bank: Arc, - shred_version: u16, - max_data_shreds_per_slot: u32, - max_code_shreds_per_slot: u32, - ) -> Self { - Self::new_with_custom_shred_limits_and_turbine_mode( - root_bank, - shred_version, - max_data_shreds_per_slot, - max_code_shreds_per_slot, - None, - ) - } - - fn new_with_custom_shred_limits_and_turbine_mode( - root_bank: Arc, - shred_version: u16, - max_data_shreds_per_slot: u32, - max_code_shreds_per_slot: u32, turbine_mode: Option, ) -> Self { let root = root_bank.slot(); @@ -186,8 +213,7 @@ impl ShredFilterContext { turbine_mode, cached_turbine_mode, discard_unexpected_data_complete_shreds_feature_slot, - max_data_shreds_per_slot, - max_code_shreds_per_slot, + shred_limit_context: ShredLimitContext::new(root_bank), stats: ShredFetchStats::default(), } } @@ -211,12 +237,19 @@ impl ShredFilterContext { self.discard_unexpected_data_complete_shreds_feature_slot = root_bank .feature_set .activated_slot(&discard_unexpected_data_complete_shreds::id()); - - // In the future as shred limit changes we can update self.max_*_shreds_per_slot based on root - // bank as well + self.shred_limit_context = ShredLimitContext::new(root_bank); } } + fn shred_limits(&self, slot: Slot) -> ShredLimits { + self.shred_limit_context.shred_limits(slot) + } + + #[cfg(test)] + fn set_shred_limits_for_tests(&mut self, shred_limits: ShredLimits) { + self.shred_limit_context.shred_limits_override = Some(shred_limits); + } + pub fn maybe_submit_stats(&mut self, metric_name: &'static str, cadence: Duration) -> bool { self.stats.maybe_submit(metric_name, cadence) } @@ -279,10 +312,11 @@ impl ShredFilterContext { self.stats.fec_set_index_bad_deserialize += 1; return true; }; + let shred_limits = self.shred_limits(slot); match ShredType::from(shred_variant) { ShredType::Code => { - if index >= self.max_code_shreds_per_slot { + if !shred_limits.is_code_index_in_bounds(index) { self.stats.index_out_of_bounds += 1; return true; } @@ -302,7 +336,7 @@ impl ShredFilterContext { } } ShredType::Data => { - if index >= self.max_data_shreds_per_slot { + if !shred_limits.is_data_index_in_bounds(index) { self.stats.index_out_of_bounds += 1; return true; } @@ -484,6 +518,7 @@ impl ShredRecoveryContext { ) -> Result<(), Error> { let shreds = shreds .into_iter() + .filter(|shred| !self.shred_filter_ctx.should_discard_shred(shred.payload())) .map(|shred| { debug_assert_matches!( shred.common_header().shred_variant, @@ -552,7 +587,10 @@ mod tests { super::{Shred, make_merkle_shreds_for_tests}, *, }, - crate::{genesis_utils::create_genesis_config, shred::tests::*}, + crate::{ + genesis_utils::create_genesis_config, + shred::{MAX_CODE_SHREDS_PER_SLOT, tests::*}, + }, assert_matches::assert_matches, itertools::Itertools, solana_leader_schedule::SlotLeader, @@ -736,6 +774,61 @@ mod tests { } } + #[test] + fn test_recovery_shred_limits() { + agave_logger::setup(); + let mut rng = rand::rng(); + let slot = 200; + let shreds = make_merkle_shreds_for_tests( + &mut rng, + slot, + 1200 * 5, // data_size + false, // is_last_in_slot + ) + .unwrap(); + let shreds: Vec<_> = shreds.into_iter().map(Shred::from).collect(); + let coding_shreds: Vec<_> = shreds + .into_iter() + .filter(|shred| shred.shred_type() == ShredType::Code) + .collect(); + let shred_version = coding_shreds[0].common_header().version; + + // Feed recovery only coding shreds. Without the custom limit below, this + // is enough parity to recover the missing data shreds. + let max_code_shreds_per_slot = coding_shreds[0].index(); + let (dummy_retransmit_sender, _) = EvictingSender::new_bounded(0); + let mut shred_recovery_context = ShredRecoveryContext::new( + ReedSolomonCache::default(), + dummy_retransmit_sender, + new_test_bank(0, false), + shred_version, + ); + shred_recovery_context + .shred_filter_ctx + .set_shred_limits_for_tests(ShredLimits::new( + ShredLimits::DEFAULT.max_data_shreds_per_slot, + // Setting the limit to the first coding index makes every + // coding shred in this batch invalid. + max_code_shreds_per_slot, + )); + let mut recovered_shreds = Vec::new(); + let mut recovered_data_shreds = Vec::new(); + + // Recovery sees insufficient parity shreds and cannot recover the FEC. + assert_matches!( + shred_recovery_context.recover( + coding_shreds, + &mut recovered_shreds, + &mut recovered_data_shreds, + ), + Err(Error::Erasure( + reed_solomon_erasure::Error::TooFewParityShards + )) + ); + assert!(recovered_shreds.is_empty()); + assert!(recovered_data_shreds.is_empty()); + } + #[test] fn test_should_discard_packet_with_turbine_mode() { agave_logger::setup(); @@ -757,7 +850,7 @@ mod tests { let mut shred_filter_context = ShredFilterContext::new_with_turbine_mode( root_bank.clone(), shred_version, - turbine_mode.clone(), + Some(turbine_mode.clone()), ); assert!(shred_filter_context.should_discard_packet(&packet)); @@ -901,34 +994,56 @@ mod tests { #[test] fn test_should_discard_shred_with_custom_shred_limits() { agave_logger::setup(); - let mut rng = rand::rng(); + let slot = 200; + let shreds = make_merkle_shreds_for_tests( + &mut rng, + slot, + 1200 * 5, // data_size + false, // is_last_in_slot + ) + .unwrap(); + let shreds: Vec<_> = shreds.into_iter().map(Shred::from).collect(); + let shred_version = shreds[0].common_header().version; let root_bank = new_test_bank(0, false); - let slot = 42; - let shreds = make_merkle_shreds_for_tests(&mut rng, slot, 10_000, false).unwrap(); - let shred = Shred::from(shreds[0].clone()); - let index = shred.common_header().index; - let shred_version = shred.common_header().version; - let mut packet = Packet::default(); - shred.copy_to_packet(&mut packet); - - let mut shred_filter_context = ShredFilterContext::new_with_custom_shred_limits( - root_bank.clone(), - shred_version, - index + 1, - index + 1, - ); - assert!(!shred_filter_context.should_discard_packet(&packet)); + for shred_type in [ShredType::Data, ShredType::Code] { + let shred = shreds + .iter() + .find(|shred| shred.shred_type() == shred_type) + .unwrap(); + let index = shred.index(); + let mut shred_filter_context = + ShredFilterContext::new(root_bank.clone(), shred_version); + match shred_type { + ShredType::Data => shred_filter_context.set_shred_limits_for_tests( + ShredLimits::new(index + 1, ShredLimits::DEFAULT.max_code_shreds_per_slot), + ), + ShredType::Code => shred_filter_context.set_shred_limits_for_tests( + ShredLimits::new(ShredLimits::DEFAULT.max_data_shreds_per_slot, index + 1), + ), + } + assert!( + !shred_filter_context.should_discard_shred(shred.payload()), + "{shred_type:?} shred should be within the custom limit" + ); - let mut shred_filter_context = ShredFilterContext::new_with_custom_shred_limits( - root_bank, - shred_version, - index, - index, - ); - assert!(shred_filter_context.should_discard_packet(&packet)); - assert_eq!(shred_filter_context.stats.index_out_of_bounds, 1); + let mut shred_filter_context = + ShredFilterContext::new(root_bank.clone(), shred_version); + match shred_type { + ShredType::Data => shred_filter_context.set_shred_limits_for_tests( + ShredLimits::new(index, ShredLimits::DEFAULT.max_code_shreds_per_slot), + ), + ShredType::Code => shred_filter_context.set_shred_limits_for_tests( + ShredLimits::new(ShredLimits::DEFAULT.max_data_shreds_per_slot, index), + ), + } + assert!( + shred_filter_context.should_discard_shred(shred.payload()), + "{shred_type:?} shred should be discarded at the custom limit" + ); + assert_eq!(shred_filter_context.stats.index_out_of_bounds, 1); + } } #[test] From 557f2eb4d7ba37f8757357a843ba3508f4c2d70e Mon Sep 17 00:00:00 2001 From: Yihau Chen Date: Thu, 21 May 2026 19:11:49 +0800 Subject: [PATCH 020/576] ci: only install cargo-build-sbf in scripts/cargo-install-all.sh (#12647) fix cargo-install-all.sh --- scripts/cargo-install-all.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/cargo-install-all.sh b/scripts/cargo-install-all.sh index f348f7e7590..45f0a042c0b 100755 --- a/scripts/cargo-install-all.sh +++ b/scripts/cargo-install-all.sh @@ -253,9 +253,8 @@ if [[ -z "$noBuildPlatformTools" ]]; then source "$SOLANA_ROOT"/scripts/cargo-build-sbf-version.sh # shellcheck disable=SC2086 + # starting from cargo-build-sbf v4.1.0, `cargo install cargo-build-sbf` installs both `cargo-build-sbf` and `cargo-test-sbf` "$cargo" $maybeRustVersion install --locked cargo-build-sbf --root "$installDir" $maybeCargoBuildSbfVersionArg - # shellcheck disable=SC2086 - "$cargo" $maybeRustVersion install --locked cargo-test-sbf --root "$installDir" $maybeCargoTestSbfVersionArg fi ( From 7a3c1a6bf17619022e44cce32be9c307a11f9c78 Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Thu, 21 May 2026 08:34:53 -0400 Subject: [PATCH 021/576] votor: do not block on SetRoot (#12620) --- core/src/block_creation_loop.rs | 6 +- core/src/replay_stage.rs | 68 +++++++----- runtime/src/bank_forks_controller.rs | 157 +++++++++++++++++++-------- votor/src/event_handler.rs | 36 +++--- votor/src/root_utils.rs | 11 +- 5 files changed, 178 insertions(+), 100 deletions(-) diff --git a/core/src/block_creation_loop.rs b/core/src/block_creation_loop.rs index e79e00b173e..1e6bb588ca4 100644 --- a/core/src/block_creation_loop.rs +++ b/core/src/block_creation_loop.rs @@ -1415,19 +1415,17 @@ mod tests { Ok(self.bank_forks.write().unwrap().insert(bank)) } - fn set_root( + fn enqueue_set_root( &self, - _my_pubkey: Pubkey, _parent_slot: Slot, new_root: Slot, highest_super_majority_root: Option, - ) -> Result<(), BankForksControllerError> { + ) { // Test code only so we allow writing bank forks directly. self.bank_forks .write() .unwrap() .set_root(new_root, None, highest_super_majority_root); - Ok(()) } fn clear_bank(&self, slot: Slot) -> Result<(), BankForksControllerError> { diff --git a/core/src/replay_stage.rs b/core/src/replay_stage.rs index f552a192ce8..78d08f92de7 100644 --- a/core/src/replay_stage.rs +++ b/core/src/replay_stage.rs @@ -84,7 +84,7 @@ use { solana_runtime::{ bank::{Bank, NewBankOptions, bank_hash_details}, bank_forks::BankForks, - bank_forks_controller::{BankForksCommand, BankForksCommandReceiver}, + bank_forks_controller::{BankForksCommand, BankForksCommandReceiver, SetRootCommand}, block_component_processor::BlockComponentProcessorError, commitment::BlockCommitmentCache, installed_scheduler_pool::BankWithScheduler, @@ -939,6 +939,7 @@ impl ReplayStage { Self::process_bank_forks_commands( &bank_forks_controller_receiver, &process_bank_forks_context, + &my_pubkey, &mut progress, &mut async_verification_freelist, ), @@ -1515,12 +1516,15 @@ impl ReplayStage { // only wait for the signal if we did not just process a bank; maybe there are more slots available let timer = Duration::from_millis(100); + let bank_forks_command_receiver = bank_forks_controller_receiver.receiver(); + let set_root_signal_receiver = + bank_forks_controller_receiver.set_root_signal_receiver(); select! { recv(ledger_signal_receiver) -> result => match result { Err(_) => break, Ok(_) => trace!("blockstore signal"), }, - recv(bank_forks_controller_receiver.receiver()) -> result => match result { + recv(bank_forks_command_receiver) -> result => match result { Err(_) => break, Ok(command) => { Self::process_bank_forks_command( @@ -1531,6 +1535,10 @@ impl ReplayStage { ); } }, + recv(set_root_signal_receiver) -> result => match result { + Err(_) => break, + Ok(()) => trace!("bank forks set-root signal"), + }, default(timer) => (), } } @@ -5006,9 +5014,14 @@ impl ReplayStage { fn process_bank_forks_commands( bank_forks_controller_receiver: &BankForksCommandReceiver, context: &ProcessBankForksContext, + my_pubkey: &Pubkey, progress: &mut ProgressMap, async_verification_freelist: &mut Vec, ) -> Result<(), TryRecvError> { + if let Some(command) = bank_forks_controller_receiver.take_set_root_command() { + Self::process_set_root_command(command, context, my_pubkey); + } + loop { let command = bank_forks_controller_receiver.receiver().try_recv()?; Self::process_bank_forks_command( @@ -5020,6 +5033,32 @@ impl ReplayStage { } } + fn process_set_root_command( + command: SetRootCommand, + context: &ProcessBankForksContext, + my_pubkey: &Pubkey, + ) { + let SetRootCommand { + parent_slot, + new_root, + highest_super_majority_root, + } = command; + root_utils::check_and_handle_new_root( + parent_slot, + new_root, + context.snapshot_controller.as_deref(), + highest_super_majority_root, + &context.bank_notification_sender, + &context.drop_bank_sender, + &context.blockstore, + &context.leader_schedule_cache, + &context.bank_forks, + context.rpc_subscriptions.as_deref(), + my_pubkey, + |_| {}, + ); + } + /// Process a bank forks command fn process_bank_forks_command( command: BankForksCommand, @@ -5056,31 +5095,6 @@ impl ReplayStage { warn!("bank forks controller insert-bank response receiver dropped") }); } - BankForksCommand::SetRoot { - my_pubkey, - parent_slot, - new_root, - highest_super_majority_root, - response_sender, - } => { - root_utils::check_and_handle_new_root( - parent_slot, - new_root, - context.snapshot_controller.as_deref(), - highest_super_majority_root, - &context.bank_notification_sender, - &context.drop_bank_sender, - &context.blockstore, - &context.leader_schedule_cache, - &context.bank_forks, - context.rpc_subscriptions.as_deref(), - &my_pubkey, - |_| {}, - ); - response_sender.send(()).unwrap_or_else(|_| { - warn!("bank forks controller set-root response receiver dropped") - }); - } BankForksCommand::ClearBank { slot, response_sender, diff --git a/runtime/src/bank_forks_controller.rs b/runtime/src/bank_forks_controller.rs index 8f099f07b6d..d8495a09a16 100644 --- a/runtime/src/bank_forks_controller.rs +++ b/runtime/src/bank_forks_controller.rs @@ -4,9 +4,9 @@ use { log::warn, solana_clock::Slot, solana_metrics::datapoint_info, - solana_pubkey::Pubkey, std::{ fmt, + sync::{Arc, Mutex}, time::{Duration, Instant}, }, thiserror::Error, @@ -27,24 +27,22 @@ pub enum BankForksCommand { bank: Box, response_sender: Sender>, }, - SetRoot { - my_pubkey: Pubkey, - parent_slot: Slot, - new_root: Slot, - highest_super_majority_root: Option, - response_sender: Sender<()>, - }, ClearBank { slot: Slot, response_sender: Sender<()>, }, } +pub struct SetRootCommand { + pub parent_slot: Slot, + pub new_root: Slot, + pub highest_super_majority_root: Option, +} + impl BankForksCommand { fn metric_slot(&self) -> Slot { match self { Self::InsertBank { bank, .. } => bank.slot(), - Self::SetRoot { new_root, .. } => *new_root, Self::ClearBank { slot, .. } => *slot, } } @@ -54,7 +52,6 @@ impl fmt::Display for BankForksCommand { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::InsertBank { .. } => write!(f, "insert_bank"), - Self::SetRoot { .. } => write!(f, "set_root"), Self::ClearBank { .. } => write!(f, "clear_bank"), } } @@ -63,13 +60,12 @@ impl fmt::Display for BankForksCommand { pub trait BankForksController: Send + Sync { fn insert_bank(&self, bank: Bank) -> Result; - fn set_root( + fn enqueue_set_root( &self, - my_pubkey: Pubkey, parent_slot: Slot, new_root: Slot, highest_super_majority_root: Option, - ) -> Result<(), BankForksControllerError>; + ); fn clear_bank(&self, slot: Slot) -> Result<(), BankForksControllerError>; } @@ -78,12 +74,27 @@ pub trait BankForksController: Send + Sync { #[derive(Clone)] pub struct BankForksControllerHandle { sender: Sender, + pending_set_root: Arc>>, + set_root_signal_sender: Sender<()>, } impl BankForksControllerHandle { pub fn new() -> (Self, BankForksCommandReceiver) { let (sender, receiver) = bounded(CHANNEL_SIZE); - (Self { sender }, BankForksCommandReceiver { receiver }) + let (set_root_signal_sender, set_root_signal_receiver) = bounded(1); + let pending_set_root = Arc::new(Mutex::new(None)); + ( + Self { + sender, + pending_set_root: pending_set_root.clone(), + set_root_signal_sender, + }, + BankForksCommandReceiver { + receiver, + pending_set_root, + set_root_signal_receiver, + }, + ) } fn send_command( @@ -146,24 +157,39 @@ impl BankForksController for BankForksControllerHandle { bank.ok_or(BankForksControllerError::UnableToInsertStaleBank(slot)) } - fn set_root( + fn enqueue_set_root( &self, - my_pubkey: Pubkey, parent_slot: Slot, new_root: Slot, highest_super_majority_root: Option, - ) -> Result<(), BankForksControllerError> { - let (response_sender, response_receiver) = bounded(1); - self.send_command( - BankForksCommand::SetRoot { - my_pubkey, - parent_slot, - new_root, - highest_super_majority_root, - response_sender, - }, - response_receiver, - ) + ) { + let total_start = Instant::now(); + let command = SetRootCommand { + parent_slot, + new_root, + highest_super_majority_root, + }; + + { + let mut pending_set_root = self.pending_set_root.lock().unwrap(); + // Replay only needs to process the highest pending root. + if pending_set_root + .as_ref() + .is_none_or(|pending| command.new_root > pending.new_root) + { + *pending_set_root = Some(command); + } + } + let _ = self.set_root_signal_sender.try_send(()); + + let total_us = total_start.elapsed().as_micros() as i64; + + datapoint_info!( + "bank_forks_controller-command", + ("command", "set_root", String), + ("slot", new_root as i64, i64), + ("total_us", total_us, i64), + ); } fn clear_bank(&self, slot: Slot) -> Result<(), BankForksControllerError> { @@ -180,12 +206,22 @@ impl BankForksController for BankForksControllerHandle { pub struct BankForksCommandReceiver { receiver: Receiver, + pending_set_root: Arc>>, + set_root_signal_receiver: Receiver<()>, } impl BankForksCommandReceiver { pub fn receiver(&self) -> &Receiver { &self.receiver } + + pub fn set_root_signal_receiver(&self) -> &Receiver<()> { + &self.set_root_signal_receiver + } + + pub fn take_set_root_command(&self) -> Option { + self.pending_set_root.lock().unwrap().take() + } } #[cfg(test)] @@ -193,19 +229,64 @@ mod tests { use { super::*, crate::{bank::SlotLeader, bank_forks::BankForks, genesis_utils::create_genesis_config}, - std::thread, + std::{thread, time::Duration}, }; + #[test] + fn test_bank_forks_controller_keeps_highest_pending_set_root() { + let (controller, receiver) = BankForksControllerHandle::new(); + + controller.enqueue_set_root(5, 5, Some(5)); + controller.enqueue_set_root(3, 3, Some(3)); + assert_eq!(receiver.take_set_root_command().unwrap().new_root, 5); + assert!(receiver.take_set_root_command().is_none()); + + controller.enqueue_set_root(3, 3, Some(3)); + controller.enqueue_set_root(5, 5, Some(5)); + assert_eq!(receiver.take_set_root_command().unwrap().new_root, 5); + } + + #[test] + fn test_bank_forks_controller_signals_pending_set_root() { + let (controller, receiver) = BankForksControllerHandle::new(); + + controller.enqueue_set_root(1, 1, Some(1)); + receiver + .set_root_signal_receiver() + .recv_timeout(Duration::from_secs(1)) + .unwrap(); + assert_eq!(receiver.take_set_root_command().unwrap().new_root, 1); + + controller.enqueue_set_root(2, 2, Some(2)); + controller.enqueue_set_root(3, 3, Some(3)); + receiver + .set_root_signal_receiver() + .recv_timeout(Duration::from_secs(1)) + .unwrap(); + assert!(receiver.set_root_signal_receiver().try_recv().is_err()); + assert_eq!(receiver.take_set_root_command().unwrap().new_root, 3); + } + #[test] fn test_bank_forks_controller_insert_and_set_root() { let genesis = create_genesis_config(10_000); let bank_forks = BankForks::new_rw_arc(Bank::new_for_tests(&genesis.genesis_config)); let (controller, receiver) = BankForksControllerHandle::new(); let replay_bank_forks = bank_forks.clone(); + let (root_sender, root_receiver) = bounded(1); let replay_thread = thread::spawn(move || { loop { - let Ok(command) = receiver.receiver().recv() else { - break; + if let Some(command) = receiver.take_set_root_command() { + { + let mut bank_forks = replay_bank_forks.write().unwrap(); + bank_forks.set_root(command.new_root, None, None); + } + root_sender.send(command.new_root).unwrap(); + } + let command = match receiver.receiver().recv_timeout(Duration::from_millis(10)) { + Ok(command) => command, + Err(RecvTimeoutError::Timeout) => continue, + Err(RecvTimeoutError::Disconnected) => break, }; match command { BankForksCommand::InsertBank { @@ -218,17 +299,6 @@ mod tests { }; response_sender.send(Some(bank)).unwrap(); } - BankForksCommand::SetRoot { - new_root, - response_sender, - .. - } => { - { - let mut bank_forks = replay_bank_forks.write().unwrap(); - bank_forks.set_root(new_root, None, None); - } - response_sender.send(()).unwrap(); - } BankForksCommand::ClearBank { slot, response_sender, @@ -256,7 +326,8 @@ mod tests { assert_eq!(inserted_bank.slot(), 1); assert!(bank_forks.read().unwrap().get(1).is_some()); - controller.set_root(Pubkey::default(), 1, 1, None).unwrap(); + controller.enqueue_set_root(1, 1, None); + assert_eq!(root_receiver.recv().unwrap(), 1); assert_eq!(bank_forks.read().unwrap().root(), 1); let parent_bank = bank_forks.read().unwrap().root_bank(); diff --git a/votor/src/event_handler.rs b/votor/src/event_handler.rs index 281a1ae8fdb..68fb757d713 100644 --- a/votor/src/event_handler.rs +++ b/votor/src/event_handler.rs @@ -26,7 +26,6 @@ use { solana_pubkey::Pubkey, solana_runtime::{ bank::Bank, - bank_forks_controller::BankForksControllerError, leader_schedule_utils::{ first_of_consecutive_leader_slots, last_of_consecutive_leader_slots, leader_slot_index, }, @@ -77,9 +76,6 @@ enum EventLoopError { #[error("Set identity error")] SetIdentityError(#[from] VoteHistoryError), - - #[error("Bank forks controller error")] - BankForksControllerError(#[from] BankForksControllerError), } pub(crate) struct EventHandler { @@ -172,7 +168,7 @@ impl EventHandler { .saturating_add(receive_event_time.as_us() as u32); let root_bank = vctx.sharable_banks.root(); - if event.should_ignore(root_bank.slot()) { + if event.should_ignore(root_bank.slot().max(vctx.vote_history.root())) { local_context.stats.ignored = local_context.stats.ignored.saturating_add(1); continue; } @@ -310,7 +306,7 @@ impl EventHandler { finalized_blocks, received_shred, stats, - )?; + ); if let Some(parent_block) = Self::add_missing_parent_ready(block, ctx, vctx, local_context) { @@ -446,7 +442,7 @@ impl EventHandler { finalized_blocks, received_shred, stats, - )?; + ); if let Some(slot) = *standstill_slot { if block.0 > slot { @@ -763,10 +759,13 @@ impl EventHandler { // In case we set root in the middle of a leader window, // it's not necessary to vote skip prior to it and we won't // be able to check vote history if we've already voted on it - let root_bank = voting_context.sharable_banks.root(); + let root_slot = voting_context + .vote_history + .root() + .max(voting_context.sharable_banks.root().slot()); // No matter what happens, we should not vote skip for slot 0 let start = first_of_consecutive_leader_slots(slot) - .max(root_bank.slot()) + .max(root_slot) .max(1); for s in start..=last_of_consecutive_leader_slots(slot) { if voting_context.vote_history.voted(s) { @@ -819,9 +818,9 @@ impl EventHandler { finalized_blocks: &mut BTreeSet, received_shred: &mut BTreeSet, stats: &mut EventHandlerStats, - ) -> Result<(), BankForksControllerError> { + ) { let bank_forks_r = ctx.bank_forks.read().unwrap(); - let old_root = bank_forks_r.root(); + let old_root = bank_forks_r.root().max(vctx.vote_history.root()); let Some(new_root) = finalized_blocks .iter() .filter_map(|&(slot, block_id)| { @@ -835,7 +834,7 @@ impl EventHandler { .max() else { // No rootable banks - return Ok(()); + return; }; drop(bank_forks_r); root_utils::set_root( @@ -847,9 +846,8 @@ impl EventHandler { pending_blocks, finalized_blocks, received_shred, - )?; + ); stats.set_root(new_root); - Ok(()) } pub(crate) fn join(self) -> thread::Result<()> { @@ -979,6 +977,7 @@ mod tests { } struct DirectBankForksController { + my_pubkey: Pubkey, bank_forks: Arc>, blockstore: Arc, leader_schedule_cache: Arc, @@ -990,13 +989,12 @@ mod tests { Ok(self.bank_forks.write().unwrap().insert(bank)) } - fn set_root( + fn enqueue_set_root( &self, - my_pubkey: Pubkey, parent_slot: Slot, new_root: Slot, highest_super_majority_root: Option, - ) -> Result<(), BankForksControllerError> { + ) { root_utils::check_and_handle_new_root( parent_slot, new_root, @@ -1008,10 +1006,9 @@ mod tests { &self.leader_schedule_cache, &self.bank_forks, None, - &my_pubkey, + &self.my_pubkey, |_| {}, ); - Ok(()) } fn clear_bank(&self, slot: Slot) -> Result<(), BankForksControllerError> { @@ -1079,6 +1076,7 @@ mod tests { &bank_forks.read().unwrap().root_bank(), )); let bank_forks_controller = Arc::new(DirectBankForksController { + my_pubkey: my_node_keypair.pubkey(), bank_forks: bank_forks.clone(), blockstore: blockstore.clone(), leader_schedule_cache, diff --git a/votor/src/root_utils.rs b/votor/src/root_utils.rs index 80091ffc84a..fc18c8dcffd 100644 --- a/votor/src/root_utils.rs +++ b/votor/src/root_utils.rs @@ -11,10 +11,8 @@ use { rpc_subscriptions::RpcSubscriptions, }, solana_runtime::{ - bank_forks::BankForks, - bank_forks_controller::{BankForksController, BankForksControllerError}, - installed_scheduler_pool::BankWithScheduler, - snapshot_controller::SnapshotController, + bank_forks::BankForks, bank_forks_controller::BankForksController, + installed_scheduler_pool::BankWithScheduler, snapshot_controller::SnapshotController, }, solana_time_utils::timestamp, std::{ @@ -41,7 +39,7 @@ pub(crate) fn set_root( pending_blocks: &mut PendingBlocks, finalized_blocks: &mut BTreeSet, received_shred: &mut BTreeSet, -) -> Result<(), BankForksControllerError> { +) { info!("{my_pubkey}: setting root {new_root}"); vctx.vote_history.set_root(new_root); *pending_blocks = pending_blocks.split_off(&new_root); @@ -49,7 +47,7 @@ pub(crate) fn set_root( *received_shred = received_shred.split_off(&new_root); rctx.bank_forks_controller - .set_root(*my_pubkey, new_root, new_root, Some(new_root))?; + .enqueue_set_root(new_root, new_root, Some(new_root)); // Distinguish between duplicate versions of same slot let hash = ctx.bank_forks.read().unwrap().bank_hash(new_root).unwrap(); @@ -78,7 +76,6 @@ pub(crate) fn set_root( dependency_work, )); } - Ok(()) } /// Sets the new root, additionally performs the callback after setting the bank forks root From a10c1fddcf6039a37215ba0c7c092a5221037937 Mon Sep 17 00:00:00 2001 From: Brennan Date: Thu, 21 May 2026 08:38:53 -0700 Subject: [PATCH 022/576] fix tests (#12659) --- local-cluster/tests/local_cluster.rs | 7 +++--- .../block_component_processor/vote_reward.rs | 25 +++++++++++++------ 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/local-cluster/tests/local_cluster.rs b/local-cluster/tests/local_cluster.rs index 90a692ae730..b416042b7bc 100644 --- a/local-cluster/tests/local_cluster.rs +++ b/local-cluster/tests/local_cluster.rs @@ -1964,12 +1964,14 @@ fn do_test_future_tower(cluster_mode: ClusterMode) { let mut cluster = LocalCluster::new(&mut config, SocketAddrSpace::Unspecified); let val_a_ledger_path = cluster.ledger_path(&validator_a_pubkey); + let root_before_restart; loop { sleep(Duration::from_millis(100)); if let Some(root) = root_in_tower(&val_a_ledger_path, &validator_a_pubkey) { if root >= 15 { + root_before_restart = root; break; } } @@ -1993,7 +1995,7 @@ fn do_test_future_tower(cluster_mode: ClusterMode) { ); let mut newly_rooted = false; - let some_root_after_restart = purged_slot_before_restart + 25; // 25 is arbitrary; just wait a bit + let some_root_after_restart = root_before_restart + 1; for _ in 0..600 { sleep(Duration::from_millis(100)); @@ -6197,8 +6199,7 @@ fn test_alpenglow_migration( let node_stakes = vec![DEFAULT_NODE_STAKE; num_nodes]; // We want the epochs to be as short as possible to reduce test time without being flaky. - // We start the migration at an offset of 32, so use 64 as the epoch length. - let slots_per_epoch = 2 * MINIMUM_SLOTS_PER_EPOCH; + let slots_per_epoch = 4 * MINIMUM_SLOTS_PER_EPOCH; assert!(slots_per_epoch > MIGRATION_SLOT_OFFSET); let mut cluster_config = ClusterConfig { validator_configs: make_identical_validator_configs(&validator_config, num_nodes), diff --git a/runtime/src/block_component_processor/vote_reward.rs b/runtime/src/block_component_processor/vote_reward.rs index 8e31f9f0dc4..7911ea87cca 100644 --- a/runtime/src/block_component_processor/vote_reward.rs +++ b/runtime/src/block_component_processor/vote_reward.rs @@ -588,13 +588,15 @@ mod tests { bank: &Bank, total_stake: u64, stake_voted: u64, - ) -> u64 { + ) -> (u64, u64) { let epoch_inflation = bank.calculate_epoch_inflation_rewards(prev_bank.capitalization(), prev_bank.epoch()); let numerator = epoch_inflation as u128 * stake_voted as u128; let denominator = bank.epoch_schedule.slots_per_epoch as u128 * total_stake as u128; let reward: u64 = (numerator / denominator).try_into().unwrap(); - reward / 2 + let validator_reward = reward / 2; + let leader_reward = reward - validator_reward; + (validator_reward, leader_reward) } #[test] @@ -660,18 +662,25 @@ mod tests { .epoch_stakes_from_slot(reward_slot) .unwrap() .total_stake(); - let expected_validator_reward = + let (expected_validator_reward, expected_leader_reward_per_validator) = calc_reward_for_test(&prev_bank, &bank, total_stake, per_validator_stake); if *validator != leader_vote_pubkey { assert_eq!(got_reward, expected_validator_reward); } - got_reward + ( + got_reward, + expected_validator_reward, + expected_leader_reward_per_validator, + ) }) .collect::>(); - let expected_leader_reward = rewards.last().unwrap() - * validator_pubkeys_to_reward.len() as u64 - + rewards.last().unwrap(); - assert_eq!(expected_leader_reward, rewards[0]); + let (leader_reward, expected_validator_reward, expected_leader_reward_per_validator) = + rewards[0]; + assert_eq!( + expected_validator_reward + + expected_leader_reward_per_validator * validator_pubkeys_to_reward.len() as u64, + leader_reward + ); } #[test] From 19fe89e3e4b8add1a5ffb27598c99a16e86457aa Mon Sep 17 00:00:00 2001 From: Mykola Dzham Date: Thu, 21 May 2026 17:51:39 +0200 Subject: [PATCH 023/576] Drop current channel version test (#12662) It is broken with the new release procedures and does not make sense anymore. --- ci/check-channel-version.sh | 27 --------------------------- ci/test-sanity.sh | 1 - 2 files changed, 28 deletions(-) delete mode 100755 ci/check-channel-version.sh diff --git a/ci/check-channel-version.sh b/ci/check-channel-version.sh deleted file mode 100755 index 6027a9ec73f..00000000000 --- a/ci/check-channel-version.sh +++ /dev/null @@ -1,27 +0,0 @@ - # Ensure the current channel version is not equal ("greater") than -# the version of the latest tag -if [[ -z $CI_TAG ]]; then - echo "--- channel version check" - ( - eval "$(ci/channel-info.sh)" - - if [[ -n $CHANNEL_LATEST_TAG ]]; then - source scripts/read-cargo-variable.sh - - version=$(readCargoVariable version Cargo.toml) - echo "latest channel tag: $CHANNEL_LATEST_TAG" - echo "current version: v$version" - - if [[ $CHANNEL_LATEST_TAG = v$version ]]; then - echo -e "\033[31mError:\033[0m A release has been tagged since your feature branch was last rebased. must be greater than .. - Possible solutions (in the order they should be tried): - 1. rebase your feature branch on the base branch - 2. merge the PR: \"Bump Version to ...\" once it has passed ci/checks, then rebase - 3. ask for help in #devops on discord" - exit 1 - fi - else - echo "Skipped. CHANNEL_LATEST_TAG (CHANNEL=$CHANNEL) unset" - fi - ) -fi diff --git a/ci/test-sanity.sh b/ci/test-sanity.sh index 3a1de04025a..c1ef0a9af63 100755 --- a/ci/test-sanity.sh +++ b/ci/test-sanity.sh @@ -50,7 +50,6 @@ fi git diff "$target" --check --oneline ) -_ ci/check-channel-version.sh _ ci/nits.sh scripts/increment-cargo-version.sh check From 939658af35fbec12b183d0c7b7aadcaf03c9ce48 Mon Sep 17 00:00:00 2001 From: Zach Brown Date: Thu, 21 May 2026 10:16:13 -0700 Subject: [PATCH 024/576] Bump sbpf to 0.21.0 (#12644) --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- dev-bins/Cargo.lock | 4 ++-- dev-bins/Cargo.toml | 2 +- programs/sbf/Cargo.lock | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3be74351852..13d802297b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10150,9 +10150,9 @@ checksum = "dcf09694a0fc14e5ffb18f9b7b7c0f15ecb6eac5b5610bf76a1853459d19daf9" [[package]] name = "solana-sbpf" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1b120fcbbc0d8562997183bef32c76a28400ecae7d8c9fff279d33002783a29" +checksum = "7f84c593fa3d4131045b606dec5acf9d8eac73791bc786ca9911057aec8f43ec" dependencies = [ "byteorder", "combine 3.8.1", diff --git a/Cargo.toml b/Cargo.toml index 56952aadda5..7b8419d2f7e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -460,7 +460,7 @@ solana-rpc-client-types = { path = "rpc-client-types", version = "=4.2.0-alpha.0 solana-runtime = { path = "runtime", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-runtime-transaction = { path = "runtime-transaction", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-sanitize = "3.0.1" -solana-sbpf = { version = "=0.20.0", default-features = false } +solana-sbpf = { version = "=0.21.0", default-features = false } solana-sdk-ids = "3.1.0" solana-secp256k1-program = "3.0.1" solana-secp256k1-recover = "3.1.1" diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 9c230a7036d..6538592f993 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -8696,9 +8696,9 @@ checksum = "dcf09694a0fc14e5ffb18f9b7b7c0f15ecb6eac5b5610bf76a1853459d19daf9" [[package]] name = "solana-sbpf" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1b120fcbbc0d8562997183bef32c76a28400ecae7d8c9fff279d33002783a29" +checksum = "7f84c593fa3d4131045b606dec5acf9d8eac73791bc786ca9911057aec8f43ec" dependencies = [ "byteorder", "combine 3.8.1", diff --git a/dev-bins/Cargo.toml b/dev-bins/Cargo.toml index 73978714589..98acfaea738 100644 --- a/dev-bins/Cargo.toml +++ b/dev-bins/Cargo.toml @@ -120,7 +120,7 @@ solana-rpc-client = { path = "../rpc-client", version = "=4.2.0-alpha.0", defaul solana-rpc-client-api = { path = "../rpc-client-api", version = "=4.2.0-alpha.0" } solana-runtime = { path = "../runtime", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-runtime-transaction = { path = "../runtime-transaction", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-sbpf = { version = "=0.20.0", default-features = false } +solana-sbpf = { version = "=0.21.0", default-features = false } solana-sdk-ids = "3.1.0" solana-shred-version = "3.0.1" solana-signature = { version = "3.4.0", default-features = false } diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index 59e8cc9f761..9c23394de49 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -9049,9 +9049,9 @@ dependencies = [ [[package]] name = "solana-sbpf" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1b120fcbbc0d8562997183bef32c76a28400ecae7d8c9fff279d33002783a29" +checksum = "7f84c593fa3d4131045b606dec5acf9d8eac73791bc786ca9911057aec8f43ec" dependencies = [ "byteorder", "combine 3.8.1", From f79d28ccaeca7ee16c658b92da9de9bae7fb2b1c Mon Sep 17 00:00:00 2001 From: Rory Harris Date: Thu, 21 May 2026 10:41:23 -0700 Subject: [PATCH 025/576] Split store_accounts_frozen into store_accounts_shrink and flush (#12610) * Split store_accounts_frozen into store_accounts_shrink and flush * Updated naming to add for --- accounts-db/src/accounts_db.rs | 101 ++++++++++++++++--------- accounts-db/src/accounts_db/tests.rs | 5 +- accounts-db/src/ancient_append_vecs.rs | 11 +-- runtime/src/snapshot_minimizer.rs | 2 +- 4 files changed, 75 insertions(+), 44 deletions(-) diff --git a/accounts-db/src/accounts_db.rs b/accounts-db/src/accounts_db.rs index 81f1f5b6a09..5d3747a3e86 100644 --- a/accounts-db/src/accounts_db.rs +++ b/accounts-db/src/accounts_db.rs @@ -2888,7 +2888,7 @@ impl AccountsDb { // mutating rooted slots; There should be no writers to them. let accounts = [(slot, &shrink_collect.alive_accounts.alive_accounts()[..])]; let storable_accounts = StorableAccountsBySlot::new(slot, &accounts, self); - stats_sub.store_accounts_timing = self.store_accounts_frozen( + stats_sub.store_accounts_timing = self.store_accounts_for_shrink( storable_accounts, shrink_in_progress.new_storage(), UpdateIndexThreadSelection::PoolWithThreshold, @@ -2897,7 +2897,7 @@ impl AccountsDb { rewrite_elapsed.stop(); stats_sub.rewrite_elapsed_us = Saturating(rewrite_elapsed.as_us()); - // `store_accounts_frozen()` above may have purged accounts from some + // `store_accounts_for_shrink()` above may have purged accounts from some // other storage entries (the ones that were just overwritten by this // new storage entry). This means some of those stores might have caused // this slot to be read to `self.shrink_candidate_slots`, so delete @@ -3692,11 +3692,11 @@ impl AccountsDb { // F1 flush_slot_cache() | N/A // | | // V | - // F2 store_accounts_frozen()/ | map of stores (creates new entry) + // F2 store_accounts_for_flush()/ | map of stores (creates new entry) // write_accounts_to_storage() | // | | // V | - // F3 store_accounts_frozen()/ | index + // F3 store_accounts_for_flush()/ | index // update_index_stored_accounts()| (replaces existing store_id, offset in caches) // | | // V | @@ -3712,11 +3712,11 @@ impl AccountsDb { // S1 do_shrink_slot_store() | N/A // | | // V | - // S2 store_accounts_frozen()/ | map of stores (creates new entry) + // S2 store_accounts_for_shrink()/ | map of stores (creates new entry) // write_accounts_to_storage() | // | | // V | - // S3 store_accounts_frozen()/ | index + // S3 store_accounts_for_shrink()/ | index // update_index_stored_accounts()| (replaces existing store_id, offset in stores) // | | // V | @@ -4755,7 +4755,7 @@ impl AccountsDb { ); let (store_accounts_timing_inner, store_accounts_total_inner_us) = - measure_us!(self._store_accounts_frozen( + measure_us!(self.store_accounts_for_flush( (slot, &accounts[..]), &flushed_store, reclaim_method, @@ -5496,33 +5496,15 @@ impl AccountsDb { } /// Stores accounts in the storage and updates the index. - /// This function is intended for accounts that are rooted (frozen). + /// This function is intended for accounts that are being shrunk (moving from one store to another) /// - `UpsertReclaims` is set to `IgnoreReclaims`. If the slot in `accounts` differs from the new slot, /// accounts may be removed from the account index. In such cases, the caller must ensure that alive /// accounts are decremented for the older storage or that the old storage is removed entirely - pub fn store_accounts_frozen<'a>( + pub fn store_accounts_for_shrink<'a>( &self, accounts: impl StorableAccounts<'a>, storage: &AccountStorageEntry, update_index_thread_selection: UpdateIndexThreadSelection, - ) -> StoreAccountsTiming { - self._store_accounts_frozen( - accounts, - storage, - UpsertReclaim::IgnoreReclaims, - update_index_thread_selection, - ) - } - - /// Stores accounts in the storage and updates the index. - /// This function is intended for accounts that are rooted (frozen). - /// - `UpsertReclaims` must be set to `IgnoreReclaims` at this time - fn _store_accounts_frozen<'a>( - &self, - accounts: impl StorableAccounts<'a>, - storage: &AccountStorageEntry, - reclaim_handling: UpsertReclaim, - update_index_thread_selection: UpdateIndexThreadSelection, ) -> StoreAccountsTiming { let slot = accounts.target_slot(); let num_accounts_stored = accounts.len(); @@ -5530,7 +5512,7 @@ impl AccountsDb { debug_assert!(self.accounts_index.is_alive_root(slot)); - // Flush the read cache if necessary. This will occur during shrink or clean + // Flush the read cache if necessary let flush_read_cache_time = Measure::start("flush_read_cache"); if self.read_only_accounts_cache.can_slot_be_in_cache(slot) { (0..accounts.len()).for_each(|index| { @@ -5547,15 +5529,65 @@ impl AccountsDb { let infos = self.write_accounts_to_storage(slot, storage, &accounts); let write_accounts_us = write_accounts_time.end_as_us(); + let update_index_time = Measure::start("update_index"); + self.update_index_stored_accounts( + infos, + &accounts, + UpsertReclaim::IgnoreReclaims, + update_index_thread_selection, + &self.thread_pool_background, + ); + let update_index_us = update_index_time.end_as_us(); + + stats + .flush_read_cache_us + .fetch_add(flush_read_cache_us, Ordering::Relaxed); + stats + .write_to_storage_us + .fetch_add(write_accounts_us, Ordering::Relaxed); + stats + .update_index_us + .fetch_add(update_index_us, Ordering::Relaxed); + stats + .num_accounts_stored + .fetch_add(num_accounts_stored as u64, Ordering::Relaxed); + stats.report(); + + StoreAccountsTiming { + store_accounts_elapsed: write_accounts_us, + update_index_elapsed: update_index_us, + handle_reclaims_elapsed: 0, + } + } + + /// Stores accounts in the storage and updates the index. + /// This function is intended for accounts that are being flushed (moving from the cache to storage) + /// - `UpsertReclaims` determines whether to reclaim old slots. If `ReclaimOldSlots` is used, all + /// old versions of the account are reclaimed. If `IgnoreReclaims` is used, old versions of the + /// account are not reclaimed and must be cleaned later. + fn store_accounts_for_flush<'a>( + &self, + accounts: impl StorableAccounts<'a>, + storage: &AccountStorageEntry, + reclaim_handling: UpsertReclaim, + update_index_thread_selection: UpdateIndexThreadSelection, + ) -> StoreAccountsTiming { + let slot = accounts.target_slot(); + let num_accounts_stored = accounts.len(); + let stats = &self.store_accounts_frozen_stats; + + debug_assert!(self.accounts_index.is_alive_root(slot)); + + // Write the accounts to storage + let write_accounts_time = Measure::start("write_accounts"); + let infos = self.write_accounts_to_storage(slot, storage, &accounts); + let write_accounts_us = write_accounts_time.end_as_us(); + let mark_zero_lamport_time = Measure::start("mark_zero_lamport"); let num_zero_lamport_single_ref_accounts_marked = self.mark_zero_lamport_single_ref_accounts(&infos, storage, reclaim_handling); let mark_zero_lamport_us = mark_zero_lamport_time.end_as_us(); - // If the cache was flushed, then because `update_index` occurs - // after the account are stored by the above `store_accounts_to` - // call and all the accounts are stored, all reads after this point - // will know to not check the cache anymore let update_index_time = Measure::start("update_index"); let reclaims = self.update_index_stored_accounts( infos, @@ -5598,9 +5630,6 @@ impl AccountsDb { } let handle_reclaims_us = handle_reclaims_time.end_as_us(); - stats - .flush_read_cache_us - .fetch_add(flush_read_cache_us, Ordering::Relaxed); stats .write_to_storage_us .fetch_add(write_accounts_us, Ordering::Relaxed); @@ -5763,7 +5792,7 @@ impl AccountsDb { infos } - /// Marks zero lamport single reference accounts in the storage during store_accounts_frozen + /// Marks zero lamport single reference accounts in the storage during store_accounts_for_flush /// /// Returns the number of accounts marked. fn mark_zero_lamport_single_ref_accounts( diff --git a/accounts-db/src/accounts_db/tests.rs b/accounts-db/src/accounts_db/tests.rs index 16b4056d134..a755f4861fa 100644 --- a/accounts-db/src/accounts_db/tests.rs +++ b/accounts-db/src/accounts_db/tests.rs @@ -582,8 +582,9 @@ fn test_flush_slots_with_reclaim_old_slots() { accounts.accounts_index.add_root(new_slot); - // Flushing this storage directly using _store_accounts_frozen. This is done to pass in UpsertReclaim::ReclaimOldSlots - accounts._store_accounts_frozen( + // Flushing this storage directly using store_accounts_for_flush. This is done to pass in + // UpsertReclaim::ReclaimOldSlots + accounts.store_accounts_for_flush( (new_slot, &accounts_list[..]), &storage, UpsertReclaim::ReclaimOldSlots, diff --git a/accounts-db/src/ancient_append_vecs.rs b/accounts-db/src/ancient_append_vecs.rs index 20614beec82..860a89e5020 100644 --- a/accounts-db/src/ancient_append_vecs.rs +++ b/accounts-db/src/ancient_append_vecs.rs @@ -563,11 +563,12 @@ impl AccountsDb { .expect("ancient shrink target slot must already have a storage"); let (shrink_in_progress, create_and_insert_store_elapsed_us) = measure_us!(self.get_store_for_shrink(target_slot, old_store, bytes)); - let (store_accounts_timing, rewrite_elapsed_us) = measure_us!(self.store_accounts_frozen( - accounts_to_write, - shrink_in_progress.new_storage(), - UpdateIndexThreadSelection::PoolWithThreshold - )); + let (store_accounts_timing, rewrite_elapsed_us) = + measure_us!(self.store_accounts_for_shrink( + accounts_to_write, + shrink_in_progress.new_storage(), + UpdateIndexThreadSelection::PoolWithThreshold + )); write_ancient_accounts.metrics.accumulate(&ShrinkStatsSub { store_accounts_timing, diff --git a/runtime/src/snapshot_minimizer.rs b/runtime/src/snapshot_minimizer.rs index 9a52baa3579..888783bc055 100644 --- a/runtime/src/snapshot_minimizer.rs +++ b/runtime/src/snapshot_minimizer.rs @@ -317,7 +317,7 @@ impl<'a> SnapshotMinimizer<'a> { let storable_accounts = StorableAccountsBySlot::new(slot, &accounts, self.accounts_db()); - self.accounts_db().store_accounts_frozen( + self.accounts_db().store_accounts_for_shrink( storable_accounts, new_storage, UpdateIndexThreadSelection::Inline, From 3022fdfcb12ed28f89f71acf0a4d556dfab94bd7 Mon Sep 17 00:00:00 2001 From: Monti <145272067+Monti-27@users.noreply.github.com> Date: Fri, 22 May 2026 01:45:24 +0530 Subject: [PATCH 026/576] fix tx-v1 binary transaction encoding (#12591) * fix tx v1 binary encoding * fix clippy in tx v1 test * update lock files --- Cargo.lock | 1 + dev-bins/Cargo.lock | 1 + programs/sbf/Cargo.lock | 1 + transaction-status-client-types/src/lib.rs | 28 +++++++++++- transaction-status/Cargo.toml | 1 + transaction-status/src/lib.rs | 52 +++++++++++++++++++--- 6 files changed, 76 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 13d802297b1..5373bedc9cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11215,6 +11215,7 @@ dependencies = [ "spl-token-interface", "spl-token-metadata-interface", "thiserror 2.0.18", + "wincode", ] [[package]] diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 6538592f993..560097dfab5 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -9622,6 +9622,7 @@ dependencies = [ "spl-token-interface", "spl-token-metadata-interface", "thiserror 2.0.18", + "wincode", ] [[package]] diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index 9c23394de49..acc6d8a9d19 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -9927,6 +9927,7 @@ dependencies = [ "spl-token-interface", "spl-token-metadata-interface", "thiserror 2.0.18", + "wincode", ] [[package]] diff --git a/transaction-status-client-types/src/lib.rs b/transaction-status-client-types/src/lib.rs index 75317d7b8c3..4391048bcb4 100644 --- a/transaction-status-client-types/src/lib.rs +++ b/transaction-status-client-types/src/lib.rs @@ -182,11 +182,11 @@ impl EncodedTransaction { TransactionBinaryEncoding::Base58 => bs58::decode(blob) .into_vec() .ok() - .and_then(|bytes| bincode::deserialize(&bytes).ok()), + .and_then(|bytes| wincode::deserialize(&bytes).ok()), TransactionBinaryEncoding::Base64 => BASE64_STANDARD .decode(blob) .ok() - .and_then(|bytes| bincode::deserialize(&bytes).ok()), + .and_then(|bytes| wincode::deserialize(&bytes).ok()), }; transaction.filter(|transaction| transaction.sanitize().is_ok()) @@ -806,6 +806,30 @@ mod test { assert!(unsanitary_transaction.decode().is_none()); } + #[test] + fn test_decode_v1_wire_transaction() { + let transaction = VersionedTransaction { + signatures: vec![solana_signature::Signature::default()], + message: solana_message::VersionedMessage::V1(solana_message::v1::Message::new( + solana_message::MessageHeader { + num_required_signatures: 1, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 0, + }, + solana_message::v1::TransactionConfig::empty(), + solana_message::Hash::default(), + vec![solana_message::Address::default()], + vec![], + )), + }; + let encoded = EncodedTransaction::Binary( + BASE64_STANDARD.encode(wincode::serialize(&transaction).unwrap()), + TransactionBinaryEncoding::Base64, + ); + + assert_eq!(encoded.decode(), Some(transaction)); + } + #[test] fn test_satisfies_commitment() { let status = TransactionStatus { diff --git a/transaction-status/Cargo.toml b/transaction-status/Cargo.toml index 171a01870c0..de075cc7ebf 100644 --- a/transaction-status/Cargo.toml +++ b/transaction-status/Cargo.toml @@ -50,6 +50,7 @@ spl-token-group-interface = { workspace = true } spl-token-interface = { workspace = true } spl-token-metadata-interface = { workspace = true } thiserror = { workspace = true } +wincode = { workspace = true } [dev-dependencies] bencher = { workspace = true } diff --git a/transaction-status/src/lib.rs b/transaction-status/src/lib.rs index d7fd8f7802f..8a1a3ffd362 100644 --- a/transaction-status/src/lib.rs +++ b/transaction-status/src/lib.rs @@ -60,6 +60,10 @@ pub mod parse_token; pub mod parse_vote; pub mod token_balances; +fn serialize_versioned_transaction(transaction: &VersionedTransaction) -> Vec { + wincode::serialize(transaction).expect("serialize versioned transaction") +} + pub struct BlockEncodingOptions { pub transaction_details: TransactionDetails, pub show_rewards: bool, @@ -634,14 +638,14 @@ impl EncodableWithMeta for VersionedTransaction { ) -> Self::Encoded { match encoding { UiTransactionEncoding::Binary => EncodedTransaction::LegacyBinary( - bs58::encode(bincode::serialize(self).unwrap()).into_string(), + bs58::encode(serialize_versioned_transaction(self)).into_string(), ), UiTransactionEncoding::Base58 => EncodedTransaction::Binary( - bs58::encode(bincode::serialize(self).unwrap()).into_string(), + bs58::encode(serialize_versioned_transaction(self)).into_string(), TransactionBinaryEncoding::Base58, ), UiTransactionEncoding::Base64 => EncodedTransaction::Binary( - BASE64_STANDARD.encode(bincode::serialize(self).unwrap()), + BASE64_STANDARD.encode(serialize_versioned_transaction(self)), TransactionBinaryEncoding::Base64, ), UiTransactionEncoding::Json => self.json_encode(), @@ -678,14 +682,14 @@ impl Encodable for VersionedTransaction { fn encode(&self, encoding: UiTransactionEncoding) -> Self::Encoded { match encoding { UiTransactionEncoding::Binary => EncodedTransaction::LegacyBinary( - bs58::encode(bincode::serialize(self).unwrap()).into_string(), + bs58::encode(serialize_versioned_transaction(self)).into_string(), ), UiTransactionEncoding::Base58 => EncodedTransaction::Binary( - bs58::encode(bincode::serialize(self).unwrap()).into_string(), + bs58::encode(serialize_versioned_transaction(self)).into_string(), TransactionBinaryEncoding::Base58, ), UiTransactionEncoding::Base64 => EncodedTransaction::Binary( - BASE64_STANDARD.encode(bincode::serialize(self).unwrap()), + BASE64_STANDARD.encode(serialize_versioned_transaction(self)), TransactionBinaryEncoding::Base64, ), UiTransactionEncoding::Json | UiTransactionEncoding::JsonParsed => { @@ -1075,4 +1079,40 @@ mod test { assert_eq!(encoded.block_time, Some(1234567890)); assert_eq!(encoded.transaction_index, Some(7)); } + + #[test] + fn test_v1_binary_encoding_uses_wire_format() { + let message = solana_message::v1::Message::new( + solana_message::MessageHeader { + num_required_signatures: 1, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 1, + }, + solana_message::v1::TransactionConfig::empty(), + solana_hash::Hash::new_from_array([7; solana_hash::HASH_BYTES]), + vec![ + solana_pubkey::Pubkey::new_unique(), + solana_pubkey::Pubkey::new_unique(), + ], + vec![], + ); + let transaction = VersionedTransaction { + signatures: vec![solana_signature::Signature::default()], + message: solana_message::VersionedMessage::V1(message), + }; + + let EncodedTransaction::Binary(encoded, TransactionBinaryEncoding::Base64) = + transaction.encode(UiTransactionEncoding::Base64) + else { + panic!("expected binary base64 output"); + }; + let bytes = BASE64_STANDARD.decode(&encoded).unwrap(); + + assert_eq!(bytes[0], solana_message::v1::V1_PREFIX); + assert_eq!(bytes, wincode::serialize(&transaction).unwrap()); + assert_eq!( + EncodedTransaction::Binary(encoded, TransactionBinaryEncoding::Base64).decode(), + Some(transaction) + ); + } } From b969406d3af8b162832e2c617e41df7456af1004 Mon Sep 17 00:00:00 2001 From: Andrew Fitzgerald Date: Thu, 21 May 2026 15:37:59 -0500 Subject: [PATCH 027/576] Sigverify: worker threads dedup (#12368) Move sigverify dedup into worker threads --- core/src/sigverify.rs | 161 ++++++++++--------- core/src/sigverify_stage.rs | 300 ++++++++++++++---------------------- 2 files changed, 192 insertions(+), 269 deletions(-) diff --git a/core/src/sigverify.rs b/core/src/sigverify.rs index 816c5534431..0fb7b9779a9 100644 --- a/core/src/sigverify.rs +++ b/core/src/sigverify.rs @@ -8,6 +8,7 @@ use { crossbeam_channel::{Receiver, Sender, TrySendError, bounded}, solana_measure::measure_us, solana_perf::{ + deduper::{self, Deduper}, packet::PacketBatch, sigverify::{self}, }, @@ -24,14 +25,6 @@ use { }, }; -pub(crate) struct TransactionSigVerifier { - worker_sender: Sender, -} - -struct TransactionVerifyTask { - batch: PacketBatch, -} - pub(crate) struct GossipVerifyTask { batch: PacketBatch, transaction: Transaction, @@ -44,33 +37,32 @@ pub(crate) struct GossipVerifiedVoteBatch { #[derive(Clone)] pub(crate) struct SigVerifyWorkerStats { + pub(crate) total_batches: Arc, + pub(crate) total_packets: Arc, + pub(crate) total_dedup: Arc, + pub(crate) total_dedup_time_us: Arc, pub(crate) total_valid_packets: Arc, pub(crate) total_verify_time_us: Arc, } -impl TransactionSigVerifier { - fn new(worker_sender: Sender) -> Self { - Self { worker_sender } - } +#[derive(Clone)] +pub(crate) struct SigVerifyWorkerState { + banking_stage_sender: BankingPacketSender, + deduper: Arc>, + stats: SigVerifyWorkerStats, +} - pub(crate) fn send_packets_to_worker_pool( - &mut self, - batches: Vec, - ) -> Result { - let mut dropped_packets = 0; - for batch in batches { - match self.worker_sender.try_send(TransactionVerifyTask { batch }) { - Ok(()) => {} - Err(TrySendError::Full(task)) => { - dropped_packets += task.batch.len(); - } - Err(TrySendError::Disconnected(_)) => { - return Err(SigVerifyServiceError::WorkerQueueClosed); - } - } +impl SigVerifyWorkerState { + pub(crate) fn new( + banking_stage_sender: BankingPacketSender, + deduper: Arc>, + stats: SigVerifyWorkerStats, + ) -> Self { + Self { + banking_stage_sender, + deduper, + stats, } - - Ok(dropped_packets) } } @@ -118,30 +110,28 @@ impl GossipSigVerifier { } } -// Work queues are kept separate so that a spam on TPU -// will not lead to us dropping votes. -const SIGVERIFY_NON_VOTE_WORK_CHANNEL_SIZE: usize = 50_000; -const SIGVERIFY_TPU_VOTE_WORK_CHANNEL_SIZE: usize = 5_000; // channel is batches not individual packets +/// Gossip votes use a bounded queue into the worker pool. const SIGVERIFY_GOSSIP_VOTE_WORK_CHANNEL_SIZE: usize = 50_000; +pub(crate) struct SigVerifyWorkerSenders { + pub(crate) gossip_verified_vote_sender: Sender, + pub(crate) forward_stage_sender: Sender<(BankingPacketBatch, bool)>, +} + #[derive(Clone)] struct WorkerPoolChannels { - non_vote_receiver: Receiver, - tpu_vote_receiver: Receiver, + non_vote_receiver: Receiver, + tpu_vote_receiver: Receiver, gossip_receiver: Receiver, - non_vote_banking_sender: BankingPacketSender, - tpu_vote_banking_sender: BankingPacketSender, gossip_verified_vote_sender: Sender, forward_stage_sender: Sender<(BankingPacketBatch, bool)>, sharable_banks: SharableBanks, - non_vote_stats: SigVerifyWorkerStats, - tpu_vote_stats: SigVerifyWorkerStats, + non_vote_state: SigVerifyWorkerState, + tpu_vote_state: SigVerifyWorkerState, } pub(crate) struct SigVerifyWorkerPool { exit: Arc, - non_vote_sender: Sender, - tpu_vote_sender: Sender, gossip_sender: Sender, worker_hdls: Vec>, } @@ -160,29 +150,24 @@ impl Drop for SigVerifyWorkerPool { impl SigVerifyWorkerPool { pub(crate) fn new( num_workers: NonZeroUsize, - non_vote_banking_sender: BankingPacketSender, - tpu_vote_banking_sender: BankingPacketSender, - gossip_verified_vote_sender: Sender, - forward_stage_sender: Sender<(BankingPacketBatch, bool)>, + non_vote_receiver: Receiver, + tpu_vote_receiver: Receiver, + senders: SigVerifyWorkerSenders, forward_non_votes: bool, sharable_banks: SharableBanks, - non_vote_stats: SigVerifyWorkerStats, - tpu_vote_stats: SigVerifyWorkerStats, + non_vote_state: SigVerifyWorkerState, + tpu_vote_state: SigVerifyWorkerState, ) -> Self { - let (non_vote_sender, non_vote_receiver) = bounded(SIGVERIFY_NON_VOTE_WORK_CHANNEL_SIZE); - let (tpu_vote_sender, tpu_vote_receiver) = bounded(SIGVERIFY_TPU_VOTE_WORK_CHANNEL_SIZE); let (gossip_sender, gossip_receiver) = bounded(SIGVERIFY_GOSSIP_VOTE_WORK_CHANNEL_SIZE); let channels = WorkerPoolChannels { non_vote_receiver, tpu_vote_receiver, gossip_receiver, - non_vote_banking_sender, - tpu_vote_banking_sender, - gossip_verified_vote_sender, - forward_stage_sender, + gossip_verified_vote_sender: senders.gossip_verified_vote_sender, + forward_stage_sender: senders.forward_stage_sender, sharable_banks, - non_vote_stats, - tpu_vote_stats, + non_vote_state, + tpu_vote_state, }; let exit = Arc::new(AtomicBool::new(false)); let worker_hdls = (0..num_workers.get()) @@ -198,21 +183,11 @@ impl SigVerifyWorkerPool { .collect(); Self { exit, - non_vote_sender, - tpu_vote_sender, gossip_sender, worker_hdls, } } - pub(crate) fn non_vote_verifier(&self) -> TransactionSigVerifier { - TransactionSigVerifier::new(self.non_vote_sender.clone()) - } - - pub(crate) fn tpu_vote_verifier(&self) -> TransactionSigVerifier { - TransactionSigVerifier::new(self.tpu_vote_sender.clone()) - } - pub(crate) fn gossip_verifier(&self) -> GossipSigVerifier { GossipSigVerifier { worker_sender: self.gossip_sender.clone(), @@ -232,30 +207,28 @@ impl SigVerifyWorkerPool { crossbeam_channel::select! { recv(&channels.non_vote_receiver) -> maybe_work => { match maybe_work { - Ok(work) => Self::run_transaction_task( - work, + Ok(batch) => Self::run_transaction_task( + batch, false, - &channels.non_vote_banking_sender, &channels.forward_stage_sender, forward_non_votes, false, &channels.sharable_banks, - &channels.non_vote_stats, + &channels.non_vote_state, ), Err(_) => false, } } recv(&channels.tpu_vote_receiver) -> maybe_work => { match maybe_work { - Ok(work) => Self::run_transaction_task( - work, + Ok(batch) => Self::run_transaction_task( + batch, true, - &channels.tpu_vote_banking_sender, &channels.forward_stage_sender, true, true, &channels.sharable_banks, - &channels.tpu_vote_stats, + &channels.tpu_vote_state, ), Err(_) => false, } @@ -274,31 +247,55 @@ impl SigVerifyWorkerPool { } fn run_transaction_task( - mut work: TransactionVerifyTask, + mut batch: PacketBatch, reject_non_vote: bool, - banking_stage_sender: &BankingPacketSender, forward_stage_sender: &Sender<(BankingPacketBatch, bool)>, should_forward: bool, is_tpu_vote: bool, sharable_banks: &SharableBanks, - stats: &SigVerifyWorkerStats, + state: &SigVerifyWorkerState, ) -> bool { + state.stats.total_batches.fetch_add(1, Ordering::Relaxed); + state + .stats + .total_packets + .fetch_add(batch.len(), Ordering::Relaxed); + + let (discard_or_dedup_fail, dedup_time_us) = + measure_us!(deduper::dedup_packets_and_count_discards( + &state.deduper, + std::slice::from_mut(&mut batch) + )); + state + .stats + .total_dedup + .fetch_add(discard_or_dedup_fail as usize, Ordering::Relaxed); + state + .stats + .total_dedup_time_us + .fetch_add(dedup_time_us as usize, Ordering::Relaxed); + let enable_tx_v1 = sharable_banks.working().feature_set.snapshot().enable_tx_v1; let (_, verify_time_us) = measure_us!(sigverify::ed25519_verify_serial( - &mut work.batch, + &mut batch, reject_non_vote, enable_tx_v1, )); - let num_valid_packets = sigverify::count_valid_packets(std::iter::once(&work.batch)); - stats + let num_valid_packets = sigverify::count_valid_packets(std::iter::once(&batch)); + state + .stats .total_valid_packets .fetch_add(num_valid_packets, Ordering::Relaxed); - stats + state + .stats .total_verify_time_us .fetch_add(verify_time_us as usize, Ordering::Relaxed); - let banking_packet_batch = BankingPacketBatch::new(vec![work.batch]); - if let Err(err) = banking_stage_sender.send(banking_packet_batch.clone()) { + let banking_packet_batch = BankingPacketBatch::new(vec![batch]); + if let Err(err) = state + .banking_stage_sender + .send(banking_packet_batch.clone()) + { error!("sigverify send failed: {err:?}"); return false; } diff --git a/core/src/sigverify_stage.rs b/core/src/sigverify_stage.rs index a69eff82013..4186c2ec862 100644 --- a/core/src/sigverify_stage.rs +++ b/core/src/sigverify_stage.rs @@ -8,26 +8,21 @@ use { crate::{ banking_trace::BankingPacketSender, sigverify::{ - GossipSigVerifier, GossipVerifiedVoteBatch, SigVerifyWorkerPool, SigVerifyWorkerStats, - TransactionSigVerifier, + GossipSigVerifier, GossipVerifiedVoteBatch, SigVerifyWorkerPool, + SigVerifyWorkerSenders, SigVerifyWorkerState, SigVerifyWorkerStats, }, }, agave_banking_stage_ingress_types::BankingPacketBatch, core::time::Duration, - crossbeam_channel::{Receiver, RecvTimeoutError, Sender, unbounded}, - solana_measure::measure::Measure, - solana_perf::{ - deduper::{self, Deduper}, - packet::PacketBatch, - }, + crossbeam_channel::{Receiver, Sender, unbounded}, + solana_perf::{deduper::Deduper, packet::PacketBatch}, solana_runtime::bank_forks::SharableBanks, - solana_streamer::streamer::{self, StreamerError}, solana_transaction::Transaction, std::{ num::NonZeroUsize, sync::{ Arc, - atomic::{AtomicUsize, Ordering}, + atomic::{AtomicBool, AtomicUsize, Ordering}, }, thread::{self, Builder, JoinHandle}, time::Instant, @@ -37,17 +32,17 @@ use { #[derive(Error, Debug)] pub enum SigVerifyServiceError { - #[error("streamer error")] - Streamer(#[from] StreamerError), #[error("sigverify worker queue closed")] WorkerQueueClosed, } -type Result = std::result::Result; +const DEDUPER_NUM_BITS: u64 = 63_999_979; +const MAX_DEDUPER_AGE: Duration = Duration::from_secs(2); +const DEDUPER_FALSE_POSITIVE_RATE: f64 = 0.001; pub struct SigVerifyStage { - non_vote_thread_hdl: JoinHandle<()>, - tpu_vote_thread_hdl: JoinHandle<()>, + exit: Arc, + servicer_thread_hdl: Option>, // pool held so workers stay alive. If dropped, workers in pool shut down. _worker_pool: SigVerifyWorkerPool, } @@ -59,18 +54,19 @@ pub struct GossipSigVerifyHandle { #[derive(Default)] struct SigVerifierStats { - recv_batches_us_hist: histogram::Histogram, // time to call recv_batch - dedup_packets_pp_us_hist: histogram::Histogram, // per-packet time to call verify_batch - batches_hist: histogram::Histogram, // number of packet batches per verify call - packets_hist: histogram::Histogram, // number of packets per verify call num_deduper_saturations: usize, - total_batches: usize, - total_packets: usize, - total_dedup: usize, + total_batches: Arc, + total_packets: Arc, + total_dedup: Arc, total_valid_packets: Arc, - total_dedup_time_us: usize, + total_dedup_time_us: Arc, total_verify_time_us: Arc, - total_dropped_on_capacity: usize, +} + +struct ServicerState { + deduper: Arc>, + metrics_name: &'static str, + stats: SigVerifierStats, } impl SigVerifierStats { @@ -78,68 +74,12 @@ impl SigVerifierStats { fn maybe_report_and_reset(&mut self, name: &'static str) { // No need to report a datapoint if no batches/packets received - if self.total_batches == 0 { + if self.total_batches.load(Ordering::Relaxed) == 0 { return; } datapoint_info!( name, - ( - "recv_batches_us_90pct", - self.recv_batches_us_hist.percentile(90.0).unwrap_or(0), - i64 - ), - ( - "recv_batches_us_min", - self.recv_batches_us_hist.minimum().unwrap_or(0), - i64 - ), - ( - "recv_batches_us_max", - self.recv_batches_us_hist.maximum().unwrap_or(0), - i64 - ), - ( - "recv_batches_us_mean", - self.recv_batches_us_hist.mean().unwrap_or(0), - i64 - ), - ( - "dedup_packets_pp_us_90pct", - self.dedup_packets_pp_us_hist.percentile(90.0).unwrap_or(0), - i64 - ), - ( - "dedup_packets_pp_us_min", - self.dedup_packets_pp_us_hist.minimum().unwrap_or(0), - i64 - ), - ( - "dedup_packets_pp_us_max", - self.dedup_packets_pp_us_hist.maximum().unwrap_or(0), - i64 - ), - ( - "dedup_packets_pp_us_mean", - self.dedup_packets_pp_us_hist.mean().unwrap_or(0), - i64 - ), - ( - "batches_90pct", - self.batches_hist.percentile(90.0).unwrap_or(0), - i64 - ), - ("batches_min", self.batches_hist.minimum().unwrap_or(0), i64), - ("batches_max", self.batches_hist.maximum().unwrap_or(0), i64), - ("batches_mean", self.batches_hist.mean().unwrap_or(0), i64), - ( - "packets_90pct", - self.packets_hist.percentile(90.0).unwrap_or(0), - i64 - ), - ("packets_min", self.packets_hist.minimum().unwrap_or(0), i64), - ("packets_max", self.packets_hist.maximum().unwrap_or(0), i64), - ("packets_mean", self.packets_hist.mean().unwrap_or(0), i64), ( "num_deduper_saturations", core::mem::take(&mut self.num_deduper_saturations), @@ -147,23 +87,22 @@ impl SigVerifierStats { ), ( "total_batches", - core::mem::take(&mut self.total_batches), + self.total_batches.swap(0, Ordering::Relaxed), i64 ), ( "total_packets", - core::mem::take(&mut self.total_packets), + self.total_packets.swap(0, Ordering::Relaxed), i64 ), - ("total_dedup", core::mem::take(&mut self.total_dedup), i64), ( - "total_dedup_time_us", - core::mem::take(&mut self.total_dedup_time_us), + "total_dedup", + self.total_dedup.swap(0, Ordering::Relaxed), i64 ), ( - "total_dropped_on_capacity", - core::mem::take(&mut self.total_dropped_on_capacity), + "total_dedup_time_us", + self.total_dedup_time_us.swap(0, Ordering::Relaxed), i64 ), ( @@ -177,11 +116,6 @@ impl SigVerifierStats { i64 ), ); - - self.recv_batches_us_hist = histogram::Histogram::new(); - self.dedup_packets_pp_us_hist = histogram::Histogram::new(); - self.batches_hist = histogram::Histogram::new(); - self.packets_hist = histogram::Histogram::new(); } } @@ -199,37 +133,58 @@ impl SigVerifyStage { let (gossip_verified_vote_sender, verified_vote_receiver) = unbounded(); let non_vote_stats = SigVerifierStats::default(); let tpu_vote_stats = SigVerifierStats::default(); + let exit = Arc::new(AtomicBool::new(false)); + let mut rng = rand::rng(); + let non_vote_deduper = Arc::new(Deduper::<2, [u8]>::new(&mut rng, DEDUPER_NUM_BITS)); + let tpu_vote_deduper = Arc::new(Deduper::<2, [u8]>::new(&mut rng, DEDUPER_NUM_BITS)); let worker_pool = SigVerifyWorkerPool::new( num_workers, - non_vote_sender, - tpu_vote_sender, - gossip_verified_vote_sender, - forward_stage_sender, + packet_receiver, + vote_packet_receiver, + SigVerifyWorkerSenders { + gossip_verified_vote_sender, + forward_stage_sender, + }, forward_non_votes, sharable_banks, - SigVerifyWorkerStats { - total_valid_packets: non_vote_stats.total_valid_packets.clone(), - total_verify_time_us: non_vote_stats.total_verify_time_us.clone(), + SigVerifyWorkerState::new( + non_vote_sender, + non_vote_deduper.clone(), + SigVerifyWorkerStats { + total_batches: non_vote_stats.total_batches.clone(), + total_packets: non_vote_stats.total_packets.clone(), + total_dedup: non_vote_stats.total_dedup.clone(), + total_dedup_time_us: non_vote_stats.total_dedup_time_us.clone(), + total_valid_packets: non_vote_stats.total_valid_packets.clone(), + total_verify_time_us: non_vote_stats.total_verify_time_us.clone(), + }, + ), + SigVerifyWorkerState::new( + tpu_vote_sender, + tpu_vote_deduper.clone(), + SigVerifyWorkerStats { + total_batches: tpu_vote_stats.total_batches.clone(), + total_packets: tpu_vote_stats.total_packets.clone(), + total_dedup: tpu_vote_stats.total_dedup.clone(), + total_dedup_time_us: tpu_vote_stats.total_dedup_time_us.clone(), + total_valid_packets: tpu_vote_stats.total_valid_packets.clone(), + total_verify_time_us: tpu_vote_stats.total_verify_time_us.clone(), + }, + ), + ); + let servicer_thread_hdl = Self::servicer( + exit.clone(), + ServicerState { + deduper: non_vote_deduper, + metrics_name: "tpu-verifier", + stats: non_vote_stats, }, - SigVerifyWorkerStats { - total_valid_packets: tpu_vote_stats.total_valid_packets.clone(), - total_verify_time_us: tpu_vote_stats.total_verify_time_us.clone(), + ServicerState { + deduper: tpu_vote_deduper, + metrics_name: "tpu-vote-verifier", + stats: tpu_vote_stats, }, ); - let non_vote_thread_hdl = Self::verifier_service( - packet_receiver, - worker_pool.non_vote_verifier(), - "solSigVerTpu", - "tpu-verifier", - non_vote_stats, - ); - let tpu_vote_thread_hdl = Self::verifier_service( - vote_packet_receiver, - worker_pool.tpu_vote_verifier(), - "solSigVerTpuVot", - "tpu-vote-verifier", - tpu_vote_stats, - ); let gossip_sigverify_handle = GossipSigVerifyHandle { verifier: worker_pool.gossip_verifier(), verified_vote_receiver, @@ -237,100 +192,71 @@ impl SigVerifyStage { ( Self { - non_vote_thread_hdl, - tpu_vote_thread_hdl, + exit, + servicer_thread_hdl: Some(servicer_thread_hdl), _worker_pool: worker_pool, }, gossip_sigverify_handle, ) } - pub fn join(self) -> thread::Result<()> { - let non_vote_result = self.non_vote_thread_hdl.join(); - let tpu_vote_result = self.tpu_vote_thread_hdl.join(); - non_vote_result?; - tpu_vote_result?; - Ok(()) + pub fn join(mut self) -> thread::Result<()> { + self.join_servicer_thread() } - fn verifier( - deduper: &Deduper, - recvr: &Receiver, - verifier: &mut TransactionSigVerifier, - stats: &mut SigVerifierStats, - ) -> Result<()> { - const SOFT_RECEIVE_CAP: usize = 5_000; - let (mut batches, num_packets, recv_duration) = - streamer::recv_packet_batches(recvr, SOFT_RECEIVE_CAP)?; - - let batches_len = batches.len(); - stats - .recv_batches_us_hist - .increment(recv_duration.as_micros() as u64) - .unwrap(); - stats.batches_hist.increment(batches_len as u64).unwrap(); - stats.packets_hist.increment(num_packets as u64).unwrap(); - stats.total_batches += batches_len; - stats.total_packets += num_packets; - - let mut dedup_time = Measure::start("sigverify_dedup_time"); - let discard_or_dedup_fail = - deduper::dedup_packets_and_count_discards(deduper, &mut batches) as usize; - dedup_time.stop(); - stats.total_dropped_on_capacity += verifier.send_packets_to_worker_pool(batches)?; - stats - .dedup_packets_pp_us_hist - .increment(dedup_time.as_us() / (num_packets as u64)) - .unwrap(); - stats.total_dedup += discard_or_dedup_fail; - stats.total_dedup_time_us += dedup_time.as_us() as usize; - - Ok(()) + fn join_servicer_thread(&mut self) -> thread::Result<()> { + self.exit.store(true, Ordering::Relaxed); + self.servicer_thread_hdl + .take() + .map(JoinHandle::join) + .unwrap_or(Ok(())) } - fn verifier_service( - packet_receiver: Receiver, - mut verifier: TransactionSigVerifier, - thread_name: &'static str, - metrics_name: &'static str, - mut stats: SigVerifierStats, + /// Drives deduper reset and metrics reporting for sigverify packet streams. + fn servicer( + exit: Arc, + mut non_vote_state: ServicerState, + mut tpu_vote_state: ServicerState, ) -> JoinHandle<()> { let mut last_print = Instant::now(); - const MAX_DEDUPER_AGE: Duration = Duration::from_secs(2); - const DEDUPER_FALSE_POSITIVE_RATE: f64 = 0.001; - const DEDUPER_NUM_BITS: u64 = 63_999_979; Builder::new() - .name(thread_name.to_string()) + .name("solSigVerSvc".to_string()) .spawn(move || { let mut rng = rand::rng(); - let deduper = Deduper::<2, [u8]>::new(&mut rng, DEDUPER_NUM_BITS); - loop { - if deduper.maybe_reset(&mut rng, DEDUPER_FALSE_POSITIVE_RATE, MAX_DEDUPER_AGE) { - stats.num_deduper_saturations += 1; - } - if let Err(e) = - Self::verifier(&deduper, &packet_receiver, &mut verifier, &mut stats) - { - match e { - SigVerifyServiceError::Streamer(StreamerError::RecvTimeout( - RecvTimeoutError::Disconnected, - )) => break, - SigVerifyServiceError::Streamer(StreamerError::RecvTimeout( - RecvTimeoutError::Timeout, - )) => (), - _ => error!("{e:?}"), + while !exit.load(Ordering::Relaxed) { + for state in [&mut non_vote_state, &mut tpu_vote_state] { + if state.deduper.maybe_reset( + &mut rng, + DEDUPER_FALSE_POSITIVE_RATE, + MAX_DEDUPER_AGE, + ) { + state.stats.num_deduper_saturations += 1; } } if last_print.elapsed() > SigVerifierStats::REPORT_INTERVAL { - stats.maybe_report_and_reset(metrics_name); + non_vote_state + .stats + .maybe_report_and_reset(non_vote_state.metrics_name); + tpu_vote_state + .stats + .maybe_report_and_reset(tpu_vote_state.metrics_name); last_print = Instant::now(); } + thread::sleep(Duration::from_millis(10)); } }) .unwrap() } } +impl Drop for SigVerifyStage { + fn drop(&mut self) { + if let Err(err) = self.join_servicer_thread() { + error!("sigverify servicer encountered unexpected error: {err:?}"); + } + } +} + impl GossipSigVerifyHandle { #[cfg(test)] pub(crate) fn new_for_tests( @@ -534,8 +460,6 @@ mod tests { wincode::serialize(&test_tx_v1()).unwrap(), )); packet_s.send(PacketBatch::from(bytes_batch)).unwrap(); - drop(packet_s); - drop(vote_packet_s); let verified_batch = verified_r.recv_timeout(Duration::from_secs(30)).unwrap(); assert_eq!(verified_batch.len(), 1); @@ -545,6 +469,8 @@ mod tests { expected_valid ); + drop(packet_s); + drop(vote_packet_s); drop(gossip_sigverify_handle); stage.join().unwrap(); } From b14df72abfaaf9332e9aee336bc824d7dae048b7 Mon Sep 17 00:00:00 2001 From: Rory Harris Date: Thu, 21 May 2026 15:33:18 -0700 Subject: [PATCH 028/576] Split Store Accounts Stats into Flush/Shrink (#12669) * Split Store Accounts Stats into Flush/Shrink * Fix naming --- accounts-db/src/accounts_db.rs | 18 +++++---- accounts-db/src/accounts_db/stats.rs | 57 +++++++++++++++++++++++----- 2 files changed, 59 insertions(+), 16 deletions(-) diff --git a/accounts-db/src/accounts_db.rs b/accounts-db/src/accounts_db.rs index 5d3747a3e86..a1e3e9c1d54 100644 --- a/accounts-db/src/accounts_db.rs +++ b/accounts-db/src/accounts_db.rs @@ -40,8 +40,8 @@ use { accounts_db::stats::{ AccountsStats, CleanAccountsStats, FlushStats, LoadAccountsStats, ObsoleteAccountsStats, PurgeStats, ShrinkAncientStats, ShrinkStats, ShrinkStatsSub, - StoreAccountsFrozenStats, StoreAccountsTiming, StoreAccountsUnfrozenStats, - WriteAccountsToCacheStats, + StoreAccountsForFlushStats, StoreAccountsForShrinkStats, StoreAccountsTiming, + StoreAccountsUnfrozenStats, WriteAccountsToCacheStats, }, accounts_file::{AccountsFile, AccountsFileProvider}, accounts_hash::{AccountLtHash, AccountsLtHash, ZERO_LAMPORT_ACCOUNT_LT_HASH}, @@ -921,8 +921,11 @@ pub struct AccountsDb { /// Stats from storing accounts unfrozen store_accounts_unfrozen_stats: StoreAccountsUnfrozenStats, - /// Stats from storing accounts frozen - store_accounts_frozen_stats: StoreAccountsFrozenStats, + /// Stats from storing accounts for shrink + store_accounts_for_shrink_stats: StoreAccountsForShrinkStats, + + /// Stats from storing accounts for flush + store_accounts_for_flush_stats: StoreAccountsForFlushStats, clean_accounts_stats: CleanAccountsStats, @@ -1143,7 +1146,8 @@ impl AccountsDb { stats: AccountsStats::default(), load_account_stats: LoadAccountsStats::default(), store_accounts_unfrozen_stats: StoreAccountsUnfrozenStats::default(), - store_accounts_frozen_stats: StoreAccountsFrozenStats::default(), + store_accounts_for_shrink_stats: StoreAccountsForShrinkStats::default(), + store_accounts_for_flush_stats: StoreAccountsForFlushStats::default(), #[cfg(test)] load_delay: u64::default(), #[cfg(test)] @@ -5508,7 +5512,7 @@ impl AccountsDb { ) -> StoreAccountsTiming { let slot = accounts.target_slot(); let num_accounts_stored = accounts.len(); - let stats = &self.store_accounts_frozen_stats; + let stats = &self.store_accounts_for_shrink_stats; debug_assert!(self.accounts_index.is_alive_root(slot)); @@ -5574,7 +5578,7 @@ impl AccountsDb { ) -> StoreAccountsTiming { let slot = accounts.target_slot(); let num_accounts_stored = accounts.len(); - let stats = &self.store_accounts_frozen_stats; + let stats = &self.store_accounts_for_flush_stats; debug_assert!(self.accounts_index.is_alive_root(slot)); diff --git a/accounts-db/src/accounts_db/stats.rs b/accounts-db/src/accounts_db/stats.rs index 91d17c3840e..5873e43a49f 100644 --- a/accounts-db/src/accounts_db/stats.rs +++ b/accounts-db/src/accounts_db/stats.rs @@ -101,13 +101,57 @@ impl StoreAccountsUnfrozenStats { } } -/// Stats from storing accounts frozen (i.e. to an account storage file) +/// Stats from storing accounts for shrink (i.e. moving accounts between storage files) #[derive(Debug, Default)] -pub struct StoreAccountsFrozenStats { +pub struct StoreAccountsForShrinkStats { pub last_report: AtomicInterval, pub flush_read_cache_us: AtomicU64, pub write_to_storage_us: AtomicU64, pub update_index_us: AtomicU64, + pub num_accounts_stored: AtomicU64, +} + +impl StoreAccountsForShrinkStats { + const REPORT_INTERVAL_MS: u64 = Duration::from_secs(1).as_millis() as u64; + + pub fn report(&self) { + let should_report = self.last_report.should_update(Self::REPORT_INTERVAL_MS); + if !should_report { + return; + } + + datapoint_info!( + "accounts_db_store_accounts_for_shrink", + ( + "flush_read_cache_us", + self.flush_read_cache_us.swap(0, Ordering::Relaxed), + i64 + ), + ( + "write_to_storage_us", + self.write_to_storage_us.swap(0, Ordering::Relaxed), + i64 + ), + ( + "update_index_us", + self.update_index_us.swap(0, Ordering::Relaxed), + i64 + ), + ( + "num_accounts_stored", + self.num_accounts_stored.swap(0, Ordering::Relaxed), + i64 + ), + ); + } +} + +/// Stats from storing accounts for flush (i.e. flushing the write cache to a storage file) +#[derive(Debug, Default)] +pub struct StoreAccountsForFlushStats { + pub last_report: AtomicInterval, + pub write_to_storage_us: AtomicU64, + pub update_index_us: AtomicU64, pub mark_zero_lamport_single_ref_accounts_us: AtomicU64, pub handle_reclaims_us: AtomicU64, pub num_accounts_stored: AtomicU64, @@ -117,7 +161,7 @@ pub struct StoreAccountsFrozenStats { pub num_obsolete_bytes_removed: AtomicU64, } -impl StoreAccountsFrozenStats { +impl StoreAccountsForFlushStats { const REPORT_INTERVAL_MS: u64 = Duration::from_secs(1).as_millis() as u64; pub fn report(&self) { @@ -127,12 +171,7 @@ impl StoreAccountsFrozenStats { } datapoint_info!( - "accounts_db_store_accounts_frozen", - ( - "flush_read_cache_us", - self.flush_read_cache_us.swap(0, Ordering::Relaxed), - i64 - ), + "accounts_db_store_accounts_for_flush", ( "write_to_storage_us", self.write_to_storage_us.swap(0, Ordering::Relaxed), From 1e98d34f94cdcca8fb8d0cd2cc472af890ba825c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 06:53:24 +0800 Subject: [PATCH 029/576] chore(deps): bump protosol from 6.1.0 to 8.0.0 (#12625) Bumps [protosol](https://github.com/firedancer-io/protosol) from 6.1.0 to 8.0.0. - [Commits](https://github.com/firedancer-io/protosol/compare/v6.1.0...v8.0.0) --- updated-dependencies: - dependency-name: protosol dependency-version: 8.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5373bedc9cb..999008b9283 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5534,9 +5534,9 @@ dependencies = [ [[package]] name = "protosol" -version = "6.1.0" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acef24e715f0ce9ac6491004da1c35c919c60517f976f61e0f05f34d22e873ec" +checksum = "4917daeaee8127b0a7e0b9c68ad8ed834b4b0eb8c8c186eb0fba8d3ecc2cd6d5" dependencies = [ "prost 0.11.9", ] diff --git a/Cargo.toml b/Cargo.toml index 7b8419d2f7e..d120f2bbdba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -306,7 +306,7 @@ proptest = "1.11" prost = "0.14.3" prost-types = "0.14.3" protobuf-src = "1.1.0" -protosol = "=6.1.0" +protosol = "=8.0.0" qualifier_attr = { version = "0.2.2", default-features = false } quinn = "0.11.9" rand = "0.9.4" From 18e89efadd597245b6436e7ab2ec8a45414d9869 Mon Sep 17 00:00:00 2001 From: Brennan Date: Fri, 22 May 2026 00:14:04 -0700 Subject: [PATCH 030/576] Use dynamic slot time (#12672) --- poh/src/poh_recorder.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/poh/src/poh_recorder.rs b/poh/src/poh_recorder.rs index da41baf3b87..b7585871dfa 100644 --- a/poh/src/poh_recorder.rs +++ b/poh/src/poh_recorder.rs @@ -612,10 +612,10 @@ impl PohRecorder { self.notify_replay_wakeup(); // Wait for the bank to be frozen with timeout - // TODO: change this to use DELTA_BLOCK from votor instead. let start = Instant::now(); + let delta_block = Duration::from_nanos_u128(working_bank.bank.ns_per_slot); while !working_bank.bank.is_frozen() && !self.is_exited.load(Ordering::Relaxed) { - if start.elapsed() > Duration::from_millis(400) { + if start.elapsed() > delta_block { break; } std::hint::spin_loop(); From 5ae8e82da3868ad957cdc68e93f346a0d3d0e0cd Mon Sep 17 00:00:00 2001 From: Joe C Date: Fri, 22 May 2026 16:03:21 +0800 Subject: [PATCH 031/576] svm: fold svm-test-harness into conformance module (#12384) * svm: add conformance module with instruction harness * svm: add conformance fixture conversions * svm: add conformance programs helpers * svm: add conformance sysvar helper * svm: add conformance harness test * svm: hoist MockCallback and add execute_instr wrapper * svm: add conformance FFI binding * programs/sbf: migrate from svm-test-harness to solana-svm conformance * drop svm-test-harness * rework crate features * sort bins --- Cargo.lock | 5 + dev-bins/Cargo.lock | 320 +++--------------- dev-bins/Cargo.toml | 9 +- programs/sbf/Cargo.lock | 53 +-- programs/sbf/Cargo.toml | 4 +- programs/sbf/tests/programs.rs | 152 ++++----- svm-test-harness/.gitignore | 1 - svm-test-harness/Cargo.toml | 37 -- svm-test-harness/Makefile | 8 - svm-test-harness/bin/test_exec_instr.rs | 52 --- svm-test-harness/fixture/Cargo.toml | 43 --- svm-test-harness/fixture/build.rs | 44 --- svm-test-harness/fixture/src/account_state.rs | 61 ---- svm-test-harness/fixture/src/error.rs | 17 - svm-test-harness/fixture/src/instr_context.rs | 81 ----- svm-test-harness/fixture/src/instr_effects.rs | 55 --- svm-test-harness/fixture/src/lib.rs | 16 - svm-test-harness/instr/Cargo.toml | 58 ---- svm-test-harness/instr/src/builtins.rs | 43 --- svm-test-harness/instr/src/file.rs | 80 ----- svm-test-harness/instr/src/fuzz.rs | 145 -------- svm-test-harness/instr/src/keyed_account.rs | 27 -- svm-test-harness/instr/src/lib.rs | 20 -- svm-test-harness/instr/src/logger.rs | 9 - svm-test-harness/src/lib.rs | 3 - svm/Cargo.toml | 36 +- svm/src/conformance/account_state.rs | 51 +++ svm/src/conformance/context.rs | 126 +++++++ .../src/conformance}/feature_set.rs | 19 +- .../src => svm/src/conformance}/harness.rs | 199 ++++++++--- svm/src/conformance/mod.rs | 10 + .../src/conformance/programs.rs | 104 ++++-- .../src/conformance/sysvar.rs | 15 +- svm/src/lib.rs | 2 + 34 files changed, 608 insertions(+), 1297 deletions(-) delete mode 100644 svm-test-harness/.gitignore delete mode 100644 svm-test-harness/Cargo.toml delete mode 100644 svm-test-harness/Makefile delete mode 100644 svm-test-harness/bin/test_exec_instr.rs delete mode 100644 svm-test-harness/fixture/Cargo.toml delete mode 100644 svm-test-harness/fixture/build.rs delete mode 100644 svm-test-harness/fixture/src/account_state.rs delete mode 100644 svm-test-harness/fixture/src/error.rs delete mode 100644 svm-test-harness/fixture/src/instr_context.rs delete mode 100644 svm-test-harness/fixture/src/instr_effects.rs delete mode 100644 svm-test-harness/fixture/src/lib.rs delete mode 100644 svm-test-harness/instr/Cargo.toml delete mode 100644 svm-test-harness/instr/src/builtins.rs delete mode 100644 svm-test-harness/instr/src/file.rs delete mode 100644 svm-test-harness/instr/src/fuzz.rs delete mode 100644 svm-test-harness/instr/src/keyed_account.rs delete mode 100644 svm-test-harness/instr/src/lib.rs delete mode 100644 svm-test-harness/instr/src/logger.rs delete mode 100644 svm-test-harness/src/lib.rs create mode 100644 svm/src/conformance/account_state.rs create mode 100644 svm/src/conformance/context.rs rename {svm-test-harness/fixture/src => svm/src/conformance}/feature_set.rs (66%) rename {svm-test-harness/instr/src => svm/src/conformance}/harness.rs (63%) create mode 100644 svm/src/conformance/mod.rs rename svm-test-harness/instr/src/program_cache.rs => svm/src/conformance/programs.rs (53%) rename svm-test-harness/instr/src/sysvar_cache.rs => svm/src/conformance/sysvar.rs (60%) diff --git a/Cargo.lock b/Cargo.lock index 999008b9283..76809e5d901 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10597,6 +10597,8 @@ dependencies = [ name = "solana-svm" version = "4.2.0-alpha.0" dependencies = [ + "agave-feature-set", + "agave-precompiles", "ahash 0.8.12", "assert_matches", "bincode", @@ -10605,6 +10607,8 @@ dependencies = [ "log", "openssl", "percentage", + "prost 0.11.9", + "protosol", "qualifier_attr", "rand 0.9.4", "serde", @@ -10623,6 +10627,7 @@ dependencies = [ "solana-frozen-abi-macro", "solana-hash 4.3.0", "solana-instruction", + "solana-instruction-error", "solana-instructions-sysvar", "solana-keypair", "solana-loader-v3-interface", diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 560097dfab5..2d0d3886287 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -138,7 +138,7 @@ dependencies = [ "agave-snapshots", "assert_cmd", "chrono", - "clap 2.34.0", + "clap", "crossbeam-channel", "csv", "dashmap", @@ -316,7 +316,7 @@ name = "agave-store-tool" version = "4.2.0-alpha.0" dependencies = [ "ahash 0.8.12", - "clap 2.34.0", + "clap", "rayon", "solana-account 4.3.0", "solana-accounts-db", @@ -1547,46 +1547,6 @@ dependencies = [ "unicode-width 0.1.14", ] -[[package]] -name = "clap" -version = "4.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim 0.11.1", -] - -[[package]] -name = "clap_derive" -version = "4.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "clap_lex" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" - [[package]] name = "cmov" version = "0.5.2" @@ -2366,7 +2326,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "quote", "syn 2.0.117", @@ -2576,12 +2536,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "059c31d7d36c43fe39d89e55711858b4da8be7eb6dabac23c7289b1a19489406" -[[package]] -name = "fixedbitset" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" - [[package]] name = "fixedbitset" version = "0.5.7" @@ -2785,7 +2739,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" dependencies = [ - "rustix 1.1.4", + "rustix", "windows-link", ] @@ -2963,12 +2917,6 @@ dependencies = [ "foldhash 0.2.0", ] -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - [[package]] name = "heck" version = "0.5.0" @@ -3023,15 +2971,6 @@ dependencies = [ "hmac 0.8.1", ] -[[package]] -name = "home" -version = "0.5.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "http" version = "0.2.12" @@ -3919,12 +3858,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -4144,12 +4077,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "multimap" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" - [[package]] name = "multimap" version = "0.10.1" @@ -4577,23 +4504,13 @@ dependencies = [ "num", ] -[[package]] -name = "petgraph" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" -dependencies = [ - "fixedbitset 0.4.2", - "indexmap", -] - [[package]] name = "petgraph" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" dependencies = [ - "fixedbitset 0.5.7", + "fixedbitset", "hashbrown 0.15.5", "indexmap", ] @@ -4736,16 +4653,6 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a65843dfefbafd3c879c683306959a6de478443ffe9c9adf02f5976432402d7" -[[package]] -name = "prettyplease" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8646e95016a7a6c4adea95bafa8a16baab64b583356217f2c85db4a39d9a86" -dependencies = [ - "proc-macro2", - "syn 1.0.109", -] - [[package]] name = "prettyplease" version = "0.2.37" @@ -4816,16 +4723,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "prost" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" -dependencies = [ - "bytes", - "prost-derive 0.11.9", -] - [[package]] name = "prost" version = "0.14.3" @@ -4833,29 +4730,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" dependencies = [ "bytes", - "prost-derive 0.14.3", -] - -[[package]] -name = "prost-build" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "119533552c9a7ffacc21e099c24a0ac8bb19c2a2a3f363de84cd9b844feab270" -dependencies = [ - "bytes", - "heck 0.4.1", - "itertools 0.10.5", - "lazy_static", - "log", - "multimap 0.8.3", - "petgraph 0.6.5", - "prettyplease 0.1.25", - "prost 0.11.9", - "prost-types 0.11.9", - "regex", - "syn 1.0.109", - "tempfile", - "which", + "prost-derive", ] [[package]] @@ -4864,14 +4739,14 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7" dependencies = [ - "heck 0.5.0", + "heck", "itertools 0.14.0", "log", - "multimap 0.10.1", - "petgraph 0.8.3", - "prettyplease 0.2.37", - "prost 0.14.3", - "prost-types 0.14.3", + "multimap", + "petgraph", + "prettyplease", + "prost", + "prost-types", "pulldown-cmark", "pulldown-cmark-to-cmark", "regex", @@ -4879,19 +4754,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "prost-derive" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" -dependencies = [ - "anyhow", - "itertools 0.10.5", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "prost-derive" version = "0.14.3" @@ -4905,22 +4767,13 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "prost-types" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213622a1460818959ac1181aaeb2dc9c7f63df720db7d788b3e24eacd1983e13" -dependencies = [ - "prost 0.11.9", -] - [[package]] name = "prost-types" version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7" dependencies = [ - "prost 0.14.3", + "prost", ] [[package]] @@ -4952,16 +4805,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "protosol" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e70d64aff3756d57a2b5e84ee2a01923f2a5350a51b93e8ec4631b817d8dcd" -dependencies = [ - "prost 0.11.9", - "prost-build 0.11.9", -] - [[package]] name = "pulldown-cmark" version = "0.13.1" @@ -5518,19 +5361,6 @@ dependencies = [ "nom", ] -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags 2.11.1", - "errno", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", -] - [[package]] name = "rustix" version = "1.1.4" @@ -5540,7 +5370,7 @@ dependencies = [ "bitflags 2.11.1", "errno", "libc", - "linux-raw-sys 0.12.1", + "linux-raw-sys", "windows-sys 0.61.2", ] @@ -6375,7 +6205,7 @@ dependencies = [ "agave-feature-set", "agave-logger", "chrono", - "clap 2.34.0", + "clap", "crossbeam-channel", "csv", "log", @@ -6606,7 +6436,7 @@ name = "solana-clap-utils" version = "4.2.0-alpha.0" dependencies = [ "chrono", - "clap 2.34.0", + "clap", "rpassword", "solana-bls-signatures", "solana-clock", @@ -6649,7 +6479,7 @@ dependencies = [ "agave-reserved-account-keys", "base64 0.22.1", "chrono", - "clap 2.34.0", + "clap", "console 0.16.3", "humantime", "indicatif", @@ -7273,7 +7103,7 @@ dependencies = [ "agave-snapshots", "base64 0.22.1", "bincode", - "clap 2.34.0", + "clap", "itertools 0.14.0", "serde", "serde_json", @@ -7636,7 +7466,7 @@ dependencies = [ "mockall", "num_cpus", "num_enum", - "prost 0.14.3", + "prost", "qualifier_attr", "rand 0.9.4", "rand_chacha 0.9.0", @@ -8992,8 +8822,8 @@ dependencies = [ "http 1.4.0", "hyper-util", "log", - "prost 0.14.3", - "prost-types 0.14.3", + "prost", + "prost-types", "serde", "smpl_jwt", "solana-clock", @@ -9019,7 +8849,7 @@ name = "solana-storage-proto" version = "4.2.0-alpha.0" dependencies = [ "bs58", - "prost 0.14.3", + "prost", "protobuf-src", "serde", "solana-account-decoder", @@ -9078,22 +8908,29 @@ dependencies = [ name = "solana-svm" version = "4.2.0-alpha.0" dependencies = [ + "agave-feature-set", + "agave-precompiles", "ahash 0.8.12", "log", "percentage", "qualifier_attr", "serde", "solana-account 4.3.0", + "solana-bpf-loader-program", "solana-clock", + "solana-compute-budget", + "solana-compute-budget-program", "solana-fee-structure", "solana-hash 4.3.0", "solana-instruction", + "solana-instruction-error", "solana-instructions-sysvar", "solana-loader-v3-interface", "solana-loader-v4-interface", "solana-message", "solana-nonce", "solana-nonce-account", + "solana-precompile-error", "solana-program-entrypoint", "solana-program-pack", "solana-program-runtime", @@ -9107,7 +8944,9 @@ dependencies = [ "solana-svm-timings", "solana-svm-transaction", "solana-svm-type-overrides", + "solana-syscalls", "solana-system-interface 3.2.0", + "solana-system-program", "solana-sysvar-id", "solana-transaction-context", "solana-transaction-error", @@ -9140,67 +8979,6 @@ dependencies = [ name = "solana-svm-measure" version = "4.2.0-alpha.0" -[[package]] -name = "solana-svm-test-harness" -version = "4.2.0-alpha.0" -dependencies = [ - "clap 4.6.1", - "prost 0.14.3", - "solana-svm-test-harness-fixture", - "solana-svm-test-harness-instr", -] - -[[package]] -name = "solana-svm-test-harness-fixture" -version = "4.2.0-alpha.0" -dependencies = [ - "agave-feature-set", - "bincode", - "prost 0.14.3", - "prost-build 0.14.3", - "protosol", - "solana-account 4.3.0", - "solana-instruction", - "solana-instruction-error", - "solana-pubkey 4.2.0", - "solana-svm-feature-set", - "thiserror 2.0.18", -] - -[[package]] -name = "solana-svm-test-harness-instr" -version = "4.2.0-alpha.0" -dependencies = [ - "agave-feature-set", - "agave-precompiles", - "bincode", - "env_logger", - "prost 0.14.3", - "solana-account 4.3.0", - "solana-bpf-loader-program", - "solana-clock", - "solana-compute-budget", - "solana-compute-budget-program", - "solana-instruction", - "solana-instruction-error", - "solana-precompile-error", - "solana-program-runtime", - "solana-pubkey 4.2.0", - "solana-rent", - "solana-sdk-ids", - "solana-svm", - "solana-svm-callback", - "solana-svm-feature-set", - "solana-svm-log-collector", - "solana-svm-test-harness-fixture", - "solana-svm-timings", - "solana-svm-transaction", - "solana-syscalls", - "solana-system-program", - "solana-sysvar-id", - "solana-transaction-context", -] - [[package]] name = "solana-svm-timings" version = "4.2.0-alpha.0" @@ -10247,7 +10025,7 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "quote", "syn 2.0.117", @@ -10392,7 +10170,7 @@ dependencies = [ "fastrand", "getrandom 0.4.2", "once_cell", - "rustix 1.1.4", + "rustix", "windows-sys 0.61.2", ] @@ -10769,7 +10547,7 @@ version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c68f61875ac5293cf72e6c8cf0158086428c82c37229e98c840878f1706b0322" dependencies = [ - "prettyplease 0.2.37", + "prettyplease", "proc-macro2", "quote", "syn 2.0.117", @@ -10782,7 +10560,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0" dependencies = [ "bytes", - "prost 0.14.3", + "prost", "tonic", ] @@ -10792,10 +10570,10 @@ version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "654e5643eff75d7f8c99197ce1440ed19a3474eada74c12bbac488b2cafdae27" dependencies = [ - "prettyplease 0.2.37", + "prettyplease", "proc-macro2", - "prost-build 0.14.3", - "prost-types 0.14.3", + "prost-build", + "prost-types", "quote", "syn 2.0.117", "tempfile", @@ -11338,18 +11116,6 @@ dependencies = [ "rustls-pki-types", ] -[[package]] -name = "which" -version = "4.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" -dependencies = [ - "either", - "home", - "once_cell", - "rustix 0.38.44", -] - [[package]] name = "wide" version = "0.7.33" @@ -11732,7 +11498,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" dependencies = [ "anyhow", - "heck 0.5.0", + "heck", "wit-parser", ] @@ -11743,9 +11509,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", - "heck 0.5.0", + "heck", "indexmap", - "prettyplease 0.2.37", + "prettyplease", "syn 2.0.117", "wasm-metadata", "wit-bindgen-core", @@ -11759,7 +11525,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" dependencies = [ "anyhow", - "prettyplease 0.2.37", + "prettyplease", "proc-macro2", "quote", "syn 2.0.117", @@ -11843,7 +11609,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", - "rustix 1.1.4", + "rustix", ] [[package]] diff --git a/dev-bins/Cargo.toml b/dev-bins/Cargo.toml index 98acfaea738..de02fbd635a 100644 --- a/dev-bins/Cargo.toml +++ b/dev-bins/Cargo.toml @@ -1,12 +1,5 @@ [workspace] -members = [ - "../accounts-db/store-tool", - "../bench-tps", - "../ledger-tool", - "../svm-test-harness", - "../svm-test-harness/fixture", - "../svm-test-harness/instr", -] +members = ["../accounts-db/store-tool", "../bench-tps", "../ledger-tool"] resolver = "2" diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index acc6d8a9d19..c839746caf9 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -8370,7 +8370,6 @@ dependencies = [ "solana-stake-interface", "solana-svm", "solana-svm-feature-set", - "solana-svm-test-harness-instr", "solana-svm-timings", "solana-svm-transaction", "solana-svm-type-overrides", @@ -9429,22 +9428,29 @@ dependencies = [ name = "solana-svm" version = "4.2.0-alpha.0" dependencies = [ + "agave-feature-set", + "agave-precompiles", "ahash 0.8.12", "log", "percentage", "qualifier_attr", "serde", "solana-account 4.3.0", + "solana-bpf-loader-program", "solana-clock", + "solana-compute-budget", + "solana-compute-budget-program", "solana-fee-structure", "solana-hash 4.3.0", "solana-instruction", + "solana-instruction-error", "solana-instructions-sysvar", "solana-loader-v3-interface", "solana-loader-v4-interface", "solana-message", "solana-nonce", "solana-nonce-account", + "solana-precompile-error", "solana-program-entrypoint", "solana-program-pack", "solana-program-runtime", @@ -9458,7 +9464,9 @@ dependencies = [ "solana-svm-timings", "solana-svm-transaction", "solana-svm-type-overrides", + "solana-syscalls", "solana-system-interface 3.2.0", + "solana-system-program", "solana-sysvar-id", "solana-transaction-context", "solana-transaction-error", @@ -9491,49 +9499,6 @@ dependencies = [ name = "solana-svm-measure" version = "4.2.0-alpha.0" -[[package]] -name = "solana-svm-test-harness-fixture" -version = "4.2.0-alpha.0" -dependencies = [ - "solana-account 4.3.0", - "solana-instruction", - "solana-instruction-error", - "solana-pubkey 4.2.0", - "solana-svm-feature-set", - "thiserror 2.0.18", -] - -[[package]] -name = "solana-svm-test-harness-instr" -version = "4.2.0-alpha.0" -dependencies = [ - "bincode", - "env_logger", - "solana-account 4.3.0", - "solana-bpf-loader-program", - "solana-clock", - "solana-compute-budget", - "solana-compute-budget-program", - "solana-instruction", - "solana-instruction-error", - "solana-precompile-error", - "solana-program-runtime", - "solana-pubkey 4.2.0", - "solana-rent", - "solana-sdk-ids", - "solana-svm", - "solana-svm-callback", - "solana-svm-feature-set", - "solana-svm-log-collector", - "solana-svm-test-harness-fixture", - "solana-svm-timings", - "solana-svm-transaction", - "solana-syscalls", - "solana-system-program", - "solana-sysvar-id", - "solana-transaction-context", -] - [[package]] name = "solana-svm-timings" version = "4.2.0-alpha.0" diff --git a/programs/sbf/Cargo.toml b/programs/sbf/Cargo.toml index 609f26a4dce..9917fcce272 100644 --- a/programs/sbf/Cargo.toml +++ b/programs/sbf/Cargo.toml @@ -143,7 +143,6 @@ solana-sha256-hasher = { version = "=3.1.0", features = ["sha2"] } solana-stake-interface = { version = "=3.1.0", features = ["bincode"] } solana-svm = { path = "../../svm", version = "=4.2.0-alpha.0" } solana-svm-feature-set = { path = "../../svm-feature-set", version = "=4.2.0-alpha.0" } -solana-svm-test-harness-instr = { path = "../../svm-test-harness/instr", version = "=4.2.0-alpha.0" } solana-svm-timings = { path = "../../svm-timings", version = "=4.2.0-alpha.0" } solana-svm-transaction = { path = "../../svm-transaction", version = "=4.2.0-alpha.0" } solana-svm-type-overrides = { path = "../../svm-type-overrides", version = "=4.2.0-alpha.0" } @@ -202,9 +201,8 @@ solana-sbf-rust-realloc-invoke-dep = { workspace = true } solana-sdk-ids = "3.1.0" solana-signer = "3.0.0" solana-stake-interface = "3.0.0" -solana-svm = { workspace = true } +solana-svm = { workspace = true, features = ["dev-context-only-utils"] } solana-svm-feature-set = { workspace = true } -solana-svm-test-harness-instr = { workspace = true } solana-svm-timings = { workspace = true } solana-svm-transaction = { workspace = true } solana-svm-type-overrides = { workspace = true } diff --git a/programs/sbf/tests/programs.rs b/programs/sbf/tests/programs.rs index 0dbbc5ca644..c756760a4b4 100644 --- a/programs/sbf/tests/programs.rs +++ b/programs/sbf/tests/programs.rs @@ -75,15 +75,33 @@ use { solana_account::Account, solana_program_runtime::sysvar_cache::SysvarCache, solana_sdk_ids::sysvar::rent, - solana_svm_test_harness_instr::{ - self as harness, fixture::instr_context::InstrContext, - keyed_account::keyed_account_for_system_program, + solana_svm::conformance::{ + context::InstrContext, + harness::execute_instr, + programs::{ + add_program_to_program_cache, keyed_account_for_system_program, + new_program_cache_with_builtins, + }, }, + std::{fs::File, io::Read, path::PathBuf}, }; +#[cfg(any(feature = "sbf_c", feature = "sbf_rust"))] +fn load_program_elf(program_name: &str) -> Vec { + let sbf_out_dir = + std::env::var("SBF_OUT_DIR").expect("SBF_OUT_DIR must be set to locate program ELFs"); + let path = PathBuf::from(sbf_out_dir).join(format!("{program_name}.so")); + let mut file = File::open(&path) + .unwrap_or_else(|e| panic!("Failed to open file {}: {}", path.display(), e)); + let mut data = Vec::new(); + file.read_to_end(&mut data) + .unwrap_or_else(|e| panic!("Failed to read file {}: {}", path.display(), e)); + data +} + #[cfg(feature = "sbf_rust")] fn default_program_cache() -> solana_program_runtime::loaded_programs::ProgramCacheForTxBatch { - harness::program_cache::new_with_builtins(/* slot */ 0) + new_program_cache_with_builtins(/* slot */ 0) } fn default_program_cache_with_program( @@ -93,7 +111,7 @@ fn default_program_cache_with_program( compute_budget: &ComputeBudget, ) -> solana_program_runtime::loaded_programs::ProgramCacheForTxBatch { let mut program_cache = default_program_cache(); - harness::program_cache::add_program( + add_program_to_program_cache( &mut program_cache, program_id, &bpf_loader_upgradeable::id(), @@ -274,7 +292,7 @@ fn test_program_sbf_sanity() { for program in programs.iter() { println!("Test program: {:?}", program.0); - let program_elf = harness::file::load_program_elf(program.0); + let program_elf = load_program_elf(program.0); let program_id = Pubkey::new_unique(); let feature_set = SVMFeatureSet::all_enabled(); @@ -306,8 +324,7 @@ fn test_program_sbf_sanity() { }; let effects = - harness::execute_instr(context, &compute_budget, &mut program_cache, &sysvar_cache) - .unwrap(); + execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache).unwrap(); let result = match effects.result { Some(err) => Err(err), @@ -340,7 +357,7 @@ fn test_program_sbf_loader_deprecated() { for program in programs.iter() { println!("Test program: {:?}", program); - let program_elf = harness::file::load_program_elf(program); + let program_elf = load_program_elf(program); let program_id = Pubkey::new_unique(); let feature_set = SVMFeatureSet::all_enabled(); @@ -354,7 +371,7 @@ fn test_program_sbf_loader_deprecated() { ]; let mut program_cache = default_program_cache(); - harness::program_cache::add_program( + add_program_to_program_cache( &mut program_cache, &program_id, &bpf_loader_deprecated::id(), @@ -374,8 +391,7 @@ fn test_program_sbf_loader_deprecated() { }; let effects = - harness::execute_instr(context, &compute_budget, &mut program_cache, &sysvar_cache) - .unwrap(); + execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache).unwrap(); assert!(effects.result.is_none()); } @@ -396,7 +412,7 @@ fn test_sol_alloc_free_no_longer_deployable_with_upgradeable_loader() { // In fact, `sol_alloc_free_` is called from sbf allocator, which is originated from // AccountInfo::realloc() in the program code. - let program_elf = harness::file::load_program_elf("solana_sbf_rust_deprecated_loader"); + let program_elf = load_program_elf("solana_sbf_rust_deprecated_loader"); let program_id = Pubkey::new_unique(); let authority_pubkey = Pubkey::new_unique(); let buffer_pubkey = Pubkey::new_unique(); @@ -497,8 +513,7 @@ fn test_sol_alloc_free_no_longer_deployable_with_upgradeable_loader() { // found in syscall table. Hence, the verification fails and the deployment // fails. let effects = - harness::execute_instr(context, &compute_budget, &mut program_cache, &sysvar_cache) - .unwrap(); + execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache).unwrap(); assert_eq!(effects.result, Some(InstructionError::InvalidAccountData)); } @@ -521,7 +536,7 @@ fn test_program_sbf_duplicate_accounts() { for program in programs.iter() { println!("Test program: {:?}", program); - let program_elf = harness::file::load_program_elf(program); + let program_elf = load_program_elf(program); let program_id = Pubkey::new_unique(); let feature_set = SVMFeatureSet::all_enabled(); let compute_budget = ComputeBudget::new_with_defaults(false); @@ -557,8 +572,7 @@ fn test_program_sbf_duplicate_accounts() { accounts, instruction, }; - harness::execute_instr(context, &compute_budget, &mut program_cache, &sysvar_cache) - .unwrap() + execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache).unwrap() }; let effects = execute(&[1]); @@ -611,8 +625,7 @@ fn test_program_sbf_duplicate_accounts() { instruction, }; let effects = - harness::execute_instr(context, &compute_budget, &mut program_cache, &sysvar_cache) - .unwrap(); + execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache).unwrap(); assert!(effects.result.is_none()); } } @@ -635,7 +648,7 @@ fn test_program_sbf_error_handling() { for program in programs.iter() { println!("Test program: {:?}", program); - let program_elf = harness::file::load_program_elf(program); + let program_elf = load_program_elf(program); let program_id = Pubkey::new_unique(); let feature_set = SVMFeatureSet::all_enabled(); @@ -664,8 +677,7 @@ fn test_program_sbf_error_handling() { instruction, }; - harness::execute_instr(context, &compute_budget, &mut program_cache, &sysvar_cache) - .unwrap() + execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache).unwrap() }; let effects = execute(&[1]); @@ -730,7 +742,7 @@ fn test_return_data_and_log_data_syscall() { for program in programs.iter() { println!("Test program: {:?}", program); - let program_elf = harness::file::load_program_elf(program); + let program_elf = load_program_elf(program); let program_id = Pubkey::new_unique(); let feature_set = SVMFeatureSet::all_enabled(); @@ -758,8 +770,7 @@ fn test_return_data_and_log_data_syscall() { }; let effects = - harness::execute_instr(context, &compute_budget, &mut program_cache, &sysvar_cache) - .unwrap(); + execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache).unwrap(); assert!(effects.result.is_none()); @@ -1406,8 +1417,8 @@ fn test_program_sbf_invoke_sanity() { #[test] #[cfg(feature = "sbf_rust")] fn test_program_sbf_program_id_spoofing() { - let spoof1_elf = harness::file::load_program_elf("solana_sbf_rust_spoof1"); - let spoof1_system_elf = harness::file::load_program_elf("solana_sbf_rust_spoof1_system"); + let spoof1_elf = load_program_elf("solana_sbf_rust_spoof1"); + let spoof1_system_elf = load_program_elf("solana_sbf_rust_spoof1_system"); let malicious_swap_pubkey = Pubkey::new_unique(); let malicious_system_pubkey = Pubkey::new_unique(); @@ -1428,8 +1439,8 @@ fn test_program_sbf_program_id_spoofing() { (to_pubkey, Account::new(0, 0, &system_program::id())), ]; - let mut program_cache = harness::program_cache::new_with_builtins(0); - harness::program_cache::add_program( + let mut program_cache = new_program_cache_with_builtins(0); + add_program_to_program_cache( &mut program_cache, &malicious_swap_pubkey, &bpf_loader_upgradeable::id(), @@ -1437,7 +1448,7 @@ fn test_program_sbf_program_id_spoofing() { &feature_set, &compute_budget, ); - harness::program_cache::add_program( + add_program_to_program_cache( &mut program_cache, &malicious_system_pubkey, &bpf_loader_upgradeable::id(), @@ -1464,8 +1475,7 @@ fn test_program_sbf_program_id_spoofing() { }; let effects = - harness::execute_instr(context, &compute_budget, &mut program_cache, &sysvar_cache) - .unwrap(); + execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache).unwrap(); assert_eq!( effects.result, @@ -1482,7 +1492,7 @@ fn test_program_sbf_program_id_spoofing() { #[test] #[cfg(feature = "sbf_rust")] fn test_program_sbf_caller_has_access_to_cpi_program() { - let caller_access_elf = harness::file::load_program_elf("solana_sbf_rust_caller_access"); + let caller_access_elf = load_program_elf("solana_sbf_rust_caller_access"); let caller_pubkey = Pubkey::new_unique(); let caller2_pubkey = Pubkey::new_unique(); @@ -1501,8 +1511,8 @@ fn test_program_sbf_caller_has_access_to_cpi_program() { ), ]; - let mut program_cache = harness::program_cache::new_with_builtins(0); - harness::program_cache::add_program( + let mut program_cache = new_program_cache_with_builtins(0); + add_program_to_program_cache( &mut program_cache, &caller_pubkey, &bpf_loader_upgradeable::id(), @@ -1510,7 +1520,7 @@ fn test_program_sbf_caller_has_access_to_cpi_program() { &feature_set, &compute_budget, ); - harness::program_cache::add_program( + add_program_to_program_cache( &mut program_cache, &caller2_pubkey, &bpf_loader_upgradeable::id(), @@ -1533,8 +1543,7 @@ fn test_program_sbf_caller_has_access_to_cpi_program() { }; let effects = - harness::execute_instr(context, &compute_budget, &mut program_cache, &sysvar_cache) - .unwrap(); + execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache).unwrap(); assert_eq!(effects.result, Some(InstructionError::MissingAccount)); } @@ -1544,7 +1553,7 @@ fn test_program_sbf_caller_has_access_to_cpi_program() { fn test_program_sbf_ro_modify() { agave_logger::setup(); - let program_elf = harness::file::load_program_elf("solana_sbf_rust_ro_modify"); + let program_elf = load_program_elf("solana_sbf_rust_ro_modify"); let program_id = Pubkey::new_unique(); let feature_set = SVMFeatureSet::all_enabled(); @@ -1580,8 +1589,7 @@ fn test_program_sbf_ro_modify() { }; let effects = - harness::execute_instr(context, &compute_budget, &mut program_cache, &sysvar_cache) - .unwrap(); + execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache).unwrap(); assert_eq!( effects.result, @@ -1595,7 +1603,7 @@ fn test_program_sbf_ro_modify() { fn test_program_sbf_call_depth() { agave_logger::setup(); - let program_elf = harness::file::load_program_elf("solana_sbf_rust_call_depth"); + let program_elf = load_program_elf("solana_sbf_rust_call_depth"); let program_id = Pubkey::new_unique(); let feature_set = SVMFeatureSet::all_enabled(); @@ -1618,7 +1626,7 @@ fn test_program_sbf_call_depth() { instruction, }; - harness::execute_instr(context, &compute_budget, &mut program_cache, &sysvar_cache).unwrap() + execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache).unwrap() }; let effects = execute(compute_budget.max_call_depth - 1); @@ -1633,7 +1641,7 @@ fn test_program_sbf_call_depth() { fn test_program_sbf_compute_budget() { agave_logger::setup(); - let program_elf = harness::file::load_program_elf("solana_sbf_rust_noop"); + let program_elf = load_program_elf("solana_sbf_rust_noop"); let program_id = Pubkey::new_unique(); let feature_set = SVMFeatureSet::all_enabled(); @@ -1662,8 +1670,7 @@ fn test_program_sbf_compute_budget() { }; let effects = - harness::execute_instr(context, &compute_budget, &mut program_cache, &sysvar_cache) - .unwrap(); + execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache).unwrap(); assert_eq!( effects.result, @@ -1720,7 +1727,7 @@ fn assert_instruction_count() { println!("\n {:36} expected actual diff", "SBF program"); for (program_name, expected_consumption) in programs.iter() { - let program_elf = harness::file::load_program_elf(program_name); + let program_elf = load_program_elf(program_name); let program_id = Pubkey::new_unique(); let mut program_cache = default_program_cache_with_program( @@ -1747,8 +1754,7 @@ fn assert_instruction_count() { }; let effects = - harness::execute_instr(context, &compute_budget, &mut program_cache, &sysvar_cache) - .unwrap(); + execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache).unwrap(); let consumption = compute_budget .compute_unit_limit @@ -1834,8 +1840,7 @@ fn test_program_sbf_instruction_introspection() { fn test_program_sbf_r2_instruction_data_pointer(num_accounts: usize, input_data_len: usize) { agave_logger::setup(); - let program_elf = - harness::file::load_program_elf("solana_sbf_rust_r2_instruction_data_pointer"); + let program_elf = load_program_elf("solana_sbf_rust_r2_instruction_data_pointer"); let program_id = Pubkey::new_unique(); let feature_set = SVMFeatureSet::all_enabled(); @@ -1878,8 +1883,7 @@ fn test_program_sbf_r2_instruction_data_pointer(num_accounts: usize, input_data_ }; let effects = - harness::execute_instr(context, &compute_budget, &mut program_cache, &sysvar_cache) - .unwrap(); + execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache).unwrap(); assert!(effects.result.is_none()); assert_eq!(input_data, effects.return_data); @@ -2375,7 +2379,7 @@ fn test_program_sbf_disguised_as_sbf_loader() { } for program in programs.iter() { - let program_elf = harness::file::load_program_elf(program); + let program_elf = load_program_elf(program); let program_id = Pubkey::new_unique(); let feature_set = SVMFeatureSet { @@ -2385,7 +2389,7 @@ fn test_program_sbf_disguised_as_sbf_loader() { let compute_budget = ComputeBudget::new_with_defaults(false); let mut program_cache = default_program_cache(); - harness::program_cache::add_program( + add_program_to_program_cache( &mut program_cache, &program_id, &bpf_loader::id(), @@ -2405,8 +2409,7 @@ fn test_program_sbf_disguised_as_sbf_loader() { }; let effects = - harness::execute_instr(context, &compute_budget, &mut program_cache, &sysvar_cache) - .unwrap(); + execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache).unwrap(); assert_eq!(effects.result, Some(InstructionError::UnsupportedProgramId)); } } @@ -2416,14 +2419,14 @@ fn test_program_sbf_disguised_as_sbf_loader() { fn test_program_reads_from_program_account() { agave_logger::setup(); - let program_elf = harness::file::load_program_elf("read_program"); + let program_elf = load_program_elf("read_program"); let program_id = Pubkey::new_unique(); let feature_set = SVMFeatureSet::all_enabled(); let compute_budget = ComputeBudget::new_with_defaults(false); - let mut program_cache = harness::program_cache::new_with_builtins(0); - harness::program_cache::add_program( + let mut program_cache = new_program_cache_with_builtins(0); + add_program_to_program_cache( &mut program_cache, &program_id, &bpf_loader::id(), @@ -2453,8 +2456,7 @@ fn test_program_reads_from_program_account() { }; let effects = - harness::execute_instr(context, &compute_budget, &mut program_cache, &sysvar_cache) - .unwrap(); + execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache).unwrap(); assert!(effects.result.is_none()); } @@ -2463,13 +2465,13 @@ fn test_program_reads_from_program_account() { fn test_program_sbf_c_dup() { agave_logger::setup(); - let program_elf = harness::file::load_program_elf("ser"); + let program_elf = load_program_elf("ser"); let program_id = Pubkey::new_unique(); let feature_set = SVMFeatureSet::all_enabled(); let compute_budget = ComputeBudget::new_with_defaults(false); - let mut program_cache = harness::program_cache::new_with_builtins(0); - harness::program_cache::add_program( + let mut program_cache = new_program_cache_with_builtins(0); + add_program_to_program_cache( &mut program_cache, &program_id, &bpf_loader_upgradeable::id(), @@ -2497,8 +2499,7 @@ fn test_program_sbf_c_dup() { }; let effects = - harness::execute_instr(context, &compute_budget, &mut program_cache, &sysvar_cache) - .unwrap(); + execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache).unwrap(); assert!(effects.result.is_none()); } @@ -2673,7 +2674,7 @@ fn test_program_sbf_upgrade_via_cpi() { fn test_program_sbf_ro_account_modify() { agave_logger::setup(); - let program_elf = harness::file::load_program_elf("solana_sbf_rust_ro_account_modify"); + let program_elf = load_program_elf("solana_sbf_rust_ro_account_modify"); let program_id = Pubkey::new_unique(); let feature_set = SVMFeatureSet::all_enabled(); @@ -2705,8 +2706,7 @@ fn test_program_sbf_ro_account_modify() { }; let effects = - harness::execute_instr(context, &compute_budget, &mut program_cache, &sysvar_cache) - .unwrap(); + execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache).unwrap(); assert_eq!(effects.result, Some(InstructionError::ReadonlyDataModified)); } @@ -4595,7 +4595,7 @@ fn test_deplete_cost_meter_with_access_violation() { fn test_program_sbf_deplete_cost_meter_with_divide_by_zero() { agave_logger::setup(); - let program_elf = harness::file::load_program_elf("solana_sbf_rust_divide_by_zero"); + let program_elf = load_program_elf("solana_sbf_rust_divide_by_zero"); let program_id = Pubkey::new_unique(); let feature_set = SVMFeatureSet::all_enabled(); @@ -4622,8 +4622,7 @@ fn test_program_sbf_deplete_cost_meter_with_divide_by_zero() { }; let effects = - harness::execute_instr(context, &compute_budget, &mut program_cache, &sysvar_cache) - .unwrap(); + execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache).unwrap(); assert_eq!( effects.result, @@ -5661,7 +5660,7 @@ fn test_mem_syscalls_overlap_account_begin_or_end() { fn test_program_sbf_rust_direct_account_pointers(num_accounts: usize, input_data_len: usize) { agave_logger::setup(); - let program_elf = harness::file::load_program_elf("solana_sbf_rust_direct_account_pointers"); + let program_elf = load_program_elf("solana_sbf_rust_direct_account_pointers"); let program_id = Pubkey::new_unique(); let feature_set = SVMFeatureSet::all_enabled(); @@ -5709,8 +5708,7 @@ fn test_program_sbf_rust_direct_account_pointers(num_accounts: usize, input_data }; let effects = - harness::execute_instr(context, &compute_budget, &mut program_cache, &sysvar_cache) - .unwrap(); + execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache).unwrap(); assert!(effects.result.is_none()); // `num_accounts * 2` will be added as return data. diff --git a/svm-test-harness/.gitignore b/svm-test-harness/.gitignore deleted file mode 100644 index 995bba6cb2c..00000000000 --- a/svm-test-harness/.gitignore +++ /dev/null @@ -1 +0,0 @@ -dump diff --git a/svm-test-harness/Cargo.toml b/svm-test-harness/Cargo.toml deleted file mode 100644 index 02ec56401cd..00000000000 --- a/svm-test-harness/Cargo.toml +++ /dev/null @@ -1,37 +0,0 @@ -[package] -name = "solana-svm-test-harness" -description = "Solana SVM test harness." -documentation = "https://docs.rs/solana-svm-test-harness" -version = { workspace = true } -authors = { workspace = true } -repository = { workspace = true } -homepage = { workspace = true } -license = { workspace = true } -edition = { workspace = true } -workspace = "../dev-bins" -publish = false - -[[bin]] -name = "test_exec_instr" -path = "bin/test_exec_instr.rs" -required-features = ["fuzz"] - -[features] -agave-unstable-api = [] -dev-context-only-utils = [] -dummy-for-ci-check = ["fuzz"] -fuzz = [ - "dep:clap", - "dep:prost", - "solana-svm-test-harness-fixture/fuzz", - "solana-svm-test-harness-instr/fuzz", -] - -[dependencies] -clap = { version = "4.5.2", features = ["derive"], optional = true } -prost = { workspace = true, optional = true } -solana-svm-test-harness-fixture = { workspace = true } -solana-svm-test-harness-instr = { workspace = true } - -[lints] -workspace = true diff --git a/svm-test-harness/Makefile b/svm-test-harness/Makefile deleted file mode 100644 index d00378429a4..00000000000 --- a/svm-test-harness/Makefile +++ /dev/null @@ -1,8 +0,0 @@ - -CARGO?=cargo - -test: | binaries - -binaries: - $(CARGO) rustc --manifest-path ./Cargo.toml --lib --release --features fuzz --crate-type cdylib - $(CARGO) build --manifest-path ./Cargo.toml --bins --release --features fuzz diff --git a/svm-test-harness/bin/test_exec_instr.rs b/svm-test-harness/bin/test_exec_instr.rs deleted file mode 100644 index 653f50b208a..00000000000 --- a/svm-test-harness/bin/test_exec_instr.rs +++ /dev/null @@ -1,52 +0,0 @@ -use { - clap::Parser, - prost::Message, - solana_svm_test_harness::instr::{ - fixture::proto::InstrFixture as ProtoInstrFixture, fuzz::execute_instr_proto, - }, - std::path::PathBuf, -}; - -#[derive(Parser)] -#[command(version, about, long_about = None)] -struct Cli { - inputs: Vec, -} - -fn exec(input: &PathBuf) -> bool { - let blob = std::fs::read(input).unwrap(); - let fixture = ProtoInstrFixture::decode(&blob[..]).unwrap(); - let Some(context) = fixture.input else { - println!("No context found."); - return false; - }; - - let Some(expected) = fixture.output else { - println!("No fixture found."); - return false; - }; - let Some(effects) = execute_instr_proto(context) else { - println!("FAIL: No instruction effects returned for input: {input:?}",); - return false; - }; - - let ok = effects == expected; - - if ok { - println!("OK: {input:?}"); - } else { - println!("FAIL: {input:?}"); - } - ok -} - -fn main() { - let cli = Cli::parse(); - let mut fail_cnt: i32 = 0; - for input in cli.inputs { - if !exec(&input) { - fail_cnt = fail_cnt.saturating_add(1); - } - } - std::process::exit(fail_cnt); -} diff --git a/svm-test-harness/fixture/Cargo.toml b/svm-test-harness/fixture/Cargo.toml deleted file mode 100644 index a8cf8def630..00000000000 --- a/svm-test-harness/fixture/Cargo.toml +++ /dev/null @@ -1,43 +0,0 @@ -[package] -name = "solana-svm-test-harness-fixture" -description = "Solana SVM test harness fixture types." -documentation = "https://docs.rs/solana-svm-test-harness-fixture" -version = { workspace = true } -authors = { workspace = true } -repository = { workspace = true } -homepage = { workspace = true } -license = { workspace = true } -edition = { workspace = true } -workspace = "../../dev-bins" -publish = false - -[features] -agave-unstable-api = [] -dev-context-only-utils = [] -dummy-for-ci-check = ["fuzz"] -fuzz = [ - "dep:agave-feature-set", - "dep:bincode", - "dep:prost", - "dep:prost-build", - "dep:protosol", - "solana-pubkey/default", -] - -[dependencies] -agave-feature-set = { workspace = true, optional = true } -bincode = { workspace = true, optional = true } -prost = { workspace = true, optional = true } -protosol = { workspace = true, optional = true } -solana-account = { workspace = true } -solana-instruction = { workspace = true } -solana-instruction-error = { workspace = true, features = ["serde"] } -solana-pubkey = { workspace = true } -solana-svm-feature-set = { workspace = true } -thiserror = { workspace = true } - -[build-dependencies] -prost-build = { workspace = true, optional = true } - -[lints] -workspace = true diff --git a/svm-test-harness/fixture/build.rs b/svm-test-harness/fixture/build.rs deleted file mode 100644 index f3010ef2a4c..00000000000 --- a/svm-test-harness/fixture/build.rs +++ /dev/null @@ -1,44 +0,0 @@ -fn main() -> Result<(), Box> { - // Only compile protobuf files when the fuzz feature is enabled. - #[cfg(feature = "fuzz")] - { - use std::{env, fs, path::PathBuf}; - - // Get absolute proto dir from producer - let proto_dir = PathBuf::from( - env::var("DEP_PROTOSOL_PROTO_DIR") - .expect("protosol did not expose PROTO_DIR, did protosol build.rs run first?"), - ); - - println!("cargo:rerun-if-env-changed=DEP_PROTOSOL_PROTO_DIR"); - println!("cargo:rerun-if-changed={}", proto_dir.display()); - - // Collect absolute .proto paths - let mut proto_files = vec![]; - for entry in fs::read_dir(&proto_dir)? { - let path = entry?.path(); - if path.extension().and_then(|e| e.to_str()) == Some("proto") { - println!("cargo:rerun-if-changed={}", path.display()); - proto_files.push(path); - } - } - - // Ensure deterministic order for rebuilds - proto_files.sort(); - - // Compile protos into Rust - let out_dir = PathBuf::from(env::var("OUT_DIR")?); - let mut config = prost_build::Config::new(); - config.out_dir(&out_dir); - - config.compile_protos( - &proto_files - .iter() - .map(|p| p.display().to_string()) - .collect::>(), - &[proto_dir.to_str().unwrap()], - )?; - } - - Ok(()) -} diff --git a/svm-test-harness/fixture/src/account_state.rs b/svm-test-harness/fixture/src/account_state.rs deleted file mode 100644 index 0f6132cb301..00000000000 --- a/svm-test-harness/fixture/src/account_state.rs +++ /dev/null @@ -1,61 +0,0 @@ -//! Account state conversions for protobuf support. - -#![cfg(feature = "fuzz")] - -use { - crate::{error::FixtureError, proto::AcctState as ProtoAccount}, - solana_account::Account, - solana_pubkey::Pubkey, -}; - -// Default `rent_epoch` field value for all accounts. -const RENT_EXEMPT_RENT_EPOCH: u64 = u64::MAX; - -impl TryFrom for (Pubkey, Account) { - type Error = FixtureError; - - fn try_from(value: ProtoAccount) -> Result { - let ProtoAccount { - address, - owner, - lamports, - data, - executable, - .. - } = value; - - let pubkey = Pubkey::try_from(address).map_err(FixtureError::InvalidPubkeyBytes)?; - let owner = Pubkey::try_from(owner).map_err(FixtureError::InvalidPubkeyBytes)?; - - Ok(( - pubkey, - Account { - data, - executable, - lamports, - owner, - rent_epoch: RENT_EXEMPT_RENT_EPOCH, - }, - )) - } -} - -impl From<(Pubkey, Account)> for ProtoAccount { - fn from(value: (Pubkey, Account)) -> Self { - let Account { - lamports, - data, - owner, - executable, - .. - } = value.1; - - ProtoAccount { - address: value.0.to_bytes().to_vec(), - owner: owner.to_bytes().to_vec(), - lamports, - data, - executable, - } - } -} diff --git a/svm-test-harness/fixture/src/error.rs b/svm-test-harness/fixture/src/error.rs deleted file mode 100644 index d26f56b5a0d..00000000000 --- a/svm-test-harness/fixture/src/error.rs +++ /dev/null @@ -1,17 +0,0 @@ -//! Error types for fixture conversions. - -#![cfg(feature = "fuzz")] - -use thiserror::Error; - -#[derive(Debug, Error, PartialEq)] -pub enum FixtureError { - #[error("Invalid fixture input")] - InvalidFixtureInput, - - #[error("Invalid public key bytes")] - InvalidPubkeyBytes(Vec), - - #[error("An account is missing for instruction account index {0}")] - AccountMissingForInstrAccount(usize), -} diff --git a/svm-test-harness/fixture/src/instr_context.rs b/svm-test-harness/fixture/src/instr_context.rs deleted file mode 100644 index eee3f2ea9a0..00000000000 --- a/svm-test-harness/fixture/src/instr_context.rs +++ /dev/null @@ -1,81 +0,0 @@ -//! Instruction context (input). - -use { - solana_account::Account, solana_instruction::Instruction, solana_pubkey::Pubkey, - solana_svm_feature_set::SVMFeatureSet, -}; - -/// Instruction context fixture. -pub struct InstrContext { - pub feature_set: SVMFeatureSet, - pub accounts: Vec<(Pubkey, Account)>, - pub instruction: Instruction, -} - -#[cfg(feature = "fuzz")] -use { - crate::{error::FixtureError, proto::InstrContext as ProtoInstrContext}, - agave_feature_set::FeatureSet, - solana_instruction::AccountMeta, -}; - -#[cfg(feature = "fuzz")] -impl TryFrom for InstrContext { - type Error = FixtureError; - - fn try_from(value: ProtoInstrContext) -> Result { - let program_id = Pubkey::new_from_array( - value - .program_id - .try_into() - .map_err(FixtureError::InvalidPubkeyBytes)?, - ); - - let agave_feature_set: FeatureSet = value - .epoch_context - .as_ref() - .and_then(|epoch_ctx| epoch_ctx.features.as_ref()) - .map(|fs| fs.into()) - .unwrap_or_default(); - let feature_set = agave_feature_set.runtime_features(); - - let accounts: Vec<(Pubkey, Account)> = value - .accounts - .into_iter() - .map(|acct_state| acct_state.try_into()) - .collect::, _>>()?; - - let instruction_accounts = value - .instr_accounts - .into_iter() - .map(|acct| { - if acct.index as usize >= accounts.len() { - return Err(FixtureError::AccountMissingForInstrAccount( - acct.index as usize, - )); - } - Ok(AccountMeta { - pubkey: accounts[acct.index as usize].0, - is_signer: acct.is_signer, - is_writable: acct.is_writable, - }) - }) - .collect::, _>>()?; - - if instruction_accounts.len() > 128 { - return Err(FixtureError::InvalidFixtureInput); - } - - let instruction = Instruction { - accounts: instruction_accounts, - data: value.data, - program_id, - }; - - Ok(Self { - feature_set, - accounts, - instruction, - }) - } -} diff --git a/svm-test-harness/fixture/src/instr_effects.rs b/svm-test-harness/fixture/src/instr_effects.rs deleted file mode 100644 index bdc2b754548..00000000000 --- a/svm-test-harness/fixture/src/instr_effects.rs +++ /dev/null @@ -1,55 +0,0 @@ -//! Instruction effects (output). - -use {solana_account::Account, solana_instruction_error::InstructionError, solana_pubkey::Pubkey}; - -/// Represents the effects of a single instruction. -pub struct InstrEffects { - pub result: Option, - pub custom_err: Option, - pub resulting_accounts: Vec<(Pubkey, Account)>, - pub cu_avail: u64, - pub return_data: Vec, - pub logs: Vec, -} - -impl InstrEffects { - /// Returns the resulting account for the given pubkey, if it exists. - pub fn get_account(&self, pubkey: &Pubkey) -> Option<&Account> { - self.resulting_accounts - .iter() - .find(|(pk, _)| pk == pubkey) - .map(|(_, acc)| acc) - } -} - -#[cfg(feature = "fuzz")] -use {crate::proto::InstrEffects as ProtoInstrEffects, bincode}; - -#[cfg(feature = "fuzz")] -impl From for ProtoInstrEffects { - fn from(value: InstrEffects) -> Self { - let InstrEffects { - result, - custom_err, - resulting_accounts, - cu_avail, - return_data, - .. - } = value; - - Self { - result: result - .as_ref() - .map(|error| { - let serialized_err = bincode::serialize(error).unwrap(); - i32::from_le_bytes((&serialized_err[0..4]).try_into().unwrap()) - .saturating_add(1) - }) - .unwrap_or_default(), - custom_err: custom_err.unwrap_or_default(), - modified_accounts: resulting_accounts.into_iter().map(Into::into).collect(), - cu_avail, - return_data, - } - } -} diff --git a/svm-test-harness/fixture/src/lib.rs b/svm-test-harness/fixture/src/lib.rs deleted file mode 100644 index f3a2c8d1fb3..00000000000 --- a/svm-test-harness/fixture/src/lib.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Fixture types for SVM testing. -//! -//! This module provides native Rust types for testing program execution. -//! When the `fuzz` feature is enabled, it also includes conversions -//! between Firedancer's protobuf payloads and Solana SDK types. - -pub mod account_state; -pub mod error; -pub mod feature_set; -pub mod instr_context; -pub mod instr_effects; - -#[cfg(feature = "fuzz")] -pub mod proto { - include!(concat!(env!("OUT_DIR"), "/org.solana.sealevel.v1.rs")); -} diff --git a/svm-test-harness/instr/Cargo.toml b/svm-test-harness/instr/Cargo.toml deleted file mode 100644 index b4f2faf886b..00000000000 --- a/svm-test-harness/instr/Cargo.toml +++ /dev/null @@ -1,58 +0,0 @@ -[package] -name = "solana-svm-test-harness-instr" -description = "Solana SVM test harness for instruction execution." -documentation = "https://docs.rs/solana-svm-test-harness-instr" -version = { workspace = true } -authors = { workspace = true } -repository = { workspace = true } -homepage = { workspace = true } -license = { workspace = true } -edition = { workspace = true } -workspace = "../../dev-bins" -publish = false - -[features] -agave-unstable-api = [] -dev-context-only-utils = [] -dummy-for-ci-check = ["fuzz"] -metrics = ["solana-program-runtime/metrics", "solana-svm/metrics"] -fuzz = [ - "dep:agave-feature-set", - "dep:agave-precompiles", - "dep:prost", - "solana-pubkey/default", - "solana-svm-test-harness-fixture/fuzz", -] - -[dependencies] -agave-feature-set = { workspace = true, optional = true } -agave-precompiles = { workspace = true, optional = true } -bincode = { workspace = true } -env_logger = { workspace = true } -prost = { workspace = true, optional = true } -solana-account = { workspace = true } -solana-bpf-loader-program = { workspace = true } -solana-clock = { workspace = true, features = ["sysvar"] } -solana-compute-budget = { workspace = true } -solana-compute-budget-program = { workspace = true } -solana-instruction = { workspace = true } -solana-instruction-error = { workspace = true, features = ["serde"] } -solana-precompile-error = { workspace = true } -solana-program-runtime = { workspace = true, features = ["dev-context-only-utils"] } -solana-pubkey = { workspace = true } -solana-rent = { workspace = true, features = ["sysvar"] } -solana-sdk-ids = { workspace = true } -solana-svm = { workspace = true } -solana-svm-callback = { workspace = true } -solana-svm-feature-set = { workspace = true } -solana-svm-log-collector = { workspace = true } -solana-svm-test-harness-fixture = { workspace = true } -solana-svm-timings = { workspace = true } -solana-svm-transaction = { workspace = true } -solana-syscalls = { workspace = true } -solana-system-program = { workspace = true } -solana-sysvar-id = { workspace = true } -solana-transaction-context = { workspace = true } - -[lints] -workspace = true diff --git a/svm-test-harness/instr/src/builtins.rs b/svm-test-harness/instr/src/builtins.rs deleted file mode 100644 index f18c146d7e4..00000000000 --- a/svm-test-harness/instr/src/builtins.rs +++ /dev/null @@ -1,43 +0,0 @@ -//! SVM-resident builtin programs for the test harness. - -use { - solana_program_runtime::{ - invoke_context::BuiltinFunctionRegisterer, solana_sbpf::program::BuiltinFunctionDefinition, - }, - solana_pubkey::Pubkey, - solana_sdk_ids::{bpf_loader, bpf_loader_deprecated, bpf_loader_upgradeable}, -}; - -pub struct SvmBuiltinPrototype { - pub program_id: Pubkey, - pub name: &'static str, - pub register_fn: BuiltinFunctionRegisterer, -} - -pub static SVM_BUILTINS: &[SvmBuiltinPrototype] = &[ - SvmBuiltinPrototype { - program_id: solana_system_program::id(), - name: "system_program", - register_fn: solana_system_program::system_processor::Entrypoint::register, - }, - SvmBuiltinPrototype { - program_id: bpf_loader_deprecated::id(), - name: "solana_bpf_loader_deprecated_program", - register_fn: solana_bpf_loader_program::Entrypoint::register, - }, - SvmBuiltinPrototype { - program_id: bpf_loader::id(), - name: "solana_bpf_loader_program", - register_fn: solana_bpf_loader_program::Entrypoint::register, - }, - SvmBuiltinPrototype { - program_id: bpf_loader_upgradeable::id(), - name: "solana_bpf_loader_upgradeable_program", - register_fn: solana_bpf_loader_program::Entrypoint::register, - }, - SvmBuiltinPrototype { - program_id: solana_sdk_ids::compute_budget::id(), - name: "compute_budget_program", - register_fn: solana_compute_budget_program::Entrypoint::register, - }, -]; diff --git a/svm-test-harness/instr/src/file.rs b/svm-test-harness/instr/src/file.rs deleted file mode 100644 index f74d11c78ce..00000000000 --- a/svm-test-harness/instr/src/file.rs +++ /dev/null @@ -1,80 +0,0 @@ -//! Module for loading files from local filesystem. -//! -//! When compiling a Solana program with `cargo build-sbf`, the environment -//! variable `SBF_OUT_DIR` defaults to `target/deploy` and the compiled program -//! ELF file is written to `target/deploy/program_name.so`. -//! -//! As a result, the default search paths for ELF files are: -//! -//! * `tests/fixtures` -//! * `BPF_OUT_DIR` -//! * `SBF_OUT_DIR` -//! * The current working directory -//! -//! Since these functions are intended for the local filesystem and for testing -//! purposes, they will panic if the file is not found or if there is an -//! error reading the file. - -use std::{ - fs::File, - io::Read, - path::{Path, PathBuf}, -}; - -fn default_shared_object_dirs() -> Vec { - let mut search_path = vec![PathBuf::from("tests/fixtures")]; - - if let Ok(bpf_out_dir) = std::env::var("BPF_OUT_DIR") { - search_path.push(PathBuf::from(bpf_out_dir)); - } - - if let Ok(sbf_out_dir) = std::env::var("SBF_OUT_DIR") { - search_path.push(PathBuf::from(sbf_out_dir)); - } - - if let Ok(dir) = std::env::current_dir() { - search_path.push(dir); - } - - search_path -} - -fn find_file(filename: &str) -> Option { - for dir in default_shared_object_dirs() { - let candidate = dir.join(filename); - if candidate.exists() { - return Some(candidate); - } - } - None -} - -/// Read the contents of a file into a `Vec`. -pub fn read_file>(path: P) -> Vec { - let path = path.as_ref(); - let mut file = File::open(path) - .unwrap_or_else(|e| panic!("Failed to open file {}: {}", path.display(), e)); - - let mut file_data = Vec::new(); - file.read_to_end(&mut file_data) - .unwrap_or_else(|e| panic!("Failed to read file {}: {}", path.display(), e)); - file_data -} - -/// Load a program ELF file from the local filesystem by program name. -/// -/// The program ELF file is expected to be located in one of the default search -/// paths: -/// -/// * `tests/fixtures` -/// * `BPF_OUT_DIR` -/// * `SBF_OUT_DIR` -/// * The current working directory -/// -/// The name of the program ELF file is expected to be `{program_name}.so`. -pub fn load_program_elf(program_name: &str) -> Vec { - let file_name = format!("{program_name}.so"); - let program_file = find_file(&file_name) - .unwrap_or_else(|| panic!("Failed to find program ELF file: {file_name}")); - read_file(program_file) -} diff --git a/svm-test-harness/instr/src/fuzz.rs b/svm-test-harness/instr/src/fuzz.rs deleted file mode 100644 index 7bad716d442..00000000000 --- a/svm-test-harness/instr/src/fuzz.rs +++ /dev/null @@ -1,145 +0,0 @@ -#![allow(clippy::missing_safety_doc)] - -use { - crate::{ - execute_instr_with_callback, - fixture::{ - instr_context::InstrContext, - proto::{InstrContext as ProtoInstrContext, InstrEffects as ProtoInstrEffects}, - }, - logger, - }, - agave_precompiles::{get_precompile, is_precompile}, - prost::Message, - solana_compute_budget::compute_budget::ComputeBudget, - solana_precompile_error::PrecompileError, - solana_pubkey::Pubkey, - solana_svm_callback::InvokeContextCallback, - solana_syscalls::create_program_runtime_environment, - std::{env, ffi::c_int}, -}; - -/// Callback with full precompile support for fuzz testing. -/// -/// All precompiles are enabled for fuzz testing. -struct FuzzInstrContextCallback; - -impl InvokeContextCallback for FuzzInstrContextCallback { - fn is_precompile(&self, program_id: &Pubkey) -> bool { - is_precompile(program_id, |_| true) - } - - fn process_precompile( - &self, - program_id: &Pubkey, - data: &[u8], - instruction_datas: Vec<&[u8]>, - ) -> std::result::Result<(), PrecompileError> { - if let Some(precompile) = get_precompile(program_id, |_| true) { - precompile.verify( - data, - &instruction_datas, - &agave_feature_set::FeatureSet::all_enabled(), - ) - } else { - Err(PrecompileError::InvalidPublicKey) - } - } -} - -#[unsafe(no_mangle)] -pub unsafe extern "C" fn sol_compat_init(_log_level: i32) { - unsafe { - env::set_var("SOLANA_RAYON_THREADS", "1"); - env::set_var("RAYON_NUM_THREADS", "1"); - } - if env::var("ENABLE_SOLANA_LOGGER").is_ok() { - /* Pairs with RUST_LOG={trace,debug,info,etc} */ - logger::setup(); - } -} - -#[unsafe(no_mangle)] -pub unsafe extern "C" fn sol_compat_fini() {} - -pub fn execute_instr_proto(input: ProtoInstrContext) -> Option { - let cu_avail = input.cu_avail; - - let Ok(instr_context) = InstrContext::try_from(input) else { - return None; - }; - - let feature_set = &instr_context.feature_set; - let simd_0268_active = feature_set.raise_cpi_nesting_limit_to_8; - - let compute_budget = { - let mut budget = ComputeBudget::new_with_defaults(simd_0268_active); - budget.compute_unit_limit = cu_avail; - budget - }; - - // When testing with protobuf, we fill the sysvar cache from input accounts. - let sysvar_cache = { - let mut cache = solana_program_runtime::sysvar_cache::SysvarCache::default(); - crate::sysvar_cache::fill_from_accounts(&mut cache, &instr_context.accounts); - cache - }; - - // When testing with protobuf, we fill the program cache from input accounts. - let mut program_cache = { - let slot = sysvar_cache.get_clock().unwrap().slot; - let environment = create_program_runtime_environment( - &instr_context.feature_set, - &compute_budget.to_budget(), - false, /* deployment */ - false, /* debugging_features */ - ) - .unwrap(); - - let mut cache = crate::program_cache::new_with_builtins(slot); - crate::program_cache::fill_from_accounts( - &mut cache, - &environment, - &instr_context.accounts, - slot, - ) - .unwrap(); - - cache - }; - - let instr_effects = execute_instr_with_callback( - &instr_context, - &FuzzInstrContextCallback, - &compute_budget, - &mut program_cache, - &sysvar_cache, - ); - instr_effects.map(Into::into) -} - -#[unsafe(no_mangle)] -pub unsafe extern "C" fn sol_compat_instr_execute_v1( - out_ptr: *mut u8, - out_psz: *mut u64, - in_ptr: *mut u8, - in_sz: u64, -) -> c_int { - let in_slice = unsafe { std::slice::from_raw_parts(in_ptr, in_sz as usize) }; - let Ok(instr_context) = ProtoInstrContext::decode(in_slice) else { - return 0; - }; - let Some(instr_effects) = execute_instr_proto(instr_context) else { - return 0; - }; - let out_slice = unsafe { std::slice::from_raw_parts_mut(out_ptr, (*out_psz) as usize) }; - let out_vec = instr_effects.encode_to_vec(); - if out_vec.len() > out_slice.len() { - return 0; - } - out_slice[..out_vec.len()].copy_from_slice(&out_vec); - unsafe { - *out_psz = out_vec.len() as u64; - } - 1 -} diff --git a/svm-test-harness/instr/src/keyed_account.rs b/svm-test-harness/instr/src/keyed_account.rs deleted file mode 100644 index b0323aac45d..00000000000 --- a/svm-test-harness/instr/src/keyed_account.rs +++ /dev/null @@ -1,27 +0,0 @@ -//! Keyed account helpers. - -use { - crate::builtins::SVM_BUILTINS, solana_account::Account, solana_pubkey::Pubkey, - solana_rent::Rent, -}; - -fn create_keyed_account_for_builtin_program(program_id: &Pubkey, name: &str) -> (Pubkey, Account) { - let data = name.as_bytes().to_vec(); - let lamports = Rent::default().minimum_balance(data.len()); - let account = Account { - lamports, - data, - owner: solana_sdk_ids::native_loader::id(), - executable: true, - ..Default::default() - }; - (*program_id, account) -} - -pub fn keyed_account_for_system_program() -> (Pubkey, Account) { - create_keyed_account_for_builtin_program(&SVM_BUILTINS[0].program_id, SVM_BUILTINS[0].name) -} - -pub fn keyed_account_for_compute_budget_program() -> (Pubkey, Account) { - create_keyed_account_for_builtin_program(&SVM_BUILTINS[4].program_id, SVM_BUILTINS[4].name) -} diff --git a/svm-test-harness/instr/src/lib.rs b/svm-test-harness/instr/src/lib.rs deleted file mode 100644 index c3c22bccbb4..00000000000 --- a/svm-test-harness/instr/src/lib.rs +++ /dev/null @@ -1,20 +0,0 @@ -//! Solana SVM test harness for instruction execution. -//! -//! This crate provides an API for Agave's program runtime in order to -//! execute program instructions directly against the VM. - -mod builtins; -pub mod file; -mod harness; -pub mod keyed_account; -pub mod logger; -pub mod program_cache; -pub mod sysvar_cache; - -pub use { - harness::{execute_instr, execute_instr_with_callback}, - solana_svm_test_harness_fixture as fixture, -}; - -#[cfg(feature = "fuzz")] -pub mod fuzz; diff --git a/svm-test-harness/instr/src/logger.rs b/svm-test-harness/instr/src/logger.rs deleted file mode 100644 index 68bcbbe774f..00000000000 --- a/svm-test-harness/instr/src/logger.rs +++ /dev/null @@ -1,9 +0,0 @@ -//! Logging utils. - -/// Initialize a test logger with default error filter. -pub fn setup() { - let _ = env_logger::Builder::from_env(env_logger::Env::new().default_filter_or("error")) - .format_timestamp_nanos() - .is_test(true) - .try_init(); -} diff --git a/svm-test-harness/src/lib.rs b/svm-test-harness/src/lib.rs deleted file mode 100644 index 2260ce1c867..00000000000 --- a/svm-test-harness/src/lib.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Solana SVM test harness. - -pub use {solana_svm_test_harness_fixture as fixture, solana_svm_test_harness_instr as instr}; diff --git a/svm/Cargo.toml b/svm/Cargo.toml index a767df95618..3e5c449fa4a 100644 --- a/svm/Cargo.toml +++ b/svm/Cargo.toml @@ -19,8 +19,28 @@ name = "solana_svm" [features] default = ["metrics"] agave-unstable-api = [] -dummy-for-ci-check = ["metrics"] -dev-context-only-utils = ["dep:qualifier_attr", "solana-program-runtime/dev-context-only-utils"] +conformance = [ + "dep:bincode", + "dep:prost", + "dep:protosol", + "dev-context-only-utils", + "solana-instruction-error/serde", + "solana-pubkey/default", +] +dev-context-only-utils = [ + "dep:agave-feature-set", + "dep:agave-precompiles", + "dep:qualifier_attr", + "dep:solana-bpf-loader-program", + "dep:solana-compute-budget", + "dep:solana-compute-budget-program", + "dep:solana-instruction-error", + "dep:solana-precompile-error", + "dep:solana-syscalls", + "dep:solana-system-program", + "solana-program-runtime/dev-context-only-utils", +] +dummy-for-ci-check = ["conformance", "metrics"] frozen-abi = [ "dep:solana-frozen-abi", "dep:solana-frozen-abi-macro", @@ -38,13 +58,21 @@ shuttle-test = [ svm-internal = ["dep:qualifier_attr"] [dependencies] +agave-feature-set = { workspace = true, optional = true } +agave-precompiles = { workspace = true, optional = true } ahash = { workspace = true } +bincode = { workspace = true, optional = true } log = { workspace = true } percentage = { workspace = true } +prost = { version = "0.11", optional = true } +protosol = { workspace = true, optional = true } qualifier_attr = { workspace = true, optional = true } serde = { workspace = true, features = ["rc"] } solana-account = { workspace = true } +solana-bpf-loader-program = { workspace = true, optional = true } solana-clock = { workspace = true } +solana-compute-budget = { workspace = true, optional = true } +solana-compute-budget-program = { workspace = true, optional = true } solana-fee-structure = { workspace = true } solana-frozen-abi = { workspace = true, optional = true, features = [ "frozen-abi", @@ -54,12 +82,14 @@ solana-frozen-abi-macro = { workspace = true, optional = true, features = [ ] } solana-hash = { workspace = true } solana-instruction = { workspace = true, features = ["std"] } +solana-instruction-error = { workspace = true, optional = true } solana-instructions-sysvar = { workspace = true } solana-loader-v3-interface = { workspace = true, features = ["bincode"] } solana-loader-v4-interface = { workspace = true } solana-message = { workspace = true } solana-nonce = { workspace = true } solana-nonce-account = { workspace = true, features = ["wincode"] } +solana-precompile-error = { workspace = true, optional = true } solana-program-entrypoint = { workspace = true } solana-program-pack = { workspace = true } solana-program-runtime = { workspace = true } @@ -73,7 +103,9 @@ solana-svm-measure = { workspace = true } solana-svm-timings = { workspace = true } solana-svm-transaction = { workspace = true } solana-svm-type-overrides = { workspace = true } +solana-syscalls = { workspace = true, optional = true } solana-system-interface = { workspace = true } +solana-system-program = { workspace = true, optional = true } solana-sysvar-id = { workspace = true } solana-transaction-context = { workspace = true } solana-transaction-error = { workspace = true } diff --git a/svm/src/conformance/account_state.rs b/svm/src/conformance/account_state.rs new file mode 100644 index 00000000000..144e9818ee0 --- /dev/null +++ b/svm/src/conformance/account_state.rs @@ -0,0 +1,51 @@ +//! Account state conversions for protobuf support. + +use {protosol::protos::AcctState as ProtoAccount, solana_account::Account, solana_pubkey::Pubkey}; + +// Default `rent_epoch` field value for all accounts. +const RENT_EXEMPT_RENT_EPOCH: u64 = u64::MAX; + +/// Convert a protobuf account state into a `(Pubkey, Account)` pair. +pub fn account_from_proto(value: ProtoAccount) -> (Pubkey, Account) { + let ProtoAccount { + address, + owner, + lamports, + data, + executable, + .. + } = value; + + let pubkey = Pubkey::try_from(address).expect("invalid account address bytes"); + let owner = Pubkey::try_from(owner).expect("invalid account owner bytes"); + + ( + pubkey, + Account { + data, + executable, + lamports, + owner, + rent_epoch: RENT_EXEMPT_RENT_EPOCH, + }, + ) +} + +/// Convert a `(Pubkey, Account)` pair into a protobuf account state. +pub fn account_to_proto(value: (Pubkey, Account)) -> ProtoAccount { + let Account { + lamports, + data, + owner, + executable, + .. + } = value.1; + + ProtoAccount { + address: value.0.to_bytes().to_vec(), + owner: owner.to_bytes().to_vec(), + lamports, + data, + executable, + } +} diff --git a/svm/src/conformance/context.rs b/svm/src/conformance/context.rs new file mode 100644 index 00000000000..591d6380a04 --- /dev/null +++ b/svm/src/conformance/context.rs @@ -0,0 +1,126 @@ +//! Instruction context and effects (input + output). + +#[cfg(feature = "conformance")] +use { + super::{ + account_state::{account_from_proto, account_to_proto}, + feature_set::feature_set_from_proto, + }, + protosol::protos::{InstrContext as ProtoInstrContext, InstrEffects as ProtoInstrEffects}, + solana_instruction::AccountMeta, +}; +use { + solana_account::Account, + solana_instruction::{Instruction, error::InstructionError}, + solana_pubkey::Pubkey, + solana_svm_feature_set::SVMFeatureSet, +}; + +/// Inputs to a single instruction. +pub struct InstrContext { + pub feature_set: SVMFeatureSet, + pub accounts: Vec<(Pubkey, Account)>, + pub instruction: Instruction, +} + +#[cfg(feature = "conformance")] +impl From for InstrContext { + fn from(value: ProtoInstrContext) -> Self { + let program_id = Pubkey::try_from(value.program_id).expect("invalid program_id bytes"); + + let feature_set = value + .features + .as_ref() + .map(feature_set_from_proto) + .unwrap_or_default() + .runtime_features(); + + let accounts: Vec<(Pubkey, Account)> = + value.accounts.into_iter().map(account_from_proto).collect(); + + let instruction_accounts = value + .instr_accounts + .into_iter() + .map(|acct| { + let pubkey = accounts + .get(acct.index as usize) + .map(|(pk, _)| *pk) + .expect("instruction account index out of bounds"); + AccountMeta { + pubkey, + is_signer: acct.is_signer, + is_writable: acct.is_writable, + } + }) + .collect::>(); + + assert!( + instruction_accounts.len() <= 128, + "too many instruction accounts", + ); + + let instruction = Instruction { + accounts: instruction_accounts, + data: value.data, + program_id, + }; + + Self { + feature_set, + accounts, + instruction, + } + } +} + +/// Represents the effects of a single instruction. +pub struct InstrEffects { + pub result: Option, + pub custom_err: Option, + pub resulting_accounts: Vec<(Pubkey, Account)>, + pub cu_avail: u64, + pub return_data: Vec, + pub logs: Vec, +} + +impl InstrEffects { + /// Returns the resulting account for the given pubkey, if it exists. + pub fn get_account(&self, pubkey: &Pubkey) -> Option<&Account> { + self.resulting_accounts + .iter() + .find(|(pk, _)| pk == pubkey) + .map(|(_, acc)| acc) + } +} + +#[cfg(feature = "conformance")] +impl From for ProtoInstrEffects { + fn from(value: InstrEffects) -> Self { + let InstrEffects { + result, + custom_err, + resulting_accounts, + cu_avail, + return_data, + .. + } = value; + + Self { + result: result + .as_ref() + .map(|error| { + let serialized_err = bincode::serialize(error).unwrap(); + i32::from_le_bytes((&serialized_err[0..4]).try_into().unwrap()) + .saturating_add(1) + }) + .unwrap_or_default(), + custom_err: custom_err.unwrap_or_default(), + modified_accounts: resulting_accounts + .into_iter() + .map(account_to_proto) + .collect(), + cu_avail, + return_data, + } + } +} diff --git a/svm-test-harness/fixture/src/feature_set.rs b/svm/src/conformance/feature_set.rs similarity index 66% rename from svm-test-harness/fixture/src/feature_set.rs rename to svm/src/conformance/feature_set.rs index fca4d6c3cc8..47c86b49bbb 100644 --- a/svm-test-harness/fixture/src/feature_set.rs +++ b/svm/src/conformance/feature_set.rs @@ -1,10 +1,8 @@ //! Feature set conversions for protobuf support. -#![cfg(feature = "fuzz")] - use { - crate::proto::FeatureSet as ProtoFeatureSet, agave_feature_set::{FEATURE_NAMES, FeatureSet}, + protosol::protos::FeatureSet as ProtoFeatureSet, solana_pubkey::Pubkey, std::{collections::HashMap, sync::LazyLock}, }; @@ -28,14 +26,13 @@ static INDEXED_FEATURES: LazyLock> = LazyLock::new(|| { .collect() }); -impl From<&ProtoFeatureSet> for FeatureSet { - fn from(value: &ProtoFeatureSet) -> Self { - let mut feature_set = FeatureSet::default(); - for id in &value.features { - if let Some(pubkey) = INDEXED_FEATURES.get(id) { - feature_set.activate(pubkey, 0); - } +/// Build a `FeatureSet` from a protobuf feature set. +pub fn feature_set_from_proto(value: &ProtoFeatureSet) -> FeatureSet { + let mut feature_set = FeatureSet::default(); + for id in &value.features { + if let Some(pubkey) = INDEXED_FEATURES.get(id) { + feature_set.activate(pubkey, 0); } - feature_set } + feature_set } diff --git a/svm-test-harness/instr/src/harness.rs b/svm/src/conformance/harness.rs similarity index 63% rename from svm-test-harness/instr/src/harness.rs rename to svm/src/conformance/harness.rs index 6f0d38656d3..79baddbb36b 100644 --- a/svm-test-harness/instr/src/harness.rs +++ b/svm/src/conformance/harness.rs @@ -1,9 +1,13 @@ -//! Instruction harness. +//! Conformance harness. use { - crate::fixture::{instr_context::InstrContext, instr_effects::InstrEffects}, + super::context::{InstrContext, InstrEffects}, + crate::message_processor::process_message, + agave_feature_set::FeatureSet, + agave_precompiles::{get_precompile, is_precompile}, solana_compute_budget::compute_budget::ComputeBudget, - solana_instruction_error::InstructionError, + solana_instruction::error::InstructionError, + solana_precompile_error::PrecompileError, solana_program_runtime::{ invoke_context::{EnvironmentConfig, InvokeContext, mock_compile_message}, loaded_programs::{ @@ -18,26 +22,52 @@ use { solana_svm_transaction::svm_message::SVMStaticMessage, solana_syscalls::create_program_runtime_environment, solana_transaction_context::transaction::TransactionContext, + solana_transaction_error::TransactionError, std::rc::Rc, }; +#[cfg(feature = "conformance")] +use { + super::{ + programs::{fill_program_cache_from_accounts, new_program_cache_with_builtins}, + sysvar::fill_sysvar_cache_from_accounts, + }, + prost::Message, + protosol::protos::{InstrContext as ProtoInstrContext, InstrEffects as ProtoInstrEffects}, + std::ffi::c_int, +}; -/// Default callback with no precompile support. +/// Default callback. Full precompile support. struct DefaultCallback; -impl InvokeContextCallback for DefaultCallback {} +impl InvokeContextCallback for DefaultCallback { + fn is_precompile(&self, program_id: &Pubkey) -> bool { + is_precompile(program_id, |_| true) + } -/// Execute a single instruction against the Solana VM. -/// -/// This version does not support precompiles. Use [`execute_instr_with_callback`] -/// if you need precompile support. + fn process_precompile( + &self, + program_id: &Pubkey, + data: &[u8], + instruction_datas: Vec<&[u8]>, + ) -> Result<(), PrecompileError> { + if let Some(precompile) = get_precompile(program_id, |_| true) { + precompile.verify(data, &instruction_datas, &FeatureSet::all_enabled()) + } else { + Err(PrecompileError::InvalidPublicKey) + } + } +} + +/// Execute a single instruction against the Solana VM with the default +/// (no-precompile) callback. pub fn execute_instr( - input: InstrContext, + input: &InstrContext, compute_budget: &ComputeBudget, program_cache: &mut ProgramCacheForTxBatch, sysvar_cache: &SysvarCache, ) -> Option { execute_instr_with_callback( - &input, + input, &DefaultCallback, compute_budget, program_cache, @@ -95,35 +125,35 @@ pub fn execute_instr_with_callback( ProgramRuntimeEnvironment::clone(&program_runtime_environment), program_runtime_environment, ); + let environment_config = EnvironmentConfig::new( + blockhash, + blockhash_lamports_per_signature, + false, + callback, + &feature_set, + &program_runtime_environments, + sysvar_cache, + ); + let mut invoke_context = InvokeContext::new( &mut transaction_context, program_cache, - EnvironmentConfig::new( - blockhash, - blockhash_lamports_per_signature, - false, - callback, - &feature_set, - &program_runtime_environments, - sysvar_cache, - ), + environment_config, Some(log_collector.clone()), compute_budget.to_budget(), compute_budget.to_cost(), ); - - invoke_context - .prepare_top_level_instructions(&sanitized_message) - .ok()?; - - if invoke_context.is_precompile(&input.instruction.program_id) { - invoke_context.process_precompile( - &input.instruction.program_id, - &input.instruction.data, - [input.instruction.data.as_slice()].into_iter(), - ) - } else { - invoke_context.process_instruction(&mut compute_units_consumed, &mut timings) + match process_message( + &sanitized_message, + &mut invoke_context, + &mut timings, + &mut compute_units_consumed, + ) { + Ok(()) => Ok(()), + Err(TransactionError::InstructionError(_, err)) => Err(err), + // `process_message` only ever returns `InstructionError`-shaped + // failures. + Err(_) => unreachable!(), } }; @@ -166,13 +196,98 @@ pub fn execute_instr_with_callback( }) } +#[cfg(feature = "conformance")] +pub fn execute_instr_proto(input: ProtoInstrContext) -> Option { + let cu_avail = input.cu_avail; + let instr_context = InstrContext::from(input); + + let feature_set = &instr_context.feature_set; + let simd_0268_active = feature_set.raise_cpi_nesting_limit_to_8; + + let compute_budget = { + let mut budget = ComputeBudget::new_with_defaults(simd_0268_active); + budget.compute_unit_limit = cu_avail; + budget + }; + + // When testing with protobuf, we fill the sysvar cache from input accounts. + let sysvar_cache = { + let mut cache = SysvarCache::default(); + fill_sysvar_cache_from_accounts(&mut cache, &instr_context.accounts); + cache + }; + + // When testing with protobuf, we fill the program cache from input accounts. + let mut program_cache = { + let slot = sysvar_cache.get_clock().unwrap().slot; + let environment = create_program_runtime_environment( + &instr_context.feature_set, + &compute_budget.to_budget(), + false, /* deployment */ + false, /* debugging_features */ + ) + .unwrap(); + + let mut cache = new_program_cache_with_builtins(slot); + fill_program_cache_from_accounts(&mut cache, &environment, &instr_context.accounts, slot) + .unwrap(); + + cache + }; + + let instr_effects = execute_instr( + &instr_context, + &compute_budget, + &mut program_cache, + &sysvar_cache, + ); + instr_effects.map(Into::into) +} + +/// # Safety +/// +/// `in_ptr` must point to `in_sz` initialized bytes. `out_ptr` must point +/// to a writable buffer of at least `*out_psz` bytes. On return, `*out_psz` +/// is updated to the number of bytes written. +#[cfg(feature = "conformance")] +#[unsafe(no_mangle)] +pub unsafe extern "C" fn sol_compat_instr_execute_v1( + out_ptr: *mut u8, + out_psz: *mut u64, + in_ptr: *mut u8, + in_sz: u64, +) -> c_int { + let in_slice = unsafe { std::slice::from_raw_parts(in_ptr, in_sz as usize) }; + let Ok(instr_context) = ProtoInstrContext::decode(in_slice) else { + return 0; + }; + let Some(instr_effects) = execute_instr_proto(instr_context) else { + return 0; + }; + let out_slice = unsafe { std::slice::from_raw_parts_mut(out_ptr, (*out_psz) as usize) }; + let out_vec = instr_effects.encode_to_vec(); + if out_vec.len() > out_slice.len() { + return 0; + } + out_slice[..out_vec.len()].copy_from_slice(&out_vec); + unsafe { + *out_psz = out_vec.len() as u64; + } + 1 +} + #[cfg(test)] mod tests { use { - super::*, + super::{ + super::{ + programs::{fill_program_cache_from_accounts, new_program_cache_with_builtins}, + sysvar::fill_sysvar_cache_from_accounts, + }, + *, + }, solana_account::Account, solana_instruction::{AccountMeta, Instruction}, - solana_pubkey::Pubkey, solana_svm_feature_set::SVMFeatureSet, solana_sysvar_id::SysvarId, }; @@ -190,14 +305,14 @@ mod tests { let slot = 10; let feature_set = SVMFeatureSet::default(); - // Create Clock sysvar + // Create Clock sysvar. let clock = solana_clock::Clock { slot, ..Default::default() }; let clock_data = bincode::serialize(&clock).unwrap(); - // Create Rent sysvar + // Create Rent sysvar. let rent = solana_rent::Rent::default(); let rent_data = bincode::serialize(&rent).unwrap(); @@ -285,12 +400,12 @@ mod tests { budget }; - // Create Sysvar Cache + // Create Sysvar Cache. let mut sysvar_cache = SysvarCache::default(); - crate::sysvar_cache::fill_from_accounts(&mut sysvar_cache, &context.accounts); + fill_sysvar_cache_from_accounts(&mut sysvar_cache, &context.accounts); // Create Program Cache - let mut program_cache = crate::program_cache::new_with_builtins(slot); + let mut program_cache = new_program_cache_with_builtins(slot); let environments = create_program_runtime_environment( &context.feature_set, @@ -300,7 +415,7 @@ mod tests { ) .unwrap(); - crate::program_cache::fill_from_accounts( + fill_program_cache_from_accounts( &mut program_cache, &environments, &context.accounts, @@ -309,7 +424,7 @@ mod tests { .unwrap(); // Execute the instruction. - let effects = execute_instr(context, &compute_budget, &mut program_cache, &sysvar_cache) + let effects = execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache) .expect("Instruction execution should succeed"); // Verify the results. diff --git a/svm/src/conformance/mod.rs b/svm/src/conformance/mod.rs new file mode 100644 index 00000000000..7856afc9667 --- /dev/null +++ b/svm/src/conformance/mod.rs @@ -0,0 +1,10 @@ +//! Solana SVM conformance. + +#[cfg(feature = "conformance")] +pub mod account_state; +pub mod context; +#[cfg(feature = "conformance")] +pub mod feature_set; +pub mod harness; +pub mod programs; +pub mod sysvar; diff --git a/svm-test-harness/instr/src/program_cache.rs b/svm/src/conformance/programs.rs similarity index 53% rename from svm-test-harness/instr/src/program_cache.rs rename to svm/src/conformance/programs.rs index 12b68a0dc5e..bb1b5498ceb 100644 --- a/svm-test-harness/instr/src/program_cache.rs +++ b/svm/src/conformance/programs.rs @@ -1,14 +1,21 @@ +//! Program helpers: builtins, keyed accounts, and program cache. + +#[cfg(feature = "metrics")] +use solana_program_runtime::program_metrics::LoadProgramMetrics; use { - crate::builtins::SVM_BUILTINS, + crate::program_loader::load_program_with_pubkey, solana_account::{Account, AccountSharedData}, solana_compute_budget::compute_budget::ComputeBudget, - solana_instruction_error::InstructionError, + solana_instruction::error::InstructionError, solana_program_runtime::{ + invoke_context::BuiltinFunctionRegisterer, loaded_programs::{ProgramCacheForTxBatch, ProgramRuntimeEnvironment}, program_cache_entry::ProgramCacheEntry, - program_metrics::LoadProgramMetrics, + solana_sbpf::program::BuiltinFunctionDefinition, }, solana_pubkey::Pubkey, + solana_rent::Rent, + solana_sdk_ids::{bpf_loader, bpf_loader_deprecated, bpf_loader_upgradeable}, solana_svm_callback::{InvokeContextCallback, TransactionProcessingCallback}, solana_svm_feature_set::SVMFeatureSet, solana_svm_timings::ExecuteTimings, @@ -16,8 +23,62 @@ use { std::{collections::HashSet, sync::Arc}, }; -/// Create a new `ProgramCacheForTxBatch` instance populated with builtins. -pub fn new_with_builtins(slot: u64) -> ProgramCacheForTxBatch { +struct SvmBuiltinPrototype { + pub program_id: Pubkey, + pub name: &'static str, + pub register_fn: BuiltinFunctionRegisterer, +} + +static SVM_BUILTINS: &[SvmBuiltinPrototype] = &[ + SvmBuiltinPrototype { + program_id: solana_system_program::id(), + name: "system_program", + register_fn: solana_system_program::system_processor::Entrypoint::register, + }, + SvmBuiltinPrototype { + program_id: bpf_loader_deprecated::id(), + name: "solana_bpf_loader_deprecated_program", + register_fn: solana_bpf_loader_program::Entrypoint::register, + }, + SvmBuiltinPrototype { + program_id: bpf_loader::id(), + name: "solana_bpf_loader_program", + register_fn: solana_bpf_loader_program::Entrypoint::register, + }, + SvmBuiltinPrototype { + program_id: bpf_loader_upgradeable::id(), + name: "solana_bpf_loader_upgradeable_program", + register_fn: solana_bpf_loader_program::Entrypoint::register, + }, + SvmBuiltinPrototype { + program_id: solana_sdk_ids::compute_budget::id(), + name: "compute_budget_program", + register_fn: solana_compute_budget_program::Entrypoint::register, + }, +]; + +fn create_keyed_account_for_builtin_program(program_id: &Pubkey, name: &str) -> (Pubkey, Account) { + let data = name.as_bytes().to_vec(); + let lamports = Rent::default().minimum_balance(data.len()); + let account = Account { + lamports, + data, + owner: solana_sdk_ids::native_loader::id(), + executable: true, + ..Default::default() + }; + (*program_id, account) +} + +pub fn keyed_account_for_system_program() -> (Pubkey, Account) { + create_keyed_account_for_builtin_program(&SVM_BUILTINS[0].program_id, SVM_BUILTINS[0].name) +} + +pub fn keyed_account_for_compute_budget_program() -> (Pubkey, Account) { + create_keyed_account_for_builtin_program(&SVM_BUILTINS[4].program_id, SVM_BUILTINS[4].name) +} + +pub fn new_program_cache_with_builtins(slot: u64) -> ProgramCacheForTxBatch { let mut cache = ProgramCacheForTxBatch::default(); cache.set_slot_for_tests(slot); @@ -36,7 +97,7 @@ pub fn new_with_builtins(slot: u64) -> ProgramCacheForTxBatch { } /// Add a program loaded from ELF bytes to the cache. -pub fn add_program( +pub fn add_program_to_program_cache( cache: &mut ProgramCacheForTxBatch, program_id: &Pubkey, loader_key: &Pubkey, @@ -59,6 +120,7 @@ pub fn add_program( 0, // effective_slot elf, elf.len(), + #[cfg(feature = "metrics")] &mut LoadProgramMetrics::default(), ) .unwrap(); @@ -67,7 +129,7 @@ pub fn add_program( } /// Populate a `ProgramCacheForTxBatch` via `load_program_with_pubkey` from any program accounts. -pub fn fill_from_accounts( +pub fn fill_program_cache_from_accounts( program_cache: &mut ProgramCacheForTxBatch, program_runtime_environment: &ProgramRuntimeEnvironment, accounts: &[(Pubkey, Account)], @@ -76,38 +138,24 @@ pub fn fill_from_accounts( let mut newly_loaded_programs = HashSet::::new(); for acc in accounts { - // FD rejects duplicate account loads if !newly_loaded_programs.insert(acc.0) { return Err(InstructionError::UnsupportedProgramId); } if program_cache.find(&acc.0).is_none() { - // load_program_with_pubkey expects the owner to be one of the bpf loader if !solana_sdk_ids::bpf_loader_deprecated::check_id(&acc.1.owner) && !solana_sdk_ids::bpf_loader::check_id(&acc.1.owner) && !solana_sdk_ids::bpf_loader_upgradeable::check_id(&acc.1.owner) { continue; } - // https://github.com/anza-xyz/agave/blob/af6930da3a99fd0409d3accd9bbe449d82725bd6/svm/src/program_loader.rs#L124 - /* pub fn load_program_with_pubkey( - callbacks: &CB, - program_cache: &ProgramCache, - pubkey: &Pubkey, - slot: Slot, - effective_epoch: Epoch, - epoch_schedule: &EpochSchedule, - reload: bool, - ) -> Option> { */ - if let Some((loaded_program, _last_modification_slot)) = - solana_svm::program_loader::load_program_with_pubkey( - &FillFromAccountsCallback(accounts), - program_runtime_environment, - &acc.0, - slot, - &mut ExecuteTimings::default(), - ) - { + if let Some((loaded_program, _last_modification_slot)) = load_program_with_pubkey( + &FillFromAccountsCallback(accounts), + program_runtime_environment, + &acc.0, + slot, + &mut ExecuteTimings::default(), + ) { program_cache.replenish(acc.0, loaded_program); } } diff --git a/svm-test-harness/instr/src/sysvar_cache.rs b/svm/src/conformance/sysvar.rs similarity index 60% rename from svm-test-harness/instr/src/sysvar_cache.rs rename to svm/src/conformance/sysvar.rs index bdc952d168c..79105ad3a22 100644 --- a/svm-test-harness/instr/src/sysvar_cache.rs +++ b/svm/src/conformance/sysvar.rs @@ -1,3 +1,5 @@ +//! Sysvar helpers. + use { solana_account::{Account, ReadableAccount}, solana_program_runtime::sysvar_cache::SysvarCache, @@ -5,12 +7,15 @@ use { }; /// Populate a `SysvarCache` via `fill_missing_entries` from any sysvar accounts. -pub fn fill_from_accounts(sysvar_cache: &mut SysvarCache, accounts: &[(Pubkey, Account)]) { +pub fn fill_sysvar_cache_from_accounts( + sysvar_cache: &mut SysvarCache, + accounts: &[(Pubkey, Account)], +) { sysvar_cache.fill_missing_entries(|pubkey, callbackback| { - if let Some(account) = accounts.iter().find(|(key, _)| key == pubkey) { - if account.1.lamports() > 0 { - callbackback(account.1.data()); - } + if let Some(account) = accounts.iter().find(|(key, _)| key == pubkey) + && account.1.lamports() > 0 + { + callbackback(account.1.data()); } }); } diff --git a/svm/src/lib.rs b/svm/src/lib.rs index cdca592f5c1..461454a72c0 100644 --- a/svm/src/lib.rs +++ b/svm/src/lib.rs @@ -4,6 +4,8 @@ pub mod account_loader; pub mod account_overrides; +#[cfg(any(feature = "conformance", feature = "dev-context-only-utils"))] +pub mod conformance; pub mod message_processor; pub mod nonce_info; pub mod program_loader; From 75e58e8a520efe8af6001816ae1c9e0de284405d Mon Sep 17 00:00:00 2001 From: Joe C Date: Fri, 22 May 2026 17:05:49 +0800 Subject: [PATCH 032/576] dev-bins: drop stale svm-test-harness workspace deps (#12676) The svm-test-harness tree was removed when the harness was folded into solana-svm::conformance, but two dangling entries in [workspace.dependencies] pointed at the deleted paths. No member crate references them, so the workspace still resolved; remove the dead declarations. --- dev-bins/Cargo.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/dev-bins/Cargo.toml b/dev-bins/Cargo.toml index de02fbd635a..f897eccfede 100644 --- a/dev-bins/Cargo.toml +++ b/dev-bins/Cargo.toml @@ -125,8 +125,6 @@ solana-svm = { path = "../svm", version = "=4.2.0-alpha.0", features = ["agave-u solana-svm-callback = { path = "../svm-callback", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-svm-feature-set = { path = "../svm-feature-set", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-svm-log-collector = { path = "../svm-log-collector", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-svm-test-harness-fixture = { path = "../svm-test-harness/fixture", version = "=4.2.0-alpha.0" } -solana-svm-test-harness-instr = { path = "../svm-test-harness/instr", version = "=4.2.0-alpha.0" } solana-svm-timings = { path = "../svm-timings", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-svm-transaction = { path = "../svm-transaction", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-syscalls = { path = "../syscalls", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } From 5262468b8764c43cef673cf313af3f558bf7a478 Mon Sep 17 00:00:00 2001 From: Joe C Date: Fri, 22 May 2026 17:46:15 +0800 Subject: [PATCH 033/576] program-runtime: drop Option from mock_compile_message (#12677) mock_compile_message has always unconditionally returned Some(..). Make the return type the tuple directly and update the in-tree callers to match. --- program-runtime/src/invoke_context.rs | 8 ++++---- svm/src/conformance/harness.rs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/program-runtime/src/invoke_context.rs b/program-runtime/src/invoke_context.rs index c81225cd53a..0a8056a00e7 100644 --- a/program-runtime/src/invoke_context.rs +++ b/program-runtime/src/invoke_context.rs @@ -915,7 +915,7 @@ pub fn mock_compile_message( accounts: &[(Pubkey, A)], program_id: &Pubkey, loader_key: &Pubkey, -) -> Option<(SanitizedMessage, Vec<(Pubkey, AccountSharedData)>)> +) -> (SanitizedMessage, Vec<(Pubkey, AccountSharedData)>) where AccountSharedData: From, A: Clone, @@ -944,7 +944,7 @@ where let sanitized_message = SanitizedMessage::Legacy(LegacyMessage::new(message, &HashSet::new())); - Some((sanitized_message, transaction_accounts)) + (sanitized_message, transaction_accounts) } #[cfg(feature = "dev-context-only-utils")] @@ -976,7 +976,7 @@ pub fn mock_process_instruction_with_feature_set< let instruction = Instruction::new_with_bytes(*program_id, instruction_data, instruction_account_metas); let (sanitized_message, transaction_accounts) = - mock_compile_message(&instruction, &accounts, program_id, &native_loader::id()).unwrap(); + mock_compile_message(&instruction, &accounts, program_id, &native_loader::id()); let program_owner = accounts .iter() @@ -1977,7 +1977,7 @@ mod tests { )]; let (message, tx_accounts) = - mock_compile_message(&instruction, &accounts, &program_id, &loader_key).unwrap(); + mock_compile_message(&instruction, &accounts, &program_id, &loader_key); assert_eq!(message.instructions().len(), 1); assert_eq!(tx_accounts.len(), 2); diff --git a/svm/src/conformance/harness.rs b/svm/src/conformance/harness.rs index 79baddbb36b..d9586324cdd 100644 --- a/svm/src/conformance/harness.rs +++ b/svm/src/conformance/harness.rs @@ -94,7 +94,7 @@ pub fn execute_instr_with_callback( let loader_key = program_cache.find(program_id)?.account_owner(); let (sanitized_message, transaction_accounts) = - mock_compile_message(&input.instruction, &input.accounts, program_id, &loader_key)?; + mock_compile_message(&input.instruction, &input.accounts, program_id, &loader_key); let mut transaction_context = TransactionContext::new( transaction_accounts, From e6eeac6814a778fa48d5df6450d698b4af9880d6 Mon Sep 17 00:00:00 2001 From: Kamil Skalski Date: Fri, 22 May 2026 12:05:45 +0200 Subject: [PATCH 034/576] chore(clippy): apply workspace level lints to all crates (#12590) --- account-decoder-client-types/Cargo.toml | 3 +++ accounts-cluster-bench/Cargo.toml | 3 +++ accounts-db/store-histogram/Cargo.toml | 3 +++ accounts-db/store-tool/Cargo.toml | 3 +++ banking-stage-ingress-types/Cargo.toml | 3 +++ banks-client/Cargo.toml | 3 +++ banks-interface/Cargo.toml | 3 +++ banks-server/Cargo.toml | 3 +++ bench-tps/Cargo.toml | 3 +++ bucket_map/Cargo.toml | 3 +++ clap-utils/Cargo.toml | 3 +++ clap-v3-utils/Cargo.toml | 3 +++ cli-config/Cargo.toml | 3 +++ cli-output/Cargo.toml | 3 +++ cli/Cargo.toml | 3 +++ client-test/Cargo.toml | 3 +++ client/Cargo.toml | 3 +++ connection-cache/Cargo.toml | 3 +++ download-utils/Cargo.toml | 3 +++ entry/Cargo.toml | 3 +++ faucet-cli/Cargo.toml | 3 +++ faucet/Cargo.toml | 3 +++ fee/Cargo.toml | 3 +++ fs/Cargo.toml | 3 +++ genesis-utils/Cargo.toml | 3 +++ genesis/Cargo.toml | 3 +++ geyser-plugin-interface/Cargo.toml | 3 +++ geyser-plugin-manager/Cargo.toml | 3 +++ gossip-cli/Cargo.toml | 3 +++ install/Cargo.toml | 3 +++ io-uring/Cargo.toml | 3 +++ keygen/Cargo.toml | 3 +++ lattice-hash/Cargo.toml | 3 +++ ledger-tool/Cargo.toml | 3 +++ local-cluster/Cargo.toml | 3 +++ logger/Cargo.toml | 3 +++ measure/Cargo.toml | 3 +++ merkle-tree/Cargo.toml | 3 +++ metrics/Cargo.toml | 3 +++ notifier/Cargo.toml | 3 +++ poh-bench/Cargo.toml | 3 +++ poh/Cargo.toml | 3 +++ program-binaries/Cargo.toml | 3 +++ program-test/Cargo.toml | 3 +++ programs/bpf-loader-tests/Cargo.toml | 3 +++ programs/bpf_loader/Cargo.toml | 3 +++ programs/compute-budget-bench/Cargo.toml | 3 +++ programs/compute-budget/Cargo.toml | 3 +++ programs/ed25519-tests/Cargo.toml | 3 +++ programs/system/Cargo.toml | 3 +++ programs/zk-elgamal-proof-tests/Cargo.toml | 3 +++ programs/zk-elgamal-proof/Cargo.toml | 3 +++ programs/zk-token-proof/Cargo.toml | 3 +++ pubsub-client/Cargo.toml | 3 +++ quic-client/Cargo.toml | 3 +++ random/Cargo.toml | 3 +++ rayon-threadlimit/Cargo.toml | 3 +++ rbpf-cli/Cargo.toml | 3 +++ remote-wallet/Cargo.toml | 3 +++ rpc-client-api/Cargo.toml | 3 +++ rpc-client-nonce-utils/Cargo.toml | 3 +++ rpc-client-types/Cargo.toml | 3 +++ rpc-client/Cargo.toml | 3 +++ rpc-test/Cargo.toml | 3 +++ rpc/Cargo.toml | 3 +++ send-transaction-service/Cargo.toml | 3 +++ snapshots/Cargo.toml | 3 +++ stake-accounts/Cargo.toml | 3 +++ storage-bigtable/Cargo.toml | 3 +++ storage-bigtable/build-proto/Cargo.toml | 3 +++ storage-proto/Cargo.toml | 3 +++ streamer/Cargo.toml | 3 +++ svm-log-collector/Cargo.toml | 3 +++ svm-measure/Cargo.toml | 3 +++ svm-timings/Cargo.toml | 3 +++ svm-transaction/Cargo.toml | 3 +++ svm-type-overrides/Cargo.toml | 3 +++ syscalls/gen-syscall-list/Cargo.toml | 3 +++ test-validator/Cargo.toml | 3 +++ tokens/Cargo.toml | 3 +++ tps-client/Cargo.toml | 3 +++ tpu-client-next/Cargo.toml | 3 +++ tpu-client/Cargo.toml | 3 +++ transaction-status-client-types/Cargo.toml | 3 +++ transaction-status/Cargo.toml | 3 +++ transaction-view/Cargo.toml | 3 +++ turbine/Cargo.toml | 3 +++ udp-client/Cargo.toml | 3 +++ unified-scheduler-logic/Cargo.toml | 3 +++ unified-scheduler-pool/Cargo.toml | 3 +++ validator/Cargo.toml | 3 +++ watchtower/Cargo.toml | 3 +++ xdp/Cargo.toml | 3 +++ 93 files changed, 279 insertions(+) diff --git a/account-decoder-client-types/Cargo.toml b/account-decoder-client-types/Cargo.toml index 5dc4e3c8f61..00520988ab3 100644 --- a/account-decoder-client-types/Cargo.toml +++ b/account-decoder-client-types/Cargo.toml @@ -26,3 +26,6 @@ serde_json = { workspace = true } solana-account = { workspace = true } solana-pubkey = { workspace = true } zstd = { workspace = true, optional = true } + +[lints] +workspace = true diff --git a/accounts-cluster-bench/Cargo.toml b/accounts-cluster-bench/Cargo.toml index b71feeda172..d474b18cda9 100644 --- a/accounts-cluster-bench/Cargo.toml +++ b/accounts-cluster-bench/Cargo.toml @@ -58,3 +58,6 @@ solana-native-token = { workspace = true } solana-poh-config = { workspace = true } solana-runtime = { path = "../runtime", features = ["agave-unstable-api", "dev-context-only-utils"] } solana-test-validator = { path = "../test-validator" } + +[lints] +workspace = true diff --git a/accounts-db/store-histogram/Cargo.toml b/accounts-db/store-histogram/Cargo.toml index 52b25994f46..2613d40cd95 100644 --- a/accounts-db/store-histogram/Cargo.toml +++ b/accounts-db/store-histogram/Cargo.toml @@ -16,3 +16,6 @@ dev-context-only-utils = [] [dependencies] clap = { workspace = true } solana-version = { workspace = true } + +[lints] +workspace = true diff --git a/accounts-db/store-tool/Cargo.toml b/accounts-db/store-tool/Cargo.toml index 930e20214a3..1b6ab18304f 100644 --- a/accounts-db/store-tool/Cargo.toml +++ b/accounts-db/store-tool/Cargo.toml @@ -25,3 +25,6 @@ solana-accounts-db = { workspace = true, features = ["dev-context-only-utils"] } solana-pubkey = { workspace = true } solana-system-interface = { workspace = true } solana-version = { workspace = true } + +[lints] +workspace = true diff --git a/banking-stage-ingress-types/Cargo.toml b/banking-stage-ingress-types/Cargo.toml index 599876d201d..81f34ab2dc3 100644 --- a/banking-stage-ingress-types/Cargo.toml +++ b/banking-stage-ingress-types/Cargo.toml @@ -15,3 +15,6 @@ agave-unstable-api = [] [dependencies] crossbeam-channel = { workspace = true } solana-perf = { workspace = true } + +[lints] +workspace = true diff --git a/banks-client/Cargo.toml b/banks-client/Cargo.toml index cdcf920a9b5..98059cc180f 100644 --- a/banks-client/Cargo.toml +++ b/banks-client/Cargo.toml @@ -47,3 +47,6 @@ solana-pubkey = { workspace = true } solana-runtime = { path = "../runtime", features = ["agave-unstable-api", "dev-context-only-utils"] } solana-signer = { workspace = true } solana-system-interface = { workspace = true } + +[lints] +workspace = true diff --git a/banks-interface/Cargo.toml b/banks-interface/Cargo.toml index 3054a768e5c..0cfa7b6dd62 100644 --- a/banks-interface/Cargo.toml +++ b/banks-interface/Cargo.toml @@ -32,3 +32,6 @@ solana-transaction = { workspace = true, features = ["serde"] } solana-transaction-context = { workspace = true, features = ["serde"] } solana-transaction-error = { workspace = true, features = ["serde"] } tarpc = { workspace = true, features = ["full"] } + +[lints] +workspace = true diff --git a/banks-server/Cargo.toml b/banks-server/Cargo.toml index 4918440c22e..13ea619c701 100644 --- a/banks-server/Cargo.toml +++ b/banks-server/Cargo.toml @@ -42,3 +42,6 @@ tarpc = { workspace = true, features = ["full"] } tokio = { workspace = true, features = ["full"] } tokio-serde = { workspace = true, features = ["bincode"] } tokio-util = { workspace = true } + +[lints] +workspace = true diff --git a/bench-tps/Cargo.toml b/bench-tps/Cargo.toml index 7b0bf98d4bc..174d22d51c7 100644 --- a/bench-tps/Cargo.toml +++ b/bench-tps/Cargo.toml @@ -84,3 +84,6 @@ solana-sdk-ids = { workspace = true } solana-test-validator = { workspace = true } solana-tps-client = { workspace = true, features = ["bank-client"] } tempfile = { workspace = true } + +[lints] +workspace = true diff --git a/bucket_map/Cargo.toml b/bucket_map/Cargo.toml index d0bbf74fd73..06a29badba9 100644 --- a/bucket_map/Cargo.toml +++ b/bucket_map/Cargo.toml @@ -37,3 +37,6 @@ fs_extra = { workspace = true } rayon = { workspace = true } solana-bucket-map = { path = ".", features = ["agave-unstable-api", "dev-context-only-utils"] } solana-pubkey = { workspace = true, features = ["rand"] } + +[lints] +workspace = true diff --git a/clap-utils/Cargo.toml b/clap-utils/Cargo.toml index d36709a1916..92ce02ec88e 100644 --- a/clap-utils/Cargo.toml +++ b/clap-utils/Cargo.toml @@ -47,3 +47,6 @@ assert_matches = { workspace = true } solana-pubkey = { workspace = true, features = ["rand"] } solana-system-interface = { workspace = true, features = ["bincode"] } tempfile = { workspace = true } + +[lints] +workspace = true diff --git a/clap-v3-utils/Cargo.toml b/clap-v3-utils/Cargo.toml index 88cccc60b3d..7880dcd34a7 100644 --- a/clap-v3-utils/Cargo.toml +++ b/clap-v3-utils/Cargo.toml @@ -52,3 +52,6 @@ solana-clap-v3-utils = { path = ".", features = ["agave-unstable-api"] } solana-pubkey = { workspace = true, features = ["rand"] } solana-system-interface = { workspace = true, features = ["bincode"] } tempfile = { workspace = true } + +[lints] +workspace = true diff --git a/cli-config/Cargo.toml b/cli-config/Cargo.toml index fec95a467ad..59d223cf7a7 100644 --- a/cli-config/Cargo.toml +++ b/cli-config/Cargo.toml @@ -25,3 +25,6 @@ url = { workspace = true } [dev-dependencies] anyhow = { workspace = true } + +[lints] +workspace = true diff --git a/cli-output/Cargo.toml b/cli-output/Cargo.toml index 5dddd99d6c4..ead6be574cc 100644 --- a/cli-output/Cargo.toml +++ b/cli-output/Cargo.toml @@ -55,3 +55,6 @@ solana-keypair = { workspace = true } solana-seed-derivable = { workspace = true } solana-signer = { workspace = true } solana-transaction-context = { path = "../transaction-context", features = ["agave-unstable-api", "bincode"] } + +[lints] +workspace = true diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 3615acc3c98..4fa2db42af1 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -109,3 +109,6 @@ solana-sha256-hasher = { workspace = true } solana-test-validator = { path = "../test-validator" } tempfile = { workspace = true } test-case = { workspace = true } + +[lints] +workspace = true diff --git a/client-test/Cargo.toml b/client-test/Cargo.toml index bb9a16ba12c..d9380134c6b 100644 --- a/client-test/Cargo.toml +++ b/client-test/Cargo.toml @@ -47,3 +47,6 @@ agave-logger = { path = "../logger", features = ["agave-unstable-api"] } solana-net-utils = { path = "../net-utils", features = ["agave-unstable-api"] } solana-rpc = { path = "../rpc", features = ["agave-unstable-api", "dev-context-only-utils"] } solana-runtime = { path = "../runtime", features = ["agave-unstable-api", "dev-context-only-utils"] } + +[lints] +workspace = true diff --git a/client/Cargo.toml b/client/Cargo.toml index e46f8c0ce4b..8aac0c7b200 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -49,3 +49,6 @@ tokio = { workspace = true, features = ["full"] } [dev-dependencies] crossbeam-channel = { workspace = true } + +[lints] +workspace = true diff --git a/connection-cache/Cargo.toml b/connection-cache/Cargo.toml index 903bf89568b..f31d7672953 100644 --- a/connection-cache/Cargo.toml +++ b/connection-cache/Cargo.toml @@ -29,3 +29,6 @@ thiserror = { workspace = true } agave-logger = { path = "../logger", features = ["agave-unstable-api"] } rand_chacha = { workspace = true } solana-net-utils = { path = "../net-utils", features = ["agave-unstable-api"] } + +[lints] +workspace = true diff --git a/download-utils/Cargo.toml b/download-utils/Cargo.toml index cc9523832e1..de448541269 100644 --- a/download-utils/Cargo.toml +++ b/download-utils/Cargo.toml @@ -29,3 +29,6 @@ solana-runtime = { workspace = true } [dev-dependencies] solana-runtime = { path = "../runtime", features = ["agave-unstable-api", "dev-context-only-utils"] } + +[lints] +workspace = true diff --git a/entry/Cargo.toml b/entry/Cargo.toml index ab6625d63ca..6c23e0f8a39 100644 --- a/entry/Cargo.toml +++ b/entry/Cargo.toml @@ -66,3 +66,6 @@ solana-transaction = { workspace = true, features = ["verify"] } [[bench]] name = "verify_signatures" harness = false + +[lints] +workspace = true diff --git a/faucet-cli/Cargo.toml b/faucet-cli/Cargo.toml index a6512ac6184..19da65a7691 100644 --- a/faucet-cli/Cargo.toml +++ b/faucet-cli/Cargo.toml @@ -27,3 +27,6 @@ solana-keypair = { workspace = true } solana-metrics = { workspace = true } solana-version = { workspace = true } tokio = { workspace = true, features = ["full"] } + +[lints] +workspace = true diff --git a/faucet/Cargo.toml b/faucet/Cargo.toml index 43f8f0b8ae7..3f9ccab0a8b 100644 --- a/faucet/Cargo.toml +++ b/faucet/Cargo.toml @@ -43,3 +43,6 @@ wincode = { workspace = true } [dev-dependencies] solana-faucet = { path = ".", features = ["agave-unstable-api", "dev-context-only-utils"] } + +[lints] +workspace = true diff --git a/fee/Cargo.toml b/fee/Cargo.toml index 6884bacc230..e84737d6e05 100644 --- a/fee/Cargo.toml +++ b/fee/Cargo.toml @@ -16,3 +16,6 @@ agave-unstable-api = [] agave-feature-set = { workspace = true } solana-fee-structure = { workspace = true } solana-svm-transaction = { workspace = true } + +[lints] +workspace = true diff --git a/fs/Cargo.toml b/fs/Cargo.toml index 968ec48cfc2..4b013ed614f 100644 --- a/fs/Cargo.toml +++ b/fs/Cargo.toml @@ -35,3 +35,6 @@ tempfile = { workspace = true } rand = { workspace = true } tempfile = { workspace = true } test-case = { workspace = true } + +[lints] +workspace = true diff --git a/genesis-utils/Cargo.toml b/genesis-utils/Cargo.toml index 9f2c9915c88..e8d7f030f74 100644 --- a/genesis-utils/Cargo.toml +++ b/genesis-utils/Cargo.toml @@ -27,3 +27,6 @@ solana-genesis-config = { workspace = true } solana-hash = { workspace = true } solana-rpc-client = { workspace = true } thiserror = { workspace = true } + +[lints] +workspace = true diff --git a/genesis/Cargo.toml b/genesis/Cargo.toml index 443c182e570..7792f763e62 100644 --- a/genesis/Cargo.toml +++ b/genesis/Cargo.toml @@ -74,3 +74,6 @@ solana-pubkey = { workspace = true, features = ["rand"] } solana-runtime = { path = "../runtime", features = ["agave-unstable-api", "dev-context-only-utils"] } solana-vote = { path = "../vote", features = ["agave-unstable-api"] } test-case = { workspace = true } + +[lints] +workspace = true diff --git a/geyser-plugin-interface/Cargo.toml b/geyser-plugin-interface/Cargo.toml index 92476f3c575..93bf3da7b6e 100644 --- a/geyser-plugin-interface/Cargo.toml +++ b/geyser-plugin-interface/Cargo.toml @@ -24,3 +24,6 @@ solana-signature = { workspace = true } solana-transaction = { workspace = true } solana-transaction-status = { workspace = true } thiserror = { workspace = true } + +[lints] +workspace = true diff --git a/geyser-plugin-manager/Cargo.toml b/geyser-plugin-manager/Cargo.toml index 5f7b10f0b5c..cbdbb948be7 100644 --- a/geyser-plugin-manager/Cargo.toml +++ b/geyser-plugin-manager/Cargo.toml @@ -44,3 +44,6 @@ solana-transaction = { workspace = true } solana-transaction-status = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } + +[lints] +workspace = true diff --git a/gossip-cli/Cargo.toml b/gossip-cli/Cargo.toml index 8d4b224faf3..d3fe2fd495b 100644 --- a/gossip-cli/Cargo.toml +++ b/gossip-cli/Cargo.toml @@ -26,3 +26,6 @@ solana-gossip = { workspace = true, features = ["agave-unstable-api"] } solana-net-utils = { workspace = true, features = ["agave-unstable-api"] } solana-pubkey = { version = "=4.2.0", features = ["rand"] } solana-version = { workspace = true } + +[lints] +workspace = true diff --git a/install/Cargo.toml b/install/Cargo.toml index dd21b5d36fa..af061341bbf 100644 --- a/install/Cargo.toml +++ b/install/Cargo.toml @@ -58,3 +58,6 @@ url = { workspace = true } [target."cfg(windows)".dependencies] winapi = { workspace = true, features = ["minwindef", "winuser"] } winreg = { workspace = true } + +[lints] +workspace = true diff --git a/io-uring/Cargo.toml b/io-uring/Cargo.toml index 9891cb807a2..7cf92569a6b 100644 --- a/io-uring/Cargo.toml +++ b/io-uring/Cargo.toml @@ -20,3 +20,6 @@ smallvec = { workspace = true } [target.'cfg(target_os = "linux")'.dependencies] io-uring = { workspace = true } + +[lints] +workspace = true diff --git a/keygen/Cargo.toml b/keygen/Cargo.toml index 26130ba34d9..9531942ccca 100644 --- a/keygen/Cargo.toml +++ b/keygen/Cargo.toml @@ -46,3 +46,6 @@ tiny-bip39 = { workspace = true } [dev-dependencies] solana-pubkey = { workspace = true, features = ["rand"] } tempfile = { workspace = true } + +[lints] +workspace = true diff --git a/lattice-hash/Cargo.toml b/lattice-hash/Cargo.toml index b0706d7c92b..392bbe39d30 100644 --- a/lattice-hash/Cargo.toml +++ b/lattice-hash/Cargo.toml @@ -26,3 +26,6 @@ solana-lattice-hash = { path = ".", features = ["agave-unstable-api"] } [[bench]] name = "bench_lt_hash" harness = false + +[lints] +workspace = true diff --git a/ledger-tool/Cargo.toml b/ledger-tool/Cargo.toml index a15e5839f9d..b6530189633 100644 --- a/ledger-tool/Cargo.toml +++ b/ledger-tool/Cargo.toml @@ -99,3 +99,6 @@ signal-hook = { workspace = true } [dev-dependencies] assert_cmd = { workspace = true } tempfile = { workspace = true } + +[lints] +workspace = true diff --git a/local-cluster/Cargo.toml b/local-cluster/Cargo.toml index d8d5f5a0a36..e4de503774e 100644 --- a/local-cluster/Cargo.toml +++ b/local-cluster/Cargo.toml @@ -95,3 +95,6 @@ solana-genesis-utils = { path = "../genesis-utils", features = ["agave-unstable- solana-ledger = { path = "../ledger", features = ["agave-unstable-api", "dev-context-only-utils"] } solana-local-cluster = { path = ".", features = ["agave-unstable-api", "dev-context-only-utils"] } solana-runtime = { path = "../runtime", features = ["agave-unstable-api", "dev-context-only-utils"] } + +[lints] +workspace = true diff --git a/logger/Cargo.toml b/logger/Cargo.toml index 946d8fffc08..e929ccfa224 100644 --- a/logger/Cargo.toml +++ b/logger/Cargo.toml @@ -25,3 +25,6 @@ log = { workspace = true } [target."cfg(unix)".dependencies] libc = { workspace = true } signal-hook = { workspace = true } + +[lints] +workspace = true diff --git a/measure/Cargo.toml b/measure/Cargo.toml index e933420e0db..dcea4940cce 100644 --- a/measure/Cargo.toml +++ b/measure/Cargo.toml @@ -14,3 +14,6 @@ targets = ["x86_64-unknown-linux-gnu"] [features] agave-unstable-api = [] + +[lints] +workspace = true diff --git a/merkle-tree/Cargo.toml b/merkle-tree/Cargo.toml index 1b31febea98..e96412c165d 100644 --- a/merkle-tree/Cargo.toml +++ b/merkle-tree/Cargo.toml @@ -26,3 +26,6 @@ solana-sha256-hasher = { workspace = true } [dev-dependencies] hex = { workspace = true } + +[lints] +workspace = true diff --git a/metrics/Cargo.toml b/metrics/Cargo.toml index 7d313da1e7d..220496e2c09 100644 --- a/metrics/Cargo.toml +++ b/metrics/Cargo.toml @@ -39,3 +39,6 @@ solana-sha256-hasher = { workspace = true, features = ["sha2"] } [[bench]] name = "metrics" harness = false + +[lints] +workspace = true diff --git a/notifier/Cargo.toml b/notifier/Cargo.toml index 72e249f617c..3837f078bc7 100644 --- a/notifier/Cargo.toml +++ b/notifier/Cargo.toml @@ -23,3 +23,6 @@ log = { workspace = true } reqwest = { workspace = true, features = ["blocking", "brotli", "deflate", "gzip", "rustls-tls", "json"] } serde_json = { workspace = true } solana-hash = { workspace = true, features = ["std", "decode"] } + +[lints] +workspace = true diff --git a/poh-bench/Cargo.toml b/poh-bench/Cargo.toml index 77ba737d735..6ce95dddd56 100644 --- a/poh-bench/Cargo.toml +++ b/poh-bench/Cargo.toml @@ -24,3 +24,6 @@ solana-entry = { workspace = true } solana-measure = { workspace = true } solana-sha256-hasher = { workspace = true } solana-version = { workspace = true } + +[lints] +workspace = true diff --git a/poh/Cargo.toml b/poh/Cargo.toml index 1df97861866..48f136daae3 100644 --- a/poh/Cargo.toml +++ b/poh/Cargo.toml @@ -66,3 +66,6 @@ name = "poh" [[bench]] name = "transaction_recorder" harness = false + +[lints] +workspace = true diff --git a/program-binaries/Cargo.toml b/program-binaries/Cargo.toml index a80bdf4ef9d..bb0fd5c5171 100644 --- a/program-binaries/Cargo.toml +++ b/program-binaries/Cargo.toml @@ -19,3 +19,6 @@ solana-pubkey = { workspace = true } solana-rent = { workspace = true } solana-sdk-ids = { workspace = true } spl-generic-token = { workspace = true } + +[lints] +workspace = true diff --git a/program-test/Cargo.toml b/program-test/Cargo.toml index a132dbc306a..3cafec1c567 100644 --- a/program-test/Cargo.toml +++ b/program-test/Cargo.toml @@ -73,3 +73,6 @@ solana-cpi = { workspace = true } solana-program = { workspace = true } solana-program-test = { path = ".", features = ["agave-unstable-api"] } test-case = { workspace = true } + +[lints] +workspace = true diff --git a/programs/bpf-loader-tests/Cargo.toml b/programs/bpf-loader-tests/Cargo.toml index 69455a4c1aa..abb4438778e 100644 --- a/programs/bpf-loader-tests/Cargo.toml +++ b/programs/bpf-loader-tests/Cargo.toml @@ -34,3 +34,6 @@ solana-signer = { workspace = true } solana-system-interface = { workspace = true } solana-transaction = { workspace = true } solana-transaction-error = { workspace = true } + +[lints] +workspace = true diff --git a/programs/bpf_loader/Cargo.toml b/programs/bpf_loader/Cargo.toml index 2ce19efaf60..f079be29680 100644 --- a/programs/bpf_loader/Cargo.toml +++ b/programs/bpf_loader/Cargo.toml @@ -78,3 +78,6 @@ harness = false [[bench]] name = "bpf_loader_upgradeable" harness = false + +[lints] +workspace = true diff --git a/programs/compute-budget-bench/Cargo.toml b/programs/compute-budget-bench/Cargo.toml index 006f322e300..02db0a32310 100644 --- a/programs/compute-budget-bench/Cargo.toml +++ b/programs/compute-budget-bench/Cargo.toml @@ -27,3 +27,6 @@ solana-svm-transaction = { workspace = true } [[bench]] name = "compute_budget" harness = false + +[lints] +workspace = true diff --git a/programs/compute-budget/Cargo.toml b/programs/compute-budget/Cargo.toml index 722526b6b09..dd11702619d 100644 --- a/programs/compute-budget/Cargo.toml +++ b/programs/compute-budget/Cargo.toml @@ -21,3 +21,6 @@ agave-unstable-api = [] [dependencies] solana-program-runtime = { workspace = true } + +[lints] +workspace = true diff --git a/programs/ed25519-tests/Cargo.toml b/programs/ed25519-tests/Cargo.toml index 4fd9fc3d125..b676e59e43b 100644 --- a/programs/ed25519-tests/Cargo.toml +++ b/programs/ed25519-tests/Cargo.toml @@ -25,3 +25,6 @@ solana-program-test = { path = "../../program-test", features = ["agave-unstable solana-signer = { workspace = true } solana-transaction = { workspace = true } solana-transaction-error = { workspace = true } + +[lints] +workspace = true diff --git a/programs/system/Cargo.toml b/programs/system/Cargo.toml index b66ef9592c6..a66dd851ed2 100644 --- a/programs/system/Cargo.toml +++ b/programs/system/Cargo.toml @@ -53,3 +53,6 @@ solana-transaction-context = { path = "../../transaction-context", features = [" [[bench]] name = "system" harness = false + +[lints] +workspace = true diff --git a/programs/zk-elgamal-proof-tests/Cargo.toml b/programs/zk-elgamal-proof-tests/Cargo.toml index f86f3760f9b..badc620ad77 100644 --- a/programs/zk-elgamal-proof-tests/Cargo.toml +++ b/programs/zk-elgamal-proof-tests/Cargo.toml @@ -24,3 +24,6 @@ solana-system-interface = { workspace = true } solana-transaction = { workspace = true } solana-transaction-error = { workspace = true } solana-zk-sdk = { workspace = true } + +[lints] +workspace = true diff --git a/programs/zk-elgamal-proof/Cargo.toml b/programs/zk-elgamal-proof/Cargo.toml index bbd0dc132eb..4943b8d65fe 100644 --- a/programs/zk-elgamal-proof/Cargo.toml +++ b/programs/zk-elgamal-proof/Cargo.toml @@ -26,3 +26,6 @@ curve25519-dalek = { workspace = true } [[bench]] name = "verify_proofs" harness = false + +[lints] +workspace = true diff --git a/programs/zk-token-proof/Cargo.toml b/programs/zk-token-proof/Cargo.toml index 7c34e2409f1..ae27b00e111 100644 --- a/programs/zk-token-proof/Cargo.toml +++ b/programs/zk-token-proof/Cargo.toml @@ -13,3 +13,6 @@ agave-unstable-api = [] [dependencies] solana-program-runtime = { workspace = true } + +[lints] +workspace = true diff --git a/pubsub-client/Cargo.toml b/pubsub-client/Cargo.toml index 486061a2f57..e3e71855e58 100644 --- a/pubsub-client/Cargo.toml +++ b/pubsub-client/Cargo.toml @@ -37,3 +37,6 @@ url = { workspace = true } anyhow = { workspace = true } solana-commitment-config = { workspace = true } solana-pubkey = { workspace = true, features = ["rand"] } + +[lints] +workspace = true diff --git a/quic-client/Cargo.toml b/quic-client/Cargo.toml index 8a6ea2caea1..dd51e9b406d 100644 --- a/quic-client/Cargo.toml +++ b/quic-client/Cargo.toml @@ -41,3 +41,6 @@ solana-perf = { path = "../perf", features = ["agave-unstable-api"] } solana-quic-client = { path = ".", features = ["agave-unstable-api"] } solana-streamer = { path = "../streamer", features = ["agave-unstable-api", "dev-context-only-utils"] } tokio-util = { workspace = true } + +[lints] +workspace = true diff --git a/random/Cargo.toml b/random/Cargo.toml index 70b23088cbb..53b5402d6ef 100644 --- a/random/Cargo.toml +++ b/random/Cargo.toml @@ -28,3 +28,6 @@ rand_chacha = { workspace = true } solana-hash = { workspace = true, features = ["decode"] } solana-sha256-hasher = { workspace = true, features = ["sha2"] } test-case = { workspace = true } + +[lints] +workspace = true diff --git a/rayon-threadlimit/Cargo.toml b/rayon-threadlimit/Cargo.toml index e0fa018d178..7a552c56eb4 100644 --- a/rayon-threadlimit/Cargo.toml +++ b/rayon-threadlimit/Cargo.toml @@ -21,3 +21,6 @@ num_cpus = { workspace = true } [dev-dependencies] solana-rayon-threadlimit = { path = ".", features = ["agave-unstable-api"] } + +[lints] +workspace = true diff --git a/rbpf-cli/Cargo.toml b/rbpf-cli/Cargo.toml index aae11450fcb..f2f380ca472 100644 --- a/rbpf-cli/Cargo.toml +++ b/rbpf-cli/Cargo.toml @@ -14,3 +14,6 @@ edition = { workspace = true } agave-unstable-api = [] [dependencies] + +[lints] +workspace = true diff --git a/remote-wallet/Cargo.toml b/remote-wallet/Cargo.toml index 02a1b06352c..d05c65d6de5 100644 --- a/remote-wallet/Cargo.toml +++ b/remote-wallet/Cargo.toml @@ -42,3 +42,6 @@ uriparse = { workspace = true } assert_matches = { workspace = true } serial_test = { workspace = true } solana-pubkey = { workspace = true, features = ["rand"] } + +[lints] +workspace = true diff --git a/rpc-client-api/Cargo.toml b/rpc-client-api/Cargo.toml index 1605b8ac2bf..f6585059034 100644 --- a/rpc-client-api/Cargo.toml +++ b/rpc-client-api/Cargo.toml @@ -31,3 +31,6 @@ thiserror = { workspace = true } [dev-dependencies] test-case = { workspace = true } + +[lints] +workspace = true diff --git a/rpc-client-nonce-utils/Cargo.toml b/rpc-client-nonce-utils/Cargo.toml index ebc3b4ec388..b7afd7b5a96 100644 --- a/rpc-client-nonce-utils/Cargo.toml +++ b/rpc-client-nonce-utils/Cargo.toml @@ -43,3 +43,6 @@ solana-signer = { workspace = true } solana-system-interface = { workspace = true } solana-transaction = { workspace = true } tokio = { workspace = true, features = ["full"] } + +[lints] +workspace = true diff --git a/rpc-client-types/Cargo.toml b/rpc-client-types/Cargo.toml index 5b9b13cee73..be050842515 100644 --- a/rpc-client-types/Cargo.toml +++ b/rpc-client-types/Cargo.toml @@ -38,3 +38,6 @@ thiserror = { workspace = true } const_format = { workspace = true } solana-pubkey = { workspace = true, features = ["rand"] } solana-signature = { workspace = true } + +[lints] +workspace = true diff --git a/rpc-client/Cargo.toml b/rpc-client/Cargo.toml index e73d25aea09..2eb096d3f73 100644 --- a/rpc-client/Cargo.toml +++ b/rpc-client/Cargo.toml @@ -67,3 +67,6 @@ solana-signer = { workspace = true } solana-system-transaction = { workspace = true } static_assertions = { workspace = true } test-case = { workspace = true } + +[lints] +workspace = true diff --git a/rpc-test/Cargo.toml b/rpc-test/Cargo.toml index 1b6064bce4b..08d627035d3 100644 --- a/rpc-test/Cargo.toml +++ b/rpc-test/Cargo.toml @@ -51,3 +51,6 @@ solana-signature = { workspace = true } solana-signer = { workspace = true } solana-system-transaction = { workspace = true } solana-transaction = { workspace = true } + +[lints] +workspace = true diff --git a/rpc/Cargo.toml b/rpc/Cargo.toml index d27ca71ec82..305ae200955 100644 --- a/rpc/Cargo.toml +++ b/rpc/Cargo.toml @@ -124,3 +124,6 @@ solana-vote-interface = { workspace = true } spl-pod = { workspace = true } symlink = { workspace = true } test-case = { workspace = true } + +[lints] +workspace = true diff --git a/send-transaction-service/Cargo.toml b/send-transaction-service/Cargo.toml index 3df3566c3d9..31f330d9752 100644 --- a/send-transaction-service/Cargo.toml +++ b/send-transaction-service/Cargo.toml @@ -47,3 +47,6 @@ solana-runtime = { path = "../runtime", features = ["agave-unstable-api", "dev-c solana-signer = { workspace = true } solana-system-interface = { workspace = true } solana-system-transaction = { workspace = true } + +[lints] +workspace = true diff --git a/snapshots/Cargo.toml b/snapshots/Cargo.toml index fa106448c55..1181b418328 100644 --- a/snapshots/Cargo.toml +++ b/snapshots/Cargo.toml @@ -48,3 +48,6 @@ zstd = { workspace = true } agave-logger = { path = "../logger", features = ["agave-unstable-api"] } assert_matches = { workspace = true } solana-hash = { workspace = true, features = ["atomic"] } + +[lints] +workspace = true diff --git a/stake-accounts/Cargo.toml b/stake-accounts/Cargo.toml index a83553e3bd8..947fa69bab8 100644 --- a/stake-accounts/Cargo.toml +++ b/stake-accounts/Cargo.toml @@ -47,3 +47,6 @@ solana-version = { workspace = true } solana-client-traits = { workspace = true } solana-program-binaries = { path = "../program-binaries", features = ["agave-unstable-api"] } solana-runtime = { path = "../runtime", features = ["agave-unstable-api", "dev-context-only-utils"] } + +[lints] +workspace = true diff --git a/storage-bigtable/Cargo.toml b/storage-bigtable/Cargo.toml index b51f10aac1a..429b9d5e8b1 100644 --- a/storage-bigtable/Cargo.toml +++ b/storage-bigtable/Cargo.toml @@ -57,3 +57,6 @@ solana-keypair = { workspace = true } solana-pubkey = { workspace = true, features = ["rand"] } solana-system-transaction = { workspace = true } solana-transaction-context = { path = "../transaction-context", features = ["agave-unstable-api", "bincode"] } + +[lints] +workspace = true diff --git a/storage-bigtable/build-proto/Cargo.toml b/storage-bigtable/build-proto/Cargo.toml index f2b65e7a58c..0d18679375b 100644 --- a/storage-bigtable/build-proto/Cargo.toml +++ b/storage-bigtable/build-proto/Cargo.toml @@ -19,3 +19,6 @@ tonic-prost-build = { workspace = true } # envar to point to the installed binary [target."cfg(not(windows))".dependencies] protobuf-src = { workspace = true } + +[lints] +workspace = true diff --git a/storage-proto/Cargo.toml b/storage-proto/Cargo.toml index a715cf2c3ea..916e94ac721 100644 --- a/storage-proto/Cargo.toml +++ b/storage-proto/Cargo.toml @@ -48,3 +48,6 @@ protobuf-src = { workspace = true } [dev-dependencies] enum-iterator = { workspace = true } test-case = { workspace = true } + +[lints] +workspace = true diff --git a/streamer/Cargo.toml b/streamer/Cargo.toml index bbb1a01b7f1..9800d5b33aa 100644 --- a/streamer/Cargo.toml +++ b/streamer/Cargo.toml @@ -61,3 +61,6 @@ clap = { version = "4.5.31", features = ["cargo", "derive", "error-context"] } solana-message = { workspace = true } solana-net-utils = { path = "../net-utils", features = ["agave-unstable-api", "dev-context-only-utils"] } solana-streamer = { path = ".", features = ["agave-unstable-api", "dev-context-only-utils"] } + +[lints] +workspace = true diff --git a/svm-log-collector/Cargo.toml b/svm-log-collector/Cargo.toml index 70ee805d50f..ad678fe7bdc 100644 --- a/svm-log-collector/Cargo.toml +++ b/svm-log-collector/Cargo.toml @@ -17,3 +17,6 @@ agave-unstable-api = [] [dependencies] log = { workspace = true } + +[lints] +workspace = true diff --git a/svm-measure/Cargo.toml b/svm-measure/Cargo.toml index e415a31acd3..dd29a885ce1 100644 --- a/svm-measure/Cargo.toml +++ b/svm-measure/Cargo.toml @@ -14,3 +14,6 @@ targets = ["x86_64-unknown-linux-gnu"] [features] agave-unstable-api = [] + +[lints] +workspace = true diff --git a/svm-timings/Cargo.toml b/svm-timings/Cargo.toml index 2c5ca50dc8b..69aef7670ab 100644 --- a/svm-timings/Cargo.toml +++ b/svm-timings/Cargo.toml @@ -22,3 +22,6 @@ solana-pubkey = { workspace = true } [dev-dependencies] solana-pubkey = { workspace = true, features = ["rand"] } + +[lints] +workspace = true diff --git a/svm-transaction/Cargo.toml b/svm-transaction/Cargo.toml index 6316c00020c..2cf59d589ff 100644 --- a/svm-transaction/Cargo.toml +++ b/svm-transaction/Cargo.toml @@ -27,3 +27,6 @@ solana-pubkey = { workspace = true, features = ["std"] } solana-system-interface = { workspace = true, features = ["bincode"] } static_assertions = { workspace = true } test-case = { workspace = true } + +[lints] +workspace = true diff --git a/svm-type-overrides/Cargo.toml b/svm-type-overrides/Cargo.toml index 93a70c79b0f..eca951aa7b6 100644 --- a/svm-type-overrides/Cargo.toml +++ b/svm-type-overrides/Cargo.toml @@ -17,3 +17,6 @@ executor = ["dep:futures"] futures = { workspace = true, optional = true } rand = { workspace = true } shuttle = { workspace = true, optional = true } + +[lints] +workspace = true diff --git a/syscalls/gen-syscall-list/Cargo.toml b/syscalls/gen-syscall-list/Cargo.toml index 08853ff936b..2da52971626 100644 --- a/syscalls/gen-syscall-list/Cargo.toml +++ b/syscalls/gen-syscall-list/Cargo.toml @@ -13,3 +13,6 @@ agave-unstable-api = [] [build-dependencies] regex = { workspace = true } + +[lints] +workspace = true diff --git a/test-validator/Cargo.toml b/test-validator/Cargo.toml index e5ad302cc3d..5b0a06c658b 100644 --- a/test-validator/Cargo.toml +++ b/test-validator/Cargo.toml @@ -65,3 +65,6 @@ tokio = { workspace = true, features = ["full"] } [dev-dependencies] solana-sdk-ids = { workspace = true } + +[lints] +workspace = true diff --git a/tokens/Cargo.toml b/tokens/Cargo.toml index 1cb78b22e6c..2e4310025df 100644 --- a/tokens/Cargo.toml +++ b/tokens/Cargo.toml @@ -63,3 +63,6 @@ assert_matches = { workspace = true } bincode = { workspace = true } solana-test-validator = { path = "../test-validator" } solana-transaction-error = { workspace = true } + +[lints] +workspace = true diff --git a/tps-client/Cargo.toml b/tps-client/Cargo.toml index 81cd812360f..46fa5ebd2ae 100644 --- a/tps-client/Cargo.toml +++ b/tps-client/Cargo.toml @@ -47,3 +47,6 @@ thiserror = { workspace = true } [dev-dependencies] solana-runtime = { path = "../runtime", features = ["agave-unstable-api", "dev-context-only-utils"] } tempfile = { workspace = true } + +[lints] +workspace = true diff --git a/tpu-client-next/Cargo.toml b/tpu-client-next/Cargo.toml index a931995aabd..fd2e312dc49 100644 --- a/tpu-client-next/Cargo.toml +++ b/tpu-client-next/Cargo.toml @@ -56,3 +56,6 @@ solana-pubkey = { workspace = true } solana-signer = { workspace = true } solana-streamer = { path = "../streamer", features = ["agave-unstable-api", "dev-context-only-utils"] } solana-tpu-client-next = { path = ".", features = ["agave-unstable-api", "websocket-node-address-service", "dev-context-only-utils"] } + +[lints] +workspace = true diff --git a/tpu-client/Cargo.toml b/tpu-client/Cargo.toml index a07486ee207..9a83298a51c 100644 --- a/tpu-client/Cargo.toml +++ b/tpu-client/Cargo.toml @@ -41,3 +41,6 @@ solana-transaction-error = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true, features = ["full"] } wincode = { workspace = true } + +[lints] +workspace = true diff --git a/transaction-status-client-types/Cargo.toml b/transaction-status-client-types/Cargo.toml index c6607054c66..ed511cf4ca5 100644 --- a/transaction-status-client-types/Cargo.toml +++ b/transaction-status-client-types/Cargo.toml @@ -35,3 +35,6 @@ wincode = { workspace = true } [dev-dependencies] test-case = { workspace = true } + +[lints] +workspace = true diff --git a/transaction-status/Cargo.toml b/transaction-status/Cargo.toml index de075cc7ebf..d5413c0c551 100644 --- a/transaction-status/Cargo.toml +++ b/transaction-status/Cargo.toml @@ -62,3 +62,6 @@ spl-token-confidential-transfer-proof-extraction = { workspace = true } [[bench]] name = "extract_memos" harness = false + +[lints] +workspace = true diff --git a/transaction-view/Cargo.toml b/transaction-view/Cargo.toml index cebbfeff6a0..95283063445 100644 --- a/transaction-view/Cargo.toml +++ b/transaction-view/Cargo.toml @@ -47,3 +47,6 @@ harness = false [[bench]] name = "transaction_view" harness = false + +[lints] +workspace = true diff --git a/turbine/Cargo.toml b/turbine/Cargo.toml index a39d4da9181..ec2670cff85 100644 --- a/turbine/Cargo.toml +++ b/turbine/Cargo.toml @@ -77,3 +77,6 @@ harness = false [[bench]] name = "cluster_nodes" harness = false + +[lints] +workspace = true diff --git a/udp-client/Cargo.toml b/udp-client/Cargo.toml index 5a7ebdb4890..ea710e31ab1 100644 --- a/udp-client/Cargo.toml +++ b/udp-client/Cargo.toml @@ -25,3 +25,6 @@ solana-transaction-error = { workspace = true } solana-net-utils = { path = "../net-utils", features = ["agave-unstable-api", "dev-context-only-utils"] } solana-packet = { workspace = true } solana-streamer = { path = "../streamer", features = ["agave-unstable-api", "dev-context-only-utils"] } + +[lints] +workspace = true diff --git a/unified-scheduler-logic/Cargo.toml b/unified-scheduler-logic/Cargo.toml index f4f5fd38777..5784d7add2c 100644 --- a/unified-scheduler-logic/Cargo.toml +++ b/unified-scheduler-logic/Cargo.toml @@ -27,3 +27,6 @@ solana-instruction = { workspace = true } solana-message = { workspace = true } solana-runtime-transaction = { path = "../runtime-transaction", features = ["agave-unstable-api", "dev-context-only-utils"] } test-case = { workspace = true } + +[lints] +workspace = true diff --git a/unified-scheduler-pool/Cargo.toml b/unified-scheduler-pool/Cargo.toml index 0ee1c522215..920a4b1c3e8 100644 --- a/unified-scheduler-pool/Cargo.toml +++ b/unified-scheduler-pool/Cargo.toml @@ -51,3 +51,6 @@ solana-system-transaction = { workspace = true } # See order-crates-for-publishing.py for using this unusual `path = "."` solana-unified-scheduler-pool = { path = ".", features = ["dev-context-only-utils"] } test-case = { workspace = true } + +[lints] +workspace = true diff --git a/validator/Cargo.toml b/validator/Cargo.toml index 71797e023c1..853c5f9eed2 100644 --- a/validator/Cargo.toml +++ b/validator/Cargo.toml @@ -115,3 +115,6 @@ solana-runtime = { path = "../runtime", features = ["agave-unstable-api", "dev-c solana-time-utils = { workspace = true } tempfile = { workspace = true } test-case = { workspace = true } + +[lints] +workspace = true diff --git a/watchtower/Cargo.toml b/watchtower/Cargo.toml index 75775f36272..52bc6021d70 100644 --- a/watchtower/Cargo.toml +++ b/watchtower/Cargo.toml @@ -31,3 +31,6 @@ solana-pubkey = { version = "=4.2.0", default-features = false } solana-rpc-client = { workspace = true } solana-rpc-client-api = { workspace = true } solana-version = { workspace = true } + +[lints] +workspace = true diff --git a/xdp/Cargo.toml b/xdp/Cargo.toml index 559cc441777..d4437e904e5 100644 --- a/xdp/Cargo.toml +++ b/xdp/Cargo.toml @@ -25,3 +25,6 @@ arrayvec = { workspace = true } aya = { workspace = true } caps = { workspace = true } core_affinity = { workspace = true } + +[lints] +workspace = true From e1695f9e97fcfbdee4f6be24834fbd53dacc36f8 Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Fri, 22 May 2026 07:39:28 -0400 Subject: [PATCH 035/576] quic-client: remove block_on for set-identity (#12671) --- quic-client/src/quic_client.rs | 10 +++++++++- quic-client/tests/quic_client.rs | 24 ++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/quic-client/src/quic_client.rs b/quic-client/src/quic_client.rs index 0af536966f1..0d9a991766b 100644 --- a/quic-client/src/quic_client.rs +++ b/quic-client/src/quic_client.rs @@ -184,5 +184,13 @@ impl ClientConnection for QuicClientConnection { pub(crate) fn close_quic_connection(connection: Arc) { // Close the connection and release resources trace!("Closing QUIC connection to {}", connection.server_addr()); - RUNTIME.block_on(connection.close()); + // Connection caches can be dropped by async callers, such as admin RPC set-identity; + // blocking on RUNTIME from a Tokio worker would panic. + if tokio::runtime::Handle::try_current().is_ok() { + let _handle = RUNTIME.spawn(async move { + connection.close().await; + }); + } else { + RUNTIME.block_on(connection.close()); + } } diff --git a/quic-client/tests/quic_client.rs b/quic-client/tests/quic_client.rs index 9fc64c0ccb5..3071a9e3d1c 100644 --- a/quic-client/tests/quic_client.rs +++ b/quic-client/tests/quic_client.rs @@ -60,6 +60,30 @@ mod tests { ) } + #[tokio::test] + async fn test_quic_connection_cache_update_key_from_tokio_runtime() { + use { + solana_connection_cache::connection_cache::{ConnectionCache, NewConnectionConfig}, + solana_quic_client::{QuicConfig, QuicConnectionManager}, + }; + + let keypair = Keypair::new(); + let mut config = QuicConfig::new().unwrap(); + config.update_client_certificate(&keypair, IpAddr::V4(Ipv4Addr::LOCALHOST)); + let connection_manager = QuicConnectionManager::new_with_connection_config(config); + let connection_cache = ConnectionCache::new( + "quic_connection_cache_update_key_test", + connection_manager, + 1, + ) + .unwrap(); + + let server_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 12345); + let _connection = connection_cache.get_nonblocking_connection(&server_addr); + + connection_cache.update_key(&Keypair::new()).unwrap(); + } + #[test] fn test_quic_client_multiple_writes() { use { From 5aeb3491d8b329bc545f0a37b6ab0b366bbf59a0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 22:21:48 +0800 Subject: [PATCH 036/576] chore(deps): bump serde_json from 1.0.149 to 1.0.150 (#12675) * chore(deps): bump serde_json from 1.0.149 to 1.0.150 Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.149 to 1.0.150. - [Release notes](https://github.com/serde-rs/json/releases) - [Commits](https://github.com/serde-rs/json/compare/v1.0.149...v1.0.150) --- updated-dependencies: - dependency-name: serde_json dependency-version: 1.0.150 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Update all workspaces --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- ci/xtask/Cargo.lock | 4 ++-- dev-bins/Cargo.lock | 4 ++-- dev-bins/Cargo.toml | 2 +- programs/sbf/Cargo.lock | 4 ++-- programs/sbf/Cargo.toml | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 76809e5d901..91b33524f88 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6447,9 +6447,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", diff --git a/Cargo.toml b/Cargo.toml index d120f2bbdba..195297bec7d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -326,7 +326,7 @@ seqlock = "0.2.0" serde = { version = "1.0.228", features = ["derive"] } serde-big-array = "0.5.1" serde_bytes = "0.11.19" -serde_json = "1.0.149" +serde_json = "1.0.150" serde_with = { version = "3.20.0", default-features = false } serde_yaml = "0.9.34" serial_test = "3.4.0" diff --git a/ci/xtask/Cargo.lock b/ci/xtask/Cargo.lock index f34d0af04ae..a3cba5d4ca4 100644 --- a/ci/xtask/Cargo.lock +++ b/ci/xtask/Cargo.lock @@ -1726,9 +1726,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 2d0d3886287..ff5e87f24ea 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -5616,9 +5616,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", diff --git a/dev-bins/Cargo.toml b/dev-bins/Cargo.toml index f897eccfede..01e8586918b 100644 --- a/dev-bins/Cargo.toml +++ b/dev-bins/Cargo.toml @@ -58,7 +58,7 @@ rayon = "1.12.0" regex = "1.12.3" serde = { version = "1.0.228", features = ["derive"] } serde_bytes = "0.11.19" -serde_json = "1.0.149" +serde_json = "1.0.150" serde_yaml = "0.9.34" serial_test = "3.2.0" signal-hook = "0.4.4" diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index c839746caf9..00fbabc12c7 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -5531,9 +5531,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", diff --git a/programs/sbf/Cargo.toml b/programs/sbf/Cargo.toml index 9917fcce272..207eee70023 100644 --- a/programs/sbf/Cargo.toml +++ b/programs/sbf/Cargo.toml @@ -102,7 +102,7 @@ libsecp256k1 = { version = "0.7.2", default-features = false } num-derive = "0.4.2" num-traits = "0.2" serde = { version = "1.0.112", features = ["derive"] } -serde_json = "1.0.149" +serde_json = "1.0.150" solana-account-info = "=3.1.1" solana-account-view = "=2.0.0" solana-address = "=2.6.0" From e69ee0f99a3470909e8dbb35c9cd57c0a2ce9519 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 23:07:52 +0800 Subject: [PATCH 037/576] chore(deps): bump spl-memo-interface from 2.0.0 to 2.1.0 (#12692) * chore(deps): bump spl-memo-interface in /programs/sbf Bumps [spl-memo-interface](https://github.com/solana-labs/solana-program-library) from 2.0.0 to 2.1.0. - [Release notes](https://github.com/solana-labs/solana-program-library/releases) - [Commits](https://github.com/solana-labs/solana-program-library/compare/memo-v2.0.0...token-cli-v2.1.0) --- updated-dependencies: - dependency-name: spl-memo-interface dependency-version: 2.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * Update all workspaces --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 6 +++--- Cargo.toml | 2 +- dev-bins/Cargo.lock | 6 +++--- programs/sbf/Cargo.lock | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 91b33524f88..fe098def683 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11689,12 +11689,12 @@ dependencies = [ [[package]] name = "spl-memo-interface" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d4e2aedd58f858337fa609af5ad7100d4a243fdaf6a40d6eb4c28c5f19505d3" +checksum = "3745d384b0afee980d43d62b66c27bdcbbd03507732b8d3626d3413cb72084f2" dependencies = [ "solana-instruction", - "solana-pubkey 3.0.0", + "solana-pubkey 4.2.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 195297bec7d..aa84a1c4e69 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -523,7 +523,7 @@ solana-zk-sdk = "5.0.1" solana-zk-token-proof-program = { path = "programs/zk-token-proof", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } spl-associated-token-account-interface = "2.0.0" spl-generic-token = "2.0.0" -spl-memo-interface = "2.0.0" +spl-memo-interface = "2.1.0" spl-pod = "0.7.3" spl-token-2022-interface = "2.1.0" spl-token-confidential-transfer-proof-extraction = "0.5.1" diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index ff5e87f24ea..9bad86ade43 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -9812,12 +9812,12 @@ dependencies = [ [[package]] name = "spl-memo-interface" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d4e2aedd58f858337fa609af5ad7100d4a243fdaf6a40d6eb4c28c5f19505d3" +checksum = "3745d384b0afee980d43d62b66c27bdcbbd03507732b8d3626d3413cb72084f2" dependencies = [ "solana-instruction", - "solana-pubkey 3.0.0", + "solana-pubkey 4.2.0", ] [[package]] diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index 00fbabc12c7..40f9a6c7ea9 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -10292,12 +10292,12 @@ dependencies = [ [[package]] name = "spl-memo-interface" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d4e2aedd58f858337fa609af5ad7100d4a243fdaf6a40d6eb4c28c5f19505d3" +checksum = "3745d384b0afee980d43d62b66c27bdcbbd03507732b8d3626d3413cb72084f2" dependencies = [ "solana-instruction", - "solana-pubkey 3.0.0", + "solana-pubkey 4.2.0", ] [[package]] From 4d040cd87a48a922989ae4a27963fa5fdaff9a25 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 23:54:26 +0800 Subject: [PATCH 038/576] chore(deps): bump actions/stale from 10.2.0 to 10.3.0 (#12649) Bumps [actions/stale](https://github.com/actions/stale) from 10.2.0 to 10.3.0. - [Release notes](https://github.com/actions/stale/releases) - [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/stale/compare/b5d41d4e1d5dceea10e7104786b73624c18a190f...eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899) --- updated-dependencies: - dependency-name: actions/stale dependency-version: 10.3.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/manage-stale-issues.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/manage-stale-issues.yml b/.github/workflows/manage-stale-issues.yml index 09b5505c299..4f12c2f19af 100644 --- a/.github/workflows/manage-stale-issues.yml +++ b/.github/workflows/manage-stale-issues.yml @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-24.04 steps: - - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0 + - uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0 with: ascending: true # Spend API operations budget on older, more-likely-to-get-closed issues first close-issue-message: 'This issue was closed because it has been stale for 7 days with no activity.' From 59b94a394d162ffd9f84098f6a2f2688c3c9eb61 Mon Sep 17 00:00:00 2001 From: Kamil Skalski Date: Fri, 22 May 2026 18:30:23 +0200 Subject: [PATCH 039/576] feat: switch from solana-clock to solana_leader_scheadule::NUM_CONSECUTIVE_LEADER_SLOTS (#12691) Migrate to solana_leader_scheadule::NUM_CONSECUTIVE_LEADER_SLOTS --- Cargo.lock | 8 ++++++-- core/src/next_leader.rs | 6 ++++-- core/src/replay_stage.rs | 10 +++++----- core/src/replay_stage/tests.rs | 4 +++- dev-bins/Cargo.lock | 5 +++++ ledger/src/leader_schedule_cache.rs | 13 ++++++++----- programs/sbf/Cargo.lock | 5 +++++ rpc/Cargo.toml | 1 + rpc/src/cluster_tpu_info.rs | 15 ++++++++++----- tpu-client-next/Cargo.toml | 1 + tpu-client-next/src/connection_worker.rs | 5 +++-- .../leader_tpu_cache_service.rs | 10 ++++++---- tpu-client/Cargo.toml | 1 + tpu-client/src/nonblocking/tpu_client.rs | 7 ++++--- turbine/Cargo.toml | 2 +- turbine/src/broadcast_stage.rs | 11 ++++++----- votor/Cargo.toml | 1 + votor/src/consensus_pool/parent_ready_tracker.rs | 13 +++++++------ votor/src/consensus_pool/slot_stake_counters.rs | 5 +++-- votor/src/timer_manager/timers.rs | 7 +++++-- 20 files changed, 85 insertions(+), 45 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fe098def683..6cdb567df32 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -458,6 +458,7 @@ dependencies = [ "solana-gossip", "solana-hash 4.3.0", "solana-keypair", + "solana-leader-schedule", "solana-ledger", "solana-measure", "solana-metrics", @@ -7667,9 +7668,9 @@ dependencies = [ [[package]] name = "solana-clock" -version = "3.0.1" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95cf11109c3b6115cc510f1e31f06fdd52f504271bc24ef5f1249fbbcae5f9f3" +checksum = "5ea35d8f69b67daddb921a9da7f78ca591b533cf5e98833cd9ae62fdc2e4652c" dependencies = [ "serde", "serde_derive", @@ -9753,6 +9754,7 @@ dependencies = [ "solana-hash 4.3.0", "solana-instruction", "solana-keypair", + "solana-leader-schedule", "solana-ledger", "solana-measure", "solana-message", @@ -11067,6 +11069,7 @@ dependencies = [ "solana-commitment-config", "solana-connection-cache", "solana-epoch-schedule", + "solana-leader-schedule", "solana-message", "solana-pubkey 4.2.0", "solana-pubsub-client", @@ -11098,6 +11101,7 @@ dependencies = [ "solana-clock", "solana-commitment-config", "solana-keypair", + "solana-leader-schedule", "solana-measure", "solana-metrics", "solana-net-utils", diff --git a/core/src/next_leader.rs b/core/src/next_leader.rs index 90187ef8a10..2fb409ba8d0 100644 --- a/core/src/next_leader.rs +++ b/core/src/next_leader.rs @@ -1,11 +1,12 @@ use { crate::banking_stage::LikeClusterInfo, itertools::Itertools, - solana_clock::{FORWARD_TRANSACTIONS_TO_LEADER_AT_SLOT_OFFSET, NUM_CONSECUTIVE_LEADER_SLOTS}, + solana_clock::FORWARD_TRANSACTIONS_TO_LEADER_AT_SLOT_OFFSET, solana_gossip::{ cluster_info::ClusterInfo, contact_info::{ContactInfoQuery, Protocol}, }, + solana_leader_schedule::NUM_CONSECUTIVE_LEADER_SLOTS, solana_poh::poh_recorder::PohRecorder, std::{net::SocketAddr, sync::RwLock}, }; @@ -46,7 +47,8 @@ pub(crate) fn next_leaders( let leader_pubkeys: Vec<_> = (0..max_count) .filter_map(|i| { recorder.leader_after_n_slots( - FORWARD_TRANSACTIONS_TO_LEADER_AT_SLOT_OFFSET + i * NUM_CONSECUTIVE_LEADER_SLOTS, + FORWARD_TRANSACTIONS_TO_LEADER_AT_SLOT_OFFSET + + i * NUM_CONSECUTIVE_LEADER_SLOTS.get() as u64, ) }) .collect(); diff --git a/core/src/replay_stage.rs b/core/src/replay_stage.rs index 78d08f92de7..07e52504338 100644 --- a/core/src/replay_stage.rs +++ b/core/src/replay_stage.rs @@ -51,12 +51,12 @@ use { itertools::Itertools, rayon::{ThreadPool, prelude::*}, solana_accounts_db::contains::Contains, - solana_clock::{BankId, NUM_CONSECUTIVE_LEADER_SLOTS, Slot}, + solana_clock::{BankId, Slot}, solana_geyser_plugin_manager::block_metadata_notifier_interface::BlockMetadataNotifierArc, solana_gossip::cluster_info::ClusterInfo, solana_hash::Hash, solana_keypair::Keypair, - solana_leader_schedule::SlotLeader, + solana_leader_schedule::{NUM_CONSECUTIVE_LEADER_SLOTS, SlotLeader}, solana_ledger::{ blockstore::{Blockstore, BlockstoreError, UpdateParentReceiver}, blockstore_meta::BlockLocation, @@ -2778,7 +2778,7 @@ impl ReplayStage { progress_map.get_latest_leader_slot_must_exist(parent_slot) { let skip_propagated_check = - poh_slot - latest_leader_slot < NUM_CONSECUTIVE_LEADER_SLOTS; + poh_slot - latest_leader_slot < NUM_CONSECUTIVE_LEADER_SLOTS.get() as Slot; if skip_propagated_check { return true; } @@ -2795,7 +2795,7 @@ impl ReplayStage { fn should_retransmit(poh_slot: Slot, last_retransmit_slot: &mut Slot) -> bool { if poh_slot < *last_retransmit_slot - || poh_slot >= *last_retransmit_slot + NUM_CONSECUTIVE_LEADER_SLOTS + || poh_slot >= *last_retransmit_slot + NUM_CONSECUTIVE_LEADER_SLOTS.get() as Slot { *last_retransmit_slot = poh_slot; true @@ -4226,7 +4226,7 @@ impl ReplayStage { return; }; - let end_slot = next_slot.saturating_add(NUM_CONSECUTIVE_LEADER_SLOTS - 1); + let end_slot = next_slot.saturating_add(NUM_CONSECUTIVE_LEADER_SLOTS.get() as Slot - 1); let leader_window_info = LeaderWindowInfo { start_slot: next_slot, end_slot, diff --git a/core/src/replay_stage/tests.rs b/core/src/replay_stage/tests.rs index 171ea94262d..318271cd33a 100644 --- a/core/src/replay_stage/tests.rs +++ b/core/src/replay_stage/tests.rs @@ -22,7 +22,6 @@ use { solana_accounts_db::accounts_db::{ACCOUNTS_DB_CONFIG_FOR_TESTING, AccountsDbConfig}, solana_bls_signatures::{BLS_SIGNATURE_AFFINE_SIZE, Signature as BLSSignature}, solana_client::connection_cache::ConnectionCache, - solana_clock::NUM_CONSECUTIVE_LEADER_SLOTS, solana_entry::{ block_component::{ BlockComponent, BlockFooterV1, BlockHeaderV1, UpdateParentV1, VersionedBlockMarker, @@ -35,6 +34,7 @@ use { solana_hash::Hash, solana_instruction::error::InstructionError, solana_keypair::Keypair, + solana_leader_schedule::NUM_CONSECUTIVE_LEADER_SLOTS as NUM_CONSECUTIVE_LEADER_SLOTS_NZ, solana_ledger::{ block_error::BlockError, blockstore::{ @@ -80,6 +80,8 @@ use { trees::{Tree, tr}, }; +const NUM_CONSECUTIVE_LEADER_SLOTS: Slot = NUM_CONSECUTIVE_LEADER_SLOTS_NZ.get() as Slot; + static_assertions::const_assert!(REFRESH_VOTE_BLOCKHEIGHT < solana_clock::MAX_PROCESSING_AGE); impl ProcessActiveBanksContext { diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 9bad86ade43..bca82688c91 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -369,6 +369,7 @@ dependencies = [ "solana-gossip", "solana-hash 4.3.0", "solana-keypair", + "solana-leader-schedule", "solana-ledger", "solana-measure", "solana-metrics", @@ -8232,6 +8233,7 @@ dependencies = [ "solana-gossip", "solana-hash 4.3.0", "solana-keypair", + "solana-leader-schedule", "solana-ledger", "solana-measure", "solana-message", @@ -9269,6 +9271,7 @@ dependencies = [ "solana-commitment-config", "solana-connection-cache", "solana-epoch-schedule", + "solana-leader-schedule", "solana-message", "solana-pubkey 4.2.0", "solana-pubsub-client", @@ -9297,6 +9300,7 @@ dependencies = [ "solana-clock", "solana-commitment-config", "solana-keypair", + "solana-leader-schedule", "solana-measure", "solana-metrics", "solana-packet 4.1.0", @@ -9449,6 +9453,7 @@ dependencies = [ "solana-gossip", "solana-hash 4.3.0", "solana-keypair", + "solana-leader-schedule", "solana-ledger", "solana-measure", "solana-metrics", diff --git a/ledger/src/leader_schedule_cache.rs b/ledger/src/leader_schedule_cache.rs index a33ea4e6661..8932187317b 100644 --- a/ledger/src/leader_schedule_cache.rs +++ b/ledger/src/leader_schedule_cache.rs @@ -244,12 +244,12 @@ mod tests { staking_utils::tests::setup_vote_and_stake_accounts, }, crossbeam_channel::unbounded, - solana_clock::{DEFAULT_SLOTS_PER_EPOCH, NUM_CONSECUTIVE_LEADER_SLOTS}, + solana_clock::DEFAULT_SLOTS_PER_EPOCH, solana_epoch_schedule::{ DEFAULT_LEADER_SCHEDULE_SLOT_OFFSET, EpochSchedule, MINIMUM_SLOTS_PER_EPOCH, }, solana_keypair::Keypair, - solana_leader_schedule::{LeaderSchedule, SlotLeader}, + solana_leader_schedule::{LeaderSchedule, NUM_CONSECUTIVE_LEADER_SLOTS, SlotLeader}, solana_runtime::stake_utils, solana_signer::Signer, std::{sync::Arc, thread::Builder}, @@ -571,7 +571,7 @@ mod tests { .unwrap(); assert_eq!(res.0, expected_slot); - assert!(res.1 >= expected_slot + NUM_CONSECUTIVE_LEADER_SLOTS - 1); + assert!(res.1 >= expected_slot + NUM_CONSECUTIVE_LEADER_SLOTS.get() as Slot - 1); let res = cache .next_leader_slot( @@ -579,12 +579,15 @@ mod tests { 0, &bank, None, - NUM_CONSECUTIVE_LEADER_SLOTS - 1, + NUM_CONSECUTIVE_LEADER_SLOTS.get() as Slot - 1, ) .unwrap(); assert_eq!(res.0, expected_slot); - assert_eq!(res.1, expected_slot + NUM_CONSECUTIVE_LEADER_SLOTS - 2); + assert_eq!( + res.1, + expected_slot + NUM_CONSECUTIVE_LEADER_SLOTS.get() as Slot - 2 + ); } #[test] diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index 40f9a6c7ea9..29c9207df50 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -357,6 +357,7 @@ dependencies = [ "solana-gossip", "solana-hash 4.3.0", "solana-keypair", + "solana-leader-schedule", "solana-ledger", "solana-measure", "solana-metrics", @@ -8039,6 +8040,7 @@ dependencies = [ "solana-gossip", "solana-hash 4.3.0", "solana-keypair", + "solana-leader-schedule", "solana-ledger", "solana-measure", "solana-message", @@ -9761,6 +9763,7 @@ dependencies = [ "solana-commitment-config", "solana-connection-cache", "solana-epoch-schedule", + "solana-leader-schedule", "solana-message", "solana-pubkey 4.2.0", "solana-pubsub-client", @@ -9789,6 +9792,7 @@ dependencies = [ "solana-clock", "solana-commitment-config", "solana-keypair", + "solana-leader-schedule", "solana-measure", "solana-metrics", "solana-packet 4.1.0", @@ -9941,6 +9945,7 @@ dependencies = [ "solana-gossip", "solana-hash 4.3.0", "solana-keypair", + "solana-leader-schedule", "solana-ledger", "solana-measure", "solana-metrics", diff --git a/rpc/Cargo.toml b/rpc/Cargo.toml index 305ae200955..6b9d3f3e8b1 100644 --- a/rpc/Cargo.toml +++ b/rpc/Cargo.toml @@ -56,6 +56,7 @@ solana-genesis-config = { workspace = true } solana-gossip = { workspace = true } solana-hash = { workspace = true } solana-keypair = { workspace = true } +solana-leader-schedule = { workspace = true } solana-ledger = { workspace = true } solana-measure = { workspace = true } solana-message = { workspace = true } diff --git a/rpc/src/cluster_tpu_info.rs b/rpc/src/cluster_tpu_info.rs index d67271f731d..f941f077eed 100644 --- a/rpc/src/cluster_tpu_info.rs +++ b/rpc/src/cluster_tpu_info.rs @@ -1,6 +1,6 @@ use { - solana_clock::NUM_CONSECUTIVE_LEADER_SLOTS, solana_gossip::{cluster_info::ClusterInfo, contact_info::Protocol}, + solana_leader_schedule::NUM_CONSECUTIVE_LEADER_SLOTS, solana_poh::poh_recorder::PohRecorder, solana_pubkey::Pubkey, solana_send_transaction_service::tpu_info::TpuInfo, @@ -43,7 +43,9 @@ impl TpuInfo for ClusterTpuInfo { fn get_leader_tpus(&self, max_count: u64) -> Vec<&SocketAddr> { let recorder = self.poh_recorder.read().unwrap(); let leaders: Vec<_> = (0..max_count) - .filter_map(|i| recorder.leader_after_n_slots(i * NUM_CONSECUTIVE_LEADER_SLOTS)) + .filter_map(|i| { + recorder.leader_after_n_slots(i * NUM_CONSECUTIVE_LEADER_SLOTS.get() as u64) + }) .collect(); drop(recorder); let mut unique_leaders = vec![]; @@ -60,7 +62,9 @@ impl TpuInfo for ClusterTpuInfo { fn get_not_unique_leader_tpus(&self, max_count: u64) -> Vec<&SocketAddr> { let recorder = self.poh_recorder.read().unwrap(); let leader_pubkeys: Vec<_> = (0..max_count) - .filter_map(|i| recorder.leader_after_n_slots(i * NUM_CONSECUTIVE_LEADER_SLOTS)) + .filter_map(|i| { + recorder.leader_after_n_slots(i * NUM_CONSECUTIVE_LEADER_SLOTS.get() as u64) + }) .collect(); drop(recorder); leader_pubkeys @@ -74,6 +78,7 @@ impl TpuInfo for ClusterTpuInfo { mod test { use { super::*, + solana_clock::Slot, solana_gossip::contact_info::ContactInfo, solana_keypair::Keypair, solana_ledger::{ @@ -239,7 +244,7 @@ mod test { ); let second_leader = solana_runtime::leader_schedule_utils::slot_leader_at( - slot + NUM_CONSECUTIVE_LEADER_SLOTS, + slot + NUM_CONSECUTIVE_LEADER_SLOTS.get() as Slot, &bank, ) .unwrap(); @@ -255,7 +260,7 @@ mod test { ); let third_leader = solana_runtime::leader_schedule_utils::slot_leader_at( - slot + (2 * NUM_CONSECUTIVE_LEADER_SLOTS), + slot + (2 * NUM_CONSECUTIVE_LEADER_SLOTS.get() as Slot), &bank, ) .unwrap(); diff --git a/tpu-client-next/Cargo.toml b/tpu-client-next/Cargo.toml index fd2e312dc49..084ed02608b 100644 --- a/tpu-client-next/Cargo.toml +++ b/tpu-client-next/Cargo.toml @@ -29,6 +29,7 @@ rustls = { workspace = true } solana-clock = { workspace = true } solana-commitment-config = { workspace = true } solana-keypair = { workspace = true } +solana-leader-schedule = { workspace = true } solana-measure = { workspace = true } solana-metrics = { workspace = true, optional = true } solana-packet = { workspace = true } diff --git a/tpu-client-next/src/connection_worker.rs b/tpu-client-next/src/connection_worker.rs index 17f516f7e2e..20c7748e0f3 100644 --- a/tpu-client-next/src/connection_worker.rs +++ b/tpu-client-next/src/connection_worker.rs @@ -11,7 +11,8 @@ use { transaction_batch::TransactionBatch, }, quinn::{ConnectError, Connection, ConnectionError, Endpoint}, - solana_clock::{DEFAULT_MS_PER_SLOT, MAX_PROCESSING_AGE, NUM_CONSECUTIVE_LEADER_SLOTS}, + solana_clock::{DEFAULT_MS_PER_SLOT, MAX_PROCESSING_AGE}, + solana_leader_schedule::NUM_CONSECUTIVE_LEADER_SLOTS, solana_measure::measure::Measure, solana_time_utils::timestamp, solana_tls_utils::socket_addr_to_quic_server_name, @@ -34,7 +35,7 @@ pub(crate) const DEFAULT_MAX_CONNECTION_HANDSHAKE_TIMEOUT: Duration = Duration:: /// Interval between retry attempts for creating a new connection. This value is /// a best-effort estimate, based on current network conditions. const RETRY_SLEEP_INTERVAL: Duration = - Duration::from_millis(NUM_CONSECUTIVE_LEADER_SLOTS * DEFAULT_MS_PER_SLOT); + Duration::from_millis(NUM_CONSECUTIVE_LEADER_SLOTS.get() as u64 * DEFAULT_MS_PER_SLOT); /// Maximum age (in milliseconds) of a blockhash, beyond which transaction /// batches are dropped. diff --git a/tpu-client-next/src/node_address_service/leader_tpu_cache_service.rs b/tpu-client-next/src/node_address_service/leader_tpu_cache_service.rs index d399c4c733e..27c12c26ae2 100644 --- a/tpu-client-next/src/node_address_service/leader_tpu_cache_service.rs +++ b/tpu-client-next/src/node_address_service/leader_tpu_cache_service.rs @@ -9,8 +9,9 @@ use { node_address_service::SlotReceiver, }, async_trait::async_trait, - solana_clock::{NUM_CONSECUTIVE_LEADER_SLOTS, Slot}, + solana_clock::Slot, solana_commitment_config::CommitmentConfig, + solana_leader_schedule::NUM_CONSECUTIVE_LEADER_SLOTS, solana_pubkey::Pubkey, solana_rpc_client::nonblocking::rpc_client::RpcClient, solana_rpc_client_api::{client_error::Error as ClientError, response::RpcContactInfo}, @@ -292,13 +293,14 @@ fn leader_sockets( slot_leaders: &SlotLeaders, leader_tpu_map: &LeaderTpuMap, ) -> Vec { - let fanout_slots = (lookahead_leaders as u64).saturating_mul(NUM_CONSECUTIVE_LEADER_SLOTS); - let mut leader_sockets = Vec::with_capacity(lookahead_leaders as usize); + let lookahead_leaders = lookahead_leaders as usize; + let fanout_slots = lookahead_leaders.saturating_mul(NUM_CONSECUTIVE_LEADER_SLOTS.get()) as u64; + let mut leader_sockets = Vec::with_capacity(lookahead_leaders); // `slot_leaders.first_slot` might have been advanced since caller last read it. Take the // greater of the two values to ensure we are reading from the latest leader schedule. let current_slot = std::cmp::max(first_slot, slot_leaders.first_slot); for leader_slot in - (current_slot..current_slot + fanout_slots).step_by(NUM_CONSECUTIVE_LEADER_SLOTS as usize) + (current_slot..current_slot + fanout_slots).step_by(NUM_CONSECUTIVE_LEADER_SLOTS.get()) { if let Some(leader) = slot_leaders.slot_leader(leader_slot) { if let Some(tpu_socket) = leader_tpu_map.get(leader) { diff --git a/tpu-client/Cargo.toml b/tpu-client/Cargo.toml index 9a83298a51c..70cb0de90df 100644 --- a/tpu-client/Cargo.toml +++ b/tpu-client/Cargo.toml @@ -29,6 +29,7 @@ solana-clock = { workspace = true } solana-commitment-config = { workspace = true } solana-connection-cache = { workspace = true } solana-epoch-schedule = { workspace = true } +solana-leader-schedule = { workspace = true } solana-message = { workspace = true, optional = true } solana-pubkey = { workspace = true } solana-pubsub-client = { workspace = true } diff --git a/tpu-client/src/nonblocking/tpu_client.rs b/tpu-client/src/nonblocking/tpu_client.rs index 1236e3bc2a9..abe797c9b7b 100644 --- a/tpu-client/src/nonblocking/tpu_client.rs +++ b/tpu-client/src/nonblocking/tpu_client.rs @@ -3,7 +3,7 @@ use { crate::tpu_client::{MAX_FANOUT_SLOTS, RecentLeaderSlots, TpuClientConfig}, futures_util::{future::join_all, stream::StreamExt}, log::*, - solana_clock::{DEFAULT_MS_PER_SLOT, NUM_CONSECUTIVE_LEADER_SLOTS, Slot}, + solana_clock::{DEFAULT_MS_PER_SLOT, Slot}, solana_commitment_config::CommitmentConfig, solana_connection_cache::{ connection_cache::{ @@ -13,6 +13,7 @@ use { nonblocking::client_connection::ClientConnection, }, solana_epoch_schedule::EpochSchedule, + solana_leader_schedule::NUM_CONSECUTIVE_LEADER_SLOTS, solana_pubkey::Pubkey, solana_pubsub_client::nonblocking::pubsub_client::{PubsubClient, PubsubClientError}, solana_rpc_client::nonblocking::rpc_client::RpcClient, @@ -153,8 +154,8 @@ impl LeaderTpuCache { // value. Take the greater of the two values to ensure we are reading from the latest // leader schedule. let current_slot = std::cmp::max(estimated_current_slot, self.first_slot); - for leader_slot in (current_slot..current_slot + fanout_slots) - .step_by(NUM_CONSECUTIVE_LEADER_SLOTS as usize) + for leader_slot in + (current_slot..current_slot + fanout_slots).step_by(NUM_CONSECUTIVE_LEADER_SLOTS.get()) { if let Some(leader) = self.get_slot_leader(leader_slot) { if let Some(tpu_socket) = self.leader_tpu_map.get(leader) { diff --git a/turbine/Cargo.toml b/turbine/Cargo.toml index ec2670cff85..3a8bd0116ae 100644 --- a/turbine/Cargo.toml +++ b/turbine/Cargo.toml @@ -33,6 +33,7 @@ solana-entry = { workspace = true } solana-gossip = { workspace = true } solana-hash = { workspace = true } solana-keypair = { workspace = true } +solana-leader-schedule = { workspace = true } solana-ledger = { workspace = true } solana-measure = { workspace = true } solana-metrics = { workspace = true } @@ -62,7 +63,6 @@ bencher = { workspace = true } bs58 = { workspace = true } solana-genesis-config = { workspace = true } solana-gossip = { path = "../gossip", features = ["agave-unstable-api", "dev-context-only-utils"] } -solana-leader-schedule = { workspace = true } solana-ledger = { path = "../ledger", features = ["agave-unstable-api", "dev-context-only-utils"] } solana-runtime = { path = "../runtime", features = ["agave-unstable-api", "dev-context-only-utils"] } solana-signature = { workspace = true, features = ["rand"] } diff --git a/turbine/src/broadcast_stage.rs b/turbine/src/broadcast_stage.rs index e899e68ed2c..708b997eb6a 100644 --- a/turbine/src/broadcast_stage.rs +++ b/turbine/src/broadcast_stage.rs @@ -13,12 +13,13 @@ use { agave_votor::event::VotorEventSender, crossbeam_channel::{Receiver, RecvError, RecvTimeoutError, Sender, unbounded}, itertools::Itertools, - solana_clock::{NUM_CONSECUTIVE_LEADER_SLOTS, Slot}, + solana_clock::Slot, solana_gossip::{ cluster_info::{ClusterInfo, ClusterInfoError}, contact_info::Protocol, }, solana_keypair::Keypair, + solana_leader_schedule::NUM_CONSECUTIVE_LEADER_SLOTS, solana_ledger::{ blockstore::Blockstore, leader_schedule_cache::LeaderScheduleCache, shred::Shred, }, @@ -470,7 +471,7 @@ fn next_broadcast_leader_pubkey( my_pubkey: &Pubkey, slot: Slot, ) -> Option { - let next_leader_slot = slot.saturating_add(NUM_CONSECUTIVE_LEADER_SLOTS); + let next_leader_slot = slot.saturating_add(NUM_CONSECUTIVE_LEADER_SLOTS.get() as Slot); leader_schedule_cache .slot_leader_at(next_leader_slot, Some(working_bank)) .map(|next_leader| next_leader.id) @@ -706,7 +707,7 @@ pub mod test { vote_address: Pubkey::new_unique(), }, ], - NUM_CONSECUTIVE_LEADER_SLOTS as usize, + NUM_CONSECUTIVE_LEADER_SLOTS.get(), &bank, ); @@ -719,7 +720,7 @@ pub mod test { &leader_schedule_cache, &bank, &leader_b.pubkey(), - NUM_CONSECUTIVE_LEADER_SLOTS, + NUM_CONSECUTIVE_LEADER_SLOTS.get() as Slot, ), Some(leader_a.pubkey()) ); @@ -728,7 +729,7 @@ pub mod test { &leader_schedule_cache, &bank, &leader_a.pubkey(), - NUM_CONSECUTIVE_LEADER_SLOTS, + NUM_CONSECUTIVE_LEADER_SLOTS.get() as Slot, ), None, ); diff --git a/votor/Cargo.toml b/votor/Cargo.toml index 3413f3717f2..84c8918c662 100644 --- a/votor/Cargo.toml +++ b/votor/Cargo.toml @@ -58,6 +58,7 @@ solana-frozen-abi-macro = { workspace = true, optional = true, features = [ solana-gossip = { workspace = true } solana-hash = { workspace = true } solana-keypair = { workspace = true } +solana-leader-schedule = { workspace = true } solana-ledger = { workspace = true } solana-measure = { workspace = true } solana-metrics = { workspace = true } diff --git a/votor/src/consensus_pool/parent_ready_tracker.rs b/votor/src/consensus_pool/parent_ready_tracker.rs index a5221512912..971ef42d46e 100644 --- a/votor/src/consensus_pool/parent_ready_tracker.rs +++ b/votor/src/consensus_pool/parent_ready_tracker.rs @@ -16,8 +16,9 @@ use { crate::{common::MAX_NOTAR_FALLBACK_BLOCKS, event::VotorEvent}, agave_votor_messages::consensus_message::Block, core::fmt, - solana_clock::{NUM_CONSECUTIVE_LEADER_SLOTS, Slot}, + solana_clock::Slot, solana_gossip::cluster_info::ClusterInfo, + solana_leader_schedule::NUM_CONSECUTIVE_LEADER_SLOTS, std::{collections::HashMap, sync::Arc}, }; @@ -121,7 +122,7 @@ impl ParentReadyTracker { status.parents_ready.push(block); // Only notify for parent ready on first leader slots - if s % NUM_CONSECUTIVE_LEADER_SLOTS == 0 { + if s.is_multiple_of(NUM_CONSECUTIVE_LEADER_SLOTS.get() as Slot) { events.push(VotorEvent::ParentReady { slot: s, parent_block: block, @@ -192,7 +193,7 @@ impl ParentReadyTracker { } status.parents_ready.push(block); // Only notify for parent ready on first leader slots - if s % NUM_CONSECUTIVE_LEADER_SLOTS == 0 { + if s.is_multiple_of(NUM_CONSECUTIVE_LEADER_SLOTS.get() as Slot) { events.push(VotorEvent::ParentReady { slot: s, parent_block: block, @@ -243,8 +244,8 @@ impl ParentReadyTracker { #[cfg(test)] mod tests { use { - super::*, crate::tests::get_cluster_info, itertools::Itertools, - solana_clock::NUM_CONSECUTIVE_LEADER_SLOTS, solana_hash::Hash, solana_keypair::Keypair, + super::*, crate::tests::get_cluster_info, itertools::Itertools, solana_hash::Hash, + solana_keypair::Keypair, }; #[test] @@ -254,7 +255,7 @@ mod tests { let mut tracker = ParentReadyTracker::new(cluster_info, genesis); let mut events = vec![]; - for i in 1..2 * NUM_CONSECUTIVE_LEADER_SLOTS { + for i in 1..2 * NUM_CONSECUTIVE_LEADER_SLOTS.get() as Slot { let block = (i, Hash::new_unique()); tracker.add_new_notar_fallback_or_stronger(block, &mut events); assert_eq!(tracker.highest_parent_ready(), i + 1); diff --git a/votor/src/consensus_pool/slot_stake_counters.rs b/votor/src/consensus_pool/slot_stake_counters.rs index ab8ba56669e..874a7a462f6 100644 --- a/votor/src/consensus_pool/slot_stake_counters.rs +++ b/votor/src/consensus_pool/slot_stake_counters.rs @@ -8,8 +8,9 @@ use { event::VotorEvent, }, agave_votor_messages::{consensus_message::Block, fraction::Fraction, vote::Vote}, - solana_clock::NUM_CONSECUTIVE_LEADER_SLOTS, + solana_clock::Slot, solana_hash::Hash, + solana_leader_schedule::NUM_CONSECUTIVE_LEADER_SLOTS, std::{collections::BTreeMap, num::NonZeroU64}, }; @@ -72,7 +73,7 @@ impl SlotStakeCounters { } let slot = vote.slot(); let is_first_in_leader_window = - slot.is_multiple_of(NUM_CONSECUTIVE_LEADER_SLOTS) || slot == 1; + slot.is_multiple_of(NUM_CONSECUTIVE_LEADER_SLOTS.get() as Slot) || slot == 1; // Check safe to notar for (block_id, stake) in &self.notarize_entry_total { diff --git a/votor/src/timer_manager/timers.rs b/votor/src/timer_manager/timers.rs index 9306c1c15f0..66781575fe7 100644 --- a/votor/src/timer_manager/timers.rs +++ b/votor/src/timer_manager/timers.rs @@ -1,7 +1,8 @@ use { crate::{event::VotorEvent, timer_manager::stats::TimerManagerStats}, crossbeam_channel::Sender, - solana_clock::{NUM_CONSECUTIVE_LEADER_SLOTS, Slot}, + solana_clock::Slot, + solana_leader_schedule::NUM_CONSECUTIVE_LEADER_SLOTS, solana_runtime::leader_schedule_utils::last_of_consecutive_leader_slots, std::{ cmp::Reverse, @@ -22,7 +23,9 @@ fn calculate_timeout_multiplier(slot: Slot, standstill_slot: Option) -> f6 Some(standstill_slot) => { // Calculate number of leader windows since standstill let slots_since_standstill = slot.saturating_sub(standstill_slot); - let leader_windows = slots_since_standstill / NUM_CONSECUTIVE_LEADER_SLOTS; + let leader_windows = slots_since_standstill + .checked_div(NUM_CONSECUTIVE_LEADER_SLOTS.get() as Slot) + .expect("NUM_CONSECUTIVE_LEADER_SLOTS is non-zero"); // Extend timeout by 5% for each leader window 1.05_f64.powi(leader_windows as i32) } From 870af33bc06fcdea34886fd9da3e70032e3e95b7 Mon Sep 17 00:00:00 2001 From: Brooks Date: Fri, 22 May 2026 13:17:45 -0400 Subject: [PATCH 040/576] Calculates 'alive bytes' for shrink consistently (#12666) * Calculates 'alive bytes' for shrink consistently * pr: ZLSR * pr: unit test for alive_bytes_after_shrink() * pr: add permutations to select_candidates test --- accounts-db/src/account_storage_entry.rs | 2 +- accounts-db/src/accounts_db.rs | 78 ++++--- accounts-db/src/accounts_db/tests.rs | 282 ++++++++++++++++++++++- accounts-db/src/ancient_append_vecs.rs | 18 +- 4 files changed, 339 insertions(+), 41 deletions(-) diff --git a/accounts-db/src/account_storage_entry.rs b/accounts-db/src/account_storage_entry.rs index d317f696cd1..99fa2bbbd4b 100644 --- a/accounts-db/src/account_storage_entry.rs +++ b/accounts-db/src/account_storage_entry.rs @@ -31,7 +31,7 @@ pub struct AccountStorageEntry { pub(crate) num_alive_bytes: AtomicUsize, - /// offsets to accounts that are zero lamport single ref stored in this + /// offsets to accounts that are zero lamport single ref (ZLSR) stored in this /// storage. These are still alive. But, shrink will be able to remove them. /// /// NOTE: It's possible that one of these zero lamport single ref accounts diff --git a/accounts-db/src/accounts_db.rs b/accounts-db/src/accounts_db.rs index a1e3e9c1d54..89aa23a70b7 100644 --- a/accounts-db/src/accounts_db.rs +++ b/accounts-db/src/accounts_db.rs @@ -2385,6 +2385,8 @@ impl AccountsDb { stats: &ShrinkStats, slot_to_shrink: Slot, ) -> LoadAccountsIndexForShrink<'a, T> { + let can_purge_zero_lamport_single_ref = + self.can_purge_zero_lamport_single_ref_after_shrink(slot_to_shrink); let count = accounts.len(); let mut alive_accounts = T::with_capacity(count, slot_to_shrink); let mut pubkeys_to_unref = Vec::with_capacity(count); @@ -2396,7 +2398,6 @@ impl AccountsDb { let mut index_scan_returned_some_count = 0; let mut index_scan_returned_none_count = 0; let mut all_are_zero_lamports = true; - let latest_full_snapshot_slot = self.latest_full_snapshot_slot(); self.accounts_index.scan( accounts.iter().map(|account| account.pubkey()), |pubkey, slots_refs| { @@ -2404,11 +2405,7 @@ impl AccountsDb { let mut do_populate_accounts_for_shrink = |ref_count, slot_list| { if stored_account.is_zero_lamport() && ref_count == 1 - && latest_full_snapshot_slot - .map(|latest_full_snapshot_slot| { - latest_full_snapshot_slot >= slot_to_shrink - }) - .unwrap_or(true) + && can_purge_zero_lamport_single_ref { // only do this if our slot is prior to the latest full snapshot // we found a zero lamport account that is the only instance of this account. We can delete it completely. @@ -2794,7 +2791,7 @@ impl AccountsDb { self.shrink_stats .num_dead_slots_added_to_clean .fetch_add(1, Ordering::Relaxed); - } else if Self::is_shrinking_productive(&store) + } else if self.is_shrinking_productive(&store) && self.is_candidate_for_shrink(&store) { // this store might be eligible for shrinking now @@ -3032,7 +3029,7 @@ impl AccountsDb { .storage .get_slot_storage_entry_shrinking_in_progress_ok(slot) { - if Self::is_shrinking_productive(&store) { + if self.is_shrinking_productive(&store) { self.shrink_storage(store) } } @@ -3057,26 +3054,28 @@ impl AccountsDb { struct StoreUsageInfo { slot: Slot, alive_ratio: f64, + alive_bytes_after_shrink: u64, store: Arc, } - let mut store_usage: Vec = Vec::with_capacity(shrink_slots.len()); + let mut store_usages = Vec::with_capacity(shrink_slots.len()); let mut total_alive_bytes: u64 = 0; let mut total_bytes: u64 = 0; for slot in shrink_slots { let Some(store) = self.storage.get_slot_storage_entry(*slot) else { continue; }; - let alive_bytes = store.alive_bytes(); - total_alive_bytes += alive_bytes as u64; + let alive_bytes_after_shrink = self.alive_bytes_after_shrink(&store) as u64; + total_alive_bytes += alive_bytes_after_shrink; total_bytes += store.capacity(); - let alive_ratio = alive_bytes as f64 / store.capacity() as f64; - store_usage.push(StoreUsageInfo { + let alive_ratio = alive_bytes_after_shrink as f64 / store.capacity() as f64; + store_usages.push(StoreUsageInfo { slot: *slot, alive_ratio, + alive_bytes_after_shrink, store: store.clone(), }); } - store_usage.sort_by(|a, b| { + store_usages.sort_by(|a, b| { a.alive_ratio .partial_cmp(&b.alive_ratio) .unwrap_or(std::cmp::Ordering::Equal) @@ -3086,15 +3085,15 @@ impl AccountsDb { // shrinking while still achieving the overall goals. let mut shrink_slots = IntMap::default(); let mut shrink_slots_next_batch = ShrinkCandidates::default(); - for usage in &store_usage { - let store = &usage.store; + for store_usage in &store_usages { + let store = &store_usage.store; let alive_ratio = (total_alive_bytes as f64) / (total_bytes as f64); debug!( "alive_ratio: {:?} store_id: {:?}, store_ratio: {:?} requirement: {:?}, \ total_bytes: {:?} total_alive_bytes: {:?}", alive_ratio, - usage.store.id(), - usage.alive_ratio, + store_usage.store.id(), + store_usage.alive_ratio, shrink_ratio, total_bytes, total_alive_bytes @@ -3104,19 +3103,19 @@ impl AccountsDb { debug!( "Shrinking goal can be achieved at slot {:?}, total_alive_bytes: {:?} \ total_bytes: {:?}, alive_ratio: {:}, shrink_ratio: {:?}", - usage.slot, total_alive_bytes, total_bytes, alive_ratio, shrink_ratio + store_usage.slot, total_alive_bytes, total_bytes, alive_ratio, shrink_ratio ); - if usage.alive_ratio < shrink_ratio { - shrink_slots_next_batch.insert(usage.slot); + if store_usage.alive_ratio < shrink_ratio { + shrink_slots_next_batch.insert(store_usage.slot); } else { break; } } else { let current_store_size = store.capacity(); - let after_shrink_size = store.alive_bytes() as u64; + let after_shrink_size = store_usage.alive_bytes_after_shrink; let bytes_saved = current_store_size.saturating_sub(after_shrink_size); total_bytes -= bytes_saved; - shrink_slots.insert(usage.slot, Arc::clone(store)); + shrink_slots.insert(store_usage.slot, Arc::clone(store)); } } (shrink_slots, shrink_slots_next_batch) @@ -3245,9 +3244,10 @@ impl AccountsDb { if let Some(store) = self.storage.get_slot_storage_entry(slot) { if !shrink_slots.contains(&slot) && capacity == store.capacity() - && Self::is_candidate_for_shrink(self, &store) + && self.is_candidate_for_shrink(&store) { - let ancient_bytes_added_to_shrink = store.alive_bytes() as u64; + let ancient_bytes_added_to_shrink = + self.alive_bytes_after_shrink(&store) as u64; shrink_slots.insert(slot, store); self.shrink_stats .ancient_bytes_added_to_shrink @@ -5134,10 +5134,28 @@ impl AccountsDb { alive_bytes >= total_bytes } - fn is_shrinking_productive(store: &AccountStorageEntry) -> bool { + /// Can zero lamport single ref accounts in `slot` be purged? + fn can_purge_zero_lamport_single_ref_after_shrink(&self, slot: Slot) -> bool { + self.latest_full_snapshot_slot() + .is_none_or(|latest_full_snapshot_slot| slot <= latest_full_snapshot_slot) + } + + /// Returns the expected alive bytes after shrinking `store`. + pub(crate) fn alive_bytes_after_shrink(&self, store: &AccountStorageEntry) -> usize { + // Obsolete accounts are already excluded from `store.alive_bytes()`. + // Zero-lamport single-ref accounts are counted as alive until shrink can purge them, + // which is gated by the latest full snapshot slot. + if self.can_purge_zero_lamport_single_ref_after_shrink(store.slot()) { + store.alive_bytes_exclude_zero_lamport_single_ref_accounts() + } else { + store.alive_bytes() + } + } + + fn is_shrinking_productive(&self, store: &AccountStorageEntry) -> bool { let alive_count = store.count(); let total_bytes = store.capacity(); - let alive_bytes = store.alive_bytes_exclude_zero_lamport_single_ref_accounts() as u64; + let alive_bytes = self.alive_bytes_after_shrink(store) as u64; if Self::should_not_shrink(alive_bytes, total_bytes) { trace!( "shrink_slot_forced ({}): not able to shrink at all: num alive: {}, bytes alive: \ @@ -5161,7 +5179,7 @@ impl AccountsDb { // It is not possible to identify ancient append vecs when we pack, so no check for ancient when we are not appending. let total_bytes = store.capacity(); - let alive_bytes = store.alive_bytes_exclude_zero_lamport_single_ref_accounts() as u64; + let alive_bytes = self.alive_bytes_after_shrink(store) as u64; match self.shrink_ratio { AccountShrinkThreshold::TotalSpace { shrink_ratio: _ } => alive_bytes < total_bytes, AccountShrinkThreshold::IndividualStore { shrink_ratio } => { @@ -5257,7 +5275,7 @@ impl AccountsDb { if remaining_accounts == 0 { self.dirty_stores.insert(*slot, store); dead_slots.insert(*slot); - } else if Self::is_shrinking_productive(&store) + } else if self.is_shrinking_productive(&store) && self.is_candidate_for_shrink(&store) { // Checking that this single storage entry is ready for shrinking, @@ -5822,7 +5840,7 @@ impl AccountsDb { // If any zero lamport accounts were marked, the storage may be valid for shrinking if num_marked > 0 && self.is_candidate_for_shrink(storage) - && Self::is_shrinking_productive(storage) + && self.is_shrinking_productive(storage) { self.shrink_candidate_slots .lock() diff --git a/accounts-db/src/accounts_db/tests.rs b/accounts-db/src/accounts_db/tests.rs index a755f4861fa..7e15eacad67 100644 --- a/accounts-db/src/accounts_db/tests.rs +++ b/accounts-db/src/accounts_db/tests.rs @@ -107,6 +107,28 @@ impl AccountStorageEntry { } } +fn create_store_for_shrink_tests( + accounts_db: &AccountsDb, + slot: Slot, + file_size: u64, + alive_bytes: usize, + num_zero_lamport_single_ref_accounts: usize, +) -> Arc { + let store = Arc::new(AccountStorageEntry::new( + Path::new(""), + slot, + slot as AccountsFileId, + file_size, + AccountsFileProvider::AppendVec, + )); + accounts_db.storage.insert(Arc::clone(&store)); + store.add_accounts(num_zero_lamport_single_ref_accounts.max(1), alive_bytes); + for offset in 0..num_zero_lamport_single_ref_accounts { + store.insert_zero_lamport_single_ref_account_offset(offset); + } + store +} + /// Macro to define tests for all permutations of accounts-db configs /// /// E.g. account storage file format @@ -1145,6 +1167,165 @@ fn test_shrink_zero_lamport_single_ref_account() { } } +/// unit test for `alive_bytes_after_shrink()` +/// +/// Check all the permutations of latest full snapshot slot w.r.t. if/how +/// zero lamport single ref accounts are counted as alive bytes or not. +#[test] +fn test_alive_bytes_after_shrink() { + let accounts_db = AccountsDb::new_single_for_tests(); + let slot = 5; + // note the initial alive bytes should be big enough so that subtracting + // all the zero lamport single ref accounts does not saturate at zero. + let initial_alive_bytes = 123_456; + let store = create_store_for_shrink_tests( + &accounts_db, + slot, + 4096, // <-- file size + initial_alive_bytes, + 2, // <-- num zero lamport single ref accounts + ); + + // test case: latest full snapshot slot is None -- ZLSR accounts are dead + { + // latest full snapshot slot starts off as None + assert!(accounts_db.latest_full_snapshot_slot().is_none()); + + // ensure ZLSR accounts are dead bytes + let alive_bytes_after_shrink1 = accounts_db.alive_bytes_after_shrink(&store); + assert!(alive_bytes_after_shrink1 < initial_alive_bytes); + + // add a ZLSR account, and ensure alive bytes reduces + store.insert_zero_lamport_single_ref_account_offset(2); + let alive_bytes_after_shrink2 = accounts_db.alive_bytes_after_shrink(&store); + assert!(alive_bytes_after_shrink2 < alive_bytes_after_shrink1); + } + + // test case: slot > latest full snapshot -- ZLSR accounts are alive + { + accounts_db.set_latest_full_snapshot_slot(slot - 1); + + // ensure ZLSR accounts are *not* dead bytes + let alive_bytes_after_shrink1 = accounts_db.alive_bytes_after_shrink(&store); + assert_eq!(alive_bytes_after_shrink1, initial_alive_bytes); + + // add a ZLSR account, and ensure alive bytes is unchanged + store.insert_zero_lamport_single_ref_account_offset(3); + let alive_bytes_after_shrink2 = accounts_db.alive_bytes_after_shrink(&store); + assert_eq!(alive_bytes_after_shrink2, initial_alive_bytes); + } + + // test case: slot == latest full snapshot -- ZLSR accounts are dead + { + accounts_db.set_latest_full_snapshot_slot(slot); + + // ensure ZLSR accounts are dead bytes + let alive_bytes_after_shrink1 = accounts_db.alive_bytes_after_shrink(&store); + assert!(alive_bytes_after_shrink1 < initial_alive_bytes); + + // add a ZLSR account, and ensure alive bytes reduces + store.insert_zero_lamport_single_ref_account_offset(4); + let alive_bytes_after_shrink2 = accounts_db.alive_bytes_after_shrink(&store); + assert!(alive_bytes_after_shrink2 < alive_bytes_after_shrink1); + } + + // test case: slot < latest full snapshot -- ZLSR accounts are dead + { + accounts_db.set_latest_full_snapshot_slot(slot + 1); + + // ensure ZLSR accounts are dead bytes + let alive_bytes_after_shrink1 = accounts_db.alive_bytes_after_shrink(&store); + assert!(alive_bytes_after_shrink1 < initial_alive_bytes); + + // add a ZLSR account, and ensure alive bytes reduces + store.insert_zero_lamport_single_ref_account_offset(5); + let alive_bytes_after_shrink2 = accounts_db.alive_bytes_after_shrink(&store); + assert!(alive_bytes_after_shrink2 < alive_bytes_after_shrink1); + } +} + +/// Ensure that shrinking a storage... +/// * with zero lamport single ref accounts +/// * in a slot *older* than the latest full snapshot slot +/// +/// Results in... +/// * the zero lamport single ref accounts *not* in the shrunk storage +/// * the expected number of alive bytes +#[test] +fn test_alive_bytes_after_shrink_with_zero_lamport_single_ref_accounts() { + let accounts_db = AccountsDb::new_single_for_tests(); + let slot = 1; + let dead_account = AccountSharedData::new(0, 123, &Pubkey::default()); + let dead_pubkeys = [ + Pubkey::new_unique(), + Pubkey::new_unique(), + Pubkey::new_unique(), + ]; + let alive_account = AccountSharedData::new(11, 17, &Pubkey::default()); + let alive_pubkey = Pubkey::new_unique(); + + accounts_db.store_for_tests(( + slot, + [ + (&dead_pubkeys[0], &dead_account), + (&dead_pubkeys[1], &dead_account), + (&dead_pubkeys[2], &dead_account), + (&alive_pubkey, &alive_account), + ] + .as_slice(), + )); + accounts_db.add_root_and_flush_write_cache(slot); + + // We must set the latest full snapshot slot to `slot` or greater + // to ensure that ZLSR accounts are treated as dead for `shrink`. + accounts_db.set_latest_full_snapshot_slot(slot); + + let storage = accounts_db.get_storage_for_slot(slot).unwrap(); + accounts_db.accounts_index.scan( + dead_pubkeys.iter(), + |_pubkey, slots_refs| { + let (slot_list, ref_count) = slots_refs.unwrap(); + assert_eq!(slot_list.len(), 1); + assert_eq!(ref_count, 1); + + let (indexed_slot, acct_info) = slot_list.first().unwrap(); + assert_eq!(*indexed_slot, slot); + assert!(acct_info.is_zero_lamport()); + accounts_db.zero_lamport_single_ref_found(*indexed_slot, acct_info.offset()); + AccountsIndexScanResult::KeepInMemory + }, + None, + ScanFilter::All, + ); + + assert_eq!( + storage.num_zero_lamport_single_ref_accounts(), + dead_pubkeys.len(), + ); + + let alive_bytes_before_shrink = storage.alive_bytes(); + let expected_alive_bytes_after_shrink = accounts_db.alive_bytes_after_shrink(&storage); + assert_ne!(expected_alive_bytes_after_shrink, 0); + assert!(expected_alive_bytes_after_shrink < alive_bytes_before_shrink); + assert_eq!( + expected_alive_bytes_after_shrink, + AppendVec::calculate_stored_size(alive_account.data().len()), + ); + + accounts_db.shrink_slot_forced(slot); + + let storage_after_shrink = accounts_db.get_storage_for_slot(slot).unwrap(); + assert_eq!( + storage_after_shrink.alive_bytes(), + expected_alive_bytes_after_shrink, + ); + assert_eq!(storage_after_shrink.count(), 1); + assert!(accounts_db.contains(&alive_pubkey)); + for pubkey in &dead_pubkeys { + assert!(!accounts_db.contains(pubkey)); + } +} + #[test] fn test_clean_multiple_zero_lamport_decrements_index_ref_count() { agave_logger::setup(); @@ -2336,6 +2517,100 @@ fn test_select_candidates_by_total_usage_all_clean() { assert_eq!(0, next_candidates.len()); } +/// Ensure selecting shrink candidates respects zero-lamport single-ref accounts. +#[test] +fn test_select_candidates_by_total_usage_with_zero_lamport_single_ref_accounts() { + let accounts_db = AccountsDb::new_single_for_tests(); + let mut shrink_candidates = ShrinkCandidates::default(); + + let num_zero_lamport_single_ref_accounts = 4; + let file_size = AppendVec::calculate_stored_size(0) * num_zero_lamport_single_ref_accounts; + + let slot_with_zlsr = 11; + let store_with_zlsr = create_store_for_shrink_tests( + &accounts_db, + slot_with_zlsr, + file_size as u64, + file_size, + num_zero_lamport_single_ref_accounts, + ); + shrink_candidates.insert(slot_with_zlsr); + + let slot_no_zlsr = 22; + let store_no_zlsr = create_store_for_shrink_tests( + &accounts_db, + slot_no_zlsr, + file_size as u64, + file_size * 9 / 10, + 0, + ); + shrink_candidates.insert(slot_no_zlsr); + + // test case: The latest full snapshot slot is *older* than + // the store with zero lamport single ref accounts. + // Ensure shrink will see ZLSR accounts as *alive*. + { + accounts_db.set_latest_full_snapshot_slot(slot_with_zlsr - 1); + + // Bytes from ZLSR accounts are alive, and will stay alive after shrink. + assert_eq!( + accounts_db.alive_bytes_after_shrink(&store_with_zlsr), + store_with_zlsr.alive_bytes(), + ); + assert!(!accounts_db.is_candidate_for_shrink(&store_with_zlsr)); + assert!(!accounts_db.is_shrinking_productive(&store_with_zlsr)); + + // Stores without ZLSR accounts use the raw alive bytes. + assert_eq!( + accounts_db.alive_bytes_after_shrink(&store_no_zlsr), + store_no_zlsr.alive_bytes(), + ); + + let (selected_candidates, next_candidates) = accounts_db + .select_candidates_by_total_usage(&shrink_candidates, DEFAULT_ACCOUNTS_SHRINK_RATIO); + + // both slots are above the shrink ratio, so neither should be shrink candidates + assert!(selected_candidates.is_empty()); + assert!(next_candidates.is_empty()); + } + + // test case: The latest full snapshot slot is either: + // * newer than the store with ZLSR accounts + // * the same as the store with ZLSR accounts + // * unset + // Ensure shrink will see ZLSR accounts as *dead*. + { + for latest_full_snapshot_slot in [Some(slot_with_zlsr + 1), Some(slot_with_zlsr), None] { + *accounts_db.latest_full_snapshot_slot.lock_write() = latest_full_snapshot_slot; + + // Bytes from ZLSR accounts are alive, but would be dead after shrink. + assert_eq!(store_with_zlsr.alive_bytes(), file_size); + assert_eq!(accounts_db.alive_bytes_after_shrink(&store_with_zlsr), 0); + assert!(accounts_db.is_candidate_for_shrink(&store_with_zlsr)); + assert!(accounts_db.is_shrinking_productive(&store_with_zlsr)); + + // Stores without ZLSR accounts use the raw alive bytes. + assert_eq!( + accounts_db.alive_bytes_after_shrink(&store_no_zlsr), + store_no_zlsr.alive_bytes(), + ); + + let (selected_candidates, next_candidates) = accounts_db + .select_candidates_by_total_usage( + &shrink_candidates, + DEFAULT_ACCOUNTS_SHRINK_RATIO, + ); + + // slot 11 with the ZLSR accounts *is* selected for shrink + assert_eq!(1, selected_candidates.len()); + assert!(selected_candidates.contains_key(&slot_with_zlsr)); + + // slot 22 is above the shrink ratio, so ensure it is not a candidate + assert!(next_candidates.is_empty()); + } + } +} + #[test] fn test_delete_dependencies() { agave_logger::setup(); @@ -4246,6 +4521,7 @@ fn test_remove_uncleaned_slots_and_collect_pubkeys_up_to_slot() { #[test] fn test_shrink_productive() { agave_logger::setup(); + let accounts = AccountsDb::new_single_for_tests(); let path = Path::new(""); let file_size = 100; let slot = 11; @@ -4258,7 +4534,7 @@ fn test_shrink_productive() { AccountsFileProvider::AppendVec, )); store.add_account(file_size as usize); - assert!(!AccountsDb::is_shrinking_productive(&store)); + assert!(!accounts.is_shrinking_productive(&store)); let store = Arc::new(AccountStorageEntry::new( path, @@ -4270,10 +4546,10 @@ fn test_shrink_productive() { store.add_account(file_size as usize / 2); store.add_account(file_size as usize / 4); store.remove_accounts(file_size as usize / 4, 1); - assert!(AccountsDb::is_shrinking_productive(&store)); + assert!(accounts.is_shrinking_productive(&store)); store.add_account(file_size as usize / 2); - assert!(!AccountsDb::is_shrinking_productive(&store)); + assert!(!accounts.is_shrinking_productive(&store)); } #[test] diff --git a/accounts-db/src/ancient_append_vecs.rs b/accounts-db/src/ancient_append_vecs.rs index 860a89e5020..1eda08e229d 100644 --- a/accounts-db/src/ancient_append_vecs.rs +++ b/accounts-db/src/ancient_append_vecs.rs @@ -61,7 +61,7 @@ struct SlotInfo { slot: Slot, /// total capacity of storage capacity: u64, - /// # alive bytes in storage + /// # alive bytes in storage *after* shrinking alive_bytes: u64, /// true if this should be shrunk due to ratio should_shrink: bool, @@ -95,14 +95,14 @@ impl AncientSlotInfos { &mut self, slot: Slot, storage: Arc, + alive_bytes_after_shrink: u64, can_randomly_shrink: bool, ideal_size: NonZeroU64, is_high_slot: bool, is_candidate_for_shrink: bool, ) -> bool { let mut was_randomly_shrunk = false; - let alive_bytes = storage.alive_bytes() as u64; - if alive_bytes > 0 { + if alive_bytes_after_shrink > 0 { let capacity = storage.accounts.capacity(); let should_shrink = if capacity > 0 { if is_candidate_for_shrink { @@ -123,11 +123,11 @@ impl AncientSlotInfos { if should_shrink { // alive ratio is too low, so prioritize combining this slot with others // to reduce disk space used - self.total_alive_bytes_shrink += alive_bytes; + self.total_alive_bytes_shrink += alive_bytes_after_shrink; self.shrink_indexes.push(self.all_infos.len()); } else { let already_ideal_size = u64::from(ideal_size) * 80 / 100; - if alive_bytes > already_ideal_size { + if alive_bytes_after_shrink > already_ideal_size { // do not include this append vec at all. It is already ideal size and not a candidate for shrink. return was_randomly_shrunk; } @@ -136,11 +136,11 @@ impl AncientSlotInfos { slot, capacity, storage, - alive_bytes, + alive_bytes: alive_bytes_after_shrink, should_shrink, is_high_slot, }); - self.total_alive_bytes += alive_bytes; + self.total_alive_bytes += alive_bytes_after_shrink; } was_randomly_shrunk } @@ -602,9 +602,11 @@ impl AccountsDb { for slot in &slots { if let Some(storage) = self.storage.get_slot_storage_entry(*slot) { let is_candidate_for_shrink = self.is_candidate_for_shrink(&storage); + let alive_bytes_after_shrink = self.alive_bytes_after_shrink(&storage) as u64; if infos.add( *slot, storage, + alive_bytes_after_shrink, tuning.can_randomly_shrink, tuning.ideal_storage_size, is_high_slot(*slot), @@ -2432,6 +2434,7 @@ mod tests { infos.add( slot1, Arc::clone(&storage), + db.alive_bytes_after_shrink(&storage) as u64, can_randomly_shrink, NonZeroU64::new(get_ancient_append_vec_capacity()).unwrap(), high_slot, @@ -2487,6 +2490,7 @@ mod tests { infos.add( slot1, Arc::clone(&storage), + db.alive_bytes_after_shrink(&storage) as u64, can_randomly_shrink, NonZeroU64::new(get_ancient_append_vec_capacity()).unwrap(), high_slot, From 9e393e71f5ce41ebe5cc55da64557be43ddad91c Mon Sep 17 00:00:00 2001 From: Rory Harris Date: Fri, 22 May 2026 11:36:35 -0700 Subject: [PATCH 041/576] Pull secondary index update out of accounts_index.upsert (#12670) --- accounts-db/benches/accounts_index.rs | 8 +- accounts-db/src/accounts_db.rs | 14 +- accounts-db/src/accounts_db/tests.rs | 24 -- accounts-db/src/accounts_index.rs | 333 +++++-------------------- accounts-db/src/accounts_index/iter.rs | 12 +- accounts-db/src/ancient_append_vecs.rs | 3 - 6 files changed, 76 insertions(+), 318 deletions(-) diff --git a/accounts-db/benches/accounts_index.rs b/accounts-db/benches/accounts_index.rs index 14a15fa8199..ce628872946 100644 --- a/accounts-db/benches/accounts_index.rs +++ b/accounts-db/benches/accounts_index.rs @@ -4,12 +4,10 @@ extern crate test; use { rand::{Rng, rng}, - solana_account::AccountSharedData, solana_accounts_db::{ account_info::AccountInfo, accounts_index::{ - ACCOUNTS_INDEX_CONFIG_FOR_BENCHMARKS, AccountSecondaryIndexes, AccountsIndex, - ReclaimsSlotList, UpsertReclaim, + ACCOUNTS_INDEX_CONFIG_FOR_BENCHMARKS, AccountsIndex, ReclaimsSlotList, UpsertReclaim, }, }, std::sync::Arc, @@ -40,8 +38,6 @@ fn bench_accounts_index(bencher: &mut Bencher) { f, f, pubkey, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), AccountInfo::default(), &mut reclaims, UpsertReclaim::PopulateReclaims, @@ -58,8 +54,6 @@ fn bench_accounts_index(bencher: &mut Bencher) { fork, fork, &pubkeys[pubkey], - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), AccountInfo::default(), &mut reclaims, UpsertReclaim::PopulateReclaims, diff --git a/accounts-db/src/accounts_db.rs b/accounts-db/src/accounts_db.rs index 89aa23a70b7..119eb49b4be 100644 --- a/accounts-db/src/accounts_db.rs +++ b/accounts-db/src/accounts_db.rs @@ -5023,12 +5023,15 @@ impl AccountsDb { target_slot, target_slot, account.pubkey(), - &account, - &self.account_indexes, info, ReclaimsSlotList::default().as_mut(), UpsertReclaim::PreviousSlotEntryWasCached, ); + self.accounts_index.update_secondary_indexes( + account.pubkey(), + &account, + &self.account_indexes, + ); }); } }); @@ -5089,12 +5092,15 @@ impl AccountsDb { target_slot, old_slot, account.pubkey(), - &account, - &self.account_indexes, info, &mut reclaims, reclaim, ); + self.accounts_index.update_secondary_indexes( + account.pubkey(), + &account, + &self.account_indexes, + ); }); }); reclaims diff --git a/accounts-db/src/accounts_db/tests.rs b/accounts-db/src/accounts_db/tests.rs index 7e15eacad67..b4275cffbd0 100644 --- a/accounts-db/src/accounts_db/tests.rs +++ b/accounts-db/src/accounts_db/tests.rs @@ -280,8 +280,6 @@ pub(crate) fn append_single_account_with_default_hash( slot, slot, pubkey, - account, - &AccountSecondaryIndexes::default(), account_info, &mut ReclaimsSlotList::new(), UpsertReclaim::IgnoreReclaims, @@ -2628,8 +2626,6 @@ fn test_delete_dependencies() { 0, 0, &key0, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), info0, &mut reclaims, UpsertReclaim::IgnoreReclaims, @@ -2638,8 +2634,6 @@ fn test_delete_dependencies() { 1, 1, &key0, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), info1, &mut reclaims, UpsertReclaim::IgnoreReclaims, @@ -2648,8 +2642,6 @@ fn test_delete_dependencies() { 1, 1, &key1, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), info1, &mut reclaims, UpsertReclaim::IgnoreReclaims, @@ -2658,8 +2650,6 @@ fn test_delete_dependencies() { 2, 2, &key1, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), info2, &mut reclaims, UpsertReclaim::IgnoreReclaims, @@ -2668,8 +2658,6 @@ fn test_delete_dependencies() { 2, 2, &key2, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), info2, &mut reclaims, UpsertReclaim::IgnoreReclaims, @@ -2678,8 +2666,6 @@ fn test_delete_dependencies() { 3, 3, &key2, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), info3, &mut reclaims, UpsertReclaim::IgnoreReclaims, @@ -5077,8 +5063,6 @@ fn test_unref_pubkeys_removed_from_accounts_index() { slot1, slot1, &pk1, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), AccountInfo::default(), &mut reclaims, UpsertReclaim::IgnoreReclaims, @@ -5129,8 +5113,6 @@ fn test_unref_accounts() { slot1, slot1, &pk1, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), AccountInfo::default(), &mut reclaims, UpsertReclaim::IgnoreReclaims, @@ -5160,8 +5142,6 @@ fn test_unref_accounts() { slot, slot, &pk, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), AccountInfo::default(), &mut reclaims, UpsertReclaim::IgnoreReclaims, @@ -5201,8 +5181,6 @@ define_accounts_db_test!(test_many_unrefs, |db| { slot, slot, &pk1, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), AccountInfo::default(), &mut reclaims, UpsertReclaim::IgnoreReclaims, @@ -5914,8 +5892,6 @@ fn populate_index(db: &AccountsDb, slots: Range) { slot, slot, account.pubkey(), - &account, - &AccountSecondaryIndexes::default(), info, &mut ReclaimsSlotList::new(), UpsertReclaim::IgnoreReclaims, diff --git a/accounts-db/src/accounts_index.rs b/accounts-db/src/accounts_index.rs index 0375f04d188..29006387a09 100644 --- a/accounts-db/src/accounts_index.rs +++ b/accounts-db/src/accounts_index.rs @@ -920,33 +920,25 @@ impl + Into> AccountsIndex { .for_each(f); } - /// Updates the given pubkey at the given slot with the new account information. - /// on return, the index's previous account info may be returned in 'reclaims' depending on 'previous_slot_entry_was_cached' + /// Updates the primary index for `pubkey` at `new_slot` with `account_info`. + /// + /// Does NOT update the secondary indexes — callers that need that must update separately. + /// The primary and secondary indexes are not updated atomically, and a brief inconsistency is + /// acceptable: the secondary index is only consulted for `scan`, which is only supported on + /// frozen banks, and is never used as a source of truth for gets/stores. + /// + /// On return, the previous account info may be returned in `reclaims` depending on `reclaim`. pub fn upsert( &self, new_slot: Slot, old_slot: Slot, pubkey: &Pubkey, - account: &impl ReadableAccount, - account_indexes: &AccountSecondaryIndexes, account_info: T, reclaims: &mut ReclaimsSlotList, reclaim: UpsertReclaim, ) { // vast majority of updates are to item already in accounts index, so store as raw to avoid unnecessary allocations let store_raw = true; - - // We don't atomically update both primary index and secondary index together. - // This certainly creates a small time window with inconsistent state across the two indexes. - // However, this is acceptable because: - // - // - A strict consistent view at any given moment of time is not necessary, because the only - // use case for the secondary index is `scan`, and `scans` are only supported/require consistency - // on frozen banks, and this inconsistency is only possible on working banks. - // - // - The secondary index is never consulted as primary source of truth for gets/stores. - // So, what the accounts_index sees alone is sufficient as a source of truth for other non-scan - // account operations. let new_item = PreAllocatedAccountMapEntry::new( new_slot, account_info, @@ -954,9 +946,7 @@ impl + Into> AccountsIndex { store_raw, ); let map = self.get_bin(pubkey); - map.upsert(pubkey, new_item, Some(old_slot), reclaims, reclaim); - self.update_secondary_indexes(pubkey, account, account_indexes); } pub fn ref_count_from_storage(&self, pubkey: &Pubkey) -> RefCount { @@ -1351,16 +1341,7 @@ mod tests { let key = solana_pubkey::new_rand(); let index = AccountsIndex::::default_for_tests(); let mut gc = ReclaimsSlotList::new(); - index.upsert( - 0, - 0, - &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - true, - &mut gc, - UPSERT_RECLAIM_TEST_DEFAULT, - ); + index.upsert(0, 0, &key, true, &mut gc, UPSERT_RECLAIM_TEST_DEFAULT); assert!(gc.is_empty()); let ancestors = Ancestors::default(); @@ -1520,8 +1501,6 @@ mod tests { slot, slot, &pubkey, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), account_info, &mut gc, UPSERT_RECLAIM_TEST_DEFAULT, @@ -1549,8 +1528,6 @@ mod tests { slot, slot, &pubkey, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), account_info, &mut gc, UpsertReclaim::IgnoreReclaims, @@ -1591,8 +1568,6 @@ mod tests { slot, slot, pubkey, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), true, &mut gc, UpsertReclaim::IgnoreReclaims, @@ -1713,16 +1688,7 @@ mod tests { match upsert_method { Some(upsert_method) => { // insert first entry for pubkey. This will use new_entry_after_update and not call update. - index.upsert( - slot0, - slot0, - &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - account_infos[0], - &mut gc, - upsert_method, - ); + index.upsert(slot0, slot0, &key, account_infos[0], &mut gc, upsert_method); } None => { let mut items = vec![(key, account_infos[0])]; @@ -1755,16 +1721,7 @@ mod tests { match upsert_method { Some(upsert_method) => { // insert second entry for pubkey. This will use update and NOT use new_entry_after_update. - index.upsert( - slot1, - slot1, - &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - account_infos[1], - &mut gc, - upsert_method, - ); + index.upsert(slot1, slot1, &key, account_infos[1], &mut gc, upsert_method); } None => { // this has the effect of aging out everything in the in-mem cache @@ -1897,16 +1854,7 @@ mod tests { let key = solana_pubkey::new_rand(); let index = AccountsIndex::::default_for_tests(); let mut gc = ReclaimsSlotList::new(); - index.upsert( - 0, - 0, - &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - true, - &mut gc, - UPSERT_RECLAIM_TEST_DEFAULT, - ); + index.upsert(0, 0, &key, true, &mut gc, UPSERT_RECLAIM_TEST_DEFAULT); assert!(gc.is_empty()); let ancestors = Ancestors::from(vec![1]); @@ -1935,8 +1883,6 @@ mod tests { slot, slot, &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), value, &mut reclaims, UpsertReclaim::PopulateReclaims, @@ -1946,8 +1892,6 @@ mod tests { slot, slot, &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), value, &mut reclaims, UpsertReclaim::PopulateReclaims, @@ -1959,8 +1903,6 @@ mod tests { slot, slot, &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), value, &mut reclaims, // since IgnoreReclaims, we should expect reclaims to be empty @@ -1981,8 +1923,6 @@ mod tests { slot, slot, &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), value, &mut reclaims, UpsertReclaim::PopulateReclaims, @@ -1992,8 +1932,6 @@ mod tests { slot, slot, &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), value, &mut reclaims, UpsertReclaim::PopulateReclaims, @@ -2004,8 +1942,6 @@ mod tests { slot, slot, &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), value, &mut reclaims, // since IgnoreReclaims, we should expect reclaims to be empty @@ -2021,16 +1957,7 @@ mod tests { let key = solana_pubkey::new_rand(); let index = AccountsIndex::::default_for_tests(); let mut gc = ReclaimsSlotList::new(); - index.upsert( - 0, - 0, - &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - true, - &mut gc, - UPSERT_RECLAIM_TEST_DEFAULT, - ); + index.upsert(0, 0, &key, true, &mut gc, UPSERT_RECLAIM_TEST_DEFAULT); assert!(gc.is_empty()); let ancestors = Ancestors::from(vec![0]); @@ -2069,8 +1996,6 @@ mod tests { root_slot, root_slot, &new_pubkey, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), true, &mut ReclaimsSlotList::new(), UPSERT_RECLAIM_TEST_DEFAULT, @@ -2086,8 +2011,6 @@ mod tests { root_slot, root_slot, &Pubkey::default(), - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), true, &mut ReclaimsSlotList::new(), UPSERT_RECLAIM_TEST_DEFAULT, @@ -2136,16 +2059,7 @@ mod tests { let key = solana_pubkey::new_rand(); let index = AccountsIndex::::default_for_tests(); let mut gc = ReclaimsSlotList::new(); - index.upsert( - 0, - 0, - &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - true, - &mut gc, - UPSERT_RECLAIM_TEST_DEFAULT, - ); + index.upsert(0, 0, &key, true, &mut gc, UPSERT_RECLAIM_TEST_DEFAULT); assert!(gc.is_empty()); index.add_root(0); @@ -2185,16 +2099,7 @@ mod tests { let index = AccountsIndex::::default_for_tests(); let ancestors = Ancestors::from(vec![0]); let mut gc = ReclaimsSlotList::new(); - index.upsert( - 0, - 0, - &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - 1, - &mut gc, - UPSERT_RECLAIM_TEST_DEFAULT, - ); + index.upsert(0, 0, &key, 1, &mut gc, UPSERT_RECLAIM_TEST_DEFAULT); assert!(gc.is_empty()); index .get_with_and_then(&key, &ancestors, false, |(slot, account_info)| { @@ -2204,16 +2109,7 @@ mod tests { .unwrap(); let mut gc = ReclaimsSlotList::new(); - index.upsert( - 0, - 0, - &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - 0, - &mut gc, - UPSERT_RECLAIM_TEST_DEFAULT, - ); + index.upsert(0, 0, &key, 0, &mut gc, UPSERT_RECLAIM_TEST_DEFAULT); assert_eq!(gc, ReclaimsSlotList::from([(0, 1)])); index .get_with_and_then(&key, &ancestors, false, |(slot, account_info)| { @@ -2230,27 +2126,9 @@ mod tests { let index = AccountsIndex::::default_for_tests(); let ancestors = Ancestors::from(vec![0]); let mut gc = ReclaimsSlotList::new(); - index.upsert( - 0, - 0, - &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - true, - &mut gc, - UpsertReclaim::PopulateReclaims, - ); + index.upsert(0, 0, &key, true, &mut gc, UpsertReclaim::PopulateReclaims); assert!(gc.is_empty()); - index.upsert( - 1, - 1, - &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - false, - &mut gc, - UpsertReclaim::PopulateReclaims, - ); + index.upsert(1, 1, &key, false, &mut gc, UpsertReclaim::PopulateReclaims); assert!(gc.is_empty()); index .get_with_and_then(&key, &ancestors, false, |(slot, account_info)| { @@ -2272,60 +2150,15 @@ mod tests { let key = solana_pubkey::new_rand(); let index = AccountsIndex::::default_for_tests(); let mut gc = ReclaimsSlotList::new(); - index.upsert( - 0, - 0, - &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - true, - &mut gc, - UpsertReclaim::PopulateReclaims, - ); + index.upsert(0, 0, &key, true, &mut gc, UpsertReclaim::PopulateReclaims); assert!(gc.is_empty()); - index.upsert( - 1, - 1, - &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - false, - &mut gc, - UpsertReclaim::PopulateReclaims, - ); - index.upsert( - 2, - 2, - &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - true, - &mut gc, - UpsertReclaim::PopulateReclaims, - ); - index.upsert( - 3, - 3, - &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - true, - &mut gc, - UpsertReclaim::PopulateReclaims, - ); + index.upsert(1, 1, &key, false, &mut gc, UpsertReclaim::PopulateReclaims); + index.upsert(2, 2, &key, true, &mut gc, UpsertReclaim::PopulateReclaims); + index.upsert(3, 3, &key, true, &mut gc, UpsertReclaim::PopulateReclaims); index.add_root(0); index.add_root(1); index.add_root(3); - index.upsert( - 4, - 4, - &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - true, - &mut gc, - UpsertReclaim::PopulateReclaims, - ); + index.upsert(4, 4, &key, true, &mut gc, UpsertReclaim::PopulateReclaims); // Updating index should not purge older roots, only purges // previous updates within the same slot @@ -2366,8 +2199,6 @@ mod tests { 0, 0, &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), CacheableIndexValueTest(true), &mut reclaims, UPSERT_RECLAIM_TEST_DEFAULT, @@ -2379,8 +2210,6 @@ mod tests { 0, 0, &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), CacheableIndexValueTest(false), &mut reclaims, UPSERT_RECLAIM_TEST_DEFAULT, @@ -2398,8 +2227,6 @@ mod tests { 0, 0, &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), CacheableIndexValueTest(false), &mut reclaims, UPSERT_RECLAIM_TEST_DEFAULT, @@ -2425,28 +2252,10 @@ mod tests { let index = AccountsIndex::::default_for_tests(); let mut gc = ReclaimsSlotList::new(); assert_eq!(0, account_maps_stats_len(&index)); - index.upsert( - 1, - 1, - &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - 12, - &mut gc, - UPSERT_RECLAIM_TEST_DEFAULT, - ); + index.upsert(1, 1, &key, 12, &mut gc, UPSERT_RECLAIM_TEST_DEFAULT); assert_eq!(1, account_maps_stats_len(&index)); - index.upsert( - 1, - 1, - &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - 10, - &mut gc, - UPSERT_RECLAIM_TEST_DEFAULT, - ); + index.upsert(1, 1, &key, 10, &mut gc, UPSERT_RECLAIM_TEST_DEFAULT); assert_eq!(1, account_maps_stats_len(&index)); let purges = index.purge_roots(&key); @@ -2457,16 +2266,7 @@ mod tests { assert_eq!(purges, (SlotList::from([(1, 10)]), true)); assert_eq!(1, account_maps_stats_len(&index)); - index.upsert( - 1, - 1, - &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - 9, - &mut gc, - UPSERT_RECLAIM_TEST_DEFAULT, - ); + index.upsert(1, 1, &key, 9, &mut gc, UPSERT_RECLAIM_TEST_DEFAULT); assert_eq!(1, account_maps_stats_len(&index)); } @@ -2552,7 +2352,13 @@ mod tests { *slot, *slot, &account_key, - // Make sure these accounts are added to secondary index + true, + &mut ReclaimsSlotList::new(), + UPSERT_RECLAIM_TEST_DEFAULT, + ); + // Make sure these accounts are added to secondary index + index.update_secondary_indexes( + &account_key, &AccountSharedData::create_from_existing_shared_data( 0, Arc::new(account_data.to_vec()), @@ -2561,9 +2367,6 @@ mod tests { 0, ), secondary_indexes, - true, - &mut ReclaimsSlotList::new(), - UPSERT_RECLAIM_TEST_DEFAULT, ); } @@ -2610,8 +2413,6 @@ mod tests { slot, slot, &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), slot, &mut gc, UpsertReclaim::IgnoreReclaims, @@ -2627,8 +2428,6 @@ mod tests { reclaim_slot + 1, reclaim_slot + 1, &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), account_value + 1, &mut gc, UpsertReclaim::IgnoreReclaims, @@ -2643,8 +2442,6 @@ mod tests { reclaim_slot, reclaim_slot, &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), account_value, &mut gc, UpsertReclaim::ReclaimOldSlots, @@ -2689,8 +2486,6 @@ mod tests { 0, 0, &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), CacheableIndexValueTest(false), &mut gc, UpsertReclaim::IgnoreReclaims, @@ -2700,8 +2495,6 @@ mod tests { 1, 1, &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), CacheableIndexValueTest(true), &mut gc, UpsertReclaim::IgnoreReclaims, @@ -2712,8 +2505,6 @@ mod tests { 2, 2, &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), CacheableIndexValueTest(true), &mut gc, UpsertReclaim::IgnoreReclaims, @@ -2724,8 +2515,6 @@ mod tests { 2, 2, &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), CacheableIndexValueTest(false), &mut gc, UpsertReclaim::ReclaimOldSlots, @@ -2929,6 +2718,12 @@ mod tests { index.upsert( 0, 0, + &account_key, + true, + &mut ReclaimsSlotList::new(), + UPSERT_RECLAIM_TEST_DEFAULT, + ); + index.update_secondary_indexes( &account_key, &AccountSharedData::create_from_existing_shared_data( 0, @@ -2938,9 +2733,6 @@ mod tests { 0, ), &secondary_indexes, - true, - &mut ReclaimsSlotList::new(), - UPSERT_RECLAIM_TEST_DEFAULT, ); assert!(secondary_index.index.is_empty()); assert!(secondary_index.reverse_index.is_empty()); @@ -2949,6 +2741,12 @@ mod tests { index.upsert( 0, 0, + &account_key, + true, + &mut ReclaimsSlotList::new(), + UPSERT_RECLAIM_TEST_DEFAULT, + ); + index.update_secondary_indexes( &account_key, &AccountSharedData::create_from_existing_shared_data( 0, @@ -2958,9 +2756,6 @@ mod tests { 0, ), &secondary_indexes, - true, - &mut ReclaimsSlotList::new(), - UPSERT_RECLAIM_TEST_DEFAULT, ); assert!(secondary_index.index.is_empty()); assert!(secondary_index.reverse_index.is_empty()); @@ -3098,6 +2893,12 @@ mod tests { index.upsert( slot, slot, + &account_key, + true, + &mut ReclaimsSlotList::new(), + UPSERT_RECLAIM_TEST_DEFAULT, + ); + index.update_secondary_indexes( &account_key, &AccountSharedData::create_from_existing_shared_data( 0, @@ -3107,15 +2908,18 @@ mod tests { 0, ), secondary_indexes, - true, - &mut ReclaimsSlotList::new(), - UPSERT_RECLAIM_TEST_DEFAULT, ); // Now write a different mint index for the same account index.upsert( slot, slot, + &account_key, + true, + &mut ReclaimsSlotList::new(), + UPSERT_RECLAIM_TEST_DEFAULT, + ); + index.update_secondary_indexes( &account_key, &AccountSharedData::create_from_existing_shared_data( 0, @@ -3125,9 +2929,6 @@ mod tests { 0, ), secondary_indexes, - true, - &mut ReclaimsSlotList::new(), - UPSERT_RECLAIM_TEST_DEFAULT, ); // Both pubkeys will now be present in the index @@ -3142,6 +2943,12 @@ mod tests { index.upsert( later_slot, later_slot, + &account_key, + true, + &mut ReclaimsSlotList::new(), + UPSERT_RECLAIM_TEST_DEFAULT, + ); + index.update_secondary_indexes( &account_key, &AccountSharedData::create_from_existing_shared_data( 0, @@ -3151,9 +2958,6 @@ mod tests { 0, ), secondary_indexes, - true, - &mut ReclaimsSlotList::new(), - UPSERT_RECLAIM_TEST_DEFAULT, ); assert_eq!(secondary_index.get(&secondary_key1), vec![account_key]); @@ -3339,16 +3143,7 @@ mod tests { UpsertReclaim::IgnoreReclaims }; - self.upsert( - slot, - slot, - key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - value, - &mut gc, - reclaim_method, - ); + self.upsert(slot, slot, key, value, &mut gc, reclaim_method); assert!(gc.is_empty()); } } diff --git a/accounts-db/src/accounts_index/iter.rs b/accounts-db/src/accounts_index/iter.rs index 95135db58fa..200cedf26af 100644 --- a/accounts-db/src/accounts_index/iter.rs +++ b/accounts-db/src/accounts_index/iter.rs @@ -43,12 +43,8 @@ impl + Into> Iterator #[cfg(test)] mod tests { use { - super::{ - super::{UpsertReclaim, secondary::AccountSecondaryIndexes}, - *, - }, + super::{super::UpsertReclaim, *}, crate::accounts_index::ReclaimsSlotList, - solana_account::AccountSharedData, std::iter, }; @@ -77,8 +73,6 @@ mod tests { 0, 0, &solana_pubkey::new_rand(), - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), true, &mut gc, UpsertReclaim::PopulateReclaims, @@ -111,8 +105,6 @@ mod tests { slot, slot, pubkey, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), value, &mut gc, UpsertReclaim::PopulateReclaims, @@ -154,8 +146,6 @@ mod tests { slot, slot, pubkey, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), value, &mut gc, UpsertReclaim::PopulateReclaims, diff --git a/accounts-db/src/ancient_append_vecs.rs b/accounts-db/src/ancient_append_vecs.rs index 1eda08e229d..32ce6cb15ba 100644 --- a/accounts-db/src/ancient_append_vecs.rs +++ b/accounts-db/src/ancient_append_vecs.rs @@ -3772,7 +3772,6 @@ mod tests { #[test] fn test_shrink_ancient_expected_unref() { let db = AccountsDb::new_single_for_tests(); - let empty_account = AccountSharedData::default(); for count in 0..3 { let pubkeys_to_unref = (0..count) .map(|_| solana_pubkey::new_rand()) @@ -3788,8 +3787,6 @@ mod tests { slot, slot, k, - &empty_account, - &crate::accounts_index::AccountSecondaryIndexes::default(), AccountInfo::default(), &mut ReclaimsSlotList::new(), UpsertReclaim::IgnoreReclaims, From a8ab0332b3f9851e0124706e09f3169f70a9462e Mon Sep 17 00:00:00 2001 From: Edvard Fagerholm Date: Fri, 22 May 2026 22:19:55 +0300 Subject: [PATCH 042/576] gossip: Remove bincode dependency switching to wincode (#11802) * gossip: migrate serialization from bincode to wincode * gossip: directly serialize packet --- Cargo.lock | 19 +- Cargo.toml | 1 + bloom/Cargo.toml | 4 +- bloom/src/bloom.rs | 26 ++- dev-bins/Cargo.lock | 16 +- gossip/Cargo.toml | 9 +- gossip/src/cluster_info.rs | 123 ++++++++-- gossip/src/contact_info.rs | 73 +++++- gossip/src/crds_data.rs | 160 ++++++++++++- gossip/src/crds_gossip_pull.rs | 39 +++- gossip/src/crds_value.rs | 120 +++++++--- gossip/src/duplicate_shred.rs | 48 +++- gossip/src/epoch_slots.rs | 107 ++++++++- gossip/src/gossip_error.rs | 2 +- gossip/src/ping_pong.rs | 46 +++- gossip/src/protocol.rs | 358 ++++++++++++++++++++++++++---- gossip/src/restart_crds_values.rs | 204 ++++++++++++++++- gossip/src/tlv.rs | 115 ++++++++-- gossip/src/wire_format_tests.rs | 8 +- gossip/tests/crds_gossip.rs | 20 +- programs/sbf/Cargo.lock | 16 +- version/Cargo.toml | 2 + version/src/v4.rs | 122 +++++++++- 23 files changed, 1450 insertions(+), 188 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6cdb567df32..b6eba8146ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7175,6 +7175,7 @@ name = "solana-bloom" version = "4.2.0-alpha.0" dependencies = [ "bencher", + "bincode", "bv", "fnv", "rand 0.9.4", @@ -7188,6 +7189,7 @@ dependencies = [ "solana-sha256-hasher", "solana-signature", "solana-time-utils", + "wincode", ] [[package]] @@ -8510,7 +8512,6 @@ dependencies = [ "agave-random", "anyhow", "arc-swap", - "arrayvec", "assert_matches", "bincode", "bs58", @@ -8568,7 +8569,11 @@ dependencies = [ "solana-vote", "solana-vote-interface", "solana-vote-program", + "solana-wincode-varint", "static_assertions", + "strum", + "strum_macros", + "tempfile", "test-case", "thiserror 2.0.18", "wincode", @@ -11391,6 +11396,8 @@ dependencies = [ "solana-frozen-abi-macro", "solana-sanitize", "solana-serde-varint", + "solana-wincode-varint", + "wincode", ] [[package]] @@ -11496,6 +11503,15 @@ dependencies = [ "test-case", ] +[[package]] +name = "solana-wincode-varint" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6679e72a01dcfe7ff180f0e9d3a0d165e4e9664426930014f2702e7571c259c7" +dependencies = [ + "wincode", +] + [[package]] name = "solana-zero-copy" version = "1.0.0" @@ -13071,6 +13087,7 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37095eb18dd6254c66217edc61a29d83d51f8818de8a2ffe88e4584ad73fb5f9" dependencies = [ + "bv", "bytes", "pastey", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index aa84a1c4e69..01cd828065e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -518,6 +518,7 @@ solana-version = { path = "version", version = "=4.2.0-alpha.0", features = ["ag solana-vote = { path = "vote", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-vote-interface = "6.0.0" solana-vote-program = { path = "programs/vote", version = "=4.2.0-alpha.0", default-features = false, features = ["agave-unstable-api"] } +solana-wincode-varint = "1.0.0" solana-zk-elgamal-proof-program = { path = "programs/zk-elgamal-proof", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-zk-sdk = "5.0.1" solana-zk-token-proof-program = { path = "programs/zk-token-proof", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } diff --git a/bloom/Cargo.toml b/bloom/Cargo.toml index 49131932441..f5e5d1fd62d 100644 --- a/bloom/Cargo.toml +++ b/bloom/Cargo.toml @@ -38,12 +38,14 @@ solana-frozen-abi-macro = { workspace = true, optional = true, features = [ ] } solana-sanitize = { workspace = true } solana-time-utils = { workspace = true } +wincode = { workspace = true, features = ["bv"] } [dev-dependencies] bencher = { workspace = true } +bincode = { workspace = true } rayon = { workspace = true } solana-bloom = { path = ".", features = ["agave-unstable-api", "dev-context-only-utils"] } -solana-hash = { workspace = true, features = ["atomic"] } +solana-hash = { workspace = true, features = ["atomic", "wincode"] } solana-sha256-hasher = { workspace = true, features = ["sha2"] } solana-signature = { workspace = true, features = ["std"] } diff --git a/bloom/src/bloom.rs b/bloom/src/bloom.rs index c54cdd12ee7..80003035eb7 100644 --- a/bloom/src/bloom.rs +++ b/bloom/src/bloom.rs @@ -14,6 +14,7 @@ use { ops::Deref, sync::atomic::{AtomicU64, Ordering}, }, + wincode::{SchemaRead, SchemaWrite}, }; /// Generate a stable hash of `self` for each `hash_index` @@ -23,7 +24,7 @@ pub trait BloomHashIndex { } #[cfg_attr(feature = "frozen-abi", derive(AbiExample))] -#[derive(Serialize, Deserialize, Default, Clone, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Default, Clone, PartialEq, Eq, SchemaWrite, SchemaRead)] pub struct Bloom { pub keys: Vec, pub bits: BitVec, @@ -479,4 +480,27 @@ mod test { } assert_eq!(bits, bloom.bits); } + + #[test] + fn test_wincode_compatibility_bloom() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let num_keys = rng.random_range(1..8usize); + let keys: Vec = (0..num_keys).map(|_| rng.random()).collect(); + let num_bits = rng.random_range(1..1024usize); + let mut bloom = Bloom::::new(num_bits, keys); + let num_inserts = rng.random_range(0..32usize); + for _ in 0..num_inserts { + bloom.add(&generate_random_hash()); + } + + let bincode_bytes = bincode::serialize(&bloom).unwrap(); + let wincode_bytes = wincode::serialize(&bloom).unwrap(); + assert_eq!(bincode_bytes, wincode_bytes); + let wincode_decoded: Bloom = wincode::deserialize(&bincode_bytes).unwrap(); + assert_eq!(bloom, wincode_decoded); + let bincode_decoded: Bloom = bincode::deserialize(&wincode_bytes).unwrap(); + assert_eq!(bloom, bincode_decoded); + } + } } diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index bca82688c91..8fa25e973f7 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -6295,6 +6295,7 @@ dependencies = [ "serde", "solana-sanitize", "solana-time-utils", + "wincode", ] [[package]] @@ -7225,9 +7226,7 @@ dependencies = [ "agave-logger", "agave-random", "arc-swap", - "arrayvec", "assert_matches", - "bincode", "bv", "crossbeam-channel", "flate2", @@ -7271,6 +7270,7 @@ dependencies = [ "solana-version", "solana-vote", "solana-vote-program", + "solana-wincode-varint", "static_assertions", "thiserror 2.0.18", "wincode", @@ -9548,6 +9548,8 @@ dependencies = [ "serde", "solana-sanitize", "solana-serde-varint", + "solana-wincode-varint", + "wincode", ] [[package]] @@ -9629,6 +9631,15 @@ dependencies = [ "solana-vote-interface", ] +[[package]] +name = "solana-wincode-varint" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6679e72a01dcfe7ff180f0e9d3a0d165e4e9664426930014f2702e7571c259c7" +dependencies = [ + "wincode", +] + [[package]] name = "solana-zero-copy" version = "1.0.0" @@ -11168,6 +11179,7 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37095eb18dd6254c66217edc61a29d83d51f8818de8a2ffe88e4584ad73fb5f9" dependencies = [ + "bv", "bytes", "pastey", "proc-macro2", diff --git a/gossip/Cargo.toml b/gossip/Cargo.toml index fc9bd345e01..280d7bcca45 100644 --- a/gossip/Cargo.toml +++ b/gossip/Cargo.toml @@ -36,9 +36,7 @@ dummy-for-ci-check = ["conformance"] agave-logger = { workspace = true } agave-random = { workspace = true } arc-swap = { workspace = true } -arrayvec = { workspace = true } assert_matches = { workspace = true } -bincode = { workspace = true } bv = { workspace = true, features = ["serde"] } crossbeam-channel = { workspace = true } flate2 = { workspace = true } @@ -86,16 +84,18 @@ solana-signature = { workspace = true, default-features = false } solana-signer = { workspace = true } solana-streamer = { workspace = true } solana-time-utils = { workspace = true } -solana-transaction = { workspace = true } +solana-transaction = { workspace = true, features = ["wincode"] } solana-version = { workspace = true } solana-vote = { workspace = true } solana-vote-program = { workspace = true } +solana-wincode-varint = { workspace = true } static_assertions = { workspace = true } thiserror = { workspace = true } wincode = { workspace = true } [dev-dependencies] anyhow = { workspace = true } +bincode = { workspace = true } bs58 = { workspace = true } criterion = { workspace = true } num_cpus = { workspace = true } @@ -108,6 +108,9 @@ solana-signature = { workspace = true, features = ["rand"] } solana-system-transaction = { workspace = true } solana-vote-interface = { workspace = true } static_assertions = { workspace = true } +strum = { workspace = true } +strum_macros = { workspace = true } +tempfile = { workspace = true } test-case = { workspace = true } [[bench]] diff --git a/gossip/src/cluster_info.rs b/gossip/src/cluster_info.rs index 25bd5e7e9b8..feb5a5e5e19 100644 --- a/gossip/src/cluster_info.rs +++ b/gossip/src/cluster_info.rs @@ -36,7 +36,7 @@ use { DUPLICATE_SHRED_MAX_PAYLOAD_SIZE, MAX_INCREMENTAL_SNAPSHOT_HASHES, MAX_PRUNE_DATA_NODES, PULL_RESPONSE_MAX_PAYLOAD_SIZE, PULL_RESPONSE_MIN_SERIALIZED_SIZE, PUSH_MESSAGE_MAX_PAYLOAD_SIZE, Ping, PingCache, - Protocol, PruneData, split_gossip_messages, + Protocol, PruneData, deserialize_protocol, split_gossip_messages, }, weighted_shuffle::WeightedShuffle, }, @@ -132,6 +132,11 @@ pub const DEFAULT_NUM_TVU_RECEIVE_SOCKETS: NonZeroUsize = MINIMUM_NUM_TVU_RECEIV pub const MINIMUM_NUM_TVU_RETRANSMIT_SOCKETS: NonZeroUsize = NonZeroUsize::new(1).unwrap(); pub const DEFAULT_NUM_TVU_RETRANSMIT_SOCKETS: NonZeroUsize = NonZeroUsize::new(12).unwrap(); +// Contact-info save/restore handles trusted local data, so disable wincode's +// default 4 MiB preallocation limit (which defends untrusted wire input). +type ContactInfoFileConfig = + wincode::config::Configuration; + #[derive(Debug, PartialEq, Eq, Error)] pub enum ClusterInfoError { #[error("NoPeers")] @@ -328,7 +333,11 @@ impl ClusterInfo { match File::create(tmp_filename) { Ok(file) => { let mut writer = BufWriter::new(file); - if let Err(err) = bincode::serialize_into(&mut writer, &nodes) { + if let Err(err) = wincode::config::serialize_into( + &mut writer, + &nodes, + ContactInfoFileConfig::new(), + ) { warn!( "Failed to serialize contact info info {}: {}", tmp_filename.display(), @@ -375,12 +384,14 @@ impl ClusterInfo { } let nodes: Vec = match File::open(&filename) { - Ok(file) => { - bincode::deserialize_from(&mut BufReader::new(file)).unwrap_or_else(|err| { - warn!("Failed to deserialize {}: {}", filename.display(), err); - vec![] - }) - } + Ok(file) => wincode::config::deserialize_from( + &mut BufReader::new(file), + ContactInfoFileConfig::new(), + ) + .unwrap_or_else(|err| { + warn!("Failed to deserialize {}: {}", filename.display(), err); + vec![] + }), Err(err) => { warn!("Failed to open {}: {}", filename.display(), err); vec![] @@ -2090,8 +2101,11 @@ impl ClusterInfo { stakes: &HashMap, stats: &GossipStats, ) -> Option<(SocketAddr, Protocol)> { - let mut protocol: Protocol = - stats.record_received_packet(packet.deserialize_slice::(..))?; + let result: wincode::ReadResult = packet + .data(..) + .ok_or(wincode::ReadError::Custom("packet discarded")) + .and_then(deserialize_protocol); + let mut protocol: Protocol = stats.record_received_packet(result)?; protocol.sanitize().ok()?; if let Protocol::PullResponse(_, values) | Protocol::PushMessage(_, values) = &mut protocol @@ -2495,9 +2509,20 @@ fn make_gossip_packet_batch>( recycler: &PacketBatchRecycler, stats: &GossipStats, ) -> RecycledPacketBatch { - let record_gossip_packet = |(_, pkt): &(_, Protocol)| stats.record_gossip_packet(pkt); - let pkts = pkts.into_iter().inspect(record_gossip_packet); - RecycledPacketBatch::new_with_recycler_data_and_dests(recycler, "gossip_packet_batch", pkts) + let pkts = pkts.into_iter(); + let mut batch = + RecycledPacketBatch::new_with_recycler(recycler, pkts.len(), "gossip_packet_batch"); + for (addr, pkt) in pkts { + let addr = addr.borrow(); + if !addr.ip().is_unspecified() && addr.port() != 0 { + if let Some(packet) = make_gossip_packet(addr, &pkt, stats) { + batch.push(packet); + } + } else { + trace!("Dropping packet, as destination is unknown"); + } + } + batch } #[inline] @@ -2506,16 +2531,22 @@ fn make_gossip_packet( pkt: &Protocol, stats: &GossipStats, ) -> Option { - match Packet::from_data(Some(addr.borrow()), pkt) { - Err(err) => { + let mut packet = Packet::default(); + let size = { + let buffer = packet.buffer_mut(); + let initial_len = buffer.len(); + let mut writer: &mut [u8] = buffer; + if let Err(err) = wincode::serialize_into(&mut writer, pkt) { error!("failed to write gossip packet: {err:?}"); - None - } - Ok(out) => { - stats.record_gossip_packet(pkt); - Some(out) + return None; } - } + initial_len - writer.len() + }; + let meta = packet.meta_mut(); + meta.size = size; + meta.set_socket_addr(addr.borrow()); + stats.record_gossip_packet(pkt); + Some(packet) } #[cfg(test)] @@ -2530,7 +2561,6 @@ mod tests { protocol::tests::new_rand_remote_node, socketaddr, }, - bincode::serialize, itertools::izip, solana_keypair::Keypair, solana_ledger::shred::Shredder, @@ -2727,9 +2757,14 @@ mod tests { pongs.into_iter() ) { assert_eq!(packet.meta().socket_addr(), socket); - let bytes = serialize(&pong).unwrap(); + let bytes = wincode::serialize(&pong).unwrap(); + assert_eq!(bytes, bincode::serialize(&pong).unwrap()); match packet.deserialize_slice(..).unwrap() { - Protocol::PongMessage(pong) => assert_eq!(serialize(&pong).unwrap(), bytes), + Protocol::PongMessage(pong) => { + let pong_bytes = wincode::serialize(&pong).unwrap(); + assert_eq!(pong_bytes, bincode::serialize(&pong).unwrap()); + assert_eq!(pong_bytes, bytes); + } _ => panic!("invalid packet!"), } } @@ -2793,6 +2828,46 @@ mod tests { assert!(gossip_crds.get::<&CrdsValue>(&label).is_some()); } + #[test] + fn test_save_restore_contact_info_round_trip() { + let tmpdir = tempfile::tempdir().unwrap(); + + let self_keypair = Arc::new(Keypair::new()); + let self_ci = ContactInfo::new_localhost(&self_keypair.pubkey(), timestamp()); + let mut cluster_info_a = + ClusterInfo::new(self_ci, self_keypair, SocketAddrSpace::Unspecified); + // Sets contact_info_path; no file to load yet. + cluster_info_a.restore_contact_info(tmpdir.path(), 0); + + let peer1 = ContactInfo::new_localhost(&solana_pubkey::new_rand(), timestamp()); + let peer2 = ContactInfo::new_localhost(&solana_pubkey::new_rand(), timestamp()); + let peer1_pubkey = *peer1.pubkey(); + let peer2_pubkey = *peer2.pubkey(); + cluster_info_a.insert_info(peer1); + cluster_info_a.insert_info(peer2); + cluster_info_a.save_contact_info(); + + let other_keypair = Arc::new(Keypair::new()); + let other_ci = ContactInfo::new_localhost(&other_keypair.pubkey(), timestamp()); + let mut cluster_info_b = + ClusterInfo::new(other_ci, other_keypair, SocketAddrSpace::Unspecified); + cluster_info_b.restore_contact_info(tmpdir.path(), 0); + + let gossip_crds = cluster_info_b.gossip.crds.read().unwrap(); + assert!( + gossip_crds + .get::<&CrdsValue>(&CrdsValueLabel::ContactInfo(peer1_pubkey)) + .is_some(), + "peer1 missing after restore" + ); + assert!( + gossip_crds + .get::<&CrdsValue>(&CrdsValueLabel::ContactInfo(peer2_pubkey)) + .is_some(), + "peer2 missing after restore" + ); + } + fn assert_in_range(x: u16, range: (u16, u16)) { assert!(x >= range.0); assert!(x < range.1); diff --git a/gossip/src/contact_info.rs b/gossip/src/contact_info.rs index 3de9e865d1e..feccc0228e2 100644 --- a/gossip/src/contact_info.rs +++ b/gossip/src/contact_info.rs @@ -15,10 +15,12 @@ use { std::{ cmp::Ordering, collections::HashSet, + mem::MaybeUninit, net::{IpAddr, Ipv4Addr, SocketAddr}, time::{SystemTime, UNIX_EPOCH}, }, thiserror::Error, + wincode::{ReadError, ReadResult, SchemaRead, SchemaWrite, config::Config, io::Reader}, }; const DEFAULT_RPC_PORT: u16 = 8899; @@ -77,10 +79,11 @@ pub enum Error { UnusedIpAddr(IpAddr), } -#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +#[derive(Clone, Debug, Eq, PartialEq, Serialize, SchemaWrite)] pub struct ContactInfo { pubkey: Pubkey, #[serde(with = "serde_varint")] + #[wincode(with = "solana_wincode_varint::Leb128Int")] wallclock: u64, // When the node instance was first created. // Identifies duplicate running instances. @@ -89,23 +92,28 @@ pub struct ContactInfo { version: solana_version::Version, // All IP addresses are unique and referenced at least once in sockets. #[serde(with = "short_vec")] + #[wincode(with = "wincode::containers::Vec")] addrs: Vec, // All sockets have a unique key and a valid IP address index. #[serde(with = "short_vec")] + #[wincode(with = "wincode::containers::Vec")] sockets: Vec, #[serde(with = "short_vec")] + #[wincode(with = "wincode::containers::Vec")] extensions: Vec, // Only sanitized socket-addrs can be cached! #[serde(skip_serializing)] + #[wincode(skip(default_val = EMPTY_SOCKET_ADDR_CACHE))] cache: [SocketAddr; SOCKET_CACHE_SIZE], } #[cfg_attr(feature = "frozen-abi", derive(AbiExample))] -#[derive(Copy, Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] +#[derive(Copy, Clone, Debug, Eq, PartialEq, Deserialize, Serialize, SchemaWrite, SchemaRead)] pub(crate) struct SocketEntry { pub(crate) key: u8, // Protocol identifier, e.g. tvu, tpu, etc pub(crate) index: u8, // IpAddr index in the accompanying addrs vector. #[serde(with = "serde_varint")] + #[wincode(with = "solana_wincode_varint::Leb128Int")] pub(crate) offset: u16, // Port offset with respect to the previous entry. } @@ -127,20 +135,24 @@ define_tlv_enum!( // verified and self.cache needs to be populated. This type serves as a // workaround since serde does not have an initializer. // https://github.com/serde-rs/serde/issues/642 -#[derive(Deserialize)] +#[derive(Deserialize, SchemaRead)] struct ContactInfoLite { pubkey: Pubkey, #[serde(with = "serde_varint")] + #[wincode(with = "solana_wincode_varint::Leb128Int")] wallclock: u64, outset: u64, shred_version: u16, version: solana_version::Version, #[serde(with = "short_vec")] + #[wincode(with = "wincode::containers::Vec")] addrs: Vec, #[serde(with = "short_vec")] + #[wincode(with = "wincode::containers::Vec")] sockets: Vec, #[allow(dead_code)] #[serde(with = "short_vec")] + #[wincode(with = "wincode::containers::Vec")] extensions: Vec, } @@ -599,6 +611,18 @@ impl TryFrom for ContactInfo { } } +unsafe impl<'de, C: Config> SchemaRead<'de, C> for ContactInfo { + type Dst = ContactInfo; + + fn read(reader: impl Reader<'de>, dst: &mut MaybeUninit) -> ReadResult<()> { + let lite = >::get(reader)?; + let node = ContactInfo::try_from(lite) + .map_err(|_| ReadError::Custom("ContactInfo: invalid entries"))?; + dst.write(node); + Ok(()) + } +} + impl Sanitize for ContactInfo { fn sanitize(&self) -> Result<(), SanitizeError> { if self.wallclock >= MAX_WALLCLOCK { @@ -977,8 +1001,10 @@ mod tests { .is_ok() ); // Assert that serde round trips. - let bytes = bincode::serialize(&node).unwrap(); - let other: ContactInfo = bincode::deserialize(&bytes).unwrap(); + let bytes = wincode::serialize(&node).unwrap(); + assert_eq!(bytes, bincode::serialize(&node).unwrap()); + let other: ContactInfo = wincode::deserialize(&bytes).unwrap(); + assert_eq!(other, bincode::deserialize::(&bytes).unwrap()); assert_eq!(node, other); } } @@ -1093,4 +1119,41 @@ mod tests { assert_eq!(other.overrides(&node), Some(true)); } } + + #[test] + fn test_wincode_compatibility_contact_info() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let node = ContactInfo::new_rand(&mut rng, None); + + let bincode_bytes = bincode::serialize(&node).unwrap(); + let wincode_decoded: ContactInfo = wincode::deserialize(&bincode_bytes).unwrap(); + assert_eq!(node, wincode_decoded); + + let wincode_bytes = wincode::serialize(&node).unwrap(); + assert_eq!(wincode_bytes, bincode_bytes); + let bincode_decoded: ContactInfo = bincode::deserialize(&wincode_bytes).unwrap(); + assert_eq!(node, bincode_decoded); + } + } + + #[test] + fn test_wincode_compatibility_socket_entry() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let entry = SocketEntry { + key: rng.random(), + index: rng.random(), + offset: rng.random(), + }; + + let bincode_bytes = bincode::serialize(&entry).unwrap(); + let wincode_decoded: SocketEntry = wincode::deserialize(&bincode_bytes).unwrap(); + assert_eq!(entry, wincode_decoded); + + let wincode_bytes = wincode::serialize(&entry).unwrap(); + let bincode_decoded: SocketEntry = bincode::deserialize(&wincode_bytes).unwrap(); + assert_eq!(entry, bincode_decoded); + } + } } diff --git a/gossip/src/crds_data.rs b/gossip/src/crds_data.rs index 4ef1193d1d9..1ba8306604a 100644 --- a/gossip/src/crds_data.rs +++ b/gossip/src/crds_data.rs @@ -14,7 +14,12 @@ use { solana_time_utils::timestamp, solana_transaction::Transaction, solana_vote::vote_parser, - std::collections::BTreeSet, + std::{collections::BTreeSet, mem::MaybeUninit}, + wincode::{ + ReadError, ReadResult, SchemaRead, SchemaWrite, TypeMeta, WriteResult, + config::Config, + io::{Reader, Writer}, + }, }; pub(crate) const MAX_WALLCLOCK: u64 = 1_000_000_000_000_000; @@ -30,22 +35,53 @@ pub(crate) type EpochSlotsIndex = u8; pub(crate) const MAX_EPOCH_SLOTS: EpochSlotsIndex = 255; // Helper for deprecated types -#[derive(Serialize, Clone, Debug, PartialEq, Eq)] +#[derive(Serialize, Clone, Debug, PartialEq, Eq, SchemaWrite)] pub(crate) struct Deprecated {} reject_deserialize!(Deprecated, "Trying to deserialize deprecated type"); +// Wincode schema that reads a u8 and rejects non-zero values, mirroring the +// serde `reject_nonzero_u8` deserializer used on the LowestSlot index field. +struct RejectNonzeroU8; +unsafe impl SchemaWrite for RejectNonzeroU8 { + type Src = u8; + const TYPE_META: TypeMeta = >::TYPE_META; + + fn size_of(src: &u8) -> WriteResult { + >::size_of(src) + } + fn write(writer: impl Writer, src: &u8) -> WriteResult<()> { + >::write(writer, src) + } +} +unsafe impl<'de, C: Config> SchemaRead<'de, C> for RejectNonzeroU8 { + type Dst = u8; + const TYPE_META: TypeMeta = >::TYPE_META.keep_zero_copy(false); + + fn read(reader: impl Reader<'de>, dst: &mut MaybeUninit) -> ReadResult<()> { + let index = >::get(reader)?; + if index != 0 { + return Err(ReadError::Custom("LowestSlot index must be 0")); + } + dst.write(index); + Ok(()) + } +} + /// CrdsData that defines the different types of items CrdsValues can hold /// * Merge Strategy - Latest wallclock is picked /// * LowestSlot index is deprecated #[allow(clippy::large_enum_variant)] #[cfg_attr(feature = "frozen-abi", derive(AbiExample, AbiEnumVisitor))] -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(test, derive(strum_macros::EnumCount))] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, SchemaWrite, SchemaRead)] pub enum CrdsData { #[allow(private_interfaces)] LegacyContactInfo(Deprecated), // Deprecated Vote(VoteIndex, Vote), LowestSlot( - #[serde(deserialize_with = "reject_nonzero_u8")] u8, // u8 is deprecated + #[serde(deserialize_with = "reject_nonzero_u8")] + #[wincode(with = "RejectNonzeroU8")] + u8, // u8 is deprecated LowestSlot, ), #[allow(private_interfaces)] @@ -216,7 +252,7 @@ impl From<&ContactInfo> for CrdsData { } #[cfg_attr(feature = "frozen-abi", derive(AbiExample))] -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, SchemaRead, SchemaWrite)] pub struct SnapshotHashes { pub from: Pubkey, pub full: (Slot, Hash), @@ -243,7 +279,7 @@ impl Sanitize for SnapshotHashes { } #[cfg_attr(feature = "frozen-abi", derive(AbiExample))] -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, SchemaRead, SchemaWrite)] pub struct LowestSlot { pub(crate) from: Pubkey, root: Slot, //deprecated @@ -324,12 +360,13 @@ where } #[cfg_attr(feature = "frozen-abi", derive(AbiExample))] -#[derive(Clone, Debug, PartialEq, Eq, Serialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, SchemaWrite)] pub struct Vote { pub(crate) from: Pubkey, transaction: Transaction, pub(crate) wallclock: u64, #[serde(skip_serializing)] + #[wincode(skip)] slot: Option, } @@ -402,6 +439,27 @@ impl<'de> Deserialize<'de> for Vote { } } +unsafe impl<'de, C: Config> SchemaRead<'de, C> for Vote { + type Dst = Self; + + fn read(reader: impl Reader<'de>, dst: &mut MaybeUninit) -> ReadResult<()> { + #[derive(SchemaRead)] + struct VoteLite { + from: Pubkey, + transaction: Transaction, + wallclock: u64, + } + let lite = >::get(reader)?; + lite.transaction + .sanitize() + .map_err(|_| ReadError::Custom("Vote: invalid transaction"))?; + let vote = Vote::new(lite.from, lite.transaction, lite.wallclock) + .ok_or(ReadError::Custom("Vote: invalid vote tx"))?; + dst.write(vote); + Ok(()) + } +} + pub(crate) fn sanitize_wallclock(wallclock: u64) -> Result<(), SanitizeError> { if wallclock >= MAX_WALLCLOCK { Err(SanitizeError::ValueOutOfBounds) @@ -421,6 +479,18 @@ macro_rules! reject_deserialize { Err(serde::de::Error::custom($msg)) } } + + // Mirror the serde rejection for wincode so deprecated types fail to deserialize + // by either path, whether read directly or as a CrdsData variant. + unsafe impl<'de, C: wincode::config::Config> wincode::SchemaRead<'de, C> for $ty { + type Dst = Self; + fn read( + _reader: impl wincode::io::Reader<'de>, + _dst: &mut std::mem::MaybeUninit, + ) -> wincode::ReadResult<()> { + Err(wincode::ReadError::Custom($msg)) + } + } }; } pub(crate) use reject_deserialize; @@ -496,8 +566,10 @@ mod test { ) .unwrap(); assert_eq!(vote.slot, Some(7)); - let bytes = bincode::serialize(&vote).unwrap(); - let other = bincode::deserialize(&bytes[..]).unwrap(); + let bytes = wincode::serialize(&vote).unwrap(); + assert_eq!(bytes, bincode::serialize(&vote).unwrap()); + let other = wincode::deserialize(&bytes[..]).unwrap(); + assert_eq!(other, bincode::deserialize::(&bytes[..]).unwrap()); assert_eq!(vote, other); assert_eq!(other.slot, Some(7)); let bytes = bincode::options().serialize(&vote).unwrap(); @@ -506,6 +578,66 @@ mod test { assert_eq!(other.slot, Some(7)); } + #[test] + fn test_wincode_compatibility_lowest_slot() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let lowest_slot = LowestSlot::new_rand(&mut rng, None); + + let bincode_bytes = bincode::serialize(&lowest_slot).unwrap(); + let wincode_decoded: LowestSlot = wincode::deserialize(&bincode_bytes).unwrap(); + assert_eq!(lowest_slot, wincode_decoded); + + let wincode_bytes = wincode::serialize(&lowest_slot).unwrap(); + let bincode_decoded: LowestSlot = bincode::deserialize(&wincode_bytes).unwrap(); + assert_eq!(lowest_slot, bincode_decoded); + + assert_eq!(bincode_bytes, wincode_bytes); + } + } + + #[test] + fn test_wincode_compatibility_snapshot_hashes() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let num_incremental = rng.random_range(0usize..5); + let snapshot_hashes = SnapshotHashes { + from: solana_pubkey::new_rand(), + full: (rng.random(), Hash::new_unique()), + incremental: (0..num_incremental) + .map(|_| (rng.random(), Hash::new_unique())) + .collect(), + wallclock: new_rand_timestamp(&mut rng), + }; + + let bincode_bytes = bincode::serialize(&snapshot_hashes).unwrap(); + let wincode_decoded: SnapshotHashes = wincode::deserialize(&bincode_bytes).unwrap(); + assert_eq!(snapshot_hashes, wincode_decoded); + + let wincode_bytes = wincode::serialize(&snapshot_hashes).unwrap(); + let bincode_decoded: SnapshotHashes = bincode::deserialize(&wincode_bytes).unwrap(); + assert_eq!(snapshot_hashes, bincode_decoded); + } + } + + #[test] + fn test_wincode_compatibility_vote() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let keypair = Keypair::new(); + let vote = + Vote::new(keypair.pubkey(), new_test_vote_tx(&mut rng), timestamp()).unwrap(); + + let bincode_bytes = bincode::serialize(&vote).unwrap(); + let wincode_decoded: Vote = wincode::deserialize(&bincode_bytes).unwrap(); + assert_eq!(vote, wincode_decoded); + + let wincode_bytes = wincode::serialize(&vote).unwrap(); + let bincode_decoded: Vote = bincode::deserialize(&wincode_bytes).unwrap(); + assert_eq!(vote, bincode_decoded); + } + } + #[test] fn test_max_epoch_slots_index() { let keypair = Keypair::new(); @@ -530,8 +662,10 @@ mod test { CrdsData::LegacySnapshotHashes(Deprecated {}), ]; - for value in deprecated_values { - let bytes = bincode::serialize(&value).unwrap(); + for value in &deprecated_values { + let bytes = wincode::serialize(value).unwrap(); + assert_eq!(bytes, bincode::serialize(value).unwrap()); + assert!(wincode::deserialize::(&bytes[..]).is_err()); assert!(bincode::deserialize::(&bytes[..]).is_err()); } @@ -546,7 +680,9 @@ mod test { // LowestSlot(0, ...) -> should be deserialized successfully let lowest_slot = CrdsData::LowestSlot(0, LowestSlot::new(keypair.pubkey(), 0, timestamp())); - let bytes = bincode::serialize(&lowest_slot).unwrap(); + let bytes = wincode::serialize(&lowest_slot).unwrap(); + assert_eq!(bytes, bincode::serialize(&lowest_slot).unwrap()); + assert!(wincode::deserialize::(&bytes[..]).is_ok()); assert!(bincode::deserialize::(&bytes[..]).is_ok()); } } diff --git a/gossip/src/crds_gossip_pull.rs b/gossip/src/crds_gossip_pull.rs index 7a8772d8ef1..015abfdb907 100644 --- a/gossip/src/crds_gossip_pull.rs +++ b/gossip/src/crds_gossip_pull.rs @@ -48,6 +48,7 @@ use { }, time::Duration, }, + wincode::{SchemaRead, SchemaWrite}, }; pub const CRDS_GOSSIP_PULL_CRDS_TIMEOUT_MS: u64 = 15000; @@ -57,7 +58,7 @@ pub const FALSE_RATE: f64 = 0.1f64; pub const KEYS: f64 = 8f64; #[cfg_attr(feature = "frozen-abi", derive(AbiExample))] -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, SchemaWrite, SchemaRead)] pub struct CrdsFilter { pub filter: Bloom, mask: u64, @@ -607,7 +608,7 @@ impl Index<&Pubkey> for CrdsTimeouts<'_> { } } -// Returns max_bytes for the bloom filter such that bincode serialized +// Returns max_bytes for the bloom filter such that the serialized // Protocol::PullRequest(CrdsFilter, CrdsValue) fits in a packet. pub(crate) fn get_max_bloom_filter_bytes(caller: &CrdsValue) -> usize { // Maps serialized size of CrdsFilter to max_bytes of bloom filter. @@ -619,7 +620,7 @@ pub(crate) fn get_max_bloom_filter_bytes(caller: &CrdsValue) -> usize { let mut iter = Vec::::from(filters) .into_iter() .map(|filter| { - bincode::serialized_size(&filter) + wincode::serialized_size(&filter) .map(usize::try_from) .unwrap() .unwrap() @@ -639,12 +640,12 @@ pub(crate) fn get_max_bloom_filter_bytes(caller: &CrdsValue) -> usize { }); out }); - // Maximum bincode serialized size of CrdsFilter in + // Maximum serialized size of CrdsFilter in // Protocol::PullRequest(CrdsFilter, CrdsValue) let size_of_filter = PACKET_DATA_SIZE .checked_sub( // 4 bytes for u32 enum variant identifier of Protocol. - 4 + caller.bincode_serialized_size(), + 4 + caller.serialized_size(), ) .unwrap(); MAX_BYTES_CACHE @@ -1328,11 +1329,15 @@ pub(crate) mod tests { let packet_data_size_range = (PACKET_DATA_SIZE - 7)..=PACKET_DATA_SIZE; let max_bytes = get_max_bloom_filter_bytes(caller); let filters = CrdsFilterSet::new(rng, num_items, max_bytes); - let request_bytes = caller.bincode_serialized_size() as u64; + let request_bytes = caller.serialized_size() as u64; for filter in Vec::::from(filters) { - let request_bytes = 4 + request_bytes + bincode::serialized_size(&filter).unwrap(); + let filter_size = wincode::serialized_size(&filter).unwrap(); + assert_eq!(filter_size, bincode::serialized_size(&filter).unwrap()); + let request_bytes = 4 + request_bytes + filter_size; let request = Protocol::PullRequest(filter, caller.clone()); - let request = bincode::serialize(&request).unwrap(); + let request_wincode = wincode::serialize(&request).unwrap(); + assert_eq!(request_wincode, bincode::serialize(&request).unwrap()); + let request = request_wincode; assert!(packet_data_size_range.contains(&request.len())); assert_eq!(request.len() as u64, request_bytes); } @@ -1482,4 +1487,22 @@ pub(crate) mod tests { assert!(filter.test_mask(&hash)); assert!(!filter.test_mask(&bad_hash)); } + + #[test] + fn test_wincode_compatibility_crds_filter() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let num_items = rng.random_range(0..1000); + let max_bytes = rng.random_range(32..512); + let filter = CrdsFilter::new_rand(num_items, max_bytes); + + let bincode_bytes = bincode::serialize(&filter).unwrap(); + let wincode_decoded: CrdsFilter = wincode::deserialize(&bincode_bytes).unwrap(); + assert_eq!(filter, wincode_decoded); + + let wincode_bytes = wincode::serialize(&filter).unwrap(); + let bincode_decoded: CrdsFilter = bincode::deserialize(&wincode_bytes).unwrap(); + assert_eq!(filter, bincode_decoded); + } + } } diff --git a/gossip/src/crds_value.rs b/gossip/src/crds_value.rs index f8310c1e5a3..718108a7919 100644 --- a/gossip/src/crds_value.rs +++ b/gossip/src/crds_value.rs @@ -5,8 +5,6 @@ use { duplicate_shred::DuplicateShredIndex, epoch_slots::EpochSlots, }, - arrayvec::ArrayVec, - bincode::serialize, rand::Rng, serde::{Deserialize, Serialize, de::Deserializer}, solana_hash::Hash, @@ -16,16 +14,21 @@ use { solana_sanitize::{Sanitize, SanitizeError}, solana_signature::Signature, solana_signer::Signer, - std::borrow::{Borrow, Cow}, + std::{ + borrow::{Borrow, Cow}, + mem::MaybeUninit, + }, + wincode::{ReadError, ReadResult, SchemaRead, SchemaWrite, config::Config, io::Reader}, }; /// CrdsValue that is replicated across the cluster #[cfg_attr(feature = "frozen-abi", derive(AbiExample))] -#[derive(Serialize, Clone, Debug, PartialEq, Eq)] +#[derive(Serialize, Clone, Debug, PartialEq, Eq, SchemaWrite)] pub struct CrdsValue { signature: Signature, data: CrdsData, #[serde(skip_serializing)] + #[wincode(skip)] hash: Hash, // Sha256 hash of [signature, data]. } @@ -42,7 +45,7 @@ impl Signable for CrdsValue { } fn signable_data(&self) -> Cow<'static, [u8]> { - Cow::Owned(serialize(&self.data).expect("failed to serialize CrdsData")) + Cow::Owned(wincode::serialize(&self.data).expect("failed to serialize CrdsData")) } fn get_signature(&self) -> Signature { @@ -90,9 +93,9 @@ impl CrdsValueLabel { impl CrdsValue { pub fn new(data: CrdsData, keypair: &Keypair) -> Self { - let bincode_serialized_data = bincode::serialize(&data).unwrap(); - let signature = keypair.sign_message(&bincode_serialized_data); - let hash = solana_sha256_hasher::hashv(&[signature.as_ref(), &bincode_serialized_data]); + let serialized_data = wincode::serialize(&data).unwrap(); + let signature = keypair.sign_message(&serialized_data); + let hash = solana_sha256_hasher::hashv(&[signature.as_ref(), &serialized_data]); Self { signature, data, @@ -102,9 +105,9 @@ impl CrdsValue { #[cfg(test)] pub(crate) fn new_unsigned(data: CrdsData) -> Self { - let bincode_serialized_data = bincode::serialize(&data).unwrap(); + let serialized_data = wincode::serialize(&data).unwrap(); let signature = Signature::default(); - let hash = solana_sha256_hasher::hashv(&[signature.as_ref(), &bincode_serialized_data]); + let hash = solana_sha256_hasher::hashv(&[signature.as_ref(), &serialized_data]); Self { signature, data, @@ -190,15 +193,50 @@ impl CrdsValue { Some(epoch_slots) } - /// Returns the bincode serialized size (in bytes) of the CrdsValue. - pub fn bincode_serialized_size(&self) -> usize { - bincode::serialized_size(&self) + /// Returns the serialized size (in bytes) of the CrdsValue. + pub fn serialized_size(&self) -> usize { + wincode::serialized_size(&self) .map(usize::try_from) .unwrap() .unwrap() } } +// Computes sha256(signature || serialize(data)) using a stack buffer. +// PACKET_DATA_SIZE is always enough since the value originated in a packet. +fn compute_crds_value_hash(signature: &Signature, data: &CrdsData) -> wincode::WriteResult { + let mut buffer = [MaybeUninit::::uninit(); PACKET_DATA_SIZE]; + let mut writer: &mut [MaybeUninit] = &mut buffer; + wincode::serialize_into(&mut writer, data)?; + let written = PACKET_DATA_SIZE - writer.len(); + // SAFETY: wincode's "Writer for &mut [MaybeUninit]" initializes every + // consumed slot before advancing the cursor, so the first "written" bytes are init. + let bytes = unsafe { std::slice::from_raw_parts(buffer.as_ptr().cast::(), written) }; + Ok(solana_sha256_hasher::hashv(&[signature.as_ref(), bytes])) +} + +// Manual implementation of SchemaRead for CrdsValue in order to populate +// CrdsValue.hash which is skipped in serialization. +unsafe impl<'de, C: Config> SchemaRead<'de, C> for CrdsValue { + type Dst = Self; + fn read(reader: impl Reader<'de>, dst: &mut MaybeUninit) -> ReadResult<()> { + #[derive(SchemaRead)] + struct CrdsValueLite { + signature: Signature, + data: CrdsData, + } + let CrdsValueLite { signature, data } = >::get(reader)?; + let hash = compute_crds_value_hash(&signature, &data) + .map_err(|_| ReadError::Custom("failed to serialize CrdsData"))?; + dst.write(Self { + signature, + data, + hash, + }); + Ok(()) + } +} + // Manual implementation of Deserialize for CrdsValue in order to populate // CrdsValue.hash which is skipped in serialization. impl<'de> Deserialize<'de> for CrdsValue { @@ -212,12 +250,7 @@ impl<'de> Deserialize<'de> for CrdsValue { data: CrdsData, } let CrdsValue { signature, data } = CrdsValue::deserialize(deserializer)?; - // To compute the hash of the received CrdsData we need to re-serialize it - // PACKET_DATA_SIZE is always enough since we have just received the value in a packet - // ArrayVec allows us to write serialized data into stack memory without initializing it - let mut buffer = ArrayVec::::new(); - bincode::serialize_into(&mut buffer, &data).map_err(serde::de::Error::custom)?; - let hash = solana_sha256_hasher::hashv(&[signature.as_ref(), &buffer]); + let hash = compute_crds_value_hash(&signature, &data).map_err(serde::de::Error::custom)?; Ok(Self { signature, data, @@ -231,7 +264,6 @@ mod test { use { super::*, crate::crds_data::{LowestSlot, Vote}, - bincode::deserialize, rand::SeedableRng as _, rand_chacha::ChaChaRng, solana_keypair::Keypair, @@ -298,8 +330,13 @@ mod test { value.sign(keypair); let original_signature = value.get_signature(); for _ in 0..num_tries { - let serialized_value = serialize(value).unwrap(); - let deserialized_value: CrdsValue = deserialize(&serialized_value).unwrap(); + let serialized_value = wincode::serialize(value).unwrap(); + assert_eq!(serialized_value, bincode::serialize(value).unwrap()); + let deserialized_value: CrdsValue = wincode::deserialize(&serialized_value).unwrap(); + assert_eq!( + deserialized_value, + bincode::deserialize::(&serialized_value).unwrap() + ); // Signatures shouldn't change let deserialized_signature = deserialized_value.get_signature(); @@ -395,16 +432,49 @@ mod test { CrdsValue::new(CrdsData::Vote(5, vote), &keypair) }, ]; - let bytes = bincode::serialize(&values).unwrap(); + let bytes = wincode::serialize(&values).unwrap(); + assert_eq!(bytes, bincode::serialize(&values).unwrap()); // Serialized bytes are fixed and should never change. assert_eq!( solana_sha256_hasher::hash(&bytes), Hash::from_str("BTg284TRo5S5PpbA9YZaab5rKeoLNAj7arwadvG6XVLT").unwrap() ); // serialize -> deserialize should round trip. + let wincode_values = wincode::deserialize::>(&bytes).unwrap(); assert_eq!( - bincode::deserialize::>(&bytes).unwrap(), - values + wincode_values, + bincode::deserialize::>(&bytes).unwrap() ); + assert_eq!(wincode_values, values); + } + + #[test] + fn test_wincode_compatibility_crds_value() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let value = CrdsValue::new_rand(&mut rng, None); + let bincode_bytes = bincode::serialize(&value).unwrap(); + let wincode_bytes = wincode::serialize(&value).unwrap(); + assert_eq!( + bincode_bytes, + wincode_bytes, + "bytes differ for {:?}", + value.label() + ); + // Deprecated types and Vote with test-only invalid transactions intentionally + // fail serde deserialization; skip those. + let Ok(bincode_decoded) = bincode::deserialize::(&bincode_bytes) else { + continue; + }; + let wincode_decoded: CrdsValue = wincode::deserialize(&bincode_bytes) + .unwrap_or_else(|e| panic!("wincode deser failed for {:?}: {e}", value.label())); + assert_eq!( + bincode_decoded, + wincode_decoded, + "deser mismatch for {:?}", + value.label() + ); + assert_eq!(value, bincode_decoded); + } } } diff --git a/gossip/src/duplicate_shred.rs b/gossip/src/duplicate_shred.rs index b1ac6cc727a..5ac48b941a0 100644 --- a/gossip/src/duplicate_shred.rs +++ b/gossip/src/duplicate_shred.rs @@ -16,6 +16,7 @@ use { num::TryFromIntError, }, thiserror::Error, + wincode::{ReadError, SchemaRead, SchemaWrite, WriteError}, }; const DUPLICATE_SHRED_HEADER_SIZE: usize = 63; @@ -24,7 +25,7 @@ pub(crate) type DuplicateShredIndex = u16; pub(crate) const MAX_DUPLICATE_SHREDS: DuplicateShredIndex = 512; #[cfg_attr(feature = "frozen-abi", derive(AbiExample))] -#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, SchemaWrite, SchemaRead)] pub struct DuplicateShred { pub(crate) from: Pubkey, pub(crate) wallclock: u64, @@ -109,9 +110,9 @@ pub enum Error { #[error("missing data chunk")] MissingDataChunk, #[error("wincode deserialization error")] - WincodeReadError(#[from] wincode::ReadError), + WincodeReadError(#[from] ReadError), #[error("wincode serialization error")] - WincodeWriteError(#[from] wincode::WriteError), + WincodeWriteError(#[from] WriteError), #[error("shred type mismatch")] ShredTypeMismatch, #[error("slot mismatch")] @@ -379,6 +380,33 @@ pub(crate) mod tests { std::sync::Arc, }; + #[test] + fn test_wincode_compatibility_duplicate_shred() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let chunk_len = rng.random_range(0..64usize); + let dup = DuplicateShred { + from: Pubkey::new_unique(), + wallclock: rng.random(), + slot: rng.random(), + _unused: rng.random(), + _unused_shred_type: rng.random(), + num_chunks: rng.random(), + chunk_index: rng.random(), + chunk: (0..chunk_len).map(|_| rng.random()).collect(), + }; + + let bincode_bytes = bincode::serialize(&dup).unwrap(); + let wincode_decoded: DuplicateShred = wincode::deserialize(&bincode_bytes).unwrap(); + assert_eq!(dup, wincode_decoded); + + let wincode_bytes = wincode::serialize(&dup).unwrap(); + assert_eq!(wincode_bytes, bincode_bytes); + let bincode_decoded: DuplicateShred = bincode::deserialize(&wincode_bytes).unwrap(); + assert_eq!(dup, bincode_decoded); + } + } + #[test] fn test_duplicate_shred_header_size() { let dup = DuplicateShred { @@ -391,14 +419,12 @@ pub(crate) mod tests { chunk: Vec::default(), _unused: 0, }; - assert_eq!( - bincode::serialize(&dup).unwrap().len(), - DUPLICATE_SHRED_HEADER_SIZE - ); - assert_eq!( - bincode::serialized_size(&dup).unwrap(), - DUPLICATE_SHRED_HEADER_SIZE as u64 - ); + let dup_bytes = wincode::serialize(&dup).unwrap(); + assert_eq!(dup_bytes, bincode::serialize(&dup).unwrap()); + assert_eq!(dup_bytes.len(), DUPLICATE_SHRED_HEADER_SIZE); + let dup_size = wincode::serialized_size(&dup).unwrap(); + assert_eq!(dup_size, bincode::serialized_size(&dup).unwrap()); + assert_eq!(dup_size, DUPLICATE_SHRED_HEADER_SIZE as u64); } pub(crate) fn new_rand_shred( diff --git a/gossip/src/epoch_slots.rs b/gossip/src/epoch_slots.rs index 8ceeb16ea2c..fc0f99f17d0 100644 --- a/gossip/src/epoch_slots.rs +++ b/gossip/src/epoch_slots.rs @@ -3,7 +3,6 @@ use { crds_data::{self, MAX_SLOT, MAX_WALLCLOCK}, protocol::MAX_CRDS_OBJECT_SIZE, }, - bincode::serialized_size, bv::BitVec, flate2::{Compress, Compression, Decompress, FlushCompress, FlushDecompress}, serde::{Deserialize, Serialize}, @@ -11,11 +10,13 @@ use { solana_pubkey::Pubkey, solana_sanitize::{Sanitize, SanitizeError}, std::{borrow::Cow, cell::RefCell, sync::Arc}, + wincode::{SchemaRead, SchemaWrite}, }; pub const MAX_SLOTS_PER_ENTRY: usize = 2048 * 8; + #[cfg_attr(feature = "frozen-abi", derive(AbiExample))] -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, SchemaRead, SchemaWrite)] pub struct Uncompressed { pub first_slot: Slot, pub num: usize, @@ -44,7 +45,7 @@ impl Sanitize for Uncompressed { } #[cfg_attr(feature = "frozen-abi", derive(AbiExample))] -#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)] +#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq, SchemaRead, SchemaWrite)] pub struct Flate2 { pub first_slot: Slot, pub num: usize, @@ -198,7 +199,7 @@ impl Uncompressed { } #[cfg_attr(feature = "frozen-abi", derive(AbiExample, AbiEnumVisitor))] -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, SchemaRead, SchemaWrite)] pub enum CompressedSlots { Flate2(Flate2), Uncompressed(Uncompressed), @@ -265,7 +266,7 @@ impl CompressedSlots { } #[cfg_attr(feature = "frozen-abi", derive(AbiExample))] -#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq, SchemaRead, SchemaWrite)] pub struct EpochSlots { pub from: Pubkey, pub slots: Vec, @@ -340,8 +341,8 @@ impl EpochSlots { Ok(()) } pub fn max_compressed_slot_size(&self) -> isize { - let len_header = serialized_size(self).unwrap(); - let len_slot = serialized_size(&CompressedSlots::default()).unwrap(); + let len_header = wincode::serialized_size(self).unwrap(); + let len_slot = wincode::serialized_size(&CompressedSlots::default()).unwrap(); MAX_CRDS_OBJECT_SIZE as isize - (len_header + len_slot) as isize } @@ -529,6 +530,98 @@ mod tests { assert!(slots.to_slots(0).eq(range)); } + #[test] + fn test_wincode_compatibility_uncompressed() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let mut unc = Uncompressed::new(rng.random_range(1..=16usize)); + let first: Slot = rng.random_range(0..10_000); + let num: u64 = rng.random_range(0..64); + let slots: Vec<_> = (0..num).map(|i| first + i).collect(); + unc.add(&slots); + + let bincode_bytes = bincode::serialize(&unc).unwrap(); + let wincode_decoded: Uncompressed = wincode::deserialize(&bincode_bytes).unwrap(); + assert_eq!(unc, wincode_decoded); + + let wincode_bytes = wincode::serialize(&unc).unwrap(); + let bincode_decoded: Uncompressed = bincode::deserialize(&wincode_bytes).unwrap(); + assert_eq!(unc, bincode_decoded); + } + } + + #[test] + fn test_wincode_compatibility_flate2() { + let mut rng = rand::rng(); + for _ in 0..1000 { + // Arbitrary bytes — wire-format equivalence does not require a valid compressed stream. + let len = rng.random_range(0..64usize); + let flate2 = Flate2 { + first_slot: rng.random(), + num: rng.random_range(0..1000), + compressed: Arc::new((0..len).map(|_| rng.random::()).collect()), + }; + + let bincode_bytes = bincode::serialize(&flate2).unwrap(); + let wincode_decoded: Flate2 = wincode::deserialize(&bincode_bytes).unwrap(); + assert_eq!(flate2, wincode_decoded); + + let wincode_bytes = wincode::serialize(&flate2).unwrap(); + let bincode_decoded: Flate2 = bincode::deserialize(&wincode_bytes).unwrap(); + assert_eq!(flate2, bincode_decoded); + } + } + + #[test] + fn test_wincode_compatibility_compressed_slots() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let mut unc = Uncompressed::new(rng.random_range(1..=16usize)); + let first: Slot = rng.random_range(0..10_000); + let num: u64 = rng.random_range(0..64); + let slots: Vec<_> = (0..num).map(|i| first + i).collect(); + unc.add(&slots); + let cs = CompressedSlots::Uncompressed(unc); + + let bincode_bytes = bincode::serialize(&cs).unwrap(); + let wincode_decoded: CompressedSlots = wincode::deserialize(&bincode_bytes).unwrap(); + assert_eq!(cs, wincode_decoded); + let wincode_bytes = wincode::serialize(&cs).unwrap(); + let bincode_decoded: CompressedSlots = bincode::deserialize(&wincode_bytes).unwrap(); + assert_eq!(cs, bincode_decoded); + + let len = rng.random_range(0..64usize); + let cs = CompressedSlots::Flate2(Flate2 { + first_slot: rng.random(), + num: rng.random_range(0..1000), + compressed: Arc::new((0..len).map(|_| rng.random::()).collect()), + }); + + let bincode_bytes = bincode::serialize(&cs).unwrap(); + let wincode_decoded: CompressedSlots = wincode::deserialize(&bincode_bytes).unwrap(); + assert_eq!(cs, wincode_decoded); + let wincode_bytes = wincode::serialize(&cs).unwrap(); + let bincode_decoded: CompressedSlots = bincode::deserialize(&wincode_bytes).unwrap(); + assert_eq!(cs, bincode_decoded); + } + } + + #[test] + fn test_wincode_compatibility_epoch_slots() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let epoch_slots = EpochSlots::new_rand(&mut rng, None); + + let bincode_bytes = bincode::serialize(&epoch_slots).unwrap(); + let wincode_decoded: EpochSlots = wincode::deserialize(&bincode_bytes).unwrap(); + assert_eq!(epoch_slots, wincode_decoded); + + let wincode_bytes = wincode::serialize(&epoch_slots).unwrap(); + let bincode_decoded: EpochSlots = bincode::deserialize(&wincode_bytes).unwrap(); + assert_eq!(epoch_slots, bincode_decoded); + } + } + fn make_rand_slots(rng: &mut R) -> impl Iterator + '_ { repeat_with(|| rng.random_range(1..5)).scan(0, |slot, step| { *slot += step; diff --git a/gossip/src/gossip_error.rs b/gossip/src/gossip_error.rs index d9457b07091..89f439ca1b2 100644 --- a/gossip/src/gossip_error.rs +++ b/gossip/src/gossip_error.rs @@ -20,7 +20,7 @@ pub enum GossipError { #[error("send error")] SendError, #[error("serialization error")] - Serialize(#[from] Box), + Serialize(#[from] wincode::Error), } impl std::convert::From> for GossipError { diff --git a/gossip/src/ping_pong.rs b/gossip/src/ping_pong.rs index 6174e5f74a1..8e7345ea1b6 100644 --- a/gossip/src/ping_pong.rs +++ b/gossip/src/ping_pong.rs @@ -17,6 +17,7 @@ use { net::{IpAddr, SocketAddr}, time::{Duration, Instant}, }, + wincode::{SchemaRead, SchemaWrite}, }; const KEY_REFRESH_CADENCE: Duration = Duration::from_secs(60); @@ -27,7 +28,7 @@ const PONG_SIGNATURE_SAMPLE_LEADING_ZEROS: u32 = 5; // N should always be >= 8 and only the first 8 bytes are used. So the new code // should only use N == 8. #[cfg_attr(feature = "frozen-abi", derive(AbiExample))] -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, PartialEq, Serialize, SchemaRead, SchemaWrite)] pub struct Ping { from: Pubkey, #[serde(with = "BigArray")] @@ -36,7 +37,9 @@ pub struct Ping { } #[cfg_attr(feature = "frozen-abi", derive(AbiExample))] -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, PartialEq, Serialize, SchemaRead, SchemaWrite)] +// repr(C) makes this struct zero-copy eligible in wincode. +#[repr(C)] pub struct Pong { from: Pubkey, hash: Hash, // Hash of received ping token. @@ -446,6 +449,45 @@ mod tests { } } + #[test] + fn test_wincode_compatibility_ping() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let keypair = Keypair::new(); + let ping = Ping::<32>::new(rng.random(), &keypair); + + let bincode_bytes = bincode::serialize(&ping).unwrap(); + let wincode_decoded: Ping<32> = wincode::deserialize(&bincode_bytes).unwrap(); + assert_eq!(ping, wincode_decoded); + + let wincode_bytes = wincode::serialize(&ping).unwrap(); + let bincode_decoded: Ping<32> = bincode::deserialize(&wincode_bytes).unwrap(); + assert_eq!(ping, bincode_decoded); + + assert_eq!(bincode_bytes, wincode_bytes); + } + } + + #[test] + fn test_wincode_compatibility_pong() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let keypair = Keypair::new(); + let ping = Ping::<32>::new(rng.random(), &keypair); + let pong = Pong::new(&ping, &keypair); + + let bincode_bytes = bincode::serialize(&pong).unwrap(); + let wincode_decoded: Pong = wincode::deserialize(&bincode_bytes).unwrap(); + assert_eq!(pong, wincode_decoded); + + let wincode_bytes = wincode::serialize(&pong).unwrap(); + let bincode_decoded: Pong = bincode::deserialize(&wincode_bytes).unwrap(); + assert_eq!(pong, bincode_decoded); + + assert_eq!(bincode_bytes, wincode_bytes); + } + } + #[test] fn test_expired_pong_returns_check_false() { let mut rng = rand::rng(); diff --git a/gossip/src/protocol.rs b/gossip/src/protocol.rs index ac9da04f1ab..8fbb5049137 100644 --- a/gossip/src/protocol.rs +++ b/gossip/src/protocol.rs @@ -6,7 +6,6 @@ use { crds_value::CrdsValue, ping_pong::{self, Pong}, }, - bincode::serialize, serde::{Deserialize, Serialize}, solana_keypair::signable::Signable, solana_perf::packet::PACKET_DATA_SIZE, @@ -18,6 +17,7 @@ use { fmt::Debug, result::Result, }, + wincode::{SchemaRead, SchemaWrite}, }; pub(crate) const MAX_CRDS_OBJECT_SIZE: usize = 928; @@ -43,9 +43,19 @@ const PRUNE_DATA_PREFIX: &[u8] = b"\xffSOLANA_PRUNE_DATA"; const GOSSIP_PING_TOKEN_SIZE: usize = 32; /// Minimum serialized size of a Protocol::PullResponse packet. pub(crate) const PULL_RESPONSE_MIN_SERIALIZED_SIZE: usize = 161; +const MIN_CRDS_VALUE_SERIALIZED_SIZE: usize = + PULL_RESPONSE_MIN_SERIALIZED_SIZE - (PACKET_DATA_SIZE - PULL_RESPONSE_MAX_PAYLOAD_SIZE); +const MAX_CRDS_VALUES_PER_PACKET: usize = + (PULL_RESPONSE_MAX_PAYLOAD_SIZE / MIN_CRDS_VALUE_SERIALIZED_SIZE) + 1; +// Wincode's preallocation limit is decoded collection memory, not input bytes. +// Bound it to the largest CRDS value vector that can fit in one gossip packet. +const GOSSIP_PROTOCOL_PREALLOC_LIMIT: usize = + MAX_CRDS_VALUES_PER_PACKET * std::mem::size_of::(); +type GossipProtocolWincodeConfig = + wincode::config::Configuration; /// Gossip protocol messages base enum -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, SchemaRead, SchemaWrite)] #[allow(clippy::large_enum_variant)] pub(crate) enum Protocol { PullRequest(CrdsFilter, CrdsValue), @@ -62,8 +72,15 @@ pub(crate) enum Protocol { pub(crate) type Ping = ping_pong::Ping; pub(crate) type PingCache = ping_pong::PingCache; +pub(crate) fn deserialize_protocol(input: &[u8]) -> wincode::ReadResult { + wincode::config::deserialize_exact::( + input, + GossipProtocolWincodeConfig::new(), + ) +} + #[cfg_attr(feature = "frozen-abi", derive(AbiExample))] -#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)] +#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, SchemaRead, SchemaWrite)] pub(crate) struct PruneData { /// Pubkey of the node that sent this prune data pub(crate) pubkey: Pubkey, @@ -78,10 +95,10 @@ pub(crate) struct PruneData { } impl Protocol { - /// Returns the bincode serialized size (in bytes) of the Protocol. + /// Returns the serialized size (in bytes) of the Protocol. #[cfg(test)] - fn bincode_serialized_size(&self) -> usize { - bincode::serialized_size(self) + fn serialized_size(&self) -> usize { + wincode::serialized_size(self) .map(usize::try_from) .unwrap() .unwrap() @@ -103,7 +120,7 @@ impl Protocol { impl PruneData { fn signable_data_without_prefix(&self) -> Cow<'static, [u8]> { - #[derive(Serialize)] + #[derive(Serialize, SchemaWrite)] struct SignData<'a> { pubkey: &'a Pubkey, prunes: &'a [Pubkey], @@ -116,11 +133,11 @@ impl PruneData { destination: &self.destination, wallclock: self.wallclock, }; - Cow::Owned(serialize(&data).expect("should serialize PruneData")) + Cow::Owned(wincode::serialize(&data).expect("should serialize PruneData")) } fn signable_data_with_prefix(&self) -> Cow<'static, [u8]> { - #[derive(Serialize)] + #[derive(Serialize, SchemaWrite)] struct SignDataWithPrefix<'a> { prefix: &'a [u8], pubkey: &'a Pubkey, @@ -135,7 +152,7 @@ impl PruneData { destination: &self.destination, wallclock: self.wallclock, }; - Cow::Owned(serialize(&data).expect("should serialize PruneDataWithPrefix")) + Cow::Owned(wincode::serialize(&data).expect("should serialize PruneDataWithPrefix")) } fn verify_data(&self, use_prefix: bool) -> bool { @@ -223,7 +240,9 @@ impl Signable for PruneData { /// max_chunk_size. /// Note: some messages cannot be contained within that size so in the worst case this returns /// N nested Vecs with 1 item each. -pub(crate) fn split_gossip_messages( +pub(crate) fn split_gossip_messages< + T: Serialize + Debug + SchemaWrite, +>( max_chunk_size: usize, data_feed: impl IntoIterator, ) -> impl Iterator> { @@ -235,7 +254,7 @@ pub(crate) fn split_gossip_messages( let Some(data) = data_feed.next() else { return (!buffer.is_empty()).then(|| std::mem::take(&mut buffer)); }; - let data_size = match bincode::serialized_size(&data) { + let data_size = match wincode::serialized_size(&data) { Ok(size) => size as usize, Err(err) => { error!("serialized_size failed: {err:?}"); @@ -262,7 +281,6 @@ pub fn gossip_decode_to_effects(input: &[u8]) -> protosol::protos::GossipEffects crds_data::CrdsData, crds_gossip_pull::CrdsFilter as NativeCrdsFilter, crds_value::CrdsValue as NativeCrdsValue, ping_pong::Pong, }, - bincode::Options as _, bv::Bits, protosol::protos::{ GossipBloom, GossipCompressedSlots, GossipContactInfo, GossipCrdsData, @@ -361,7 +379,7 @@ pub fn gossip_decode_to_effects(input: &[u8]) -> protosol::protos::GossipEffects } CrdsData::Vote(idx, vote) => { let tx_bytes = - bincode::serialize(vote.transaction()).expect("vote transaction serialization"); + wincode::serialize(vote.transaction()).expect("vote transaction serialization"); Some(gossip_crds_data::Data::Vote(GossipVote { index: *idx as u32, from: vote.from().to_bytes().to_vec(), @@ -499,11 +517,16 @@ pub fn gossip_decode_to_effects(input: &[u8]) -> protosol::protos::GossipEffects } } - let result = bincode::options() - .with_limit(PACKET_DATA_SIZE as u64) - .with_fixint_encoding() - .reject_trailing_bytes() - .deserialize::(input); + // Reject over-long inputs upfront so an over-long input whose prefix + // encodes a valid Protocol can't slip through; deserialize_exact rejects + // any trailing bytes that remain within the packet window. + if input.len() > PACKET_DATA_SIZE { + return GossipEffects { + valid: false, + msg: None, + }; + } + let result = deserialize_protocol(input); match result { Ok(proto) if proto.sanitize().is_ok() => { @@ -526,15 +549,17 @@ pub(crate) mod tests { super::*, crate::{ contact_info::ContactInfo, - crds_data::{self, CrdsData, LowestSlot, SnapshotHashes, Vote as CrdsVote}, + crds_data::{self, CrdsData, Deprecated, LowestSlot, SnapshotHashes, Vote as CrdsVote}, duplicate_shred::{self, MAX_DUPLICATE_SHREDS, tests::new_rand_shred}, + epoch_slots::EpochSlots, + restart_crds_values::{RestartHeaviestFork, RestartLastVotedForkSlots}, }, rand::Rng, solana_clock::Slot, solana_hash::Hash, solana_keypair::Keypair, solana_ledger::shred::Shredder, - solana_perf::packet::Packet, + solana_perf::{packet::Packet, test_tx::new_test_vote_tx}, solana_signer::Signer, solana_time_utils::timestamp, solana_transaction::Transaction, @@ -599,6 +624,20 @@ pub(crate) mod tests { prune_data } + #[test] + fn test_deserialize_protocol_rejects_large_vec_preallocation() { + let mut bytes = Vec::new(); + bytes.extend_from_slice(&2u32.to_le_bytes()); // Protocol::PushMessage. + bytes.extend_from_slice(&Pubkey::new_unique().to_bytes()); + bytes.extend_from_slice(&u64::MAX.to_le_bytes()); // Vec length. + + let err = deserialize_protocol(&bytes).unwrap_err(); + assert!(matches!( + err, + wincode::ReadError::PreallocationSizeLimit { .. } + )); + } + #[test] fn test_max_snapshot_hashes_with_push_messages() { let mut rng = rand::rng(); @@ -654,7 +693,7 @@ pub(crate) mod tests { let header = Protocol::PushMessage(Pubkey::default(), Vec::default()); assert_eq!( PUSH_MESSAGE_MAX_PAYLOAD_SIZE, - PACKET_DATA_SIZE - header.bincode_serialized_size() + PACKET_DATA_SIZE - header.serialized_size() ); } @@ -663,7 +702,7 @@ pub(crate) mod tests { let header = Protocol::PullResponse(Pubkey::default(), Vec::default()); assert_eq!( PULL_RESPONSE_MAX_PAYLOAD_SIZE, - PACKET_DATA_SIZE - header.bincode_serialized_size() + PACKET_DATA_SIZE - header.serialized_size() ); } @@ -703,9 +742,9 @@ pub(crate) mod tests { let data = CrdsData::DuplicateShred(MAX_DUPLICATE_SHREDS - 1, chunk); let value = CrdsValue::new(data, &keypair); let pull_response = Protocol::PullResponse(keypair.pubkey(), vec![value.clone()]); - assert!(pull_response.bincode_serialized_size() < PACKET_DATA_SIZE); + assert!(pull_response.serialized_size() < PACKET_DATA_SIZE); let push_message = Protocol::PushMessage(keypair.pubkey(), vec![value.clone()]); - assert!(push_message.bincode_serialized_size() < PACKET_DATA_SIZE); + assert!(push_message.serialized_size() < PACKET_DATA_SIZE); } } @@ -715,7 +754,7 @@ pub(crate) mod tests { for _ in 0..100 { let crds_values = vec![CrdsValue::new_rand(&mut rng, None)]; let pull_response = Protocol::PullResponse(Pubkey::new_unique(), crds_values); - let size = pull_response.bincode_serialized_size(); + let size = pull_response.serialized_size(); assert!( PULL_RESPONSE_MIN_SERIALIZED_SIZE <= size, "pull-response serialized size: {size}" @@ -723,6 +762,85 @@ pub(crate) mod tests { } } + #[test] + fn test_min_crds_value_serialized_size_holds() { + let mut rng = rand::rng(); + let keypair = Keypair::new(); + + // Build a DuplicateShred for the corresponding variant. + let leader = Arc::new(Keypair::new()); + let (slot, parent_slot, reference_tick, version) = (53084024, 53084023, 0, 0); + let shredder = Shredder::new(slot, parent_slot, reference_tick, version).unwrap(); + let next_shred_index = rng.random_range(0..32_000); + let shred = new_rand_shred(&mut rng, next_shred_index, &shredder, &leader); + let other_payload = { + let other = new_rand_shred(&mut rng, next_shred_index, &shredder, &leader); + other.into_payload() + }; + let leader_schedule = |s| (s == slot).then_some(leader.pubkey()); + let dup_shred = duplicate_shred::from_shred( + shred, + keypair.pubkey(), + other_payload, + Some(leader_schedule), + timestamp(), + DUPLICATE_SHRED_MAX_PAYLOAD_SIZE, + version, + ) + .unwrap() + .next() + .unwrap(); + + let vote = + CrdsVote::new(keypair.pubkey(), new_test_vote_tx(&mut rng), timestamp()).unwrap(); + + // One representative per CrdsData variant. The array length is keyed to + // strum::EnumCount, so adding a new CrdsData variant without listing it + // here is a compile error (wrong array length). + use strum::EnumCount; + let variants: [CrdsData; CrdsData::COUNT] = [ + CrdsData::LegacyContactInfo(Deprecated {}), + CrdsData::Vote(0, vote), + CrdsData::LowestSlot(0, LowestSlot::new(Pubkey::new_unique(), 0, timestamp())), + CrdsData::LegacySnapshotHashes(Deprecated {}), + CrdsData::AccountsHashes(Deprecated {}), + CrdsData::EpochSlots(0, EpochSlots::new_rand(&mut rng, None)), + CrdsData::LegacyVersion(Deprecated {}), + CrdsData::Version(Deprecated {}), + CrdsData::NodeInstance(Deprecated {}), + CrdsData::DuplicateShred(0, dup_shred), + CrdsData::SnapshotHashes(SnapshotHashes { + from: Pubkey::new_unique(), + full: (0, Hash::default()), + incremental: vec![], + wallclock: timestamp(), + }), + CrdsData::ContactInfo(ContactInfo::new_localhost( + &Pubkey::new_unique(), + timestamp(), + )), + CrdsData::RestartLastVotedForkSlots(RestartLastVotedForkSlots::new_rand( + &mut rng, None, + )), + CrdsData::RestartHeaviestFork(RestartHeaviestFork::new_rand(&mut rng, None)), + ]; + + for data in variants { + let value = CrdsValue::new_unsigned(data); + let bytes = wincode::serialize(&value).unwrap(); + // Deprecated variants fail to deserialize; only assert the bound + // for variants that can appear in a successfully-parsed Protocol. + if wincode::deserialize::(&bytes).is_ok() { + assert!( + bytes.len() >= MIN_CRDS_VALUE_SERIALIZED_SIZE, + "MIN_CRDS_VALUE_SERIALIZED_SIZE ({MIN_CRDS_VALUE_SERIALIZED_SIZE}) \ + underestimates serialized size {}", + bytes.len(), + ); + } + } + } + #[test] fn test_split_messages_small() { let value = CrdsValue::new_unsigned(CrdsData::from(ContactInfo::default())); @@ -763,13 +881,9 @@ pub(crate) mod tests { let header_size = PACKET_DATA_SIZE - PUSH_MESSAGE_MAX_PAYLOAD_SIZE; for values in splits { // Assert that sum of parts equals the whole. - let size = header_size - + values - .iter() - .map(CrdsValue::bincode_serialized_size) - .sum::(); + let size = header_size + values.iter().map(CrdsValue::serialized_size).sum::(); let message = Protocol::PushMessage(self_pubkey, values); - assert_eq!(message.bincode_serialized_size(), size); + assert_eq!(message.serialized_size(), size); // Assert that the message fits into a packet. assert!(Packet::from_data(Some(&socket), message).is_ok()); } @@ -801,13 +915,9 @@ pub(crate) mod tests { let header_size = PACKET_DATA_SIZE - PULL_RESPONSE_MAX_PAYLOAD_SIZE; for values in splits { // Assert that sum of parts equals the whole. - let size = header_size - + values - .iter() - .map(CrdsValue::bincode_serialized_size) - .sum::(); + let size = header_size + values.iter().map(CrdsValue::serialized_size).sum::(); let message = Protocol::PullResponse(self_pubkey, values); - assert_eq!(message.bincode_serialized_size(), size); + assert_eq!(message.serialized_size(), size); // Assert that the message fits into a packet. assert!(Packet::from_data(Some(&socket), message).is_ok()); } @@ -824,7 +934,7 @@ pub(crate) mod tests { incremental: incremental.clone(), wallclock: 0, })); - while value.bincode_serialized_size() < PUSH_MESSAGE_MAX_PAYLOAD_SIZE { + while value.serialized_size() < PUSH_MESSAGE_MAX_PAYLOAD_SIZE { incremental.push((0, Hash::default())); value = CrdsValue::new_unsigned(CrdsData::SnapshotHashes(SnapshotHashes { from: Pubkey::default(), @@ -840,7 +950,7 @@ pub(crate) mod tests { fn test_split_messages(value: CrdsValue) { const NUM_VALUES: usize = 30; - let value_size = value.bincode_serialized_size(); + let value_size = value.serialized_size(); let num_values_per_payload = (PUSH_MESSAGE_MAX_PAYLOAD_SIZE / value_size).max(1); // Expected len is the ceiling of the division @@ -902,7 +1012,7 @@ pub(crate) mod tests { ) .unwrap(); let vote = CrdsValue::new(CrdsData::Vote(1, vote), &Keypair::new()); - assert!(vote.bincode_serialized_size() <= PUSH_MESSAGE_MAX_PAYLOAD_SIZE); + assert!(vote.serialized_size() <= PUSH_MESSAGE_MAX_PAYLOAD_SIZE); } #[test] @@ -932,6 +1042,172 @@ pub(crate) mod tests { assert!(is_valid, "Signature should be valid with prefix"); } + #[test] + fn test_wincode_compatibility_prune_data() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let keypair = Keypair::new(); + let prune_data = new_rand_prune_data(&mut rng, &keypair, None); + + let bincode_bytes = bincode::serialize(&prune_data).unwrap(); + let wincode_decoded: PruneData = wincode::deserialize(&bincode_bytes).unwrap(); + assert_eq!(prune_data, wincode_decoded); + + let wincode_bytes = wincode::serialize(&prune_data).unwrap(); + let bincode_decoded: PruneData = bincode::deserialize(&wincode_bytes).unwrap(); + assert_eq!(prune_data, bincode_decoded); + } + } + + #[test] + fn test_wincode_compatibility_vote_transaction() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let keypair = Keypair::new(); + let slots = (0..rng.random_range(1..32)).map(|_| rng.random()).collect(); + let vote = Vote::new(slots, Hash::new_unique()); + let vote_ix = vote_instruction::vote_switch( + &keypair.pubkey(), + &keypair.pubkey(), + vote, + Hash::new_unique(), + ); + let mut vote_tx = Transaction::new_with_payer(&[vote_ix], Some(&keypair.pubkey())); + vote_tx.partial_sign(&[&keypair], Hash::new_unique()); + + let bincode_bytes = bincode::serialize(&vote_tx).unwrap(); + let wincode_bytes = wincode::serialize(&vote_tx).unwrap(); + assert_eq!(bincode_bytes, wincode_bytes); + } + } + + #[test] + fn test_wincode_compatibility_prune_signable_data() { + // Signed/verified bytes — any divergence would silently break signatures. + #[derive(Serialize)] + struct SignDataMirror<'a> { + pubkey: &'a Pubkey, + prunes: &'a [Pubkey], + destination: &'a Pubkey, + wallclock: u64, + } + #[derive(Serialize)] + struct SignDataWithPrefixMirror<'a> { + prefix: &'a [u8], + pubkey: &'a Pubkey, + prunes: &'a [Pubkey], + destination: &'a Pubkey, + wallclock: u64, + } + let mut rng = rand::rng(); + for _ in 0..1000 { + let keypair = Keypair::new(); + let prune_data = new_rand_prune_data(&mut rng, &keypair, None); + + let wincode_no_prefix = prune_data.signable_data_without_prefix(); + let bincode_no_prefix = bincode::serialize(&SignDataMirror { + pubkey: &prune_data.pubkey, + prunes: &prune_data.prunes, + destination: &prune_data.destination, + wallclock: prune_data.wallclock, + }) + .unwrap(); + assert_eq!(wincode_no_prefix.as_ref(), bincode_no_prefix.as_slice()); + + let wincode_with_prefix = prune_data.signable_data_with_prefix(); + let bincode_with_prefix = bincode::serialize(&SignDataWithPrefixMirror { + prefix: PRUNE_DATA_PREFIX, + pubkey: &prune_data.pubkey, + prunes: &prune_data.prunes, + destination: &prune_data.destination, + wallclock: prune_data.wallclock, + }) + .unwrap(); + assert_eq!(wincode_with_prefix.as_ref(), bincode_with_prefix.as_slice()); + } + } + + #[test] + fn test_wincode_compatibility_protocol_prune_message() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let keypair = Keypair::new(); + let prune_data = new_rand_prune_data(&mut rng, &keypair, None); + let protocol = Protocol::PruneMessage(keypair.pubkey(), prune_data); + + let bincode_bytes = bincode::serialize(&protocol).unwrap(); + let wincode_bytes = wincode::serialize(&protocol).unwrap(); + assert_eq!(bincode_bytes, wincode_bytes); + + let wincode_decoded: Protocol = wincode::deserialize(&bincode_bytes).unwrap(); + assert_eq!(wincode::serialize(&wincode_decoded).unwrap(), bincode_bytes); + + let bincode_decoded: Protocol = bincode::deserialize(&wincode_bytes).unwrap(); + assert_eq!(bincode::serialize(&bincode_decoded).unwrap(), wincode_bytes); + } + } + + fn new_rand_contact_info_crds_value(rng: &mut R) -> CrdsValue { + let keypair = Keypair::new(); + let ci = ContactInfo::new_rand(rng, Some(keypair.pubkey())); + CrdsValue::new(CrdsData::ContactInfo(ci), &keypair) + } + + fn assert_protocol_wincode_compat(protocol: &Protocol) { + let bincode_bytes = bincode::serialize(protocol).unwrap(); + let wincode_bytes = wincode::serialize(protocol).unwrap(); + assert_eq!(bincode_bytes, wincode_bytes); + let wincode_decoded: Protocol = wincode::deserialize(&bincode_bytes).unwrap(); + assert_eq!(wincode::serialize(&wincode_decoded).unwrap(), bincode_bytes); + let bincode_decoded: Protocol = bincode::deserialize(&wincode_bytes).unwrap(); + assert_eq!(bincode::serialize(&bincode_decoded).unwrap(), wincode_bytes); + } + + #[test] + fn test_wincode_compatibility_protocol_pull_request() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let filter = CrdsFilter::new_rand(rng.random_range(0..1000), rng.random_range(32..512)); + let crds_value = new_rand_contact_info_crds_value(&mut rng); + assert_protocol_wincode_compat(&Protocol::PullRequest(filter, crds_value)); + } + } + + #[test] + fn test_wincode_compatibility_protocol_pull_response() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let values: Vec<_> = (0..rng.random_range(1usize..8)) + .map(|_| new_rand_contact_info_crds_value(&mut rng)) + .collect(); + assert_protocol_wincode_compat(&Protocol::PullResponse(Pubkey::new_unique(), values)); + } + } + + #[test] + fn test_wincode_compatibility_protocol_push_message() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let values: Vec<_> = (0..rng.random_range(1usize..8)) + .map(|_| new_rand_contact_info_crds_value(&mut rng)) + .collect(); + assert_protocol_wincode_compat(&Protocol::PushMessage(Pubkey::new_unique(), values)); + } + } + + #[test] + fn test_wincode_compatibility_protocol_ping_pong() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let keypair = Keypair::new(); + let token: [u8; GOSSIP_PING_TOKEN_SIZE] = rng.random(); + let ping = Ping::new(token, &keypair); + let pong = Pong::new(&ping, &keypair); + assert_protocol_wincode_compat(&Protocol::PingMessage(ping)); + assert_protocol_wincode_compat(&Protocol::PongMessage(pong)); + } + } + #[test] fn test_prune_data_verify_with_and_without_prefix() { let mut rng = rand::rng(); diff --git a/gossip/src/restart_crds_values.rs b/gossip/src/restart_crds_values.rs index 46b668893cd..4de7270bd9c 100644 --- a/gossip/src/restart_crds_values.rs +++ b/gossip/src/restart_crds_values.rs @@ -9,11 +9,13 @@ use { solana_pubkey::Pubkey, solana_sanitize::{Sanitize, SanitizeError}, solana_serde_varint as serde_varint, + solana_wincode_varint::Leb128Int, thiserror::Error, + wincode::{SchemaRead, SchemaWrite}, }; #[cfg_attr(feature = "frozen-abi", derive(AbiExample))] -#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)] +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug, SchemaWrite, SchemaRead)] pub struct RestartLastVotedForkSlots { pub from: Pubkey, pub wallclock: u64, @@ -30,7 +32,9 @@ pub enum RestartLastVotedForkSlotsError { } #[cfg_attr(feature = "frozen-abi", derive(AbiExample))] -#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)] +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug, SchemaWrite, SchemaRead)] +// repr(C) makes this struct zero-copy eligible in wincode. +#[repr(C)] pub struct RestartHeaviestFork { pub from: Pubkey, pub wallclock: u64, @@ -41,24 +45,28 @@ pub struct RestartHeaviestFork { } #[cfg_attr(feature = "frozen-abi", derive(AbiExample, AbiEnumVisitor))] -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, SchemaWrite, SchemaRead)] enum SlotsOffsets { RunLengthEncoding(RunLengthEncoding), RawOffsets(RawOffsets), } #[cfg_attr(feature = "frozen-abi", derive(AbiExample))] -#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)] -struct U16(#[serde(with = "serde_varint")] u16); +#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq, SchemaWrite, SchemaRead)] +struct U16( + #[serde(with = "serde_varint")] + #[wincode(with = "Leb128Int")] + u16, +); // The vector always starts with 1. Encode number of 1's and 0's consecutively. // For example, 110000111 is [2, 4, 3]. #[cfg_attr(feature = "frozen-abi", derive(AbiExample))] -#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)] +#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq, SchemaWrite, SchemaRead)] struct RunLengthEncoding(Vec); #[cfg_attr(feature = "frozen-abi", derive(AbiExample))] -#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)] +#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq, SchemaWrite, SchemaRead)] struct RawOffsets(BitVec); impl Sanitize for RestartLastVotedForkSlots { @@ -225,7 +233,7 @@ mod test { crds_value::{CrdsValue, CrdsValueLabel}, protocol::MAX_CRDS_OBJECT_SIZE, }, - bincode::serialized_size, + bv::BitsPush, solana_keypair::Keypair, solana_signer::Signer, solana_time_utils::timestamp, @@ -251,9 +259,11 @@ mod test { ) .unwrap(); // If the following assert fails, please update RestartLastVotedForkSlots::MAX_BYTES + let header_size = wincode::serialized_size(&header).unwrap(); + assert_eq!(header_size, bincode::serialized_size(&header).unwrap()); assert_eq!( RestartLastVotedForkSlots::MAX_BYTES, - MAX_CRDS_OBJECT_SIZE - serialized_size(&header).unwrap() as usize + MAX_CRDS_OBJECT_SIZE - header_size as usize ); // Create large enough slots to make sure we are discarding some to make slots fit. @@ -268,7 +278,12 @@ mod test { 0, ) .unwrap(); - assert!(serialized_size(&large_slots).unwrap() <= MAX_CRDS_OBJECT_SIZE as u64); + let large_slots_size = wincode::serialized_size(&large_slots).unwrap(); + assert_eq!( + large_slots_size, + bincode::serialized_size(&large_slots).unwrap() + ); + assert!(large_slots_size <= MAX_CRDS_OBJECT_SIZE as u64); let retrieved_slots = large_slots.to_slots(0); assert!(retrieved_slots.len() <= range.len()); assert!(retrieved_slots.last().unwrap() - retrieved_slots.first().unwrap() > 5000); @@ -322,7 +337,12 @@ mod test { shred_version, ) .unwrap(); - assert!(serialized_size(&large_slots).unwrap() < MAX_CRDS_OBJECT_SIZE as u64); + let large_slots_size = wincode::serialized_size(&large_slots).unwrap(); + assert_eq!( + large_slots_size, + bincode::serialized_size(&large_slots).unwrap() + ); + assert!(large_slots_size < MAX_CRDS_OBJECT_SIZE as u64); let retrieved_slots = large_slots.to_slots(0); assert_eq!(retrieved_slots, large_slots_vec); } @@ -357,6 +377,168 @@ mod test { check_run_length_encoding(range); } + #[test] + fn test_wincode_compatibility_run_length_encoding() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let len = rng.random_range(0..32usize); + let rle = RunLengthEncoding((0..len).map(|_| U16(rng.random())).collect()); + let bincode_bytes = bincode::serialize(&rle).unwrap(); + let wincode_bytes = wincode::serialize(&rle).unwrap(); + assert_eq!(bincode_bytes, wincode_bytes); + assert_eq!( + rle, + wincode::deserialize::(&bincode_bytes).unwrap() + ); + assert_eq!( + rle, + bincode::deserialize::(&wincode_bytes).unwrap() + ); + } + } + + #[test] + fn test_wincode_compatibility_raw_offsets() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let nblocks = rng.random_range(0usize..64); + let mut bitvec = BitVec::::new(); + for _ in 0..nblocks { + bitvec.push_block(rng.random::()); + } + let nbits = if nblocks == 0 { + 0 + } else { + rng.random_range(0u64..nblocks as u64 * 8 + 1) + }; + bitvec.truncate(nbits); + // shrink_to_fit matches RawOffsets::new behavior: serde serializes + // the backing block count, so it must equal block_len() for the + // two wire formats to be identical. + bitvec.shrink_to_fit(); + let raw = RawOffsets(bitvec); + let bincode_bytes = bincode::serialize(&raw).unwrap(); + let wincode_bytes = wincode::serialize(&raw).unwrap(); + assert_eq!(bincode_bytes, wincode_bytes); + assert_eq!( + raw, + wincode::deserialize::(&bincode_bytes).unwrap() + ); + assert_eq!( + raw, + bincode::deserialize::(&wincode_bytes).unwrap() + ); + } + } + + #[test] + fn test_wincode_compatibility_slots_offsets() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let len = rng.random_range(0..32usize); + let offsets = SlotsOffsets::RunLengthEncoding(RunLengthEncoding( + (0..len).map(|_| U16(rng.random())).collect(), + )); + let bincode_bytes = bincode::serialize(&offsets).unwrap(); + let wincode_bytes = wincode::serialize(&offsets).unwrap(); + assert_eq!(bincode_bytes, wincode_bytes); + assert_eq!( + offsets, + wincode::deserialize::(&bincode_bytes).unwrap() + ); + assert_eq!( + offsets, + bincode::deserialize::(&wincode_bytes).unwrap() + ); + } + for _ in 0..1000 { + let nblocks = rng.random_range(0usize..64); + let mut bitvec = BitVec::::new(); + for _ in 0..nblocks { + bitvec.push_block(rng.random::()); + } + let nbits = if nblocks == 0 { + 0 + } else { + rng.random_range(0u64..nblocks as u64 * 8 + 1) + }; + bitvec.truncate(nbits); + // shrink_to_fit matches RawOffsets::new behavior: serde serializes + // the backing block count, so it must equal block_len() for the + // two wire formats to be identical. + bitvec.shrink_to_fit(); + let offsets = SlotsOffsets::RawOffsets(RawOffsets(bitvec)); + let bincode_bytes = bincode::serialize(&offsets).unwrap(); + let wincode_bytes = wincode::serialize(&offsets).unwrap(); + assert_eq!(bincode_bytes, wincode_bytes); + assert_eq!( + offsets, + wincode::deserialize::(&bincode_bytes).unwrap() + ); + assert_eq!( + offsets, + bincode::deserialize::(&wincode_bytes).unwrap() + ); + } + } + + #[test] + fn test_wincode_compatibility_restart_last_voted_fork_slots() { + let mut rng = rand::rng(); + // Test RawOffsets variant (small slot ranges from new_rand). + for _ in 0..1000 { + let slots = RestartLastVotedForkSlots::new_rand(&mut rng, None); + let bincode_bytes = bincode::serialize(&slots).unwrap(); + let wincode_decoded: RestartLastVotedForkSlots = + wincode::deserialize(&bincode_bytes).unwrap(); + assert_eq!(slots, wincode_decoded); + let wincode_bytes = wincode::serialize(&slots).unwrap(); + let bincode_decoded: RestartLastVotedForkSlots = + bincode::deserialize(&wincode_bytes).unwrap(); + assert_eq!(slots, bincode_decoded); + } + // Test RunLengthEncoding variant: consecutive slots produce a single large run + // (num_encoded_slots > MAX_BYTES * 8), forcing RunLengthEncoding to be chosen. + for _ in 0..100 { + let base: Slot = rng.random_range(0..100_000); + let range: Vec = (base..base + 8001).collect(); + let keypair = solana_keypair::Keypair::new(); + let slots = RestartLastVotedForkSlots::new( + keypair.pubkey(), + new_rand_timestamp(&mut rng), + &range, + Hash::new_unique(), + 1, + ) + .unwrap(); + assert!(matches!(slots.offsets, SlotsOffsets::RunLengthEncoding(_))); + let bincode_bytes = bincode::serialize(&slots).unwrap(); + let wincode_decoded: RestartLastVotedForkSlots = + wincode::deserialize(&bincode_bytes).unwrap(); + assert_eq!(slots, wincode_decoded); + let wincode_bytes = wincode::serialize(&slots).unwrap(); + let bincode_decoded: RestartLastVotedForkSlots = + bincode::deserialize(&wincode_bytes).unwrap(); + assert_eq!(slots, bincode_decoded); + } + } + + #[test] + fn test_wincode_compatibility_restart_heaviest_fork() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let fork = RestartHeaviestFork::new_rand(&mut rng, None); + let bincode_bytes = bincode::serialize(&fork).unwrap(); + let wincode_decoded: RestartHeaviestFork = + wincode::deserialize(&bincode_bytes).unwrap(); + assert_eq!(fork, wincode_decoded); + let wincode_bytes = wincode::serialize(&fork).unwrap(); + let bincode_decoded: RestartHeaviestFork = + bincode::deserialize(&wincode_bytes).unwrap(); + assert_eq!(fork, bincode_decoded); + } + } + #[test] fn test_restart_heaviest_fork() { let keypair = Keypair::new(); diff --git a/gossip/src/tlv.rs b/gossip/src/tlv.rs index 521cae7e5d9..02cfc14b6b4 100644 --- a/gossip/src/tlv.rs +++ b/gossip/src/tlv.rs @@ -1,15 +1,17 @@ use { serde::{Deserialize, Serialize}, solana_short_vec as short_vec, + wincode::{ReadError, SchemaRead, SchemaWrite, WriteError}, }; -/// Type-Length-Value encoding wrapper for bincode -#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] +/// Type-Length-Value encoding wrapper +#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize, SchemaWrite, SchemaRead)] pub(crate) struct TlvRecord { // type pub(crate) typ: u8, // length and serialized bytes of the value #[serde(with = "short_vec")] + #[wincode(with = "wincode::containers::Vec")] pub(crate) bytes: Vec, } @@ -32,6 +34,20 @@ macro_rules! define_tlv_enum { )* } + unsafe impl wincode::SchemaWrite for $enum_name { + type Src = Self; + + fn size_of(value: &Self::Src) -> wincode::WriteResult { + let tlv_rec = TlvRecord::try_from(value).map_err(|_| wincode::WriteError::Custom("invalid as tlv_rec"))?; + >::size_of(&tlv_rec) + } + + fn write(writer: impl wincode::io::Writer, value: &Self::Src) -> wincode::WriteResult<()> { + let tlv_rec = TlvRecord::try_from(value).map_err(|_| wincode::WriteError::Custom("invalid as tlv_rec"))?; + >::write(writer, &tlv_rec) + } + } + // Serialize enum by first converting into TlvRecord, and then serializing that impl serde::Serialize for $enum_name { fn serialize(&self, serializer: S) -> Result @@ -49,7 +65,7 @@ macro_rules! define_tlv_enum { fn try_from(value: &TlvRecord) -> Result { match value.typ { $( - $typ => Ok(Self::$variant(bincode::deserialize::<$inner>(&value.bytes)?)), + $typ => Ok(Self::$variant(wincode::deserialize::<$inner>(&value.bytes)?)), )* _ => Err(TlvDecodeError::UnknownType(value.typ)), } @@ -57,21 +73,35 @@ macro_rules! define_tlv_enum { } // define conversion into TLV wire format impl TryFrom<&$enum_name> for TlvRecord { - type Error = bincode::Error; + type Error = $crate::tlv::TlvEncodeError; fn try_from(value: &$enum_name) -> Result { - use serde::ser::Error; match value { $( $enum_name::$variant(inner) => Ok(TlvRecord { typ: $typ, - bytes: bincode::serialize(inner)?, + bytes: wincode::serialize(inner)?, }), )* #[allow(unreachable_patterns)] - _ => Err(bincode::Error::custom("Unsupported enum variant")), + _ => Err($crate::tlv::TlvEncodeError::UnsupportedVariant), } } } + + unsafe impl<'de, C: wincode::config::Config> wincode::SchemaRead<'de, C> for $enum_name { + type Dst = Self; + + fn read( + reader: impl wincode::io::Reader<'de>, + dst: &mut std::mem::MaybeUninit, + ) -> wincode::ReadResult<()> { + let rec = <$crate::tlv::TlvRecord as wincode::SchemaRead<'de, C>>::get(reader)?; + let value = Self::try_from(&rec) + .map_err(|_| wincode::ReadError::Custom("TlvRecord conversion failed"))?; + dst.write(value); + Ok(()) + } + } }; } @@ -80,7 +110,15 @@ pub enum TlvDecodeError { #[error("Unknown type: {0}")] UnknownType(u8), #[error("Malformed payload: {0}")] - MalformedPayload(#[from] bincode::Error), + MalformedPayload(#[from] ReadError), +} + +#[derive(Debug, thiserror::Error)] +pub enum TlvEncodeError { + #[error("Serialization failed: {0}")] + Wincode(#[from] WriteError), + #[error("Unsupported enum variant")] + UnsupportedVariant, } /// Parses a slice of serialized TLV records into a provided type. Unsupported @@ -91,7 +129,10 @@ pub(crate) fn parse<'a, T: TryFrom<&'a TlvRecord>>(entries: &'a [TlvRecord]) -> #[cfg(test)] mod tests { - use crate::tlv::{TlvDecodeError, TlvRecord}; + use { + crate::tlv::{TlvDecodeError, TlvRecord}, + rand::Rng, + }; define_tlv_enum! (pub(crate) enum ExtensionNew { 1=>Test(u64), @@ -114,8 +155,13 @@ mod tests { ExtensionNew::NewString(String::from("bla")), ]; - let new_bytes = bincode::serialize(&new_tlv_data).unwrap(); - let tlv_vec: Vec = bincode::deserialize(&new_bytes).unwrap(); + let new_bytes = wincode::serialize(&new_tlv_data).unwrap(); + assert_eq!(new_bytes, bincode::serialize(&new_tlv_data).unwrap()); + let tlv_vec: Vec = wincode::deserialize(&new_bytes).unwrap(); + assert_eq!( + tlv_vec, + bincode::deserialize::>(&new_bytes).unwrap() + ); // check that both TLV are encoded correctly let new: Vec = crate::tlv::parse(&tlv_vec); assert!(matches!(new[0], ExtensionNew::Test(42))); @@ -143,9 +189,14 @@ mod tests { ExtensionLegacy::Test(42), ExtensionLegacy::LegacyString(String::from("foo")), ]; - let legacy_bytes = bincode::serialize(&legacy_tlv_data).unwrap(); + let legacy_bytes = wincode::serialize(&legacy_tlv_data).unwrap(); + assert_eq!(legacy_bytes, bincode::serialize(&legacy_tlv_data).unwrap()); - let tlv_vec: Vec = bincode::deserialize(&legacy_bytes).unwrap(); + let tlv_vec: Vec = wincode::deserialize(&legacy_bytes).unwrap(); + assert_eq!( + tlv_vec, + bincode::deserialize::>(&legacy_bytes).unwrap() + ); // Just in case make sure that legacy data is serialized correctly let legacy: Vec = crate::tlv::parse(&tlv_vec); assert!(matches!(legacy[0], ExtensionLegacy::Test(42))); @@ -163,4 +214,42 @@ mod tests { panic!("Wrong deserialization") }; } + + #[test] + fn test_wincode_compatibility_tlv_record() { + let mut rng = rand::rng(); + // Test various byte lengths to exercise all ShortU16 varint widths: + // 0-127: 1-byte varint + // 128-16383: 2-byte varint + let lengths: &[usize] = &[0, 1, 64, 127, 128, 255, 1000, 16383]; + for &len in lengths { + let record = TlvRecord { + typ: rng.random::(), + bytes: (0..len).map(|_| rng.random::()).collect(), + }; + let bincode_bytes = bincode::serialize(&record).unwrap(); + let wincode_bytes = wincode::serialize(&record).unwrap(); + assert_eq!( + bincode_bytes, wincode_bytes, + "bytes differ for TlvRecord with len={len}" + ); + let wincode_decoded: TlvRecord = wincode::deserialize(&bincode_bytes).unwrap(); + assert_eq!(record, wincode_decoded); + let bincode_decoded: TlvRecord = bincode::deserialize(&wincode_bytes).unwrap(); + assert_eq!(record, bincode_decoded); + } + // Also fuzz with random lengths and types + for _ in 0..1000 { + let len = rng.random_range(0usize..256); + let record = TlvRecord { + typ: rng.random::(), + bytes: (0..len).map(|_| rng.random::()).collect(), + }; + let bincode_bytes = bincode::serialize(&record).unwrap(); + let wincode_bytes = wincode::serialize(&record).unwrap(); + assert_eq!(bincode_bytes, wincode_bytes); + let wincode_decoded: TlvRecord = wincode::deserialize(&bincode_bytes).unwrap(); + assert_eq!(record, wincode_decoded); + } + } } diff --git a/gossip/src/wire_format_tests.rs b/gossip/src/wire_format_tests.rs index 440dbbe44da..da2a6e79f3f 100644 --- a/gossip/src/wire_format_tests.rs +++ b/gossip/src/wire_format_tests.rs @@ -17,8 +17,12 @@ mod tests { Ok(pkt) } - fn serialize(pkt: T) -> Vec { - bincode::serialize(&pkt).unwrap() + fn serialize>( + pkt: T, + ) -> Vec { + let wincode_bytes = wincode::serialize(&pkt).unwrap(); + assert_eq!(wincode_bytes, bincode::serialize(&pkt).unwrap()); + wincode_bytes } fn find_differences(a: &[u8], b: &[u8]) -> Option { diff --git a/gossip/tests/crds_gossip.rs b/gossip/tests/crds_gossip.rs index 0be89ba29a1..7d35f5c42fa 100644 --- a/gossip/tests/crds_gossip.rs +++ b/gossip/tests/crds_gossip.rs @@ -1,6 +1,5 @@ #![allow(clippy::arithmetic_side_effects)] use { - bincode::serialized_size, itertools::Itertools, log::*, rayon::{ThreadPool, ThreadPoolBuilder, prelude::*}, @@ -393,10 +392,7 @@ fn network_run_push( let mut pruned: HashSet<(Pubkey, Pubkey)> = HashSet::new(); for (to, msgs) in push_messages { // 8 bytes for encoding the length of the vector. - bytes += 8 + msgs - .iter() - .map(CrdsValue::bincode_serialized_size) - .sum::(); + bytes += 8 + msgs.iter().map(CrdsValue::serialized_size).sum::(); num_msgs += 1; let origins: HashSet<_> = network .get(&to) @@ -420,7 +416,12 @@ fn network_run_push( pruned.insert((from, *prune_key)); } - bytes += serialized_size(&prune_keys).unwrap() as usize; + let prune_keys_size = wincode::serialized_size(&prune_keys).unwrap(); + assert_eq!( + prune_keys_size, + bincode::serialized_size(&prune_keys).unwrap() + ); + bytes += prune_keys_size as usize; delivered += 1; network @@ -575,7 +576,7 @@ fn network_run_pull( .iter() .map(|f| f.filter.bits.len() as usize / 8) .sum::(); - bytes += caller_info.bincode_serialized_size(); + bytes += caller_info.serialized_size(); let requests: Vec<_> = filters .into_iter() .map(|filter| PullRequest { @@ -603,10 +604,7 @@ fn network_run_pull( }) .unwrap(); // 8 bytes for encoding the length of the vector. - bytes += 8 + rsp - .iter() - .map(CrdsValue::bincode_serialized_size) - .sum::(); + bytes += 8 + rsp.iter().map(CrdsValue::serialized_size).sum::(); msgs += rsp.len(); if let Some(node) = network.get(&from) { let mut stats = ProcessPullStats::default(); diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index 29c9207df50..1ec39a13cf7 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -6142,6 +6142,7 @@ dependencies = [ "serde", "solana-sanitize", "solana-time-utils", + "wincode", ] [[package]] @@ -7058,9 +7059,7 @@ dependencies = [ "agave-logger", "agave-random", "arc-swap", - "arrayvec", "assert_matches", - "bincode", "bv", "crossbeam-channel", "flate2", @@ -7104,6 +7103,7 @@ dependencies = [ "solana-version", "solana-vote", "solana-vote-program", + "solana-wincode-varint", "static_assertions", "thiserror 2.0.18", "wincode", @@ -10040,6 +10040,8 @@ dependencies = [ "serde", "solana-sanitize", "solana-serde-varint", + "solana-wincode-varint", + "wincode", ] [[package]] @@ -10121,6 +10123,15 @@ dependencies = [ "solana-vote-interface", ] +[[package]] +name = "solana-wincode-varint" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6679e72a01dcfe7ff180f0e9d3a0d165e4e9664426930014f2702e7571c259c7" +dependencies = [ + "wincode", +] + [[package]] name = "solana-zero-copy" version = "1.0.0" @@ -11640,6 +11651,7 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37095eb18dd6254c66217edc61a29d83d51f8818de8a2ffe88e4584ad73fb5f9" dependencies = [ + "bv", "bytes", "pastey", "proc-macro2", diff --git a/version/Cargo.toml b/version/Cargo.toml index 71aeaf4cd80..48466aeb6f0 100644 --- a/version/Cargo.toml +++ b/version/Cargo.toml @@ -33,6 +33,8 @@ solana-frozen-abi-macro = { workspace = true, optional = true, features = [ ] } solana-sanitize = { workspace = true } solana-serde-varint = { workspace = true } +solana-wincode-varint = { workspace = true } +wincode = { workspace = true } [dev-dependencies] bincode = { workspace = true } diff --git a/version/src/v4.rs b/version/src/v4.rs index 4b79e94b99f..88ca9b81132 100644 --- a/version/src/v4.rs +++ b/version/src/v4.rs @@ -4,7 +4,13 @@ use { serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _, ser::Error as _}, solana_sanitize::Sanitize, solana_serde_varint as serde_varint, - std::{convert::TryInto, fmt, str::FromStr}, + solana_wincode_varint::Leb128Int, + std::{convert::TryInto, fmt, mem::MaybeUninit, str::FromStr}, + wincode::{ + ReadError, ReadResult, SchemaRead, SchemaWrite, WriteError, WriteResult, + config::Config, + io::{Reader, Writer}, + }, }; #[cfg_attr(feature = "frozen-abi", derive(AbiExample))] @@ -80,9 +86,13 @@ impl FromStr for Prerelease { } #[cfg_attr(feature = "frozen-abi", derive(AbiExample))] -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, SchemaRead, SchemaWrite)] #[serde(transparent)] -struct PackedMinor(#[serde(with = "serde_varint")] u16); +struct PackedMinor( + #[serde(with = "serde_varint")] + #[wincode(with = "Leb128Int")] + u16, +); #[derive(Clone, Debug, PartialEq)] pub enum PackedMinorPackError { @@ -271,18 +281,81 @@ impl fmt::Display for Version { } impl Sanitize for Version {} + +unsafe impl SchemaWrite for Version { + type Src = Version; + + fn size_of(src: &Version) -> WriteResult { + let serialized = Self::to_serialized(src)?; + >::size_of(&serialized) + } + + fn write(writer: impl Writer, src: &Version) -> WriteResult<()> { + let serialized = Self::to_serialized(src)?; + >::write(writer, &serialized) + } +} + +impl Version { + fn to_serialized(src: &Version) -> WriteResult { + let (packed_minor, patch) = PackedMinor::try_pack(src.minor, src.patch, &src.prerelease) + .map_err(|_| WriteError::Custom("Version: invalid minor/patch/prerelease"))?; + let client = u16::try_from(src.client.clone()) + .map_err(|_| WriteError::Custom("Version: invalid client ID"))?; + Ok(SerializedVersion { + major: src.major, + packed_minor, + patch, + commit: src.commit, + feature_set: src.feature_set, + client, + }) + } +} + +unsafe impl<'de, C: Config> SchemaRead<'de, C> for Version { + type Dst = Version; + + fn read(reader: impl Reader<'de>, dst: &mut MaybeUninit) -> ReadResult<()> { + let SerializedVersion { + major, + packed_minor, + patch, + commit, + feature_set, + client, + } = >::get(reader)?; + let (minor, patch, prerelease) = packed_minor + .try_unpack(patch) + .map_err(|_| ReadError::Custom("Version: invalid packed minor"))?; + dst.write(Version { + major, + minor, + patch, + commit, + feature_set, + client: ClientId::from(client), + prerelease, + }); + Ok(()) + } +} + #[cfg_attr(feature = "frozen-abi", derive(AbiExample))] -#[derive(Deserialize, Serialize)] +#[derive(Deserialize, Serialize, SchemaRead, SchemaWrite)] struct SerializedVersion { #[serde(with = "serde_varint")] + #[wincode(with = "Leb128Int")] major: u16, #[serde(rename = "minor")] packed_minor: PackedMinor, #[serde(with = "serde_varint")] + #[wincode(with = "Leb128Int")] patch: u16, commit: u32, feature_set: u32, #[serde(with = "serde_varint")] + #[wincode(with = "Leb128Int")] client: u16, } @@ -351,7 +424,46 @@ impl<'de> Deserialize<'de> for Version { #[cfg(test)] mod tests { - use {super::*, crate::v3}; + use {super::*, crate::v3, rand::Rng}; + + fn random_version(rng: &mut impl Rng) -> Version { + let minor = rng.random::() & PackedMinor::PRERELEASE_MINOR_MAX; + let (prerelease, patch) = match rng.random::() % 4 { + 0 => (Prerelease::Stable, rng.random::()), + 1 => (Prerelease::ReleaseCandidate(rng.random()), 0), + 2 => (Prerelease::Beta(rng.random()), 0), + _ => (Prerelease::Alpha(rng.random()), 0), + }; + Version::new_from_parts( + rng.random(), + minor, + patch, + rng.random(), + rng.random(), + ClientId::from(rng.random::()), + prerelease, + ) + } + + #[test] + fn test_wincode_compatibility() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let version = random_version(&mut rng); + + // Serialize with bincode, deserialize with wincode, check results agree. + let bincode_bytes = bincode::serialize(&version).unwrap(); + let wincode_decoded: Version = wincode::deserialize(&bincode_bytes).unwrap(); + assert_eq!(version, wincode_decoded); + + // Serialize with wincode, deserialize with bincode, check results agree. + let wincode_bytes = wincode::serialize(&version).unwrap(); + let bincode_decoded: Version = bincode::deserialize(&wincode_bytes).unwrap(); + assert_eq!(version, bincode_decoded); + + assert_eq!(bincode_bytes, wincode_bytes); + } + } #[test] fn test_prerelease_patch_is_valid() { From 11750681b3cef59ed066bca6c33d384d13bd684e Mon Sep 17 00:00:00 2001 From: Brooks Date: Fri, 22 May 2026 16:28:13 -0400 Subject: [PATCH 043/576] Removes AccountsDb::shrink_paths (#12699) --- accounts-db/src/accounts_db.rs | 10 +----- .../src/accounts_db/accounts_db_config.rs | 3 -- core/src/validator.rs | 5 --- ledger-tool/src/args.rs | 1 - validator/src/cli.rs | 1 + validator/src/commands/run/execute.rs | 32 ------------------- 6 files changed, 2 insertions(+), 50 deletions(-) diff --git a/accounts-db/src/accounts_db.rs b/accounts-db/src/accounts_db.rs index 119eb49b4be..ccd33f8d2da 100644 --- a/accounts-db/src/accounts_db.rs +++ b/accounts-db/src/accounts_db.rs @@ -902,8 +902,6 @@ pub struct AccountsDb { /// directory for bank hash details files bank_hash_details_dir: PathBuf, - shrink_paths: Vec, - /// Directory of paths this accounts_db needs to hold/remove #[allow(dead_code)] pub temp_paths: Option>, @@ -1060,11 +1058,6 @@ impl AccountsDb { (paths, None) }; - let shrink_paths = accounts_db_config - .shrink_paths - .clone() - .unwrap_or_else(|| paths.clone()); - let read_cache_size = accounts_db_config.read_cache_limit_bytes.unwrap_or(( Self::DEFAULT_MAX_READ_ONLY_CACHE_DATA_SIZE_LO, Self::DEFAULT_MAX_READ_ONLY_CACHE_DATA_SIZE_HI, @@ -1105,7 +1098,6 @@ impl AccountsDb { paths, bank_hash_details_dir: accounts_db_config.bank_hash_details_dir, temp_paths, - shrink_paths, skip_initial_hash_calc: accounts_db_config.skip_initial_hash_calc, ancient_append_vec_offset: accounts_db_config .ancient_append_vec_offset @@ -3015,7 +3007,7 @@ impl AccountsDb { old_store: Arc, size: u64, ) -> ShrinkInProgress<'_> { - let shrunken_store = self.create_store(slot, size, "shrink", self.shrink_paths.as_slice()); + let shrunken_store = self.create_store(slot, size, "shrink", self.paths.as_slice()); self.storage .shrinking_in_progress(slot, old_store, shrunken_store) } diff --git a/accounts-db/src/accounts_db/accounts_db_config.rs b/accounts-db/src/accounts_db/accounts_db_config.rs index a7e0a577162..32927228b69 100644 --- a/accounts-db/src/accounts_db/accounts_db_config.rs +++ b/accounts-db/src/accounts_db/accounts_db_config.rs @@ -17,7 +17,6 @@ pub struct AccountsDbConfig { pub index: Option, pub account_indexes: Option, pub bank_hash_details_dir: PathBuf, - pub shrink_paths: Option>, pub shrink_ratio: AccountShrinkThreshold, /// The low and high watermark sizes for the read cache, in bytes. /// If None, defaults will be used. @@ -48,7 +47,6 @@ pub const ACCOUNTS_DB_CONFIG_FOR_TESTING: AccountsDbConfig = AccountsDbConfig { index: Some(ACCOUNTS_INDEX_CONFIG_FOR_TESTING), account_indexes: None, bank_hash_details_dir: PathBuf::new(), // tests don't use bank hash details - shrink_paths: None, shrink_ratio: DEFAULT_ACCOUNTS_SHRINK_THRESHOLD_OPTION, read_cache_limit_bytes: None, read_cache_evict_sample_size: None, @@ -69,7 +67,6 @@ pub const ACCOUNTS_DB_CONFIG_FOR_BENCHMARKS: AccountsDbConfig = AccountsDbConfig index: Some(ACCOUNTS_INDEX_CONFIG_FOR_BENCHMARKS), account_indexes: None, bank_hash_details_dir: PathBuf::new(), // benches don't use bank hash details - shrink_paths: None, shrink_ratio: DEFAULT_ACCOUNTS_SHRINK_THRESHOLD_OPTION, read_cache_limit_bytes: None, read_cache_evict_sample_size: None, diff --git a/core/src/validator.rs b/core/src/validator.rs index 0b6545ccbd2..6a75e4b4400 100644 --- a/core/src/validator.rs +++ b/core/src/validator.rs @@ -2903,11 +2903,6 @@ fn cleanup_accounts_paths(config: &ValidatorConfig) { for account_path in &config.account_paths { move_and_async_delete_path_contents(account_path); } - if let Some(shrink_paths) = &config.accounts_db_config.shrink_paths { - for shrink_path in shrink_paths { - move_and_async_delete_path_contents(shrink_path); - } - } } fn validate_account_paths(config: &ValidatorConfig) -> std::io::Result<()> { diff --git a/ledger-tool/src/args.rs b/ledger-tool/src/args.rs index 70a4ba34939..5cfdd36b73e 100644 --- a/ledger-tool/src/args.rs +++ b/ledger-tool/src/args.rs @@ -362,7 +362,6 @@ pub fn get_accounts_db_config( index: Some(accounts_index_config), account_indexes: None, bank_hash_details_dir: ledger_tool_ledger_path, - shrink_paths: None, shrink_ratio: AccountShrinkThreshold::default(), read_cache_limit_bytes: None, read_cache_evict_sample_size: None, diff --git a/validator/src/cli.rs b/validator/src/cli.rs index c063bd4c115..e03db4cab24 100644 --- a/validator/src/cli.rs +++ b/validator/src/cli.rs @@ -137,6 +137,7 @@ fn deprecated_arguments() -> Vec { .takes_value(true) .multiple(true) .help("Path to accounts shrink path which can hold a compacted account set."), + usage_warning: "Shrink paths are no longer used.", ); add_arg!( // deprecated in v4.2.0; the `mmap` value was deprecated in v4.0.0, and now mmap mode has diff --git a/validator/src/commands/run/execute.rs b/validator/src/commands/run/execute.rs index 4506aea6110..761427610aa 100644 --- a/validator/src/commands/run/execute.rs +++ b/validator/src/commands/run/execute.rs @@ -656,26 +656,6 @@ pub fn execute( const MB: usize = 1_024 * 1_024; - let account_shrink_paths: Option> = - values_t!(matches, "account_shrink_path", String) - .map(|shrink_paths| shrink_paths.into_iter().map(PathBuf::from).collect()) - .ok(); - let account_shrink_paths = account_shrink_paths - .as_ref() - .map(|paths| { - create_and_canonicalize_directories(paths) - .map_err(|err| format!("unable to access account shrink path: {err}")) - }) - .transpose()?; - - let (account_shrink_run_paths, account_shrink_snapshot_paths) = account_shrink_paths - .map(|paths| { - create_all_accounts_run_and_snapshot_dirs(&paths) - .map_err(|err| format!("unable to create account subdirectories: {err}")) - }) - .transpose()? - .unzip(); - let read_cache_limit_bytes = values_of::(matches, "accounts_db_read_cache_limit").map(|limits| { match limits.len() { @@ -704,7 +684,6 @@ pub fn execute( index: Some(accounts_index_config), account_indexes: Some(account_indexes.clone()), bank_hash_details_dir: ledger_path.clone(), - shrink_paths: account_shrink_run_paths, shrink_ratio, read_cache_limit_bytes, read_cache_evict_sample_size: None, @@ -759,17 +738,6 @@ pub fn execute( create_all_accounts_run_and_snapshot_dirs(&account_paths) .map_err(|err| format!("unable to create account directories: {err}"))?; - // These snapshot paths are only used for initial clean up, add in shrink paths if they exist. - let account_snapshot_paths = - if let Some(account_shrink_snapshot_paths) = account_shrink_snapshot_paths { - account_snapshot_paths - .into_iter() - .chain(account_shrink_snapshot_paths) - .collect() - } else { - account_snapshot_paths - }; - let snapshot_config = new_snapshot_config( matches, &ledger_path, From c4de1a5800890bf122b9b2abdda342609727838a Mon Sep 17 00:00:00 2001 From: kirill lykov Date: Sun, 24 May 2026 09:26:02 +0200 Subject: [PATCH 044/576] remove libpoh-simd.so loading (#12596) remove `libpoh-simd.so` loading from code and scripts libpoh-simd logic was introduced in https://github.com/solana-labs/solana/pull/10604 to speed up poh computations when avx2/512 instructions are available. Currently, this logic is not used by anyone. Hence, in this PR we removes all the libpoh-simd code from relevant crates and scripts. --- Cargo.lock | 24 ---- Cargo.toml | 1 - dev-bins/Cargo.lock | 24 ---- entry/Cargo.toml | 1 - entry/src/entry.rs | 179 +------------------------- fetch-perf-libs.sh | 63 --------- net/net.sh | 2 +- net/remote/remote-node.sh | 2 - perf/src/lib.rs | 1 - perf/src/perf_libs.rs | 25 ---- poh-bench/src/main.rs | 46 +------ programs/sbf/Cargo.lock | 24 ---- scripts/cargo-install-all.sh | 13 +- validator/src/commands/run/execute.rs | 1 - 14 files changed, 6 insertions(+), 400 deletions(-) delete mode 100755 fetch-perf-libs.sh delete mode 100644 perf/src/perf_libs.rs diff --git a/Cargo.lock b/Cargo.lock index b6eba8146ef..67260406832 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2567,29 +2567,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "dlopen2" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" -dependencies = [ - "dlopen2_derive", - "libc", - "once_cell", - "winapi", -] - -[[package]] -name = "dlopen2_derive" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "788160fb30de9cdd857af31c6a2675904b16ece8fc2737b2c7127ba368c9d0f4" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "downcast" version = "0.11.0" @@ -8125,7 +8102,6 @@ dependencies = [ "assert_matches", "criterion", "crossbeam-channel", - "dlopen2", "log", "num_cpus", "proptest", diff --git a/Cargo.toml b/Cargo.toml index 01cd828065e..1c78e22ec55 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -229,7 +229,6 @@ derive_more = { version = "2.1.1", features = ["full"] } dialoguer = "0.12.0" digest = "0.10.7" dirs-next = "2.0.0" -dlopen2 = "0.8.2" dyn-clone = "1.0.20" eager = "0.1.0" ed25519-dalek = "=1.0.1" diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 8fa25e973f7..ad47c8a0e32 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -2157,29 +2157,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "dlopen2" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" -dependencies = [ - "dlopen2_derive", - "libc", - "once_cell", - "winapi", -] - -[[package]] -name = "dlopen2_derive" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "downcast" version = "0.11.0" @@ -6937,7 +6914,6 @@ version = "4.2.0-alpha.0" dependencies = [ "agave-votor-messages", "crossbeam-channel", - "dlopen2", "log", "num_cpus", "rayon", diff --git a/entry/Cargo.toml b/entry/Cargo.toml index 6c23e0f8a39..2af48e6765b 100644 --- a/entry/Cargo.toml +++ b/entry/Cargo.toml @@ -23,7 +23,6 @@ dev-context-only-utils = [] [dependencies] agave-votor-messages = { workspace = true } crossbeam-channel = { workspace = true } -dlopen2 = { workspace = true } log = { workspace = true } num_cpus = { workspace = true } rayon = { workspace = true } diff --git a/entry/src/entry.rs b/entry/src/entry.rs index 60ffbad5b00..5f713afda3e 100644 --- a/entry/src/entry.rs +++ b/entry/src/entry.rs @@ -5,7 +5,6 @@ use { crate::poh::Poh, crossbeam_channel::{Receiver, Sender}, - dlopen2::symbor::{Container, SymBorApi, Symbol}, log::*, rayon::{ThreadPool, prelude::*}, smallvec::SmallVec, @@ -17,68 +16,13 @@ use { solana_signature::Signature, solana_transaction::{Transaction, versioned::VersionedTransaction}, solana_transaction_error::{TransactionError, TransactionResult as Result}, - std::{ - ffi::OsStr, - iter::repeat_with, - sync::{Once, OnceLock}, - time::Instant, - }, + std::{iter::repeat_with, time::Instant}, wincode::{SchemaRead, SchemaWrite, containers::Vec as WincodeVec, len::BincodeLen}, }; pub type EntrySender = Sender>; pub type EntryReceiver = Receiver>; -static API: OnceLock> = OnceLock::new(); - -pub fn init_poh() { - init(OsStr::new("libpoh-simd.so")); -} - -fn init(name: &OsStr) { - static INIT_HOOK: Once = Once::new(); - - info!("Loading {name:?}"); - INIT_HOOK.call_once(|| { - let path; - let lib_name = if let Some(perf_libs_path) = solana_perf::perf_libs::locate_perf_libs() { - solana_perf::perf_libs::append_to_ld_library_path( - perf_libs_path.to_str().unwrap_or("").to_string(), - ); - path = perf_libs_path.join(name); - path.as_os_str() - } else { - name - }; - - match unsafe { Container::load(lib_name) } { - Ok(api) => _ = API.set(api), - Err(err) => error!("Unable to load {lib_name:?}: {err}"), - } - }) -} - -pub fn api() -> Option<&'static Container>> { - { - static INIT_HOOK: Once = Once::new(); - INIT_HOOK.call_once(|| { - if std::env::var("TEST_PERF_LIBS").is_ok() { - init_poh() - } - }); - } - - API.get() -} - -#[derive(SymBorApi)] -pub struct Api<'a> { - pub poh_verify_many_simd_avx512skx: - Symbol<'a, unsafe extern "C" fn(hashes: *mut u8, num_hashes: *const u64)>, - pub poh_verify_many_simd_avx2: - Symbol<'a, unsafe extern "C" fn(hashes: *mut u8, num_hashes: *const u64)>, -} - pub const MAX_DATA_SHREDS_SIZE: usize = DEFAULT_MAX_DATA_SHREDS_PER_SLOT as usize * solana_packet::PACKET_DATA_SIZE; pub type MaxDataShredsLen = BincodeLen; @@ -480,20 +424,6 @@ where }) } -fn compare_hashes(computed_hash: Hash, ref_entry: &EntryVerificationData) -> bool { - let actual = if ref_entry.num_transactions != 0 { - let tx_hash = hash_signatures(&ref_entry.signatures); - let mut poh = Poh::new(computed_hash, None); - poh.record(tx_hash).unwrap().hash - } else if ref_entry.num_hashes > 0 { - let mut poh = Poh::new(computed_hash, None); - poh.tick().unwrap().hash - } else { - computed_hash - }; - actual == ref_entry.hash -} - pub fn verify_entries_cpu_in_pool( entries: &[EntryVerificationData], start_hash: &Hash, @@ -531,110 +461,11 @@ fn verify_entries_cpu_generic( } } -fn verify_entries_cpu_x86_simd( - entries: &[EntryVerificationData], - start_hash: &Hash, - simd_len: usize, -) -> EntryVerificationState { - use solana_hash::HASH_BYTES; - let now = Instant::now(); - let genesis = [EntryVerificationData { - num_hashes: 0, - hash: *start_hash, - num_transactions: 0, - signatures: Vec::new(), - }]; - - let aligned_len = entries.len().div_ceil(simd_len) * simd_len; - let mut hashes_bytes = vec![0u8; HASH_BYTES * aligned_len]; - genesis - .iter() - .chain(entries) - .enumerate() - .for_each(|(i, entry)| { - if i < entries.len() { - let start = i * HASH_BYTES; - let end = start + HASH_BYTES; - hashes_bytes[start..end].copy_from_slice(&entry.hash.to_bytes()); - } - }); - let mut hashes_chunked: Vec<_> = hashes_bytes.chunks_mut(simd_len * HASH_BYTES).collect(); - - let mut num_hashes: Vec = entries - .iter() - .map(|entry| entry.num_hashes.saturating_sub(1)) - .collect(); - num_hashes.resize(aligned_len, 0); - let num_hashes: Vec<_> = num_hashes.chunks(simd_len).collect(); - - let res = hashes_chunked - .par_iter_mut() - .zip(num_hashes) - .enumerate() - .all(|(i, (chunk, num_hashes))| { - match simd_len { - 8 => unsafe { - (api().unwrap().poh_verify_many_simd_avx2)( - chunk.as_mut_ptr(), - num_hashes.as_ptr(), - ); - }, - 16 => unsafe { - (api().unwrap().poh_verify_many_simd_avx512skx)( - chunk.as_mut_ptr(), - num_hashes.as_ptr(), - ); - }, - _ => { - panic!("unsupported simd len: {simd_len}"); - } - } - let entry_start = i * simd_len; - // The last chunk may produce indexes larger than what we have in the reference entries - // because it is aligned to simd_len. - let entry_end = std::cmp::min(entry_start + simd_len, entries.len()); - entries[entry_start..entry_end] - .iter() - .enumerate() - .all(|(j, ref_entry)| { - let start = j * HASH_BYTES; - let end = start + HASH_BYTES; - let hash = <[u8; HASH_BYTES]>::try_from(&chunk[start..end]) - .map(Hash::new_from_array) - .unwrap(); - compare_hashes(hash, ref_entry) - }) - }); - let poh_duration_us = now.elapsed().as_micros() as u64; - EntryVerificationState { - verification_status: res, - poh_duration_us, - } -} - pub fn verify_entries_cpu( entries: &[EntryVerificationData], start_hash: &Hash, ) -> EntryVerificationState { - #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] - let (has_avx2, has_avx512) = ( - is_x86_feature_detected!("avx2"), - is_x86_feature_detected!("avx512f"), - ); - #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] - let (has_avx2, has_avx512) = (false, false); - - if api().is_some() { - if has_avx512 && entries.len() >= 128 { - verify_entries_cpu_x86_simd(entries, start_hash, 16) - } else if has_avx2 && entries.len() >= 48 { - verify_entries_cpu_x86_simd(entries, start_hash, 8) - } else { - verify_entries_cpu_generic(entries, start_hash) - } - } else { - verify_entries_cpu_generic(entries, start_hash) - } + verify_entries_cpu_generic(entries, start_hash) } // an EntrySlice is a slice of Entries @@ -642,7 +473,6 @@ pub trait EntrySlice { /// Verifies the hashes and counts of a slice of transactions are all consistent. fn verify_cpu(&self, start_hash: &Hash) -> EntryVerificationState; fn verify_cpu_generic(&self, start_hash: &Hash) -> EntryVerificationState; - fn verify_cpu_x86_simd(&self, start_hash: &Hash, simd_len: usize) -> EntryVerificationState; fn verify(&self, start_hash: &Hash, thread_pool: &ThreadPool) -> EntryVerificationState; /// Checks that each entry tick has the correct number of hashes. Entry slices do not /// necessarily end in a tick, so `tick_hash_count` is used to carry over the hash count @@ -663,11 +493,6 @@ impl EntrySlice for [Entry] { verify_entries_cpu_generic(&verification_entries, start_hash) } - fn verify_cpu_x86_simd(&self, start_hash: &Hash, simd_len: usize) -> EntryVerificationState { - let verification_entries = entries_to_verification_data(self); - verify_entries_cpu_x86_simd(&verification_entries, start_hash, simd_len) - } - fn verify_cpu(&self, start_hash: &Hash) -> EntryVerificationState { let verification_entries = entries_to_verification_data(self); verify_entries_cpu(&verification_entries, start_hash) diff --git a/fetch-perf-libs.sh b/fetch-perf-libs.sh deleted file mode 100755 index 759d0c72f21..00000000000 --- a/fetch-perf-libs.sh +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env bash - -function installPerfLibSymlinks() { - for dir in target/{debug,release}/{,deps/}; do - mkdir -p "$dir" - ln -sfT "$1" "${dir}perf-libs" - done -} - -if [[ -n "$SOLANA_PERF_LIBS_PATH" ]]; then - mkdir -p target - - ln -sfT "$SOLANA_PERF_LIBS_PATH" target/perf-libs - installPerfLibSymlinks "$SOLANA_PERF_LIBS_PATH" - - exit 0 -fi - -PERF_LIBS_VERSION=v0.19.3 -VERSION=$PERF_LIBS_VERSION-1 - -set -e -cd "$(dirname "$0")" - -if [[ $VERSION != "$(cat target/perf-libs/.version 2> /dev/null)" ]]; then - if [[ $(uname) != Linux ]]; then - echo Note: Performance libraries are only available for Linux - exit 0 - fi - - if [[ $(uname -m) != x86_64 ]]; then - echo Note: Performance libraries are only available for x86_64 architecture - exit 0 - fi - - rm -rf target/perf-libs - mkdir -p target/perf-libs - ( - set -x - cd target/perf-libs - - if [[ -r ~/.cache/solana-perf-$PERF_LIBS_VERSION.tgz ]]; then - cp ~/.cache/solana-perf-$PERF_LIBS_VERSION.tgz solana-perf.tgz - else - curl -L --retry 5 --retry-delay 2 --retry-connrefused -o solana-perf.tgz \ - https://github.com/solana-labs/solana-perf-libs/releases/download/$PERF_LIBS_VERSION/solana-perf.tgz - fi - tar zxvf solana-perf.tgz - - if [[ ! -r ~/.cache/solana-perf-$PERF_LIBS_VERSION.tgz ]]; then - # Save it for next time - mkdir -p ~/.cache - mv solana-perf.tgz ~/.cache/solana-perf-$PERF_LIBS_VERSION.tgz - fi - echo "$VERSION" > .version - ) - - # Setup symlinks so the perf-libs/ can be found from all binaries run out of - # target/ - installPerfLibSymlinks ../perf-libs -fi - -exit 0 diff --git a/net/net.sh b/net/net.sh index 69d0cd4470b..eb615bca60a 100755 --- a/net/net.sh +++ b/net/net.sh @@ -262,7 +262,7 @@ syncScripts() { local remoteSolanaHome="${remoteHome}/solana" rsync -vPrc -e "ssh ${sshOptions[*]}" \ --exclude 'net/log*' \ - "$SOLANA_ROOT"/{fetch-perf-libs.sh,fetch-programs.sh,fetch-core-bpf.sh,fetch-spl.sh,scripts,net,multinode-demo} \ + "$SOLANA_ROOT"/{fetch-programs.sh,fetch-core-bpf.sh,fetch-spl.sh,scripts,net,multinode-demo} \ "$ipAddress":"$remoteSolanaHome"/ > /dev/null } diff --git a/net/remote/remote-node.sh b/net/remote/remote-node.sh index 6b2de4a9f5b..a89a3851602 100755 --- a/net/remote/remote-node.sh +++ b/net/remote/remote-node.sh @@ -79,8 +79,6 @@ local|tar|skip) PATH="$HOME"/.cargo/bin:"$PATH" export USE_INSTALL=1 - ./fetch-perf-libs.sh - cat >> ~/solana/on-reboot < Option { - let exe = env::current_exe().expect("Unable to get executable path"); - let perf_libs = exe.parent().unwrap().join("perf-libs"); - if perf_libs.is_dir() { - info!("perf-libs found at {perf_libs:?}"); - return Some(perf_libs); - } - warn!("{perf_libs:?} does not exist"); - None -} - -pub fn append_to_ld_library_path(mut ld_library_path: String) { - if let Ok(env_value) = env::var("LD_LIBRARY_PATH") { - ld_library_path.push(':'); - ld_library_path.push_str(&env_value); - } - info!("setting ld_library_path to: {ld_library_path:?}"); - // Safety: caller (init_poh) is executed before any spawned threads might access the environment - unsafe { env::set_var("LD_LIBRARY_PATH", ld_library_path) } -} diff --git a/poh-bench/src/main.rs b/poh-bench/src/main.rs index f1282a9b579..fdc9bda398b 100644 --- a/poh-bench/src/main.rs +++ b/poh-bench/src/main.rs @@ -1,10 +1,7 @@ #![allow(clippy::arithmetic_side_effects)] -#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] -use solana_entry::entry::{self, EntrySlice, create_ticks, init_poh}; -#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] -use solana_entry::entry::{EntrySlice, create_ticks, init_poh}; use { clap::{Arg, Command, crate_description, crate_name}, + solana_entry::entry::{EntrySlice, create_ticks}, solana_measure::measure::Measure, solana_sha256_hasher::hash, }; @@ -72,7 +69,6 @@ fn main() { .thread_name(|i| format!("solPohBench{i:02}")) .build() .expect("new rayon threadpool"); - init_poh(); while num_entries <= max_num_entries as usize { let mut time = Measure::start("time"); for _ in 0..iterations { @@ -89,46 +85,6 @@ fn main() { time.as_us() / iterations as u64 ); - // A target_arch check is required here since calling - // is_x86_feature_detected from a non-x86_64 arch results in a build - // error. - #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] - { - if is_x86_feature_detected!("avx2") && entry::api().is_some() { - let mut time = Measure::start("time"); - for _ in 0..iterations { - assert!(thread_pool.install(|| { - ticks[..num_entries] - .verify_cpu_x86_simd(&start_hash, 8) - .status() - })); - } - time.stop(); - println!( - "{},cpu_simd_avx2,{}", - num_entries, - time.as_us() / iterations as u64 - ); - } - - if is_x86_feature_detected!("avx512f") && entry::api().is_some() { - let mut time = Measure::start("time"); - for _ in 0..iterations { - assert!(thread_pool.install(|| { - ticks[..num_entries] - .verify_cpu_x86_simd(&start_hash, 16) - .status() - })); - } - time.stop(); - println!( - "{},cpu_simd_avx512,{}", - num_entries, - time.as_us() / iterations as u64 - ); - } - } - println!(); num_entries *= 2; } diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index 1ec39a13cf7..8af373a8592 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -2067,29 +2067,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "dlopen2" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" -dependencies = [ - "dlopen2_derive", - "libc", - "once_cell", - "winapi", -] - -[[package]] -name = "dlopen2_derive" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "788160fb30de9cdd857af31c6a2675904b16ece8fc2737b2c7127ba368c9d0f4" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "downcast" version = "0.11.0" @@ -6790,7 +6767,6 @@ version = "4.2.0-alpha.0" dependencies = [ "agave-votor-messages", "crossbeam-channel", - "dlopen2", "log", "num_cpus", "rayon", diff --git a/scripts/cargo-install-all.sh b/scripts/cargo-install-all.sh index 45f0a042c0b..e52475a8409 100755 --- a/scripts/cargo-install-all.sh +++ b/scripts/cargo-install-all.sh @@ -42,7 +42,7 @@ usage: $0 [+] [options] --no-build-end-user-bins Do not build end user binaries. --no-build-platform-tools Do not build solana-platform-tools. --no-build-validator-bins Do not build validator binaries. - --no-perf-libs Do not fetch and install perf-libs. (Note: Not using this flag may require internet at build time) + --no-perf-libs Deprecated no-op. perf-libs are no longer applicable to agave. --no-spl-token Do not fetch and install SPL-Token. (Note: Not using this flag requires internet at build time) --help Show this help information and exit. EOF @@ -64,7 +64,6 @@ noBuildDevBins= noBuildEndUserBins= noBuildPlatformTools= noBuildValidatorBins= -noPerfLibs= noSPLToken= while [[ -n $1 ]]; do @@ -100,7 +99,7 @@ while [[ -n $1 ]]; do noBuildValidatorBins=true shift elif [[ $1 = --no-perf-libs ]]; then - noPerfLibs=true + echo "WARNING: --no-perf-libs has been deprecated and is now a no-op. perf-libs are no longer applicable to agave." >&2 shift elif [[ $1 = --no-spl-token ]]; then noSPLToken=true @@ -237,14 +236,6 @@ for bin in "${DCOU_BINS[@]}"; do cp -fv "dev-bins/target/$buildProfile/$bin" "$installDir"/bin done -if [[ -z "$noPerfLibs" && $OSTYPE != msys ]]; then - ./fetch-perf-libs.sh - - if [[ -d target/perf-libs ]]; then - cp -a target/perf-libs "$installDir"/bin/perf-libs - fi -fi - if [[ -z "$noBuildPlatformTools" ]]; then # shellcheck disable=SC2086 # Don't want to double quote $rust_version "$cargo" $maybeRustVersion build --manifest-path syscalls/gen-syscall-list/Cargo.toml diff --git a/validator/src/commands/run/execute.rs b/validator/src/commands/run/execute.rs index 761427610aa..5e44c5cc3ed 100644 --- a/validator/src/commands/run/execute.rs +++ b/validator/src/commands/run/execute.rs @@ -148,7 +148,6 @@ pub fn execute( solana_metrics::set_host_id(identity_keypair.pubkey().to_string()); solana_metrics::set_panic_hook("validator", Some(String::from(solana_version))); - solana_entry::entry::init_poh(); let bind_addresses = { let parsed = matches From f6ac9bb465f21eb41c78c8e378e0418aecfc2c97 Mon Sep 17 00:00:00 2001 From: Alex Pyattaev Date: Sun, 24 May 2026 21:33:12 +0300 Subject: [PATCH 045/576] remove unnecessary key rotation in PingPong (#12585) Instead of periodic rotation gossip and repair will now make unique tokens for every Ping they send. Validation and wire format stay the same. Invalidates Ping token once success is observed. Add regression test for repair to make sure it does not break. This also removes dep on siphasher crate, and reduces complexity of all protocols using pingpong. --- Cargo.lock | 1 - Cargo.toml | 1 - core/src/repair/serve_repair.rs | 46 ++++++++++++++++--- dev-bins/Cargo.lock | 1 - gossip/Cargo.toml | 1 - gossip/src/cluster_info.rs | 2 - gossip/src/crds_gossip.rs | 2 - gossip/src/crds_gossip_pull.rs | 2 - gossip/src/crds_gossip_push.rs | 2 - gossip/src/ping_pong.rs | 79 ++++++++++----------------------- gossip/tests/crds_gossip.rs | 2 - programs/sbf/Cargo.lock | 1 - 12 files changed, 65 insertions(+), 75 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 67260406832..b1f584dc582 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8510,7 +8510,6 @@ dependencies = [ "serde-big-array", "serde_bytes", "serial_test", - "siphasher 1.0.2", "solana-bloom", "solana-client", "solana-clock", diff --git a/Cargo.toml b/Cargo.toml index 1c78e22ec55..4ba6d3a3976 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -332,7 +332,6 @@ serial_test = "3.4.0" shaq = { version = "3.0.0" } shuttle = "0.7.1" signal-hook = "0.4.4" -siphasher = "1.0.2" slab = "0.4.12" slotmap = "1.0.7" smallvec = { version = "1.15.1", default-features = false, features = ["union"] } diff --git a/core/src/repair/serve_repair.rs b/core/src/repair/serve_repair.rs index 9dec36b39c0..1e172e6de4d 100644 --- a/core/src/repair/serve_repair.rs +++ b/core/src/repair/serve_repair.rs @@ -1339,8 +1339,6 @@ impl ServeRepair { assert!(REPAIR_PING_CACHE_RATE_LIMIT_DELAY > Duration::from_millis(REPAIR_MS)); let mut ping_cache = PingCache::new( - &mut rand::rng(), - Instant::now(), REPAIR_PING_CACHE_TTL, REPAIR_PING_CACHE_RATE_LIMIT_DELAY, REPAIR_PING_CACHE_CAPACITY, @@ -1956,7 +1954,10 @@ mod tests { solana_pubkey::Pubkey, solana_runtime::bank::Bank, solana_time_utils::timestamp, - std::{io::Cursor, net::Ipv4Addr}, + std::{ + io::Cursor, + net::{Ipv4Addr, SocketAddrV4}, + }, }; fn discard_malformed_repair_requests( @@ -1998,8 +1999,6 @@ mod tests { let remote_keypair = Keypair::new(); let from_addr = socketaddr!(Ipv4Addr::LOCALHOST, 1234); let mut ping_cache = PingCache::new( - &mut rand::rng(), - Instant::now(), REPAIR_PING_CACHE_TTL, REPAIR_PING_CACHE_RATE_LIMIT_DELAY, REPAIR_PING_CACHE_CAPACITY, @@ -3173,4 +3172,41 @@ mod tests { response.push((request_slot, Hash::new_unique())); assert!(!repair.verify_response(&AncestorHashesResponse::Hashes(response))); } + + // A second check() within REPAIR_PING_CACHE_RATE_LIMIT_DELAY must not generate + // a new ping. If it did, it would overwrite the stored token and invalidate the Pong, + // making Ping fail for no reason. + #[test] + fn test_repair_no_ping_overwrite_within_rate_limit_delay() { + let mut rng = rand::rng(); + let this_node = Keypair::new(); + let remote_socket = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 8001)); + let remote_keypair = Keypair::new(); + let remote_node = (remote_keypair.pubkey(), remote_socket); + let mut cache = PingCache::new( + REPAIR_PING_CACHE_TTL, + REPAIR_PING_CACHE_RATE_LIMIT_DELAY, + REPAIR_PING_CACHE_CAPACITY, + ); + let now = Instant::now(); + + let (_, ping1) = cache.check(&mut rng, &this_node, now, remote_node); + let ping1 = ping1.expect("should generate ping for unknown node"); + + // Second check within REPAIR_PING_CACHE_RATE_LIMIT_DELAY must not generate + // a new ping — that would overwrite the stored hash and invalidate the in-flight pong. + let within_delay = now + REPAIR_PING_CACHE_RATE_LIMIT_DELAY - Duration::from_millis(1); + let (_, ping2) = cache.check(&mut rng, &this_node, within_delay, remote_node); + assert!( + ping2.is_none(), + "must not generate a second ping within REPAIR_PING_CACHE_RATE_LIMIT_DELAY" + ); + + // Pong for ping1 must still be valid — token was not overwritten. + let pong1 = solana_gossip::ping_pong::Pong::new(&ping1, &remote_keypair); + assert!( + cache.add(&pong1, remote_socket, within_delay), + "pong for original ping must still be valid — token was not overwritten" + ); + } } diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index ad47c8a0e32..1d6d2281a37 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -7217,7 +7217,6 @@ dependencies = [ "serde", "serde-big-array", "serde_bytes", - "siphasher 1.0.2", "solana-bloom", "solana-client", "solana-clock", diff --git a/gossip/Cargo.toml b/gossip/Cargo.toml index 280d7bcca45..ed5e2dad4e5 100644 --- a/gossip/Cargo.toml +++ b/gossip/Cargo.toml @@ -53,7 +53,6 @@ rayon = { workspace = true } serde = { workspace = true } serde-big-array = { workspace = true } serde_bytes = { workspace = true } -siphasher = { workspace = true } solana-bloom = { workspace = true } solana-client = { workspace = true } solana-clock = { workspace = true } diff --git a/gossip/src/cluster_info.rs b/gossip/src/cluster_info.rs index feb5a5e5e19..1a35aba1f92 100644 --- a/gossip/src/cluster_info.rs +++ b/gossip/src/cluster_info.rs @@ -187,8 +187,6 @@ impl ClusterInfo { outbound_budget: DataBudget::default(), my_contact_info: RwLock::new(contact_info), ping_cache: Mutex::new(PingCache::new( - &mut rand::rng(), - Instant::now(), GOSSIP_PING_CACHE_TTL, GOSSIP_PING_CACHE_RATE_LIMIT_DELAY, GOSSIP_PING_CACHE_CAPACITY, diff --git a/gossip/src/crds_gossip.rs b/gossip/src/crds_gossip.rs index e3eb3d06cdc..73b1e5fb9d7 100644 --- a/gossip/src/crds_gossip.rs +++ b/gossip/src/crds_gossip.rs @@ -414,8 +414,6 @@ mod test { ) .unwrap(); let ping_cache = PingCache::new( - &mut rand::rng(), - Instant::now(), Duration::from_secs(20 * 60), // ttl Duration::from_secs(20 * 60) / 64, // rate_limit_delay 128, // capacity diff --git a/gossip/src/crds_gossip_pull.rs b/gossip/src/crds_gossip_pull.rs index 015abfdb907..282e606e65e 100644 --- a/gossip/src/crds_gossip_pull.rs +++ b/gossip/src/crds_gossip_pull.rs @@ -730,8 +730,6 @@ pub(crate) mod tests { fn new_ping_cache() -> PingCache { PingCache::new( - &mut rand::rng(), - Instant::now(), Duration::from_secs(20 * 60), // ttl Duration::from_secs(20 * 60) / 64, // rate_limit_delay 128, // capacity diff --git a/gossip/src/crds_gossip_push.rs b/gossip/src/crds_gossip_push.rs index c8f2346efd9..5f9353dd247 100644 --- a/gossip/src/crds_gossip_push.rs +++ b/gossip/src/crds_gossip_push.rs @@ -294,8 +294,6 @@ mod tests { fn new_ping_cache() -> PingCache { PingCache::new( - &mut rand::rng(), - Instant::now(), Duration::from_secs(20 * 60), // ttl Duration::from_secs(20 * 60) / 64, // rate_limit_delay 128, // capacity diff --git a/gossip/src/ping_pong.rs b/gossip/src/ping_pong.rs index 8e7345ea1b6..c119a4c56f6 100644 --- a/gossip/src/ping_pong.rs +++ b/gossip/src/ping_pong.rs @@ -4,7 +4,6 @@ use { rand::{CryptoRng, Rng}, serde::{Deserialize, Serialize}, serde_big_array::BigArray, - siphasher::sip::SipHasher24, solana_hash::Hash, solana_keypair::{Keypair, signable::Signable}, solana_pubkey::Pubkey, @@ -13,14 +12,12 @@ use { solana_signer::Signer, std::{ borrow::Cow, - hash::{Hash as _, Hasher}, net::{IpAddr, SocketAddr}, time::{Duration, Instant}, }, wincode::{SchemaRead, SchemaWrite}, }; -const KEY_REFRESH_CADENCE: Duration = Duration::from_secs(60); const PING_PONG_HASH_PREFIX: &[u8] = "SOLANA_PING_PONG".as_bytes(); const PONG_SIGNATURE_SAMPLE_LEADING_ZEROS: u32 = 5; @@ -55,15 +52,9 @@ pub struct PingCache { ttl: Duration, // Rate limit delay to generate pings for a given address rate_limit_delay: Duration, - // Hashers initialized with random keys, rotated at KEY_REFRESH_CADENCE. - // Because at the moment that the keys are rotated some pings might already - // be in the flight, we need to keep the two most recent hashers. - hashers: [SipHasher24; 2], - // When hashers were last refreshed. - key_refresh: Instant, - // Timestamp of last ping message sent to a remote node. - // Used to rate limit pings to remote nodes. - pings: LruCache<(Pubkey, SocketAddr), Instant>, + // Timestamp and expected pong hash for each pinged remote node. + // Used for rate-limiting and pong validation. + pings: LruCache<(Pubkey, SocketAddr), (Instant, Hash)>, // Verified pong responses from remote nodes. pongs: LruCache<(Pubkey, SocketAddr), Instant>, // Timestamp of last ping message sent to a remote IP. @@ -157,38 +148,27 @@ impl Signable for Pong { } impl PingCache { - pub fn new( - rng: &mut R, - now: Instant, - ttl: Duration, - rate_limit_delay: Duration, - cap: usize, - ) -> Self { + pub fn new(ttl: Duration, rate_limit_delay: Duration, cap: usize) -> Self { // Sanity check ttl/rate_limit_delay assert!(rate_limit_delay <= ttl / 2); Self { ttl, rate_limit_delay, - hashers: std::array::from_fn(|_| SipHasher24::new_with_key(&rng.random())), - key_refresh: now, pings: LruCache::new(cap), pongs: LruCache::new(cap), ping_times: LruCache::new(cap), } } - /// Checks if the pong hash, pubkey and socket match a ping message sent - /// out previously. If so records current timestamp for the remote node and - /// returns true. + /// Checks if the pong hash matches a ping message sent out previously. + /// If so records current timestamp for the remote node and returns true. /// Note: Does not verify the signature. pub fn add(&mut self, pong: &Pong, socket: SocketAddr, now: Instant) -> bool { let remote_node = (pong.pubkey(), socket); - if !self.hashers.iter().copied().any(|hasher| { - let token = make_ping_token::(hasher, &remote_node); - hash_ping_token(&token) == pong.hash - }) { + if !matches!(self.pings.peek(&remote_node), Some((_, h)) if *h == pong.hash) { return false; }; + self.pings.pop(&remote_node); self.pongs.put(remote_node, now); if let Some(sent_time) = self.ping_times.pop(&socket.ip()) && should_report_message_signature( @@ -218,13 +198,23 @@ impl PingCache { ) -> Option> { // Rate limit consecutive pings sent to a remote node. if matches!(self.pings.peek(&remote_node), - Some(&t) if now.saturating_duration_since(t) < self.rate_limit_delay) + Some((t, _)) if now.saturating_duration_since(*t) < self.rate_limit_delay) { return None; } - self.pings.put(remote_node, now); - self.maybe_refresh_key(rng, now); - let token = make_ping_token::(self.hashers[0], &remote_node); + let token = { + let mut token = [0u8; N]; + const FILL: usize = std::mem::size_of::(); + const { assert!(N >= FILL, "N must be >= size_of::()") }; + let entropy: [u8; FILL] = rng.random(); + *token + .first_chunk_mut::() + .expect("token is known to fit FILL bytes") = entropy; + token + }; + // The hash we expect to see in the Pong message + let ping_hash = hash_ping_token(&token); + self.pings.put(remote_node, (now, ping_hash)); self.ping_times.put(remote_node.1.ip(), Instant::now()); Some(Ping::new(token, keypair)) } @@ -265,33 +255,12 @@ impl PingCache { (check, ping) } - fn maybe_refresh_key(&mut self, rng: &mut R, now: Instant) { - if now.checked_duration_since(self.key_refresh) > Some(KEY_REFRESH_CADENCE) { - let hasher = SipHasher24::new_with_key(&rng.random()); - self.hashers[1] = std::mem::replace(&mut self.hashers[0], hasher); - self.key_refresh = now; - } - } - /// Only for tests and simulations. pub fn mock_pong(&mut self, node: Pubkey, socket: SocketAddr, now: Instant) { self.pongs.put((node, socket), now); } } -fn make_ping_token( - mut hasher: SipHasher24, - remote_node: &(Pubkey, SocketAddr), -) -> [u8; N] { - // TODO: Consider including local node's (pubkey, socket-addr). - remote_node.hash(&mut hasher); - let hash = hasher.finish().to_le_bytes(); - debug_assert!(N >= std::mem::size_of::()); - let mut token = [0u8; N]; - token[..std::mem::size_of::()].copy_from_slice(&hash); - token -} - fn hash_ping_token(token: &[u8; N]) -> Hash { solana_sha256_hasher::hashv(&[PING_PONG_HASH_PREFIX, token]) } @@ -330,7 +299,7 @@ mod tests { let mut rng = rand::rng(); let ttl = Duration::from_millis(256); let delay = ttl / 64; - let mut cache = PingCache::new(&mut rng, Instant::now(), ttl, delay, /*cap=*/ 1000); + let mut cache = PingCache::new(ttl, delay, /*cap=*/ 1000); let this_node = Keypair::new(); let sockets: Vec<_> = (1u8..=3) .map(|i| { @@ -498,7 +467,7 @@ mod tests { let ttl = Duration::from_secs(20 * 60); // 20 minutes let delay = ttl / 64; let mut now = Instant::now(); - let mut cache = PingCache::<32>::new(&mut rng, now, ttl, delay, /*cap=*/ 1000); + let mut cache = PingCache::<32>::new(ttl, delay, /*cap=*/ 1000); // Add a pong for the remote node cache.mock_pong(remote_node.0, remote_node.1, now); diff --git a/gossip/tests/crds_gossip.rs b/gossip/tests/crds_gossip.rs index 7d35f5c42fa..f2c98a88bf9 100644 --- a/gossip/tests/crds_gossip.rs +++ b/gossip/tests/crds_gossip.rs @@ -661,8 +661,6 @@ fn build_gossip_thread_pool() -> ThreadPool { fn new_ping_cache() -> Mutex { let ping_cache = PingCache::new( - &mut rand::rng(), - Instant::now(), Duration::from_secs(20 * 60), // ttl Duration::from_secs(20 * 60) / 64, // rate_limit_delay 2048, // capacity diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index 8af373a8592..6828848274d 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -7050,7 +7050,6 @@ dependencies = [ "serde", "serde-big-array", "serde_bytes", - "siphasher 1.0.2", "solana-bloom", "solana-client", "solana-clock", From 6c0aa19a3494b738d5bb74dd6da8bf673552c7a2 Mon Sep 17 00:00:00 2001 From: Yihau Chen Date: Mon, 25 May 2026 19:55:46 +0800 Subject: [PATCH 046/576] ci: add trigger-buildkite-pipeline.yml (#12449) * ci: add trigger-buildkite-pipeline.yml * use env repo * add permission * remove version branches * add concurrency * Update .github/workflows/trigger-buildkite-pipeline.yml Co-authored-by: Nikita Belenkov --------- Co-authored-by: Nikita Belenkov --- .../workflows/trigger-buildkite-pipeline.yml | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 .github/workflows/trigger-buildkite-pipeline.yml diff --git a/.github/workflows/trigger-buildkite-pipeline.yml b/.github/workflows/trigger-buildkite-pipeline.yml new file mode 100644 index 00000000000..6b925ad2f5e --- /dev/null +++ b/.github/workflows/trigger-buildkite-pipeline.yml @@ -0,0 +1,133 @@ +name: Trigger Buildkite Pipeline (Pull Request) + +on: + pull_request_target: + branches: + - master + types: + - opened + - synchronize + - reopened + - labeled + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-pr-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + member_check: + runs-on: ubuntu-24.04 + if: github.repository == 'anza-xyz/agave' && github.event.action != 'labeled' + outputs: + should_trigger: ${{ steps.check.outputs.should_trigger }} + permissions: + contents: read + steps: + - name: Check permission + id: check + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + USERNAME: ${{ github.event.sender.login }} + run: | + ROLE_NAME=$(curl -sS -L \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer $GH_TOKEN" \ + -H "X-GitHub-Api-Version: 2026-03-10" \ + https://api.github.com/repos/$REPO/collaborators/$USERNAME/permission | jq -r ".role_name") + + case "$ROLE_NAME" in + admin|maintain|write|triage) + echo "should_trigger=true" >> $GITHUB_OUTPUT + ;; + *) + echo "should_trigger=false" >> $GITHUB_OUTPUT + exit 1 + ;; + esac + + label_check: + runs-on: ubuntu-24.04 + if: github.repository == 'anza-xyz/agave' && github.event.action == 'labeled' && github.event.label.name == 'CI' + outputs: + should_trigger: ${{ steps.check.outputs.should_trigger }} + permissions: + contents: read + pull-requests: write + steps: + - name: Remove CI label + id: check + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + echo "should_trigger=true" >> $GITHUB_OUTPUT + + curl -sS -X DELETE \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer $GH_TOKEN" \ + "https://api.github.com/repos/$REPO/issues/$PR_NUMBER/labels/CI" \ + || echo "CI label was already removed." + + trigger: + runs-on: ubuntu-24.04 + needs: + - member_check + - label_check + if: >- + always() && + (needs.member_check.outputs.should_trigger == 'true' || needs.label_check.outputs.should_trigger == 'true') + permissions: {} + steps: + - name: Prepare Buildkite pipeline info + id: prepare + env: + BUILDKITE_PIPELINE: ${{ vars.BUILDKITE_PIPELINE }} + COMMIT: ${{ github.event.pull_request.head.sha }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + COMMIT_SHORT="${COMMIT:0:8}" + BRANCH="pull/${PR_NUMBER}/head" + MESSAGE="PR#${PR_NUMBER} - ${COMMIT_SHORT}" + BUILD_META_DATA="{\"pr_number\":\"${PR_NUMBER}\"}" + + echo "branch=$BRANCH" | tee -a $GITHUB_OUTPUT + echo "build_meta_data=$BUILD_META_DATA" | tee -a $GITHUB_OUTPUT + echo "commit=$COMMIT" | tee -a $GITHUB_OUTPUT + echo "message=$MESSAGE" | tee -a $GITHUB_OUTPUT + echo "pipeline=$BUILDKITE_PIPELINE" | tee -a $GITHUB_OUTPUT + echo "pr_number=$PR_NUMBER" | tee -a $GITHUB_OUTPUT + + - name: Trigger a Buildkite Build + uses: "buildkite/trigger-pipeline-action@909fed762c73d5ae2b5d555ab910d66b3fae2670" # v2.4.1 + with: + pipeline: ${{ steps.prepare.outputs.pipeline }} + buildkite_api_access_token: ${{ secrets.BUILDKITE_API_ACCESS_TOKEN }} + branch: "${{ steps.prepare.outputs.branch }}" + build_meta_data: "${{ steps.prepare.outputs.build_meta_data }}" + commit: "${{ steps.prepare.outputs.commit }}" + message: "${{ steps.prepare.outputs.message }}" + + - name: Summary + env: + BRANCH: ${{ steps.prepare.outputs.branch }} + BUILDKITE_PIPELINE: ${{ vars.BUILDKITE_PIPELINE }} + COMMIT: ${{ github.event.pull_request.head.sha }} + MESSAGE: ${{ steps.prepare.outputs.message }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + { + echo "## Buildkite Pipeline Info" + echo "" + echo "| Key | Value |" + echo "|-----|-------|" + echo "| Pipeline | \`$BUILDKITE_PIPELINE\` |" + echo "| Branch | \`$BRANCH\` |" + echo "| Commit | \`$COMMIT\` |" + echo "| PR Number | #$PR_NUMBER |" + echo "| Message | $MESSAGE |" + } >> $GITHUB_STEP_SUMMARY From faee1efb1cc57fb8207cfdfb3351ac9fe23d5345 Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Mon, 25 May 2026 20:04:10 -0400 Subject: [PATCH 047/576] A2A: don't broadcast certificates if unstaked (#12715) --- votor/src/consensus_pool_service.rs | 87 ++++++++++++++++++++++- votor/src/consensus_pool_service/stats.rs | 8 +++ 2 files changed, 92 insertions(+), 3 deletions(-) diff --git a/votor/src/consensus_pool_service.rs b/votor/src/consensus_pool_service.rs index 7c35c442f0b..8e6ffea44a9 100644 --- a/votor/src/consensus_pool_service.rs +++ b/votor/src/consensus_pool_service.rs @@ -84,6 +84,7 @@ impl ConsensusPoolService { fn maybe_update_root_and_send_new_certificates( consensus_pool: &mut ConsensusPool, sharable_banks: &SharableBanks, + my_pubkey: &Pubkey, bls_sender: &Sender, new_finalized_slot: Option, new_certificates_to_send: Vec>, @@ -103,10 +104,45 @@ impl ConsensusPoolService { consensus_pool.maybe_prune(bank.slot()); stats.prune_old_state_called += 1; // Send new certificates to peers - Self::send_certificates(bls_sender, new_certificates_to_send, stats) + Self::send_certificates( + sharable_banks, + my_pubkey, + bls_sender, + new_certificates_to_send, + stats, + ) } fn send_certificates( + sharable_banks: &SharableBanks, + my_pubkey: &Pubkey, + bls_sender: &Sender, + certificates_to_send: Vec>, + stats: &mut ConsensusPoolServiceStats, + ) -> Result<(), AddVoteError> { + let num_certs = certificates_to_send.len(); + if num_certs == 0 { + return Ok(()); + } + // If we are not a staked identity (hot spare / RPC / new validator / failed VAT) + // we should not send out the certificate. A2A quic only accepts connections + // from staked identities + if !Self::is_current_identity_staked(sharable_banks, my_pubkey) { + stats.certificates_skipped_unstaked += num_certs; + return Ok(()); + } + Self::enqueue_certificates(bls_sender, certificates_to_send, stats) + } + + fn is_current_identity_staked(sharable_banks: &SharableBanks, my_pubkey: &Pubkey) -> bool { + sharable_banks + .root() + .current_epoch_staked_nodes() + .get(my_pubkey) + .is_some_and(|stake| *stake > 0) + } + + fn enqueue_certificates( bls_sender: &Sender, certificates_to_send: Vec>, stats: &mut ConsensusPoolServiceStats, @@ -164,6 +200,7 @@ impl ConsensusPoolService { Self::maybe_update_root_and_send_new_certificates( consensus_pool, &ctx.sharable_banks, + &ctx.cluster_info.id(), &ctx.bls_sender, new_finalized_slot, new_certificates_to_send, @@ -265,6 +302,8 @@ impl ConsensusPoolService { stats.standstill = true; standstill_timer = Instant::now(); match Self::send_certificates( + &ctx.sharable_banks, + &ctx.cluster_info.id(), &ctx.bls_sender, consensus_pool.get_certs_for_standstill(), &mut stats, @@ -697,6 +736,7 @@ mod tests { ConsensusPoolService::maybe_update_root_and_send_new_certificates( &mut ctx.consensus_pool, &ctx.sharable_banks, + &ctx.my_pubkey, &ctx.bls_sender, new_finalized_slot, new_certificates_to_send, @@ -778,6 +818,7 @@ mod tests { ConsensusPoolService::maybe_update_root_and_send_new_certificates( &mut ctx.consensus_pool, &ctx.sharable_banks, + &ctx.my_pubkey, &ctx.bls_sender, new_finalized_slot, new_certificates_to_send, @@ -902,6 +943,8 @@ mod tests { let mut stats = ConsensusPoolServiceStats::new(); let result = ConsensusPoolService::send_certificates( + &ctx.sharable_banks, + &ctx.my_pubkey, &ctx.bls_sender, certificates.clone(), &mut stats, @@ -923,6 +966,38 @@ mod tests { } } + #[test] + fn test_send_certificates_skips_unstaked_identity() { + let ctx = TestContext::default(); + let certificates = vec![ + Arc::new(Certificate { + cert_type: CertificateType::Skip(1), + signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), + bitmap: vec![], + }), + Arc::new(Certificate { + cert_type: CertificateType::Skip(2), + signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), + bitmap: vec![], + }), + ]; + let unstaked_identity = Pubkey::new_unique(); + let mut stats = ConsensusPoolServiceStats::new(); + + let result = ConsensusPoolService::send_certificates( + &ctx.sharable_banks, + &unstaked_identity, + &ctx.bls_sender, + certificates, + &mut stats, + ); + + assert!(result.is_ok()); + assert_eq!(stats.certificates_sent.0, 0); + assert_eq!(stats.certificates_skipped_unstaked.0, 2); + assert!(ctx.bls_receiver.try_recv().is_err()); + } + #[test] fn test_send_certificates_channel_disconnected() { let ctx = TestContext::default(); @@ -935,8 +1010,13 @@ mod tests { })]; let mut stats = ConsensusPoolServiceStats::new(); - let result = - ConsensusPoolService::send_certificates(&ctx.bls_sender, certificates, &mut stats); + let result = ConsensusPoolService::send_certificates( + &ctx.sharable_banks, + &ctx.my_pubkey, + &ctx.bls_sender, + certificates, + &mut stats, + ); assert!(matches!(result, Err(AddVoteError::ChannelDisconnected(_)))); } @@ -958,6 +1038,7 @@ mod tests { let result = ConsensusPoolService::maybe_update_root_and_send_new_certificates( &mut ctx.consensus_pool, &ctx.sharable_banks, + &ctx.my_pubkey, &ctx.bls_sender, Some(5), // new finalized slot certificates, diff --git a/votor/src/consensus_pool_service/stats.rs b/votor/src/consensus_pool_service/stats.rs index 3bc559815f9..c35e12898cd 100644 --- a/votor/src/consensus_pool_service/stats.rs +++ b/votor/src/consensus_pool_service/stats.rs @@ -13,6 +13,7 @@ pub(super) struct ConsensusPoolServiceStats { pub(super) add_message_failed: Saturating, pub(super) certificates_sent: Saturating, pub(super) certificates_dropped: Saturating, + pub(super) certificates_skipped_unstaked: Saturating, pub(super) new_finalized_slot: Saturating, pub(super) parent_ready_missed_window: Saturating, pub(super) parent_ready_produce_window: Saturating, @@ -31,6 +32,7 @@ impl ConsensusPoolServiceStats { add_message_failed: Saturating(0), certificates_sent: Saturating(0), certificates_dropped: Saturating(0), + certificates_skipped_unstaked: Saturating(0), new_finalized_slot: Saturating(0), parent_ready_missed_window: Saturating(0), parent_ready_produce_window: Saturating(0), @@ -49,6 +51,7 @@ impl ConsensusPoolServiceStats { add_message_failed: Saturating(add_message_failed), certificates_sent: Saturating(certificates_sent), certificates_dropped: Saturating(certificates_dropped), + certificates_skipped_unstaked: Saturating(certificates_skipped_unstaked), new_finalized_slot: Saturating(new_finalized_slot), parent_ready_missed_window: Saturating(parent_ready_missed_window), parent_ready_produce_window: Saturating(parent_ready_produce_window), @@ -65,6 +68,11 @@ impl ConsensusPoolServiceStats { ("add_message_failed", add_message_failed, i64), ("certificates_sent", certificates_sent, i64), ("certificates_dropped", certificates_dropped, i64), + ( + "certificates_skipped_unstaked", + certificates_skipped_unstaked, + i64 + ), ("new_finalized_slot", new_finalized_slot, i64), ( "parent_ready_missed_window", From 1539e4d3958ef8eda736f54cfd2bd87e52173a19 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 May 2026 09:49:12 +0800 Subject: [PATCH 048/576] chore(deps): bump rpassword from 7.5.2 to 7.5.3 (#12709) * chore(deps): bump rpassword from 7.5.2 to 7.5.3 Bumps [rpassword](https://github.com/conradkleinespel/rpassword) from 7.5.2 to 7.5.3. - [Release notes](https://github.com/conradkleinespel/rpassword/releases) - [Commits](https://github.com/conradkleinespel/rpassword/compare/v7.5.2...v7.5.3) --- updated-dependencies: - dependency-name: rpassword dependency-version: 7.5.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Update all workspaces --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 8 ++++---- dev-bins/Cargo.lock | 20 ++++++++++---------- programs/sbf/Cargo.lock | 10 +++++----- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b1f584dc582..436469c7649 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6027,9 +6027,9 @@ dependencies = [ [[package]] name = "rpassword" -version = "7.5.2" +version = "7.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac5b223d9738ef56e0b98305410be40fa0941bf6036c56f1506751e43552d64" +checksum = "835a57a69104632d64deb0df2e09a69945cd7a6eab4070fc9b1d7e50cf6c3edc" dependencies = [ "libc", "rtoolbox", @@ -6134,7 +6134,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.0", + "windows-sys 0.52.0", ] [[package]] @@ -12063,7 +12063,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix 1.1.4", - "windows-sys 0.61.0", + "windows-sys 0.52.0", ] [[package]] diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 1d6d2281a37..d6230ba87b1 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -532,7 +532,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -543,7 +543,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -2399,7 +2399,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -5264,9 +5264,9 @@ dependencies = [ [[package]] name = "rpassword" -version = "7.5.2" +version = "7.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac5b223d9738ef56e0b98305410be40fa0941bf6036c56f1506751e43552d64" +checksum = "835a57a69104632d64deb0df2e09a69945cd7a6eab4070fc9b1d7e50cf6c3edc" dependencies = [ "libc", "rtoolbox", @@ -5349,7 +5349,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -5407,7 +5407,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -5877,7 +5877,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -10162,7 +10162,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -11139,7 +11139,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index 6828848274d..a4f2653df59 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -5172,9 +5172,9 @@ dependencies = [ [[package]] name = "rpassword" -version = "7.5.2" +version = "7.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac5b223d9738ef56e0b98305410be40fa0941bf6036c56f1506751e43552d64" +checksum = "835a57a69104632d64deb0df2e09a69945cd7a6eab4070fc9b1d7e50cf6c3edc" dependencies = [ "libc", "rtoolbox", @@ -5270,7 +5270,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.0", + "windows-sys 0.52.0", ] [[package]] @@ -5765,7 +5765,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.61.0", + "windows-sys 0.60.2", ] [[package]] @@ -10642,7 +10642,7 @@ dependencies = [ "getrandom 0.3.1", "once_cell", "rustix 1.1.4", - "windows-sys 0.61.0", + "windows-sys 0.52.0", ] [[package]] From 29865eb3dd29fef1bd34b3a612889e5d2c0f6b04 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 May 2026 09:50:52 +0800 Subject: [PATCH 049/576] chore(deps): bump log from 0.4.29 to 0.4.30 (#12710) * chore(deps): bump log from 0.4.29 to 0.4.30 Bumps [log](https://github.com/rust-lang/log) from 0.4.29 to 0.4.30. - [Release notes](https://github.com/rust-lang/log/releases) - [Changelog](https://github.com/rust-lang/log/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-lang/log/compare/0.4.29...0.4.30) --- updated-dependencies: - dependency-name: log dependency-version: 0.4.30 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Update all workspaces --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- ci/xtask/Cargo.lock | 4 ++-- dev-bins/Cargo.lock | 4 ++-- programs/sbf/Cargo.lock | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 436469c7649..0d085b5f310 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4415,9 +4415,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" [[package]] name = "lru" diff --git a/Cargo.toml b/Cargo.toml index 4ba6d3a3976..195984e2d3a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -279,7 +279,7 @@ libsecp256k1 = { version = "0.7.2", default-features = false, features = [ "std", "static-context", ] } -log = "0.4.29" +log = "0.4.30" lru = "0.18.0" lz4 = "1.28.1" memmap2 = "0.9.10" diff --git a/ci/xtask/Cargo.lock b/ci/xtask/Cargo.lock index a3cba5d4ca4..bbdc474d7f3 100644 --- a/ci/xtask/Cargo.lock +++ b/ci/xtask/Cargo.lock @@ -1090,9 +1090,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" [[package]] name = "memchr" diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index d6230ba87b1..715d223b147 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -3859,9 +3859,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" [[package]] name = "lru" diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index a4f2653df59..fb291f6d91e 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -3783,9 +3783,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" [[package]] name = "lru" From d73cb5b8fc8e038bdf7a9fdc40d692b3b5a0dd96 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 May 2026 09:51:44 +0800 Subject: [PATCH 050/576] chore(deps): bump http from 1.4.0 to 1.4.1 (#12717) * chore(deps): bump http from 1.4.0 to 1.4.1 in /dev-bins Bumps [http](https://github.com/hyperium/http) from 1.4.0 to 1.4.1. - [Release notes](https://github.com/hyperium/http/releases) - [Changelog](https://github.com/hyperium/http/blob/master/CHANGELOG.md) - [Commits](https://github.com/hyperium/http/compare/v1.4.0...v1.4.1) --- updated-dependencies: - dependency-name: http dependency-version: 1.4.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Update all workspaces --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 36 ++++++++++++++++++------------------ Cargo.toml | 2 +- ci/xtask/Cargo.lock | 4 ++-- dev-bins/Cargo.lock | 36 ++++++++++++++++++------------------ programs/sbf/Cargo.lock | 36 ++++++++++++++++++------------------ 5 files changed, 57 insertions(+), 57 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0d085b5f310..142eb00af51 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1132,7 +1132,7 @@ checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9" dependencies = [ "base64 0.22.1", "flate2", - "http 1.4.0", + "http 1.4.1", "log", "native-tls", "serde", @@ -1165,7 +1165,7 @@ dependencies = [ "axum-core", "bytes", "futures-util", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "http-body-util", "itoa", @@ -1189,7 +1189,7 @@ checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ "bytes", "futures-core", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "http-body-util", "mime", @@ -3292,7 +3292,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http 1.4.0", + "http 1.4.1", "indexmap 2.14.0", "slab", "tokio", @@ -3445,9 +3445,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" dependencies = [ "bytes", "itoa", @@ -3471,7 +3471,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.4.0", + "http 1.4.1", ] [[package]] @@ -3482,7 +3482,7 @@ checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", "futures-util", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "pin-project-lite", ] @@ -3554,7 +3554,7 @@ dependencies = [ "futures-channel", "futures-core", "h2", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "httparse", "httpdate", @@ -3573,7 +3573,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" dependencies = [ "futures-util", - "http 1.4.0", + "http 1.4.1", "hyper 1.8.1", "hyper-util", "rustls", @@ -3624,7 +3624,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "hyper 1.8.1", "ipnet", @@ -5900,7 +5900,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "http-body-util", "hyper 1.8.1", @@ -5941,7 +5941,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "http-body-util", "hyper 1.8.1", @@ -5975,7 +5975,7 @@ checksum = "57f17d28a6e6acfe1733fe24bcd30774d13bffa4b8a22535b4c8c98423088d4e" dependencies = [ "anyhow", "async-trait", - "http 1.4.0", + "http 1.4.1", "reqwest 0.12.28", "serde", "thiserror 1.0.69", @@ -10477,7 +10477,7 @@ dependencies = [ "flate2", "futures 0.3.32", "goauth", - "http 1.4.0", + "http 1.4.1", "hyper-util", "log", "prost 0.14.3", @@ -12450,7 +12450,7 @@ dependencies = [ "base64 0.22.1", "bytes", "h2", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "http-body-util", "hyper 1.8.1", @@ -12538,7 +12538,7 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "http-body-util", "iri-string", @@ -12652,7 +12652,7 @@ checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" dependencies = [ "bytes", "data-encoding", - "http 1.4.0", + "http 1.4.1", "httparse", "log", "rand 0.9.4", diff --git a/Cargo.toml b/Cargo.toml index 195984e2d3a..52bba61421c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -250,7 +250,7 @@ hex = "0.4.3" hex-literal = "1.1.0" hidapi = { version = "2.6.6", default-features = false } histogram = "0.6.9" -http = "1.4.0" +http = "1.4.1" humantime = "2.3.0" hyper-util = "0.1.20" imbl = "7.0.0" diff --git a/ci/xtask/Cargo.lock b/ci/xtask/Cargo.lock index bbdc474d7f3..6059891382b 100644 --- a/ci/xtask/Cargo.lock +++ b/ci/xtask/Cargo.lock @@ -712,9 +712,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" dependencies = [ "bytes", "itoa", diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 715d223b147..01efeb41630 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -948,7 +948,7 @@ checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9" dependencies = [ "base64 0.22.1", "flate2", - "http 1.4.0", + "http 1.4.1", "log", "native-tls", "serde", @@ -981,7 +981,7 @@ dependencies = [ "axum-core", "bytes", "futures-util", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "http-body-util", "itoa", @@ -1005,7 +1005,7 @@ checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ "bytes", "futures-core", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "http-body-util", "mime", @@ -2832,7 +2832,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http 1.4.0", + "http 1.4.1", "indexmap", "slab", "tokio", @@ -2962,9 +2962,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" dependencies = [ "bytes", "itoa", @@ -2988,7 +2988,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.4.0", + "http 1.4.1", ] [[package]] @@ -2999,7 +2999,7 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "pin-project-lite", ] @@ -3065,7 +3065,7 @@ dependencies = [ "futures-channel", "futures-core", "h2", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "httparse", "httpdate", @@ -3083,7 +3083,7 @@ version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "http 1.4.0", + "http 1.4.1", "hyper 1.8.1", "hyper-util", "rustls", @@ -3133,7 +3133,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "hyper 1.8.1", "ipnet", @@ -5138,7 +5138,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "http-body-util", "hyper 1.8.1", @@ -5178,7 +5178,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "http-body-util", "hyper 1.8.1", @@ -5212,7 +5212,7 @@ checksum = "57f17d28a6e6acfe1733fe24bcd30774d13bffa4b8a22535b4c8c98423088d4e" dependencies = [ "anyhow", "async-trait", - "http 1.4.0", + "http 1.4.1", "reqwest 0.12.28", "serde", "thiserror 1.0.69", @@ -8796,7 +8796,7 @@ dependencies = [ "flate2", "futures 0.3.32", "goauth", - "http 1.4.0", + "http 1.4.1", "hyper-util", "log", "prost", @@ -10513,7 +10513,7 @@ dependencies = [ "base64 0.22.1", "bytes", "h2", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "http-body-util", "hyper 1.8.1", @@ -10601,7 +10601,7 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "http-body-util", "iri-string", @@ -10716,7 +10716,7 @@ checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" dependencies = [ "bytes", "data-encoding", - "http 1.4.0", + "http 1.4.1", "httparse", "log", "rand 0.9.4", diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index fb291f6d91e..0db007021cf 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -932,7 +932,7 @@ checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9" dependencies = [ "base64 0.22.1", "flate2", - "http 1.4.0", + "http 1.4.1", "log", "native-tls", "serde", @@ -965,7 +965,7 @@ dependencies = [ "axum-core", "bytes", "futures-util", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "http-body-util", "itoa", @@ -989,7 +989,7 @@ checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ "bytes", "futures-core", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "http-body-util", "mime", @@ -2727,7 +2727,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http 1.4.0", + "http 1.4.1", "indexmap", "slab", "tokio", @@ -2861,9 +2861,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" dependencies = [ "bytes", "itoa", @@ -2887,7 +2887,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.4.0", + "http 1.4.1", ] [[package]] @@ -2898,7 +2898,7 @@ checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", "futures-util", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "pin-project-lite", ] @@ -2964,7 +2964,7 @@ dependencies = [ "futures-channel", "futures-core", "h2", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "httparse", "httpdate", @@ -2983,7 +2983,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" dependencies = [ "futures-util", - "http 1.4.0", + "http 1.4.1", "hyper 1.8.1", "hyper-util", "rustls", @@ -3033,7 +3033,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "hyper 1.8.1", "ipnet", @@ -5046,7 +5046,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "http-body-util", "hyper 1.8.1", @@ -5086,7 +5086,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "http-body-util", "hyper 1.8.1", @@ -5120,7 +5120,7 @@ checksum = "57f17d28a6e6acfe1733fe24bcd30774d13bffa4b8a22535b4c8c98423088d4e" dependencies = [ "anyhow", "async-trait", - "http 1.4.0", + "http 1.4.1", "reqwest 0.12.28", "serde", "thiserror 1.0.69", @@ -9316,7 +9316,7 @@ dependencies = [ "flate2", "futures 0.3.32", "goauth", - "http 1.4.0", + "http 1.4.1", "hyper-util", "log", "prost", @@ -11014,7 +11014,7 @@ dependencies = [ "base64 0.22.1", "bytes", "h2", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "http-body-util", "hyper 1.8.1", @@ -11102,7 +11102,7 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "http-body-util", "iri-string", @@ -11217,7 +11217,7 @@ checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" dependencies = [ "bytes", "data-encoding", - "http 1.4.0", + "http 1.4.1", "httparse", "log", "rand 0.9.4", From 28784debec727677144c7e739db48a787e1805a1 Mon Sep 17 00:00:00 2001 From: steviez Date: Mon, 25 May 2026 22:30:14 -0500 Subject: [PATCH 051/576] chore: Remove deleted teams from mergify config (#12724) --- .mergify.yml | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/.mergify.yml b/.mergify.yml index c06a16e455d..de40fdf55bd 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -2,7 +2,6 @@ pull_request_rules: - name: label changes from community conditions: - - author≠@agave-external-triage - author≠@anza - author≠mergify[bot] - author≠dependabot[bot] @@ -14,7 +13,6 @@ pull_request_rules: - need:merge-assist - name: request review for community changes conditions: - - author≠@agave-external-triage - author≠@anza - author≠mergify[bot] - author≠dependabot[bot] @@ -30,17 +28,6 @@ pull_request_rules: request_reviews: teams: - "@anza-xyz/community-pr-subscribers" - - name: label changes from agave-external-triage - conditions: - - author≠@anza - - author≠mergify[bot] - - author≠dependabot[bot] - - author≠github-actions[bot] - - author=@agave-external-triage - actions: - label: - add: - - need:merge-assist - name: automatic merge (squash) on CI success conditions: - and: From bfb1217bdeaa3c088a3ddbf2968bc8fb6c78cc78 Mon Sep 17 00:00:00 2001 From: Joe C Date: Tue, 26 May 2026 15:43:24 +0800 Subject: [PATCH 052/576] svm: drop Option from instruction harness return types (#12725) The conformance harness is a test driver; the only failure modes wrapped by Option were setup mistakes (program not in cache) that should fail loudly so fuzzer drivers surface them. Make execute_instr, execute_instr_with_callback, and execute_instr_proto return their effects directly, panic on the cache-miss case, and drop the now-vacuous unwraps from the programs/sbf callers. --- programs/sbf/tests/programs.rs | 57 ++++++++++++---------------------- svm/src/conformance/harness.rs | 28 ++++++++--------- 2 files changed, 34 insertions(+), 51 deletions(-) diff --git a/programs/sbf/tests/programs.rs b/programs/sbf/tests/programs.rs index c756760a4b4..559d1e4a3e7 100644 --- a/programs/sbf/tests/programs.rs +++ b/programs/sbf/tests/programs.rs @@ -323,8 +323,7 @@ fn test_program_sbf_sanity() { instruction, }; - let effects = - execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache).unwrap(); + let effects = execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache); let result = match effects.result { Some(err) => Err(err), @@ -390,8 +389,7 @@ fn test_program_sbf_loader_deprecated() { instruction, }; - let effects = - execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache).unwrap(); + let effects = execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache); assert!(effects.result.is_none()); } @@ -512,8 +510,7 @@ fn test_sol_alloc_free_no_longer_deployable_with_upgradeable_loader() { // in elf inside syscall table. In this case, `sol_alloc_free_` can't be // found in syscall table. Hence, the verification fails and the deployment // fails. - let effects = - execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache).unwrap(); + let effects = execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache); assert_eq!(effects.result, Some(InstructionError::InvalidAccountData)); } @@ -572,7 +569,7 @@ fn test_program_sbf_duplicate_accounts() { accounts, instruction, }; - execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache).unwrap() + execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache) }; let effects = execute(&[1]); @@ -624,8 +621,7 @@ fn test_program_sbf_duplicate_accounts() { accounts, instruction, }; - let effects = - execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache).unwrap(); + let effects = execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache); assert!(effects.result.is_none()); } } @@ -677,7 +673,7 @@ fn test_program_sbf_error_handling() { instruction, }; - execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache).unwrap() + execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache) }; let effects = execute(&[1]); @@ -769,8 +765,7 @@ fn test_return_data_and_log_data_syscall() { instruction, }; - let effects = - execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache).unwrap(); + let effects = execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache); assert!(effects.result.is_none()); @@ -1474,8 +1469,7 @@ fn test_program_sbf_program_id_spoofing() { instruction, }; - let effects = - execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache).unwrap(); + let effects = execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache); assert_eq!( effects.result, @@ -1542,8 +1536,7 @@ fn test_program_sbf_caller_has_access_to_cpi_program() { instruction, }; - let effects = - execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache).unwrap(); + let effects = execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache); assert_eq!(effects.result, Some(InstructionError::MissingAccount)); } @@ -1588,8 +1581,7 @@ fn test_program_sbf_ro_modify() { instruction, }; - let effects = - execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache).unwrap(); + let effects = execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache); assert_eq!( effects.result, @@ -1626,7 +1618,7 @@ fn test_program_sbf_call_depth() { instruction, }; - execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache).unwrap() + execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache) }; let effects = execute(compute_budget.max_call_depth - 1); @@ -1669,8 +1661,7 @@ fn test_program_sbf_compute_budget() { instruction, }; - let effects = - execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache).unwrap(); + let effects = execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache); assert_eq!( effects.result, @@ -1753,8 +1744,7 @@ fn assert_instruction_count() { instruction, }; - let effects = - execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache).unwrap(); + let effects = execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache); let consumption = compute_budget .compute_unit_limit @@ -1882,8 +1872,7 @@ fn test_program_sbf_r2_instruction_data_pointer(num_accounts: usize, input_data_ instruction, }; - let effects = - execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache).unwrap(); + let effects = execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache); assert!(effects.result.is_none()); assert_eq!(input_data, effects.return_data); @@ -2408,8 +2397,7 @@ fn test_program_sbf_disguised_as_sbf_loader() { instruction, }; - let effects = - execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache).unwrap(); + let effects = execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache); assert_eq!(effects.result, Some(InstructionError::UnsupportedProgramId)); } } @@ -2455,8 +2443,7 @@ fn test_program_reads_from_program_account() { instruction, }; - let effects = - execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache).unwrap(); + let effects = execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache); assert!(effects.result.is_none()); } @@ -2498,8 +2485,7 @@ fn test_program_sbf_c_dup() { instruction, }; - let effects = - execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache).unwrap(); + let effects = execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache); assert!(effects.result.is_none()); } @@ -2705,8 +2691,7 @@ fn test_program_sbf_ro_account_modify() { instruction, }; - let effects = - execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache).unwrap(); + let effects = execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache); assert_eq!(effects.result, Some(InstructionError::ReadonlyDataModified)); } @@ -4621,8 +4606,7 @@ fn test_program_sbf_deplete_cost_meter_with_divide_by_zero() { instruction, }; - let effects = - execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache).unwrap(); + let effects = execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache); assert_eq!( effects.result, @@ -5707,8 +5691,7 @@ fn test_program_sbf_rust_direct_account_pointers(num_accounts: usize, input_data instruction, }; - let effects = - execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache).unwrap(); + let effects = execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache); assert!(effects.result.is_none()); // `num_accounts * 2` will be added as return data. diff --git a/svm/src/conformance/harness.rs b/svm/src/conformance/harness.rs index d9586324cdd..d21c69eb8f2 100644 --- a/svm/src/conformance/harness.rs +++ b/svm/src/conformance/harness.rs @@ -65,7 +65,7 @@ pub fn execute_instr( compute_budget: &ComputeBudget, program_cache: &mut ProgramCacheForTxBatch, sysvar_cache: &SysvarCache, -) -> Option { +) -> InstrEffects { execute_instr_with_callback( input, &DefaultCallback, @@ -82,7 +82,7 @@ pub fn execute_instr_with_callback( compute_budget: &ComputeBudget, program_cache: &mut ProgramCacheForTxBatch, sysvar_cache: &SysvarCache, -) -> Option { +) -> InstrEffects { let mut compute_units_consumed = 0; let mut timings = ExecuteTimings::default(); @@ -91,7 +91,10 @@ pub fn execute_instr_with_callback( let rent = sysvar_cache.get_rent().unwrap(); let program_id = &input.instruction.program_id; - let loader_key = program_cache.find(program_id)?.account_owner(); + let loader_key = program_cache + .find(program_id) + .expect("program not loaded in cache") + .account_owner(); let (sanitized_message, transaction_accounts) = mock_compile_message(&input.instruction, &input.accounts, program_id, &loader_key); @@ -176,7 +179,7 @@ pub fn execute_instr_with_callback( }) .collect::>(); - Some(InstrEffects { + InstrEffects { custom_err: if let Err(InstructionError::Custom(code)) = result { Some(code) } else { @@ -193,11 +196,11 @@ pub fn execute_instr_with_callback( cu_avail, return_data, logs, - }) + } } #[cfg(feature = "conformance")] -pub fn execute_instr_proto(input: ProtoInstrContext) -> Option { +pub fn execute_instr_proto(input: ProtoInstrContext) -> ProtoInstrEffects { let cu_avail = input.cu_avail; let instr_context = InstrContext::from(input); @@ -235,13 +238,13 @@ pub fn execute_instr_proto(input: ProtoInstrContext) -> Option out_slice.len() { @@ -424,8 +425,7 @@ mod tests { .unwrap(); // Execute the instruction. - let effects = execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache) - .expect("Instruction execution should succeed"); + let effects = execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache); // Verify the results. assert_eq!(effects.result, None); From ce2ce998e94ebb1fe8b9df9dce2e0316946b153e Mon Sep 17 00:00:00 2001 From: Yihau Chen Date: Tue, 26 May 2026 20:18:53 +0800 Subject: [PATCH 053/576] ci: remove unused label-actions (#12712) --- .github/label-actions.yml | 27 --------------------------- .github/workflows/label-actions.yml | 15 --------------- 2 files changed, 42 deletions(-) delete mode 100644 .github/label-actions.yml delete mode 100644 .github/workflows/label-actions.yml diff --git a/.github/label-actions.yml b/.github/label-actions.yml deleted file mode 100644 index 8a67d20c3de..00000000000 --- a/.github/label-actions.yml +++ /dev/null @@ -1,27 +0,0 @@ -question: - issues: - # Post a comment, `{issue-author}` is an optional placeholder - comment: > - Hi @{issue-author}, - - - Thanks for your question! - - - We want to make sure to keep signal strong in the GitHub issue tracker – to make sure - that it remains the best place to track issues that affect the development of Solana itself. - - - Questions like yours deserve a purpose-built Q&A forum. Unless there exists evidence that - this is a bug with Solana itself, please post your question to the Solana Stack Exchange - using this link: https://solana.stackexchange.com/questions/ask - - - --- - - _This - [automated message](https://github.com/anza-xyz/agave/blob/master/.github/label-actions.yml) - is a result of having added the ‘question’ tag_. - - # Close the issue - close: true diff --git a/.github/workflows/label-actions.yml b/.github/workflows/label-actions.yml deleted file mode 100644 index 49e13945d12..00000000000 --- a/.github/workflows/label-actions.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: "Issue Label Actions" - -on: - issues: - types: [labeled, unlabeled] - -permissions: - contents: read - issues: write - -jobs: - action: - runs-on: ubuntu-24.04 - steps: - - uses: dessant/label-actions@809e0f9bc11ce08744e7ccac7cd10621d00aba2f # v5.0.2 From b8c21018ed652fc86e8305196d628f8e33f06d64 Mon Sep 17 00:00:00 2001 From: Yihau Chen Date: Tue, 26 May 2026 21:54:52 +0800 Subject: [PATCH 054/576] ci: fix trigger-buildkite-pipeline missing pull_request_base_branch (#12730) --- .github/workflows/trigger-buildkite-pipeline.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/trigger-buildkite-pipeline.yml b/.github/workflows/trigger-buildkite-pipeline.yml index 6b925ad2f5e..90e530edcab 100644 --- a/.github/workflows/trigger-buildkite-pipeline.yml +++ b/.github/workflows/trigger-buildkite-pipeline.yml @@ -111,6 +111,7 @@ jobs: build_meta_data: "${{ steps.prepare.outputs.build_meta_data }}" commit: "${{ steps.prepare.outputs.commit }}" message: "${{ steps.prepare.outputs.message }}" + pull_request_base_branch: "${{ github.event.pull_request.base.ref }}" - name: Summary env: From cb1cbf04e5ee0b775c7f95d6adfee93c851b6268 Mon Sep 17 00:00:00 2001 From: BitWonka <79578887+BitWonka@users.noreply.github.com> Date: Tue, 26 May 2026 18:32:14 +0200 Subject: [PATCH 055/576] Fix secondary index memory (#12726) --- accounts-db/src/accounts_index/secondary.rs | 49 ++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/accounts-db/src/accounts_index/secondary.rs b/accounts-db/src/accounts_index/secondary.rs index 26137158c45..8abecc12f0f 100644 --- a/accounts-db/src/accounts_index/secondary.rs +++ b/accounts-db/src/accounts_index/secondary.rs @@ -132,7 +132,11 @@ impl /// Inserts `inner_key` into `key`'s map. pub fn insert(&self, key: &Pubkey, inner_key: &Pubkey) { // Note: Always lock the reverse index first, so we synchronize with remove(). - let reverse_index_entry = self.reverse_index.entry(*inner_key).or_default(); + // Pre-size to 1 to avoid push() over-allocating an empty Vec to capacity 4. + let reverse_index_entry = self + .reverse_index + .entry(*inner_key) + .or_insert_with(|| RwLock::new(Vec::with_capacity(1))); let mut outer_keys = reverse_index_entry.write().unwrap(); // Now insert into the index. @@ -369,4 +373,47 @@ mod tests { 0, ); } + + /// Regression guard for reverse-index entry capacity. Each entry must be + /// created with capacity 1. An empty Vec plus push() would over-allocate to + /// capacity 4 (Rust RawVec rule), wasting 96 bytes per entry. + #[test] + fn test_reverse_index_entry_not_over_allocated() { + let secondary_index = + SecondaryIndex::::new("test_secondary_index"); + let outer_key = Pubkey::new_unique(); + let inner_key = Pubkey::new_unique(); + + secondary_index.insert(&outer_key, &inner_key); + + let reverse_entry = secondary_index + .reverse_index + .get(&inner_key) + .expect("reverse index entry should exist after insert"); + let outer_keys = reverse_entry.read().unwrap(); + assert_eq!(outer_keys.len(), 1); + assert_eq!(outer_keys[0], outer_key); + // Regression guard: `or_default()` + push() would make this 4. + assert_eq!(outer_keys.capacity(), 1); + } + + /// A reverse entry with multiple outer keys still grows correctly when + /// started at capacity 1, exercising the push() realloc path. + #[test] + fn test_reverse_index_multiple_outer_keys() { + let secondary_index = + SecondaryIndex::::new("test_secondary_index"); + let inner_key = Pubkey::new_unique(); + let outer_key_1 = Pubkey::new_unique(); + let outer_key_2 = Pubkey::new_unique(); + + secondary_index.insert(&outer_key_1, &inner_key); + secondary_index.insert(&outer_key_2, &inner_key); + + let reverse_entry = secondary_index.reverse_index.get(&inner_key).unwrap(); + let outer_keys = reverse_entry.read().unwrap(); + assert_eq!(outer_keys.len(), 2); + assert!(outer_keys.contains(&outer_key_1)); + assert!(outer_keys.contains(&outer_key_2)); + } } From 7231e4e8b0e31f1312253b8005e2ccb6e5d3baf0 Mon Sep 17 00:00:00 2001 From: Greg Cusack Date: Tue, 26 May 2026 10:56:22 -0700 Subject: [PATCH 056/576] gossip: remove random ip:port generation in contact_info tests (#10912) remove random ip:port generation in contact_info tests --- gossip/src/contact_info.rs | 89 +++++++++++++++----------------------- 1 file changed, 34 insertions(+), 55 deletions(-) diff --git a/gossip/src/contact_info.rs b/gossip/src/contact_info.rs index feccc0228e2..a6c18ae7d88 100644 --- a/gossip/src/contact_info.rs +++ b/gossip/src/contact_info.rs @@ -730,51 +730,17 @@ macro_rules! socketaddr_any { mod tests { use { super::*, - rand::{ - Rng, - prelude::{IndexedRandom as _, SliceRandom as _}, - }, + rand::{Rng, prelude::SliceRandom as _}, solana_keypair::Keypair, solana_signer::Signer, std::{ collections::{HashMap, HashSet}, - iter::repeat_with, net::{Ipv4Addr, Ipv6Addr}, ops::Range, time::Duration, }, }; - fn new_rand_addr(rng: &mut R) -> IpAddr { - if rng.random() { - let addr = Ipv4Addr::new(rng.random(), rng.random(), rng.random(), rng.random()); - IpAddr::V4(addr) - } else { - let addr = Ipv6Addr::new( - rng.random(), - rng.random(), - rng.random(), - rng.random(), - rng.random(), - rng.random(), - rng.random(), - rng.random(), - ); - IpAddr::V6(addr) - } - } - - fn new_rand_port(rng: &mut R) -> u16 { - let port = rng.random::(); - let bits = u16::BITS - port.leading_zeros(); - let shift = rng.random_range(0u32..bits + 1u32); - port.checked_shr(shift).unwrap_or_default() - } - - fn new_rand_socket(rng: &mut R) -> SocketAddr { - SocketAddr::new(new_rand_addr(rng), new_rand_port(rng)) - } - #[test] fn test_new_gossip_entry_point() { let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, 10)); @@ -798,7 +764,13 @@ mod tests { #[test] fn test_sanitize_entries() { let mut rng = rand::rng(); - let addrs: Vec = repeat_with(|| new_rand_addr(&mut rng)).take(5).collect(); + let addrs = [ + IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)), + IpAddr::V4(Ipv4Addr::new(10, 0, 0, 2)), + IpAddr::V4(Ipv4Addr::new(10, 0, 0, 3)), + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 1, 0, 0, 0, 0, 1)), + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 1, 0, 0, 0, 0, 2)), + ]; let mut keys: Vec = (0u8..=u8::MAX).collect(); keys.shuffle(&mut rng); // Duplicate IP addresses. @@ -881,7 +853,16 @@ mod tests { fn test_round_trip() { const KEYS: Range = 0u8..16u8; let mut rng = rand::rng(); - let addrs: Vec = repeat_with(|| new_rand_addr(&mut rng)).take(8).collect(); + let addrs = [ + IpAddr::V4(Ipv4Addr::new(10, 0, 1, 1)), + IpAddr::V4(Ipv4Addr::new(10, 0, 1, 2)), + IpAddr::V4(Ipv4Addr::new(10, 0, 1, 3)), + IpAddr::V4(Ipv4Addr::new(10, 0, 1, 4)), + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 2, 0, 0, 0, 0, 1)), + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 2, 0, 0, 0, 0, 2)), + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 2, 0, 0, 0, 0, 3)), + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 2, 0, 0, 0, 0, 4)), + ]; let mut node = ContactInfo { pubkey: Pubkey::new_unique(), wallclock: rng.random(), @@ -894,17 +875,13 @@ mod tests { cache: EMPTY_SOCKET_ADDR_CACHE, }; let mut sockets = HashMap::::new(); - for _ in 0..1 << 14 { - let addr = addrs.choose(&mut rng).unwrap(); - let socket = SocketAddr::new(*addr, new_rand_port(&mut rng)); + for i in 0..1 << 10 { + let addr = addrs[i % addrs.len()]; + let socket = SocketAddr::new(addr, 10_000u16 + i as u16); let key = rng.random_range(KEYS.start..KEYS.end); - if sanitize_socket(&socket).is_ok() { - sockets.insert(key, socket); - assert_matches!(node.set_socket(key, socket), Ok(())); - assert_matches!(sanitize_entries(&node.addrs, &node.sockets), Ok(())); - } else { - assert_matches!(node.set_socket(key, socket), Err(_)); - } + sockets.insert(key, socket); + assert_matches!(node.set_socket(key, socket), Ok(())); + assert_matches!(sanitize_entries(&node.addrs, &node.sockets), Ok(())); for key in KEYS.clone() { let socket = sockets.get(&key); assert_eq!(node.get_socket(key).ok().as_ref(), socket); @@ -1017,9 +994,7 @@ mod tests { rng.random(), // wallclock rng.random(), // shred_version ); - let socket = repeat_with(|| new_rand_socket(&mut rng)) - .find(|socket| matches!(sanitize_socket(socket), Ok(()))) - .unwrap(); + let socket = SocketAddr::from((Ipv4Addr::new(10, 1, 0, 1), 12_345)); node.set_alpenglow(socket).unwrap(); assert_eq!(node.alpenglow().unwrap(), socket); node.remove_alpenglow(); @@ -1045,11 +1020,15 @@ mod tests { // Updated socket address is not a duplicate instance. { let mut other = node.clone(); - while other.set_gossip(new_rand_socket(&mut rng)).is_err() {} - while other - .set_serve_repair(Protocol::UDP, new_rand_socket(&mut rng)) - .is_err() - {} + other + .set_gossip(SocketAddr::from((Ipv4Addr::new(10, 1, 0, 2), 12_346))) + .unwrap(); + other + .set_serve_repair( + Protocol::UDP, + SocketAddr::from((Ipv4Addr::new(10, 1, 0, 3), 12_347)), + ) + .unwrap(); assert!(!node.check_duplicate(&other)); assert!(!other.check_duplicate(&node)); assert_eq!(node.overrides(&other), None); From 2cb8886132cf6dc4d57712d7c75ba429eca17507 Mon Sep 17 00:00:00 2001 From: Kamil Skalski Date: Tue, 26 May 2026 22:19:53 +0200 Subject: [PATCH 057/576] perf(snapshots): prefetch storage files while archiving (#12678) --- accounts-db/src/account_storage_reader.rs | 60 ++++++++++------ fs/src/buffered_reader.rs | 39 +++++++++- fs/src/io_uring/sequential_file_reader.rs | 30 ++++++-- runtime/src/serde_snapshot/tests.rs | 3 +- snapshots/src/archive.rs | 86 ++++++++++++++++------- 5 files changed, 160 insertions(+), 58 deletions(-) diff --git a/accounts-db/src/account_storage_reader.rs b/accounts-db/src/account_storage_reader.rs index c6eb8a04c2f..74fb5abe7a6 100644 --- a/accounts-db/src/account_storage_reader.rs +++ b/accounts-db/src/account_storage_reader.rs @@ -16,6 +16,18 @@ use { // file fits entirely within the buffer. pub const ACCOUNT_STORAGE_MAX_BUFFER_SIZE: usize = 10 * 1024 * 1024; +#[cfg(not(target_os = "linux"))] +const READER_STACK_BUFFER_SIZE: usize = 64 * 1024; + +/// Concrete reader type returned by [`storage_file_buf_reader`]. +/// +/// The concrete type is exposed (rather than `impl FileBufRead<'a>`) so callers +/// can use inherent methods like `rebind`. +#[cfg(target_os = "linux")] +type StorageFileBufReader<'a> = buffered_reader::SequentialFileReader<'a>; +#[cfg(not(target_os = "linux"))] +type StorageFileBufReader<'a> = buffered_reader::BufferedReader<'a, READER_STACK_BUFFER_SIZE>; + /// When `use_page_cache` is `true`, direct I/O is forced off regardless of /// `io_setup.use_direct_io` so that reads can hit the kernel's page cache. /// Otherwise, the `io_setup.use_direct_io` setting is honored. @@ -23,35 +35,31 @@ pub fn storage_file_buf_reader<'a>( max_buf_size: usize, use_page_cache: bool, io_setup: &IoSetupState, -) -> io::Result + use<'a>> { +) -> io::Result> { #[cfg(target_os = "linux")] - let reader = buffered_reader::SequentialFileReaderBuilder::new() - .shared_sqpoll(io_setup.shared_sqpoll_fd()) - .use_direct_io(io_setup.use_direct_io && !use_page_cache) - .use_registered_buffers(io_setup.use_registered_io_uring_buffers) - .build(max_buf_size)?; + { + buffered_reader::SequentialFileReaderBuilder::new() + .shared_sqpoll(io_setup.shared_sqpoll_fd()) + .use_direct_io(io_setup.use_direct_io && !use_page_cache) + .use_registered_buffers(io_setup.use_registered_io_uring_buffers) + .build(max_buf_size) + } #[cfg(not(target_os = "linux"))] - let reader = { + { let _ = (max_buf_size, use_page_cache, io_setup); - const READER_STACK_BUFFER_SIZE: usize = 64 * 1024; - buffered_reader::BufferedReader::::new() - }; - Ok(reader) + Ok(StorageFileBufReader::new()) + } } -/// Returns a file handle for each storage suitable for archive-style reads -/// matching `use_direct_io` (see [`OpenFileForArchive`]). -/// -/// Returned as a `Vec` so the handles outlive the buffered reader they'll be -/// fed into. +/// Lazy iterator yielding a file handle for each storage suitable for +/// archive-style reads matching `use_direct_io` (see [`OpenFileForArchive`]). pub fn open_storage_files<'s>( - storages: impl IntoIterator, + storages: impl IntoIterator + 's, use_direct_io: bool, -) -> io::Result>> { +) -> impl Iterator>> + 's { storages .into_iter() - .map(|storage| storage.accounts.open_file_for_archive(use_direct_io)) - .collect() + .map(move |storage| storage.accounts.open_file_for_archive(use_direct_io)) } /// A wrapper type around `AccountStorageEntry` that implements the `Read` trait. @@ -207,7 +215,9 @@ mod tests { storage.accounts.write_accounts(&(slot, &accounts[..]), 0); - let files = open_storage_files(iter::once(&storage), false).unwrap(); + let files = open_storage_files(iter::once(&storage), false) + .collect::>>() + .unwrap(); let mut buf_reader = storage_file_buf_reader( ACCOUNT_STORAGE_MAX_BUFFER_SIZE, false, @@ -284,7 +294,9 @@ mod tests { let storage = storage.reopen_as_readonly().unwrap_or(storage); // Create the reader and check the length - let files = open_storage_files(iter::once(&storage), false).unwrap(); + let files = open_storage_files(iter::once(&storage), false) + .collect::>>() + .unwrap(); let mut file_reader = storage_file_buf_reader( ACCOUNT_STORAGE_MAX_BUFFER_SIZE, false, @@ -406,7 +418,9 @@ mod tests { let temp_dir = tempfile::tempdir().unwrap(); // Now iterate through all the possible snapshot slots and verify correctness - let files = open_storage_files(iter::once(&storage), false).unwrap(); + let files = open_storage_files(iter::once(&storage), false) + .collect::>>() + .unwrap(); let mut file_reader = storage_file_buf_reader( ACCOUNT_STORAGE_MAX_BUFFER_SIZE, false, diff --git a/fs/src/buffered_reader.rs b/fs/src/buffered_reader.rs index a48104755ad..584d9fb55b1 100644 --- a/fs/src/buffered_reader.rs +++ b/fs/src/buffered_reader.rs @@ -14,7 +14,9 @@ //! When reading full accounts data whose sizes exceed the small stack buffer, the `BufReaderWithOverflow` //! should be used, which supports dynamically allocated buffer for preparing contiguous data slices. #[cfg(target_os = "linux")] -pub use crate::io_uring::sequential_file_reader::SequentialFileReaderBuilder; +pub use crate::io_uring::sequential_file_reader::{ + SequentialFileReader, SequentialFileReaderBuilder, +}; use { crate::{ FileSize, @@ -71,8 +73,21 @@ pub trait FileBufRead<'a>: BufRead { /// /// `read_limit` provides a pre-defined limit on the number of bytes that can be read /// from the file (unless EOF is reached). + /// + /// If the file was previously queued via `add_file_to_prefetch`, the reader + /// advances to that file (validating identity / `read_limit`) rather than + /// re-queueing it. fn set_file(&mut self, file: &'a File, read_limit: FileSize) -> io::Result<()>; + /// Queue `file` for read-ahead (prefetch). Files are processed in FIFO order + /// by subsequent `set_file` calls. + /// + /// This is a hint to keep the read pipeline saturated for readers that + /// support concurrent read-ahead. Implementations without read-ahead may + /// ignore the call. Callers must still invoke `set_file` to switch the + /// active file. + fn add_file_to_prefetch(&mut self, file: &'a File, read_limit: FileSize) -> io::Result<()>; + /// Returns the current file offset corresponding to the start of the buffer /// that will be returned by the next call to `fill_buf`. /// @@ -155,6 +170,18 @@ impl<'a, const N: usize> BufferedReader<'a, N> { self.file_offset_of_next_read = 0; self.buf_valid_bytes = 0..0; } + + /// Reset to idle state and re-type with a fresh lifetime `'b`. + pub fn rebind<'b>(self) -> io::Result> { + Ok(BufferedReader { + file_offset_of_next_read: 0, + buf: self.buf, + buf_valid_bytes: 0..0, + file_last_offset: 0, + file_len_valid: 0, + file: None, + }) + } } impl<'a, const N: usize> FileBufRead<'a> for BufferedReader<'a, N> { @@ -163,6 +190,12 @@ impl<'a, const N: usize> FileBufRead<'a> for BufferedReader<'a, N> { Ok(()) } + /// `BufferedReader` does not perform read-ahead — the call is a no-op and + /// the file becomes active only when `set_file` is later invoked. + fn add_file_to_prefetch(&mut self, _file: &'a File, _read_limit: FileSize) -> io::Result<()> { + Ok(()) + } + #[inline(always)] fn get_file_offset(&self) -> FileSize { if self.buf_valid_bytes.is_empty() { @@ -360,6 +393,10 @@ impl<'a, R: FileBufRead<'a>> FileBufRead<'a> for BufReaderWithOverflow { self.reader.set_file(file, read_limit) } + fn add_file_to_prefetch(&mut self, file: &'a File, read_limit: FileSize) -> io::Result<()> { + self.reader.add_file_to_prefetch(file, read_limit) + } + fn get_file_offset(&self) -> FileSize { self.reader.get_file_offset() - self.overflow_buf.len() as FileSize } diff --git a/fs/src/io_uring/sequential_file_reader.rs b/fs/src/io_uring/sequential_file_reader.rs index 29042e1191b..bfe4b283868 100644 --- a/fs/src/io_uring/sequential_file_reader.rs +++ b/fs/src/io_uring/sequential_file_reader.rs @@ -139,7 +139,7 @@ impl<'sp> SequentialFileReaderBuilder<'sp> { if self.register_buffer { // Safety: kernel holds unsafe pointers to `buffer`, struct field declaration order - // guarantees that the ring is destroyed before `_backing_buffer` is dropped. + // guarantees that the ring is destroyed before `backing_buffer` is dropped. unsafe { IoBufferChunk::register(buf_slice_mut, &ring)? }; } @@ -166,7 +166,7 @@ impl<'sp> SequentialFileReaderBuilder<'sp> { ring, state: SequentialFileReaderState::default(), open_file_flags, - _backing_buffer: buffer, + backing_buffer: buffer, _phantom: PhantomData, }) } @@ -200,14 +200,14 @@ impl<'sp> SequentialFileReaderBuilder<'sp> { /// /// Implements read-ahead using io_uring. pub struct SequentialFileReader<'a> { - // Note: ring's state is tied to `_backing_buffer` - contains unsafe pointer references - // to the buffer. Ring should be drained and dropped before `_backing_buffer`. + // Note: ring's state is tied to `backing_buffer` - contains unsafe pointer references + // to the buffer. Ring should be drained and dropped before `backing_buffer`. ring: Ring, open_file_flags: i32, state: SequentialFileReaderState, /// Owned buffer used (chunked into `FixedIoBuffer` items) across lifespan of `inner` /// (should get dropped last) - _backing_buffer: PageAlignedMemory, + backing_buffer: PageAlignedMemory, _phantom: PhantomData<&'a ()>, } @@ -251,8 +251,20 @@ impl<'a> SequentialFileReader<'a> { Ok(()) } - fn add_file_to_prefetch(&mut self, file: &'a File, read_limit: FileSize) -> io::Result<()> { - self.add_file_by_fd(file.as_raw_fd(), read_limit) + /// Reset to idle state and re-type with a fresh lifetime `'b`. + /// + /// Drains the prefetch queue (cancels in-flight reads) before returning. + pub fn rebind<'b>(mut self) -> io::Result> { + while !self.state.files.is_empty() { + self.move_to_next_file()?; + } + Ok(SequentialFileReader { + ring: self.ring, + open_file_flags: self.open_file_flags, + state: self.state, + backing_buffer: self.backing_buffer, + _phantom: PhantomData, + }) } /// Caller must ensure that the file is not closed while the reader is using it. @@ -490,6 +502,10 @@ impl<'a> FileBufRead<'a> for SequentialFileReader<'a> { Ok(()) } + fn add_file_to_prefetch(&mut self, file: &'a File, read_limit: FileSize) -> io::Result<()> { + self.add_file_by_fd(file.as_raw_fd(), read_limit) + } + fn get_file_offset(&self) -> FileSize { self.state.current_offset } diff --git a/runtime/src/serde_snapshot/tests.rs b/runtime/src/serde_snapshot/tests.rs index 25dc6938868..c91798d3325 100644 --- a/runtime/src/serde_snapshot/tests.rs +++ b/runtime/src/serde_snapshot/tests.rs @@ -126,7 +126,8 @@ mod serde_snapshot_tests { let storage: AccountStorageMap = AccountStorageMap::with_capacity(storage_entries.len()); let mut next_append_vec_id = 0; const MAX_BUFFER_SIZE: usize = 2 * 1024 * 1024; - let storage_files = open_storage_files(storage_entries.iter().map(|s| s.as_ref()), false)?; + let storage_files = open_storage_files(storage_entries.iter().map(|s| s.as_ref()), false) + .collect::>>()?; let mut buf_reader = storage_file_buf_reader(MAX_BUFFER_SIZE, false, &IoSetupState::default())?; for (storage_entry, file) in storage_entries.iter().zip(storage_files.iter()) { diff --git a/snapshots/src/archive.rs b/snapshots/src/archive.rs index 235e2f0621f..2759bec5d13 100644 --- a/snapshots/src/archive.rs +++ b/snapshots/src/archive.rs @@ -4,7 +4,7 @@ use { snapshot_archive_info::SnapshotArchiveInfo, snapshot_hash::SnapshotHash, }, agave_fs::{ - buffered_reader::FileBufRead as _, buffered_writer::large_file_buf_writer, + FileSize, buffered_reader::FileBufRead as _, buffered_writer::large_file_buf_writer, io_setup::IoSetupState, }, log::info, @@ -28,6 +28,13 @@ use { // and towards the end of archive (sizes equalize) writes are >256KiB / file. const INTERLEAVE_TAR_ENTRIES_SMALL_TO_LARGE_RATIO: (usize, usize) = (4, 1); +// Max number of storage files to open at once for archiving. Bounds extra fd +// usage and io_uring prefetch queue depth. A multiple of the interleave ratio +// sum keeps each chunk balanced between small and large files. +const STORAGE_FILE_OPEN_CHUNK_SIZE: usize = 25 + * (INTERLEAVE_TAR_ENTRIES_SMALL_TO_LARGE_RATIO.0 + + INTERLEAVE_TAR_ENTRIES_SMALL_TO_LARGE_RATIO.1); + /// Archives a snapshot into `archive_path` pub fn archive_snapshot( snapshot_archive_kind: SnapshotArchiveKind, @@ -130,37 +137,64 @@ pub fn archive_snapshot( let use_page_cache = matches!(snapshot_archive_kind, SnapshotArchiveKind::Incremental(_)); let use_direct_io = io_setup.use_direct_io && !use_page_cache; - // Pre-open every storage file (with the buffered reader's - // direct-IO setting) so they outlive `buf_reader` — the reader's - // `'a` lifetime needs to borrow into them across loop iterations. - let storage_files = open_storage_files(storages_orderer.iter(), use_direct_io) - .map_err(E::StorageFileBufReaderError)?; + + // Walk storages and their (lazily-opened) file handles in chunks, + // bounding how many archive-mode fds are simultaneously open. + let mut storage_file_pairs = storages_orderer + .iter() + .zip(open_storage_files(storages_orderer.iter(), use_direct_io)); let mut buf_reader = storage_file_buf_reader(ACCOUNT_STORAGE_MAX_BUFFER_SIZE, use_page_cache, io_setup) .map_err(E::StorageFileBufReaderError)?; - for (storage, file) in storages_orderer.iter().zip(storage_files.iter()) { - let path_in_archive = Path::new(ACCOUNTS_DIR) - .join(AccountsFile::file_name(storage.slot(), storage.id())); - - buf_reader - .set_file(file.as_ref(), storage.accounts.len() as u64) - .map_err(|err| { - E::AccountStorageReaderError(err, storage.path().to_path_buf()) - })?; - let reader = - AccountStorageReader::new(storage, Some(snapshot_slot), &mut buf_reader) + let mut chunk = Vec::with_capacity(STORAGE_FILE_OPEN_CHUNK_SIZE); + loop { + chunk.clear(); + for (storage, file) in (&mut storage_file_pairs).take(STORAGE_FILE_OPEN_CHUNK_SIZE) + { + chunk.push((storage, file.map_err(E::StorageFileBufReaderError)?)); + } + if chunk.is_empty() { + break; + } + // Cheaply re-bind the reader to scope file borrows to this chunk. + let mut chunk_reader = buf_reader.rebind().map_err(E::StorageFileBufReaderError)?; + + // Queue the whole chunk for read-ahead before consuming any of + // it, so the io_uring pipeline can saturate across files. + for (storage, file) in &chunk { + chunk_reader + .add_file_to_prefetch(file.as_ref(), storage.accounts.len() as FileSize) + .map_err(E::StorageFileBufReaderError)?; + } + + for (storage, file) in &chunk { + let path_in_archive = Path::new(ACCOUNTS_DIR) + .join(AccountsFile::file_name(storage.slot(), storage.id())); + + chunk_reader + .set_file(file.as_ref(), storage.accounts.len() as FileSize) .map_err(|err| { E::AccountStorageReaderError(err, storage.path().to_path_buf()) })?; - let mut header = tar::Header::new_gnu(); - header.set_path(path_in_archive).map_err(|err| { - E::ArchiveAccountStorageFile(err, storage.path().to_path_buf()) - })?; - header.set_size(reader.len() as u64); - header.set_cksum(); - archive.append(&header, reader).map_err(|err| { - E::ArchiveAccountStorageFile(err, storage.path().to_path_buf()) - })?; + let reader = + AccountStorageReader::new(storage, Some(snapshot_slot), &mut chunk_reader) + .map_err(|err| { + E::AccountStorageReaderError(err, storage.path().to_path_buf()) + })?; + let mut header = tar::Header::new_gnu(); + header.set_path(path_in_archive).map_err(|err| { + E::ArchiveAccountStorageFile(err, storage.path().to_path_buf()) + })?; + header.set_size(reader.len() as u64); + header.set_cksum(); + archive.append(&header, reader).map_err(|err| { + E::ArchiveAccountStorageFile(err, storage.path().to_path_buf()) + })?; + } + + buf_reader = chunk_reader + .rebind() + .map_err(E::StorageFileBufReaderError)?; } archive.into_inner().map_err(E::FinishArchive)?; From 0229f6b8a61de1e800bceb737b3058f5c81b8613 Mon Sep 17 00:00:00 2001 From: Faycel Kouteib Date: Tue, 26 May 2026 14:22:09 -0700 Subject: [PATCH 058/576] Enable agave-unstable-api for self-referencing dev-dependencies (#12746) --- rpc/Cargo.toml | 2 +- unified-scheduler-pool/Cargo.toml | 2 +- votor-messages/Cargo.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/rpc/Cargo.toml b/rpc/Cargo.toml index 6b9d3f3e8b1..4f72fad0480 100644 --- a/rpc/Cargo.toml +++ b/rpc/Cargo.toml @@ -113,7 +113,7 @@ solana-nonce-account = { workspace = true } solana-program-option = { workspace = true } solana-program-runtime = { path = "../program-runtime", features = ["agave-unstable-api"] } solana-rent = { workspace = true } -solana-rpc = { path = ".", features = ["dev-context-only-utils"] } +solana-rpc = { path = ".", features = ["agave-unstable-api", "dev-context-only-utils"] } solana-runtime = { path = "../runtime", features = ["agave-unstable-api", "dev-context-only-utils"] } solana-runtime-transaction = { path = "../runtime-transaction", features = ["agave-unstable-api", "dev-context-only-utils"] } solana-sdk-ids = { workspace = true } diff --git a/unified-scheduler-pool/Cargo.toml b/unified-scheduler-pool/Cargo.toml index 920a4b1c3e8..d821b010894 100644 --- a/unified-scheduler-pool/Cargo.toml +++ b/unified-scheduler-pool/Cargo.toml @@ -49,7 +49,7 @@ solana-keypair = { workspace = true } solana-runtime = { path = "../runtime", features = ["agave-unstable-api", "dev-context-only-utils"] } solana-system-transaction = { workspace = true } # See order-crates-for-publishing.py for using this unusual `path = "."` -solana-unified-scheduler-pool = { path = ".", features = ["dev-context-only-utils"] } +solana-unified-scheduler-pool = { path = ".", features = ["agave-unstable-api", "dev-context-only-utils"] } test-case = { workspace = true } [lints] diff --git a/votor-messages/Cargo.toml b/votor-messages/Cargo.toml index 77ed23ecc76..be52ed32ade 100644 --- a/votor-messages/Cargo.toml +++ b/votor-messages/Cargo.toml @@ -44,7 +44,7 @@ thiserror = { workspace = true } wincode = { workspace = true } [dev-dependencies] -agave-votor-messages = { path = ".", features = ["dev-context-only-utils"] } +agave-votor-messages = { path = ".", features = ["agave-unstable-api", "dev-context-only-utils"] } tempfile = { workspace = true } [lints] From 2e6926904a89b43ec55507d8f43bf391b9dc0106 Mon Sep 17 00:00:00 2001 From: mjain-jump <150074777+mjain-jump@users.noreply.github.com> Date: Tue, 26 May 2026 17:28:33 -0400 Subject: [PATCH 059/576] runtime: fix InvokeContext::push() ordering (#12748) --- program-runtime/src/invoke_context.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/program-runtime/src/invoke_context.rs b/program-runtime/src/invoke_context.rs index 0a8056a00e7..c4a998cec9c 100644 --- a/program-runtime/src/invoke_context.rs +++ b/program-runtime/src/invoke_context.rs @@ -296,8 +296,9 @@ impl<'a, 'ix_data> InvokeContext<'a, 'ix_data> { } } + self.transaction_context.push()?; self.memory_contexts.push_placeholder(); - self.transaction_context.push() + Ok(()) } /// Pop a stack frame from the invocation stack From 1989ee8fd1feea77b65dfc4c96ecddd8a0e93369 Mon Sep 17 00:00:00 2001 From: steviez Date: Tue, 26 May 2026 17:35:47 -0500 Subject: [PATCH 060/576] test-validator: Cleanup TestValidatorNodeConfig for CI (#12727) TestValidatorNodeConfig currently chooses a port_range field based on whether debug assertions are enabled. There are a handful of unit tests that create a TestValidator object so this logic aims to differentiate between the test-validator bin (disabled) and unit tests (enabled) While this assumption is probably valid for most cases, it is a bit of a hack and complicates some other logic. So, remove the debug-assertion switch in favor of an extra default_for_tests() constructor --- Cargo.lock | 1 + accounts-cluster-bench/Cargo.toml | 2 +- bench-tps/Cargo.toml | 2 +- bench-tps/tests/bench_tps.rs | 2 +- cli/Cargo.toml | 2 +- cli/tests/program.rs | 4 +-- cli/tests/stake.rs | 2 +- client-test/Cargo.toml | 2 +- programs/sbf/Cargo.lock | 1 + programs/sbf/Cargo.toml | 2 ++ programs/sbf/tests/simulation.rs | 2 +- rpc-test/Cargo.toml | 1 + rpc-test/tests/nonblocking.rs | 8 +++-- test-validator/Cargo.toml | 3 ++ test-validator/src/lib.rs | 56 ++++++++++++++++++++++++------- tokens/Cargo.toml | 2 +- 16 files changed, 67 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 142eb00af51..8b0a41e5fb0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10932,6 +10932,7 @@ dependencies = [ "solana-signer", "solana-streamer", "solana-syscalls", + "solana-test-validator", "solana-transaction", "solana-validator-exit", "tokio", diff --git a/accounts-cluster-bench/Cargo.toml b/accounts-cluster-bench/Cargo.toml index d474b18cda9..73cfed028d6 100644 --- a/accounts-cluster-bench/Cargo.toml +++ b/accounts-cluster-bench/Cargo.toml @@ -57,7 +57,7 @@ solana-local-cluster = { path = "../local-cluster", features = ["agave-unstable- solana-native-token = { workspace = true } solana-poh-config = { workspace = true } solana-runtime = { path = "../runtime", features = ["agave-unstable-api", "dev-context-only-utils"] } -solana-test-validator = { path = "../test-validator" } +solana-test-validator = { path = "../test-validator", features = ["dev-context-only-utils"] } [lints] workspace = true diff --git a/bench-tps/Cargo.toml b/bench-tps/Cargo.toml index 174d22d51c7..c8fe8e7513c 100644 --- a/bench-tps/Cargo.toml +++ b/bench-tps/Cargo.toml @@ -81,7 +81,7 @@ solana-local-cluster = { workspace = true } solana-rent = { workspace = true } solana-runtime = { workspace = true, features = ["dev-context-only-utils"] } solana-sdk-ids = { workspace = true } -solana-test-validator = { workspace = true } +solana-test-validator = { workspace = true, features = ["dev-context-only-utils"] } solana-tps-client = { workspace = true, features = ["bank-client"] } tempfile = { workspace = true } diff --git a/bench-tps/tests/bench_tps.rs b/bench-tps/tests/bench_tps.rs index 96303cc22b0..d5dfb8ebc47 100644 --- a/bench-tps/tests/bench_tps.rs +++ b/bench-tps/tests/bench_tps.rs @@ -86,7 +86,7 @@ fn create_test_validator_and_client() -> ( 0, /* port */ ); - let test_validator = TestValidatorGenesis::default() + let test_validator = TestValidatorGenesis::default_for_tests() .fee_rate_governor(FeeRateGovernor::new(0, 0)) .rent(Rent { lamports_per_byte: 1, diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 4fa2db42af1..339ff8d034b 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -106,7 +106,7 @@ solana-nonce-account = { workspace = true } solana-presigner = { workspace = true } solana-rpc = { path = "../rpc", features = ["agave-unstable-api"] } solana-sha256-hasher = { workspace = true } -solana-test-validator = { path = "../test-validator" } +solana-test-validator = { path = "../test-validator", features = ["dev-context-only-utils"] } tempfile = { workspace = true } test-case = { workspace = true } diff --git a/cli/tests/program.rs b/cli/tests/program.rs index 3b4a8023654..3117f075569 100644 --- a/cli/tests/program.rs +++ b/cli/tests/program.rs @@ -59,7 +59,7 @@ fn test_validator_genesis( mint_keypair: &Keypair, features: LoaderV3Features, ) -> TestValidatorGenesis { - let mut genesis = TestValidatorGenesis::default(); + let mut genesis = TestValidatorGenesis::default_for_tests(); genesis .fee_rate_governor(FeeRateGovernor::new(0, 0)) .rent(Rent { @@ -3235,7 +3235,7 @@ async fn test_cli_program_deploy_with_args(compute_unit_price: Option, use_ let mint_keypair = Keypair::new(); let faucet_addr = run_local_faucet_with_unique_port_for_tests(mint_keypair.insecure_clone()); - let test_validator = TestValidatorGenesis::default() + let test_validator = TestValidatorGenesis::default_for_tests() .fee_rate_governor(FeeRateGovernor::new(0, 0)) .rent(Rent { lamports_per_byte: 1, diff --git a/cli/tests/stake.rs b/cli/tests/stake.rs index 5756f64c814..5bf74b5f5eb 100644 --- a/cli/tests/stake.rs +++ b/cli/tests/stake.rs @@ -40,7 +40,7 @@ async fn test_stake_delegation_force() { let authorized_withdrawer = Keypair::new().pubkey(); let faucet_addr = run_local_faucet_with_unique_port_for_tests(mint_keypair.insecure_clone()); let slots_per_epoch = 32; - let test_validator = TestValidatorGenesis::default() + let test_validator = TestValidatorGenesis::default_for_tests() .fee_rate_governor(FeeRateGovernor::new(0, 0)) .rent(Rent { lamports_per_byte: 1, diff --git a/client-test/Cargo.toml b/client-test/Cargo.toml index d9380134c6b..0b6f6b05961 100644 --- a/client-test/Cargo.toml +++ b/client-test/Cargo.toml @@ -35,7 +35,6 @@ solana-runtime = { workspace = true } solana-signer = { workspace = true } solana-system-interface = { workspace = true } solana-system-transaction = { workspace = true } -solana-test-validator = { workspace = true } solana-transaction-status = { workspace = true } solana-version = { workspace = true } systemstat = { workspace = true } @@ -47,6 +46,7 @@ agave-logger = { path = "../logger", features = ["agave-unstable-api"] } solana-net-utils = { path = "../net-utils", features = ["agave-unstable-api"] } solana-rpc = { path = "../rpc", features = ["agave-unstable-api", "dev-context-only-utils"] } solana-runtime = { path = "../runtime", features = ["agave-unstable-api", "dev-context-only-utils"] } +solana-test-validator = { path = "../test-validator", features = ["dev-context-only-utils"] } [lints] workspace = true diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index 0db007021cf..ba95f0e3b6f 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -8352,6 +8352,7 @@ dependencies = [ "solana-svm-type-overrides", "solana-system-interface 3.2.0", "solana-sysvar", + "solana-test-validator", "solana-transaction", "solana-transaction-error", "solana-vote", diff --git a/programs/sbf/Cargo.toml b/programs/sbf/Cargo.toml index 207eee70023..3982cd5c40d 100644 --- a/programs/sbf/Cargo.toml +++ b/programs/sbf/Cargo.toml @@ -148,6 +148,7 @@ solana-svm-transaction = { path = "../../svm-transaction", version = "=4.2.0-alp solana-svm-type-overrides = { path = "../../svm-type-overrides", version = "=4.2.0-alpha.0" } solana-system-interface = { version = "=3.2", features = ["bincode"] } solana-sysvar = "=4.0.0" +solana-test-validator = { path = "../../test-validator", version = "=4.2.0-alpha.0" } solana-vote = { path = "../../vote", version = "=4.2.0-alpha.0" } solana-vote-interface = "6.0.0" solana-vote-program = { path = "../../programs/vote", version = "=4.2.0-alpha.0" } @@ -208,6 +209,7 @@ solana-svm-transaction = { workspace = true } solana-svm-type-overrides = { workspace = true } solana-system-interface = { workspace = true } solana-sysvar = "4.0.0" +solana-test-validator = { workspace = true, features = ["dev-context-only-utils"] } solana-transaction = "4.1.0" solana-transaction-error = "3.2.0" solana-vote = { workspace = true } diff --git a/programs/sbf/tests/simulation.rs b/programs/sbf/tests/simulation.rs index a2a874e5252..cf6674ae8b9 100644 --- a/programs/sbf/tests/simulation.rs +++ b/programs/sbf/tests/simulation.rs @@ -61,7 +61,7 @@ fn test_no_panic_rpc_client() { agave_logger::setup(); let program_id = Pubkey::new_unique(); - let (test_validator, payer) = TestValidatorGenesis::default() + let (test_validator, payer) = TestValidatorGenesis::default_for_tests() .add_program("solana_sbf_rust_simulation", program_id) .start(); let rpc_client = test_validator.get_rpc_client(); diff --git a/rpc-test/Cargo.toml b/rpc-test/Cargo.toml index 08d627035d3..0b76288bdb4 100644 --- a/rpc-test/Cargo.toml +++ b/rpc-test/Cargo.toml @@ -50,6 +50,7 @@ solana-rent = { workspace = true } solana-signature = { workspace = true } solana-signer = { workspace = true } solana-system-transaction = { workspace = true } +solana-test-validator = { workspace = true, features = ["dev-context-only-utils"] } solana-transaction = { workspace = true } [lints] diff --git a/rpc-test/tests/nonblocking.rs b/rpc-test/tests/nonblocking.rs index c276d80ad65..05e1a50e1ff 100644 --- a/rpc-test/tests/nonblocking.rs +++ b/rpc-test/tests/nonblocking.rs @@ -15,7 +15,9 @@ use { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_tpu_send_transaction() { - let (test_validator, mint_keypair) = TestValidatorGenesis::default().start_async().await; + let (test_validator, mint_keypair) = TestValidatorGenesis::default_for_tests() + .start_async() + .await; let rpc_client = Arc::new(test_validator.get_async_rpc_client()); let mut tpu_client = TpuClient::new( "tpu_client_test", @@ -49,7 +51,9 @@ async fn test_tpu_send_transaction() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_tpu_cache_slot_updates() { - let (test_validator, _) = TestValidatorGenesis::default().start_async().await; + let (test_validator, _) = TestValidatorGenesis::default_for_tests() + .start_async() + .await; let rpc_client = Arc::new(test_validator.get_async_rpc_client()); let exit = Arc::new(AtomicBool::new(false)); let mut leader_tpu_service = LeaderTpuService::new( diff --git a/test-validator/Cargo.toml b/test-validator/Cargo.toml index 5b0a06c658b..01fe168573d 100644 --- a/test-validator/Cargo.toml +++ b/test-validator/Cargo.toml @@ -13,6 +13,7 @@ targets = ["x86_64-unknown-linux-gnu"] [features] agave-unstable-api = [] +dev-context-only-utils = [] [dependencies] agave-feature-set = { workspace = true } @@ -65,6 +66,8 @@ tokio = { workspace = true, features = ["full"] } [dev-dependencies] solana-sdk-ids = { workspace = true } +# See order-crates-for-publishing.py for using this unusual `path = "."` +solana-test-validator = { path = ".", features = ["dev-context-only-utils"] } [lints] workspace = true diff --git a/test-validator/src/lib.rs b/test-validator/src/lib.rs index 3a6bc616520..7e5d4cb5303 100644 --- a/test-validator/src/lib.rs +++ b/test-validator/src/lib.rs @@ -107,9 +107,21 @@ pub struct TestValidatorNodeConfig { impl Default for TestValidatorNodeConfig { fn default() -> Self { let bind_ip_addr = IpAddr::V4(Ipv4Addr::LOCALHOST); - #[cfg(not(debug_assertions))] let port_range = solana_net_utils::VALIDATOR_PORT_RANGE; - #[cfg(debug_assertions)] + Self { + gossip_addr: SocketAddr::new(bind_ip_addr, port_range.0), + port_range, + bind_ip_addr, + } + } +} + +#[cfg(feature = "dev-context-only-utils")] +impl TestValidatorNodeConfig { + /// Defaults suitable for unit tests; a disjoint port range will be used to + /// avoid "port already in use" errors for tests running in parallel + pub fn default_for_tests() -> Self { + let bind_ip_addr = IpAddr::V4(Ipv4Addr::LOCALHOST); let port_range = solana_net_utils::sockets::localhost_port_range_for_tests(); Self { gossip_addr: SocketAddr::new(bind_ip_addr, port_range.0), @@ -188,6 +200,18 @@ impl Default for TestValidatorGenesis { } } +#[cfg(feature = "dev-context-only-utils")] +impl TestValidatorGenesis { + /// Defaults suitable for unit tests; a disjoint port range will be used to + /// avoid "port already in use" errors for tests running in parallel + pub fn default_for_tests() -> Self { + Self { + node_config: TestValidatorNodeConfig::default_for_tests(), + ..Self::default() + } + } +} + fn try_transform_program_data( address: &Pubkey, account: &mut AccountSharedData, @@ -807,12 +831,13 @@ pub struct TestValidator { impl TestValidator { /// Create a configured genesis and start validator /// Sync only; calling from a tokio runtime will panic due to nested runtimes. + #[cfg(feature = "dev-context-only-utils")] pub fn start_with_config( mint_address: Pubkey, faucet_addr: Option, socket_addr_space: SocketAddrSpace, ) -> Self { - TestValidatorGenesis::default() + TestValidatorGenesis::default_for_tests() .rent(Rent { lamports_per_byte: 1, ..Rent::default() @@ -823,12 +848,13 @@ impl TestValidator { } /// Create a configured genesis and start validator (async version) + #[cfg(feature = "dev-context-only-utils")] pub async fn async_start_with_config( mint_keypair: &Keypair, faucet_addr: Option, socket_addr_space: SocketAddrSpace, ) -> Self { - TestValidatorGenesis::default() + TestValidatorGenesis::default_for_tests() .rent(Rent { lamports_per_byte: 1, ..Rent::default() @@ -1364,14 +1390,16 @@ mod test { #[test] fn get_health() { - let (test_validator, _payer) = TestValidatorGenesis::default().start(); + let (test_validator, _payer) = TestValidatorGenesis::default_for_tests().start(); let rpc_client = test_validator.get_rpc_client(); rpc_client.get_health().expect("health"); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn nonblocking_get_health() { - let (test_validator, _payer) = TestValidatorGenesis::default().start_async().await; + let (test_validator, _payer) = TestValidatorGenesis::default_for_tests() + .start_async() + .await; let rpc_client = test_validator.get_async_rpc_client(); rpc_client.get_health().await.expect("health"); } @@ -1379,7 +1407,7 @@ mod test { #[test] fn test_upgradeable_program_deploayment() { let program_id = Pubkey::new_unique(); - let (test_validator, payer) = TestValidatorGenesis::default() + let (test_validator, payer) = TestValidatorGenesis::default_for_tests() .add_program("../programs/bpf-loader-tests/noop", program_id) .start(); let rpc_client = test_validator.get_rpc_client(); @@ -1406,7 +1434,7 @@ mod test { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_nonblocking_upgradeable_program_deploayment() { let program_id = Pubkey::new_unique(); - let (test_validator, payer) = TestValidatorGenesis::default() + let (test_validator, payer) = TestValidatorGenesis::default_for_tests() .add_program("../programs/bpf-loader-tests/noop", program_id) .start_async() .await; @@ -1436,7 +1464,7 @@ mod test { #[should_panic] async fn document_tokio_panic() { // `start()` blows up when run within tokio - let (_test_validator, _payer) = TestValidatorGenesis::default().start(); + let (_test_validator, _payer) = TestValidatorGenesis::default_for_tests().start(); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -1460,7 +1488,7 @@ mod test { // Convert to `Vec` so we can get a slice. let control: Vec = control.into_iter().collect(); - let (test_validator, _payer) = TestValidatorGenesis::default() + let (test_validator, _payer) = TestValidatorGenesis::default_for_tests() .deactivate_features(&deactivate_features) .start_async() .await; @@ -1495,7 +1523,7 @@ mod test { let owner = Pubkey::new_unique(); let account = || AccountSharedData::new(100_000, 0, &owner); - let (test_validator, _payer) = TestValidatorGenesis::default() + let (test_validator, _payer) = TestValidatorGenesis::default_for_tests() .deactivate_features(&[with_deactivate_flag]) // Just deactivate one feature. .add_accounts([ (with_deactivate_flag, account()), // But add both accounts. @@ -1527,7 +1555,9 @@ mod test { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_core_bpf_programs() { - let (test_validator, _payer) = TestValidatorGenesis::default().start_async().await; + let (test_validator, _payer) = TestValidatorGenesis::default_for_tests() + .start_async() + .await; let rpc_client = test_validator.get_async_rpc_client(); @@ -1565,7 +1595,7 @@ mod test { #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_wait_for_program_with_unfunded_payer() { let program_id = Pubkey::new_unique(); - let (test_validator, _mint_keypair) = TestValidatorGenesis::default() + let (test_validator, _mint_keypair) = TestValidatorGenesis::default_for_tests() .add_program("../programs/bpf-loader-tests/noop", program_id) .start_async() .await; diff --git a/tokens/Cargo.toml b/tokens/Cargo.toml index 2e4310025df..69fbeb967e9 100644 --- a/tokens/Cargo.toml +++ b/tokens/Cargo.toml @@ -61,7 +61,7 @@ thiserror = { workspace = true } agave-logger = { path = "../logger", features = ["agave-unstable-api"] } assert_matches = { workspace = true } bincode = { workspace = true } -solana-test-validator = { path = "../test-validator" } +solana-test-validator = { path = "../test-validator", features = ["dev-context-only-utils"] } solana-transaction-error = { workspace = true } [lints] From 907277a6b64fcdf72860597a877e34ec42c3368e Mon Sep 17 00:00:00 2001 From: Faycel Kouteib Date: Tue, 26 May 2026 18:22:36 -0700 Subject: [PATCH 061/576] Enable agave-unstable-api in program-runtime dev dependency (#12747) --- program-runtime/Cargo.toml | 2 +- program-runtime/src/loaded_programs.rs | 12 ++++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/program-runtime/Cargo.toml b/program-runtime/Cargo.toml index ce51107bcb1..f0c5c4ea6cc 100644 --- a/program-runtime/Cargo.toml +++ b/program-runtime/Cargo.toml @@ -81,7 +81,7 @@ solana-account-info = { workspace = true } solana-instruction = { workspace = true, features = ["bincode"] } solana-instruction-error = { workspace = true, features = ["serde"] } solana-keypair = { workspace = true } -solana-program-runtime = { path = ".", features = ["dev-context-only-utils"] } +solana-program-runtime = { path = ".", features = ["agave-unstable-api", "dev-context-only-utils"] } solana-pubkey = { workspace = true, features = ["rand"] } solana-signer = { workspace = true } solana-transaction = { workspace = true, features = ["dev-context-only-utils"] } diff --git a/program-runtime/src/loaded_programs.rs b/program-runtime/src/loaded_programs.rs index 1f4f487deae..b55b3d526da 100644 --- a/program-runtime/src/loaded_programs.rs +++ b/program-runtime/src/loaded_programs.rs @@ -970,15 +970,11 @@ pub(crate) mod tests { solana_clock::Slot, solana_pubkey::Pubkey, solana_sbpf::{elf::Executable, program::BuiltinProgram}, - std::{ - fs::File, - io::Read, - ops::ControlFlow, - sync::{ - Arc, RwLock, - atomic::{AtomicU64, Ordering}, - }, + solana_svm_type_overrides::sync::{ + Arc, RwLock, + atomic::{AtomicU64, Ordering}, }, + std::{fs::File, io::Read, ops::ControlFlow}, test_case::{test_case, test_matrix}, }; From 1ada86f78221c19785ed52948d3e0bcab358fa13 Mon Sep 17 00:00:00 2001 From: Alex Pyattaev Date: Wed, 27 May 2026 10:56:22 +0300 Subject: [PATCH 062/576] turbine: use bounded channels in broadcast (#12257) * turbine: use bounded channels in broadcast * error! in logs when broadcast stage does not keep up with shred insert or send --- turbine/src/broadcast_stage.rs | 61 +++++++++++++++++-- .../broadcast_duplicates_run.rs | 17 +++--- .../src/broadcast_stage/broadcast_metrics.rs | 2 +- .../broadcast_stage/standard_broadcast_run.rs | 10 ++- 4 files changed, 70 insertions(+), 20 deletions(-) diff --git a/turbine/src/broadcast_stage.rs b/turbine/src/broadcast_stage.rs index 708b997eb6a..b6750593ca0 100644 --- a/turbine/src/broadcast_stage.rs +++ b/turbine/src/broadcast_stage.rs @@ -11,7 +11,9 @@ use { cluster_nodes::{ClusterNodes, ClusterNodesCache}, }, agave_votor::event::VotorEventSender, - crossbeam_channel::{Receiver, RecvError, RecvTimeoutError, Sender, unbounded}, + crossbeam_channel::{ + Receiver, RecvError, RecvTimeoutError, SendError, Sender, TrySendError, bounded, + }, itertools::Itertools, solana_clock::Slot, solana_gossip::{ @@ -21,7 +23,9 @@ use { solana_keypair::Keypair, solana_leader_schedule::NUM_CONSECUTIVE_LEADER_SLOTS, solana_ledger::{ - blockstore::Blockstore, leader_schedule_cache::LeaderScheduleCache, shred::Shred, + blockstore::Blockstore, + leader_schedule_cache::LeaderScheduleCache, + shred::{MAX_FEC_SETS_PER_SLOT, Shred}, }, solana_measure::measure::Measure, solana_metrics::inc_new_counter_error, @@ -60,9 +64,54 @@ const _: () = const { const CLUSTER_NODES_CACHE_NUM_EPOCH_CAP: usize = MAX_LEADER_SCHEDULE_STAKES as usize; const CLUSTER_NODES_CACHE_TTL: Duration = Duration::from_secs(5); +// Capacity in batches. One batch typically packs 1-2 FEC sets worth of bytes +// (see get_target_batch_bytes_default), so this is at least 4 slots' worth of +// broadcast traffic. Filling the channel means the consumer (blockstore record +// or socket transmit) has fallen at least 4 slots behind, which means we've +// been skipped already. +const BROADCAST_CHANNEL_CAPACITY: usize = MAX_FEC_SETS_PER_SLOT as usize * 4; + pub(crate) type RecordReceiver = Receiver<(Arc>, Option)>; pub(crate) type TransmitReceiver = Receiver<(Arc>, Option)>; +/// Try to send into a bounded channel. If the channel is full, +/// log an error and fall back to a blocking send so shreds are not dropped. +/// A full channel means there is something very wrong with our node, +/// so we want a loud signal but must preserve shreds so the validator +/// does not start repairing its own blocks. +fn try_send_or_block( + sender: &Sender, + msg: T, + channel_name: &str, +) -> std::result::Result<(), SendError> { + match sender.try_send(msg) { + Ok(()) => Ok(()), + Err(TrySendError::Full(msg)) => { + error!("The channel to {channel_name} is full, node is resource starved!"); + sender.send(msg) + } + Err(TrySendError::Disconnected(msg)) => Err(SendError(msg)), + } +} + +/// Fan a batch of shreds out to both the blockstore record path and the +/// network transmit path. Each leg uses [`try_send_or_block`] so any full +/// channel is logged loudly but never drops shreds. +#[allow(clippy::type_complexity)] +pub(crate) fn dispatch_shreds( + blockstore_sender: &Sender<(Arc>, Option)>, + socket_sender: &Sender<(Arc>, Option)>, + shreds: Arc>, + batch_info: Option, +) -> std::result::Result<(), SendError<(Arc>, Option)>> { + try_send_or_block( + blockstore_sender, + (shreds.clone(), batch_info.clone()), + "blockstore", + )?; + try_send_or_block(socket_sender, (shreds, batch_info), "network egress") +} + #[derive(Debug, Error)] pub enum Error { #[error(transparent)] @@ -275,8 +324,8 @@ impl BroadcastStage { mut broadcast_stage_run: impl BroadcastRun + Send + 'static + Clone, xdp_sender: Option, ) -> Self { - let (socket_sender, socket_receiver) = unbounded(); - let (blockstore_sender, blockstore_receiver) = unbounded(); + let (socket_sender, socket_receiver) = bounded(BROADCAST_CHANNEL_CAPACITY); + let (blockstore_sender, blockstore_receiver) = bounded(BROADCAST_CHANNEL_CAPACITY); let bs_run = broadcast_stage_run.clone(); let socket_sender_ = socket_sender.clone(); @@ -419,7 +468,7 @@ impl BroadcastStage { .all(|shred| shred.slot() == new_retransmit_slot) ); if !data_shreds.is_empty() { - socket_sender.send((data_shreds, None))?; + try_send_or_block(socket_sender, (data_shreds, None), "network egress")?; } let coding_shreds = Arc::new( @@ -434,7 +483,7 @@ impl BroadcastStage { .all(|shred| shred.slot() == new_retransmit_slot) ); if !coding_shreds.is_empty() { - socket_sender.send((coding_shreds, None))?; + try_send_or_block(socket_sender, (coding_shreds, None), "network egress")?; } } diff --git a/turbine/src/broadcast_stage/broadcast_duplicates_run.rs b/turbine/src/broadcast_stage/broadcast_duplicates_run.rs index dec8495b624..ed1ef56c008 100644 --- a/turbine/src/broadcast_stage/broadcast_duplicates_run.rs +++ b/turbine/src/broadcast_stage/broadcast_duplicates_run.rs @@ -263,8 +263,6 @@ impl BroadcastRun for BroadcastDuplicatesRun { if !data_shreds.is_empty() { let data_shreds = Arc::new(data_shreds); - blockstore_sender.send((data_shreds.clone(), None))?; - // 3) Start broadcast step info!( "{} Sending good shreds for slot {} to network", @@ -272,7 +270,7 @@ impl BroadcastRun for BroadcastDuplicatesRun { data_shreds.first().unwrap().slot() ); assert!(data_shreds.iter().all(|shred| shred.slot() == bank.slot())); - socket_sender.send((data_shreds, None))?; + dispatch_shreds(blockstore_sender, socket_sender, data_shreds, None)?; } // Special handling of last shred to cause partition @@ -294,9 +292,6 @@ impl BroadcastRun for BroadcastDuplicatesRun { let original_last_data_shred = Arc::new(original_last_data_shred); let partition_last_data_shred = Arc::new(partition_last_data_shred); - // Store the original shreds that this node replayed - blockstore_sender.send((original_last_data_shred.clone(), None))?; - assert!( original_last_data_shred .iter() @@ -311,7 +306,15 @@ impl BroadcastRun for BroadcastDuplicatesRun { if let Some(duplicate_slot_sender) = &self.config.duplicate_slot_sender { let _ = duplicate_slot_sender.send(bank.slot()); } - socket_sender.send((original_last_data_shred, None))?; + // Store the original shreds that this node replayed; + dispatch_shreds( + blockstore_sender, + socket_sender, + original_last_data_shred, + None, + )?; + // the partition shreds go only over the wire to create the duplicate slot. + // blocking here is ok since this is only ever used in tests. socket_sender.send((partition_last_data_shred, None))?; } diff --git a/turbine/src/broadcast_stage/broadcast_metrics.rs b/turbine/src/broadcast_stage/broadcast_metrics.rs index 57926b400e8..eab86ffd02e 100644 --- a/turbine/src/broadcast_stage/broadcast_metrics.rs +++ b/turbine/src/broadcast_stage/broadcast_metrics.rs @@ -307,7 +307,7 @@ mod test { let slot_broadcast_stats = Arc::new(Mutex::new(SlotBroadcastStats::default())); let num_threads = 5; let slot = 0; - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); let thread_handles: Vec<_> = (0..num_threads) .map(|i| { let slot_broadcast_stats = slot_broadcast_stats.clone(); diff --git a/turbine/src/broadcast_stage/standard_broadcast_run.rs b/turbine/src/broadcast_stage/standard_broadcast_run.rs index f40996a4855..d13b951dc34 100644 --- a/turbine/src/broadcast_stage/standard_broadcast_run.rs +++ b/turbine/src/broadcast_stage/standard_broadcast_run.rs @@ -305,8 +305,8 @@ impl StandardBroadcastRun { receive_results: ReceiveResults, bank_forks: &RwLock, ) -> Result<()> { - let (bsend, brecv) = unbounded(); - let (ssend, srecv) = unbounded(); + let (bsend, brecv) = bounded(BROADCAST_CHANNEL_CAPACITY); + let (ssend, srecv) = bounded(BROADCAST_CHANNEL_CAPACITY); self.process_receive_results( keypair, blockstore, @@ -362,8 +362,7 @@ impl StandardBroadcastRun { was_interrupted: true, }); let shreds = Arc::new(shreds); - socket_sender.send((shreds.clone(), batch_info.clone()))?; - blockstore_sender.send((shreds, batch_info))?; + dispatch_shreds(blockstore_sender, socket_sender, shreds, batch_info)?; } // If blockstore already has shreds for this slot, // it should not recreate the slot: @@ -463,8 +462,7 @@ impl StandardBroadcastRun { let shreds = Arc::new(shreds); debug_assert!(shreds.iter().all(|shred| shred.slot() == bank.slot())); - socket_sender.send((shreds.clone(), batch_info.clone()))?; - blockstore_sender.send((shreds, batch_info))?; + dispatch_shreds(blockstore_sender, socket_sender, shreds, batch_info)?; coding_send_time.stop(); From 08aa0d774f01aa4709c9a0951fe2d5ac4c0c2145 Mon Sep 17 00:00:00 2001 From: Faycel Kouteib Date: Wed, 27 May 2026 01:01:07 -0700 Subject: [PATCH 063/576] Votor: switch to wincode (#12673) * Switch votor crate to wincode * Review feedback. Clean up map_err(). new VoteHistoryVariants for ser/de and drop manual impls. Revert serialize_into() API changes. * Add test cases for deserialize error paths --- Cargo.lock | 1 - dev-bins/Cargo.lock | 1 - programs/sbf/Cargo.lock | 1 - votor/Cargo.toml | 1 - votor/src/consensus_pool.rs | 4 +- .../src/consensus_pool/certificate_builder.rs | 6 +- votor/src/consensus_pool_service.rs | 2 +- votor/src/consensus_rewards/entry.rs | 2 +- .../consensus_rewards/entry/partial_cert.rs | 2 +- votor/src/event_handler.rs | 4 +- votor/src/vote_history.rs | 8 +- votor/src/vote_history_storage.rs | 77 +++++++++++++------ votor/src/voting_utils.rs | 2 +- 13 files changed, 72 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8b0a41e5fb0..56412e695a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -435,7 +435,6 @@ dependencies = [ "agave-math-utils", "agave-votor", "agave-votor-messages", - "bincode", "bitvec", "crossbeam-channel", "itertools 0.14.0", diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 01efeb41630..2b8e14dabcb 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -349,7 +349,6 @@ dependencies = [ "agave-logger", "agave-math-utils", "agave-votor-messages", - "bincode", "bitvec", "crossbeam-channel", "itertools 0.14.0", diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index ba95f0e3b6f..a9045de1804 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -337,7 +337,6 @@ dependencies = [ "agave-logger", "agave-math-utils", "agave-votor-messages", - "bincode", "bitvec", "crossbeam-channel", "itertools 0.14.0", diff --git a/votor/Cargo.toml b/votor/Cargo.toml index 84c8918c662..50794e2696d 100644 --- a/votor/Cargo.toml +++ b/votor/Cargo.toml @@ -32,7 +32,6 @@ frozen-abi = [ agave-logger = { workspace = true } agave-math-utils = { workspace = true } agave-votor-messages = { workspace = true } -bincode = { workspace = true } bitvec = { workspace = true } crossbeam-channel = { workspace = true } itertools = { workspace = true } diff --git a/votor/src/consensus_pool.rs b/votor/src/consensus_pool.rs index fe637c5a6b7..b3c854bb7a7 100644 --- a/votor/src/consensus_pool.rs +++ b/votor/src/consensus_pool.rs @@ -812,7 +812,7 @@ mod tests { BLSKeypair::derive_from_signer(&keypairs[rank].vote_keypair, BLS_KEYPAIR_DERIVE_SEED) .unwrap(); let signature: BLSSignature = bls_keypair - .sign(bincode::serialize(vote).unwrap().as_slice()) + .sign(wincode::serialize(vote).unwrap().as_slice()) .into(); ConsensusMessage::new_vote(*vote, signature, rank as u16) } @@ -2098,7 +2098,7 @@ mod tests { BLSKeypair::derive_from_signer(validator_vote_keypair, BLS_KEYPAIR_DERIVE_SEED) .unwrap(); - let signed_message = bincode::serialize(&vote).unwrap(); + let signed_message = wincode::serialize(&vote).unwrap(); vote_message .signature .verify(&bls_keypair.public, &signed_message) diff --git a/votor/src/consensus_pool/certificate_builder.rs b/votor/src/consensus_pool/certificate_builder.rs index 993bd9532b9..9b367be4b75 100644 --- a/votor/src/consensus_pool/certificate_builder.rs +++ b/votor/src/consensus_pool/certificate_builder.rs @@ -460,7 +460,7 @@ mod tests { let mut keypairs = Vec::new(); let mut vote_messages = Vec::new(); let vote = Vote::new_notarization_vote(slot, hash); - let serialized_vote = bincode::serialize(&vote).unwrap(); + let serialized_vote = wincode::serialize(&vote).unwrap(); for i in 0..num_validators { let keypair = BLSKeypair::new(); @@ -502,7 +502,7 @@ mod tests { let mut all_pubkeys = Vec::new(); // Group 1: Signs a Notarize vote. let notarize_vote = Vote::new_notarization_vote(slot, hash); - let serialized_notarize_vote = bincode::serialize(¬arize_vote).unwrap(); + let serialized_notarize_vote = wincode::serialize(¬arize_vote).unwrap(); for i in 0..3 { let keypair = BLSKeypair::new(); let signature = keypair.sign(&serialized_notarize_vote); @@ -516,7 +516,7 @@ mod tests { // Group 2: Signs a NotarizeFallback vote. let notarize_fallback_vote = Vote::new_notarization_fallback_vote(slot, hash); - let serialized_fallback_vote = bincode::serialize(¬arize_fallback_vote).unwrap(); + let serialized_fallback_vote = wincode::serialize(¬arize_fallback_vote).unwrap(); for i in 3..6 { let keypair = BLSKeypair::new(); let signature = keypair.sign(&serialized_fallback_vote); diff --git a/votor/src/consensus_pool_service.rs b/votor/src/consensus_pool_service.rs index 8e6ffea44a9..1e31b13b216 100644 --- a/votor/src/consensus_pool_service.rs +++ b/votor/src/consensus_pool_service.rs @@ -708,7 +708,7 @@ mod tests { let vote_keypair = &ctx.validator_keypairs[my_rank].vote_keypair; let bls_keypair = BLSKeypair::derive_from_signer(vote_keypair, BLS_KEYPAIR_DERIVE_SEED).unwrap(); - let vote_serialized = bincode::serialize(¬arize_vote).unwrap(); + let vote_serialized = wincode::serialize(¬arize_vote).unwrap(); let message = ConsensusMessage::Vote(VoteMessage { vote: notarize_vote, signature: bls_keypair.sign(&vote_serialized).into(), diff --git a/votor/src/consensus_rewards/entry.rs b/votor/src/consensus_rewards/entry.rs index 711cc1beda5..b3ed4881236 100644 --- a/votor/src/consensus_rewards/entry.rs +++ b/votor/src/consensus_rewards/entry.rs @@ -141,7 +141,7 @@ mod tests { } pub(crate) fn new_vote(vote: Vote, rank: usize, keypairs: &[BlsKeypair]) -> VoteMessage { - let serialized = bincode::serialize(&vote).unwrap(); + let serialized = wincode::serialize(&vote).unwrap(); let signature = keypairs[rank].sign(&serialized).into(); VoteMessage { vote, diff --git a/votor/src/consensus_rewards/entry/partial_cert.rs b/votor/src/consensus_rewards/entry/partial_cert.rs index c4be4b65279..a8fbc51b775 100644 --- a/votor/src/consensus_rewards/entry/partial_cert.rs +++ b/votor/src/consensus_rewards/entry/partial_cert.rs @@ -112,7 +112,7 @@ mod tests { }; fn new_invalid_vote(vote: Vote, rank: usize) -> VoteMessage { - let serialized = bincode::serialize(&vote).unwrap(); + let serialized = wincode::serialize(&vote).unwrap(); let keypair = BlsKeypair::new(); let signature = keypair.sign(&serialized).into(); VoteMessage { diff --git a/votor/src/event_handler.rs b/votor/src/event_handler.rs index 68fb757d713..6e05935e835 100644 --- a/votor/src/event_handler.rs +++ b/votor/src/event_handler.rs @@ -1334,7 +1334,7 @@ mod tests { fn check_for_votes(&mut self, expected_votes: &[Vote]) { for v in expected_votes { - let expected_vote_serialized = bincode::serialize(v).unwrap(); + let expected_vote_serialized = wincode::serialize(v).unwrap(); let signature: BLSSignature = self.my_bls_keypair.sign(&expected_vote_serialized).into(); let expected_message = ConsensusMessage::Vote(VoteMessage { @@ -1354,7 +1354,7 @@ mod tests { } fn check_for_vote(&mut self, expected_vote: &Vote) { - let expected_vote_serialized = bincode::serialize(expected_vote).unwrap(); + let expected_vote_serialized = wincode::serialize(expected_vote).unwrap(); let signature: BLSSignature = self.my_bls_keypair.sign(&expected_vote_serialized).into(); let expected_message = ConsensusMessage::Vote(VoteMessage { diff --git a/votor/src/vote_history.rs b/votor/src/vote_history.rs index 877cc7337ad..cef64fd0c8b 100644 --- a/votor/src/vote_history.rs +++ b/votor/src/vote_history.rs @@ -10,6 +10,7 @@ use { solana_pubkey::Pubkey, std::collections::{HashMap, HashSet, hash_map::Entry}, thiserror::Error, + wincode::{SchemaRead, SchemaWrite}, }; #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] @@ -33,7 +34,7 @@ impl VoteHistoryVersions { derive(AbiExample), frozen_abi(digest = "9dp4rEVqAsT7mfiL5oEgWrxgWCUiEe4Fk8xJoTWwSN1X") )] -#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Default)] +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Default, SchemaWrite, SchemaRead)] pub struct VoteHistory { /// The validator identity that cast votes pub node_pubkey: Pubkey, @@ -274,7 +275,10 @@ pub enum VoteHistoryError { IoError(#[from] std::io::Error), #[error("Serialization Error: {0}")] - SerializeError(#[from] bincode::Error), + SerializeError(#[from] wincode::WriteError), + + #[error("Deserialization Error: {0}")] + DeserializeError(#[from] wincode::ReadError), #[error("The signature on the saved vote history is invalid")] InvalidSignature, diff --git a/votor/src/vote_history_storage.rs b/votor/src/vote_history_storage.rs index 18528bdcbb5..418571efc7f 100644 --- a/votor/src/vote_history_storage.rs +++ b/votor/src/vote_history_storage.rs @@ -7,15 +7,16 @@ use { solana_signer::Signer, std::{ fs::{self, File}, - io::{self, BufReader}, + io::{self, BufReader, BufWriter}, path::PathBuf, }, + wincode::{SchemaRead, SchemaWrite}, }; pub type Result = std::result::Result; #[cfg_attr(feature = "frozen-abi", derive(AbiExample))] -#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, SchemaWrite, SchemaRead)] pub enum SavedVoteHistoryVersions { Current(SavedVoteHistory), } @@ -30,25 +31,22 @@ impl SavedVoteHistoryVersions { if !t.signature.verify(node_pubkey.as_ref(), &t.data) { return Err(VoteHistoryError::InvalidSignature); } - bincode::deserialize(&t.data).map(VoteHistoryVersions::Current) + wincode::deserialize(&t.data).map(VoteHistoryVersions::Current)? } }; - vote_history - .map_err(|e| e.into()) - .and_then(|vote_history: VoteHistoryVersions| { - let vote_history = vote_history.convert_to_current(); - if vote_history.node_pubkey != *node_pubkey { - return Err(VoteHistoryError::WrongVoteHistory(format!( - "node_pubkey is {:?} but found vote history for {:?}", - node_pubkey, vote_history.node_pubkey - ))); - } - Ok(vote_history) - }) + let vote_history = vote_history.convert_to_current(); + if vote_history.node_pubkey != *node_pubkey { + return Err(VoteHistoryError::WrongVoteHistory(format!( + "node_pubkey is {:?} but found vote history for {:?}", + node_pubkey, vote_history.node_pubkey + ))); + } + Ok(vote_history) } fn serialize_into(&self, file: &mut File) -> Result<()> { - bincode::serialize_into(file, self).map_err(|e| e.into()) + wincode::serialize_into(BufWriter::new(file), self)?; + Ok(()) } fn pubkey(&self) -> Pubkey { @@ -69,12 +67,13 @@ impl From for SavedVoteHistoryVersions { derive(AbiExample), frozen_abi(digest = "J6vB6FWFT8CFEvxndXWes461hroo8Q5L9Wq9cv4FEzaQ") )] -#[derive(Default, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] +#[derive(Default, Clone, Serialize, Deserialize, Debug, PartialEq, Eq, SchemaWrite, SchemaRead)] pub struct SavedVoteHistory { signature: Signature, #[serde(with = "serde_bytes")] data: Vec, #[serde(skip)] + #[wincode(skip)] node_pubkey: Pubkey, } @@ -88,7 +87,7 @@ impl SavedVoteHistory { ))); } - let data = bincode::serialize(&vote_history)?; + let data = wincode::serialize(&vote_history)?; let signature = keypair.sign_message(&data); Ok(Self { signature, @@ -147,9 +146,8 @@ impl VoteHistoryStorage for FileVoteHistoryStorage { let file = File::open(&filename)?; let mut stream = BufReader::new(file); - bincode::deserialize_from(&mut stream) - .map_err(|e| e.into()) - .and_then(|t: SavedVoteHistoryVersions| t.try_into_vote_history(node_pubkey)) + let saved_vote_history: SavedVoteHistoryVersions = wincode::deserialize_from(&mut stream)?; + saved_vote_history.try_into_vote_history(node_pubkey) } fn store(&self, saved_vote_history: &SavedVoteHistoryVersions) -> Result<()> { @@ -172,7 +170,10 @@ impl VoteHistoryStorage for FileVoteHistoryStorage { #[cfg(test)] mod test { - use {super::*, agave_votor_messages::vote::Vote, solana_keypair::Keypair, tempfile::TempDir}; + use { + super::*, agave_votor_messages::vote::Vote, solana_keypair::Keypair, solana_signer::Signer, + tempfile::TempDir, + }; #[test] fn test_file_vote_history_storage() { @@ -238,4 +239,36 @@ mod test { storage.store(&saved_vote_history_versions).unwrap(); assert!(storage.load(&pubkey).is_err()); } + + #[test] + fn test_load_corrupt_vote_history_storage_returns_deserialize_error() { + let tmp_dir = TempDir::new().unwrap(); + let storage = FileVoteHistoryStorage::new(tmp_dir.path().to_path_buf()); + let pubkey = Pubkey::new_unique(); + + fs::write(storage.filename(&pubkey), [1, 2, 3]).unwrap(); + + let error = storage.load(&pubkey).err().unwrap(); + assert!(matches!(error, VoteHistoryError::DeserializeError(_))); + } + + #[test] + fn test_load_signed_corrupt_vote_history_data_returns_deserialize_error() { + let tmp_dir = TempDir::new().unwrap(); + let storage = FileVoteHistoryStorage::new(tmp_dir.path().to_path_buf()); + let keypair = Keypair::new(); + let pubkey = keypair.pubkey(); + let data = Vec::new(); + let saved_vote_history = SavedVoteHistory { + signature: keypair.sign_message(&data), + data, + node_pubkey: pubkey, + }; + storage + .store(&SavedVoteHistoryVersions::from(saved_vote_history)) + .unwrap(); + + let error = storage.load(&pubkey).err().unwrap(); + assert!(matches!(error, VoteHistoryError::DeserializeError(_))); + } } diff --git a/votor/src/voting_utils.rs b/votor/src/voting_utils.rs index 75f4f47a87d..1fdf58477a6 100644 --- a/votor/src/voting_utils.rs +++ b/votor/src/voting_utils.rs @@ -320,7 +320,7 @@ mod tests { vote: Vote, my_bls_keypair: &BLSKeypair, ) -> ConsensusMessage { - let vote_serialized = bincode::serialize(&vote).unwrap(); + let vote_serialized = wincode::serialize(&vote).unwrap(); let signature = my_bls_keypair.sign(&vote_serialized); ConsensusMessage::Vote(VoteMessage { vote, From f1728abb7a1936174fba144a1ce734c42e787ab3 Mon Sep 17 00:00:00 2001 From: Quentin Kniep Date: Wed, 27 May 2026 10:18:02 +0200 Subject: [PATCH 064/576] refactor: replace `SwitchBankEvent` channel with a single shared cell (#12593) --- core/src/replay_stage.rs | 21 +++++++++----------- core/src/tvu.rs | 14 +++++--------- votor/src/event.rs | 38 +++++++++++++++++++++++++++++++------ votor/src/event_handler.rs | 39 +++++++++++--------------------------- votor/src/votor.rs | 10 +++++----- 5 files changed, 62 insertions(+), 60 deletions(-) diff --git a/core/src/replay_stage.rs b/core/src/replay_stage.rs index 07e52504338..f705d7c5006 100644 --- a/core/src/replay_stage.rs +++ b/core/src/replay_stage.rs @@ -34,8 +34,7 @@ use { }, agave_votor::{ event::{ - CompletedBlock, LeaderWindowInfo, SwitchBankEvent, SwitchBankEventReceiver, VotorEvent, - VotorEventSender, + CompletedBlock, LatestSwitchRequest, LeaderWindowInfo, VotorEvent, VotorEventSender, }, root_utils, vote_history_storage::SavedVoteHistory, @@ -461,7 +460,7 @@ pub struct ReplayReceivers { pub gossip_verified_vote_hash_receiver: Receiver<(Pubkey, u64, Hash)>, pub popular_pruned_forks_receiver: Receiver>, pub bank_forks_controller_receiver: BankForksCommandReceiver, - pub switch_bank_receiver: SwitchBankEventReceiver, + pub latest_switch_request: LatestSwitchRequest, } /// Timing information for the ReplayStage main processing loop @@ -776,7 +775,7 @@ impl ReplayStage { gossip_verified_vote_hash_receiver, popular_pruned_forks_receiver, bank_forks_controller_receiver, - switch_bank_receiver, + latest_switch_request, } = receivers; trace!("replay stage"); @@ -1046,7 +1045,7 @@ impl ReplayStage { ); Self::process_switch_bank_events( &my_pubkey, - &switch_bank_receiver, + &latest_switch_request, &mut pending_switch, &blockstore, &bank_forks, @@ -2369,7 +2368,7 @@ impl ReplayStage { /// this in `pending_switch` fn process_switch_bank_events( my_pubkey: &Pubkey, - switch_bank_receiver: &SwitchBankEventReceiver, + latest_switch_request: &LatestSwitchRequest, pending_switch: &mut Option<(Slot, Hash)>, blockstore: &Blockstore, bank_forks: &RwLock, @@ -2378,13 +2377,11 @@ impl ReplayStage { ) -> Result<(), BlockstoreError> { let root = bank_forks.read().unwrap().root(); - if let Some(switch_bank_event) = switch_bank_receiver - .try_iter() - .max() - .filter(|SwitchBankEvent::Switch { slot, .. }| *slot > root) + if let Some((slot, block_id)) = latest_switch_request + .take() + .map(|ev| ev.block()) + .filter(|(slot, _)| *slot > root) { - let (slot, block_id) = switch_bank_event.block(); - // Overwrite the pending switch, later switches take precedence if Some(slot) >= pending_switch.map(|(slot, _)| slot) { if let Some(prev_switch_request) = pending_switch.replace((slot, block_id)) { diff --git a/core/src/tvu.rs b/core/src/tvu.rs index 1dcc8fb67de..ac2c77faff9 100644 --- a/core/src/tvu.rs +++ b/core/src/tvu.rs @@ -30,7 +30,7 @@ use { }, agave_votor::{ consensus_metrics::MAX_IN_FLIGHT_CONSENSUS_EVENTS, - event::{LeaderWindowInfo, VotorEventReceiver, VotorEventSender}, + event::{LatestSwitchRequest, LeaderWindowInfo, VotorEventReceiver, VotorEventSender}, generated_cert_types::GeneratedCertTypes, vote_history::VoteHistory, vote_history_storage::VoteHistoryStorage, @@ -428,12 +428,8 @@ impl Tvu { completed_slots_receiver, }; - // Create switch block event channel for ReplayStage - // We emit a switch bank event when we observe a ParentReady. - // The event is immediately consumed and the latest is stored in ReplayStage. - // We overprovision at 100 leader windows - we would require almost 3 minutes of stuck - // replay to hit the limit. - let (switch_bank_sender, switch_bank_receiver) = bounded(100); + // Shared latest switch-bank request from Votor to ReplayStage. + let latest_switch_request = LatestSwitchRequest::default(); let window_service = { let epoch_schedule = bank_forks @@ -522,7 +518,7 @@ impl Tvu { leader_window_info_sender, highest_parent_ready, event_sender: votor_event_sender.clone(), - switch_bank_sender, + latest_switch_request: latest_switch_request.clone(), own_vote_sender: consensus_message_sender.clone(), reward_certs_sender, repair_event_sender, @@ -566,7 +562,7 @@ impl Tvu { gossip_verified_vote_hash_receiver, popular_pruned_forks_receiver, bank_forks_controller_receiver, - switch_bank_receiver, + latest_switch_request, }; let replay_stage_config = ReplayStageConfig { diff --git a/votor/src/event.rs b/votor/src/event.rs index 24a120c53ef..27fc0435e85 100644 --- a/votor/src/event.rs +++ b/votor/src/event.rs @@ -4,7 +4,10 @@ use { solana_clock::Slot, solana_hash::Hash, solana_runtime::bank::Bank, - std::{sync::Arc, time::Instant}, + std::{ + sync::{Arc, Mutex}, + time::Instant, + }, }; #[derive(Debug, Clone)] @@ -119,13 +122,10 @@ impl RepairEvent { } } -pub type SwitchBankEventSender = Sender; -pub type SwitchBankEventReceiver = Receiver; - -/// Event sent to replay_stage when a bank needs to be switched as a result of a ParentReady +/// Event sent to replay_stage when a bank needs to be switched as a result of a ParentReady. #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] pub enum SwitchBankEvent { - /// We need to switch any existing banks to this bank including ancestors + /// We need to switch any existing banks to this bank including ancestors. Switch { slot: Slot, block_id: Hash }, } @@ -136,3 +136,29 @@ impl SwitchBankEvent { } } } + +/// Shared single-cell holding the most recent switch-bank request from votor to replay. +/// +/// Used instead of a channel because replay only ever acts on the latest event — buffering +/// older events doesn't help, and blocking on a full channel could lead to a stall in Votor. +/// Writer (votor) advances monotonically via [`Self::try_advance`]; reader (replay) pulls the +/// current value via [`Self::take`]. +#[derive(Clone, Default)] +pub struct LatestSwitchRequest(Arc>>); + +impl LatestSwitchRequest { + /// Records `event` as the latest pending request, iff it is strictly newer than what's + /// currently held. Returns the previous value (if any) when it was overwritten. + pub fn try_advance(&self, event: SwitchBankEvent) -> Option { + let mut guard = self.0.lock().unwrap(); + match guard.as_ref() { + Some(cur) if event <= *cur => None, + _ => guard.replace(event), + } + } + + /// Atomically takes the current request, leaving the cell empty. + pub fn take(&self) -> Option { + self.0.lock().unwrap().take() + } +} diff --git a/votor/src/event_handler.rs b/votor/src/event_handler.rs index 6e05935e835..63079a76dd8 100644 --- a/votor/src/event_handler.rs +++ b/votor/src/event_handler.rs @@ -6,7 +6,7 @@ use { commitment::{CommitmentType, update_commitment_cache}, consensus_metrics::ConsensusMetricsEvent, event::{ - CompletedBlock, RepairEvent, RepairEventSender, SwitchBankEvent, SwitchBankEventSender, + CompletedBlock, LatestSwitchRequest, RepairEvent, RepairEventSender, SwitchBankEvent, VotorEvent, VotorEventReceiver, }, event_handler::stats::EventHandlerStats, @@ -218,7 +218,7 @@ impl EventHandler { // We need to ensure that we've replayed the parent bank. if parent_slot > vctx.sharable_banks.root().slot() { - request_switch(&ctx.switch_bank_sender, *my_pubkey, parent_block)?; + request_switch(&ctx.latest_switch_request, *my_pubkey, parent_block); } let should_set_timeouts = vctx.vote_history.add_parent_ready(slot, parent_block); @@ -880,28 +880,14 @@ fn request_repair( } } -/// Sends a switch bank event to replay. -fn request_switch( - sender: &SwitchBankEventSender, - my_pubkey: Pubkey, - block: Block, -) -> Result<(), EventLoopError> { +/// Updates the latest switch-bank request for replay to consume. +fn request_switch(latest: &LatestSwitchRequest, my_pubkey: Pubkey, block: Block) { let (slot, block_id) = block; let event = SwitchBankEvent::Switch { slot, block_id }; - match sender.try_send(event) { - Ok(()) => Ok(()), - Err(TrySendError::Full(event)) => { - error!( - "{my_pubkey}: Switch bank event channel is full, this should not happen. Blocking \ - to send event for slot {slot}" - ); - sender - .send(event) - .map_err(|_| EventLoopError::SenderDisconnected(SendError(()))) - } - Err(TrySendError::Disconnected(_)) => { - Err(EventLoopError::SenderDisconnected(SendError(()))) - } + if let Some(prev) = latest.try_advance(event) { + trace!( + "{my_pubkey}: Overwriting previous switch request {prev:?} with ({slot}, {block_id})" + ); } } @@ -912,7 +898,7 @@ mod tests { crate::{ commitment::CommitmentAggregationData, consensus_metrics::ConsensusMetricsEventReceiver, - event::{LeaderWindowInfo, RepairEventReceiver, SwitchBankEventReceiver}, + event::{LeaderWindowInfo, RepairEventReceiver}, vote_history_storage::{ FileVoteHistoryStorage, SavedVoteHistory, SavedVoteHistoryVersions, VoteHistoryStorage, @@ -967,8 +953,6 @@ mod tests { consensus_metrics_receiver: ConsensusMetricsEventReceiver, #[allow(dead_code)] // Keep receiver alive to prevent SenderDisconnected errors repair_event_receiver: RepairEventReceiver, - #[allow(dead_code)] - switch_bank_receiver: SwitchBankEventReceiver, shared_context: SharedContext, voting_context: VotingContext, root_context: RootContext, @@ -1032,7 +1016,7 @@ mod tests { let (consensus_metrics_sender, consensus_metrics_receiver) = unbounded(); let (leader_window_info_sender, leader_window_info_receiver) = unbounded(); let (repair_event_sender, repair_event_receiver) = unbounded(); - let (switch_bank_sender, switch_bank_receiver) = unbounded(); + let latest_switch_request = LatestSwitchRequest::default(); let timer_manager = Arc::new(PlRwLock::new(TimerManager::new( event_sender, exit, @@ -1092,7 +1076,7 @@ mod tests { blockstore: blockstore.clone(), highest_parent_ready: highest_parent_ready.clone(), repair_event_sender, - switch_bank_sender, + latest_switch_request, }; let vote_history = VoteHistory::new(my_node_keypair.pubkey(), 0); @@ -1136,7 +1120,6 @@ mod tests { cluster_info, consensus_metrics_receiver, repair_event_receiver, - switch_bank_receiver, highest_parent_ready, shared_context, voting_context, diff --git a/votor/src/votor.rs b/votor/src/votor.rs index 9d596c4265c..8b1d8f47030 100644 --- a/votor/src/votor.rs +++ b/votor/src/votor.rs @@ -52,7 +52,7 @@ use { consensus_pool_service::{ConsensusPoolContext, ConsensusPoolService}, consensus_rewards::ConsensusRewardsService, event::{ - LeaderWindowInfo, RepairEventSender, SwitchBankEventSender, VotorEventReceiver, + LatestSwitchRequest, LeaderWindowInfo, RepairEventSender, VotorEventReceiver, VotorEventSender, }, event_handler::{EventHandler, EventHandlerContext}, @@ -119,7 +119,7 @@ pub struct VotorConfig { pub own_vote_sender: Sender>, pub reward_certs_sender: Sender, pub repair_event_sender: RepairEventSender, - pub switch_bank_sender: SwitchBankEventSender, + pub latest_switch_request: LatestSwitchRequest, // Receivers pub event_receiver: VotorEventReceiver, @@ -138,7 +138,7 @@ pub(crate) struct SharedContext { pub(crate) highest_parent_ready: Arc>, pub(crate) vote_history_storage: Arc, pub(crate) repair_event_sender: RepairEventSender, - pub(crate) switch_bank_sender: SwitchBankEventSender, + pub(crate) latest_switch_request: LatestSwitchRequest, } pub struct Votor { @@ -170,7 +170,7 @@ impl Votor { event_sender, own_vote_sender, repair_event_sender, - switch_bank_sender, + latest_switch_request, event_receiver, consensus_message_receiver, consensus_metrics_sender, @@ -197,7 +197,7 @@ impl Votor { leader_window_info_sender, vote_history_storage, repair_event_sender: repair_event_sender.clone(), - switch_bank_sender, + latest_switch_request, }; let voting_context = VotingContext { From 1b10efb5767efa626933c6a72696203edd972414 Mon Sep 17 00:00:00 2001 From: Joe C Date: Wed, 27 May 2026 16:19:16 +0800 Subject: [PATCH 065/576] svm: gate agave precompile deps behind conformance feature (#12733) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Splits the conformance harness callback into two: - `DefaultCallback` (always available) — uses trait defaults, no precompile support. - `ConformanceCallback` (gated on `conformance`) — full precompile support across all features, used by the protobuf FFI entrypoint. `agave-feature-set` and `agave-precompiles` move from `dev-context-only-utils` to `conformance`, since they're only needed for the conformance callback's precompile verification path. --- dev-bins/Cargo.lock | 2 -- programs/sbf/Cargo.lock | 2 -- svm/Cargo.toml | 4 ++-- svm/src/conformance/harness.rs | 20 ++++++++++++++------ 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 2b8e14dabcb..66a3a57682e 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -8884,8 +8884,6 @@ dependencies = [ name = "solana-svm" version = "4.2.0-alpha.0" dependencies = [ - "agave-feature-set", - "agave-precompiles", "ahash 0.8.12", "log", "percentage", diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index a9045de1804..cdd0a31f71c 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -9405,8 +9405,6 @@ dependencies = [ name = "solana-svm" version = "4.2.0-alpha.0" dependencies = [ - "agave-feature-set", - "agave-precompiles", "ahash 0.8.12", "log", "percentage", diff --git a/svm/Cargo.toml b/svm/Cargo.toml index 3e5c449fa4a..6bd86523947 100644 --- a/svm/Cargo.toml +++ b/svm/Cargo.toml @@ -20,6 +20,8 @@ name = "solana_svm" default = ["metrics"] agave-unstable-api = [] conformance = [ + "dep:agave-feature-set", + "dep:agave-precompiles", "dep:bincode", "dep:prost", "dep:protosol", @@ -28,8 +30,6 @@ conformance = [ "solana-pubkey/default", ] dev-context-only-utils = [ - "dep:agave-feature-set", - "dep:agave-precompiles", "dep:qualifier_attr", "dep:solana-bpf-loader-program", "dep:solana-compute-budget", diff --git a/svm/src/conformance/harness.rs b/svm/src/conformance/harness.rs index d21c69eb8f2..628162553e5 100644 --- a/svm/src/conformance/harness.rs +++ b/svm/src/conformance/harness.rs @@ -3,11 +3,8 @@ use { super::context::{InstrContext, InstrEffects}, crate::message_processor::process_message, - agave_feature_set::FeatureSet, - agave_precompiles::{get_precompile, is_precompile}, solana_compute_budget::compute_budget::ComputeBudget, solana_instruction::error::InstructionError, - solana_precompile_error::PrecompileError, solana_program_runtime::{ invoke_context::{EnvironmentConfig, InvokeContext, mock_compile_message}, loaded_programs::{ @@ -31,15 +28,25 @@ use { programs::{fill_program_cache_from_accounts, new_program_cache_with_builtins}, sysvar::fill_sysvar_cache_from_accounts, }, + agave_feature_set::FeatureSet, + agave_precompiles::{get_precompile, is_precompile}, prost::Message, protosol::protos::{InstrContext as ProtoInstrContext, InstrEffects as ProtoInstrEffects}, + solana_precompile_error::PrecompileError, std::ffi::c_int, }; -/// Default callback. Full precompile support. +/// Default callback. No precompile support. struct DefaultCallback; -impl InvokeContextCallback for DefaultCallback { +impl InvokeContextCallback for DefaultCallback {} + +/// Conformance callback. Full precompile support across all features. +#[cfg(feature = "conformance")] +struct ConformanceCallback; + +#[cfg(feature = "conformance")] +impl InvokeContextCallback for ConformanceCallback { fn is_precompile(&self, program_id: &Pubkey) -> bool { is_precompile(program_id, |_| true) } @@ -238,8 +245,9 @@ pub fn execute_instr_proto(input: ProtoInstrContext) -> ProtoInstrEffects { cache }; - execute_instr( + execute_instr_with_callback( &instr_context, + &ConformanceCallback, &compute_budget, &mut program_cache, &sysvar_cache, From 496500e3587affdedc6d7de9e6a4f9b685066109 Mon Sep 17 00:00:00 2001 From: Mykola Dzham Date: Wed, 27 May 2026 10:28:55 +0200 Subject: [PATCH 066/576] Promote stable to v4.0 and beta to v4.1 (#12736) --- ci/channel-overrides | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ci/channel-overrides b/ci/channel-overrides index 88579ad7687..da4556621cc 100644 --- a/ci/channel-overrides +++ b/ci/channel-overrides @@ -1,5 +1,5 @@ # Channel override file consumed by ci/channel-info.sh. # Single source of truth: edit on master only, no backports. # Empty = auto-detect. -PINNED_BETA_CHANNEL=v4.0 -PINNED_STABLE_CHANNEL=v3.1 +PINNED_BETA_CHANNEL=v4.1 +PINNED_STABLE_CHANNEL=v4.0 From 2c7d1fa05528da3045572cf573ca2b2ec721d140 Mon Sep 17 00:00:00 2001 From: kirill lykov Date: Wed, 27 May 2026 10:56:03 +0200 Subject: [PATCH 067/576] xdp: propagate error from TxLoopBuilder instead of panic on socket creation (#12504) propagate error from TxLoopBuilder instead of panic on socket creation --- xdp/src/transmitter.rs | 4 ++-- xdp/src/tx_loop.rs | 22 ++++++++++++++-------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/xdp/src/transmitter.rs b/xdp/src/transmitter.rs index 0dce51881e4..5508c2a60cd 100644 --- a/xdp/src/transmitter.rs +++ b/xdp/src/transmitter.rs @@ -219,7 +219,7 @@ impl TransmitterBuilder { use { caps::Capability::{CAP_BPF, CAP_NET_ADMIN, CAP_NET_RAW, CAP_PERFMON}, log::debug, - std::collections::HashSet, + std::{collections::HashSet, io}, }; let XdpConfig { interface: maybe_interface, @@ -285,7 +285,7 @@ impl TransmitterBuilder { let tx_loops = tx_loop_builders .into_iter() .map(|tx_loop_builder| tx_loop_builder.build()) - .collect::>(); + .collect::, io::Error>>()?; let tables_result = RoutingTables::from_netlink(RouteTable::Main); diff --git a/xdp/src/tx_loop.rs b/xdp/src/tx_loop.rs index 1edd31bec37..79887141fac 100644 --- a/xdp/src/tx_loop.rs +++ b/xdp/src/tx_loop.rs @@ -21,6 +21,7 @@ use { crossbeam_channel::{Receiver, Sender, TryRecvError}, libc::{_SC_PAGESIZE, sysconf}, std::{ + io, net::{IpAddr, SocketAddr, SocketAddrV4}, thread, time::Duration, @@ -81,7 +82,6 @@ pub struct TxLoopConfig { pub struct TxLoopBuilder { cpu_id: usize, - queue_id: QueueId, zero_copy: bool, src_mac: MacAddress, queue: DeviceQueue, @@ -138,7 +138,6 @@ impl TxLoopBuilder> { TxLoopBuilder { cpu_id, - queue_id, zero_copy, src_mac, queue, @@ -147,10 +146,9 @@ impl TxLoopBuilder> { } } - pub fn build(self) -> TxLoop> { + pub fn build(self) -> Result>, io::Error> { let TxLoopBuilder { cpu_id, - queue_id, zero_copy, src_mac, queue, @@ -158,8 +156,16 @@ impl TxLoopBuilder> { umem, } = self; - let Ok((socket, tx)) = Socket::tx(queue, umem, zero_copy, tx_size * 2, tx_size) else { - panic!("failed to create AF_XDP socket on queue {queue_id:?}"); + let queue_id = queue.id(); + let (socket, tx) = match Socket::tx(queue, umem, zero_copy, tx_size * 2, tx_size) { + Ok(socket_tx) => socket_tx, + Err(err) => { + log::error!( + "failed to create AF_XDP TX socket for queue {queue_id:?} on CPU {cpu_id}: \ + {err}" + ); + return Err(err); + } }; let Tx { @@ -170,13 +176,13 @@ impl TxLoopBuilder> { } = tx; let ring = ring.unwrap(); - TxLoop { + Ok(TxLoop { cpu_id, src_mac, socket, ring, completion, - } + }) } } From b2eef0aa6f755484b894b8419a5c03756afbd76a Mon Sep 17 00:00:00 2001 From: Joe C Date: Wed, 27 May 2026 18:48:59 +0800 Subject: [PATCH 068/576] svm: conformance: inline sysvar cache helper (#12756) --- svm/src/conformance/harness.rs | 61 ++++++++++------------------------ svm/src/conformance/mod.rs | 1 - svm/src/conformance/sysvar.rs | 21 ------------ 3 files changed, 18 insertions(+), 65 deletions(-) delete mode 100644 svm/src/conformance/sysvar.rs diff --git a/svm/src/conformance/harness.rs b/svm/src/conformance/harness.rs index 628162553e5..f2ef894c15f 100644 --- a/svm/src/conformance/harness.rs +++ b/svm/src/conformance/harness.rs @@ -24,14 +24,12 @@ use { }; #[cfg(feature = "conformance")] use { - super::{ - programs::{fill_program_cache_from_accounts, new_program_cache_with_builtins}, - sysvar::fill_sysvar_cache_from_accounts, - }, + super::programs::{fill_program_cache_from_accounts, new_program_cache_with_builtins}, agave_feature_set::FeatureSet, agave_precompiles::{get_precompile, is_precompile}, prost::Message, protosol::protos::{InstrContext as ProtoInstrContext, InstrEffects as ProtoInstrEffects}, + solana_account::ReadableAccount, solana_precompile_error::PrecompileError, std::ffi::c_int, }; @@ -223,7 +221,13 @@ pub fn execute_instr_proto(input: ProtoInstrContext) -> ProtoInstrEffects { // When testing with protobuf, we fill the sysvar cache from input accounts. let sysvar_cache = { let mut cache = SysvarCache::default(); - fill_sysvar_cache_from_accounts(&mut cache, &instr_context.accounts); + cache.fill_missing_entries(|pubkey, callbackback| { + if let Some(account) = instr_context.accounts.iter().find(|(key, _)| key == pubkey) + && account.1.lamports() > 0 + { + callbackback(account.1.data()); + } + }); cache }; @@ -289,23 +293,19 @@ pub unsafe extern "C" fn sol_compat_instr_execute_v1( mod tests { use { super::{ - super::{ - programs::{fill_program_cache_from_accounts, new_program_cache_with_builtins}, - sysvar::fill_sysvar_cache_from_accounts, - }, + super::programs::{fill_program_cache_from_accounts, new_program_cache_with_builtins}, *, }, solana_account::Account, solana_instruction::{AccountMeta, Instruction}, + solana_rent::Rent, solana_svm_feature_set::SVMFeatureSet, - solana_sysvar_id::SysvarId, }; #[test] fn test_system_program_exec() { let system_program_id = solana_sdk_ids::system_program::id(); let native_loader_id = solana_sdk_ids::native_loader::id(); - let sysvar_id = solana_sysvar_id::id(); let from_pubkey = Pubkey::new_from_array([1u8; 32]); let to_pubkey = Pubkey::new_from_array([2u8; 32]); @@ -314,17 +314,6 @@ mod tests { let slot = 10; let feature_set = SVMFeatureSet::default(); - // Create Clock sysvar. - let clock = solana_clock::Clock { - slot, - ..Default::default() - }; - let clock_data = bincode::serialize(&clock).unwrap(); - - // Create Rent sysvar. - let rent = solana_rent::Rent::default(); - let rent_data = bincode::serialize(&rent).unwrap(); - // Build the instruction context. let context = InstrContext { feature_set, @@ -359,26 +348,6 @@ mod tests { rent_epoch: u64::MAX, }, ), - ( - solana_clock::Clock::id(), - Account { - lamports: 1, - data: clock_data, - owner: sysvar_id, - executable: false, - rent_epoch: u64::MAX, - }, - ), - ( - solana_rent::Rent::id(), - Account { - lamports: 1, - data: rent_data, - owner: sysvar_id, - executable: false, - rent_epoch: u64::MAX, - }, - ), ], instruction: Instruction { program_id: system_program_id, @@ -411,7 +380,13 @@ mod tests { // Create Sysvar Cache. let mut sysvar_cache = SysvarCache::default(); - fill_sysvar_cache_from_accounts(&mut sysvar_cache, &context.accounts); + sysvar_cache.fill_missing_entries(|pubkey, callback| { + if pubkey == &solana_sdk_ids::sysvar::rent::id() { + let rent = Rent::default(); + let rent_data = bincode::serialize(&rent).unwrap(); + callback(&rent_data); + } + }); // Create Program Cache let mut program_cache = new_program_cache_with_builtins(slot); diff --git a/svm/src/conformance/mod.rs b/svm/src/conformance/mod.rs index 7856afc9667..f6b06301478 100644 --- a/svm/src/conformance/mod.rs +++ b/svm/src/conformance/mod.rs @@ -7,4 +7,3 @@ pub mod context; pub mod feature_set; pub mod harness; pub mod programs; -pub mod sysvar; diff --git a/svm/src/conformance/sysvar.rs b/svm/src/conformance/sysvar.rs deleted file mode 100644 index 79105ad3a22..00000000000 --- a/svm/src/conformance/sysvar.rs +++ /dev/null @@ -1,21 +0,0 @@ -//! Sysvar helpers. - -use { - solana_account::{Account, ReadableAccount}, - solana_program_runtime::sysvar_cache::SysvarCache, - solana_pubkey::Pubkey, -}; - -/// Populate a `SysvarCache` via `fill_missing_entries` from any sysvar accounts. -pub fn fill_sysvar_cache_from_accounts( - sysvar_cache: &mut SysvarCache, - accounts: &[(Pubkey, Account)], -) { - sysvar_cache.fill_missing_entries(|pubkey, callbackback| { - if let Some(account) = accounts.iter().find(|(key, _)| key == pubkey) - && account.1.lamports() > 0 - { - callbackback(account.1.data()); - } - }); -} From 0aeb9c2e51e452f53409a7367c0cda9307b5d835 Mon Sep 17 00:00:00 2001 From: Quentin Kniep Date: Wed, 27 May 2026 12:58:38 +0200 Subject: [PATCH 069/576] fix(votor): only add vote to vote_history if voting (#12713) --- votor/src/voting_utils.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/votor/src/voting_utils.rs b/votor/src/voting_utils.rs index 1fdf58477a6..aacfc32afe1 100644 --- a/votor/src/voting_utils.rs +++ b/votor/src/voting_utils.rs @@ -235,11 +235,6 @@ fn insert_vote_and_create_bls_message( is_refresh: bool, context: &mut VotingContext, ) -> Result { - // Update and save the vote history - if !is_refresh { - context.vote_history.add_vote(vote); - } - let bank = context.sharable_banks.root(); let message = match generate_vote_tx( vote, @@ -259,6 +254,15 @@ fn insert_vote_and_create_bls_message( } } }; + + // Only record after generate_vote_tx has produced a signed message. + // Recording before would leave vote_history claiming we voted for the + // slot when HotSpare/NonVoting/NoRankFound short-circuited the broadcast, + // permanently blocking us from voting for that slot. + if !is_refresh { + context.vote_history.add_vote(vote); + } + context .own_vote_sender .send(vec![message.clone()]) From 37ddf95d399d1a011febb4b16f470fa3afdec730 Mon Sep 17 00:00:00 2001 From: Quentin Kniep Date: Wed, 27 May 2026 12:59:07 +0200 Subject: [PATCH 070/576] fix: possible panic when fetching double Merkle meta for block ID repair (#12679) --- ledger/src/blockstore.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/ledger/src/blockstore.rs b/ledger/src/blockstore.rs index 0d13749e37b..9129a078134 100644 --- a/ledger/src/blockstore.rs +++ b/ledger/src/blockstore.rs @@ -989,10 +989,11 @@ impl Blockstore { return Ok(None); }; - // Get the doulbe merkle meta - the block must be full, so we can unwrap here - let dmm = self - .get_double_merkle_meta_maybe_populate_proofs(slot, location)? - .expect("block is full, double merkle meta must exist"); + // Block was full above, get_block_location returned Some, but may have + // been purged by BlockstoreCleanupService in between. + let Some(dmm) = self.get_double_merkle_meta_maybe_populate_proofs(slot, location)? else { + return Ok(None); + }; Ok(Some((dmm, location))) } From de0065fd610f17fd0bcabbfd93145cbcc6bbdf6d Mon Sep 17 00:00:00 2001 From: Quentin Kniep Date: Wed, 27 May 2026 12:59:44 +0200 Subject: [PATCH 071/576] refactor: extract timeout constituent parameters into `TimeoutConfig` (#12564) --- votor/src/timer_manager/timers.rs | 123 +++++++++++++++--------------- 1 file changed, 63 insertions(+), 60 deletions(-) diff --git a/votor/src/timer_manager/timers.rs b/votor/src/timer_manager/timers.rs index 66781575fe7..4f462f8975a 100644 --- a/votor/src/timer_manager/timers.rs +++ b/votor/src/timer_manager/timers.rs @@ -12,7 +12,7 @@ use { }; /// Dynamic skip timeouts can be increased to a maximum of 1hour. -const MAX_TIMEOUT_SECS: f64 = 3600.0; +const MAX_TIMEOUT: Duration = Duration::from_secs(3600); /// Calculate the timeout multiplier based on standstill state. /// Returns 1.0 if not in standstill, or 1.05^n where n is the number of @@ -32,6 +32,24 @@ fn calculate_timeout_multiplier(slot: Slot, standstill_slot: Option) -> f6 } } +#[derive(Clone)] +struct TimeoutConfig { + delta_timeout: Duration, + delta_first_slice: Duration, + delta_block: Duration, +} + +impl TimeoutConfig { + fn scaled(&self, slot: Slot, standstill_slot: Option) -> TimeoutConfig { + let multiplier = calculate_timeout_multiplier(slot, standstill_slot); + TimeoutConfig { + delta_timeout: self.delta_timeout.mul_f64(multiplier).min(MAX_TIMEOUT), + delta_first_slice: self.delta_first_slice, + delta_block: self.delta_block, + } + } +} + /// Encodes a basic state machine of the different stages involved in handling /// timeouts for a window of slots. enum TimerState { @@ -41,10 +59,8 @@ enum TimerState { window: VecDeque, /// Time when this stage will end. timeout: Instant, - /// The maximum allowed time for producing the first slice. - delta_first_slice: Duration, - /// Protocol slot time, used in [`TimerState::WaitForBlock`]. - delta_block: Duration, + /// Timeout constituent parameters. + timeout_config: TimeoutConfig, }, /// Waiting for DELTA_TIMEOUT + i * DELTA_BLOCK for each block i in window. WaitForBlock { @@ -52,8 +68,8 @@ enum TimerState { window: VecDeque, /// Time when this stage will end. timeout: Instant, - /// Protocol slot time. - delta_block: Duration, + /// Timeout constituent parameters. + timeout_config: TimeoutConfig, }, /// The state machine is done. Done, @@ -66,34 +82,23 @@ impl TimerState { /// (`delta_timeout`). `delta_first_slice` and `delta_block` are protocol /// pacing, not a function of network delays, so they are not scaled. Also /// returns the next time the timer should fire. - fn new( - slot: Slot, - delta_timeout: Duration, - delta_first_slice: Duration, - delta_block: Duration, - now: Instant, - timeout_multiplier: f64, - ) -> (Self, Instant) { + fn new(slot: Slot, timeout_config: TimeoutConfig, now: Instant) -> (Self, Instant) { let window = (slot..=last_of_consecutive_leader_slots(slot)).collect::>(); assert!(!window.is_empty()); - let scaled_delta_timeout = Duration::from_secs_f64( - (delta_timeout.as_secs_f64() * timeout_multiplier).min(MAX_TIMEOUT_SECS), - ); // A correct leader may take up to `delta_first_slice` to send their first slice, // so the earliest sound point to declare them crashed is // `delta_first_slice + delta_timeout` after their window starts. let timeout = now - .checked_add(scaled_delta_timeout) + .checked_add(timeout_config.delta_timeout) .unwrap() - .checked_add(delta_first_slice) + .checked_add(timeout_config.delta_first_slice) .unwrap(); ( Self::WaitForFirstSlice { window, timeout, - delta_first_slice, - delta_block, + timeout_config: timeout_config.clone(), }, timeout, ) @@ -107,8 +112,7 @@ impl TimerState { Self::WaitForFirstSlice { window, timeout, - delta_first_slice, - delta_block, + timeout_config, } => { assert!(!window.is_empty()); if &now < timeout { @@ -118,21 +122,21 @@ impl TimerState { // Slot 0's block deadline is `T + delta_block + scaled_delta_timeout`; // subtract the `delta_first_slice` paid up-front to `WaitForFirstSlice`. let new_timeout = timeout - .checked_add(*delta_block) + .checked_add(timeout_config.delta_block) .unwrap() - .checked_sub(*delta_first_slice) + .checked_sub(timeout_config.delta_first_slice) .unwrap(); *self = Self::WaitForBlock { window: window.to_owned(), timeout: new_timeout, - delta_block: *delta_block, + timeout_config: timeout_config.clone(), }; Some(VotorEvent::TimeoutCrashedLeader(slot)) } Self::WaitForBlock { window, timeout, - delta_block, + timeout_config, } => { assert!(!window.is_empty()); if &now < timeout { @@ -143,7 +147,7 @@ impl TimerState { match window.front() { None => *self = Self::Done, Some(_next_slot) => { - *timeout = timeout.checked_add(*delta_block).unwrap(); + *timeout = timeout.checked_add(timeout_config.delta_block).unwrap(); } } ret @@ -199,15 +203,13 @@ impl Timers { delta_block: Duration, ) { assert_eq!(self.heap.len(), self.timers.len()); - let timeout_multiplier = calculate_timeout_multiplier(slot, standstill_slot); - let (timer, next_fire) = TimerState::new( - slot, - self.delta_timeout, + let timeout_config = TimeoutConfig { + delta_timeout: self.delta_timeout, delta_first_slice, delta_block, - now, - timeout_multiplier, - ); + } + .scaled(slot, standstill_slot); + let (timer, next_fire) = TimerState::new(slot, timeout_config, now); // It is possible that this slot already has a timer set e.g. if there // are multiple ParentReady for the same slot. Do not insert new timer then. let mut new_timer_inserted = false; @@ -276,8 +278,13 @@ mod tests { let one_micro = Duration::from_micros(1); let now = Instant::now(); let slot = 0; - let (mut timer_state, next_fire) = - TimerState::new(slot, one_micro, one_micro, one_micro, now, 1.0); + let timeout_config = TimeoutConfig { + delta_timeout: one_micro, + delta_first_slice: one_micro, + delta_block: one_micro, + } + .scaled(slot, None); + let (mut timer_state, next_fire) = TimerState::new(slot, timeout_config, now); assert!(matches!( timer_state.progress(next_fire).unwrap(), @@ -353,20 +360,19 @@ mod tests { let delta_block = Duration::from_millis(50); let now = Instant::now(); let slot = 0; - let multiplier = 1.5; // 50% longer timeouts - let (mut timer_state, next_fire) = TimerState::new( - slot, + let timeout_config = TimeoutConfig { delta_timeout, delta_first_slice, delta_block, - now, - multiplier, - ); + } + // 1 leader window standstill -> 1.05x multiplier + .scaled(NUM_CONSECUTIVE_LEADER_SLOTS.get() as Slot, Some(0)); + let (mut timer_state, next_fire) = TimerState::new(slot, timeout_config, now); - // The first timeout should fire at now + (delta_timeout * 1.5) + delta_first_slice - // = now + 150ms + delta_first_slice - let expected_first_fire = now + Duration::from_millis(150) + delta_first_slice; + // The first timeout should fire at now + (delta_timeout * 1.05) + delta_first_slice + // = now + 105ms + delta_first_slice + let expected_first_fire = now + Duration::from_millis(105) + delta_first_slice; assert!( next_fire >= expected_first_fire - Duration::from_micros(100) && next_fire <= expected_first_fire + Duration::from_micros(100), @@ -380,9 +386,9 @@ mod tests { )); // Slot 0's block deadline is `now + delta_block + scaled_delta_timeout` - // = now + 50ms + 150ms = now + 200ms (unaffected by delta_first_slice). + // = now + 50ms + 105ms = now + 155ms (unaffected by delta_first_slice). let next = timer_state.next_fire().unwrap(); - let expected_next = now + Duration::from_millis(200); + let expected_next = now + Duration::from_millis(155); let skew = if next >= expected_next { next - expected_next } else { @@ -424,29 +430,26 @@ mod tests { #[test] fn timer_state_caps_at_max_timeout() { let now = Instant::now(); - // Use a large multiplier that would exceed MAX_TIMEOUT - let multiplier = 1000000.0; let delta_block = Duration::from_millis(DEFAULT_MS_PER_SLOT); let delta_first_slice = delta_block; - let (mut timer_state, next_fire) = TimerState::new( - 100, - DELTA_TIMEOUT, + let timeout_config = TimeoutConfig { + delta_timeout: DELTA_TIMEOUT, delta_first_slice, delta_block, - now, - multiplier, - ); + } + // ensure a large multiplier that would exceed MAX_TIMEOUT + .scaled(1000, Some(0)); + let (mut timer_state, next_fire) = TimerState::new(1000, timeout_config, now); // The first timeout should be capped at MAX_TIMEOUT (+ unscaled delta_first_slice). - let expected_first_fire = - now + Duration::from_secs(MAX_TIMEOUT_SECS as u64) + delta_first_slice; + let expected_first_fire = now + MAX_TIMEOUT + delta_first_slice; assert_eq!(next_fire, expected_first_fire); // Progress the timer to get TimeoutCrashedLeader assert!(matches!( timer_state.progress(next_fire).unwrap(), - VotorEvent::TimeoutCrashedLeader(100) + VotorEvent::TimeoutCrashedLeader(1000) )); // Slot 0's block deadline is `T + delta_block + scaled_delta_timeout`, so From 52bc5bb3d07690b8ce53977ddf92ed471d2d8915 Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Wed, 27 May 2026 07:40:16 -0400 Subject: [PATCH 072/576] repair: turn off popular pruned requests in alpenglow (#12753) --- core/src/repair/repair_service.rs | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/core/src/repair/repair_service.rs b/core/src/repair/repair_service.rs index e83885cee68..d3e1c037871 100644 --- a/core/src/repair/repair_service.rs +++ b/core/src/repair/repair_service.rs @@ -18,6 +18,7 @@ use { }, }, }, + agave_votor_messages::migration::MigrationStatus, crossbeam_channel::{Receiver as CrossbeamReceiver, Sender as CrossbeamSender}, lazy_lru::LruCache, rand::prelude::IndexedRandom as _, @@ -851,6 +852,7 @@ impl RepairService { repair_tracker: &mut RepairTracker, outstanding_requests: &RwLock, repair_socket: &UdpSocket, + migration_status: &MigrationStatus, ) { let RepairChannels { verified_voter_slots_receiver, @@ -889,13 +891,15 @@ impl RepairService { repair_metrics, ); - Self::handle_popular_pruned_forks( - root_bank.clone(), - repair_weight, - popular_pruned_forks_requests, - popular_pruned_forks_sender, - repair_metrics, - ); + if !migration_status.is_alpenglow_enabled() { + Self::handle_popular_pruned_forks( + root_bank.clone(), + repair_weight, + popular_pruned_forks_requests, + popular_pruned_forks_sender, + repair_metrics, + ); + } Self::build_and_send_repair_batch( serve_repair, @@ -933,7 +937,7 @@ impl RepairService { sharable_banks, repair_info.repair_whitelist.clone(), Box::new(StandardRepairHandler::new(blockstore.clone())), - migration_status, + migration_status.clone(), ) }, repair_metrics: RepairMetrics::default(), @@ -951,6 +955,7 @@ impl RepairService { &mut repair_tracker, outstanding_requests, repair_socket, + migration_status.as_ref(), ); repair_tracker.repair_metrics.maybe_report(); sleep(Duration::from_millis(REPAIR_MS)); From 84bea45dd235abd96693b5d6f1c00004b05691b6 Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Wed, 27 May 2026 07:43:25 -0400 Subject: [PATCH 073/576] feature: remove DCOU Alpenglow feature (#12745) --- feature-set/src/lib.rs | 20 ---------------- local-cluster/Cargo.toml | 1 - local-cluster/tests/local_cluster.rs | 34 ++++++++-------------------- runtime/Cargo.toml | 1 - runtime/src/bank/tests.rs | 8 +++---- 5 files changed, 13 insertions(+), 51 deletions(-) diff --git a/feature-set/src/lib.rs b/feature-set/src/lib.rs index 56b55d0d8bc..dba7ac117c0 100644 --- a/feature-set/src/lib.rs +++ b/feature-set/src/lib.rs @@ -1317,27 +1317,7 @@ pub mod formalize_loaded_transaction_data_size { } pub mod alpenglow { - #[cfg(feature = "dev-context-only-utils")] - use { - solana_keypair::{Keypair, Signer}, - std::sync::LazyLock, - }; - - // Used to activate alpenglow in local-cluster tests without exposing the actual feature's private key - #[cfg(feature = "dev-context-only-utils")] - pub static TEST_KEYPAIR: LazyLock = LazyLock::new(|| { - let keypair = Keypair::from_base58_string( - "2Vzd6oTWU4RtM5UmsSyBH3tAhPSi1sKqMeMC8bF1jzHHLBMRhEWtrfmBV4EmwQbGSwkunk5Wy67kXNAL1ZL1xQhR", - ); - assert_eq!(keypair.pubkey(), super::alpenglow::id()); - keypair - }); - - #[cfg(not(feature = "dev-context-only-utils"))] solana_pubkey::declare_id!("mustRekeyVm2QHYB3JPefBiU4BY3Z6JkW2k3Scw5GWP"); - - #[cfg(feature = "dev-context-only-utils")] - solana_pubkey::declare_id!("8KpruRFrT59jQ9NfFX9DU6j8a1hW7y6xchvZNQ5rxD4P"); } pub mod disable_zk_elgamal_proof_program { diff --git a/local-cluster/Cargo.toml b/local-cluster/Cargo.toml index e4de503774e..cf9a9ab1c49 100644 --- a/local-cluster/Cargo.toml +++ b/local-cluster/Cargo.toml @@ -16,7 +16,6 @@ targets = ["x86_64-unknown-linux-gnu"] agave-unstable-api = [] dev-context-only-utils = [ "solana-core/dev-context-only-utils", - "agave-feature-set/dev-context-only-utils", "agave-votor-messages/dev-context-only-utils", ] diff --git a/local-cluster/tests/local_cluster.rs b/local-cluster/tests/local_cluster.rs index b416042b7bc..14b38494231 100644 --- a/local-cluster/tests/local_cluster.rs +++ b/local-cluster/tests/local_cluster.rs @@ -6211,6 +6211,15 @@ fn test_alpenglow_migration( skip_warmup_slots: false, ..ClusterConfig::default() }; + // Seed the feature account as pending activation. The runtime activates it at the next epoch + // boundary without needing the Alpenglow feature authority keypair. + cluster_config.additional_accounts.push(( + agave_feature_set::alpenglow::id(), + solana_feature_gate_interface::create_account( + &solana_feature_gate_interface::Feature::default(), + 1, + ), + )); // Create local cluster with alpenglow accounts but feature not activated let cluster = LocalCluster::new(&mut cluster_config, SocketAddrSpace::Unspecified); @@ -6221,35 +6230,10 @@ fn test_alpenglow_migration( .map(|v| v.info.keypair.clone()) .collect(); - // Send feature activation transaction - info!("Sending feature activation transaction"); let client = RpcClient::new_socket_with_commitment( cluster.entry_point_info.rpc().unwrap(), CommitmentConfig::processed(), ); - let faucet_keypair = &cluster.funding_keypair; - let feature_keypair = &*agave_feature_set::alpenglow::TEST_KEYPAIR; - let blockhash = client.get_latest_blockhash().unwrap(); - let lamports = client - .get_minimum_balance_for_rent_exemption(solana_feature_gate_interface::Feature::size_of()) - .unwrap(); - - let activation_message = solana_message::Message::new( - &solana_feature_gate_interface::activate_with_lamports( - &agave_feature_set::alpenglow::id(), - &faucet_keypair.pubkey(), - lamports, - ), - Some(&faucet_keypair.pubkey()), - ); - let activation_tx = solana_transaction::Transaction::new( - &[&feature_keypair, &faucet_keypair], - activation_message, - blockhash, - ); - - client.send_and_confirm_transaction(&activation_tx).unwrap(); - info!("Feature activation transaction confirmed"); // Monitor for feature activation let activation_slot; diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index d8a7326d38a..c60a7011285 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -19,7 +19,6 @@ name = "solana_runtime" [features] agave-unstable-api = [] dev-context-only-utils = [ - "agave-feature-set/dev-context-only-utils", "agave-votor-messages/dev-context-only-utils", "solana-accounts-db/dev-context-only-utils", "solana-program-binaries", diff --git a/runtime/src/bank/tests.rs b/runtime/src/bank/tests.rs index cdbf7605f0e..b2425df5cb8 100644 --- a/runtime/src/bank/tests.rs +++ b/runtime/src/bank/tests.rs @@ -5246,9 +5246,9 @@ fn test_bank_hash_consistency(deprecate_rent_exemption_threshold: bool) { assert_eq!( bank.hash().to_string(), if deprecate_rent_exemption_threshold { - "8xa8W2GxXP2fVXgY6EaTQFfWnL3jW9apWuu6FueGZPXs" + "9ycftRwjpQ17PrhnwhGPbVzDKd3Q9BybmLYU8UD1Pg1T" } else { - "CeTjbRdS8Nt2Wb1QMuMf8rXQL3YGsUGLwsdT4Ahkvhut" + "4XzjZMjhP9s8iBeFQsnHFwaQ991dpiBGHEkzj1ifAcpS" }, ); } @@ -5257,9 +5257,9 @@ fn test_bank_hash_consistency(deprecate_rent_exemption_threshold: bool) { assert_eq!( bank.hash().to_string(), if deprecate_rent_exemption_threshold { - "748qUop2J7kyQjtYs9SDrxKRswjbeJPNT3mJqCJWmGfA" + "7sbqfkN4W3PkBREyduDc3R2eiKu68JKYRXZt6gCWNt4N" } else { - "9Kr5dG4tSeS6gSboWMDHJ3GWs85uFXKh9yaHLHP73MJ4" + "ArB3XNVLJsyV7AtrG3C3Bj4akb8s7AzpS5rHJnqGW5sw" }, ); break; From 2c9eb8ef48ea1aa9c81aa35f7202410a4ae5e175 Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Wed, 27 May 2026 08:08:58 -0400 Subject: [PATCH 074/576] votor: return the proper error for NonVoting validators (#12752) --- votor/src/voting_utils.rs | 51 ++++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/votor/src/voting_utils.rs b/votor/src/voting_utils.rs index aacfc32afe1..660fcbbbc37 100644 --- a/votor/src/voting_utils.rs +++ b/votor/src/voting_utils.rs @@ -152,6 +152,9 @@ pub fn generate_vote_tx( wait_to_vote_slot: Option, derived_bls_keypairs: &mut HashMap>, ) -> GenerateVoteTxResult { + if authorized_voter_keypairs.read().unwrap().is_empty() { + return GenerateVoteTxResult::NonVoting; + } if bank.get_vote_account(&vote_account_pubkey).is_none() { return GenerateVoteTxResult::VoteAccountNotFound(vote_account_pubkey); } @@ -479,12 +482,18 @@ mod tests { // Empty authorized voter keypairs to simulate non voting node voting_context.authorized_voter_keypairs = Arc::new(std::sync::RwLock::new(vec![])); let vote = Vote::new_skip_vote(5); - // For non-voting nodes, we just return Ok(None) - assert!( - generate_vote_message(vote, false, &mut voting_context) - .unwrap() - .is_none() - ); + assert!(matches!( + generate_vote_tx( + vote, + &voting_context.sharable_banks.root(), + voting_context.vote_account_pubkey, + &voting_context.identity_keypair, + &voting_context.authorized_voter_keypairs, + voting_context.wait_to_vote_slot, + &mut voting_context.derived_bls_keypairs, + ), + GenerateVoteTxResult::NonVoting + )); // Recover correct value to vote again voting_context.authorized_voter_keypairs = Arc::new(RwLock::new(vec![Arc::new( @@ -508,23 +517,21 @@ mod tests { let mut voting_context = setup_voting_context_and_bank_forks(own_vote_sender, &validator_keypairs, my_index); - // Wrong identity keypair - voting_context.identity_keypair = Arc::new(Keypair::new()); + // Wrong identity keypair should return HotSpare based on rank_map.node_pubkey. + let wrong_identity_keypair = Arc::new(Keypair::new()); let vote = Vote::new_notarization_vote(6, Hash::new_unique()); - assert!( - generate_vote_message(vote, true, &mut voting_context) - .unwrap() - .is_none() - ); - - // Recover correct value to vote again - voting_context.identity_keypair = - Arc::new(validator_keypairs[my_index].node_keypair.insecure_clone()); - assert!( - generate_vote_message(vote, true, &mut voting_context) - .unwrap() - .is_some() - ); + assert!(matches!( + generate_vote_tx( + vote, + &voting_context.sharable_banks.root(), + voting_context.vote_account_pubkey, + &wrong_identity_keypair, + &voting_context.authorized_voter_keypairs, + voting_context.wait_to_vote_slot, + &mut voting_context.derived_bls_keypairs, + ), + GenerateVoteTxResult::HotSpare + )); } #[test] From 81c0e483cdcd2ca9a31d1052a25f23486fd04267 Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Wed, 27 May 2026 08:09:20 -0400 Subject: [PATCH 075/576] validator: add `--do-not-require-vote-history` for ag startup/set-identity (#12750) validator: add --do-not-require-vote-history for ag startup/set-identity --- core/src/validator.rs | 146 ++++++++++++++++++++- local-cluster/src/validator_configs.rs | 1 + validator/src/admin_rpc_service.rs | 68 +++++++++- validator/src/bin/solana-test-validator.rs | 3 + validator/src/commands/run/args.rs | 6 + validator/src/commands/run/execute.rs | 2 + validator/src/commands/set_identity/mod.rs | 44 ++++++- votor/src/event_handler.rs | 4 +- 8 files changed, 261 insertions(+), 13 deletions(-) diff --git a/core/src/validator.rs b/core/src/validator.rs index 6a75e4b4400..8d4ae0752ae 100644 --- a/core/src/validator.rs +++ b/core/src/validator.rs @@ -147,7 +147,7 @@ use { solana_turbine::{self, XdpSender, broadcast_stage::BroadcastStageType}, solana_unified_scheduler_pool::DefaultSchedulerPool, solana_validator_exit::Exit, - solana_vote_program::vote_state::VoteStateV4, + solana_vote_program::vote_state::{VoteStateV4, handler::VoteStateHandler}, std::{ borrow::Cow, cmp, @@ -349,6 +349,7 @@ pub struct ValidatorConfig { /// processing. pub run_verification: bool, pub require_tower: bool, + pub require_vote_history: bool, pub tower_storage: Arc, pub vote_history_storage: Arc, pub debug_keys: Option>>, @@ -429,6 +430,7 @@ impl ValidatorConfig { max_genesis_archive_unpacked_size: MAX_GENESIS_ARCHIVE_UNPACKED_SIZE, run_verification: true, require_tower: false, + require_vote_history: false, tower_storage: Arc::new(NullTowerStorage::default()), vote_history_storage: Arc::new(NullVoteHistoryStorage::default()), debug_keys: None, @@ -1556,8 +1558,7 @@ impl Validator { }; // Future upstream PR will handle reconciliation of VoteHistory against hard forks let vote_history = - VoteHistory::restore(config.vote_history_storage.as_ref(), &cluster_info.id()) - .unwrap_or(VoteHistory::new(cluster_info.id(), 0)); + restore_vote_history(config, &bank_forks, &cluster_info.id(), vote_account); migration_status.log_phase(); let outstanding_repair_requests = @@ -2006,6 +2007,65 @@ fn active_vote_account_exists_in_bank(bank: &Bank, vote_account: &Pubkey) -> boo false } +pub fn active_vote_account_exists_in_bank_alpenglow(bank: &Bank, vote_account: &Pubkey) -> bool { + let Some(genesis_certificate) = bank.get_alpenglow_genesis_certificate() else { + return false; + }; + let Some(Ok(vote_state)) = bank + .get_account(vote_account) + .map(|acct| acct.deserialize_data()) + else { + return false; + }; + let Ok(vote_state_handler) = VoteStateHandler::try_new_from_vote_state_versions(vote_state) + else { + return false; + }; + + let Some(last_voted_slot) = vote_state_handler.last_voted_slot() else { + return false; + }; + let genesis_slot = genesis_certificate.cert_type.slot(); + + last_voted_slot > genesis_slot +} + +fn restore_vote_history( + config: &ValidatorConfig, + bank_forks: &Arc>, + identity: &Pubkey, + vote_account: &Pubkey, +) -> VoteHistory { + match VoteHistory::restore(config.vote_history_storage.as_ref(), identity) { + Ok(vote_history) => vote_history, + Err(err) => { + let voting_has_been_active = { + let bank_forks = bank_forks.read().unwrap(); + active_vote_account_exists_in_bank_alpenglow( + &bank_forks.working_bank(), + vote_account, + ) + }; + if config.require_vote_history && voting_has_been_active { + panic!( + "Unable to retrieve vote history for identity {identity}. The vote account \ + {vote_account} has prior Alpenglow votes. If this is intentional, use \ + --do-not-require-vote-history: {err:?}" + ); + } + if err.is_file_missing() && !voting_has_been_active { + info!( + "Ignoring expected failed vote history restore because this vote account has \ + not voted before" + ); + } else { + warn!("Unable to retrieve vote history: {err:?} creating default vote history..."); + } + VoteHistory::new(*identity, 0) + } + } +} + fn check_poh_speed(bank: &Bank, maybe_hash_samples: Option) -> Result<(), ValidatorError> { let Some(hashes_per_tick) = bank.hashes_per_tick() else { warn!("Unable to read hashes per tick from Bank, skipping PoH speed check"); @@ -2956,9 +3016,89 @@ mod tests { }, solana_poh_config::PohConfig, solana_sha256_hasher::hash, + solana_vote_program::vote_state::{LandedVote, Lockout, VoteStateVersions}, std::{fs::remove_dir_all, num::NonZeroU64, thread, time::Duration}, }; + #[test] + fn active_vote_account_exists_in_bank_alpenglow_checks_genesis_certificate_and_votes() { + use { + agave_votor_messages::consensus_message::{Certificate, CertificateType}, + solana_account::{AccountSharedData, state_traits::StateMut}, + solana_bls_signatures::{BLS_SIGNATURE_AFFINE_SIZE, Signature as BLSSignature}, + }; + + let genesis_config = create_genesis_config(1_000_000).0; + let bank = Bank::new_for_tests(&genesis_config); + let vote_account_pubkey = Pubkey::new_unique(); + + assert!(!active_vote_account_exists_in_bank( + &bank, + &vote_account_pubkey + )); + assert!(!active_vote_account_exists_in_bank_alpenglow( + &bank, + &vote_account_pubkey + )); + + let mut vote_state = VoteStateV4::default(); + let mut vote_account = + AccountSharedData::new(1, VoteStateV4::size_of(), &solana_vote_program::id()); + vote_account + .set_state(&VoteStateVersions::new_v4(vote_state.clone())) + .unwrap(); + bank.store_account(&vote_account_pubkey, &vote_account); + assert!(!active_vote_account_exists_in_bank( + &bank, + &vote_account_pubkey + )); + assert!(!active_vote_account_exists_in_bank_alpenglow( + &bank, + &vote_account_pubkey + )); + + vote_state.votes.push_back(LandedVote { + latency: 0, + lockout: Lockout::new(7), + }); + vote_account + .set_state(&VoteStateVersions::new_v4(vote_state.clone())) + .unwrap(); + bank.store_account(&vote_account_pubkey, &vote_account); + assert!(active_vote_account_exists_in_bank( + &bank, + &vote_account_pubkey + )); + assert!(!active_vote_account_exists_in_bank_alpenglow( + &bank, + &vote_account_pubkey + )); + + bank.set_alpenglow_genesis_certificate(&Certificate { + cert_type: CertificateType::Genesis(40, Hash::new_unique()), + signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), + bitmap: vec![], + }); + assert!(!active_vote_account_exists_in_bank_alpenglow( + &bank, + &vote_account_pubkey + )); + + vote_state.votes.push_back(LandedVote { + latency: 0, + lockout: Lockout::new(43), + }); + vote_state.root_slot = Some(42); + vote_account + .set_state(&VoteStateVersions::new_v4(vote_state)) + .unwrap(); + bank.store_account(&vote_account_pubkey, &vote_account); + assert!(active_vote_account_exists_in_bank_alpenglow( + &bank, + &vote_account_pubkey + )); + } + #[test] fn validator_exit() { agave_logger::setup(); diff --git a/local-cluster/src/validator_configs.rs b/local-cluster/src/validator_configs.rs index d2264d897bc..ebd76009044 100644 --- a/local-cluster/src/validator_configs.rs +++ b/local-cluster/src/validator_configs.rs @@ -34,6 +34,7 @@ pub fn safe_clone_config(config: &ValidatorConfig) -> ValidatorConfig { max_genesis_archive_unpacked_size: config.max_genesis_archive_unpacked_size, run_verification: config.run_verification, require_tower: config.require_tower, + require_vote_history: config.require_vote_history, tower_storage: config.tower_storage.clone(), vote_history_storage: config.vote_history_storage.clone(), debug_keys: config.debug_keys.clone(), diff --git a/validator/src/admin_rpc_service.rs b/validator/src/admin_rpc_service.rs index 7833618c1f8..ce30415e205 100644 --- a/validator/src/admin_rpc_service.rs +++ b/validator/src/admin_rpc_service.rs @@ -1,5 +1,7 @@ use { - agave_votor::event::VotorEvent, + agave_votor::{ + event::VotorEvent, vote_history::VoteHistory, vote_history_storage::VoteHistoryStorage, + }, crossbeam_channel::Sender, jsonrpc_core::{BoxFuture, ErrorCode, MetaIoHandler, Metadata, Result}, jsonrpc_core_client::{RpcError, transports::ipc}, @@ -20,6 +22,7 @@ use { repair::repair_service, validator::{ BlockProductionMethod, SchedulerPacing, TransactionStructure, ValidatorStartProgress, + active_vote_account_exists_in_bank_alpenglow, }, }, solana_geyser_plugin_manager::GeyserPluginManagerRequest, @@ -56,6 +59,7 @@ pub struct AdminRpcRequestMetadata { pub validator_exit_backpressure: HashMap>, pub authorized_voter_keypairs: Arc>>>, pub tower_storage: Arc, + pub vote_history_storage: Arc, pub staked_nodes_overrides: Arc>>, pub post_init: Arc>>, pub rpc_to_plugin_manager_sender: Option>, @@ -219,6 +223,7 @@ pub trait AdminRpc { meta: Self::Metadata, keypair_file: String, require_tower: bool, + require_vote_history: bool, ) -> Result<()>; #[rpc(meta, name = "setIdentityFromBytes")] @@ -227,6 +232,7 @@ pub trait AdminRpc { meta: Self::Metadata, identity_keypair: Vec, require_tower: bool, + require_vote_history: bool, ) -> Result<()>; #[rpc(meta, name = "setStakedNodesOverrides")] @@ -559,6 +565,7 @@ impl AdminRpc for AdminRpcImpl { meta: Self::Metadata, keypair_file: String, require_tower: bool, + require_vote_history: bool, ) -> Result<()> { debug!("set_identity request received"); @@ -568,7 +575,12 @@ impl AdminRpc for AdminRpcImpl { )) })?; - AdminRpcImpl::set_identity_keypair(meta, identity_keypair, require_tower) + AdminRpcImpl::set_identity_keypair( + meta, + identity_keypair, + require_tower, + require_vote_history, + ) } fn set_identity_from_bytes( @@ -576,6 +588,7 @@ impl AdminRpc for AdminRpcImpl { meta: Self::Metadata, identity_keypair: Vec, require_tower: bool, + require_vote_history: bool, ) -> Result<()> { debug!("set_identity_from_bytes request received"); @@ -585,7 +598,12 @@ impl AdminRpc for AdminRpcImpl { )) })?; - AdminRpcImpl::set_identity_keypair(meta, identity_keypair, require_tower) + AdminRpcImpl::set_identity_keypair( + meta, + identity_keypair, + require_tower, + require_vote_history, + ) } fn set_staked_nodes_overrides(&self, meta: Self::Metadata, path: String) -> Result<()> { @@ -880,6 +898,7 @@ impl AdminRpcImpl { meta: AdminRpcRequestMetadata, identity_keypair: Keypair, require_tower: bool, + require_vote_history: bool, ) -> Result<()> { meta.with_post_init(|post_init| { if require_tower { @@ -893,6 +912,33 @@ impl AdminRpcImpl { })?; } + if require_vote_history { + let voting_has_been_active = { + let bank_forks = post_init.bank_forks.read().unwrap(); + active_vote_account_exists_in_bank_alpenglow( + &bank_forks.root_bank(), + &post_init.vote_account, + ) + }; + if voting_has_been_active { + let _ = VoteHistory::restore( + meta.vote_history_storage.as_ref(), + &identity_keypair.pubkey(), + ) + .map_err(|err| { + jsonrpc_core::error::Error::invalid_params(format!( + "Unable to load vote history file for identity {}: {}. The vote \ + account {} has prior Alpenglow votes. Ensure the vote history file \ + is present or (dangerous) use --do-not-require-vote-history if you \ + know what you're doing", + identity_keypair.pubkey(), + err, + post_init.vote_account + )) + })?; + } + } + for (key, notifier) in &*post_init.notifies.read().unwrap() { if let Err(err) = notifier.update_key(&identity_keypair) { error!("Error updating network layer keypair: {err} on {key:?}"); @@ -1147,6 +1193,9 @@ mod tests { validator_exit_backpressure: HashMap::default(), authorized_voter_keypairs: Arc::new(RwLock::new(vec![vote_keypair])), tower_storage: Arc::new(NullTowerStorage {}), + vote_history_storage: Arc::new( + agave_votor::vote_history_storage::NullVoteHistoryStorage::default(), + ), post_init: Arc::new(RwLock::new(Some(AdminRpcRequestMetadataPostInit { cluster_info, bank_forks, @@ -1205,7 +1254,7 @@ mod tests { let validator_id_bytes = format!("{:?}", expected_validator_id.to_bytes()); let set_id_request = format!( - r#"{{"jsonrpc":"2.0","id":1,"method":"setIdentityFromBytes","params":[{validator_id_bytes}, false]}}"#, + r#"{{"jsonrpc":"2.0","id":1,"method":"setIdentityFromBytes","params":[{validator_id_bytes}, false, false]}}"#, ); let response = io.handle_request_sync(&set_id_request, meta.clone()); let actual_parsed_response: Value = @@ -1279,6 +1328,9 @@ mod tests { validator_exit_backpressure: HashMap::default(), authorized_voter_keypairs: authorized_voter_keypairs.clone(), tower_storage: Arc::new(NullTowerStorage {}), + vote_history_storage: Arc::new( + agave_votor::vote_history_storage::NullVoteHistoryStorage::default(), + ), post_init: post_init.clone(), staked_nodes_overrides: Arc::new(RwLock::new(HashMap::new())), rpc_to_plugin_manager_sender: None, @@ -1359,6 +1411,9 @@ mod tests { validator_exit_backpressure: HashMap::default(), authorized_voter_keypairs: authorized_voter_keypairs.clone(), tower_storage: Arc::new(NullTowerStorage {}), + vote_history_storage: Arc::new( + agave_votor::vote_history_storage::NullVoteHistoryStorage::default(), + ), post_init: post_init.clone(), staked_nodes_overrides: Arc::new(RwLock::new(HashMap::new())), rpc_to_plugin_manager_sender: None, @@ -1376,7 +1431,7 @@ mod tests { let validator_id_bytes = format!("{:?}", expected_validator_id.to_bytes()); let set_id_request = format!( - r#"{{"jsonrpc":"2.0","id":1,"method":"setIdentityFromBytes","params":[{validator_id_bytes}, false]}}"#, + r#"{{"jsonrpc":"2.0","id":1,"method":"setIdentityFromBytes","params":[{validator_id_bytes}, false, false]}}"#, ); let response = test_validator.handle_request(&set_id_request); let actual_parsed_response: Value = @@ -1452,6 +1507,9 @@ mod tests { validator_exit_backpressure: HashMap::default(), authorized_voter_keypairs, tower_storage: Arc::new(NullTowerStorage {}), + vote_history_storage: Arc::new( + agave_votor::vote_history_storage::NullVoteHistoryStorage::default(), + ), post_init: Arc::new(RwLock::new(None)), staked_nodes_overrides: Arc::new(RwLock::new(HashMap::new())), rpc_to_plugin_manager_sender: None, diff --git a/validator/src/bin/solana-test-validator.rs b/validator/src/bin/solana-test-validator.rs index 70ffe055595..692e7680c38 100644 --- a/validator/src/bin/solana-test-validator.rs +++ b/validator/src/bin/solana-test-validator.rs @@ -3,6 +3,7 @@ use { admin_rpc_service, cli, commands::FromClapArgMatches, dashboard::Dashboard, ledger_lockfile, lock_ledger, println_name_value, }, + agave_votor::vote_history_storage::FileVoteHistoryStorage, clap::{crate_name, value_t, value_t_or_exit, values_t_or_exit}, crossbeam_channel::unbounded, itertools::Itertools, @@ -418,6 +419,7 @@ fn main() { genesis.enable_scheduler_bindings = matches.is_present("enable_scheduler_bindings"); let tower_storage = Arc::new(FileTowerStorage::new(ledger_path.clone())); + let vote_history_storage = Arc::new(FileVoteHistoryStorage::new(ledger_path.clone())); let admin_service_post_init = Arc::new(RwLock::new(None)); // If geyser_plugin_config value is invalid, the validator will exit when the values are extracted below @@ -440,6 +442,7 @@ fn main() { staked_nodes_overrides: genesis.staked_nodes_overrides.clone(), post_init: admin_service_post_init, tower_storage: tower_storage.clone(), + vote_history_storage: vote_history_storage.clone(), rpc_to_plugin_manager_sender, }, ); diff --git a/validator/src/commands/run/args.rs b/validator/src/commands/run/args.rs index 22845086a82..25d311e7af5 100644 --- a/validator/src/commands/run/args.rs +++ b/validator/src/commands/run/args.rs @@ -546,6 +546,12 @@ pub fn add_args<'a>(app: App<'a, 'a>, default_args: &'a DefaultArgs) -> App<'a, .takes_value(false) .help("Refuse to start if saved tower state is not found"), ) + .arg( + clap::Arg::with_name("do_not_require_vote_history") + .long("do-not-require-vote-history") + .takes_value(false) + .help("Do not require saved vote history state for startup"), + ) .arg( Arg::with_name("expected_genesis_hash") .long("expected-genesis-hash") diff --git a/validator/src/commands/run/execute.rs b/validator/src/commands/run/execute.rs index 5e44c5cc3ed..4a43fa3a41c 100644 --- a/validator/src/commands/run/execute.rs +++ b/validator/src/commands/run/execute.rs @@ -753,6 +753,7 @@ pub fn execute( let mut validator_config = ValidatorConfig { log_config, require_tower: matches.is_present("require_tower"), + require_vote_history: !matches.is_present("do_not_require_vote_history"), tower_storage, vote_history_storage, max_genesis_archive_unpacked_size: MAX_GENESIS_ARCHIVE_UNPACKED_SIZE, @@ -932,6 +933,7 @@ pub fn execute( authorized_voter_keypairs: authorized_voter_keypairs.clone(), post_init: admin_service_post_init.clone(), tower_storage: validator_config.tower_storage.clone(), + vote_history_storage: validator_config.vote_history_storage.clone(), staked_nodes_overrides, rpc_to_plugin_manager_sender, }, diff --git a/validator/src/commands/set_identity/mod.rs b/validator/src/commands/set_identity/mod.rs index 0e2c9d7e250..9c3a834095c 100644 --- a/validator/src/commands/set_identity/mod.rs +++ b/validator/src/commands/set_identity/mod.rs @@ -13,10 +13,20 @@ use { const COMMAND: &str = "set-identity"; #[derive(Debug, PartialEq)] -#[cfg_attr(test, derive(Default))] pub struct SetIdentityArgs { pub identity: Option, pub require_tower: bool, + pub require_vote_history: bool, +} + +impl Default for SetIdentityArgs { + fn default() -> Self { + Self { + identity: None, + require_tower: false, + require_vote_history: true, + } + } } impl FromClapArgMatches for SetIdentityArgs { @@ -24,6 +34,7 @@ impl FromClapArgMatches for SetIdentityArgs { Ok(SetIdentityArgs { identity: value_t!(matches, "identity", String).ok(), require_tower: matches.is_present("require_tower"), + require_vote_history: !matches.is_present("do_not_require_vote_history"), }) } } @@ -45,6 +56,12 @@ pub fn command<'a>() -> App<'a, 'a> { .takes_value(false) .help("Refuse to set the validator identity if saved tower state is not found"), ) + .arg( + clap::Arg::with_name("do_not_require_vote_history") + .long("do-not-require-vote-history") + .takes_value(false) + .help("Do not require saved vote history state for identity change"), + ) .after_help( "Note: the new identity only applies to the currently running validator instance", ) @@ -54,6 +71,7 @@ pub fn execute(matches: &ArgMatches, ledger_path: &Path) -> Result<()> { let SetIdentityArgs { identity, require_tower, + require_vote_history, } = SetIdentityArgs::from_clap_arg_match(matches)?; if let Some(identity_keypair) = identity { @@ -68,7 +86,11 @@ pub fn execute(matches: &ArgMatches, ledger_path: &Path) -> Result<()> { admin_rpc_service::runtime().block_on(async move { admin_client .await? - .set_identity(identity_keypair.display().to_string(), require_tower) + .set_identity( + identity_keypair.display().to_string(), + require_tower, + require_vote_history, + ) .await })?; } else { @@ -81,7 +103,11 @@ pub fn execute(matches: &ArgMatches, ledger_path: &Path) -> Result<()> { admin_rpc_service::runtime().block_on(async move { admin_client .await? - .set_identity_from_bytes(Vec::from(identity_keypair.to_bytes()), require_tower) + .set_identity_from_bytes( + Vec::from(identity_keypair.to_bytes()), + require_tower, + require_vote_history, + ) .await })?; } @@ -129,4 +155,16 @@ mod tests { }, ); } + + #[test] + fn verify_args_struct_by_command_set_identity_do_not_require_vote_history() { + verify_args_struct_by_command( + command(), + vec![COMMAND, "--do-not-require-vote-history"], + SetIdentityArgs { + require_vote_history: false, + ..SetIdentityArgs::default() + }, + ); + } } diff --git a/votor/src/event_handler.rs b/votor/src/event_handler.rs index 63079a76dd8..1b8254dc662 100644 --- a/votor/src/event_handler.rs +++ b/votor/src/event_handler.rs @@ -600,8 +600,8 @@ impl EventHandler { if *my_pubkey != new_pubkey || vctx.vote_history.node_pubkey != new_pubkey { let my_old_pubkey = vctx.vote_history.node_pubkey; *my_pubkey = new_pubkey; - // The vote history file for the new identity must exist for set-identity to succeed - vctx.vote_history = VoteHistory::restore(ctx.vote_history_storage.as_ref(), my_pubkey)?; + vctx.vote_history = VoteHistory::restore(ctx.vote_history_storage.as_ref(), my_pubkey) + .unwrap_or_else(|_| VoteHistory::new(new_pubkey, 0)); vctx.identity_keypair = new_identity; warn!("set-identity: from {my_old_pubkey} to {my_pubkey}"); } From 0b9c64b563d0f4085cb76acb219664cb47c28619 Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Wed, 27 May 2026 08:15:07 -0400 Subject: [PATCH 076/576] replay: add metrics around switch bank events (#12754) --- core/src/consensus/progress_map.rs | 20 +++++++++++ core/src/replay_stage.rs | 12 +++++++ ledger/src/blockstore.rs | 58 +++++++++++++++++++++++++++--- ledger/src/blockstore_metrics.rs | 27 +++++++++++++- ledger/src/blockstore_processor.rs | 5 +++ 5 files changed, 117 insertions(+), 5 deletions(-) diff --git a/core/src/consensus/progress_map.rs b/core/src/consensus/progress_map.rs index 1054a47fdaa..c919b72750f 100644 --- a/core/src/consensus/progress_map.rs +++ b/core/src/consensus/progress_map.rs @@ -287,6 +287,9 @@ impl PropagatedStats { #[derive(Default)] pub struct ProgressMap { progress_map: HashMap, + /// Tracks the number of times a slot was switched from an alternate location. + /// This persists even if the slot is removed from the progress_map due to a switch. + bank_switch_counts: HashMap, } impl std::ops::Deref for ProgressMap { @@ -304,6 +307,12 @@ impl std::ops::DerefMut for ProgressMap { impl ProgressMap { pub fn insert(&mut self, slot: Slot, fork_progress: ForkProgress) { + let num_bank_switches = self.get_num_bank_switches(slot); + fork_progress + .replay_stats + .write() + .unwrap() + .num_bank_switches = num_bank_switches; self.progress_map.insert(slot, fork_progress); } @@ -330,6 +339,15 @@ impl ProgressMap { .map(|fork_progress| &fork_progress.fork_stats) } + pub fn increment_num_bank_switches(&mut self, slot: Slot) { + let count = self.bank_switch_counts.entry(slot).or_insert(0); + *count = count.saturating_add(1); + } + + pub fn get_num_bank_switches(&self, slot: Slot) -> u64 { + self.bank_switch_counts.get(&slot).cloned().unwrap_or(0) + } + pub fn get_fork_stats_mut(&mut self, slot: Slot) -> Option<&mut ForkStats> { self.progress_map .get_mut(&slot) @@ -431,6 +449,8 @@ impl ProgressMap { pub fn handle_new_root(&mut self, bank_forks: &BankForks) { self.progress_map .retain(|k, _| bank_forks.get(*k).is_some()); + self.bank_switch_counts + .retain(|k, _| bank_forks.get(*k).is_some()); } pub fn log_propagated_stats(&self, slot: Slot, bank_forks: &RwLock) { diff --git a/core/src/replay_stage.rs b/core/src/replay_stage.rs index f705d7c5006..9d645849aaa 100644 --- a/core/src/replay_stage.rs +++ b/core/src/replay_stage.rs @@ -488,6 +488,7 @@ struct ReplayLoopTiming { process_duplicate_slots_elapsed_us: u64, process_unfrozen_gossip_verified_vote_hashes_elapsed_us: u64, process_popular_pruned_forks_elapsed_us: u64, + process_switch_bank_events_elapsed_us: u64, repair_correct_slots_elapsed_us: u64, retransmit_not_propagated_elapsed_us: u64, generate_new_bank_forks_read_lock_us: Saturating, @@ -649,6 +650,11 @@ impl ReplayLoopTiming { self.process_popular_pruned_forks_elapsed_us as i64, i64 ), + ( + "process_switch_bank_events_elapsed_us", + self.process_switch_bank_events_elapsed_us as i64, + i64 + ), ( "wait_receive_elapsed_us", self.wait_receive_elapsed_us as i64, @@ -1043,6 +1049,8 @@ impl ReplayStage { &mut progress, did_complete_bank, ); + let mut process_switch_bank_events_time = + Measure::start("process_switch_bank_events_time"); Self::process_switch_bank_events( &my_pubkey, &latest_switch_request, @@ -1053,6 +1061,10 @@ impl ReplayStage { &mut async_verification_freelist, ) .expect("Blockstore operations must succeed"); + process_switch_bank_events_time.stop(); + replay_timing.process_switch_bank_events_elapsed_us += + process_switch_bank_events_time.as_us(); + // Banks might have been switched above, these maps are no longer accurate drop(ancestors); drop(descendants); diff --git a/ledger/src/blockstore.rs b/ledger/src/blockstore.rs index 9129a078134..2809bbe7bb2 100644 --- a/ledger/src/blockstore.rs +++ b/ledger/src/blockstore.rs @@ -47,7 +47,7 @@ use { solana_genesis_config::{DEFAULT_GENESIS_ARCHIVE, DEFAULT_GENESIS_FILE, GenesisConfig}, solana_hash::{HASH_BYTES, Hash}, solana_keypair::Keypair, - solana_measure::measure::Measure, + solana_measure::{measure::Measure, measure_us}, solana_metrics::datapoint_error, solana_pubkey::Pubkey, solana_runtime::bank::Bank, @@ -101,7 +101,7 @@ pub use { blockstore::error::{BlockstoreError, Result}, blockstore_db::{default_num_compaction_threads, default_num_flush_threads}, blockstore_meta::{OptimisticSlotMetaVersioned, SlotMeta}, - blockstore_metrics::BlockstoreInsertionMetrics, + blockstore_metrics::{BlockstoreInsertionMetrics, BlockstoreSwitchBankMetrics}, }, blockstore_purge::PurgeType, rocksdb::properties as RocksProperties, @@ -2364,9 +2364,15 @@ impl Blockstore { "Cannot switch from Original location" ); - let lock = self.insert_shreds_lock.lock().unwrap(); + let mut metrics = BlockstoreSwitchBankMetrics::default(); + + let mut total_measure = Measure::start("switch_block_from_alternate_total"); + + let (lock, lock_time_us) = measure_us!(self.insert_shreds_lock.lock().unwrap()); + metrics.lock_elapsed_us = lock_time_us; // 1. Backup the original block if needed + let mut backup_measure = Measure::start("switch_block_from_alternate_backup"); if let Some(dmr) = self.get_double_merkle_root(slot, BlockLocation::Original)? { let backup_location = BlockLocation::Alternate { block_id: dmr }; if self @@ -2376,8 +2382,11 @@ impl Blockstore { self.copy_shreds_locked(&lock, slot, BlockLocation::Original, backup_location)?; } } + backup_measure.stop(); + metrics.backup_elapsed_us = backup_measure.as_us(); // 2. Purge the original column data, keeping alternate columns intact + let mut purge_measure = Measure::start("switch_block_from_alternate_purge"); self.purge_slot_cleanup_chaining_keep_alt(slot) .or_else(|err| { if matches!(err, BlockstoreError::SlotUnavailable) { @@ -2387,14 +2396,19 @@ impl Blockstore { Err(err) } })?; + purge_measure.stop(); + metrics.purge_elapsed_us = purge_measure.as_us(); // 3. Copy shreds from alternate location to original + let mut copy_measure = Measure::start("switch_block_from_alternate_copy"); let alt_meta = self .meta_from_location(slot, from_location)? .expect("Alternate slot must have SlotMeta"); debug_assert!(alt_meta.is_full(), "Alternate slot must be full"); self.copy_shreds_locked(&lock, slot, from_location, BlockLocation::Original)?; + copy_measure.stop(); + metrics.copy_elapsed_us = copy_measure.as_us(); // 4. Verify the switch was successful debug_assert!( @@ -2403,7 +2417,10 @@ impl Blockstore { .is_full(), "Slot must be full after switch" ); + total_measure.stop(); + metrics.total_elapsed_us = total_measure.as_us(); + metrics.report_metrics(slot, from_location); Ok(()) } @@ -2834,7 +2851,12 @@ impl Blockstore { write_batch, shred_source, ); - newly_completed_data_sets.extend(completed_data_sets); + + if matches!(location, BlockLocation::Original) { + // We don't currently notify RPC when we complete data sets in alternate columns. This can be extended in the future + // if necessary. + newly_completed_data_sets.extend(completed_data_sets); + } merkle_root_metas .entry((location, erasure_set)) .or_insert(WorkingEntry::Dirty(MerkleRootMeta::from_shred(&shred))); @@ -6476,6 +6498,34 @@ pub fn test_all_empty_or_min(blockstore: &Blockstore, min_slot: Slot) { .unwrap() .next() .map(|(slot, _)| slot >= min_slot) + .unwrap_or(true) + & blockstore + .alt_meta_cf + .iter(IteratorMode::Start) + .unwrap() + .next() + .map(|((slot, _), _)| slot >= min_slot) + .unwrap_or(true) + & blockstore + .alt_index_cf + .iter(IteratorMode::Start) + .unwrap() + .next() + .map(|((slot, _), _)| slot >= min_slot) + .unwrap_or(true) + & blockstore + .alt_data_shred_cf + .iter(IteratorMode::Start) + .unwrap() + .next() + .map(|((slot, _, _), _)| slot >= min_slot) + .unwrap_or(true) + & blockstore + .alt_merkle_root_meta_cf + .iter(IteratorMode::Start) + .unwrap() + .next() + .map(|((slot, _, _), _)| slot >= min_slot) .unwrap_or(true); assert!(condition_met); } diff --git a/ledger/src/blockstore_metrics.rs b/ledger/src/blockstore_metrics.rs index c32555d39ff..1631eda4803 100644 --- a/ledger/src/blockstore_metrics.rs +++ b/ledger/src/blockstore_metrics.rs @@ -1,9 +1,10 @@ use { - crate::blockstore_options::LedgerColumnOptions, + crate::{blockstore_meta::BlockLocation, blockstore_options::LedgerColumnOptions}, rocksdb::{ PerfContext, perf::{PerfMetric, PerfStatsLevel, set_perf_stats}, }, + solana_clock::Slot, solana_metrics::datapoint_info, solana_time_utils::timestamp, std::{ @@ -43,6 +44,30 @@ pub struct BlockstoreInsertionMetrics { pub num_coding_shreds_inserted: usize, } +#[derive(Default)] +pub struct BlockstoreSwitchBankMetrics { + pub total_elapsed_us: u64, + pub lock_elapsed_us: u64, + pub backup_elapsed_us: u64, + pub purge_elapsed_us: u64, + pub copy_elapsed_us: u64, +} + +impl BlockstoreSwitchBankMetrics { + pub fn report_metrics(self, slot: Slot, from_location: BlockLocation) { + datapoint_info!( + "blockstore_switch_bank", + "from_location" => from_location.to_string(), + ("slot", slot as i64, i64), + ("total_elapsed_us", self.total_elapsed_us as i64, i64), + ("lock_elapsed_us", self.lock_elapsed_us as i64, i64), + ("backup_elapsed_us", self.backup_elapsed_us as i64, i64), + ("purge_elapsed_us", self.purge_elapsed_us as i64, i64), + ("copy_elapsed_us", self.copy_elapsed_us as i64, i64), + ); + } +} + impl BlockstoreInsertionMetrics { const NAME: &str = "blockstore-insert-shreds"; diff --git a/ledger/src/blockstore_processor.rs b/ledger/src/blockstore_processor.rs index d044c2c7f14..f8a6cb6e79a 100644 --- a/ledger/src/blockstore_processor.rs +++ b/ledger/src/blockstore_processor.rs @@ -1203,6 +1203,9 @@ pub struct ConfirmationTiming { /// `batch_execute()` measurements. pub batch_execute: BatchExecutionTiming, + + /// Number of times this slot was switched from an alternate location. + pub num_bank_switches: u64, } impl Default for ConfirmationTiming { @@ -1216,6 +1219,7 @@ impl Default for ConfirmationTiming { fetch_elapsed: 0, fetch_fail_elapsed: 0, batch_execute: BatchExecutionTiming::default(), + num_bank_switches: 0, } } } @@ -1378,6 +1382,7 @@ impl ReplaySlotStats { (confirmation_elapsed, self.confirmation_elapsed as i64, i64), (replay_elapsed, self.replay_elapsed as i64, i64), ("execute_batches_us", execute_batches_us, Option), + ("num_bank_switches", self.num_bank_switches as i64, i64), ( "replay_total_elapsed", self.started.elapsed().as_micros() as i64, From 5af98636a820905203bb613b125bc3233d9d5f3d Mon Sep 17 00:00:00 2001 From: Quentin Kniep Date: Wed, 27 May 2026 15:45:36 +0200 Subject: [PATCH 077/576] docs: keep debug asserts guarding against vote equivocation (#12759) --- votor/src/vote_history.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/votor/src/vote_history.rs b/votor/src/vote_history.rs index cef64fd0c8b..0615775a8b9 100644 --- a/votor/src/vote_history.rs +++ b/votor/src/vote_history.rs @@ -164,8 +164,7 @@ impl VoteHistory { /// Add a new vote to the voting history pub fn add_vote(&mut self, vote: Vote) { assert!(vote.slot() >= self.root); - // TODO: these assert!s are for my debugging, can consider removing - // in final version + // Asserts here guard against vote equivocation. match vote { Vote::Notarize(vote) => { assert!(self.voted.insert(vote.slot)); From 302704480f9c4977fd64e61ab7c09e2c6fb418eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Mei=C3=9Fner?= Date: Wed, 27 May 2026 09:45:56 -0400 Subject: [PATCH 078/576] Refactor - program owner in `ProgramCache::extract()` (#12683) * Splits PROGRAM_OWNERS.contains() into individual checks. * Adds loader field to ProgramToLoad. * Allows entries to only differ by their account_owner in ProgramCache::assign_program(). * Makes ProgramCache::extract() skip over entries of different loaders. * Removes unreachable case. --- program-runtime/src/loaded_programs.rs | 15 ++-- program-runtime/src/program_cache_entry.rs | 2 +- svm/src/program_loader.rs | 81 ++++++++++++---------- svm/src/transaction_processor.rs | 6 +- svm/tests/concurrent_tests.rs | 3 +- 5 files changed, 64 insertions(+), 43 deletions(-) diff --git a/program-runtime/src/loaded_programs.rs b/program-runtime/src/loaded_programs.rs index b55b3d526da..84d06a5a50c 100644 --- a/program-runtime/src/loaded_programs.rs +++ b/program-runtime/src/loaded_programs.rs @@ -2,7 +2,9 @@ use { crate::{ invoke_context::InvokeContext, loading_task::LoadingTaskWaiter, - program_cache_entry::{ProgramCacheEntry, ProgramCacheEntryType, retention_score}, + program_cache_entry::{ + ProgramCacheEntry, ProgramCacheEntryOwner, ProgramCacheEntryType, retention_score, + }, program_metrics::{EMA_SCALE, ProgramCacheStats}, }, log::error, @@ -188,6 +190,8 @@ impl EpochBoundaryPreparation { pub struct ProgramToLoad<'a> { /// The program address pub program_id: &'a Pubkey, + /// The program loader + pub loader: ProgramCacheEntryOwner, /// Potentially filter out / ignore some entries during the start up / catch up phase pub match_criteria: ProgramCacheMatchCriteria, /// When the program account was last written to (might be after the deployment slot) @@ -408,6 +412,7 @@ impl ProgramCache { at.effective_slot .cmp(&entry.effective_slot) .then(at.deployment_slot.cmp(&entry.deployment_slot)) + .then(at.account_owner.cmp(&entry.account_owner)) .then( // This `.then()` has no effect during normal operation. // Only during the cache preparation phase this does allow entries @@ -434,8 +439,6 @@ impl ProgramCache { ProgramCacheEntryType::Unloaded(_), ProgramCacheEntryType::Loaded(_), ) => {} - (ProgramCacheEntryType::Closed, ProgramCacheEntryType::Closed) - if existing.account_owner != entry.account_owner => {} _ => { // Something is wrong, I can feel it ... error!( @@ -605,7 +608,9 @@ impl ProgramCache { for entry in second_level.iter().rev() { let required_deployment_slot = filter_by_deployment_slot.unwrap_or(entry.deployment_slot); - if required_deployment_slot != entry.deployment_slot { + if required_deployment_slot != entry.deployment_slot + || program_to_load.loader != entry.account_owner + { continue; } let entry_in_same_branch = entry.deployment_slot @@ -1826,6 +1831,7 @@ pub(crate) mod tests { }) .map(|(_program_id, entry)| ProgramToLoad { program_id: key, + loader: entry.account_owner, match_criteria: ProgramCacheMatchCriteria::NoCriteria, last_modification_slot: entry.deployment_slot, }) @@ -2248,6 +2254,7 @@ pub(crate) mod tests { let program1 = Pubkey::new_unique(); let mut missing = vec![ProgramToLoad { program_id: &program1, + loader: ProgramCacheEntryOwner::LoaderV3, match_criteria: ProgramCacheMatchCriteria::NoCriteria, last_modification_slot: 0, }]; diff --git a/program-runtime/src/program_cache_entry.rs b/program-runtime/src/program_cache_entry.rs index 35764a7bd99..80f9cd8a746 100644 --- a/program-runtime/src/program_cache_entry.rs +++ b/program-runtime/src/program_cache_entry.rs @@ -21,7 +21,7 @@ use { pub const DELAY_VISIBILITY_SLOT_OFFSET: Slot = 1; /// The owner of a programs accounts, thus the loader of a program -#[derive(Default, Clone, Copy, PartialEq, Eq, Debug)] +#[derive(Default, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Debug)] pub enum ProgramCacheEntryOwner { #[default] NativeLoader, diff --git a/svm/src/program_loader.rs b/svm/src/program_loader.rs index def37cd9126..9bc992d188b 100644 --- a/svm/src/program_loader.rs +++ b/svm/src/program_loader.rs @@ -1,7 +1,6 @@ #[cfg(feature = "metrics")] use solana_program_runtime::program_metrics::LoadProgramMetrics; use { - crate::account_loader::PROGRAM_OWNERS, solana_account::{AccountSharedData, ReadableAccount, state_traits::StateMut}, solana_clock::Slot, solana_instruction::error::InstructionError, @@ -215,30 +214,34 @@ pub fn load_program_with_pubkey( pub(crate) fn get_program_deployment_slot( callbacks: &CB, program: &AccountSharedData, + loader: ProgramCacheEntryOwner, ) -> TransactionResult { - if bpf_loader_upgradeable::check_id(program.owner()) { - if let Ok(UpgradeableLoaderState::Program { - programdata_address, - }) = program.state() - { - let (programdata, _slot) = callbacks - .get_account_shared_data(&programdata_address) - .ok_or(TransactionError::ProgramAccountNotFound)?; - if let Ok(UpgradeableLoaderState::ProgramData { - slot, - upgrade_authority_address: _, - }) = programdata.state() + match loader { + ProgramCacheEntryOwner::LoaderV1 | ProgramCacheEntryOwner::LoaderV2 => Ok(0), + ProgramCacheEntryOwner::LoaderV3 => { + if let Ok(UpgradeableLoaderState::Program { + programdata_address, + }) = program.state() { - return Ok(slot); + let (programdata, _slot) = callbacks + .get_account_shared_data(&programdata_address) + .ok_or(TransactionError::ProgramAccountNotFound)?; + if let Ok(UpgradeableLoaderState::ProgramData { + slot, + upgrade_authority_address: _, + }) = programdata.state() + { + return Ok(slot); + } } + Err(TransactionError::ProgramAccountNotFound) } - Err(TransactionError::ProgramAccountNotFound) - } else if loader_v4::check_id(program.owner()) { - let state = loader_v4_get_state(program.data()) - .map_err(|_| TransactionError::ProgramAccountNotFound)?; - Ok(state.slot) - } else { - Ok(0) + ProgramCacheEntryOwner::LoaderV4 => { + let state = loader_v4_get_state(program.data()) + .map_err(|_| TransactionError::ProgramAccountNotFound)?; + Ok(state.slot) + } + ProgramCacheEntryOwner::NativeLoader => unreachable!(), } } @@ -256,10 +259,20 @@ pub fn filter_executable_program_accounts<'a, CB: TransactionProcessingCallback> cache_entry.stats.uses.fetch_add(1, Ordering::Relaxed); } else if let Some((account, last_modification_slot)) = callbacks.get_account_shared_data(account_key) - && PROGRAM_OWNERS.contains(account.owner()) { + let loader = if loader_v4::check_id(account.owner()) { + ProgramCacheEntryOwner::LoaderV4 + } else if bpf_loader_upgradeable::check_id(account.owner()) { + ProgramCacheEntryOwner::LoaderV3 + } else if bpf_loader::check_id(account.owner()) { + ProgramCacheEntryOwner::LoaderV2 + } else if bpf_loader_deprecated::check_id(account.owner()) { + ProgramCacheEntryOwner::LoaderV1 + } else { + continue; + }; let match_criteria = if check_program_deployment_slot { - get_program_deployment_slot(callbacks, &account) + get_program_deployment_slot(callbacks, &account, loader) .map_or(ProgramCacheMatchCriteria::Tombstone, |slot| { ProgramCacheMatchCriteria::DeployedOnOrAfterSlot(slot) }) @@ -268,6 +281,7 @@ pub fn filter_executable_program_accounts<'a, CB: TransactionProcessingCallback> }; result.push(ProgramToLoad { program_id: account_key, + loader, match_criteria, last_modification_slot, }); @@ -739,6 +753,7 @@ mod tests { let result = get_program_deployment_slot( &mock_bank, &mock_bank.get_account_shared_data(&key).unwrap().0, + ProgramCacheEntryOwner::LoaderV3, ); assert_eq!(result.err(), Some(TransactionError::ProgramAccountNotFound)); @@ -754,6 +769,7 @@ mod tests { let result = get_program_deployment_slot( &mock_bank, &mock_bank.get_account_shared_data(&key).unwrap().0, + ProgramCacheEntryOwner::LoaderV3, ); assert_eq!(result.err(), Some(TransactionError::ProgramAccountNotFound)); } @@ -778,7 +794,7 @@ mod tests { .borrow_mut() .insert(key1, (account_data, 0)); - let mut account_data = AccountSharedData::new_data( + let account_data = AccountSharedData::new_data( 100, &UpgradeableLoaderState::ProgramData { slot: 77, @@ -795,20 +811,9 @@ mod tests { let result = get_program_deployment_slot( &mock_bank, &mock_bank.get_account_shared_data(&key1).unwrap().0, + ProgramCacheEntryOwner::LoaderV3, ); assert_eq!(result.unwrap(), 77); - - account_data.set_owner(Pubkey::new_unique()); - mock_bank - .account_shared_data - .borrow_mut() - .insert(key2, (account_data, 0)); - - let result = get_program_deployment_slot( - &mock_bank, - &mock_bank.get_account_shared_data(&key2).unwrap().0, - ); - assert_eq!(result.unwrap(), 0); } #[test] @@ -891,11 +896,13 @@ mod tests { &[ ProgramToLoad { program_id: &program_ids[1], + loader: ProgramCacheEntryOwner::LoaderV2, match_criteria: ProgramCacheMatchCriteria::NoCriteria, last_modification_slot: 0, }, ProgramToLoad { program_id: &program_ids[2], + loader: ProgramCacheEntryOwner::LoaderV3, match_criteria: ProgramCacheMatchCriteria::NoCriteria, last_modification_slot: 0, }, @@ -913,11 +920,13 @@ mod tests { &[ ProgramToLoad { program_id: &program_ids[1], + loader: ProgramCacheEntryOwner::LoaderV2, match_criteria: ProgramCacheMatchCriteria::DeployedOnOrAfterSlot(0), last_modification_slot: 0, }, ProgramToLoad { program_id: &program_ids[2], + loader: ProgramCacheEntryOwner::LoaderV3, match_criteria: ProgramCacheMatchCriteria::Tombstone, last_modification_slot: 0, }, diff --git a/svm/src/transaction_processor.rs b/svm/src/transaction_processor.rs index 6dff7307d8d..568fe285858 100644 --- a/svm/src/transaction_processor.rs +++ b/svm/src/transaction_processor.rs @@ -46,7 +46,7 @@ use { ProgramCacheMatchCriteria, ProgramRuntimeEnvironment, ProgramRuntimeEnvironments, ProgramToLoad, }, - program_cache_entry::ProgramCacheEntry, + program_cache_entry::{ProgramCacheEntry, ProgramCacheEntryOwner}, solana_sbpf::{program::BuiltinProgram, vm::Config as VmConfig}, sysvar_cache::SysvarCache, }, @@ -321,6 +321,7 @@ impl TransactionBatchProcessor { .iter() .map(|program_id| ProgramToLoad { program_id, + loader: ProgramCacheEntryOwner::NativeLoader, match_criteria: ProgramCacheMatchCriteria::NoCriteria, last_modification_slot: 0, }) @@ -1703,6 +1704,7 @@ mod tests { &account_loader, vec![ProgramToLoad { program_id: &key, + loader: ProgramCacheEntryOwner::LoaderV3, match_criteria: ProgramCacheMatchCriteria::NoCriteria, last_modification_slot: 0, }], @@ -1741,6 +1743,7 @@ mod tests { &account_loader, vec![ProgramToLoad { program_id: &key, + loader: ProgramCacheEntryOwner::LoaderV2, match_criteria: ProgramCacheMatchCriteria::NoCriteria, last_modification_slot: 0, }], @@ -1963,6 +1966,7 @@ mod tests { .extract( &mut vec![ProgramToLoad { program_id: &key, + loader: ProgramCacheEntryOwner::NativeLoader, match_criteria: ProgramCacheMatchCriteria::NoCriteria, last_modification_slot: 0, }], diff --git a/svm/tests/concurrent_tests.rs b/svm/tests/concurrent_tests.rs index f71cb223ebc..be8701d3493 100644 --- a/svm/tests/concurrent_tests.rs +++ b/svm/tests/concurrent_tests.rs @@ -17,7 +17,7 @@ use { ProgramCacheForTxBatch, ProgramCacheMatchCriteria, ProgramRuntimeEnvironments, ProgramToLoad, }, - program_cache_entry::ProgramCacheEntryType, + program_cache_entry::{ProgramCacheEntryOwner, ProgramCacheEntryType}, }, solana_pubkey::Pubkey, solana_svm::{ @@ -63,6 +63,7 @@ fn program_cache_execution(threads: usize) { .iter() .map(|program_id| ProgramToLoad { program_id, + loader: ProgramCacheEntryOwner::LoaderV3, match_criteria: ProgramCacheMatchCriteria::NoCriteria, last_modification_slot: 0, }) From c1be20c6070a65543e1239d4689e9d8dadea1648 Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Wed, 27 May 2026 10:00:51 -0400 Subject: [PATCH 079/576] rpc: fix leader targetting for alpenglow (#12630) --- Cargo.lock | 1 + dev-bins/Cargo.lock | 1 + programs/sbf/Cargo.lock | 1 + rpc/Cargo.toml | 7 +- rpc/src/cluster_tpu_info.rs | 205 +++++++++++++++++++++++++++++++----- rpc/src/rpc_service.rs | 13 ++- 6 files changed, 199 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 56412e695a5..130eeb7c238 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9693,6 +9693,7 @@ dependencies = [ "agave-feature-set", "agave-reserved-account-keys", "agave-snapshots", + "agave-votor-messages", "base64 0.22.1", "bs58", "crossbeam-channel", diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 66a3a57682e..de7fc166a88 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -8174,6 +8174,7 @@ version = "4.2.0-alpha.0" dependencies = [ "agave-feature-set", "agave-snapshots", + "agave-votor-messages", "base64 0.22.1", "bs58", "crossbeam-channel", diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index cdd0a31f71c..92787fa0204 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -7981,6 +7981,7 @@ version = "4.2.0-alpha.0" dependencies = [ "agave-feature-set", "agave-snapshots", + "agave-votor-messages", "base64 0.22.1", "bs58", "crossbeam-channel", diff --git a/rpc/Cargo.toml b/rpc/Cargo.toml index 4f72fad0480..08f270ccdd0 100644 --- a/rpc/Cargo.toml +++ b/rpc/Cargo.toml @@ -18,11 +18,16 @@ name = "solana_rpc" [features] agave-unstable-api = [] -dev-context-only-utils = ["solana-ledger/dev-context-only-utils", "solana-rpc/dev-context-only-utils"] +dev-context-only-utils = [ + "agave-votor-messages/dev-context-only-utils", + "solana-ledger/dev-context-only-utils", + "solana-rpc/dev-context-only-utils", +] [dependencies] agave-feature-set = { workspace = true } agave-snapshots = { workspace = true } +agave-votor-messages = { workspace = true } base64 = { workspace = true } bs58 = { workspace = true } crossbeam-channel = { workspace = true } diff --git a/rpc/src/cluster_tpu_info.rs b/rpc/src/cluster_tpu_info.rs index f941f077eed..f4cf7fc2243 100644 --- a/rpc/src/cluster_tpu_info.rs +++ b/rpc/src/cluster_tpu_info.rs @@ -1,8 +1,12 @@ use { + agave_votor_messages::migration::MigrationStatus, + solana_clock::Slot, solana_gossip::{cluster_info::ClusterInfo, contact_info::Protocol}, solana_leader_schedule::NUM_CONSECUTIVE_LEADER_SLOTS, + solana_ledger::leader_schedule_cache::LeaderScheduleCache, solana_poh::poh_recorder::PohRecorder, solana_pubkey::Pubkey, + solana_runtime::commitment::BlockCommitmentCache, solana_send_transaction_service::tpu_info::TpuInfo, std::{ collections::HashMap, @@ -16,17 +20,78 @@ use { pub struct ClusterTpuInfo { cluster_info: Arc, poh_recorder: Arc>, + block_commitment_cache: Arc>, + leader_schedule_cache: Arc, + migration_status: Arc, recent_peers: HashMap, // values are socket address for QUIC protocol } impl ClusterTpuInfo { - pub fn new(cluster_info: Arc, poh_recorder: Arc>) -> Self { + pub fn new( + cluster_info: Arc, + poh_recorder: Arc>, + block_commitment_cache: Arc>, + leader_schedule_cache: Arc, + migration_status: Arc, + ) -> Self { Self { cluster_info, poh_recorder, + block_commitment_cache, + leader_schedule_cache, + migration_status, recent_peers: HashMap::new(), } } + + fn leader_pubkeys(&self, max_count: u64) -> Vec { + let current_slot = self.commitment_current_slot(); + // Commitment-cache slots represent banks we have already processed, so + // transactions can only land in later slots. + let target_slot = current_slot.saturating_add(1); + if self + .migration_status + .should_have_alpenglow_ticks(target_slot) + { + self.alpenglow_leader_pubkeys(target_slot, max_count) + } else { + self.poh_leader_pubkeys(max_count) + } + } + + fn commitment_current_slot(&self) -> Slot { + let commitment_slots = self + .block_commitment_cache + .read() + .unwrap() + .commitment_slots(); + commitment_slots + .slot + .max(commitment_slots.root) + .max(commitment_slots.highest_confirmed_slot) + .max(commitment_slots.highest_super_majority_root) + } + + fn alpenglow_leader_pubkeys(&self, current_slot: Slot, max_count: u64) -> Vec { + (0..max_count) + .filter_map(|i| { + let target_slot = current_slot + .saturating_add(i.saturating_mul(NUM_CONSECUTIVE_LEADER_SLOTS.get() as u64)); + self.leader_schedule_cache + .slot_leader_at(target_slot, None) + .map(|leader| leader.id) + }) + .collect() + } + + fn poh_leader_pubkeys(&self, max_count: u64) -> Vec { + let recorder = self.poh_recorder.read().unwrap(); + (0..max_count) + .filter_map(|i| { + recorder.leader_after_n_slots(i * NUM_CONSECUTIVE_LEADER_SLOTS.get() as u64) + }) + .collect() + } } impl TpuInfo for ClusterTpuInfo { @@ -41,13 +106,7 @@ impl TpuInfo for ClusterTpuInfo { } fn get_leader_tpus(&self, max_count: u64) -> Vec<&SocketAddr> { - let recorder = self.poh_recorder.read().unwrap(); - let leaders: Vec<_> = (0..max_count) - .filter_map(|i| { - recorder.leader_after_n_slots(i * NUM_CONSECUTIVE_LEADER_SLOTS.get() as u64) - }) - .collect(); - drop(recorder); + let leaders = self.leader_pubkeys(max_count); let mut unique_leaders = vec![]; for leader in leaders.iter() { if let Some(addr) = self.recent_peers.get(leader) { @@ -60,14 +119,7 @@ impl TpuInfo for ClusterTpuInfo { } fn get_not_unique_leader_tpus(&self, max_count: u64) -> Vec<&SocketAddr> { - let recorder = self.poh_recorder.read().unwrap(); - let leader_pubkeys: Vec<_> = (0..max_count) - .filter_map(|i| { - recorder.leader_after_n_slots(i * NUM_CONSECUTIVE_LEADER_SLOTS.get() as u64) - }) - .collect(); - drop(recorder); - leader_pubkeys + self.leader_pubkeys(max_count) .iter() .filter_map(|leader_pubkey| self.recent_peers.get(leader_pubkey)) .collect() @@ -78,7 +130,6 @@ impl TpuInfo for ClusterTpuInfo { mod test { use { super::*, - solana_clock::Slot, solana_gossip::contact_info::ContactInfo, solana_keypair::Keypair, solana_ledger::{ @@ -92,6 +143,7 @@ mod test { genesis_utils::{ GenesisConfigInfo, ValidatorVoteKeypairs, create_genesis_config_with_vote_accounts, }, + leader_schedule_utils::slot_leader_at, }, solana_signer::Signer, solana_time_utils::timestamp, @@ -124,6 +176,7 @@ mod test { ); let bank = Arc::new(Bank::new_for_tests(&genesis_config)); + let leader_schedule_cache = Arc::new(LeaderScheduleCache::new_from_bank(&bank)); let (poh_recorder, _entry_receiver) = PohRecorder::new( 0, bank.last_blockhash(), @@ -131,7 +184,7 @@ mod test { Some((2, 2)), bank.ticks_per_slot(), Arc::new(blockstore), - &Arc::new(LeaderScheduleCache::new_from_bank(&bank)), + &leader_schedule_cache, &PohConfig::default(), Arc::new(AtomicBool::default()), ); @@ -156,8 +209,13 @@ mod test { cluster_info.insert_info(validator1_contact_info); cluster_info.insert_info(validator2_contact_info); - let mut leader_info = - ClusterTpuInfo::new(cluster_info, Arc::new(RwLock::new(poh_recorder))); + let mut leader_info = ClusterTpuInfo::new( + cluster_info, + Arc::new(RwLock::new(poh_recorder)), + Arc::new(RwLock::new(BlockCommitmentCache::new_for_tests())), + leader_schedule_cache, + Arc::new(MigrationStatus::default()), + ); leader_info.refresh_recent_peers(); let mut refreshed_recent_peers = leader_info.recent_peers.keys().copied().collect::>(); @@ -186,6 +244,7 @@ mod test { ); let bank = Arc::new(Bank::new_for_tests(&genesis_config)); + let leader_schedule_cache = Arc::new(LeaderScheduleCache::new_from_bank(&bank)); let (poh_recorder, _entry_receiver) = PohRecorder::new( 0, bank.last_blockhash(), @@ -193,7 +252,7 @@ mod test { Some((2, 2)), bank.ticks_per_slot(), Arc::new(blockstore), - &Arc::new(LeaderScheduleCache::new_from_bank(&bank)), + &leader_schedule_cache, &PohConfig::default(), Arc::new(AtomicBool::default()), ); @@ -225,11 +284,14 @@ mod test { .iter() .cloned() .collect(); - let leader_info = ClusterTpuInfo { + let mut leader_info = ClusterTpuInfo::new( cluster_info, - poh_recorder: Arc::new(RwLock::new(poh_recorder)), - recent_peers: recent_peers.clone(), - }; + Arc::new(RwLock::new(poh_recorder)), + Arc::new(RwLock::new(BlockCommitmentCache::new_for_tests())), + leader_schedule_cache, + Arc::new(MigrationStatus::default()), + ); + leader_info.recent_peers = recent_peers.clone(); let slot = bank.slot(); let first_leader = @@ -285,4 +347,97 @@ mod test { assert_eq!(leader_info.get_not_unique_leader_tpus(x).len(), x as usize); } } + + #[test] + fn test_get_leader_tpus_uses_commitment_slot_after_alpenglow() { + let ledger_path = get_tmp_ledger_path_auto_delete!(); + let blockstore = Blockstore::open(ledger_path.path()).unwrap(); + + let validator_vote_keypairs0 = ValidatorVoteKeypairs::new_rand(); + let validator_vote_keypairs1 = ValidatorVoteKeypairs::new_rand(); + let validator_vote_keypairs2 = ValidatorVoteKeypairs::new_rand(); + let validator_keypairs = vec![ + &validator_vote_keypairs0, + &validator_vote_keypairs1, + &validator_vote_keypairs2, + ]; + let GenesisConfigInfo { genesis_config, .. } = create_genesis_config_with_vote_accounts( + 1_000_000_000, + &validator_keypairs, + vec![10_000; 3], + ); + let bank = Arc::new(Bank::new_for_tests(&genesis_config)); + let leader_schedule_cache = Arc::new(LeaderScheduleCache::new_from_bank(&bank)); + + let (poh_recorder, _entry_receiver) = PohRecorder::new( + 0, + bank.last_blockhash(), + bank.clone(), + Some((2, 2)), + bank.ticks_per_slot(), + Arc::new(blockstore), + &leader_schedule_cache, + &PohConfig::default(), + Arc::new(AtomicBool::default()), + ); + + let node_keypair = Arc::new(Keypair::new()); + let cluster_info = Arc::new(ClusterInfo::new( + ContactInfo::new_localhost(&node_keypair.pubkey(), timestamp()), + node_keypair, + SocketAddrSpace::Unspecified, + )); + + let validator0_socket = SocketAddr::from((Ipv4Addr::LOCALHOST, 1112)); + let validator1_socket = SocketAddr::from((Ipv4Addr::LOCALHOST, 2223)); + let validator2_socket = SocketAddr::from((Ipv4Addr::LOCALHOST, 3334)); + let recent_peers: HashMap<_, _> = [ + ( + validator_vote_keypairs0.node_keypair.pubkey(), + validator0_socket, + ), + ( + validator_vote_keypairs1.node_keypair.pubkey(), + validator1_socket, + ), + ( + validator_vote_keypairs2.node_keypair.pubkey(), + validator2_socket, + ), + ] + .iter() + .cloned() + .collect(); + + let completed_alpenglow_slot = (1..64) + .find(|slot| { + slot_leader_at(*slot, &bank).unwrap() + != slot_leader_at(slot.saturating_add(1), &bank).unwrap() + }) + .expect("leader schedule should rotate within 64 slots"); + let completed_slot_leader = slot_leader_at(completed_alpenglow_slot, &bank).unwrap(); + let target_slot_leader = + slot_leader_at(completed_alpenglow_slot.saturating_add(1), &bank).unwrap(); + + let mut leader_info = ClusterTpuInfo::new( + cluster_info, + Arc::new(RwLock::new(poh_recorder)), + Arc::new(RwLock::new(BlockCommitmentCache::new_for_tests_with_slots( + completed_alpenglow_slot, + completed_alpenglow_slot, + ))), + leader_schedule_cache, + Arc::new(MigrationStatus::post_migration_status()), + ); + leader_info.recent_peers = recent_peers.clone(); + + assert_eq!( + leader_info.get_not_unique_leader_tpus(1), + vec![recent_peers.get(&target_slot_leader).unwrap()] + ); + assert_ne!( + leader_info.get_not_unique_leader_tpus(1), + vec![recent_peers.get(&completed_slot_leader).unwrap()] + ); + } } diff --git a/rpc/src/rpc_service.rs b/rpc/src/rpc_service.rs index 4fb9d692e7f..29b50357666 100644 --- a/rpc/src/rpc_service.rs +++ b/rpc/src/rpc_service.rs @@ -512,9 +512,16 @@ impl JsonRpcService { config.rpc_config.rpc_blocking_threads, config.rpc_config.rpc_niceness_adj, ); - let leader_info = config - .poh_recorder - .map(|recorder| ClusterTpuInfo::new(config.cluster_info.clone(), recorder)); + let migration_status = config.bank_forks.read().unwrap().migration_status(); + let leader_info = config.poh_recorder.clone().map(|recorder| { + ClusterTpuInfo::new( + config.cluster_info.clone(), + recorder, + config.block_commitment_cache.clone(), + config.leader_schedule_cache.clone(), + migration_status.clone(), + ) + }); let RpcTpuClientArgs(identity_keypair, tpu_client_socket, client_runtime, cancel) = config.rpc_tpu_client_args; From dd749a04b914369eec80878b086eb11ca723736b Mon Sep 17 00:00:00 2001 From: Faycel Kouteib Date: Wed, 27 May 2026 07:41:41 -0700 Subject: [PATCH 080/576] Guard solana-test-validator crate with agave-unstable-api feature (#12749) --- accounts-cluster-bench/Cargo.toml | 2 +- bench-tps/Cargo.toml | 2 +- cli/Cargo.toml | 2 +- client-test/Cargo.toml | 2 +- programs/sbf/Cargo.toml | 2 +- programs/sbf/tests/simulation.rs | 2 +- rpc-test/Cargo.toml | 3 +-- test-validator/Cargo.toml | 2 +- test-validator/src/lib.rs | 1 + tokens/Cargo.toml | 2 +- validator/Cargo.toml | 1 + 11 files changed, 11 insertions(+), 10 deletions(-) diff --git a/accounts-cluster-bench/Cargo.toml b/accounts-cluster-bench/Cargo.toml index 73cfed028d6..b2035d11bf4 100644 --- a/accounts-cluster-bench/Cargo.toml +++ b/accounts-cluster-bench/Cargo.toml @@ -57,7 +57,7 @@ solana-local-cluster = { path = "../local-cluster", features = ["agave-unstable- solana-native-token = { workspace = true } solana-poh-config = { workspace = true } solana-runtime = { path = "../runtime", features = ["agave-unstable-api", "dev-context-only-utils"] } -solana-test-validator = { path = "../test-validator", features = ["dev-context-only-utils"] } +solana-test-validator = { path = "../test-validator", features = ["agave-unstable-api", "dev-context-only-utils"] } [lints] workspace = true diff --git a/bench-tps/Cargo.toml b/bench-tps/Cargo.toml index c8fe8e7513c..3e4247af455 100644 --- a/bench-tps/Cargo.toml +++ b/bench-tps/Cargo.toml @@ -81,7 +81,7 @@ solana-local-cluster = { workspace = true } solana-rent = { workspace = true } solana-runtime = { workspace = true, features = ["dev-context-only-utils"] } solana-sdk-ids = { workspace = true } -solana-test-validator = { workspace = true, features = ["dev-context-only-utils"] } +solana-test-validator = { workspace = true, features = ["agave-unstable-api", "dev-context-only-utils"] } solana-tps-client = { workspace = true, features = ["bank-client"] } tempfile = { workspace = true } diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 339ff8d034b..9c01f273b10 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -106,7 +106,7 @@ solana-nonce-account = { workspace = true } solana-presigner = { workspace = true } solana-rpc = { path = "../rpc", features = ["agave-unstable-api"] } solana-sha256-hasher = { workspace = true } -solana-test-validator = { path = "../test-validator", features = ["dev-context-only-utils"] } +solana-test-validator = { path = "../test-validator", features = ["agave-unstable-api", "dev-context-only-utils"] } tempfile = { workspace = true } test-case = { workspace = true } diff --git a/client-test/Cargo.toml b/client-test/Cargo.toml index 0b6f6b05961..b4f6aaadede 100644 --- a/client-test/Cargo.toml +++ b/client-test/Cargo.toml @@ -46,7 +46,7 @@ agave-logger = { path = "../logger", features = ["agave-unstable-api"] } solana-net-utils = { path = "../net-utils", features = ["agave-unstable-api"] } solana-rpc = { path = "../rpc", features = ["agave-unstable-api", "dev-context-only-utils"] } solana-runtime = { path = "../runtime", features = ["agave-unstable-api", "dev-context-only-utils"] } -solana-test-validator = { path = "../test-validator", features = ["dev-context-only-utils"] } +solana-test-validator = { path = "../test-validator", features = ["agave-unstable-api", "dev-context-only-utils"] } [lints] workspace = true diff --git a/programs/sbf/Cargo.toml b/programs/sbf/Cargo.toml index 3982cd5c40d..b6d79c2bc0d 100644 --- a/programs/sbf/Cargo.toml +++ b/programs/sbf/Cargo.toml @@ -209,7 +209,7 @@ solana-svm-transaction = { workspace = true } solana-svm-type-overrides = { workspace = true } solana-system-interface = { workspace = true } solana-sysvar = "4.0.0" -solana-test-validator = { workspace = true, features = ["dev-context-only-utils"] } +solana-test-validator = { workspace = true, features = ["agave-unstable-api", "dev-context-only-utils"] } solana-transaction = "4.1.0" solana-transaction-error = "3.2.0" solana-vote = { workspace = true } diff --git a/programs/sbf/tests/simulation.rs b/programs/sbf/tests/simulation.rs index cf6674ae8b9..efd689c8d35 100644 --- a/programs/sbf/tests/simulation.rs +++ b/programs/sbf/tests/simulation.rs @@ -1,7 +1,6 @@ #![cfg(feature = "sbf_rust")] use { - agave_validator::test_validator::*, solana_instruction::{AccountMeta, Instruction}, solana_message::Message, solana_pubkey::Pubkey, @@ -15,6 +14,7 @@ use { solana_sdk_ids::bpf_loader_upgradeable, solana_signer::Signer, solana_sysvar::{clock, slot_history}, + solana_test_validator::TestValidatorGenesis, solana_transaction::Transaction, std::{thread::sleep, time::Duration}, }; diff --git a/rpc-test/Cargo.toml b/rpc-test/Cargo.toml index 0b76288bdb4..bcc56c1c491 100644 --- a/rpc-test/Cargo.toml +++ b/rpc-test/Cargo.toml @@ -31,7 +31,6 @@ solana-net-utils = { workspace = true } solana-pubsub-client = { workspace = true } solana-rpc-client = { workspace = true } solana-rpc-client-api = { workspace = true } -solana-test-validator = { workspace = true } solana-tpu-client = { workspace = true } solana-tpu-client-next = { workspace = true } solana-transaction-status = { workspace = true } @@ -50,7 +49,7 @@ solana-rent = { workspace = true } solana-signature = { workspace = true } solana-signer = { workspace = true } solana-system-transaction = { workspace = true } -solana-test-validator = { workspace = true, features = ["dev-context-only-utils"] } +solana-test-validator = { workspace = true, features = ["agave-unstable-api", "dev-context-only-utils"] } solana-transaction = { workspace = true } [lints] diff --git a/test-validator/Cargo.toml b/test-validator/Cargo.toml index 01fe168573d..81bdad4c208 100644 --- a/test-validator/Cargo.toml +++ b/test-validator/Cargo.toml @@ -67,7 +67,7 @@ tokio = { workspace = true, features = ["full"] } [dev-dependencies] solana-sdk-ids = { workspace = true } # See order-crates-for-publishing.py for using this unusual `path = "."` -solana-test-validator = { path = ".", features = ["dev-context-only-utils"] } +solana-test-validator = { path = ".", features = ["agave-unstable-api", "dev-context-only-utils"] } [lints] workspace = true diff --git a/test-validator/src/lib.rs b/test-validator/src/lib.rs index 7e5d4cb5303..f689ec94345 100644 --- a/test-validator/src/lib.rs +++ b/test-validator/src/lib.rs @@ -1,3 +1,4 @@ +#![cfg(feature = "agave-unstable-api")] #![allow(clippy::arithmetic_side_effects)] use { agave_feature_set::{FEATURE_NAMES, FeatureSet, alpenglow, raise_cpi_nesting_limit_to_8}, diff --git a/tokens/Cargo.toml b/tokens/Cargo.toml index 69fbeb967e9..6318551d4ba 100644 --- a/tokens/Cargo.toml +++ b/tokens/Cargo.toml @@ -61,7 +61,7 @@ thiserror = { workspace = true } agave-logger = { path = "../logger", features = ["agave-unstable-api"] } assert_matches = { workspace = true } bincode = { workspace = true } -solana-test-validator = { path = "../test-validator", features = ["dev-context-only-utils"] } +solana-test-validator = { path = "../test-validator", features = ["agave-unstable-api", "dev-context-only-utils"] } solana-transaction-error = { workspace = true } [lints] diff --git a/validator/Cargo.toml b/validator/Cargo.toml index 853c5f9eed2..99297bb342c 100644 --- a/validator/Cargo.toml +++ b/validator/Cargo.toml @@ -112,6 +112,7 @@ solana-core = { path = "../core", features = ["agave-unstable-api", "dev-context solana-program-option = { workspace = true } solana-program-pack = { workspace = true } solana-runtime = { path = "../runtime", features = ["agave-unstable-api", "dev-context-only-utils"] } +solana-test-validator = { workspace = true, features = ["agave-unstable-api"] } solana-time-utils = { workspace = true } tempfile = { workspace = true } test-case = { workspace = true } From 1d7232615028f328738a641cfcd2f6aa3c88d0a5 Mon Sep 17 00:00:00 2001 From: Quentin Kniep Date: Wed, 27 May 2026 18:29:19 +0200 Subject: [PATCH 081/576] nit: add assert for `delta_first_slice <= delta_block` (#12765) add assert for `delta_first_slice <= delta_block` --- votor/src/timer_manager/timers.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/votor/src/timer_manager/timers.rs b/votor/src/timer_manager/timers.rs index 4f462f8975a..8f56ee3082a 100644 --- a/votor/src/timer_manager/timers.rs +++ b/votor/src/timer_manager/timers.rs @@ -202,6 +202,7 @@ impl Timers { delta_first_slice: Duration, delta_block: Duration, ) { + assert!(delta_first_slice <= delta_block); assert_eq!(self.heap.len(), self.timers.len()); let timeout_config = TimeoutConfig { delta_timeout: self.delta_timeout, From 875146286a56d529075e8fc7fa7e600a1cc2dabd Mon Sep 17 00:00:00 2001 From: Quentin Kniep Date: Wed, 27 May 2026 21:13:39 +0200 Subject: [PATCH 082/576] fix(votor): prevent overflow when scaling `delta_timeout` (#12772) --- votor/src/timer_manager/timers.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/votor/src/timer_manager/timers.rs b/votor/src/timer_manager/timers.rs index 8f56ee3082a..950852abe41 100644 --- a/votor/src/timer_manager/timers.rs +++ b/votor/src/timer_manager/timers.rs @@ -42,8 +42,12 @@ struct TimeoutConfig { impl TimeoutConfig { fn scaled(&self, slot: Slot, standstill_slot: Option) -> TimeoutConfig { let multiplier = calculate_timeout_multiplier(slot, standstill_slot); + let delta_timeout = Duration::try_from_secs_f64( + (self.delta_timeout.as_secs_f64() * multiplier).clamp(0.0, MAX_TIMEOUT.as_secs_f64()), + ) + .unwrap_or(MAX_TIMEOUT); TimeoutConfig { - delta_timeout: self.delta_timeout.mul_f64(multiplier).min(MAX_TIMEOUT), + delta_timeout, delta_first_slice: self.delta_first_slice, delta_block: self.delta_block, } From 2e916ca2d8cc853aae3021d4bcb66a259375cf2f Mon Sep 17 00:00:00 2001 From: Kamil Skalski Date: Wed, 27 May 2026 21:22:52 +0200 Subject: [PATCH 083/576] chore(build): switch all crates to workspace level edition (#12738) chore(build): switch crates to workspace level edition --- bloom/Cargo.toml | 2 +- compute-budget-instruction/Cargo.toml | 2 +- fee/Cargo.toml | 2 +- gossip/Cargo.toml | 2 +- net-utils/Cargo.toml | 2 +- program-runtime/Cargo.toml | 2 +- programs/bpf_loader/Cargo.toml | 2 +- runtime-transaction/Cargo.toml | 2 +- svm-transaction/Cargo.toml | 2 +- svm/Cargo.toml | 2 +- tls-utils/Cargo.toml | 2 +- transaction-context/Cargo.toml | 2 +- transaction-view/Cargo.toml | 2 +- turbine/Cargo.toml | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/bloom/Cargo.toml b/bloom/Cargo.toml index f5e5d1fd62d..2756425e38b 100644 --- a/bloom/Cargo.toml +++ b/bloom/Cargo.toml @@ -7,7 +7,7 @@ authors = { workspace = true } repository = { workspace = true } homepage = { workspace = true } license = { workspace = true } -edition = "2024" +edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] diff --git a/compute-budget-instruction/Cargo.toml b/compute-budget-instruction/Cargo.toml index 1a1dade28d6..4fe0d3cc512 100644 --- a/compute-budget-instruction/Cargo.toml +++ b/compute-budget-instruction/Cargo.toml @@ -7,7 +7,7 @@ authors = { workspace = true } repository = { workspace = true } homepage = { workspace = true } license = { workspace = true } -edition = "2024" +edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] diff --git a/fee/Cargo.toml b/fee/Cargo.toml index e84737d6e05..06d9cf2057a 100644 --- a/fee/Cargo.toml +++ b/fee/Cargo.toml @@ -7,7 +7,7 @@ authors = { workspace = true } repository = { workspace = true } homepage = { workspace = true } license = { workspace = true } -edition = "2024" +edition = { workspace = true } [features] agave-unstable-api = [] diff --git a/gossip/Cargo.toml b/gossip/Cargo.toml index ed5e2dad4e5..bf085dd4974 100644 --- a/gossip/Cargo.toml +++ b/gossip/Cargo.toml @@ -7,7 +7,7 @@ description = { workspace = true } repository = { workspace = true } homepage = { workspace = true } license = { workspace = true } -edition = "2024" +edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] diff --git a/net-utils/Cargo.toml b/net-utils/Cargo.toml index c5517d6983e..16f2b2f2e93 100644 --- a/net-utils/Cargo.toml +++ b/net-utils/Cargo.toml @@ -7,7 +7,7 @@ authors = { workspace = true } repository = { workspace = true } homepage = { workspace = true } license = { workspace = true } -edition = "2024" +edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] diff --git a/program-runtime/Cargo.toml b/program-runtime/Cargo.toml index f0c5c4ea6cc..2662a3ec2f7 100644 --- a/program-runtime/Cargo.toml +++ b/program-runtime/Cargo.toml @@ -7,7 +7,7 @@ authors = { workspace = true } repository = { workspace = true } homepage = { workspace = true } license = { workspace = true } -edition = "2024" +edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] diff --git a/programs/bpf_loader/Cargo.toml b/programs/bpf_loader/Cargo.toml index f079be29680..2dade0e942c 100644 --- a/programs/bpf_loader/Cargo.toml +++ b/programs/bpf_loader/Cargo.toml @@ -7,7 +7,7 @@ authors = { workspace = true } repository = { workspace = true } homepage = { workspace = true } license = { workspace = true } -edition = "2024" +edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] diff --git a/runtime-transaction/Cargo.toml b/runtime-transaction/Cargo.toml index 607c4eca939..44cc039a367 100644 --- a/runtime-transaction/Cargo.toml +++ b/runtime-transaction/Cargo.toml @@ -7,7 +7,7 @@ authors = { workspace = true } repository = { workspace = true } homepage = { workspace = true } license = { workspace = true } -edition = "2024" +edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] diff --git a/svm-transaction/Cargo.toml b/svm-transaction/Cargo.toml index 2cf59d589ff..ff4e99c99fb 100644 --- a/svm-transaction/Cargo.toml +++ b/svm-transaction/Cargo.toml @@ -7,7 +7,7 @@ authors = { workspace = true } repository = { workspace = true } homepage = { workspace = true } license = { workspace = true } -edition = "2024" +edition = { workspace = true } [features] agave-unstable-api = [] diff --git a/svm/Cargo.toml b/svm/Cargo.toml index 6bd86523947..0190e853111 100644 --- a/svm/Cargo.toml +++ b/svm/Cargo.toml @@ -7,7 +7,7 @@ authors = { workspace = true } repository = { workspace = true } homepage = { workspace = true } license = { workspace = true } -edition = "2024" +edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] diff --git a/tls-utils/Cargo.toml b/tls-utils/Cargo.toml index 559c5f9295c..6afd2b351f3 100644 --- a/tls-utils/Cargo.toml +++ b/tls-utils/Cargo.toml @@ -7,7 +7,7 @@ authors = { workspace = true } repository = { workspace = true } homepage = { workspace = true } license = { workspace = true } -edition = "2024" +edition = { workspace = true } [features] agave-unstable-api = [] diff --git a/transaction-context/Cargo.toml b/transaction-context/Cargo.toml index c74318e27ca..67cbb6cfb3c 100644 --- a/transaction-context/Cargo.toml +++ b/transaction-context/Cargo.toml @@ -7,7 +7,7 @@ authors = { workspace = true } repository = { workspace = true } homepage = { workspace = true } license = { workspace = true } -edition = "2024" +edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] diff --git a/transaction-view/Cargo.toml b/transaction-view/Cargo.toml index 95283063445..beb2d6547f0 100644 --- a/transaction-view/Cargo.toml +++ b/transaction-view/Cargo.toml @@ -7,7 +7,7 @@ authors = { workspace = true } repository = { workspace = true } homepage = { workspace = true } license = { workspace = true } -edition = "2024" +edition = { workspace = true } [features] agave-unstable-api = [] diff --git a/turbine/Cargo.toml b/turbine/Cargo.toml index 3a8bd0116ae..eea5f3fe7fe 100644 --- a/turbine/Cargo.toml +++ b/turbine/Cargo.toml @@ -7,7 +7,7 @@ description = { workspace = true } repository = { workspace = true } homepage = { workspace = true } license = { workspace = true } -edition = "2024" +edition = { workspace = true } [features] agave-unstable-api = [] From a3e8f8d915270a5942a5878c729dab968cce8651 Mon Sep 17 00:00:00 2001 From: steviez Date: Wed, 27 May 2026 14:32:23 -0500 Subject: [PATCH 084/576] ci: Specify user-agent for crates.io curl (#12775) --- ci/check-crates.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ci/check-crates.sh b/ci/check-crates.sh index b83a5606aae..fa62985d7f9 100755 --- a/ci/check-crates.sh +++ b/ci/check-crates.sh @@ -88,7 +88,9 @@ for file in "${files[@]}"; do exit 1 fi - response=$(curl -s https://crates.io/api/v1/crates/"$crate_name"/owners) + # crates.io API docs state to include a user-agent header: + # https://crates.io/data-access#api + response=$(curl -A 'Anza (https://github.com/anza-xyz/agave)' -s https://crates.io/api/v1/crates/"$crate_name"/owners) errors=$(echo "$response" | jq .errors) if [[ $errors != "null" ]]; then details=$(echo "$response" | jq .errors | jq -r ".[0].detail") From 8bc1399e06f411d2c2ebb4adba3f23248d3b52cd Mon Sep 17 00:00:00 2001 From: Rory Harris Date: Wed, 27 May 2026 12:46:23 -0700 Subject: [PATCH 085/576] Split out update_index between shrink and flush (#12751) * Split out update_index between shrink and flush * Updates from review: - Use variables for duplicated constants in tests - Update naming - Remove preallocated account map entry --- accounts-db/src/accounts_db.rs | 53 ++++++++- accounts-db/src/accounts_index.rs | 106 ++++++++++++++++++ .../accounts_index/in_mem_accounts_index.rs | 40 +++++++ 3 files changed, 196 insertions(+), 3 deletions(-) diff --git a/accounts-db/src/accounts_db.rs b/accounts-db/src/accounts_db.rs index ccd33f8d2da..df104075953 100644 --- a/accounts-db/src/accounts_db.rs +++ b/accounts-db/src/accounts_db.rs @@ -3713,7 +3713,7 @@ impl AccountsDb { // | | // V | // S3 store_accounts_for_shrink()/ | index - // update_index_stored_accounts()| (replaces existing store_id, offset in stores) + // update_index_for_shrink() | (replaces existing store_id, offset in stores) // | | // V | // S4 do_shrink_slot_store()/ | map of stores (removes old entry) @@ -5128,6 +5128,54 @@ impl AccountsDb { } } + /// Updates the accounts index for the shrink path: each account at `accounts.slot(i)` has + /// its existing index entry replaced to point at the rewritten storage at `target_slot`. + /// + /// Unlike `update_index_stored_accounts` this does not collect reclaims — the caller is + /// responsible for the source storage's alive-bytes accounting. Secondary indexes are also + /// not touched, since shrink only changes `(store_id, offset)` and they index by pubkey. + fn update_index_for_shrink<'a>( + &self, + infos: Vec, + accounts: &impl StorableAccounts<'a>, + update_index_thread_selection: UpdateIndexThreadSelection, + thread_pool: &ThreadPool, + ) { + let target_slot = accounts.target_slot(); + let len = std::cmp::min(accounts.len(), infos.len()); + + let update = |start, end| { + (start..end).for_each(|i| { + let info: AccountInfo = infos[i]; + debug_assert!(!info.is_cached()); + accounts.account(i, |account| { + let old_slot = accounts.slot(i); + self.accounts_index + .replace(target_slot, old_slot, account.pubkey(), info); + }); + }); + }; + + let threshold = 1; + if matches!( + update_index_thread_selection, + UpdateIndexThreadSelection::PoolWithThreshold, + ) && len > threshold + { + let chunk_size = len.div_ceil(thread_pool.current_num_threads()); + let batches = 1 + len / chunk_size; + thread_pool.install(|| { + (0..batches).into_par_iter().for_each(|batch| { + let start = batch * chunk_size; + let end = std::cmp::min(start + chunk_size, len); + update(start, end) + }) + }); + } else { + update(0, len); + } + } + fn should_not_shrink(alive_bytes: u64, total_bytes: u64) -> bool { alive_bytes >= total_bytes } @@ -5550,10 +5598,9 @@ impl AccountsDb { let write_accounts_us = write_accounts_time.end_as_us(); let update_index_time = Measure::start("update_index"); - self.update_index_stored_accounts( + self.update_index_for_shrink( infos, &accounts, - UpsertReclaim::IgnoreReclaims, update_index_thread_selection, &self.thread_pool_background, ); diff --git a/accounts-db/src/accounts_index.rs b/accounts-db/src/accounts_index.rs index 29006387a09..b906c48124c 100644 --- a/accounts-db/src/accounts_index.rs +++ b/accounts-db/src/accounts_index.rs @@ -949,6 +949,18 @@ impl + Into> AccountsIndex { map.upsert(pubkey, new_item, Some(old_slot), reclaims, reclaim); } + /// Replaces the slot list entry at `old_slot` with `(new_slot, account_info)` for `pubkey`. + /// + /// Used by the shrink path: the account already exists in the index at `old_slot`, and + /// shrink is rewriting it into a new storage at `new_slot`. The previous entry is discarded + /// (no reclaims are returned — the caller manages the source storage's alive-bytes accounting). + /// + /// Panics if `old_slot` is not present in the slot list. + pub fn replace(&self, new_slot: Slot, old_slot: Slot, pubkey: &Pubkey, account_info: T) { + let map = self.get_bin(pubkey); + map.replace(pubkey, (new_slot, account_info), old_slot); + } + pub fn ref_count_from_storage(&self, pubkey: &Pubkey) -> RefCount { let map = self.get_bin(pubkey); map.get_internal_inner(pubkey, |entry| { @@ -2242,6 +2254,100 @@ mod tests { assert_eq!(slot_list_len, 1); } + #[test] + fn test_replace_same_slot() { + // When new_slot == old_slot, replace acts as an in-place update of the account_info. + let key = solana_pubkey::new_rand(); + let index = AccountsIndex::::default_for_tests(); + let mut gc = ReclaimsSlotList::new(); + + let slot = 5; + index.upsert( + slot, + slot, + &key, + 100, + &mut gc, + UpsertReclaim::IgnoreReclaims, + ); + assert_eq!(index.ref_count_from_storage(&key), 1); + + let account_info = 200; + + index.replace(slot, slot, &key, account_info); + + // Slot list now holds the new account_info at the same slot. + let slot_list = index.get_and_then(&key, |entry| { + (false, entry.unwrap().slot_list_read_lock().clone_list()) + }); + assert_eq!(slot_list, SlotList::from([(slot, account_info)])); + // Replace doesn't change refcounts. + assert_eq!(index.ref_count_from_storage(&key), 1); + } + + #[test] + fn test_replace_moves_entry_to_new_slot() { + // Replace finds the entry at old_slot, swaps it out for one at new_slot. + let key = solana_pubkey::new_rand(); + let index = AccountsIndex::::default_for_tests(); + let mut gc = ReclaimsSlotList::new(); + + let old_slot = 5; + let new_slot = 10; + let account_info = 200; + index.upsert( + old_slot, + old_slot, + &key, + 100, + &mut gc, + UpsertReclaim::IgnoreReclaims, + ); + assert_eq!(index.ref_count_from_storage(&key), 1); + + index.replace(new_slot, old_slot, &key, account_info); + + let slot_list = index.get_and_then(&key, |entry| { + (false, entry.unwrap().slot_list_read_lock().clone_list()) + }); + assert_eq!(slot_list, SlotList::from([(new_slot, account_info)])); + // Moving an entry between slots must not change the ref count. + assert_eq!(index.ref_count_from_storage(&key), 1); + } + + #[test] + #[should_panic(expected = "Expected to find a slot to replace in the slot list")] + fn test_replace_missing_old_slot_panics() { + let key = solana_pubkey::new_rand(); + let index = AccountsIndex::::default_for_tests(); + let mut gc = ReclaimsSlotList::new(); + + index.upsert(5, 5, &key, 100, &mut gc, UpsertReclaim::IgnoreReclaims); + // No entry at slot 99 — replace must panic rather than silently appending. + index.replace(10, 99, &key, 200); + } + + #[test] + #[should_panic(expected = "Replace should only be used for uncached accounts")] + fn test_replace_cached_account_info_panics() { + // Shrink only ever rewrites uncached accounts; passing a cached AccountInfo to replace + // is a programming error and must trip the assertion. + let key = solana_pubkey::new_rand(); + let index = + AccountsIndex::::default_for_tests(); + let mut gc = ReclaimsSlotList::new(); + + index.upsert( + 5, + 5, + &key, + CacheableIndexValueTest(false), + &mut gc, + UpsertReclaim::IgnoreReclaims, + ); + index.replace(10, 5, &key, CacheableIndexValueTest(true)); + } + fn account_maps_stats_len(index: &AccountsIndex) -> usize { index.storage.storage.stats.total_count() } diff --git a/accounts-db/src/accounts_index/in_mem_accounts_index.rs b/accounts-db/src/accounts_index/in_mem_accounts_index.rs index 38ed952a23f..52d639ce39f 100644 --- a/accounts-db/src/accounts_index/in_mem_accounts_index.rs +++ b/accounts-db/src/accounts_index/in_mem_accounts_index.rs @@ -572,6 +572,46 @@ impl + Into> InMemAccountsIndex, old_slot: Slot) { + debug_assert!( + !new_item.1.is_cached(), + "Replace should only be used for uncached accounts" + ); + let mut should_write_through = false; + + self.get_or_create_index_entry_for_pubkey(pubkey, |entry| { + let mut slot_list = entry.slot_list_write_lock(); + let mut found_slot = false; + let slot_list_length = slot_list.retain_and_count(|cur_item| { + if cur_item.0 == old_slot { + assert!( + !found_slot, + "duplicate entry at slot {old_slot} in slot_list" + ); + found_slot = true; + *cur_item = new_item; + } + true + }); + assert!( + found_slot, + "Expected to find a slot to replace in the slot list" + ); + entry.mark_dirty(); + + should_write_through = + self.should_write_through && slot_list_length == 1 && entry.ref_count() == 1; + }); + if should_write_through { + let (slot, account_info) = new_item; + self.write_through(pubkey, slot, account_info); + } + } + /// Gets an entry for `pubkey` and calls `callback` with it. /// If the entry is not in the index, an empty entry will be created and passed to `callback`. /// If the entry is in the index, it will be passed to `callback` as is. From fad6f64bcce7802092f01d93e3ab288d0530faf5 Mon Sep 17 00:00:00 2001 From: Alex Pyattaev Date: Wed, 27 May 2026 23:09:22 +0300 Subject: [PATCH 086/576] Bound internal channels for tpu-banking stage path (#12734) bound internal channels for tpu-banking stage path --- core/src/banking_trace.rs | 112 ++++++++++++++++++++++++++++++++++---- core/src/fetch_stage.rs | 14 +++-- core/src/tpu.rs | 11 +++- 3 files changed, 118 insertions(+), 19 deletions(-) diff --git a/core/src/banking_trace.rs b/core/src/banking_trace.rs index 211e4231f50..6f6186b50fd 100644 --- a/core/src/banking_trace.rs +++ b/core/src/banking_trace.rs @@ -2,18 +2,23 @@ use { agave_banking_stage_ingress_types::{BankingPacketBatch, BankingPacketReceiver}, bincode::serialize_into, chrono::{DateTime, Local}, - crossbeam_channel::{Receiver, SendError, Sender, TryRecvError, unbounded}, + crossbeam_channel::{ + Receiver, SendError, Sender, TryRecvError, TrySendError, bounded, unbounded, + }, rolling_file::{RollingCondition, RollingConditionBasic, RollingFileAppender}, serde::{Deserialize, Serialize}, solana_clock::Slot, solana_hash::Hash, + solana_metrics::datapoint_info, + solana_streamer::{evicting_sender::EvictingSender, streamer::ChannelSend}, + solana_time_utils::timestamp, std::{ fs::{create_dir_all, remove_dir_all}, io::{self, Write}, path::PathBuf, sync::{ Arc, - atomic::{AtomicBool, Ordering}, + atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering}, }, thread::{self, JoinHandle, sleep}, time::{Duration, SystemTime}, @@ -21,6 +26,17 @@ use { thiserror::Error, }; +/// Capacity of the vote channel between sigverify and the banking-stage. +/// Sized to fit all votes from a reasonably sized cluster for 1 slot, + margin. +const VOTE_CHANNEL_CAPACITY: usize = 1024 * 8; + +/// Capacity of the non-vote (transaction) channel between sigverify and the banking-stage. +/// Larger than the vote channel to absorb bursty TPU load. +const NON_VOTE_CHANNEL_CAPACITY: usize = 1024 * 16; + +/// Period between metric emissions per channel from inside `TracedSender::send`. +const STATS_REPORT_INTERVAL: Duration = Duration::from_secs(5); + pub type BankingPacketSender = TracedSender; pub type TracerThreadResult = Result<(), TraceError>; pub type TracerThread = Option>; @@ -85,6 +101,17 @@ pub enum ChannelLabel { Dummy, } +impl ChannelLabel { + fn metric_name(&self) -> &'static str { + match self { + ChannelLabel::NonVote => "banking_trace_channel_non_vote", + ChannelLabel::TpuVote => "banking_trace_channel_tpu_vote", + ChannelLabel::GossipVote => "banking_trace_channel_gossip_vote", + ChannelLabel::Dummy => "banking_trace_channel_dummy", + } + } +} + struct RollingConditionGrouped { basic: RollingConditionBasic, tried_rollover_after_opened: bool, @@ -252,10 +279,14 @@ impl BankingTracer { pub fn create_channels(&self) -> Channels { let (non_vote_sender, non_vote_receiver) = self.create_channel_non_vote(); - let (tpu_vote_sender, tpu_vote_receiver) = - Self::channel(ChannelLabel::TpuVote, self.active_tracer.as_ref().cloned()); + let (tpu_vote_sender, tpu_vote_receiver) = Self::channel( + ChannelLabel::TpuVote, + VOTE_CHANNEL_CAPACITY, + self.active_tracer.as_ref().cloned(), + ); let (gossip_vote_sender, gossip_vote_receiver) = Self::channel( ChannelLabel::GossipVote, + VOTE_CHANNEL_CAPACITY, self.active_tracer.as_ref().cloned(), ); @@ -270,20 +301,26 @@ impl BankingTracer { } pub fn create_channel_non_vote(&self) -> (BankingPacketSender, BankingPacketReceiver) { - Self::channel(ChannelLabel::NonVote, self.active_tracer.as_ref().cloned()) + Self::channel( + ChannelLabel::NonVote, + NON_VOTE_CHANNEL_CAPACITY, + self.active_tracer.as_ref().cloned(), + ) } pub fn channel_for_test() -> (TracedSender, Receiver) { - Self::channel(ChannelLabel::Dummy, None) + Self::channel(ChannelLabel::Dummy, VOTE_CHANNEL_CAPACITY, None) } fn channel( label: ChannelLabel, + capacity: usize, active_tracer: Option, ) -> (TracedSender, Receiver) { - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(capacity); + let evicting = EvictingSender::new(sender, receiver.clone()); - (TracedSender::new(label, sender, active_tracer), receiver) + (TracedSender::new(label, evicting, active_tracer), receiver) } pub fn ensure_cleanup_path(path: &PathBuf) -> Result<(), io::Error> { @@ -357,19 +394,33 @@ impl BankingTracer { #[derive(Clone)] pub struct TracedSender { label: ChannelLabel, - sender: Sender, + sender: EvictingSender, + stats: Arc, active_tracer: Option, } +#[derive(Default)] +struct TracedSenderStats { + /// Total number of batches sent downstream. + sent: AtomicU64, + /// Max channel length observed since last report. + max_len: AtomicUsize, + /// Number of batches the EvictingSender dropped due to a full channel since last report. + eviction_drops: AtomicU64, + /// `solana_time_utils::timestamp` of the last report. + last_report_ms: AtomicU64, +} + impl TracedSender { fn new( label: ChannelLabel, - sender: Sender, + sender: EvictingSender, active_tracer: Option, ) -> Self { Self { label, sender, + stats: Arc::new(TracedSenderStats::default()), active_tracer, } } @@ -388,7 +439,23 @@ impl TracedSender { })?; } } - self.sender.send(batch) + let (result, sent) = match self.sender.try_send(batch) { + // New batch queued; nothing was evicted. + Ok(()) => (Ok(()), self.stats.sent.fetch_add(1, Ordering::Relaxed)), + // EvictingSender popped the oldest to make room. + Err(TrySendError::Full(_)) => { + self.stats.eviction_drops.fetch_add(1, Ordering::Relaxed); + (Ok(()), 0) + } + Err(TrySendError::Disconnected(b)) => (Err(SendError(b)), 0), + }; + let len = self.sender.len(); + self.stats.max_len.fetch_max(len, Ordering::Relaxed); + // if we've made reasonable progress, or dropped packets + if sent % 128 == 0 { + self.maybe_report_stats(); + } + result } pub fn len(&self) -> usize { @@ -398,6 +465,29 @@ impl TracedSender { pub fn is_empty(&self) -> bool { self.len() == 0 } + + fn maybe_report_stats(&self) { + let now_ms = timestamp(); + let last = self.stats.last_report_ms.load(Ordering::Relaxed); + if Duration::from_millis(now_ms.saturating_sub(last)) < STATS_REPORT_INTERVAL { + return; + } + if self + .stats + .last_report_ms + .compare_exchange(last, now_ms, Ordering::Relaxed, Ordering::Relaxed) + .is_err() + { + return; + } + let max_len = self.stats.max_len.swap(0, Ordering::Relaxed); + let eviction_drops = self.stats.eviction_drops.swap(0, Ordering::Relaxed); + datapoint_info!( + self.label.metric_name(), + ("max_len", max_len as i64, i64), + ("eviction_drops", eviction_drops as i64, i64), + ); + } } #[cfg(any(test, feature = "dev-context-only-utils"))] diff --git a/core/src/fetch_stage.rs b/core/src/fetch_stage.rs index 3772677c675..bb02cef8c34 100644 --- a/core/src/fetch_stage.rs +++ b/core/src/fetch_stage.rs @@ -7,12 +7,13 @@ use { solana_metrics::{inc_new_counter_debug, inc_new_counter_info}, solana_packet::PacketFlags, solana_perf::{ - packet::{PacketBatchRecycler, PacketRefMut}, + packet::{PacketBatch, PacketBatchRecycler, PacketRefMut}, recycler::Recycler, }, solana_poh::poh_recorder::PohRecorder, - solana_streamer::streamer::{ - self, PacketBatchReceiver, PacketBatchSender, StreamerReceiveStats, + solana_streamer::{ + evicting_sender::EvictingSender, + streamer::{self, PacketBatchReceiver, PacketBatchSender, StreamerReceiveStats}, }, std::{ net::UdpSocket, @@ -37,7 +38,8 @@ impl FetchStage { coalesce: Option, ) -> (Self, PacketBatchReceiver, PacketBatchReceiver) { let (sender, receiver) = unbounded(); - let (vote_sender, vote_receiver) = unbounded(); + let (vote_sender, vote_receiver) = + EvictingSender::new_bounded(crate::tpu::TPU_VOTE_CHANNEL_SIZE); let (_forward_sender, forward_receiver) = unbounded(); ( Self::new_with_sender( @@ -58,7 +60,7 @@ impl FetchStage { tpu_vote_sockets: Vec, exit: Arc, sender: &PacketBatchSender, - vote_sender: &PacketBatchSender, + vote_sender: &EvictingSender, forward_receiver: PacketBatchReceiver, poh_recorder: &Arc>, coalesce: Option, @@ -132,7 +134,7 @@ impl FetchStage { tpu_vote_sockets: Vec>, exit: Arc, sender: &PacketBatchSender, - vote_sender: &PacketBatchSender, + vote_sender: &EvictingSender, forward_receiver: PacketBatchReceiver, poh_recorder: &Arc>, coalesce: Option, diff --git a/core/src/tpu.rs b/core/src/tpu.rs index 143e8dadfa6..9d4c908fbd9 100644 --- a/core/src/tpu.rs +++ b/core/src/tpu.rs @@ -48,6 +48,7 @@ use { vote_sender_types::{ReplayVoteReceiver, ReplayVoteSender}, }, solana_streamer::{ + evicting_sender::EvictingSender, quic::{ SimpleQosQuicStreamerConfig, SpawnServerResult, SwQosQuicStreamerConfig, spawn_simple_qos_server, spawn_stake_weighted_qos_server, @@ -88,6 +89,10 @@ pub const MAX_VOTES_PER_SECOND: u64 = 20; /// be conservative max of obsersed on mnb during high-load events. const TPU_CHANNEL_SIZE: usize = 50_000; +/// Size of the channel between the vote streamer and the TPU sigverify stage. +/// Chosen based on nominal voting load for a cluster with ~2000 validators + some margin. +pub(crate) const TPU_VOTE_CHANNEL_SIZE: usize = 4_000; + pub struct Tpu { fetch_stage: FetchStage, cluster_info_vote_listener: ClusterInfoVoteListener, @@ -164,13 +169,15 @@ impl Tpu { } = sockets; let (packet_sender, packet_receiver) = bounded(TPU_CHANNEL_SIZE); - let (vote_packet_sender, vote_packet_receiver) = unbounded(); + let (vote_packet_sender, vote_packet_receiver) = bounded(TPU_VOTE_CHANNEL_SIZE); + let evicting_vote_sender = + EvictingSender::new(vote_packet_sender.clone(), vote_packet_receiver.clone()); let (forwarded_packet_sender, forwarded_packet_receiver) = unbounded(); let fetch_stage = FetchStage::new_with_sender( tpu_vote_sockets, exit.clone(), &packet_sender, - &vote_packet_sender, + &evicting_vote_sender, forwarded_packet_receiver, poh_recorder, None, // coalesce From 8ddb028a6f6d4bb812f2de0a97db9e0e560f97e9 Mon Sep 17 00:00:00 2001 From: Greg Cusack Date: Wed, 27 May 2026 13:26:55 -0700 Subject: [PATCH 087/576] add agave-unstable-api (#12770) --- Cargo.toml | 2 +- validator/Cargo.toml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 52bba61421c..fbec0b676e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -497,7 +497,7 @@ solana-system-program = { path = "programs/system", version = "=4.2.0-alpha.0", solana-system-transaction = "4.0.0" solana-sysvar = "4.0.0" solana-sysvar-id = "3.1.0" -solana-test-validator = { path = "test-validator", version = "=4.2.0-alpha.0" } +solana-test-validator = { path = "test-validator", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-time-utils = "3.0.0" solana-tls-utils = { path = "tls-utils", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-tpu-client = { path = "tpu-client", version = "=4.2.0-alpha.0", default-features = false, features = ["agave-unstable-api"] } diff --git a/validator/Cargo.toml b/validator/Cargo.toml index 99297bb342c..853c5f9eed2 100644 --- a/validator/Cargo.toml +++ b/validator/Cargo.toml @@ -112,7 +112,6 @@ solana-core = { path = "../core", features = ["agave-unstable-api", "dev-context solana-program-option = { workspace = true } solana-program-pack = { workspace = true } solana-runtime = { path = "../runtime", features = ["agave-unstable-api", "dev-context-only-utils"] } -solana-test-validator = { workspace = true, features = ["agave-unstable-api"] } solana-time-utils = { workspace = true } tempfile = { workspace = true } test-case = { workspace = true } From 958e7035b9426b59e12e3cc752ca6b66fd0e9acb Mon Sep 17 00:00:00 2001 From: steviez Date: Wed, 27 May 2026 16:48:27 -0500 Subject: [PATCH 088/576] test-validator: Update outdated --dynamic-port-range help (#12728) --- validator/src/cli.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/validator/src/cli.rs b/validator/src/cli.rs index e03db4cab24..c5451970f0d 100644 --- a/validator/src/cli.rs +++ b/validator/src/cli.rs @@ -655,8 +655,9 @@ pub fn test_app<'a>(version: &'a str, default_args: &'a DefaultTestArgs) -> App< .long("dynamic-port-range") .value_name("MIN_PORT-MAX_PORT") .takes_value(true) + .default_value(&default_args.dynamic_port_range) .validator(port_range_validator) - .help("Range to use for dynamically assigned ports [default: 1024-65535]"), + .help("Range to use for dynamically assigned ports"), ) .arg( Arg::with_name("bind_address") @@ -878,6 +879,7 @@ pub fn test_app<'a>(version: &'a str, default_args: &'a DefaultTestArgs) -> App< pub struct DefaultTestArgs { pub rpc_port: String, pub faucet_port: String, + pub dynamic_port_range: String, pub limit_ledger_size: String, pub faucet_sol: String, pub faucet_time_slice_secs: String, @@ -888,6 +890,7 @@ impl DefaultTestArgs { DefaultTestArgs { rpc_port: 8899.to_string(), faucet_port: FAUCET_PORT.to_string(), + dynamic_port_range: format!("{}-{}", VALIDATOR_PORT_RANGE.0, VALIDATOR_PORT_RANGE.1), /* 10,000 was derived empirically by watching the size * of the rocksdb/ directory self-limit itself to the * 40MB-150MB range when running `solana-test-validator` From 201ccb236c8472a4df70755dd73bbaaf871f05e7 Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Wed, 27 May 2026 17:54:13 -0400 Subject: [PATCH 089/576] gossip: do not propagate duplicate proofs for alpenglow blocks (#12761) --- core/src/window_service.rs | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/core/src/window_service.rs b/core/src/window_service.rs index 63bfba32004..9ec0ad2c90d 100644 --- a/core/src/window_service.rs +++ b/core/src/window_service.rs @@ -114,7 +114,10 @@ fn run_check_duplicate( duplicate_slots_sender: &DuplicateSlotSender, bank_forks: &RwLock, ) -> Result<()> { - let mut root_bank = bank_forks.read().unwrap().root_bank(); + let (mut root_bank, migration_status) = { + let bank_forks_r = bank_forks.read().unwrap(); + (bank_forks_r.root_bank(), bank_forks_r.migration_status()) + }; let mut last_updated = Instant::now(); let check_duplicate = |shred: PossibleDuplicateShred| -> Result<()> { if last_updated.elapsed().as_nanos() > root_bank.ns_per_slot { @@ -159,10 +162,19 @@ fn run_check_duplicate( } }; - // Propagate duplicate proof through gossip - cluster_info.push_duplicate_shred(&shred1, &shred2)?; - // Notify duplicate consensus state machine - duplicate_slots_sender.send(shred_slot)?; + if migration_status.should_respond_to_ancestor_hashes_requests(shred_slot) { + // In alpenglow we store the duplicate block proofs in blockstore for the purposes of slashing, + // however we do not need to propagate the duplicate proof through gossip. + // We still propagate during the mixed migration epoch, to account for other nodes that are stuck + // and require a duplicate proof to proceed + cluster_info.push_duplicate_shred(&shred1, &shred2)?; + } + + if !migration_status.is_alpenglow_enabled() { + // The state machine can be exited as soon as alpenglow is enabled. + // Notify duplicate consensus state machine + duplicate_slots_sender.send(shred_slot)?; + } Ok(()) }; From 97a822b87e65badac186416c775356da57aad876 Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Wed, 27 May 2026 19:08:44 -0400 Subject: [PATCH 090/576] votor: panic if bank hash mismatch on a finalized block (#12776) --- core/src/block_creation_loop.rs | 2 +- runtime/src/block_component_processor.rs | 11 +++-- votor/src/event_handler.rs | 62 ++++++++++++++++++++++-- 3 files changed, 67 insertions(+), 8 deletions(-) diff --git a/core/src/block_creation_loop.rs b/core/src/block_creation_loop.rs index 1e6bb588ca4..bd091cc9f4e 100644 --- a/core/src/block_creation_loop.rs +++ b/core/src/block_creation_loop.rs @@ -766,7 +766,7 @@ fn record_and_complete_block( &bank, i64::try_from(footer.block_producer_time_nanos) .expect("locally produced block timestamp must fit in i64"), - Hash::default(), // Banks we produce do not need the bank hash mismatch check + None, // Banks we produce do not need the bank hash mismatch check reward_cert, final_cert_input, )?; diff --git a/runtime/src/block_component_processor.rs b/runtime/src/block_component_processor.rs index fb565e38570..15f01bdf755 100644 --- a/runtime/src/block_component_processor.rs +++ b/runtime/src/block_component_processor.rs @@ -351,7 +351,7 @@ impl BlockComponentProcessor { Self::update_bank_with_footer_fields( &bank, block_producer_time_nanos, - bank_hash, + Some(bank_hash), reward_cert, footer_input .as_ref() @@ -503,14 +503,17 @@ impl BlockComponentProcessor { pub fn update_bank_with_footer_fields( bank: &Bank, block_producer_time_nanos: i64, - bank_hash: Hash, + bank_hash: Option, reward_cert: Option, final_cert_input: Option<(&HashSet, Slot)>, ) -> Result<(), BankFooterError> { bank.update_clock_from_footer(block_producer_time_nanos); calc_vote_rewards_update_vote_states(bank, reward_cert, final_cert_input)?; - // Record expected bank hash from footer for later verification when the bank is frozen. - bank.set_expected_bank_hash(bank_hash); + + if let Some(hash) = bank_hash { + // Record expected bank hash from footer for later verification when the bank is frozen. + bank.set_expected_bank_hash(hash); + } Ok(()) } } diff --git a/votor/src/event_handler.rs b/votor/src/event_handler.rs index 1b8254dc662..c86d434f465 100644 --- a/votor/src/event_handler.rs +++ b/votor/src/event_handler.rs @@ -297,7 +297,7 @@ impl EventHandler { .or_default() .push((block, parent_block)); } - Self::check_rootable_blocks( + Self::check_rootable_blocks_and_bank_hash_mismatches( my_pubkey, ctx, vctx, @@ -433,7 +433,7 @@ impl EventHandler { finalized_blocks.insert(block); request_repair(&ctx.repair_event_sender, *my_pubkey, block)?; - Self::check_rootable_blocks( + Self::check_rootable_blocks_and_bank_hash_mismatches( my_pubkey, ctx, vctx, @@ -809,7 +809,10 @@ impl EventHandler { /// - Block has a finalization certificate /// /// If so set root on the highest block that fits these conditions - fn check_rootable_blocks( + /// + /// Additionally check if any of the finalized blocks is frozen with a bank hash mismatch. + /// If so panic as it is unrecoverable. + fn check_rootable_blocks_and_bank_hash_mismatches( my_pubkey: &Pubkey, ctx: &SharedContext, vctx: &mut VotingContext, @@ -825,6 +828,24 @@ impl EventHandler { .iter() .filter_map(|&(slot, block_id)| { let bank = bank_forks_r.get(slot)?; + + // Check for bank hash mismatch + if bank.is_frozen() + && bank.block_id() == Some(block_id) + && let Some(expected_hash) = bank.expected_bank_hash() + && expected_hash != bank.hash() + { + panic!( + "{my_pubkey}: Slot {slot} block_id {block_id} has been finalized, however \ + we have a bank hash mismatch. The cluster bank hash is {expected_hash} \ + however we computed {}. At this point we will be unable to recover. \ + Please save a copy of your ledger to share on discord and restart from a \ + snapshot > {slot}.", + bank.hash(), + ); + } + + // Check if this block is rootable (slot > old_root && vctx.vote_history.voted(slot) && bank.is_frozen() @@ -1693,6 +1714,41 @@ mod tests { assert_eq!(test_context.bank_forks.read().unwrap().root(), 1); } + #[test] + #[should_panic(expected = "we have a bank hash mismatch")] + fn test_finalized_block_with_bank_hash_mismatch_panics() { + let mut test_context = setup(); + let root_bank = test_context + .bank_forks + .read() + .unwrap() + .sharable_banks() + .root(); + let bank = test_context.create_block_only(1, root_bank); + let block_id = bank.block_id().unwrap(); + let expected_hash = Hash::new_unique(); + bank.set_expected_bank_hash(expected_hash); + + test_context.send_finalized_event((1, block_id), true); + } + + #[test] + fn test_finalized_different_block_id_with_bank_hash_mismatch_does_not_panic() { + let mut test_context = setup(); + let root_bank = test_context + .bank_forks + .read() + .unwrap() + .sharable_banks() + .root(); + let bank = test_context.create_block_only(1, root_bank); + let expected_hash = Hash::new_unique(); + bank.set_expected_bank_hash(expected_hash); + let finalized_block_id = Hash::new_unique(); + + test_context.send_finalized_event((1, finalized_block_id), true); + } + #[test] fn test_parent_ready_in_middle_of_window() { let mut test_context = setup(); From 141187a9f40f7addbe2b59739b3811abc0baf34c Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Wed, 27 May 2026 19:10:06 -0400 Subject: [PATCH 091/576] replay: handle set-identity when ag is active (#12757) --- core/src/replay_stage.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core/src/replay_stage.rs b/core/src/replay_stage.rs index 9d645849aaa..05b6c5d3d0e 100644 --- a/core/src/replay_stage.rs +++ b/core/src/replay_stage.rs @@ -1025,6 +1025,15 @@ impl ReplayStage { } if migration_status.is_alpenglow_enabled() { + if my_pubkey != cluster_info.id() { + identity_keypair = cluster_info.keypair(); + let my_old_pubkey = my_pubkey; + my_pubkey = identity_keypair.pubkey(); + migration_status.set_pubkey(my_pubkey); + + warn!("Identity changed from {my_old_pubkey} to {my_pubkey}"); + } + process_soft_dead_slots( &my_pubkey, &blockstore, From 52e9e4b4ae5f12d3aad74f797131f8d21406b527 Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Wed, 27 May 2026 21:09:38 -0400 Subject: [PATCH 092/576] blockstore_processor: fix BlockFooter race condition (#12780) * blockstore_processor: fix BlockFooter race condition * pr feedback: fix leader side --- core/src/block_creation_loop.rs | 6 +++++- entry/src/block_component.rs | 14 ++++++++++++++ ledger/src/blockstore_processor.rs | 18 +++++++++++------- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/core/src/block_creation_loop.rs b/core/src/block_creation_loop.rs index bd091cc9f4e..cfe6947d9ca 100644 --- a/core/src/block_creation_loop.rs +++ b/core/src/block_creation_loop.rs @@ -733,7 +733,7 @@ fn record_and_complete_block( "optimistic_parent should be None after receiving ParentReady" ); - // Alpentick and clear bank + // Alpentick, produce the footer, and clear bank let mut w_poh_recorder = ctx.poh_recorder.write().unwrap(); let bank = w_poh_recorder .bank() @@ -762,6 +762,10 @@ fn record_and_complete_block( let footer = produce_block_footer(&bank, skip, notar, guard.as_ref()); let final_cert_input = guard.as_ref().map(|c| c.vote_rewards_input()); + // BankingStage may still be executing batches that were already recorded. + // Footer processing mutates vote accounts directly, so wait for execution to complete first. + bank.wait_for_inflight_commits(); + BlockComponentProcessor::update_bank_with_footer_fields( &bank, i64::try_from(footer.block_producer_time_nanos) diff --git a/entry/src/block_component.rs b/entry/src/block_component.rs index 1bfcadf5472..d0c4b018b9b 100644 --- a/entry/src/block_component.rs +++ b/entry/src/block_component.rs @@ -446,6 +446,20 @@ impl VersionedBlockMarker { let g = BlockMarkerV1::GenesisCertificate(LengthPrefixed::new(g)); VersionedBlockMarker::V1(g) } + + pub fn is_update_parent(&self) -> bool { + match self { + VersionedBlockMarker::V1(BlockMarkerV1::UpdateParent(_)) => true, + VersionedBlockMarker::V1(_) => false, + } + } + + pub fn is_footer(&self) -> bool { + match self { + VersionedBlockMarker::V1(BlockMarkerV1::BlockFooter(_)) => true, + VersionedBlockMarker::V1(_) => false, + } + } } #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/ledger/src/blockstore_processor.rs b/ledger/src/blockstore_processor.rs index f8a6cb6e79a..d9db5d8dd5a 100644 --- a/ledger/src/blockstore_processor.rs +++ b/ledger/src/blockstore_processor.rs @@ -22,7 +22,7 @@ use { solana_clock::Slot, solana_cost_model::{cost_model::CostModel, transaction_cost::TransactionCost}, solana_entry::{ - block_component::{BlockComponent, VersionedBlockMarker}, + block_component::BlockComponent, entry::{self, Entry, EntrySlice, EntryType, create_ticks}, }, solana_genesis_config::GenesisConfig, @@ -1861,13 +1861,17 @@ fn confirm_slot_with_components( )?; } BlockComponent::BlockMarker(marker) => { + if marker.is_footer() { + // The footer path mutates vote accounts directly to pay rewards. + // All prior transactions must finish first so vote account view is deterministic. + if let Some((result, execute_time)) = bank.wait_for_completed_scheduler() { + timing.batch_execute.totals.accumulate(&execute_time); + result?; + } + } if let Some(parent_bank) = bank.parent() { - let allow_initial_update_parent = replay_starts_at_update_parent - && matches!( - &marker, - VersionedBlockMarker::V1(marker) - if marker.as_update_parent().is_some() - ); + let allow_initial_update_parent = + replay_starts_at_update_parent && marker.is_update_parent(); processor .on_marker( bank.clone_without_scheduler(), From b6258130c13ca0010510f26f1784a26df6121bad Mon Sep 17 00:00:00 2001 From: steviez Date: Wed, 27 May 2026 22:05:11 -0500 Subject: [PATCH 093/576] chore: Cleanup Cargo.toml dependencies (#12773) Several workspace members state dependencies and relist agave-unstable-api as a necessary feature. This feature is presumed to be required and is listed in the workspace Cargo.toml file. Thus, workspace members can simply use workspace = true --- faucet-cli/Cargo.toml | 2 +- gossip-cli/Cargo.toml | 4 ++-- scheduling-utils/Cargo.toml | 2 +- validator/Cargo.toml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/faucet-cli/Cargo.toml b/faucet-cli/Cargo.toml index 19da65a7691..7b52feea980 100644 --- a/faucet-cli/Cargo.toml +++ b/faucet-cli/Cargo.toml @@ -22,7 +22,7 @@ clap = { workspace = true } log = { workspace = true } solana-clap-utils = { workspace = true } solana-cli-config = { workspace = true } -solana-faucet = { workspace = true, features = ["agave-unstable-api"] } +solana-faucet = { workspace = true } solana-keypair = { workspace = true } solana-metrics = { workspace = true } solana-version = { workspace = true } diff --git a/gossip-cli/Cargo.toml b/gossip-cli/Cargo.toml index d3fe2fd495b..b92fd8fb31e 100644 --- a/gossip-cli/Cargo.toml +++ b/gossip-cli/Cargo.toml @@ -22,8 +22,8 @@ agave-logger = { workspace = true } clap = { workspace = true } log = { workspace = true } solana-clap-utils = { workspace = true } -solana-gossip = { workspace = true, features = ["agave-unstable-api"] } -solana-net-utils = { workspace = true, features = ["agave-unstable-api"] } +solana-gossip = { workspace = true } +solana-net-utils = { workspace = true } solana-pubkey = { version = "=4.2.0", features = ["rand"] } solana-version = { workspace = true } diff --git a/scheduling-utils/Cargo.toml b/scheduling-utils/Cargo.toml index 365a921b4e3..5594d6fdbfd 100644 --- a/scheduling-utils/Cargo.toml +++ b/scheduling-utils/Cargo.toml @@ -14,7 +14,7 @@ agave-unstable-api = [] dev-context-only-utils = ["dep:wincode", "dep:solana-transaction"] [dependencies] -agave-feature-set = { workspace = true, features = ["agave-unstable-api"] } +agave-feature-set = { workspace = true } agave-scheduler-bindings = { workspace = true } agave-transaction-view = { workspace = true } ahash = { workspace = true } diff --git a/validator/Cargo.toml b/validator/Cargo.toml index 853c5f9eed2..f843577fa3d 100644 --- a/validator/Cargo.toml +++ b/validator/Cargo.toml @@ -57,7 +57,7 @@ solana-epoch-schedule = { workspace = true } solana-faucet = { workspace = true } solana-genesis-utils = { workspace = true } solana-geyser-plugin-manager = { workspace = true } -solana-gossip = { workspace = true, features = ["agave-unstable-api"] } +solana-gossip = { workspace = true } solana-hash = { workspace = true } solana-inflation = { workspace = true } solana-keypair = { workspace = true } From bdd84ff796e3d3068977c74d6c8348a6c6d84e75 Mon Sep 17 00:00:00 2001 From: Gabe Rodriguez Date: Wed, 27 May 2026 21:22:23 -0700 Subject: [PATCH 094/576] Bump stake-interface to v4.2 (#12684) --- Cargo.lock | 8 ++-- Cargo.toml | 2 +- account-decoder/src/parse_stake.rs | 2 +- account-decoder/src/parse_sysvar.rs | 8 +++- cli/src/cluster_query.rs | 9 ++-- cli/src/stake.rs | 10 +++-- dev-bins/Cargo.lock | 24 +++++------ dev-bins/Cargo.toml | 2 +- program-runtime/src/sysvar_cache.rs | 25 ++++++----- program-test/tests/warp.rs | 7 ++- programs/sbf/Cargo.lock | 14 +++--- programs/sbf/Cargo.toml | 6 +-- programs/sbf/rust/sysvar/src/lib.rs | 9 +++- programs/sbf/tests/sysvar.rs | 30 +++++++++++-- runtime/src/bank.rs | 22 ++++++++-- .../partitioned_epoch_rewards/distribution.rs | 7 +-- runtime/src/bank/serde_snapshot.rs | 2 +- runtime/src/bank/tests.rs | 12 ++++-- runtime/src/inflation_rewards/mod.rs | 11 ++--- runtime/src/inflation_rewards/points.rs | 1 + runtime/src/stakes.rs | 43 ++++++++++++++----- syscalls/src/lib.rs | 20 ++++++--- 22 files changed, 188 insertions(+), 86 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 130eeb7c238..8c8ab24f582 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6133,7 +6133,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.52.0", + "windows-sys 0.61.0", ] [[package]] @@ -10446,9 +10446,9 @@ dependencies = [ [[package]] name = "solana-stake-interface" -version = "3.1.0" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f49eb5c77484214c3484921e2cdda79d185373118b5458c1b2df0f1a04c3bc30" +checksum = "19f34562cda7df74d85fcb1f7063d8cdec3e14f3d402aa3062605c116d8b8115" dependencies = [ "borsh", "num-traits", @@ -12064,7 +12064,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix 1.1.4", - "windows-sys 0.52.0", + "windows-sys 0.61.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index fbec0b676e7..8d8c1c9cb0b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -479,7 +479,7 @@ solana-signer-store = "0.1.0" solana-slot-hashes = "3.0.2" solana-slot-history = "3.0.1" solana-stable-layout = "3.0.1" -solana-stake-interface = "3.1.0" +solana-stake-interface = "4.2.0" solana-storage-bigtable = { path = "storage-bigtable", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-storage-proto = { path = "storage-proto", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-streamer = { path = "streamer", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } diff --git a/account-decoder/src/parse_stake.rs b/account-decoder/src/parse_stake.rs index 433e5079a8f..32e603f9c97 100644 --- a/account-decoder/src/parse_stake.rs +++ b/account-decoder/src/parse_stake.rs @@ -194,7 +194,7 @@ mod test { stake: 20, activation_epoch: 2, deactivation_epoch: u64::MAX, - warmup_cooldown_rate: 0.25, + _reserved: [0; 8], }, credits_observed: 10, }; diff --git a/account-decoder/src/parse_sysvar.rs b/account-decoder/src/parse_sysvar.rs index af48808f51b..36219ec2594 100644 --- a/account-decoder/src/parse_sysvar.rs +++ b/account-decoder/src/parse_sysvar.rs @@ -266,8 +266,11 @@ mod test { #[allow(deprecated)] use solana_sysvar::recent_blockhashes::IterItem; use { - super::*, solana_account::create_account_for_test, solana_fee_calculator::FeeCalculator, + super::*, + solana_account::{Account, create_account_for_test}, + solana_fee_calculator::FeeCalculator, solana_hash::Hash, + solana_stake_interface::stake_history::SIZE, }; #[test] @@ -362,7 +365,8 @@ mod test { deactivating: 3, }; stake_history.add(1, stake_history_entry.clone()); - let stake_history_sysvar = create_account_for_test(&stake_history); + let stake_history_sysvar = + Account::new_data_with_space(1, &stake_history, SIZE, &sysvar::id()).unwrap(); assert_eq!( parse_sysvar(&stake_history_sysvar.data, &sysvar::stake_history::id()).unwrap(), SysvarAccountType::StakeHistory(vec![UiStakeHistoryEntry { diff --git a/cli/src/cluster_query.rs b/cli/src/cluster_query.rs index a19b9578954..c70fb8baaaa 100644 --- a/cli/src/cluster_query.rs +++ b/cli/src/cluster_query.rs @@ -41,7 +41,7 @@ use { solana_sdk_ids::sysvar::{self, stake_history}, solana_signature::Signature, solana_slot_history::{self as slot_history, SlotHistory}, - solana_stake_interface::{self as stake, state::StakeStateV2}, + solana_stake_interface::{self as stake, stake_history::StakeHistory, state::StakeStateV2}, solana_system_interface::MAX_PERMITTED_DATA_LENGTH, solana_transaction_status::{ EncodableWithMeta, EncodedConfirmedTransactionWithStatusMeta, UiTransactionEncoding, @@ -1654,9 +1654,10 @@ pub async fn process_show_stakes( let clock: Clock = from_account(&clock_account).ok_or_else(|| { CliError::RpcRequestError("Failed to deserialize clock sysvar".to_string()) })?; - let stake_history = from_account(&stake_history_account).ok_or_else(|| { - CliError::RpcRequestError("Failed to deserialize stake history".to_string()) - })?; + let stake_history: StakeHistory = + bincode::deserialize(&stake_history_account.data).map_err(|_| { + CliError::RpcRequestError("Failed to deserialize stake history".to_string()) + })?; let new_rate_activation_epoch = get_feature_activation_epoch( rpc_client, &agave_feature_set::reduce_stake_warmup_cooldown::id(), diff --git a/cli/src/stake.rs b/cli/src/stake.rs index 7c706be3328..1de40fe80ba 100644 --- a/cli/src/stake.rs +++ b/cli/src/stake.rs @@ -2456,6 +2456,7 @@ pub fn build_stake_state( _, ) => { let current_epoch = clock.epoch; + #[allow(deprecated)] let StakeActivationStatus { effective, activating, @@ -2719,9 +2720,10 @@ pub async fn get_account_stake_state( match stake_account.state() { Ok(stake_state) => { let stake_history_account = rpc_client.get_account(&stake_history::id()).await?; - let stake_history = from_account(&stake_history_account).ok_or_else(|| { - CliError::RpcRequestError("Failed to deserialize stake history".to_string()) - })?; + let stake_history: StakeHistory = bincode::deserialize(&stake_history_account.data) + .map_err(|_| { + CliError::RpcRequestError("Failed to deserialize stake history".to_string()) + })?; let clock_account = rpc_client.get_account(&clock::id()).await?; let clock: Clock = from_account(&clock_account).ok_or_else(|| { CliError::RpcRequestError("Failed to deserialize clock sysvar".to_string()) @@ -2779,7 +2781,7 @@ pub async fn process_show_stake_history( ) -> ProcessResult { let stake_history_account = rpc_client.get_account(&stake_history::id()).await?; let stake_history = - from_account::(&stake_history_account).ok_or_else(|| { + bincode::deserialize::(&stake_history_account.data).map_err(|_| { CliError::RpcRequestError("Failed to deserialize stake history".to_string()) })?; diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index de7fc166a88..278d012a5c1 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -531,7 +531,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -542,7 +542,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2398,7 +2398,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -5348,7 +5348,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -5406,7 +5406,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -5876,7 +5876,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -6546,9 +6546,9 @@ dependencies = [ [[package]] name = "solana-clock" -version = "3.0.1" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95cf11109c3b6115cc510f1e31f06fdd52f504271bc24ef5f1249fbbcae5f9f3" +checksum = "5ea35d8f69b67daddb921a9da7f78ca591b533cf5e98833cd9ae62fdc2e4652c" dependencies = [ "serde", "serde_derive", @@ -8767,9 +8767,9 @@ dependencies = [ [[package]] name = "solana-stake-interface" -version = "3.1.0" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f49eb5c77484214c3484921e2cdda79d185373118b5458c1b2df0f1a04c3bc30" +checksum = "19f34562cda7df74d85fcb1f7063d8cdec3e14f3d402aa3062605c116d8b8115" dependencies = [ "borsh", "num-traits", @@ -10160,7 +10160,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -11137,7 +11137,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/dev-bins/Cargo.toml b/dev-bins/Cargo.toml index 01e8586918b..dddce3f1cc1 100644 --- a/dev-bins/Cargo.toml +++ b/dev-bins/Cargo.toml @@ -118,7 +118,7 @@ solana-sdk-ids = "3.1.0" solana-shred-version = "3.0.1" solana-signature = { version = "3.4.0", default-features = false } solana-signer = "3.0.0" -solana-stake-interface = "3.0.0" +solana-stake-interface = "4.2.0" solana-storage-bigtable = { path = "../storage-bigtable", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-streamer = { path = "../streamer", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-svm = { path = "../svm", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } diff --git a/program-runtime/src/sysvar_cache.rs b/program-runtime/src/sysvar_cache.rs index 8d018cdce81..a7a82da782d 100644 --- a/program-runtime/src/sysvar_cache.rs +++ b/program-runtime/src/sysvar_cache.rs @@ -366,7 +366,10 @@ pub mod get_sysvar_with_account_check { #[cfg(test)] mod tests { - use {super::*, test_case::test_case}; + use { + super::*, solana_stake_interface::stake_history::SIZE as STAKE_HISTORY_ACCOUNT_SIZE, + test_case::test_case, + }; // sysvar cache provides the full account data of a sysvar // the setters MUST NOT be changed to serialize an object representation @@ -375,17 +378,17 @@ mod tests { // * account data is larger than struct sysvar // * vector sysvar has fewer than its maximum entries // if at any point the data is roundtripped through bincode, the vector will shrink - #[test_case(Clock::default(); "clock")] - #[test_case(EpochSchedule::default(); "epoch_schedule")] - #[test_case(EpochRewards::default(); "epoch_rewards")] - #[test_case(Rent::default(); "rent")] - #[test_case(SlotHashes::default(); "slot_hashes")] - #[test_case(StakeHistory::default(); "stake_history")] - #[test_case(LastRestartSlot::default(); "last_restart_slot")] - fn test_sysvar_cache_preserves_bytes(_: T) { + #[test_case(Clock::default(), 40; "clock")] + #[test_case(EpochSchedule::default(), 33; "epoch_schedule")] + #[test_case(EpochRewards::default(), 81; "epoch_rewards")] + #[test_case(Rent::default(), 17; "rent")] + #[test_case(SlotHashes::default(), 20_488; "slot_hashes")] + #[test_case(StakeHistory::default(), STAKE_HISTORY_ACCOUNT_SIZE; "stake_history")] + #[test_case(LastRestartSlot::default(), 8; "last_restart_slot")] + fn test_sysvar_cache_preserves_bytes(_: T, account_size: usize) { let id = T::id(); - let size = T::size_of().saturating_mul(2); - let in_buf = vec![0; size]; + let account_size = account_size.saturating_mul(2); + let in_buf = vec![0; account_size]; let mut sysvar_cache = SysvarCache::default(); sysvar_cache.fill_missing_entries(|pubkey, callback| { diff --git a/program-test/tests/warp.rs b/program-test/tests/warp.rs index bcf32646a62..25c5c137241 100644 --- a/program-test/tests/warp.rs +++ b/program-test/tests/warp.rs @@ -194,10 +194,13 @@ async fn stake_rewards_from_warp() { let stake_history: StakeHistory = deserialize(&stake_history_account.data).unwrap(); let clock: Clock = deserialize(&clock_account.data).unwrap(); let stake = stake_state.stake().unwrap(); - assert_eq!( + #[allow(deprecated)] + let stake_activation_status = stake .delegation - .stake_activating_and_deactivating(clock.epoch, &stake_history, None), + .stake_activating_and_deactivating(clock.epoch, &stake_history, None); + assert_eq!( + stake_activation_status, StakeActivationStatus::with_effective(stake.delegation.stake), ); } diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index 92787fa0204..df1abc3da06 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -5269,7 +5269,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.52.0", + "windows-sys 0.61.0", ] [[package]] @@ -5764,7 +5764,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] @@ -6393,9 +6393,9 @@ dependencies = [ [[package]] name = "solana-clock" -version = "3.0.1" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95cf11109c3b6115cc510f1e31f06fdd52f504271bc24ef5f1249fbbcae5f9f3" +checksum = "5ea35d8f69b67daddb921a9da7f78ca591b533cf5e98833cd9ae62fdc2e4652c" dependencies = [ "serde", "serde_derive", @@ -9289,9 +9289,9 @@ dependencies = [ [[package]] name = "solana-stake-interface" -version = "3.1.0" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f49eb5c77484214c3484921e2cdda79d185373118b5458c1b2df0f1a04c3bc30" +checksum = "19f34562cda7df74d85fcb1f7063d8cdec3e14f3d402aa3062605c116d8b8115" dependencies = [ "num-traits", "serde", @@ -10641,7 +10641,7 @@ dependencies = [ "getrandom 0.3.1", "once_cell", "rustix 1.1.4", - "windows-sys 0.52.0", + "windows-sys 0.61.0", ] [[package]] diff --git a/programs/sbf/Cargo.toml b/programs/sbf/Cargo.toml index b6d79c2bc0d..6804bb42220 100644 --- a/programs/sbf/Cargo.toml +++ b/programs/sbf/Cargo.toml @@ -108,7 +108,7 @@ solana-account-view = "=2.0.0" solana-address = "=2.6.0" solana-blake3-hasher = { version = "=3.1.0", features = ["blake3"] } solana-bn254 = "=3.2.1" -solana-clock = { version = "=3.0.1", features = ["serde", "sysvar"] } +solana-clock = { version = "=3.1.0", features = ["serde", "sysvar"] } solana-compute-budget = { path = "../../compute-budget", version = "=4.2.0-alpha.0" } solana-compute-budget-instruction = { path = "../../compute-budget-instruction", version = "=4.2.0-alpha.0" } solana-cpi = "=3.1.0" @@ -140,7 +140,7 @@ solana-sbf-rust-realloc-invoke-dep = { path = "rust/realloc_invoke_dep", version solana-sdk-ids = "=3.1.0" solana-secp256k1-recover = "=3.1.1" solana-sha256-hasher = { version = "=3.1.0", features = ["sha2"] } -solana-stake-interface = { version = "=3.1.0", features = ["bincode"] } +solana-stake-interface = { version = "=4.2.0", features = ["bincode"] } solana-svm = { path = "../../svm", version = "=4.2.0-alpha.0" } solana-svm-feature-set = { path = "../../svm-feature-set", version = "=4.2.0-alpha.0" } solana-svm-timings = { path = "../../svm-timings", version = "=4.2.0-alpha.0" } @@ -201,7 +201,7 @@ solana-sbf-rust-realloc-dep = { workspace = true } solana-sbf-rust-realloc-invoke-dep = { workspace = true } solana-sdk-ids = "3.1.0" solana-signer = "3.0.0" -solana-stake-interface = "3.0.0" +solana-stake-interface = "4.2.0" solana-svm = { workspace = true, features = ["dev-context-only-utils"] } solana-svm-feature-set = { workspace = true } solana-svm-timings = { workspace = true } diff --git a/programs/sbf/rust/sysvar/src/lib.rs b/programs/sbf/rust/sysvar/src/lib.rs index 9d5101995bb..8b5d208d898 100644 --- a/programs/sbf/rust/sysvar/src/lib.rs +++ b/programs/sbf/rust/sysvar/src/lib.rs @@ -185,7 +185,14 @@ pub fn process_instruction( { msg!("StakeHistory identifier:"); sysvar::stake_history::id().log(); - let _ = StakeHistory::from_account_info(&accounts[9]).unwrap(); + let _: StakeHistory = + if sysvar::stake_history::check_id(accounts[9].unsigned_key()) { + bincode::deserialize(&accounts[9].data.borrow()) + .map_err(|_| ProgramError::InvalidArgument) + } else { + Err(ProgramError::InvalidArgument) + } + .unwrap(); // Syscall `sol_get_sysvar`. let stake_history_sysvar = StakeHistorySysvar(1); assert!(stake_history_sysvar.get_entry(0).is_some()); diff --git a/programs/sbf/tests/sysvar.rs b/programs/sbf/tests/sysvar.rs index 2750d33a32a..eae08d55132 100644 --- a/programs/sbf/tests/sysvar.rs +++ b/programs/sbf/tests/sysvar.rs @@ -2,6 +2,7 @@ use { agave_feature_set::disable_fees_sysvar, + solana_account::{AccountSharedData, ReadableAccount, WritableAccount}, solana_instruction::{AccountMeta, Instruction}, solana_message::Message, solana_pubkey::Pubkey, @@ -15,16 +16,39 @@ use { solana_sdk_ids::{ bpf_loader_upgradeable, sysvar::{ - clock, epoch_schedule, instructions, recent_blockhashes, rent, slot_hashes, + self, clock, epoch_schedule, instructions, recent_blockhashes, rent, slot_hashes, slot_history, stake_history, }, }, solana_signer::Signer, - solana_stake_interface::stake_history::{StakeHistory, StakeHistoryEntry}, + solana_stake_interface::stake_history::{ + SIZE as STAKE_HISTORY_ACCOUNT_SIZE, StakeHistory, StakeHistoryEntry, + }, solana_sysvar::epoch_rewards, solana_transaction::Transaction, }; +fn set_stake_history_sysvar_for_tests(bank: &Bank, stake_history: &StakeHistory) { + let stake_history_account_size = + STAKE_HISTORY_ACCOUNT_SIZE.max(bincode::serialized_size(stake_history).unwrap() as usize); + let old_stake_history_account = bank.get_account(&stake_history::id()).unwrap(); + let lamports = old_stake_history_account + .lamports() + .max(bank.get_minimum_balance_for_rent_exemption(stake_history_account_size)); + let mut stake_history_account = AccountSharedData::new_data_with_space( + lamports, + stake_history, + stake_history_account_size, + &sysvar::id(), + ) + .unwrap(); + stake_history_account.set_rent_epoch(old_stake_history_account.rent_epoch()); + bank.store_account(&stake_history::id(), &stake_history_account); + bank.get_transaction_processor().reset_sysvar_cache(); + bank.get_transaction_processor() + .fill_missing_sysvar_cache_entries(bank); +} + #[test] fn test_sysvar_syscalls() { agave_logger::setup(); @@ -59,7 +83,7 @@ fn test_sysvar_syscalls() { ); stake_history }; - bank.set_sysvar_for_tests(&stake_history); + set_stake_history_sysvar_for_tests(&bank, &stake_history); let (bank, bank_forks) = bank.wrap_with_bank_forks_for_tests(); let program_id = create_program( diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index c889026d92a..4bdaacf3918 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -140,13 +140,17 @@ use { solana_runtime_transaction::{ runtime_transaction::RuntimeTransaction, transaction_with_meta::TransactionWithMeta, }, - solana_sdk_ids::{bpf_loader_upgradeable, incinerator, native_loader, system_program}, + solana_sdk_ids::{ + bpf_loader_upgradeable, incinerator, native_loader, system_program, sysvar as sysvar_id, + }, solana_sha256_hasher::hashv, solana_signature::Signature, solana_slot_hashes::SlotHashes, solana_slot_history::{Check, SlotHistory}, solana_stake_interface::{ - stake_history::StakeHistory, state::Delegation, sysvar::stake_history, + stake_history::{SIZE as STAKE_HISTORY_ACCOUNT_SIZE, StakeHistory}, + state::Delegation, + sysvar::stake_history, }, solana_svm::{ account_loader::LoadedTransaction, @@ -1138,6 +1142,18 @@ struct NewEpochBundle { update_rewards_with_thread_pool_time_us: u64, } +fn create_stake_history_account( + stake_history: &StakeHistory, + (lamports, rent_epoch): InheritableAccountFields, +) -> AccountSharedData { + let data_len = + STAKE_HISTORY_ACCOUNT_SIZE.max(bincode::serialized_size(stake_history).unwrap() as usize); + let mut account = AccountSharedData::new(lamports, data_len, &sysvar_id::id()); + account.serialize_data(stake_history).unwrap(); + account.set_rent_epoch(rent_epoch); + account +} + impl Bank { fn default_with_accounts(accounts: Accounts) -> Self { let partitioned_rewards_stake_account_stores_per_block = accounts @@ -2646,7 +2662,7 @@ impl Bank { } // if I'm the first Bank in an epoch, ensure stake_history is updated self.update_sysvar_account(&stake_history::id(), |account| { - create_account::( + create_stake_history_account( self.stakes_cache.stakes().history(), self.inherit_specially_retained_account_fields(account), ) diff --git a/runtime/src/bank/partitioned_epoch_rewards/distribution.rs b/runtime/src/bank/partitioned_epoch_rewards/distribution.rs index 9c52e692dd0..ec4d1b31ffe 100644 --- a/runtime/src/bank/partitioned_epoch_rewards/distribution.rs +++ b/runtime/src/bank/partitioned_epoch_rewards/distribution.rs @@ -246,12 +246,13 @@ impl Bank { )) .map_err(|_| DistributionError::UnableToSetState)?; - let reward_type = if partitioned_stake_reward.stake.delegation.stake( + #[allow(deprecated)] + let stake_at_distribution_epoch = partitioned_stake_reward.stake.delegation.stake( distribution_epoch, stake_history, new_warmup_cooldown_rate_epoch, - ) == 0 - { + ); + let reward_type = if stake_at_distribution_epoch == 0 { RewardType::DeactivatedStake } else { RewardType::Staking diff --git a/runtime/src/bank/serde_snapshot.rs b/runtime/src/bank/serde_snapshot.rs index cf146012ae8..98c538ce4d9 100644 --- a/runtime/src/bank/serde_snapshot.rs +++ b/runtime/src/bank/serde_snapshot.rs @@ -366,7 +366,7 @@ mod tests { #[cfg_attr( feature = "frozen-abi", derive(AbiExample), - frozen_abi(digest = "5ESyZ9Aseo4v1XA2xnC8dENK8MueieQESYfeNcxByCmd") + frozen_abi(digest = "HPAeeQDtsPexZuCjrCRv1MU6oo8MLHForsk1bXy1863V") )] #[derive(serde::Serialize)] pub struct BankAbiTestWrapper { diff --git a/runtime/src/bank/tests.rs b/runtime/src/bank/tests.rs index b2425df5cb8..a0f7ad586af 100644 --- a/runtime/src/bank/tests.rs +++ b/runtime/src/bank/tests.rs @@ -2917,8 +2917,10 @@ fn test_bank_epoch_vote_accounts() { // epoch_stakes are a snapshot at the leader_schedule_slot_offset boundary // in the prior epoch (0 in this case) + #[allow(deprecated)] + let expected_stake = leader_stake.stake(0, &StakeHistory::default(), None); assert_eq!( - leader_stake.stake(0, &StakeHistory::default(), None), + expected_stake, vote_accounts.unwrap().get(&leader_vote_account).unwrap().0 ); @@ -2933,8 +2935,10 @@ fn test_bank_epoch_vote_accounts() { ); assert!(child.epoch_vote_accounts(epoch).is_some()); + #[allow(deprecated)] + let expected_stake = leader_stake.stake(child.epoch(), &StakeHistory::default(), None); assert_eq!( - leader_stake.stake(child.epoch(), &StakeHistory::default(), None), + expected_stake, child .epoch_vote_accounts(epoch) .unwrap() @@ -2951,8 +2955,10 @@ fn test_bank_epoch_vote_accounts() { SLOTS_PER_EPOCH - (LEADER_SCHEDULE_SLOT_OFFSET % SLOTS_PER_EPOCH) + 1, ); assert!(child.epoch_vote_accounts(epoch).is_some()); + #[allow(deprecated)] + let expected_stake = leader_stake.stake(child.epoch(), &StakeHistory::default(), None); assert_eq!( - leader_stake.stake(child.epoch(), &StakeHistory::default(), None), + expected_stake, child .epoch_vote_accounts(epoch) .unwrap() diff --git a/runtime/src/inflation_rewards/mod.rs b/runtime/src/inflation_rewards/mod.rs index 4e90506421e..570cae4115c 100644 --- a/runtime/src/inflation_rewards/mod.rs +++ b/runtime/src/inflation_rewards/mod.rs @@ -45,12 +45,13 @@ pub(crate) fn redeem_rewards<'a>( commission_rate_in_basis_points, .. } = calculation_environment; + #[allow(deprecated)] + let effective_stake_at_rewarded_epoch = + stake.stake(rewarded_epoch, stake_history, new_rate_activation_epoch); inflation_point_calc_tracer( - &InflationPointCalculationEvent::EffectiveStakeAtRewardedEpoch(stake.stake( - rewarded_epoch, - stake_history, - new_rate_activation_epoch, - )), + &InflationPointCalculationEvent::EffectiveStakeAtRewardedEpoch( + effective_stake_at_rewarded_epoch, + ), ); inflation_point_calc_tracer(&InflationPointCalculationEvent::PriorTotalLamports( current_lamports, diff --git a/runtime/src/inflation_rewards/points.rs b/runtime/src/inflation_rewards/points.rs index 8b26ebe170b..bd91dd5d1b0 100644 --- a/runtime/src/inflation_rewards/points.rs +++ b/runtime/src/inflation_rewards/points.rs @@ -191,6 +191,7 @@ pub(crate) fn calculate_stake_points_and_credits( for epoch_credits_item in vote_state.epoch_credits_iter { let (epoch, final_epoch_credits, initial_epoch_credits) = epoch_credits_item; + #[allow(deprecated)] let stake_amount = u128::from(stake.delegation.stake( epoch, stake_history, diff --git a/runtime/src/stakes.rs b/runtime/src/stakes.rs index cdd05a9bfbf..09c60dfb84d 100644 --- a/runtime/src/stakes.rs +++ b/runtime/src/stakes.rs @@ -269,6 +269,7 @@ impl Stakes { StakeAccount::try_from(create_account_shared_data(account)) { let delegation = stake_account.delegation(); + #[allow(deprecated)] let stake = delegation.stake(epoch, &stake_history, new_rate_activation_epoch); *delegated_stakes.entry(delegation.voter_pubkey).or_default() += stake; stake_delegations.insert(*pubkey, stake_account); @@ -398,11 +399,13 @@ impl Stakes { StakeActivationStatus::default, |acc, (_stake_pubkey, stake_account)| { let delegation = stake_account.delegation(); - acc + delegation.stake_activating_and_deactivating( + #[allow(deprecated)] + let activation_status = delegation.stake_activating_and_deactivating( self.epoch, &self.stake_history, new_rate_activation_epoch, - ) + ); + acc + activation_status }, ) .reduce(StakeActivationStatus::default, Add::add) @@ -434,6 +437,7 @@ impl Stakes { } /// Sum the stakes that point to the given voter_pubkey + #[allow(deprecated)] fn calculate_stake( stake_delegations: &ImblHashMap, voter_pubkey: &Pubkey, @@ -460,6 +464,7 @@ impl Stakes { ) { if let Some(stake_account) = self.stake_delegations.remove(stake_pubkey) { let removed_delegation = stake_account.delegation(); + #[allow(deprecated)] let removed_stake = removed_delegation.stake( self.epoch, &self.stake_history, @@ -499,12 +504,14 @@ impl Stakes { debug_assert_ne!(stake_account.lamports(), 0u64); let delegation = stake_account.delegation(); let voter_pubkey = delegation.voter_pubkey; + #[allow(deprecated)] let stake = delegation.stake(self.epoch, &self.stake_history, new_rate_activation_epoch); match self.stake_delegations.insert(stake_pubkey, stake_account) { None => self.vote_accounts.add_stake(&voter_pubkey, stake), Some(old_stake_account) => { let old_delegation = old_stake_account.delegation(); let old_voter_pubkey = old_delegation.voter_pubkey; + #[allow(deprecated)] let old_stake = old_delegation.stake( self.epoch, &self.stake_history, @@ -644,7 +651,9 @@ fn refresh_vote_accounts( |mut delegated_stakes, (_stake_pubkey, stake_account)| { let delegation = stake_account.delegation(); let entry = delegated_stakes.entry(delegation.voter_pubkey).or_default(); - *entry += delegation.stake(epoch, stake_history, new_rate_activation_epoch); + #[allow(deprecated)] + let stake = delegation.stake(epoch, stake_history, new_rate_activation_epoch); + *entry += stake; delegated_stakes }, ) @@ -768,9 +777,11 @@ pub(crate) mod tests { let stakes = stakes_cache.stakes(); let vote_accounts = stakes.vote_accounts(); assert!(vote_accounts.get(&vote_pubkey).is_some()); + #[allow(deprecated)] + let expected_stake = stake.stake(i, &StakeHistory::default(), None); assert_eq!( vote_accounts.get_delegated_stake(&vote_pubkey), - stake.stake(i, &StakeHistory::default(), None) + expected_stake ); } @@ -780,9 +791,11 @@ pub(crate) mod tests { let stakes = stakes_cache.stakes(); let vote_accounts = stakes.vote_accounts(); assert!(vote_accounts.get(&vote_pubkey).is_some()); + #[allow(deprecated)] + let expected_stake = stake.stake(i, &StakeHistory::default(), None); assert_eq!( vote_accounts.get_delegated_stake(&vote_pubkey), - stake.stake(i, &StakeHistory::default(), None) + expected_stake ); // stays old stake, because only 10 is activated } @@ -799,9 +812,11 @@ pub(crate) mod tests { let stakes = stakes_cache.stakes(); let vote_accounts = stakes.vote_accounts(); assert!(vote_accounts.get(&vote_pubkey).is_some()); + #[allow(deprecated)] + let expected_stake = stake.stake(i, &StakeHistory::default(), None); assert_eq!( vote_accounts.get_delegated_stake(&vote_pubkey), - stake.stake(i, &StakeHistory::default(), None) + expected_stake ); // now stake of 42 is activated } @@ -956,9 +971,11 @@ pub(crate) mod tests { let stakes = stakes_cache.stakes(); let vote_accounts = stakes.vote_accounts(); assert!(vote_accounts.get(&vote_pubkey).is_some()); + #[allow(deprecated)] + let expected_stake = stake.stake(stakes.epoch, &stakes.stake_history, None); assert_eq!( vote_accounts.get_delegated_stake(&vote_pubkey), - stake.stake(stakes.epoch, &stakes.stake_history, None) + expected_stake ); assert!(vote_accounts.get(&vote_pubkey2).is_some()); assert_eq!(vote_accounts.get_delegated_stake(&vote_pubkey2), 0); @@ -973,9 +990,11 @@ pub(crate) mod tests { assert!(vote_accounts.get(&vote_pubkey).is_some()); assert_eq!(vote_accounts.get_delegated_stake(&vote_pubkey), 0); assert!(vote_accounts.get(&vote_pubkey2).is_some()); + #[allow(deprecated)] + let expected_stake = stake.stake(stakes.epoch, &stakes.stake_history, None); assert_eq!( vote_accounts.get_delegated_stake(&vote_pubkey2), - stake.stake(stakes.epoch, &stakes.stake_history, None) + expected_stake ); } } @@ -1026,9 +1045,11 @@ pub(crate) mod tests { { let stakes = stakes_cache.stakes(); let vote_accounts = stakes.vote_accounts(); + #[allow(deprecated)] + let expected_stake = stake.stake(stakes.epoch, &stakes.stake_history, None); assert_eq!( vote_accounts.get_delegated_stake(&vote_pubkey), - stake.stake(stakes.epoch, &stakes.stake_history, None) + expected_stake ); } let thread_pool = ThreadPoolBuilder::new().num_threads(1).build().unwrap(); @@ -1042,9 +1063,11 @@ pub(crate) mod tests { { let stakes = stakes_cache.stakes(); let vote_accounts = stakes.vote_accounts(); + #[allow(deprecated)] + let expected_stake = stake.stake(stakes.epoch, &stakes.stake_history, None); assert_eq!( vote_accounts.get_delegated_stake(&vote_pubkey), - stake.stake(stakes.epoch, &stakes.stake_history, None) + expected_stake ); } } diff --git a/syscalls/src/lib.rs b/syscalls/src/lib.rs index 572a3c4d256..020b69e495a 100644 --- a/syscalls/src/lib.rs +++ b/syscalls/src/lib.rs @@ -2701,7 +2701,9 @@ mod tests { solana_sha256_hasher::hashv, solana_slot_hashes::{self as slot_hashes, SlotHashes}, solana_stable_layout::stable_instruction::StableInstruction, - solana_stake_interface::stake_history::{self, StakeHistory, StakeHistoryEntry}, + solana_stake_interface::stake_history::{ + self, SIZE as STAKE_HISTORY_ACCOUNT_SIZE, StakeHistory, StakeHistoryEntry, + }, solana_sysvar_id::SysvarId, solana_transaction_context::instruction_accounts::InstructionAccount, std::{ @@ -2712,6 +2714,14 @@ mod tests { test_case::test_case, }; + fn create_stake_history_account_for_test(stake_history: &StakeHistory) -> AccountSharedData { + let data_len = STAKE_HISTORY_ACCOUNT_SIZE + .max(bincode::serialized_size(stake_history).unwrap() as usize); + let mut account = AccountSharedData::new(1, data_len, &sysvar::id()); + account.serialize_data(stake_history).unwrap(); + account + } + macro_rules! assert_access_violation { ($result:expr, $va:expr, $len:expr) => { match $result.unwrap_err().downcast_ref::().unwrap() { @@ -4529,17 +4539,17 @@ mod tests { let src_history = src_history; - let mut src_history_buf = vec![0; StakeHistory::size_of()]; + let mut src_history_buf = vec![0; STAKE_HISTORY_ACCOUNT_SIZE]; bincode::serialize_into(&mut src_history_buf, &src_history).unwrap(); let transaction_accounts = vec![( sysvar::stake_history::id(), - create_account_shared_data_for_test(&src_history), + create_stake_history_account_for_test(&src_history), )]; with_mock_invoke_context!(invoke_context, transaction_context, transaction_accounts); { - let mut got_history_buf = vec![0; StakeHistory::size_of()]; + let mut got_history_buf = vec![0; STAKE_HISTORY_ACCOUNT_SIZE]; let got_history_buf_va = 0x100000000; let history_id_va = 0x200000000; let history_id = StakeHistory::id().to_bytes(); @@ -4564,7 +4574,7 @@ mod tests { history_id_va, got_history_buf_va, 0, - StakeHistory::size_of() as u64, + STAKE_HISTORY_ACCOUNT_SIZE as u64, 0, ); assert_eq!(result.unwrap(), 0); From c8e7e1728faba3ec43317c058bb5031a730fe7bc Mon Sep 17 00:00:00 2001 From: Andrew Fitzgerald Date: Thu, 28 May 2026 01:17:35 -0500 Subject: [PATCH 095/576] banking-trace: use bounded channel (#12778) --- core/src/banking_trace.rs | 27 +++++++++++---------------- streamer/src/evicting_sender.rs | 11 ++++++++++- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/core/src/banking_trace.rs b/core/src/banking_trace.rs index 6f6186b50fd..a2867cfabd6 100644 --- a/core/src/banking_trace.rs +++ b/core/src/banking_trace.rs @@ -2,9 +2,7 @@ use { agave_banking_stage_ingress_types::{BankingPacketBatch, BankingPacketReceiver}, bincode::serialize_into, chrono::{DateTime, Local}, - crossbeam_channel::{ - Receiver, SendError, Sender, TryRecvError, TrySendError, bounded, unbounded, - }, + crossbeam_channel::{Receiver, SendError, TryRecvError, TrySendError, bounded}, rolling_file::{RollingCondition, RollingConditionBasic, RollingFileAppender}, serde::{Deserialize, Serialize}, solana_clock::Slot, @@ -66,13 +64,12 @@ pub const DISABLED_BAKING_TRACE_DIR: DirByteLimit = 0; pub const BANKING_TRACE_DIR_DEFAULT_BYTE_LIMIT: DirByteLimit = TRACE_FILE_DEFAULT_ROTATE_BYTE_THRESHOLD * TRACE_FILE_ROTATE_COUNT; -#[derive(Clone, Debug)] +#[derive(Clone)] struct ActiveTracer { - trace_sender: Sender, + trace_sender: EvictingSender, exit: Arc, } -#[derive(Debug)] pub struct BankingTracer { active_tracer: Option, } @@ -230,7 +227,9 @@ impl BankingTracer { )); } - let (trace_sender, trace_receiver) = unbounded(); + const TRACING_CHANNEL_CAPACITY: usize = 50_000; + let (trace_sender, trace_receiver) = + EvictingSender::new_bounded(TRACING_CHANNEL_CAPACITY); let file_appender = Self::create_file_appender(path, rotate_threshold_size)?; @@ -428,15 +427,11 @@ impl TracedSender { pub fn send(&self, batch: BankingPacketBatch) -> Result<(), SendError> { if let Some(ActiveTracer { trace_sender, exit }) = &self.active_tracer { if !exit.load(Ordering::Relaxed) { - trace_sender - .send(TimedTracedEvent( - SystemTime::now(), - TracedEvent::PacketBatch(self.label, BankingPacketBatch::clone(&batch)), - )) - .map_err(|err| { - error!("unexpected error when tracing a banking event...: {err:?}"); - SendError(BankingPacketBatch::clone(&batch)) - })?; + // Ignore errors in sending to tracer - it is a non-critical component. + let _ = trace_sender.try_send(TimedTracedEvent( + SystemTime::now(), + TracedEvent::PacketBatch(self.label, BankingPacketBatch::clone(&batch)), + )); } } let (result, sent) = match self.sender.try_send(batch) { diff --git a/streamer/src/evicting_sender.rs b/streamer/src/evicting_sender.rs index 847952a684c..4a5ae056eb8 100644 --- a/streamer/src/evicting_sender.rs +++ b/streamer/src/evicting_sender.rs @@ -4,12 +4,21 @@ use { }; /// A sender implementation that evicts the oldest message when the channel is full. -#[derive(Clone)] pub struct EvictingSender { sender: Sender, receiver: Receiver, } +// Manual implementation of Clone since `T` is not required to implement Clone. +impl Clone for EvictingSender { + fn clone(&self) -> Self { + Self { + sender: self.sender.clone(), + receiver: self.receiver.clone(), + } + } +} + impl EvictingSender { /// Create a new evicting sender with provided sender, receiver. #[inline] From 899c6afce321259a79d01590fb327848acefdad1 Mon Sep 17 00:00:00 2001 From: Ansh tyagi <155187454+Ansh-699@users.noreply.github.com> Date: Thu, 28 May 2026 11:49:11 +0530 Subject: [PATCH 096/576] validator: Clarify --dynamic-port-range semantics (#7910) The specified range X-Y yields the half open range [X, Y). This might be confusing so make the argument help reflect what the underlying code does --- validator/src/cli.rs | 6 +++++- validator/src/commands/run/args.rs | 5 ++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/validator/src/cli.rs b/validator/src/cli.rs index c5451970f0d..7c5484ac007 100644 --- a/validator/src/cli.rs +++ b/validator/src/cli.rs @@ -354,6 +354,7 @@ pub fn port_validator(port: String) -> Result<(), String> { pub fn port_range_validator(port_range: String) -> Result<(), String> { if let Some((start, end)) = solana_net_utils::parse_port_range(&port_range) { + // The port range is half-open: [start, end) if end - start < MINIMUM_VALIDATOR_PORT_RANGE_WIDTH { Err(format!( "Port range is too small. Try --dynamic-port-range {}-{}", @@ -657,7 +658,10 @@ pub fn test_app<'a>(version: &'a str, default_args: &'a DefaultTestArgs) -> App< .takes_value(true) .default_value(&default_args.dynamic_port_range) .validator(port_range_validator) - .help("Range to use for dynamically assigned ports"), + .help( + "Range to use for dynamically assigned ports. MIN_PORT-MAX_PORT yields the \ + range [MIN_PORT, MAX_PORT)", + ), ) .arg( Arg::with_name("bind_address") diff --git a/validator/src/commands/run/args.rs b/validator/src/commands/run/args.rs index 25d311e7af5..71292bc14ff 100644 --- a/validator/src/commands/run/args.rs +++ b/validator/src/commands/run/args.rs @@ -368,7 +368,10 @@ pub fn add_args<'a>(app: App<'a, 'a>, default_args: &'a DefaultArgs) -> App<'a, .takes_value(true) .default_value(&default_args.dynamic_port_range) .validator(port_range_validator) - .help("Range to use for dynamically assigned ports"), + .help( + "Range to use for dynamically assigned ports. MIN_PORT-MAX_PORT yields the range \ + [MIN_PORT, MAX_PORT)", + ), ) .arg( Arg::with_name("maximum_local_snapshot_age") From 5fea454187aa3de9f1842b4e8e9dcdbf6652d436 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 11:08:11 +0200 Subject: [PATCH 097/576] chore(deps): bump wincode from 0.5.4 to 0.5.5 (#12788) * chore(deps): bump wincode from 0.5.4 to 0.5.5 Bumps [wincode](https://github.com/anza-xyz/wincode) from 0.5.4 to 0.5.5. - [Release notes](https://github.com/anza-xyz/wincode/releases) - [Commits](https://github.com/anza-xyz/wincode/compare/wincode@v0.5.4...wincode@v0.5.5) --- updated-dependencies: - dependency-name: wincode dependency-version: 0.5.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Update all workspaces --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 57 ++++++++--------------------------------- Cargo.toml | 2 +- dev-bins/Cargo.lock | 57 ++++++++--------------------------------- programs/sbf/Cargo.lock | 57 ++++++++--------------------------------- 4 files changed, 34 insertions(+), 139 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8c8ab24f582..da7fbffdf86 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2257,38 +2257,14 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "darling" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" -dependencies = [ - "darling_core 0.21.3", - "darling_macro 0.21.3", -] - [[package]] name = "darling" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ - "darling_core 0.23.0", - "darling_macro 0.23.0", -] - -[[package]] -name = "darling_core" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim 0.11.1", - "syn 2.0.117", + "darling_core", + "darling_macro", ] [[package]] @@ -2304,24 +2280,13 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "darling_macro" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" -dependencies = [ - "darling_core 0.21.3", - "quote", - "syn 2.0.117", -] - [[package]] name = "darling_macro" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ - "darling_core 0.23.0", + "darling_core", "quote", "syn 2.0.117", ] @@ -5052,9 +5017,9 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pastey" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b867cad97c0791bbd3aaa6472142568c6c9e8f71937e98379f584cfb0cf35bec" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" [[package]] name = "pbkdf2" @@ -6463,7 +6428,7 @@ version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" dependencies = [ - "darling 0.23.0", + "darling", "proc-macro2", "quote", "syn 2.0.117", @@ -13059,9 +13024,9 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "wincode" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37095eb18dd6254c66217edc61a29d83d51f8818de8a2ffe88e4584ad73fb5f9" +checksum = "66d967db7705dc29120bb6e8ce5b5a2e27734ed5976d1c904e95bd238d1c3c5a" dependencies = [ "bv", "bytes", @@ -13074,11 +13039,11 @@ dependencies = [ [[package]] name = "wincode-derive" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e262d55d1261f31e2cfe49cc6385a421d14d99faa0526bbe3cc1bda0d3005c62" +checksum = "15ab90b719560d0fda79c74550ad1c948d17b118765942838055ebaf34d67071" dependencies = [ - "darling 0.21.3", + "darling", "proc-macro2", "quote", "syn 2.0.117", diff --git a/Cargo.toml b/Cargo.toml index 8d8c1c9cb0b..8fc83172c10 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -560,7 +560,7 @@ unwrap_none = "0.1.2" uriparse = "0.6.4" url = "2.5.8" winapi = "0.3.8" -wincode = { version = "0.5.4", features = ["derive"] } +wincode = { version = "0.5.5", features = ["derive"] } winreg = "0.55" x509-parser = "0.18.1" zstd = "0.13.3" diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 278d012a5c1..ed9d13a06c3 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -1887,38 +1887,14 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "darling" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" -dependencies = [ - "darling_core 0.21.3", - "darling_macro 0.21.3", -] - [[package]] name = "darling" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ - "darling_core 0.23.0", - "darling_macro 0.23.0", -] - -[[package]] -name = "darling_core" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim 0.11.1", - "syn 2.0.117", + "darling_core", + "darling_macro", ] [[package]] @@ -1934,24 +1910,13 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "darling_macro" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" -dependencies = [ - "darling_core 0.21.3", - "quote", - "syn 2.0.117", -] - [[package]] name = "darling_macro" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ - "darling_core 0.23.0", + "darling_core", "quote", "syn 2.0.117", ] @@ -4428,9 +4393,9 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pastey" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b867cad97c0791bbd3aaa6472142568c6c9e8f71937e98379f584cfb0cf35bec" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" [[package]] name = "pbkdf2" @@ -5632,7 +5597,7 @@ version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" dependencies = [ - "darling 0.23.0", + "darling", "proc-macro2", "quote", "syn 2.0.117", @@ -11148,9 +11113,9 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "wincode" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37095eb18dd6254c66217edc61a29d83d51f8818de8a2ffe88e4584ad73fb5f9" +checksum = "66d967db7705dc29120bb6e8ce5b5a2e27734ed5976d1c904e95bd238d1c3c5a" dependencies = [ "bv", "bytes", @@ -11163,11 +11128,11 @@ dependencies = [ [[package]] name = "wincode-derive" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e262d55d1261f31e2cfe49cc6385a421d14d99faa0526bbe3cc1bda0d3005c62" +checksum = "15ab90b719560d0fda79c74550ad1c948d17b118765942838055ebaf34d67071" dependencies = [ - "darling 0.21.3", + "darling", "proc-macro2", "quote", "syn 2.0.117", diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index df1abc3da06..b5c31ff5174 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -1803,38 +1803,14 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "darling" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" -dependencies = [ - "darling_core 0.21.3", - "darling_macro 0.21.3", -] - [[package]] name = "darling" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ - "darling_core 0.23.0", - "darling_macro 0.23.0", -] - -[[package]] -name = "darling_core" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim 0.11.1", - "syn 2.0.117", + "darling_core", + "darling_macro", ] [[package]] @@ -1850,24 +1826,13 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "darling_macro" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" -dependencies = [ - "darling_core 0.21.3", - "quote", - "syn 2.0.117", -] - [[package]] name = "darling_macro" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ - "darling_core 0.23.0", + "darling_core", "quote", "syn 2.0.117", ] @@ -4369,9 +4334,9 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pastey" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b867cad97c0791bbd3aaa6472142568c6c9e8f71937e98379f584cfb0cf35bec" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" [[package]] name = "pbkdf2" @@ -5547,7 +5512,7 @@ version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" dependencies = [ - "darling 0.23.0", + "darling", "proc-macro2", "quote", "syn 2.0.117", @@ -11621,9 +11586,9 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "wincode" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37095eb18dd6254c66217edc61a29d83d51f8818de8a2ffe88e4584ad73fb5f9" +checksum = "66d967db7705dc29120bb6e8ce5b5a2e27734ed5976d1c904e95bd238d1c3c5a" dependencies = [ "bv", "bytes", @@ -11636,11 +11601,11 @@ dependencies = [ [[package]] name = "wincode-derive" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e262d55d1261f31e2cfe49cc6385a421d14d99faa0526bbe3cc1bda0d3005c62" +checksum = "15ab90b719560d0fda79c74550ad1c948d17b118765942838055ebaf34d67071" dependencies = [ - "darling 0.21.3", + "darling", "proc-macro2", "quote", "syn 2.0.117", From 56c6db31c377a62ee80da4d59c5f160fac8c00c1 Mon Sep 17 00:00:00 2001 From: Jon C Date: Thu, 28 May 2026 12:33:07 +0200 Subject: [PATCH 098/576] SIMD-0232: Respect inflation collector (take 2) (#12550) * SIMD-0232: Respect inflation rewards collector account #### Problem SIMD-0232 has been accepted, but not implemented. Specifically, the inflation rewards collector account set in VoteStateV4 is not respected during rewards payout. #### Summary of changes Pay out inflation rewards into the configured collector account, as specified in SIMD-0232. * Handle commission receiver duplicates * Include burned lamports in distributed amount * Handle case of a collector account becoming a vote account * Fix rebase issue * Fail for non-existent vote account, update assert and comment * Update burned lamports during payout fail * Fix rebase conflicts post #12588 * Separate accounting for incinerator lamports * Explain `unwrap_or_default()` behavior better * Rename CollectorType, remove commission_bps logic, more docs --- runtime/src/bank/fee_distribution.rs | 46 +- .../partitioned_epoch_rewards/calculation.rs | 999 ++++++++++++++++-- .../src/bank/partitioned_epoch_rewards/mod.rs | 27 +- 3 files changed, 996 insertions(+), 76 deletions(-) diff --git a/runtime/src/bank/fee_distribution.rs b/runtime/src/bank/fee_distribution.rs index f5e1dcb9b4a..7c9815fac4e 100644 --- a/runtime/src/bank/fee_distribution.rs +++ b/runtime/src/bank/fee_distribution.rs @@ -19,7 +19,7 @@ use { }; #[derive(Error, Debug, PartialEq)] -enum DepositFeeError { +pub(super) enum DepositFeeError { #[error("fee account became rent paying")] InvalidRentPayingAccount, #[error("lamport overflow")] @@ -30,6 +30,19 @@ enum DepositFeeError { ReservedCollector, } +/// Helper enum used to distinguish external collector types allowed by +/// SIMD-0232. +/// +/// The term "external" is used to exclude the vote account itself, which is a +/// valid collector. +pub(super) enum ExternalCollectorType { + /// A rent-exempt, non-incinerator, non-reserved account owned by the system + /// program + SystemAccount, + /// Specifically, the incinerator account, denoted by `incinerator::id()` + Incinerator, +} + #[derive(Default)] pub struct FeeDistribution { deposit: u64, @@ -188,7 +201,7 @@ impl Bank { .checked_add_lamports(fees) .map_err(|_| DepositFeeError::LamportOverflow)?; if collector_id != &self.leader.vote_address { - Bank::check_collector_account( + Bank::collector_type_checked( collector_id, pre_lamports, &account, @@ -228,14 +241,20 @@ impl Bank { Ok(account.lamports()) } - fn check_collector_account( + /// Checks if a collector account adheres to the rules outlined in SIMD-0232: + /// * system program owned account + /// * rent-exempt after depositing inflation rewards commission + /// * not a reserved account + /// + /// Returns the kind of collector + pub(super) fn collector_type_checked( collector_id: &Pubkey, pre_lamports: u64, account: &AccountSharedData, reserved_account_keys: &ReservedAccountKeys, rent: &Rent, relax_post_execution_balance_checks: bool, - ) -> Result<(), DepositFeeError> { + ) -> Result { if !system_program::check_id(account.owner()) { return Err(DepositFeeError::InvalidAccountOwner); } @@ -245,15 +264,18 @@ impl Bank { } // Don't perform rent check on the incinerator, so that the deposit - // always works. The incinerator is cleaned right after this step - if *collector_id != incinerator::id() - && !rent.is_exempt(account.lamports(), account.data().len()) - && (!relax_post_execution_balance_checks || pre_lamports == 0) - { - return Err(DepositFeeError::InvalidRentPayingAccount); + // always works. The incinerator is run at the end of a block + if *collector_id == incinerator::id() { + Ok(ExternalCollectorType::Incinerator) + } else { + if !rent.is_exempt(account.lamports(), account.data().len()) + && (!relax_post_execution_balance_checks || pre_lamports == 0) + { + Err(DepositFeeError::InvalidRentPayingAccount) + } else { + Ok(ExternalCollectorType::SystemAccount) + } } - - Ok(()) } } diff --git a/runtime/src/bank/partitioned_epoch_rewards/calculation.rs b/runtime/src/bank/partitioned_epoch_rewards/calculation.rs index bb5f5456701..0779177cece 100644 --- a/runtime/src/bank/partitioned_epoch_rewards/calculation.rs +++ b/runtime/src/bank/partitioned_epoch_rewards/calculation.rs @@ -3,11 +3,14 @@ use { Bank, CachedVoteAccounts, CalculateValidatorRewardsResult, EpochRewardCalculateParamInfo, PartitionedRewardsCalculation, PartitionedStakeReward, PartitionedStakeRewards, REWARD_CALCULATION_NUM_BLOCKS, RewardCommission, RewardCommissionAccounts, - RewardCommissionAccountsStorable, RewardCommissions, StakeRewardCalculation, - epoch_rewards_hasher::hash_rewards_into_partitions, + RewardCommissionAccountsStorable, RewardCommissionLamportAmounts, RewardCommissions, + StakeRewardCalculation, epoch_rewards_hasher::hash_rewards_into_partitions, }, crate::{ - bank::{RewardCalcTracer, RewardCalculationEvent, RewardsMetrics, null_tracer}, + bank::{ + RewardCalcTracer, RewardCalculationEvent, RewardsMetrics, + fee_distribution::ExternalCollectorType, null_tracer, + }, inflation_rewards::{ adjust_delegation_for_rent, points::{CalculationEnvironment, DelegatedVoteState, PointValue, calculate_points}, @@ -29,7 +32,10 @@ use { solana_reward_info::RewardType, solana_stake_interface::{stake_history::StakeHistory, state::Delegation}, solana_sysvar::epoch_rewards::EpochRewards, - std::sync::{Arc, atomic::Ordering::Relaxed}, + std::sync::{ + Arc, + atomic::{AtomicU64, Ordering::Relaxed}, + }, }; #[derive(Debug)] @@ -46,6 +52,61 @@ struct RewardsAccumulator { total_stake_rewards_lamports: u64, } +/// Merge the lamport and `is_vote_account` fields of two `RewardCommission`s +/// +/// This pays special attention to the case where `is_vote_account` does not +/// match, which can happen in the following situation: +/// +/// * a vote account A sets the inflation collector to valid system account B +/// * at some point in the future, that system account B gets allocated and +/// initialized as a vote account B +/// * vote account B sets itself as the inflation reward collector +/// +/// In that situation, the rewards for vote account A will get burned, but the +/// rewards for vote account B will not. According to the rules of SIMD-0232, +/// a collector account must either be the vote account itself or a system +/// account that fulfills certain criteria. In the case of vote account A, we +/// are already sure that the collector account is invalid. +/// +/// NOTE: if vote account B sets a system account as its inflation collector, +/// then the commission lamports for vote account A will NOT get burned here, +/// but will get burned during `load_and_reward_commission_accounts` +fn accumulate_lamports(src: &RewardCommission, dst: &mut RewardCommission) { + match (src.is_vote_account, dst.is_vote_account) { + (false, true) => { + // Don't accumulate, burn everything in the source + // reward commission entry. + // + // NOTE: There shouldn't be any burned lamports in the + // source entry, but we're defensive + dst.burned_lamports = dst + .burned_lamports + .saturating_add(src.commission_lamports) + .saturating_add(src.burned_lamports); + } + (true, false) => { + // The commission lamports on the source are the only + // ones that get distributed, all others get burned. + // + // NOTE: There shouldn't be any burned lamports in the + // destination entry, but we're defensive + dst.is_vote_account = true; + dst.burned_lamports = dst + .burned_lamports + .saturating_add(dst.commission_lamports) + .saturating_add(src.burned_lamports); + dst.commission_lamports = src.commission_lamports; + } + _ => { + // Normal case, just accumulate both + dst.commission_lamports = dst + .commission_lamports + .saturating_add(src.commission_lamports); + dst.burned_lamports = dst.burned_lamports.saturating_add(src.burned_lamports); + } + } +} + impl RewardsAccumulator { fn add_reward( &mut self, @@ -56,9 +117,7 @@ impl RewardsAccumulator { self.reward_commissions .entry(commission_pubkey) .and_modify(|dst_reward_commission| { - dst_reward_commission.commission_lamports = dst_reward_commission - .commission_lamports - .saturating_add(reward_commission.commission_lamports) + accumulate_lamports(&reward_commission, dst_reward_commission); }) .or_insert(reward_commission); self.num_stake_rewards = self.num_stake_rewards.saturating_add(1); @@ -84,9 +143,7 @@ impl RewardsAccumulator { dst.reward_commissions .entry(commission_pubkey) .and_modify(|dst_reward_commission: &mut RewardCommission| { - dst_reward_commission.commission_lamports = dst_reward_commission - .commission_lamports - .saturating_add(reward_commission.commission_lamports) + accumulate_lamports(&reward_commission, dst_reward_commission); }) .or_insert(reward_commission); } @@ -102,7 +159,8 @@ impl Bank { /// Begin the process of calculating and distributing rewards. /// This process can take multiple slots. /// - /// Returns the epoch validator rewards. + /// Returns the distributed epoch validator rewards, not including lamports + /// distributed to the incinerator. pub(in crate::bank) fn begin_partitioned_rewards( &mut self, parent_epoch: Epoch, @@ -112,7 +170,11 @@ impl Bank { rewards_metrics: &mut RewardsMetrics, thread_pool: &ThreadPool, ) -> u64 { - let distributed_rewards = self.distribute_reward_commissions( + let RewardCommissionLamportAmounts { + distributed_lamports, + distributed_to_incinerator_lamports, + burned_lamports, + } = self.distribute_reward_commissions( parent_epoch, rewards_calculation, rewards_metrics, @@ -135,7 +197,7 @@ impl Bank { self.set_epoch_reward_status_calculation(distribution_starting_block_height, stake_rewards); self.create_epoch_rewards_sysvar( - distributed_rewards, + distributed_lamports + distributed_to_incinerator_lamports + burned_lamports, distribution_starting_block_height, num_partitions, point_value, @@ -149,7 +211,7 @@ impl Bank { ("parent_slot", parent_slot, i64), ("parent_block_height", parent_block_height, i64), ); - distributed_rewards + distributed_lamports } // Calculate rewards from previous epoch and distribute reward commissions @@ -203,14 +265,15 @@ impl Bank { rewards_calculation } - /// Returns the total amount of commission lamports distributed. + /// Returns a tuple containing the total amount of commission lamports + /// distributed and the total amount of lamports burned. pub(in crate::bank) fn distribute_reward_commissions( &mut self, prev_epoch: Epoch, rewards_calculation: &PartitionedRewardsCalculation, rewards_metrics: &mut RewardsMetrics, thread_pool: &ThreadPool, - ) -> u64 { + ) -> RewardCommissionLamportAmounts { let PartitionedRewardsCalculation { reward_commissions, stake_rewards, @@ -234,7 +297,12 @@ impl Bank { reward_commission_accounts.accounts_with_rewards.len(), load_and_reward_commission_accounts_us, ); - let total_reward_commissions = reward_commission_accounts.total_reward_commission_lamports; + + let RewardCommissionLamportAmounts { + distributed_lamports, + distributed_to_incinerator_lamports, + burned_lamports, + } = reward_commission_accounts.amounts; self.store_commission_accounts_partitioned(&reward_commission_accounts, rewards_metrics); self.update_reward_commissions(&reward_commission_accounts); @@ -244,16 +312,26 @@ impl Bank { } = stake_rewards; // verify that we didn't pay any more than we expected to - assert!(point_value.rewards >= total_reward_commissions + total_stake_rewards_lamports); + assert!( + point_value.rewards + >= distributed_lamports + + distributed_to_incinerator_lamports + + burned_lamports + + total_stake_rewards_lamports + ); info!( "distributed reward commissions: {} out of {}, remaining {}", - total_reward_commissions, point_value.rewards, total_stake_rewards_lamports + distributed_lamports + distributed_to_incinerator_lamports + burned_lamports, + point_value.rewards, + total_stake_rewards_lamports ); let num_stake_accounts = self.stakes_cache.stakes().stake_delegations().len(); let num_vote_accounts = *num_filtered_vote_accounts; - self.capitalization - .fetch_add(total_reward_commissions, Relaxed); + self.capitalization.fetch_add( + distributed_lamports + distributed_to_incinerator_lamports, + Relaxed, + ); let active_stake = if let Some(stake_history_entry) = self.stakes_cache.stakes().history().get(prev_epoch) @@ -267,7 +345,13 @@ impl Bank { "epoch_rewards", ("slot", self.slot, i64), ("epoch", prev_epoch, i64), - ("validator_rewards", total_reward_commissions, i64), + ("validator_rewards", distributed_lamports, i64), + ( + "validator_rewards_to_incinerator", + distributed_to_incinerator_lamports, + i64 + ), + ("validator_rewards_burned", burned_lamports, i64), ("active_stake", active_stake, i64), ("pre_capitalization", *capitalization, i64), ("post_capitalization", self.capitalization(), i64), @@ -275,7 +359,7 @@ impl Bank { ("num_vote_accounts", num_vote_accounts, i64), ); - total_reward_commissions + reward_commission_accounts.amounts } fn store_commission_accounts_partitioned( @@ -478,9 +562,14 @@ impl Bank { stake_reward: 0, commission_bps: (!custom_commission_collector).then_some(0), }; + // Set `is_vote_account` to `false` in order to deliberately + // fail during commission collector checks. This avoids + // creating a reward entry during payout. let reward_commission = RewardCommission { commission_bps: (!custom_commission_collector).then_some(0), commission_lamports: 0, + burned_lamports: 0, + is_vote_account: false, }; return Some(DelegationRewards { stake_reward, @@ -543,13 +632,23 @@ impl Bank { stake_reward, commission_bps: (!custom_commission_collector).then_some(commission_bps), }; + let (commission_pubkey, is_vote_account) = if custom_commission_collector { + let commission_pubkey = *vote_state + .inflation_rewards_collector() + .unwrap_or(&vote_pubkey); + (commission_pubkey, commission_pubkey == vote_pubkey) + } else { + (vote_pubkey, true) + }; let reward_commission = RewardCommission { commission_bps: (!custom_commission_collector).then_some(commission_bps), commission_lamports, + burned_lamports: 0, + is_vote_account, }; Some(DelegationRewards { stake_reward, - commission_pubkey: vote_pubkey, + commission_pubkey, reward_commission, }) } @@ -817,6 +916,14 @@ impl Bank { reward_commissions: &RewardCommissions, thread_pool: &ThreadPool, ) -> RewardCommissionAccounts { + let reserved_account_keys = &self.reserved_account_keys; + let rent = &self.rent_collector().rent; + let feature_snapshot = self.feature_set.snapshot(); + let relax_post_exec_min_balance_check = feature_snapshot.relax_post_exec_min_balance_check; + let custom_commission_collector = feature_snapshot.custom_commission_collector; + let total_non_incinerator_burned_lamports = AtomicU64::new(0); + let total_incinerator_lamports = AtomicU64::new(0); + let accounts_with_rewards: Vec<_> = thread_pool.install(|| { reward_commissions .par_iter() @@ -826,24 +933,70 @@ impl Bank { RewardCommission { commission_bps, commission_lamports, - .. + burned_lamports, + is_vote_account, }, )| { - let Some(mut commission_account) = - self.get_account_with_fixed_root_no_cache(commission_pubkey) - else { - debug!( - "commission account {commission_pubkey} missing at distribution \ - time" - ); - return None; + let maybe_commission_account = + self.get_account_with_fixed_root_no_cache(commission_pubkey); + let mut commission_account = if custom_commission_collector { + // If the account doesn't exist, the vote commission + // may be enough lamports to cover rent-exemption + // and properly create the commission account. + maybe_commission_account.unwrap_or_default() + } else { + // Before SIMD-0232, commission accounts were always + // vote accounts, which cannot be closed unless the + // account hasn't voted for at least a full epoch. + // This means that `maybe_commission_account` should + // always exist. + let Some(commission_account) = maybe_commission_account else { + debug!( + "commission account {commission_pubkey} missing at \ + distribution time" + ); + return None; + }; + commission_account }; + if *burned_lamports != 0 { + total_non_incinerator_burned_lamports + .fetch_add(*burned_lamports, Relaxed); + } + let pre_lamports = commission_account.lamports(); if let Err(err) = commission_account.checked_add_lamports(*commission_lamports) { debug!("reward redemption failed for {commission_pubkey}: {err:?}"); + total_non_incinerator_burned_lamports + .fetch_add(*commission_lamports, Relaxed); return None; } + if !is_vote_account { + match Self::collector_type_checked( + commission_pubkey, + pre_lamports, + &commission_account, + reserved_account_keys, + rent, + relax_post_exec_min_balance_check, + ) { + Ok(ExternalCollectorType::SystemAccount) => {} + Ok(ExternalCollectorType::Incinerator) => { + total_incinerator_lamports + .fetch_add(*commission_lamports, Relaxed); + } + Err(err) => { + debug!( + "reward redemption failed for {commission_pubkey} due to \ + commission account error: {err:?}" + ); + total_non_incinerator_burned_lamports + .fetch_add(*commission_lamports, Relaxed); + return None; + } + } + } Some(( *commission_pubkey, RewardInfo { @@ -858,13 +1011,21 @@ impl Bank { ) .collect() }); - let total_reward_commission_lamports = accounts_with_rewards + + let distributed_to_incinerator_lamports = total_incinerator_lamports.into_inner(); + let distributed_lamports = accounts_with_rewards .iter() .map(|(_, info, _)| info.lamports as u64) - .sum(); + .sum::() + .checked_sub(distributed_to_incinerator_lamports) + .expect("incinerator lamports must be a subset of all distributed lamports"); RewardCommissionAccounts { accounts_with_rewards, - total_reward_commission_lamports, + amounts: RewardCommissionLamportAmounts { + distributed_lamports, + distributed_to_incinerator_lamports, + burned_lamports: total_non_incinerator_burned_lamports.into_inner(), + }, } } @@ -918,6 +1079,7 @@ mod tests { solana_keypair::Keypair, solana_native_token::LAMPORTS_PER_SOL, solana_rent::Rent, + solana_sdk_ids::incinerator, solana_signer::Signer, solana_stake_interface::{ stake_flags::StakeFlags, @@ -931,7 +1093,7 @@ mod tests { collections::HashSet, sync::{Arc, RwLock, RwLockReadGuard}, }, - test_case::test_case, + test_case::{test_case, test_matrix}, }; #[test] @@ -965,12 +1127,12 @@ mod tests { *info, commission_account.clone(), )); - reward_commission_accounts.total_reward_commission_lamports += info.lamports as u64; + reward_commission_accounts.amounts.distributed_lamports += info.lamports as u64; } let metrics = RewardsMetrics::default(); - let total_reward_commissions = reward_commission_accounts.total_reward_commission_lamports; + let total_reward_commissions = reward_commission_accounts.amounts.distributed_lamports; bank.store_commission_accounts_partitioned(&reward_commission_accounts, &metrics); assert_eq!( num_reward_commissions, @@ -1001,7 +1163,7 @@ mod tests { let expected = 0; let reward_commission_accounts = RewardCommissionAccounts::default(); let metrics = RewardsMetrics::default(); - let total_reward_commissions = reward_commission_accounts.total_reward_commission_lamports; + let total_reward_commissions = reward_commission_accounts.amounts.distributed_lamports; bank.store_commission_accounts_partitioned(&reward_commission_accounts, &metrics); assert_eq!( @@ -1144,17 +1306,18 @@ mod tests { #[derive(Default)] struct VoteOperations { expect_reward: bool, + // Additional lamport amount to ignore in collector account, used for + // VAT burns in Alpenglow when the incinerator is an inflation collector + extra_reward_lamport_amount: Option, // ops to perform before epoch ends create_with_balance: Option, delegate_stake_amount: Option, new_commission: Option, earned_credits: Option, + new_inflation_rewards_collector: Option, } - fn recalculate_reward_commission_for_tests( - bank: &Bank, - commission_pubkey: &Pubkey, - ) -> Option { + fn recalculate_reward_commissions_for_tests(bank: &Bank) -> RewardCommissions { let epoch_rewards_sysvar = bank.get_epoch_rewards_sysvar(); let thread_pool = ThreadPoolBuilder::new().num_threads(1).build().unwrap(); assert!(epoch_rewards_sysvar.active); @@ -1182,19 +1345,42 @@ mod tests { null_tracer(), &mut RewardsMetrics::default(), // This is required, but not reporting anything at the moment ); - reward_commissions.get(commission_pubkey).map(|rc| { + reward_commissions + } + + fn recalculate_reward_commission_for_tests( + bank: &Bank, + commission_pubkey: &Pubkey, + ) -> Option { + let reward_commissions = recalculate_reward_commissions_for_tests(bank); + reward_commissions.get(commission_pubkey).and_then(|rc| { + let commission_account = bank.get_account(commission_pubkey).unwrap_or_default(); // In the recalculation path, commissions have already been // distributed — so the current account balance already includes // them. We report that balance as the post_balance. - let post_balance = bank - .get_account(commission_pubkey) - .map(|a| a.lamports()) - .unwrap_or(0); - RewardInfo { - reward_type: RewardType::Voting, - lamports: rc.commission_lamports as i64, - post_balance, - commission_bps: rc.commission_bps, + let post_balance = commission_account.lamports(); + + // This is artificial, but mimics the actual distribution logic a + // bit better + if *commission_account.owner() != solana_vote_program::id() + && Bank::collector_type_checked( + commission_pubkey, + post_balance.saturating_sub(rc.commission_lamports), + &commission_account, + &bank.reserved_account_keys, + &bank.rent_collector().rent, + true, + ) + .is_err() + { + None + } else { + Some(RewardInfo { + reward_type: RewardType::Voting, + lamports: rc.commission_lamports as i64, + post_balance, + commission_bps: rc.commission_bps, + }) } }) } @@ -1300,6 +1486,12 @@ mod tests { }); } + if let Some(inflation_rewards_collector) = vote_op.new_inflation_rewards_collector { + modify_vote_state(&|vote_state: &mut VoteStateV4| { + vote_state.inflation_rewards_collector = inflation_rewards_collector; + }); + } + if let Some(earned_credits) = vote_op.earned_credits { modify_vote_state(&|vote_state: &mut VoteStateV4| { let last_credits = vote_state @@ -1323,28 +1515,39 @@ mod tests { Bank::new_from_parent_with_bank_forks(bank_forks, bank, SlotLeader::new_unique(), slot); for (vote_address, vote_op) in &op.vote_operations { + // some tests delegate before the vote account exists + let collector_address = bank + .get_account(vote_address) + .map(|vote_account| { + VoteStateV4::deserialize(vote_account.data(), vote_address) + .unwrap() + .inflation_rewards_collector + }) + .unwrap_or(*vote_address); let recalculated_vote_reward = - recalculate_reward_commission_for_tests(&bank, vote_address); + recalculate_reward_commission_for_tests(&bank, &collector_address); let vote_reward = bank .rewards .read() .unwrap() .iter() - .find(|(address, _reward)| address == vote_address) + .find(|(address, _reward)| *address == collector_address) .map(|(_address, reward)| *reward); - let prev_vote_balance = prev_bank - .get_account(vote_address) + let prev_collector_balance = prev_bank + .get_account(&collector_address) .unwrap_or_default() .lamports(); - let vote_balance = bank.get_balance(vote_address); + let collector_balance = bank.get_balance(&collector_address); if vote_op.expect_reward { - let reward_lamports = vote_balance - prev_vote_balance; + let reward_lamports = collector_balance + - prev_collector_balance + - vote_op.extra_reward_lamport_amount.unwrap_or(0); let expected_vote_reward = RewardInfo { reward_type: RewardType::Voting, lamports: reward_lamports as i64, - post_balance: vote_balance, + post_balance: collector_balance, commission_bps: None, }; @@ -2245,6 +2448,8 @@ mod tests { RewardCommission { commission_bps: Some(1_000), commission_lamports: 50, + burned_lamports: 0, + is_vote_account: true, }, 50, ); @@ -2253,6 +2458,8 @@ mod tests { RewardCommission { commission_bps: Some(1_000), commission_lamports: 50, + burned_lamports: 0, + is_vote_account: true, }, 50, ); @@ -2261,6 +2468,8 @@ mod tests { RewardCommission { commission_bps: Some(1_000), commission_lamports: 30, + burned_lamports: 0, + is_vote_account: true, }, 30, ); @@ -2269,6 +2478,8 @@ mod tests { RewardCommission { commission_bps: Some(1_000), commission_lamports: 50, + burned_lamports: 0, + is_vote_account: true, }, 50, ); @@ -2330,6 +2541,157 @@ mod tests { assert_eq!(reward_commission_c.commission_lamports, 50); } + fn check_accumulator( + accumulator: &RewardsAccumulator, + commission_pubkey: &Pubkey, + left_reward: &RewardCommission, + right_reward: &RewardCommission, + ) { + let reward_commission = accumulator + .reward_commissions + .get(commission_pubkey) + .unwrap(); + + assert_eq!( + left_reward.is_vote_account || right_reward.is_vote_account, + reward_commission.is_vote_account + ); + match (left_reward.is_vote_account, right_reward.is_vote_account) { + (false, true) => { + assert_eq!( + reward_commission.commission_lamports, + right_reward.commission_lamports + ); + assert_eq!( + reward_commission.burned_lamports, + left_reward.burned_lamports + + right_reward.burned_lamports + + left_reward.commission_lamports + ); + } + (true, false) => { + assert_eq!( + reward_commission.commission_lamports, + left_reward.commission_lamports + ); + assert_eq!( + reward_commission.burned_lamports, + left_reward.burned_lamports + + right_reward.burned_lamports + + right_reward.commission_lamports + ); + } + _ => { + assert_eq!( + reward_commission.commission_lamports, + left_reward.commission_lamports + right_reward.commission_lamports + ); + assert_eq!( + reward_commission.burned_lamports, + left_reward.burned_lamports + right_reward.burned_lamports + ); + } + } + } + + #[test_matrix( + [false, true], + [false, true] + )] + fn test_reward_accumulator_add_rewards( + left_is_vote_account: bool, + right_is_vote_account: bool, + ) { + let mut accumulator = RewardsAccumulator::default(); + let commission_pubkey = Pubkey::new_unique(); + let commission_bps = Some(1_000); + + let left_reward = RewardCommission { + commission_bps, + commission_lamports: 1, + burned_lamports: 10, + is_vote_account: left_is_vote_account, + }; + + let right_reward = RewardCommission { + commission_bps, + commission_lamports: 100, + burned_lamports: 1_000, + is_vote_account: right_is_vote_account, + }; + accumulator.add_reward(commission_pubkey, left_reward.clone(), 50); + accumulator.add_reward(commission_pubkey, right_reward.clone(), 50); + + check_accumulator( + &accumulator, + &commission_pubkey, + &left_reward, + &right_reward, + ); + } + + #[test_matrix( + [false, true], + [false, true], + [false, true] + )] + fn test_reward_accumulator_accumulate_into_larger( + left_is_vote_account: bool, + right_is_vote_account: bool, + right_is_larger: bool, + ) { + let mut accumulator1 = RewardsAccumulator::default(); + let mut accumulator2 = RewardsAccumulator::default(); + let commission_pubkey = Pubkey::new_unique(); + let commission_bps = Some(1_000); + + let left_reward = RewardCommission { + commission_bps, + commission_lamports: 1, + burned_lamports: 10, + is_vote_account: left_is_vote_account, + }; + + let right_reward = RewardCommission { + commission_bps, + commission_lamports: 100, + burned_lamports: 1_000, + is_vote_account: right_is_vote_account, + }; + + accumulator1.add_reward(commission_pubkey, left_reward.clone(), 50); + accumulator2.add_reward(commission_pubkey, right_reward.clone(), 50); + + let additional_commission_pubkey = Pubkey::new_unique(); + let additional_reward_commission = RewardCommission { + commission_bps, + commission_lamports: 1, + burned_lamports: 2, + is_vote_account: true, + }; + if right_is_larger { + accumulator2.add_reward( + additional_commission_pubkey, + additional_reward_commission, + 50, + ); + } else { + accumulator1.add_reward( + additional_commission_pubkey, + additional_reward_commission, + 50, + ); + }; + + let accumulator = accumulator1.accumulate_into_larger(accumulator2); + check_accumulator( + &accumulator, + &commission_pubkey, + &left_reward, + &right_reward, + ); + } + #[test] fn test_epoch_rewards_cache_multiple_forks() { let (mut genesis_config, _mint_keypair) = @@ -2596,6 +2958,8 @@ mod tests { RewardCommission { commission_bps: Some(0), commission_lamports: 1, // enough to overflow + burned_lamports: 0, + is_vote_account: true, }, ); let result = bank.load_and_reward_commission_accounts(&reward_commissions, &thread_pool); @@ -2622,6 +2986,8 @@ mod tests { RewardCommission { commission_bps: Some(500), commission_lamports, + burned_lamports: 0, + is_vote_account: true, }, ); @@ -2650,7 +3016,7 @@ mod tests { commission_bps: Some(500), } ); - assert_eq!(result.total_reward_commission_lamports, commission_lamports); + assert_eq!(result.amounts.distributed_lamports, commission_lamports); } #[test] @@ -2670,6 +3036,8 @@ mod tests { RewardCommission { commission_bps: Some(commission_bps), commission_lamports, + burned_lamports: 0, + is_vote_account: true, }, ); let result = @@ -2690,4 +3058,511 @@ mod tests { } } } + + #[test] + fn test_inflation_rewards_collector() { + let GenesisConfigInfo { + mut genesis_config, .. + } = genesis_utils::create_genesis_config_with_leader( + 1_000_000 * LAMPORTS_PER_SOL, + &Pubkey::new_unique(), + 42 * LAMPORTS_PER_SOL, + ); + + genesis_config.rent = Rent::default(); + genesis_config.epoch_schedule = EpochSchedule::new(SLOTS_PER_EPOCH); + + let (bank, bank_forks) = + Bank::new_for_tests(&genesis_config).wrap_with_bank_forks_for_tests(); + let vote_address = Pubkey::new_unique(); + + // Vote account just created + let mut bank = apply_epoch_operations( + bank, + bank_forks.as_ref(), + EpochOperations { + epoch: 0, + vote_operations: vec![( + vote_address, + VoteOperations { + create_with_balance: Some(LAMPORTS_PER_SOL), + new_commission: Some(1), + earned_credits: Some(1000), + delegate_stake_amount: Some(LAMPORTS_PER_SOL), + ..VoteOperations::default() + }, + )], + }, + ); + + for (epoch, (collector_address, maybe_account, expect_reward)) in [ + // system account with lamports, success + ( + Pubkey::new_unique(), + Some(AccountSharedData::new( + LAMPORTS_PER_SOL, + 0, + &solana_sdk_ids::system_program::id(), + )), + true, + ), + // vote account, success + (vote_address, None, true), + // incinerator, success + (incinerator::id(), None, true), + // non-rent-exempt system account with 1 lamport, success with relaxed checks + ( + Pubkey::new_unique(), + Some(AccountSharedData::new( + 1, + 0, + &solana_sdk_ids::system_program::id(), + )), + true, + ), + // invalid owner, no commission + ( + Pubkey::new_unique(), + Some(AccountSharedData::new( + LAMPORTS_PER_SOL, + 0, + &Pubkey::new_unique(), + )), + false, + ), + // reserved account, no commission + (solana_sdk_ids::native_loader::id(), None, false), + // non-rent-exempt system account, no commission + (Pubkey::new_unique(), None, false), + ] + .into_iter() + .enumerate() + { + if let Some(account) = maybe_account { + bank.store_account(&collector_address, &account); + } + bank = apply_epoch_operations( + bank, + bank_forks.as_ref(), + EpochOperations { + epoch: epoch as u64 + 1, + vote_operations: vec![( + vote_address, + VoteOperations { + earned_credits: Some(1), + new_inflation_rewards_collector: Some(collector_address), + expect_reward, + ..VoteOperations::default() + }, + )], + }, + ); + } + } + + #[test] + fn test_inflation_rewards_collector_becomes_rent_exempt() { + let GenesisConfigInfo { + mut genesis_config, .. + } = genesis_utils::create_genesis_config_with_leader( + 1_000_000 * LAMPORTS_PER_SOL, + &Pubkey::new_unique(), + 42 * LAMPORTS_PER_SOL, + ); + + genesis_config.rent = Rent::default(); + genesis_config.epoch_schedule = EpochSchedule::new(SLOTS_PER_EPOCH); + + let (bank, bank_forks) = + Bank::new_for_tests(&genesis_config).wrap_with_bank_forks_for_tests(); + let vote_address = Pubkey::new_unique(); + + // Vote account just created + let bank = apply_epoch_operations( + bank, + bank_forks.as_ref(), + EpochOperations { + epoch: 0, + vote_operations: vec![( + vote_address, + VoteOperations { + create_with_balance: Some(LAMPORTS_PER_SOL), + new_commission: Some(100), + earned_credits: Some(1000), + delegate_stake_amount: Some(LAMPORTS_PER_SOL), + new_inflation_rewards_collector: Some(Pubkey::new_unique()), + ..VoteOperations::default() + }, + )], + }, + ); + + // next epoch, get reward into new account + let epoch = bank.epoch(); + apply_epoch_operations( + bank, + bank_forks.as_ref(), + EpochOperations { + epoch, + vote_operations: vec![( + vote_address, + VoteOperations { + earned_credits: Some(1), + expect_reward: true, + ..VoteOperations::default() + }, + )], + }, + ); + } + + #[test] + fn test_repeated_inflation_rewards_collector() { + let GenesisConfigInfo { + mut genesis_config, .. + } = genesis_utils::create_genesis_config_with_leader( + 1_000_000 * LAMPORTS_PER_SOL, + &Pubkey::new_unique(), + 42 * LAMPORTS_PER_SOL, + ); + + genesis_config.rent = Rent::default(); + genesis_config.epoch_schedule = EpochSchedule::new(SLOTS_PER_EPOCH); + + let (bank, bank_forks) = + Bank::new_for_tests(&genesis_config).wrap_with_bank_forks_for_tests(); + + let collector_address = Pubkey::new_unique(); + let vote1_address = Pubkey::new_unique(); + let vote2_address = Pubkey::new_unique(); + // Vote account just created + let bank = apply_epoch_operations( + bank, + bank_forks.as_ref(), + EpochOperations { + epoch: 0, + vote_operations: vec![ + ( + vote1_address, + VoteOperations { + create_with_balance: Some(LAMPORTS_PER_SOL), + new_commission: Some(50), + earned_credits: Some(1000), + delegate_stake_amount: Some(LAMPORTS_PER_SOL), + new_inflation_rewards_collector: Some(collector_address), + ..VoteOperations::default() + }, + ), + ( + vote2_address, + VoteOperations { + create_with_balance: Some(LAMPORTS_PER_SOL), + new_commission: Some(100), + earned_credits: Some(1000), + delegate_stake_amount: Some(LAMPORTS_PER_SOL), + new_inflation_rewards_collector: Some(collector_address), + ..VoteOperations::default() + }, + ), + ], + }, + ); + + // next epoch, get double reward into collector + let epoch = bank.epoch(); + apply_epoch_operations( + bank, + bank_forks.as_ref(), + EpochOperations { + epoch, + vote_operations: vec![ + ( + vote1_address, + VoteOperations { + earned_credits: Some(1), + expect_reward: true, + ..VoteOperations::default() + }, + ), + ( + vote2_address, + VoteOperations { + earned_credits: Some(1), + expect_reward: true, + ..VoteOperations::default() + }, + ), + ], + }, + ); + } + + #[test] + fn test_invalid_inflation_rewards_collector_burns_sysvar_rewards() { + let GenesisConfigInfo { + mut genesis_config, .. + } = genesis_utils::create_genesis_config_with_leader( + 1_000_000 * LAMPORTS_PER_SOL, + &Pubkey::new_unique(), + 42 * LAMPORTS_PER_SOL, + ); + + genesis_config.rent = Rent::default(); + genesis_config.epoch_schedule = EpochSchedule::new(SLOTS_PER_EPOCH); + + let (bank, bank_forks) = + Bank::new_for_tests(&genesis_config).wrap_with_bank_forks_for_tests(); + let vote_address = Pubkey::new_unique(); + + // Create a vote account with delegated stake and 100% commission. + // Using 100% commission makes the expected accounting simple: every lamport + // of the epoch reward should go through the commission collector path. + let bank = apply_epoch_operations( + bank, + bank_forks.as_ref(), + EpochOperations { + epoch: 0, + vote_operations: vec![( + vote_address, + VoteOperations { + create_with_balance: Some(LAMPORTS_PER_SOL), + new_commission: Some(100), + earned_credits: Some(1000), + delegate_stake_amount: Some(LAMPORTS_PER_SOL), + ..VoteOperations::default() + }, + )], + }, + ); + + // Simulate a collector account that is invalid at reward distribution time. + // Started as system account but now it's owned by a random program + let invalid_collector = Pubkey::new_unique(); + bank.store_account( + &invalid_collector, + &AccountSharedData::new(LAMPORTS_PER_SOL, 0, &Pubkey::new_unique()), + ); + + // Point the vote account at the invalid collector and advance to the reward + // boundary. Account is not credited, so the commission lamports are burned. + let bank = apply_epoch_operations( + bank, + bank_forks.as_ref(), + EpochOperations { + epoch: 1, + vote_operations: vec![( + vote_address, + VoteOperations { + earned_credits: Some(1000), + new_inflation_rewards_collector: Some(invalid_collector), + ..VoteOperations::default() + }, + )], + }, + ); + + let epoch_rewards = bank.get_epoch_rewards_sysvar(); + + // The burned commission lamports must be reflected in the epoch rewards + // sysvar. Since this test uses 100% commission, all calculated rewards + // went through the invalid collector path. + assert!(epoch_rewards.total_rewards > 0); + assert_eq!( + epoch_rewards.total_rewards, + epoch_rewards.distributed_rewards + ); + } + + #[test] + fn test_incinerator_not_included_load_and_reward_commission_accounts() { + let (genesis_config, _mint_keypair) = create_genesis_config(1_000 * LAMPORTS_PER_SOL); + let bank = Bank::new_for_tests(&genesis_config); + let thread_pool = ThreadPoolBuilder::new().num_threads(1).build().unwrap(); + + let pubkey = incinerator::id(); + let mut incinerator_account = AccountSharedData::default(); + incinerator_account.set_lamports(1); + bank.store_account_and_update_capitalization(&pubkey, &incinerator_account); + let mut reward_commissions = RewardCommissions::default(); + + let commission_lamports = 100; + reward_commissions.insert( + pubkey, + RewardCommission { + commission_bps: None, + commission_lamports, + burned_lamports: 0, + is_vote_account: false, + }, + ); + let result = bank.load_and_reward_commission_accounts(&reward_commissions, &thread_pool); + assert_eq!( + result.amounts.distributed_to_incinerator_lamports, + commission_lamports + ); + assert_eq!(result.amounts.distributed_lamports, 0); + assert_eq!(result.amounts.burned_lamports, 0); + } + + #[test] + fn test_inflation_collector_becomes_vote_account_burns_rewards() { + let GenesisConfigInfo { + mut genesis_config, .. + } = genesis_utils::create_genesis_config_with_leader( + 1_000_000 * LAMPORTS_PER_SOL, + &Pubkey::new_unique(), + 42 * LAMPORTS_PER_SOL, + ); + + genesis_config.rent = Rent::default(); + genesis_config.epoch_schedule = EpochSchedule::new(SLOTS_PER_EPOCH); + + let (bank, bank_forks) = + Bank::new_for_tests(&genesis_config).wrap_with_bank_forks_for_tests(); + let vote_address = Pubkey::new_unique(); + let collector_into_vote_address = Pubkey::new_unique(); + + // Create a normal vote account with a currently valid inflation collector + let bank = apply_epoch_operations( + bank, + bank_forks.as_ref(), + EpochOperations { + epoch: 0, + vote_operations: vec![( + vote_address, + VoteOperations { + create_with_balance: Some(LAMPORTS_PER_SOL), + new_commission: Some(100), + earned_credits: Some(1000), + delegate_stake_amount: Some(LAMPORTS_PER_SOL), + new_inflation_rewards_collector: Some(collector_into_vote_address), + ..VoteOperations::default() + }, + )], + }, + ); + + // New vote account gets nothing + let rewards = bank.get_balance(&collector_into_vote_address); + assert_eq!(rewards, 0); + + // Next epoch gets something + let bank = apply_epoch_operations( + bank, + bank_forks.as_ref(), + EpochOperations { + epoch: 1, + vote_operations: vec![( + vote_address, + VoteOperations { + earned_credits: Some(1000), + expect_reward: true, + ..VoteOperations::default() + }, + )], + }, + ); + let pre_balance = bank.get_balance(&collector_into_vote_address); + assert_ne!(pre_balance, 0); + + // Transform the collector into a vote account, see that all rewards + // are burned for this epoch + let bank = apply_epoch_operations( + bank, + bank_forks.as_ref(), + EpochOperations { + epoch: 2, + vote_operations: vec![ + ( + vote_address, + VoteOperations { + earned_credits: Some(1000), + expect_reward: true, + ..VoteOperations::default() + }, + ), + ( + collector_into_vote_address, + VoteOperations { + create_with_balance: Some(pre_balance), + new_commission: Some(100), + earned_credits: Some(1000), + delegate_stake_amount: Some(LAMPORTS_PER_SOL), + ..VoteOperations::default() + }, + ), + ], + }, + ); + + let vote_reward = bank + .rewards + .read() + .unwrap() + .iter() + .find(|(address, _reward)| *address == collector_into_vote_address) + .map(|(_address, reward)| *reward) + .unwrap(); + assert_eq!(vote_reward.lamports, 0); + + let unchanged_balance = bank.get_balance(&collector_into_vote_address); + assert_eq!(unchanged_balance, pre_balance); + + // `collector_into_vote_address` receives its rewards, but `vote_address` + // has its rewards burned + let bank = apply_epoch_operations( + bank, + bank_forks.as_ref(), + EpochOperations { + epoch: 3, + vote_operations: vec![ + ( + vote_address, + VoteOperations { + earned_credits: Some(1000), + expect_reward: true, + ..VoteOperations::default() + }, + ), + ( + collector_into_vote_address, + VoteOperations { + earned_credits: Some(1000), + expect_reward: true, + ..VoteOperations::default() + }, + ), + ], + }, + ); + + // Some rewards were distributed + let post_balance = bank.get_balance(&collector_into_vote_address); + assert!(post_balance > pre_balance); + + // They're reflected in the reported rewards + let vote_reward = bank + .rewards + .read() + .unwrap() + .iter() + .find(|(address, _reward)| *address == collector_into_vote_address) + .map(|(_address, reward)| *reward) + .unwrap(); + assert_eq!(vote_reward.lamports as u64, post_balance - pre_balance); + + // Some lamports were burned + let reward_commissions = recalculate_reward_commissions_for_tests(&bank); + let reward_commission = reward_commissions + .get(&collector_into_vote_address) + .unwrap(); + assert_ne!(reward_commission.burned_lamports, 0); + + // The burned lamports are included in the epoch rewards sysvar + let epoch_rewards = bank.get_epoch_rewards_sysvar(); + assert_eq!( + reward_commission.burned_lamports + reward_commission.commission_lamports, + epoch_rewards.distributed_rewards + ); + } } diff --git a/runtime/src/bank/partitioned_epoch_rewards/mod.rs b/runtime/src/bank/partitioned_epoch_rewards/mod.rs index 5bf473a87c5..9218ceac4e7 100644 --- a/runtime/src/bank/partitioned_epoch_rewards/mod.rs +++ b/runtime/src/bank/partitioned_epoch_rewards/mod.rs @@ -152,21 +152,44 @@ pub(crate) enum EpochRewardPhase { } #[derive(Debug)] +#[cfg_attr(test, derive(Clone))] pub(super) struct RewardCommission { // Note: This field becomes always `None` once SIMD-0232 is activated. // After full activation, it can be removed on feature cleanup. pub(super) commission_bps: Option, pub(super) commission_lamports: u64, + pub(super) burned_lamports: u64, + pub(super) is_vote_account: bool, } pub(super) type RewardCommissions = HashMap; +/// Helper struct to give the amounts distributed to commission accounts or +/// burned in different manners +#[derive(Debug, Default)] +pub(super) struct RewardCommissionLamportAmounts { + /// Lamports distributed across all commission collectors, except the + /// incinerator. + /// + /// Also the maximum capitalization increase by the end of the first block + /// of the epoch. + pub(super) distributed_lamports: u64, + /// lamports distributed to the incinerator. + /// + /// Tracked separately to give a better upper bound for capitalization at + /// the end of the first block of an epoch, needed for Alpenglow's + /// `EpochInflationAccountState` + pub(super) distributed_to_incinerator_lamports: u64, + /// lamports burned from undistributed commissions + pub(super) burned_lamports: u64, +} + #[derive(Debug, Default)] pub(super) struct RewardCommissionAccounts { /// accounts with rewards to be stored pub(super) accounts_with_rewards: Vec<(Pubkey, RewardInfo, AccountSharedData)>, - /// total lamports across all `accounts_with_rewards` - pub(super) total_reward_commission_lamports: u64, + /// amounts distributed to those accounts, and burned after calculation + pub(super) amounts: RewardCommissionLamportAmounts, } /// Wrapper struct to implement StorableAccounts for RewardCommissionAccounts From 33f2d7999f19937ef58466dc8e231ab6fb050c0b Mon Sep 17 00:00:00 2001 From: Joe C Date: Thu, 28 May 2026 18:40:36 +0800 Subject: [PATCH 099/576] svm: conformance: internalize compute budget arg (#12792) * svm: conformance: add cu_avail to context * svm: conformance: internalize compute budget arg --- programs/sbf/tests/programs.rs | 284 +++++++++----------------------- svm/src/conformance/context.rs | 22 +++ svm/src/conformance/harness.rs | 43 ++--- svm/src/conformance/programs.rs | 2 +- 4 files changed, 110 insertions(+), 241 deletions(-) diff --git a/programs/sbf/tests/programs.rs b/programs/sbf/tests/programs.rs index 559d1e4a3e7..28128588ace 100644 --- a/programs/sbf/tests/programs.rs +++ b/programs/sbf/tests/programs.rs @@ -7,6 +7,8 @@ #![allow(clippy::unnecessary_cast)] #![allow(clippy::uninlined_format_args)] +#[cfg(not(feature = "sbf_sanity_list"))] +use solana_program_runtime::execution_budget::MAX_COMPUTE_UNIT_LIMIT; #[cfg(all(feature = "sbf_rust", feature = "sbpf-v3"))] use solana_runtime::loader_utils::{ load_upgradeable_program_and_advance_slot, set_upgrade_authority, upgrade_program, @@ -108,7 +110,6 @@ fn default_program_cache_with_program( program_id: &Pubkey, program_elf: &[u8], feature_set: &SVMFeatureSet, - compute_budget: &ComputeBudget, ) -> solana_program_runtime::loaded_programs::ProgramCacheForTxBatch { let mut program_cache = default_program_cache(); add_program_to_program_cache( @@ -117,7 +118,6 @@ fn default_program_cache_with_program( &bpf_loader_upgradeable::id(), program_elf, feature_set, - compute_budget, ); program_cache } @@ -308,22 +308,18 @@ fn test_program_sbf_sanity() { let accounts = vec![(pubkey1, Account::default()), (pubkey2, Account::default())]; - let compute_budget = ComputeBudget::new_with_defaults(false); - let mut program_cache = default_program_cache_with_program( - &program_id, - &program_elf, - &feature_set, - &compute_budget, - ); + let mut program_cache = + default_program_cache_with_program(&program_id, &program_elf, &feature_set); let sysvar_cache = default_sysvar_cache(); let context = InstrContext { feature_set, accounts, instruction, + cu_avail: MAX_COMPUTE_UNIT_LIMIT as u64, // Widen to 1.4 M }; - let effects = execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache); + let effects = execute_instr(&context, &mut program_cache, &sysvar_cache); let result = match effects.result { Some(err) => Err(err), @@ -361,8 +357,6 @@ fn test_program_sbf_loader_deprecated() { let feature_set = SVMFeatureSet::all_enabled(); - let compute_budget = ComputeBudget::new_with_defaults(false); - let pubkey = Pubkey::new_unique(); let accounts = vec![ (program_id, Account::new(0, 0, &bpf_loader_deprecated::id())), @@ -376,20 +370,15 @@ fn test_program_sbf_loader_deprecated() { &bpf_loader_deprecated::id(), &program_elf, &feature_set, - &compute_budget, ); let sysvar_cache = default_sysvar_cache(); let account_metas = vec![AccountMeta::new(pubkey, true)]; let instruction = Instruction::new_with_bytes(program_id, &[255], account_metas); - let context = InstrContext { - feature_set, - accounts, - instruction, - }; + let context = InstrContext::new_with_default_budget(feature_set, accounts, instruction); - let effects = execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache); + let effects = execute_instr(&context, &mut program_cache, &sysvar_cache); assert!(effects.result.is_none()); } @@ -417,7 +406,6 @@ fn test_sol_alloc_free_no_longer_deployable_with_upgradeable_loader() { let payer_pubkey = Pubkey::new_unique(); let feature_set = SVMFeatureSet::all_enabled(); - let compute_budget = ComputeBudget::new_with_defaults(false); let (programdata_address, _) = Pubkey::find_program_address(&[program_id.as_ref()], &bpf_loader_upgradeable::id()); @@ -499,18 +487,14 @@ fn test_sol_alloc_free_no_longer_deployable_with_upgradeable_loader() { .pop() .unwrap(); - let context = InstrContext { - feature_set, - accounts, - instruction, - }; + let context = InstrContext::new_with_default_budget(feature_set, accounts, instruction); // Expect that deployment to fail. B/C during deployment, there is an elf // verification step, which uses the runtime to look up relocatable symbols // in elf inside syscall table. In this case, `sol_alloc_free_` can't be // found in syscall table. Hence, the verification fails and the deployment // fails. - let effects = execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache); + let effects = execute_instr(&context, &mut program_cache, &sysvar_cache); assert_eq!(effects.result, Some(InstructionError::InvalidAccountData)); } @@ -536,13 +520,8 @@ fn test_program_sbf_duplicate_accounts() { let program_elf = load_program_elf(program); let program_id = Pubkey::new_unique(); let feature_set = SVMFeatureSet::all_enabled(); - let compute_budget = ComputeBudget::new_with_defaults(false); - let mut program_cache = default_program_cache_with_program( - &program_id, - &program_elf, - &feature_set, - &compute_budget, - ); + let mut program_cache = + default_program_cache_with_program(&program_id, &program_elf, &feature_set); let sysvar_cache = default_sysvar_cache(); let payer_pubkey = Pubkey::new_unique(); @@ -564,12 +543,8 @@ fn test_program_sbf_duplicate_accounts() { (pubkey, account.clone()), ]; let instruction = Instruction::new_with_bytes(program_id, data, account_metas.clone()); - let context = InstrContext { - feature_set, - accounts, - instruction, - }; - execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache) + let context = InstrContext::new_with_default_budget(feature_set, accounts, instruction); + execute_instr(&context, &mut program_cache, &sysvar_cache) }; let effects = execute(&[1]); @@ -616,12 +591,8 @@ fn test_program_sbf_duplicate_accounts() { (pubkey, Account::new(10, 1, &program_id)), ]; let instruction = Instruction::new_with_bytes(program_id, &[7], account_metas); - let context = InstrContext { - feature_set, - accounts, - instruction, - }; - let effects = execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache); + let context = InstrContext::new_with_default_budget(feature_set, accounts, instruction); + let effects = execute_instr(&context, &mut program_cache, &sysvar_cache); assert!(effects.result.is_none()); } } @@ -653,13 +624,8 @@ fn test_program_sbf_error_handling() { let accounts = vec![(pubkey1, Account::default())]; - let compute_budget = ComputeBudget::new_with_defaults(false); - let mut program_cache = default_program_cache_with_program( - &program_id, - &program_elf, - &feature_set, - &compute_budget, - ); + let mut program_cache = + default_program_cache_with_program(&program_id, &program_elf, &feature_set); let sysvar_cache = default_sysvar_cache(); // Helper to execute an instruction with the given data byte. @@ -667,13 +633,10 @@ fn test_program_sbf_error_handling() { let account_metas = vec![AccountMeta::new(pubkey1, true)]; let instruction = Instruction::new_with_bytes(program_id, data, account_metas); - let context = InstrContext { - feature_set, - accounts: accounts.clone(), - instruction, - }; + let context = + InstrContext::new_with_default_budget(feature_set, accounts.clone(), instruction); - execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache) + execute_instr(&context, &mut program_cache, &sysvar_cache) }; let effects = execute(&[1]); @@ -742,30 +705,21 @@ fn test_return_data_and_log_data_syscall() { let program_id = Pubkey::new_unique(); let feature_set = SVMFeatureSet::all_enabled(); - let compute_budget = ComputeBudget::new_with_defaults(false); let pubkey = Pubkey::new_unique(); let accounts = vec![(pubkey, Account::default())]; - let mut program_cache = default_program_cache_with_program( - &program_id, - &program_elf, - &feature_set, - &compute_budget, - ); + let mut program_cache = + default_program_cache_with_program(&program_id, &program_elf, &feature_set); let sysvar_cache = default_sysvar_cache(); let account_metas = vec![AccountMeta::new(pubkey, true)]; let instruction = Instruction::new_with_bytes(program_id, &[1, 2, 3, 0, 4, 5, 6], account_metas); - let context = InstrContext { - feature_set, - accounts, - instruction, - }; + let context = InstrContext::new_with_default_budget(feature_set, accounts, instruction); - let effects = execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache); + let effects = execute_instr(&context, &mut program_cache, &sysvar_cache); assert!(effects.result.is_none()); @@ -1419,7 +1373,6 @@ fn test_program_sbf_program_id_spoofing() { let malicious_system_pubkey = Pubkey::new_unique(); let feature_set = SVMFeatureSet::all_enabled(); - let compute_budget = ComputeBudget::new_with_defaults(false); let from_pubkey = Pubkey::new_unique(); let to_pubkey = Pubkey::new_unique(); @@ -1441,7 +1394,6 @@ fn test_program_sbf_program_id_spoofing() { &bpf_loader_upgradeable::id(), &spoof1_elf, &feature_set, - &compute_budget, ); add_program_to_program_cache( &mut program_cache, @@ -1449,7 +1401,6 @@ fn test_program_sbf_program_id_spoofing() { &bpf_loader_upgradeable::id(), &spoof1_system_elf, &feature_set, - &compute_budget, ); let sysvar_cache = default_sysvar_cache(); @@ -1463,13 +1414,9 @@ fn test_program_sbf_program_id_spoofing() { let instruction = Instruction::new_with_bytes(malicious_swap_pubkey, &[], account_metas.clone()); - let context = InstrContext { - feature_set, - accounts, - instruction, - }; + let context = InstrContext::new_with_default_budget(feature_set, accounts, instruction); - let effects = execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache); + let effects = execute_instr(&context, &mut program_cache, &sysvar_cache); assert_eq!( effects.result, @@ -1492,7 +1439,6 @@ fn test_program_sbf_caller_has_access_to_cpi_program() { let caller2_pubkey = Pubkey::new_unique(); let feature_set = SVMFeatureSet::all_enabled(); - let compute_budget = ComputeBudget::new_with_defaults(false); let accounts = vec![ ( @@ -1512,7 +1458,6 @@ fn test_program_sbf_caller_has_access_to_cpi_program() { &bpf_loader_upgradeable::id(), &caller_access_elf, &feature_set, - &compute_budget, ); add_program_to_program_cache( &mut program_cache, @@ -1520,7 +1465,6 @@ fn test_program_sbf_caller_has_access_to_cpi_program() { &bpf_loader_upgradeable::id(), &caller_access_elf, &feature_set, - &compute_budget, ); let sysvar_cache = default_sysvar_cache(); @@ -1530,13 +1474,9 @@ fn test_program_sbf_caller_has_access_to_cpi_program() { ]; let instruction = Instruction::new_with_bytes(caller_pubkey, &[1], account_metas); - let context = InstrContext { - feature_set, - accounts, - instruction, - }; + let context = InstrContext::new_with_default_budget(feature_set, accounts, instruction); - let effects = execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache); + let effects = execute_instr(&context, &mut program_cache, &sysvar_cache); assert_eq!(effects.result, Some(InstructionError::MissingAccount)); } @@ -1550,7 +1490,6 @@ fn test_program_sbf_ro_modify() { let program_id = Pubkey::new_unique(); let feature_set = SVMFeatureSet::all_enabled(); - let compute_budget = ComputeBudget::new_with_defaults(false); let test_pubkey = Pubkey::new_unique(); let accounts = vec![ @@ -1558,12 +1497,8 @@ fn test_program_sbf_ro_modify() { (test_pubkey, Account::new(10, 0, &system_program::id())), ]; - let mut program_cache = default_program_cache_with_program( - &program_id, - &program_elf, - &feature_set, - &compute_budget, - ); + let mut program_cache = + default_program_cache_with_program(&program_id, &program_elf, &feature_set); let sysvar_cache = default_sysvar_cache(); let account_metas = vec![ @@ -1575,13 +1510,10 @@ fn test_program_sbf_ro_modify() { let instruction = Instruction::new_with_bytes(program_id, &[instr_data], account_metas.clone()); - let context = InstrContext { - feature_set, - accounts: accounts.clone(), - instruction, - }; + let context = + InstrContext::new_with_default_budget(feature_set, accounts.clone(), instruction); - let effects = execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache); + let effects = execute_instr(&context, &mut program_cache, &sysvar_cache); assert_eq!( effects.result, @@ -1601,24 +1533,16 @@ fn test_program_sbf_call_depth() { let feature_set = SVMFeatureSet::all_enabled(); let compute_budget = ComputeBudget::new_with_defaults(false); - let mut program_cache = default_program_cache_with_program( - &program_id, - &program_elf, - &feature_set, - &compute_budget, - ); + let mut program_cache = + default_program_cache_with_program(&program_id, &program_elf, &feature_set); let sysvar_cache = default_sysvar_cache(); let mut execute = |depth: usize| { let instruction = Instruction::new_with_bincode(program_id, &depth, vec![]); - let context = InstrContext { - feature_set, - accounts: vec![], - instruction, - }; + let context = InstrContext::new_with_default_budget(feature_set, vec![], instruction); - execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache) + execute_instr(&context, &mut program_cache, &sysvar_cache) }; let effects = execute(compute_budget.max_call_depth - 1); @@ -1637,20 +1561,14 @@ fn test_program_sbf_compute_budget() { let program_id = Pubkey::new_unique(); let feature_set = SVMFeatureSet::all_enabled(); - let mut compute_budget = ComputeBudget::new_with_defaults(false); - compute_budget.compute_unit_limit = 0; let accounts = vec![( program_id, Account::new(0, 0, &bpf_loader_upgradeable::id()), )]; - let mut program_cache = default_program_cache_with_program( - &program_id, - &program_elf, - &feature_set, - &compute_budget, - ); + let mut program_cache = + default_program_cache_with_program(&program_id, &program_elf, &feature_set); let sysvar_cache = default_sysvar_cache(); let instruction = Instruction::new_with_bincode(program_id, &0, vec![]); @@ -1659,9 +1577,10 @@ fn test_program_sbf_compute_budget() { feature_set, accounts, instruction, + cu_avail: 0, }; - let effects = execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache); + let effects = execute_instr(&context, &mut program_cache, &sysvar_cache); assert_eq!( effects.result, @@ -1713,7 +1632,6 @@ fn assert_instruction_count() { } let feature_set = SVMFeatureSet::all_enabled(); - let compute_budget = ComputeBudget::new_with_defaults(false); let sysvar_cache = default_sysvar_cache(); println!("\n {:36} expected actual diff", "SBF program"); @@ -1721,12 +1639,8 @@ fn assert_instruction_count() { let program_elf = load_program_elf(program_name); let program_id = Pubkey::new_unique(); - let mut program_cache = default_program_cache_with_program( - &program_id, - &program_elf, - &feature_set, - &compute_budget, - ); + let mut program_cache = + default_program_cache_with_program(&program_id, &program_elf, &feature_set); let account_pubkey = Pubkey::new_unique(); let accounts = vec![(account_pubkey, Account::new(0, 0, &program_id))]; @@ -1738,17 +1652,11 @@ fn assert_instruction_count() { }]; let instruction = Instruction::new_with_bytes(program_id, &[], instruction_accounts); - let context = InstrContext { - feature_set, - accounts, - instruction, - }; + let context = InstrContext::new_with_default_budget(feature_set, accounts, instruction); - let effects = execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache); + let effects = execute_instr(&context, &mut program_cache, &sysvar_cache); - let consumption = compute_budget - .compute_unit_limit - .saturating_sub(effects.cu_avail); + let consumption = context.cu_avail.saturating_sub(effects.cu_avail); let diff: i64 = consumption as i64 - *expected_consumption as i64; println!( " {:36} {:8}{:6} {:+5} ({:+3.0}%)", @@ -1834,14 +1742,9 @@ fn test_program_sbf_r2_instruction_data_pointer(num_accounts: usize, input_data_ let program_id = Pubkey::new_unique(); let feature_set = SVMFeatureSet::all_enabled(); - let compute_budget = ComputeBudget::new_with_defaults(false); - let mut program_cache = default_program_cache_with_program( - &program_id, - &program_elf, - &feature_set, - &compute_budget, - ); + let mut program_cache = + default_program_cache_with_program(&program_id, &program_elf, &feature_set); let sysvar_cache = default_sysvar_cache(); let mut accounts = Vec::new(); @@ -1866,13 +1769,9 @@ fn test_program_sbf_r2_instruction_data_pointer(num_accounts: usize, input_data_ let instruction = Instruction::new_with_bytes(program_id, &input_data, account_metas); - let context = InstrContext { - feature_set, - accounts, - instruction, - }; + let context = InstrContext::new_with_default_budget(feature_set, accounts, instruction); - let effects = execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache); + let effects = execute_instr(&context, &mut program_cache, &sysvar_cache); assert!(effects.result.is_none()); assert_eq!(input_data, effects.return_data); @@ -2375,7 +2274,6 @@ fn test_program_sbf_disguised_as_sbf_loader() { remove_bpf_loader_incorrect_program_id: false, ..SVMFeatureSet::all_enabled() }; - let compute_budget = ComputeBudget::new_with_defaults(false); let mut program_cache = default_program_cache(); add_program_to_program_cache( @@ -2384,20 +2282,15 @@ fn test_program_sbf_disguised_as_sbf_loader() { &bpf_loader::id(), &program_elf, &feature_set, - &compute_budget, ); let sysvar_cache = default_sysvar_cache(); let account_metas = vec![AccountMeta::new_readonly(program_id, false)]; let instruction = Instruction::new_with_bytes(bpf_loader::id(), &[1], account_metas); - let context = InstrContext { - feature_set, - accounts: vec![], - instruction, - }; + let context = InstrContext::new_with_default_budget(feature_set, vec![], instruction); - let effects = execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache); + let effects = execute_instr(&context, &mut program_cache, &sysvar_cache); assert_eq!(effects.result, Some(InstructionError::UnsupportedProgramId)); } } @@ -2411,7 +2304,6 @@ fn test_program_reads_from_program_account() { let program_id = Pubkey::new_unique(); let feature_set = SVMFeatureSet::all_enabled(); - let compute_budget = ComputeBudget::new_with_defaults(false); let mut program_cache = new_program_cache_with_builtins(0); add_program_to_program_cache( @@ -2420,7 +2312,6 @@ fn test_program_reads_from_program_account() { &bpf_loader::id(), &program_elf, &feature_set, - &compute_budget, ); let sysvar_cache = default_sysvar_cache(); @@ -2437,13 +2328,13 @@ fn test_program_reads_from_program_account() { let account_metas = vec![AccountMeta::new_readonly(program_id, false)]; let instruction = Instruction::new_with_bytes(program_id, &program_elf[..48], account_metas); - let context = InstrContext { + let context = InstrContext::new_with_default_budget( feature_set, - accounts: vec![(program_id, program_account)], + vec![(program_id, program_account)], instruction, - }; + ); - let effects = execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache); + let effects = execute_instr(&context, &mut program_cache, &sysvar_cache); assert!(effects.result.is_none()); } @@ -2456,7 +2347,6 @@ fn test_program_sbf_c_dup() { let program_id = Pubkey::new_unique(); let feature_set = SVMFeatureSet::all_enabled(); - let compute_budget = ComputeBudget::new_with_defaults(false); let mut program_cache = new_program_cache_with_builtins(0); add_program_to_program_cache( &mut program_cache, @@ -2464,7 +2354,6 @@ fn test_program_sbf_c_dup() { &bpf_loader_upgradeable::id(), &program_elf, &feature_set, - &compute_budget, ); let sysvar_cache = default_sysvar_cache(); @@ -2479,13 +2368,13 @@ fn test_program_sbf_c_dup() { ]; let instruction = Instruction::new_with_bytes(program_id, &[4, 5, 6, 7], account_metas); - let context = InstrContext { + let context = InstrContext::new_with_default_budget( feature_set, - accounts: vec![(account_address, account)], + vec![(account_address, account)], instruction, - }; + ); - let effects = execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache); + let effects = execute_instr(&context, &mut program_cache, &sysvar_cache); assert!(effects.result.is_none()); } @@ -2664,14 +2553,9 @@ fn test_program_sbf_ro_account_modify() { let program_id = Pubkey::new_unique(); let feature_set = SVMFeatureSet::all_enabled(); - let compute_budget = ComputeBudget::new_with_defaults(false); - let mut program_cache = default_program_cache_with_program( - &program_id, - &program_elf, - &feature_set, - &compute_budget, - ); + let mut program_cache = + default_program_cache_with_program(&program_id, &program_elf, &feature_set); let sysvar_cache = default_sysvar_cache(); let argument_pubkey = Pubkey::new_unique(); @@ -2685,13 +2569,10 @@ fn test_program_sbf_ro_account_modify() { for case in [0, 1, 2] { let instruction = Instruction::new_with_bytes(program_id, &[case], account_metas.clone()); - let context = InstrContext { - feature_set, - accounts: accounts.clone(), - instruction, - }; + let context = + InstrContext::new_with_default_budget(feature_set, accounts.clone(), instruction); - let effects = execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache); + let effects = execute_instr(&context, &mut program_cache, &sysvar_cache); assert_eq!(effects.result, Some(InstructionError::ReadonlyDataModified)); } @@ -4584,18 +4465,9 @@ fn test_program_sbf_deplete_cost_meter_with_divide_by_zero() { let program_id = Pubkey::new_unique(); let feature_set = SVMFeatureSet::all_enabled(); - let compute_budget = { - let mut budget = ComputeBudget::new_with_defaults(false); - budget.compute_unit_limit = 10_000u64; - budget - }; - let mut program_cache = default_program_cache_with_program( - &program_id, - &program_elf, - &feature_set, - &compute_budget, - ); + let mut program_cache = + default_program_cache_with_program(&program_id, &program_elf, &feature_set); let sysvar_cache = default_sysvar_cache(); let instruction = Instruction::new_with_bytes(program_id, &[], vec![]); @@ -4604,9 +4476,10 @@ fn test_program_sbf_deplete_cost_meter_with_divide_by_zero() { feature_set, accounts: vec![], instruction, + cu_avail: 10_000, }; - let effects = execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache); + let effects = execute_instr(&context, &mut program_cache, &sysvar_cache); assert_eq!( effects.result, @@ -5648,14 +5521,9 @@ fn test_program_sbf_rust_direct_account_pointers(num_accounts: usize, input_data let program_id = Pubkey::new_unique(); let feature_set = SVMFeatureSet::all_enabled(); - let compute_budget = ComputeBudget::new_with_defaults(false); - let mut program_cache = default_program_cache_with_program( - &program_id, - &program_elf, - &feature_set, - &compute_budget, - ); + let mut program_cache = + default_program_cache_with_program(&program_id, &program_elf, &feature_set); let sysvar_cache = default_sysvar_cache(); let mut accounts = Vec::new(); @@ -5685,13 +5553,9 @@ fn test_program_sbf_rust_direct_account_pointers(num_accounts: usize, input_data let instruction = Instruction::new_with_bytes(program_id, &input_data, account_metas); - let context = InstrContext { - feature_set, - accounts, - instruction, - }; + let context = InstrContext::new_with_default_budget(feature_set, accounts, instruction); - let effects = execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache); + let effects = execute_instr(&context, &mut program_cache, &sysvar_cache); assert!(effects.result.is_none()); // `num_accounts * 2` will be added as return data. diff --git a/svm/src/conformance/context.rs b/svm/src/conformance/context.rs index 591d6380a04..035a1eb494c 100644 --- a/svm/src/conformance/context.rs +++ b/svm/src/conformance/context.rs @@ -12,6 +12,7 @@ use { use { solana_account::Account, solana_instruction::{Instruction, error::InstructionError}, + solana_program_runtime::execution_budget::DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT, solana_pubkey::Pubkey, solana_svm_feature_set::SVMFeatureSet, }; @@ -21,6 +22,24 @@ pub struct InstrContext { pub feature_set: SVMFeatureSet, pub accounts: Vec<(Pubkey, Account)>, pub instruction: Instruction, + pub cu_avail: u64, +} + +impl InstrContext { + /// Create a new [`InstrContext`] with the default compute unit budget + /// (200,000 CUs). + pub fn new_with_default_budget( + feature_set: SVMFeatureSet, + accounts: Vec<(Pubkey, Account)>, + instruction: Instruction, + ) -> Self { + Self { + feature_set, + accounts, + instruction, + cu_avail: DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT as u64, + } + } } #[cfg(feature = "conformance")] @@ -65,10 +84,13 @@ impl From for InstrContext { program_id, }; + let cu_avail = value.cu_avail; + Self { feature_set, accounts, instruction, + cu_avail, } } } diff --git a/svm/src/conformance/harness.rs b/svm/src/conformance/harness.rs index f2ef894c15f..0730b1513c7 100644 --- a/svm/src/conformance/harness.rs +++ b/svm/src/conformance/harness.rs @@ -67,24 +67,16 @@ impl InvokeContextCallback for ConformanceCallback { /// (no-precompile) callback. pub fn execute_instr( input: &InstrContext, - compute_budget: &ComputeBudget, program_cache: &mut ProgramCacheForTxBatch, sysvar_cache: &SysvarCache, ) -> InstrEffects { - execute_instr_with_callback( - input, - &DefaultCallback, - compute_budget, - program_cache, - sysvar_cache, - ) + execute_instr_with_callback(input, &DefaultCallback, program_cache, sysvar_cache) } /// Execute a single instruction against the Solana VM with a custom callback. pub fn execute_instr_with_callback( input: &InstrContext, callback: &C, - compute_budget: &ComputeBudget, program_cache: &mut ProgramCacheForTxBatch, sysvar_cache: &SysvarCache, ) -> InstrEffects { @@ -93,6 +85,10 @@ pub fn execute_instr_with_callback( let log_collector = LogCollector::new_ref(); let feature_set = input.feature_set; + let simd_0268_active = feature_set.raise_cpi_nesting_limit_to_8; + + let mut compute_budget = ComputeBudget::new_with_defaults(simd_0268_active); + compute_budget.compute_unit_limit = input.cu_avail; // Clamp budget for execution by cu_avail let rent = sysvar_cache.get_rent().unwrap(); let program_id = &input.instruction.program_id; @@ -206,18 +202,8 @@ pub fn execute_instr_with_callback( #[cfg(feature = "conformance")] pub fn execute_instr_proto(input: ProtoInstrContext) -> ProtoInstrEffects { - let cu_avail = input.cu_avail; let instr_context = InstrContext::from(input); - let feature_set = &instr_context.feature_set; - let simd_0268_active = feature_set.raise_cpi_nesting_limit_to_8; - - let compute_budget = { - let mut budget = ComputeBudget::new_with_defaults(simd_0268_active); - budget.compute_unit_limit = cu_avail; - budget - }; - // When testing with protobuf, we fill the sysvar cache from input accounts. let sysvar_cache = { let mut cache = SysvarCache::default(); @@ -234,8 +220,12 @@ pub fn execute_instr_proto(input: ProtoInstrContext) -> ProtoInstrEffects { // When testing with protobuf, we fill the program cache from input accounts. let mut program_cache = { let slot = sysvar_cache.get_clock().unwrap().slot; + let feature_set = &instr_context.feature_set; + let simd_0268_active = feature_set.raise_cpi_nesting_limit_to_8; + let compute_budget = ComputeBudget::new_with_defaults(simd_0268_active); + let environment = create_program_runtime_environment( - &instr_context.feature_set, + feature_set, &compute_budget.to_budget(), false, /* deployment */ false, /* debugging_features */ @@ -252,7 +242,6 @@ pub fn execute_instr_proto(input: ProtoInstrContext) -> ProtoInstrEffects { execute_instr_with_callback( &instr_context, &ConformanceCallback, - &compute_budget, &mut program_cache, &sysvar_cache, ) @@ -369,13 +358,7 @@ mod tests { 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ], }, - }; - - // Set up the Compute Budget. - let compute_budget = { - let mut budget = ComputeBudget::new_with_defaults(false); - budget.compute_unit_limit = cu_avail; - budget + cu_avail, }; // Create Sysvar Cache. @@ -393,7 +376,7 @@ mod tests { let environments = create_program_runtime_environment( &context.feature_set, - &compute_budget.to_budget(), + &ComputeBudget::new_with_defaults(feature_set.raise_cpi_nesting_limit_to_8).to_budget(), false, /* deployment */ false, /* debugging_features */ ) @@ -408,7 +391,7 @@ mod tests { .unwrap(); // Execute the instruction. - let effects = execute_instr(&context, &compute_budget, &mut program_cache, &sysvar_cache); + let effects = execute_instr(&context, &mut program_cache, &sysvar_cache); // Verify the results. assert_eq!(effects.result, None); diff --git a/svm/src/conformance/programs.rs b/svm/src/conformance/programs.rs index bb1b5498ceb..63c78bd2735 100644 --- a/svm/src/conformance/programs.rs +++ b/svm/src/conformance/programs.rs @@ -103,8 +103,8 @@ pub fn add_program_to_program_cache( loader_key: &Pubkey, elf: &[u8], feature_set: &SVMFeatureSet, - compute_budget: &ComputeBudget, ) { + let compute_budget = ComputeBudget::new_with_defaults(feature_set.raise_cpi_nesting_limit_to_8); let program_runtime_environment = create_program_runtime_environment( feature_set, &compute_budget.to_budget(), From cb0db7107867f0a97d56b537259d31c0878aa043 Mon Sep 17 00:00:00 2001 From: Jon C Date: Thu, 28 May 2026 13:05:13 +0200 Subject: [PATCH 100/576] accounts-db: Avoid `mut` on test stake creator (#12551) #### Problem As pointed out at [this comment](https://github.com/anza-xyz/agave/pull/12408/changes/BASE..5945d73bcbc38b0abff64955dd7c327efe031acd#r3251012342), we don't need a `mut` variable. #### Summary of changes Construct the correct `Delegation` directly. --- accounts-db/src/stake_rewards.rs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/accounts-db/src/stake_rewards.rs b/accounts-db/src/stake_rewards.rs index 60839d8fdc2..296a7876b6f 100644 --- a/accounts-db/src/stake_rewards.rs +++ b/accounts-db/src/stake_rewards.rs @@ -226,15 +226,21 @@ fn create_stake_account( ..Meta::default() }; - let mut stake = Stake { - delegation: Delegation::new(voter_pubkey, stake_amount, Epoch::MAX), + let stake = Stake { + delegation: Delegation { + voter_pubkey: *voter_pubkey, + stake: stake_amount, + activation_epoch: Epoch::MAX, + deactivation_epoch: if stake_amount == 0 { + last_epoch + } else { + Epoch::MAX + }, + ..Default::default() + }, credits_observed, }; - if stake_amount == 0 { - stake.delegation.deactivation_epoch = last_epoch; - } - stake_account .set_state(&StakeStateV2::Stake(meta, stake, StakeFlags::empty())) .expect("set_state"); From 2c68dfd6a4f7c5e839e336e62a537d373069405c Mon Sep 17 00:00:00 2001 From: Yihau Chen Date: Thu, 28 May 2026 20:16:28 +0800 Subject: [PATCH 101/576] ci: remove unused verify-packets action (#12740) ci: remove unused verify-packets actions --- .github/workflows/verify-packets.yml | 44 ---------------------------- ci/test-verify-packets-gossip.sh | 13 -------- 2 files changed, 57 deletions(-) delete mode 100644 .github/workflows/verify-packets.yml delete mode 100755 ci/test-verify-packets-gossip.sh diff --git a/.github/workflows/verify-packets.yml b/.github/workflows/verify-packets.yml deleted file mode 100644 index c348be3731a..00000000000 --- a/.github/workflows/verify-packets.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Verify Packets - -on: - push: - branches: - - master - pull_request: - branches: - - master - paths: - - .github/workflows/verify-packets.yml - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - gossip: - timeout-minutes: 30 - runs-on: ubuntu-22.04 - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Install required packages - run: | - sudo apt update - sudo apt install -y \ - libclang-dev \ - libprotobuf-dev \ - libssl-dev \ - libudev-dev \ - pkg-config \ - zlib1g-dev \ - llvm \ - clang \ - cmake \ - make \ - protobuf-compiler \ - git-lfs - - - name: Run packet verify - run: | - ./ci/test-verify-packets-gossip.sh diff --git a/ci/test-verify-packets-gossip.sh b/ci/test-verify-packets-gossip.sh deleted file mode 100755 index 64236fe93cd..00000000000 --- a/ci/test-verify-packets-gossip.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash - -set -e -here=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) - -if ! git lfs --version &>/dev/null; then - echo "Git LFS is not installed. Please install Git LFS to proceed." - exit 1 -fi - -rm -rf "$here"/solana-packets -git clone https://github.com/anza-xyz/solana-packets.git "$here"/solana-packets -GOSSIP_WIRE_FORMAT_PACKETS="$here/solana-packets/GOSSIP_PACKETS" cargo test --package solana-gossip -- wire_format_tests::tests::test_gossip_wire_format --exact --show-output From 3f03791b4eddb1fae5b136bc8ac2647ba6e2aa2c Mon Sep 17 00:00:00 2001 From: kirill lykov Date: Thu, 28 May 2026 14:34:38 +0200 Subject: [PATCH 102/576] xdp: optionally log warning on packet mtu oversize drop (#12731) Optionally log warning on packet mtu oversize drop --- streamer/src/quic_socket.rs | 7 +++---- xdp/src/transmitter.rs | 13 +++++++++++++ xdp/src/tx_loop.rs | 34 +++++++++++++++++++++------------- 3 files changed, 37 insertions(+), 17 deletions(-) diff --git a/streamer/src/quic_socket.rs b/streamer/src/quic_socket.rs index 30b883d851f..61f351138c8 100644 --- a/streamer/src/quic_socket.rs +++ b/streamer/src/quic_socket.rs @@ -266,10 +266,9 @@ impl QuicXdpSender { let src_addr = SocketAddrV4::new(src_ip, self.src_addr.port()); let ecn = ecn.map(quinn_ecn_to_xdp); - self.xdp_sender.try_send( - sender_idx, - BytesTxPacket::new(src_addr, destination, ecn, payload), - ) + let mut packet = BytesTxPacket::new(src_addr, destination, ecn, payload); + packet.set_allow_mtu_overflow(true); + self.xdp_sender.try_send(sender_idx, packet) } } diff --git a/xdp/src/transmitter.rs b/xdp/src/transmitter.rs index 5508c2a60cd..e72b6ad793d 100644 --- a/xdp/src/transmitter.rs +++ b/xdp/src/transmitter.rs @@ -79,6 +79,7 @@ pub struct BytesTxPacket { src_addr: SocketAddrV4, dst_addrs: XdpAddrs, ecn: Option, + allow_mtu_overflow: bool, payload: Bytes, } @@ -97,9 +98,15 @@ impl BytesTxPacket { src_addr, dst_addrs: dst_addrs.into(), ecn, + allow_mtu_overflow: false, payload, } } + + /// Sets whether MTU overflow is possible for this packet. + pub fn set_allow_mtu_overflow(&mut self, allow: bool) { + self.allow_mtu_overflow = allow; + } } #[cfg(not(target_os = "linux"))] @@ -112,6 +119,8 @@ impl BytesTxPacket { ) -> Self { Self } + + pub fn set_allow_mtu_overflow(&mut self, _allow: bool) {} } #[cfg(target_os = "linux")] @@ -134,6 +143,10 @@ impl TxPacket for BytesTxPacket { fn ecn(&self) -> Option { self.ecn } + + fn allow_mtu_overflow(&self) -> bool { + self.allow_mtu_overflow + } } #[derive(Clone)] diff --git a/xdp/src/tx_loop.rs b/xdp/src/tx_loop.rs index 79887141fac..2576b1ae613 100644 --- a/xdp/src/tx_loop.rs +++ b/xdp/src/tx_loop.rs @@ -211,6 +211,9 @@ pub trait TxPacket { /// Explicit congestion notification bits to set on the packet. fn ecn(&self) -> Option; + + /// Returns true when this packet is expected to occasionally exceed the route MTU. + fn allow_mtu_overflow(&self) -> bool; } impl TxLoop { @@ -290,6 +293,7 @@ impl TxLoop { let src_ip = src_addr.ip(); let src_port = src_addr.port(); let ecn = item.ecn(); + let can_overflow_mtu = item.allow_mtu_overflow(); for addr in item.dst_addrs().as_ref() { if ring.available() == 0 || umem.available() == 0 { commit_pending(&mut ring, &mut written_uncommitted); @@ -343,14 +347,16 @@ impl TxLoop { if l3_inner_packet_len > gre.mtu as usize || l3_outer_gre_packet_len > next_hop.mtu as usize { - log::warn!( - "dropping packet: GRE payload exceeds MTU for {addr}: L3 inner \ - packet length {l3_inner_packet_len}, L3 outer GRE packet length \ - {l3_outer_gre_packet_len}, MTU: {mtu}, underlay_mtu: \ - {underlay_mtu}.", - mtu = gre.mtu, - underlay_mtu = next_hop.mtu - ); + if !can_overflow_mtu { + log::warn!( + "dropping packet: GRE payload exceeds MTU for {addr}: L3 \ + inner packet length {l3_inner_packet_len}, L3 outer GRE \ + packet length {l3_outer_gre_packet_len}, MTU: {mtu}, \ + underlay_mtu: {underlay_mtu}.", + mtu = gre.mtu, + underlay_mtu = next_hop.mtu + ); + } batched_packets -= 1; umem.release(frame.offset()); continue; @@ -402,11 +408,13 @@ impl TxLoop { let l3_packet_len = IP_HEADER_SIZE + UDP_HEADER_SIZE + len; if l3_packet_len > next_hop.mtu as usize { - log::warn!( - "dropping packet: packet size {l3_packet_len} exceeds MTU {mtu} \ - for {addr}", - mtu = next_hop.mtu - ); + if !can_overflow_mtu { + log::warn!( + "dropping packet: packet size {l3_packet_len} exceeds MTU \ + {mtu} for {addr}", + mtu = next_hop.mtu + ); + } batched_packets -= 1; umem.release(frame.offset()); continue; From 77c062fda355fe49db6d4ae9599504dc1d562dfa Mon Sep 17 00:00:00 2001 From: Kamil Skalski Date: Thu, 28 May 2026 15:09:02 +0200 Subject: [PATCH 103/576] bench(accounts-db): migrate accounts_index bench to criterion (#12785) * bench(accounts-db): migrate accounts_index bench to criterion * Use checked arithmetic --- accounts-db/Cargo.toml | 4 +++ accounts-db/benches/accounts_index.rs | 46 +++++++++++++-------------- 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/accounts-db/Cargo.toml b/accounts-db/Cargo.toml index 650be86d053..86f62663331 100644 --- a/accounts-db/Cargo.toml +++ b/accounts-db/Cargo.toml @@ -117,6 +117,10 @@ test-case = { workspace = true } [target.'cfg(not(any(target_env = "msvc", target_os = "freebsd")))'.dev-dependencies] jemallocator = { workspace = true } +[[bench]] +name = "accounts_index" +harness = false + [[bench]] name = "bench_accounts_file" harness = false diff --git a/accounts-db/benches/accounts_index.rs b/accounts-db/benches/accounts_index.rs index ce628872946..0d14a9c9ec7 100644 --- a/accounts-db/benches/accounts_index.rs +++ b/accounts-db/benches/accounts_index.rs @@ -1,8 +1,5 @@ -#![feature(test)] - -extern crate test; - use { + criterion::{Criterion, criterion_group, criterion_main}, rand::{Rng, rng}, solana_accounts_db::{ account_info::AccountInfo, @@ -11,15 +8,13 @@ use { }, }, std::sync::Arc, - test::Bencher, }; #[cfg(not(any(target_env = "msvc", target_os = "freebsd")))] #[global_allocator] static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc; -#[bench] -fn bench_accounts_index(bencher: &mut Bencher) { +fn bench_accounts_index(c: &mut Criterion) { const NUM_PUBKEYS: usize = 10_000; let pubkeys: Vec<_> = (0..NUM_PUBKEYS) .map(|_| solana_pubkey::new_rand()) @@ -47,21 +42,26 @@ fn bench_accounts_index(bencher: &mut Bencher) { let mut fork = NUM_FORKS; let mut root = 0; - bencher.iter(|| { - for _p in 0..NUM_PUBKEYS { - let pubkey = rng().random_range(0..NUM_PUBKEYS); - index.upsert( - fork, - fork, - &pubkeys[pubkey], - AccountInfo::default(), - &mut reclaims, - UpsertReclaim::PopulateReclaims, - ); - reclaims.clear(); - } - index.add_root(root); - root += 1; - fork += 1; + c.bench_function("accounts_index", |b| { + b.iter(|| { + for _p in 0..NUM_PUBKEYS { + let pubkey = rng().random_range(0..NUM_PUBKEYS); + index.upsert( + fork, + fork, + &pubkeys[pubkey], + AccountInfo::default(), + &mut reclaims, + UpsertReclaim::PopulateReclaims, + ); + reclaims.clear(); + } + index.add_root(root); + root = root.checked_add(1).expect("root overflow"); + fork = fork.checked_add(1).expect("fork overflow"); + }); }); } + +criterion_group!(benches, bench_accounts_index); +criterion_main!(benches); From fe343c81156c8b7ebfd7d214bac19a06ce66d778 Mon Sep 17 00:00:00 2001 From: Akhilesh Singhania Date: Thu, 28 May 2026 16:29:13 +0200 Subject: [PATCH 104/576] Bugfix: do not drop verified certs if channel is full (#12685) * Removes msgs from cache if sending fails * Revert "Removes msgs from cache if sending fails" This reverts commit ebd7545e436a4843b1dc8bd65ed85f40454c01b6. * PR feedback * rm unnecessary metrics --- core/src/bls_sigverify/stats.rs | 9 ++++++++- core/src/bls_sigverify/utils.rs | 14 ++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/core/src/bls_sigverify/stats.rs b/core/src/bls_sigverify/stats.rs index 202c1a25d8c..6a5c0317773 100644 --- a/core/src/bls_sigverify/stats.rs +++ b/core/src/bls_sigverify/stats.rs @@ -193,9 +193,11 @@ pub(super) struct SigVerifyCertStats { /// Number of times the cert was too far in the future and discarded. pub(super) too_far_in_future: u64, + /// How many messages are outstanding on the channel. + pub(super) pool_outstanding_msgs: u64, /// Number of votes sent successfully over the channel to consensus pool. pub(super) pool_sent: u64, - /// Number of times the channel to consensus pool was full. + /// Number of times the channel to consensus pool was full and we resorted to blocking send. pub(super) pool_channel_full: u64, /// Stats for [`verify_and_send_certificates`]. @@ -212,6 +214,7 @@ impl SigVerifyCertStats { stake_verification_failed, signature_verification_failed, too_far_in_future, + pool_outstanding_msgs, pool_sent, pool_channel_full, fn_verify_and_send_certs_stats, @@ -223,6 +226,7 @@ impl SigVerifyCertStats { self.stake_verification_failed += stake_verification_failed; self.signature_verification_failed += signature_verification_failed; self.too_far_in_future += too_far_in_future; + self.pool_outstanding_msgs += pool_outstanding_msgs; self.pool_sent += pool_sent; self.pool_channel_full += pool_channel_full; self.fn_verify_and_send_certs_stats @@ -238,10 +242,12 @@ impl SigVerifyCertStats { stake_verification_failed, signature_verification_failed, too_far_in_future, + pool_outstanding_msgs, pool_sent, pool_channel_full, fn_verify_and_send_certs_stats, } = self; + datapoint_info!( "bls_cert_sigverify_stats", ("certs_to_sig_verify", *certs_to_sig_verify, i64), @@ -259,6 +265,7 @@ impl SigVerifyCertStats { i64 ), ("too_far_in_future", *too_far_in_future, i64), + ("pool_outstanding_msgs", *pool_outstanding_msgs, i64), ("pool_sent", *pool_sent, i64), ("pool_channel_full", *pool_channel_full, i64), ( diff --git a/core/src/bls_sigverify/utils.rs b/core/src/bls_sigverify/utils.rs index 889afe8455a..720034a29d2 100644 --- a/core/src/bls_sigverify/utils.rs +++ b/core/src/bls_sigverify/utils.rs @@ -98,6 +98,8 @@ pub(super) fn send_votes_to_repair( Ok(()) } +/// Sends the `messages` to the consensus pool. If the channel is bounded and full, then does a +/// blocking send. pub(super) fn send_certs_to_pool( messages: Vec, channel_to_pool: &Sender>, @@ -110,11 +112,19 @@ pub(super) fn send_certs_to_pool( match channel_to_pool.try_send(messages) { Ok(()) => { stats.pool_sent += len as u64; + stats.pool_outstanding_msgs = channel_to_pool.len() as u64; Ok(()) } - Err(TrySendError::Full(_)) => { + Err(TrySendError::Full(msgs)) => { stats.pool_channel_full += 1; - Ok(()) + error!("certs channel to consensus pool is full. Doing a blocking send."); + match channel_to_pool.send(msgs) { + Ok(()) => { + info!("certs channel to consensus pool has space again"); + Ok(()) + } + Err(_) => Err(SigVerifyCertError::ConsensusPoolChannelDisconnected), + } } Err(TrySendError::Disconnected(_)) => { Err(SigVerifyCertError::ConsensusPoolChannelDisconnected) From dab673d75a46e7ade9e6add3199167cb7bcfd307 Mon Sep 17 00:00:00 2001 From: Joe C Date: Fri, 29 May 2026 00:31:26 +0800 Subject: [PATCH 105/576] svm: conformance: add more unit tests for instr harness (#12795) --- svm/src/conformance/harness.rs | 230 ++++++++++++++++++--------------- 1 file changed, 123 insertions(+), 107 deletions(-) diff --git a/svm/src/conformance/harness.rs b/svm/src/conformance/harness.rs index 0730b1513c7..fb3bf2eb673 100644 --- a/svm/src/conformance/harness.rs +++ b/svm/src/conformance/harness.rs @@ -282,136 +282,152 @@ pub unsafe extern "C" fn sol_compat_instr_execute_v1( mod tests { use { super::{ - super::programs::{fill_program_cache_from_accounts, new_program_cache_with_builtins}, + super::programs::{add_program_to_program_cache, new_program_cache_with_builtins}, *, }, + crate::conformance::programs::keyed_account_for_system_program, solana_account::Account, - solana_instruction::{AccountMeta, Instruction}, + solana_instruction::Instruction, solana_rent::Rent, solana_svm_feature_set::SVMFeatureSet, + solana_system_program::system_processor::DEFAULT_COMPUTE_UNITS as SYSTEM_TRANSFER_CUS, + std::cell::RefCell, + test_case::test_case, }; - #[test] - fn test_system_program_exec() { - let system_program_id = solana_sdk_ids::system_program::id(); - let native_loader_id = solana_sdk_ids::native_loader::id(); + const NOOP_ELF: &[u8] = + include_bytes!("../../../programs/bpf_loader/test_elfs/out/noop_aligned.so"); - let from_pubkey = Pubkey::new_from_array([1u8; 32]); - let to_pubkey = Pubkey::new_from_array([2u8; 32]); + const FROM_BASE_LAMPORTS: u64 = 5_000; + const TO_BASE_LAMPORTS: u64 = 1_000; - let cu_avail = 10000u64; - let slot = 10; - let feature_set = SVMFeatureSet::default(); + #[derive(Default)] + struct CountingCallback { + // Just a simple little mock so we can test our callback is being used. + precompile_checks: RefCell, + } - // Build the instruction context. - let context = InstrContext { - feature_set, - accounts: vec![ - ( - from_pubkey, - Account { - lamports: 1000, - data: vec![], - owner: system_program_id, - executable: false, - rent_epoch: u64::MAX, - }, - ), - ( - to_pubkey, - Account { - lamports: 0, - data: vec![], - owner: system_program_id, - executable: false, - rent_epoch: u64::MAX, - }, - ), - ( - system_program_id, - Account { - lamports: 10000000, - data: b"Solana Program".to_vec(), - owner: native_loader_id, - executable: true, - rent_epoch: u64::MAX, - }, - ), - ], - instruction: Instruction { - program_id: system_program_id, - accounts: vec![ - AccountMeta { - pubkey: from_pubkey, - is_signer: true, - is_writable: true, - }, - AccountMeta { - pubkey: to_pubkey, - is_signer: false, - is_writable: true, - }, - ], - data: vec![ - // Transfer - 0x02, 0x00, 0x00, 0x00, // Lamports - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - ], - }, - cu_avail, - }; - - // Create Sysvar Cache. + impl InvokeContextCallback for CountingCallback { + fn is_precompile(&self, _program_id: &Pubkey) -> bool { + *self.precompile_checks.borrow_mut() += 1; + false + } + } + + fn system_account_with_lamports(lamports: u64) -> Account { + Account { + lamports, + data: vec![], + owner: solana_sdk_ids::system_program::id(), + executable: false, + rent_epoch: u64::MAX, + } + } + + fn sysvar_cache_with_rent() -> SysvarCache { let mut sysvar_cache = SysvarCache::default(); sysvar_cache.fill_missing_entries(|pubkey, callback| { if pubkey == &solana_sdk_ids::sysvar::rent::id() { - let rent = Rent::default(); - let rent_data = bincode::serialize(&rent).unwrap(); + let rent_data = bincode::serialize(&Rent::default()).unwrap(); callback(&rent_data); } }); + sysvar_cache + } - // Create Program Cache - let mut program_cache = new_program_cache_with_builtins(slot); + fn build_system_transfer_context(from: &Pubkey, to: &Pubkey, amount: u64) -> InstrContext { + let feature_set = SVMFeatureSet::default(); + let accounts = vec![ + ( + *from, + system_account_with_lamports(FROM_BASE_LAMPORTS + amount), + ), + (*to, system_account_with_lamports(TO_BASE_LAMPORTS)), + keyed_account_for_system_program(), + ]; + let instruction = solana_system_interface::instruction::transfer(from, to, amount); + InstrContext { + feature_set, + accounts, + instruction, + cu_avail: SYSTEM_TRANSFER_CUS, + } + } - let environments = create_program_runtime_environment( - &context.feature_set, - &ComputeBudget::new_with_defaults(feature_set.raise_cpi_nesting_limit_to_8).to_budget(), - false, /* deployment */ - false, /* debugging_features */ - ) - .unwrap(); + fn assert_system_transfer_effects( + effects: &InstrEffects, + from: &Pubkey, + to: &Pubkey, + amount: u64, + ) { + // Success + assert_eq!(effects.result, None); + assert_eq!(effects.custom_err, None); + // CUs exhausted + assert_eq!(effects.cu_avail, 0); + // Lamports transferred + assert_eq!( + effects.get_account(from).unwrap().lamports, + FROM_BASE_LAMPORTS + ); + assert_eq!( + effects.get_account(to).unwrap().lamports, + TO_BASE_LAMPORTS + amount + ); + } - fill_program_cache_from_accounts( - &mut program_cache, - &environments, - &context.accounts, - slot, - ) - .unwrap(); + #[test] + fn test_system_program_exec() { + let from = Pubkey::new_unique(); + let to = Pubkey::new_unique(); + let amount = 1_000; + let context = build_system_transfer_context(&from, &to, amount); + let sysvar_cache = sysvar_cache_with_rent(); + let mut program_cache = new_program_cache_with_builtins(0); - // Execute the instruction. let effects = execute_instr(&context, &mut program_cache, &sysvar_cache); + assert_system_transfer_effects(&effects, &from, &to, amount); + } + + #[test] + fn test_system_program_exec_with_callback() { + let from = Pubkey::new_unique(); + let to = Pubkey::new_unique(); + let amount = 1_000; + let context = build_system_transfer_context(&from, &to, amount); + let sysvar_cache = sysvar_cache_with_rent(); + let mut program_cache = new_program_cache_with_builtins(0); + + let callback = CountingCallback::default(); + + let effects = + execute_instr_with_callback(&context, &callback, &mut program_cache, &sysvar_cache); + assert_system_transfer_effects(&effects, &from, &to, amount); + } + + #[test_case(solana_sdk_ids::bpf_loader_deprecated::id(); "loader_v1")] + #[test_case(solana_sdk_ids::bpf_loader::id(); "loader_v2")] + #[test_case(solana_sdk_ids::bpf_loader_upgradeable::id(); "loader_v3")] + fn test_bpf_noop_program_exec(loader_key: Pubkey) { + let program_id = Pubkey::new_unique(); + let context = InstrContext::new_with_default_budget( + SVMFeatureSet::default(), + vec![], + Instruction::new_with_bytes(program_id, &[], vec![]), + ); + let sysvar_cache = sysvar_cache_with_rent(); - // Verify the results. + let mut program_cache = new_program_cache_with_builtins(0); + add_program_to_program_cache( + &mut program_cache, + &program_id, + &loader_key, + NOOP_ELF, + &context.feature_set, + ); + + let effects = execute_instr(&context, &mut program_cache, &sysvar_cache); assert_eq!(effects.result, None); assert_eq!(effects.custom_err, None); - assert_eq!(effects.cu_avail, 9850u64); - assert_eq!(effects.return_data, Vec::::new(),); - - // Verify account changes. - let from_account = effects - .resulting_accounts - .iter() - .find(|(k, _)| k == &from_pubkey) - .unwrap(); - assert_eq!(from_account.1.lamports, 999); - - let to_account = effects - .resulting_accounts - .iter() - .find(|(k, _)| k == &to_pubkey) - .unwrap(); - assert_eq!(to_account.1.lamports, 1); } } From b54f7de2d3aeac1a56dbfbb72a17ec36fc1986e9 Mon Sep 17 00:00:00 2001 From: Brennan Date: Thu, 28 May 2026 10:16:23 -0700 Subject: [PATCH 106/576] Bank Slot Time (#12723) slot params --- runtime/src/bank.rs | 140 +++++++++++++++++++++++++------- runtime/src/bank/tests.rs | 84 ++++++++++++++++++- runtime/src/lib.rs | 1 + runtime/src/slot_params.rs | 162 +++++++++++++++++++++++++++++++++++++ 4 files changed, 356 insertions(+), 31 deletions(-) create mode 100644 runtime/src/slot_params.rs diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 4bdaacf3918..9cc3c9de3f6 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -33,6 +33,11 @@ //! It offers a high-level API that signs transactions //! on behalf of the caller, and a low-level API for when they have //! already been signed and verified. +pub use { + crate::slot_params::DEFAULT_MAX_ENTRY_BYTES_PER_SLOT, + partitioned_epoch_rewards::KeyedRewardsAndNumPartitions, solana_leader_schedule::SlotLeader, + solana_reward_info::RewardType, +}; use { crate::{ account_saver::collect_accounts_to_store, @@ -56,6 +61,7 @@ use { rent_collector::RentCollector, reward_info::RewardInfo, runtime_config::RuntimeConfig, + slot_params::{SlotParams, SlotParamsArchive}, stake_account::StakeAccount, stake_history::StakeHistory as CowStakeHistory, stake_weighted_timestamp::{ @@ -107,11 +113,7 @@ use { solana_cluster_type::ClusterType, solana_compute_budget::compute_budget::ComputeBudget, solana_compute_budget_instruction::instructions_processor::process_compute_budget_instructions, - solana_cost_model::{ - block_cost_limits::simd_0286_block_limit, - cost_tracker::CostTracker, - shred_limit::{DEFAULT_MAX_CODE_SHREDS_PER_SLOT, DEFAULT_MAX_DATA_SHREDS_PER_SLOT}, - }, + solana_cost_model::{block_cost_limits::simd_0286_block_limit, cost_tracker::CostTracker}, solana_epoch_info::EpochInfo, solana_epoch_schedule::EpochSchedule, solana_feature_gate_interface as feature, @@ -222,10 +224,6 @@ use { solana_nonce_account::{SystemAccountKind, get_system_account_kind}, solana_program_runtime::sysvar_cache::SysvarCache, }; -pub use { - partitioned_epoch_rewards::KeyedRewardsAndNumPartitions, solana_leader_schedule::SlotLeader, - solana_reward_info::RewardType, -}; /// params to `verify_accounts_hash` struct VerifyAccountsHashConfig { @@ -611,6 +609,7 @@ impl PartialEq for Bank { ns_per_slot, genesis_creation_time, slots_per_year, + slot_params: _, slot, bank_id: _, epoch, @@ -738,9 +737,6 @@ impl BankFieldsToSerialize { pub enum RewardCalculationEvent<'a, 'b> { Staking(&'a Pubkey, &'b InflationPointCalculationEvent), } -/// Default maximum serialized entry bytes a bank may accept in one slot. -pub const DEFAULT_MAX_ENTRY_BYTES_PER_SLOT: u64 = 20 * 1024 * 1024; // 20 MiB - /// type alias is not supported for trait in rust yet. As a workaround, we define the /// `RewardCalcTracer` trait explicitly and implement it on any type that implement /// `Fn(&RewardCalculationEvent) + Send + Sync`. @@ -884,6 +880,9 @@ pub struct Bank { /// The number of slots per year, used for inflation slots_per_year: f64, + /// Slot-scoped parameter history used for slot-relative parameter lookups. + slot_params: SlotParamsArchive, + /// Bank slot (i.e. block) slot: Slot, @@ -1186,6 +1185,7 @@ impl Bank { ns_per_slot: u128::default(), genesis_creation_time: UnixTimestamp::default(), slots_per_year: f64::default(), + slot_params: SlotParamsArchive::default(), slot: Slot::default(), bank_id: BankId::default(), epoch: Epoch::default(), @@ -1416,6 +1416,7 @@ impl Bank { ns_per_slot: parent.ns_per_slot, genesis_creation_time: parent.genesis_creation_time, slots_per_year: parent.slots_per_year, + slot_params: parent.slot_params.clone(), epoch_schedule, rent_collector: Self::get_rent_collector_from(&parent.rent_collector, epoch), max_tick_height: slot @@ -2106,6 +2107,7 @@ impl Bank { ns_per_slot: fields.ns_per_slot, genesis_creation_time: fields.genesis_creation_time, slots_per_year: fields.slots_per_year, + slot_params: SlotParamsArchive::default(), slot, bank_id: 0, epoch, @@ -2184,6 +2186,11 @@ impl Bank { ); assert_eq!(bank.epoch_schedule, genesis_config.epoch_schedule); + bank.refresh_slot_params_with_baseline(Self::genesis_config_slot_params( + genesis_config, + bank.partitioned_rewards_stake_account_stores_per_block, + )); + bank.initialize_after_snapshot_restore(|| rewards_calculation_thread_pool); datapoint_info!( @@ -2669,16 +2676,76 @@ impl Bank { }); } + /// Rebuilds cached slot params while preserving the supplied slot-0 baseline. + /// + /// The cache is not serialized into snapshots; it is reconstructed from + /// existing Bank fields during genesis and snapshot restore. + fn refresh_slot_params_with_baseline(&mut self, baseline_params: SlotParams) { + self.slot_params = SlotParamsArchive::new(&self.epoch_schedule, baseline_params); + } + + /// Builds slot-0 params from the genesis config. + fn genesis_config_slot_params( + genesis_config: &GenesisConfig, + partitioned_rewards_stake_account_stores_per_block: u64, + ) -> SlotParams { + SlotParams::genesis_baseline( + genesis_config.ns_per_slot(), + genesis_config.slots_per_year(), + genesis_config.hashes_per_tick(), + partitioned_rewards_stake_account_stores_per_block, + ) + } + + /// Returns the slot params effective at `slot`. + fn slot_params_at_slot(&self, slot: Slot) -> SlotParams { + self.slot_params.params_at_slot(slot) + } + + /// Returns the effective slot duration for `slot`. + pub fn ns_per_slot_at_slot(&self, slot: Slot) -> u128 { + self.slot_params_at_slot(slot).ns_per_slot() + } + + /// Returns slots/year for the slot params active at `epoch` start. + fn slots_per_year_for_epoch(&self, epoch: Epoch) -> f64 { + let first_slot = self.epoch_schedule().get_first_slot_in_epoch(epoch); + self.slot_params_at_slot(first_slot).slots_per_year() + } + + /// Returns the wall-clock duration in years for `[start_slot, end_slot)`. + fn slot_range_duration_in_years(&self, start_slot: Slot, end_slot: Slot) -> f64 { + if start_slot >= end_slot { + return 0.0; + } + + let mut cursor = start_slot; + let mut params = self.slot_params.baseline_params(); + let mut duration = 0.0; + + for (effective_slot, effective_params) in self.slot_params.param_transitions() { + if *effective_slot <= start_slot { + params = *effective_params; + continue; + } + if *effective_slot >= end_slot { + break; + } + + duration += (*effective_slot - cursor) as f64 / params.slots_per_year(); + cursor = *effective_slot; + params = *effective_params; + } + + duration + (end_slot - cursor) as f64 / params.slots_per_year() + } + pub fn epoch_duration_in_years(&self, epoch: Epoch) -> f64 { // period: time that has passed as a fraction of a year, basically the length of // an epoch as a fraction of a year // calculated as: slots_elapsed / (slots / year) - self.epoch_schedule().get_slots_in_epoch(epoch) as f64 / self.slots_per_year - } - - /// Returns the slot duration to use for `slot`. - pub fn ns_per_slot_at_slot(&self, _slot: Slot) -> u128 { - self.ns_per_slot + self.epoch_schedule().get_slots_in_epoch(epoch) as f64 + / self.slots_per_year_for_epoch(epoch) } pub fn max_processing_age(&self) -> usize { @@ -2705,22 +2772,27 @@ impl Bank { }) } + /// Returns slots since inflation started, aligned to the first slot used for rewards accrual. fn get_inflation_num_slots(&self) -> u64 { + let inflation_start_slot = self.inflation_start_slot_aligned_to_rewards(); + self.epoch_schedule().get_first_slot_in_epoch(self.epoch()) - inflation_start_slot + } + + /// Returns the inflation rewards start slot aligned to an epoch boundary. + fn inflation_start_slot_aligned_to_rewards(&self) -> Slot { let inflation_activation_slot = self.get_inflation_start_slot(); - // Normalize inflation_start to align with the start of rewards accrual. - let inflation_start_slot = self.epoch_schedule().get_first_slot_in_epoch( + self.epoch_schedule().get_first_slot_in_epoch( self.epoch_schedule() .get_epoch(inflation_activation_slot) .saturating_sub(1), - ); - self.epoch_schedule().get_first_slot_in_epoch(self.epoch()) - inflation_start_slot + ) } + /// Returns elapsed inflation time in years for slots since inflation started. pub fn slot_in_year_for_inflation(&self) -> f64 { let num_slots = self.get_inflation_num_slots(); - - // calculated as: num_slots / (slots / year) - num_slots as f64 / self.slots_per_year + let inflation_start_slot = self.inflation_start_slot_aligned_to_rewards(); + self.slot_range_duration_in_years(inflation_start_slot, inflation_start_slot + num_slots) } /// For a given `capitalization` (total_supply in lamports) and `epoch`, returns the @@ -2987,6 +3059,10 @@ impl Bank { self.slots_per_year = genesis_config.slots_per_year(); self.epoch_schedule = genesis_config.epoch_schedule.clone(); + self.refresh_slot_params_with_baseline(Self::genesis_config_slot_params( + genesis_config, + self.partitioned_rewards_stake_account_stores_per_block, + )); self.inflation = Arc::new(RwLock::new(genesis_config.inflation)); @@ -4888,16 +4964,16 @@ impl Bank { /// /// Limit changes are delayed by an epoch, so a root bank can derive the /// limit for any slot inside the shred intake window. - pub fn max_data_shreds_per_slot_for_slot(&self, _slot: Slot) -> u32 { - DEFAULT_MAX_DATA_SHREDS_PER_SLOT + pub fn max_data_shreds_per_slot_for_slot(&self, slot: Slot) -> u32 { + self.slot_params_at_slot(slot).max_data_shreds_per_slot() } /// Returns the code shred limit applicable to `slot`. /// /// Limit changes are delayed by an epoch, so a root bank can derive the /// limit for any slot inside the shred intake window. - pub fn max_code_shreds_per_slot_for_slot(&self, _slot: Slot) -> u32 { - DEFAULT_MAX_CODE_SHREDS_PER_SLOT + pub fn max_code_shreds_per_slot_for_slot(&self, slot: Slot) -> u32 { + self.slot_params_at_slot(slot).max_code_shreds_per_slot() } pub fn max_entry_bytes_per_slot(&self) -> u64 { @@ -6448,6 +6524,12 @@ impl Bank { bank.transaction_processor = TransactionBatchProcessor::new_uninitialized(slot, epoch); bank.accounts_lt_hash = Mutex::new(fields.accounts_lt_hash); bank.bank_hash_stats = AtomicBankHashStats::new(&fields.bank_hash_stats); + bank.refresh_slot_params_with_baseline(SlotParams::genesis_baseline( + bank.ns_per_slot, + bank.slots_per_year, + bank.hashes_per_tick, + bank.partitioned_rewards_stake_account_stores_per_block, + )); bank } diff --git a/runtime/src/bank/tests.rs b/runtime/src/bank/tests.rs index a0f7ad586af..14e9d350479 100644 --- a/runtime/src/bank/tests.rs +++ b/runtime/src/bank/tests.rs @@ -19,6 +19,7 @@ use { }, runtime_config::RuntimeConfig, serde_snapshot::fields_from_stream, + slot_params::{DEFAULT_MAX_ENTRY_BYTES_PER_SLOT, LEGACY_SLOT_PARAMS}, stake_history::StakeHistory, stake_utils, stakes::{DeserializableStakes, InvalidCacheEntryReason, SerdeStakesToStakeFormat, Stakes}, @@ -46,6 +47,7 @@ use { accounts_scan::ScanError, ancestors::Ancestors, blockhash_queue::BlockhashQueue, + partitioned_rewards::PartitionedEpochRewardsConfig, }, solana_client_traits::SyncClient, solana_clock::{ @@ -57,8 +59,11 @@ use { compute_budget::ComputeBudget, compute_budget_limits::ComputeBudgetLimits, }, solana_compute_budget_interface::ComputeBudgetInstruction, - solana_cost_model::block_cost_limits::{ - MAX_BLOCK_UNITS, MAX_BLOCK_UNITS_SIMD_0286, MAX_WRITABLE_ACCOUNT_UNITS, + solana_cost_model::{ + block_cost_limits::{ + MAX_BLOCK_UNITS, MAX_BLOCK_UNITS_SIMD_0286, MAX_WRITABLE_ACCOUNT_UNITS, + }, + shred_limit::{DEFAULT_MAX_CODE_SHREDS_PER_SLOT, DEFAULT_MAX_DATA_SHREDS_PER_SLOT}, }, solana_cpi::MAX_RETURN_DATA, solana_epoch_schedule::{EpochSchedule, MINIMUM_SLOTS_PER_EPOCH}, @@ -6598,6 +6603,81 @@ fn test_ns_per_slot() { ); } +#[test] +fn test_slot_params_use_genesis_baseline_without_features() { + let (mut genesis_config, _) = create_genesis_config(1_000_000); + genesis_config.poh_config.target_tick_duration = Duration::from_millis(10); + let genesis_ns_per_slot = genesis_config.ns_per_slot(); + let genesis_slots_per_year = genesis_config.slots_per_year(); + let stake_account_stores_per_block = 17; + let mut accounts_db_config = ACCOUNTS_DB_CONFIG_FOR_TESTING; + accounts_db_config.partitioned_epoch_rewards_config = + PartitionedEpochRewardsConfig::new_for_test(stake_account_stores_per_block); + + let bank = Bank::new_from_genesis( + &genesis_config, + Arc::::default(), + Vec::new(), + None, + accounts_db_config, + None, + None, + Arc::default(), + None, + None, + ); + let baseline_params = bank.slot_params.baseline_params(); + + assert_ne!(baseline_params, LEGACY_SLOT_PARAMS); + assert_eq!(bank.ns_per_slot, genesis_ns_per_slot); + assert_eq!(baseline_params.ns_per_slot(), genesis_ns_per_slot); + assert_eq!(bank.ns_per_slot_at_slot(bank.slot()), genesis_ns_per_slot); + assert_eq!( + bank.ns_per_slot_at_slot(bank.slot().saturating_add(1)), + genesis_ns_per_slot + ); + assert_eq!( + bank.slots_per_year.to_bits(), + genesis_slots_per_year.to_bits() + ); + assert_eq!( + baseline_params.slots_per_year().to_bits(), + genesis_slots_per_year.to_bits() + ); + assert_eq!( + baseline_params.hashes_per_tick(), + genesis_config.hashes_per_tick() + ); + assert_eq!( + bank.epoch_duration_in_years(0).to_bits(), + (bank.get_slots_in_epoch(0) as f64 / genesis_slots_per_year).to_bits() + ); + assert_eq!( + bank.slot_range_duration_in_years(0, 64).to_bits(), + (64.0 / genesis_slots_per_year).to_bits() + ); + assert_eq!( + bank.partitioned_rewards_stake_account_stores_per_block, + stake_account_stores_per_block + ); + assert_eq!( + baseline_params.partitioned_epoch_rewards_stake_account_stores_per_block(), + stake_account_stores_per_block + ); + assert_eq!( + bank.max_data_shreds_per_slot(), + DEFAULT_MAX_DATA_SHREDS_PER_SLOT + ); + assert_eq!( + bank.max_code_shreds_per_slot(), + DEFAULT_MAX_CODE_SHREDS_PER_SLOT + ); + assert_eq!( + bank.max_entry_bytes_per_slot(), + DEFAULT_MAX_ENTRY_BYTES_PER_SLOT + ); +} + #[test] fn test_timestamp_slow() { fn max_allowable_delta_since_epoch(bank: &Bank, max_allowable_drift: u32) -> i64 { diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 73c247dd727..300e3d2c1ff 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -29,6 +29,7 @@ pub mod rent_collector; mod reward_info; pub mod runtime_config; pub mod serde_snapshot; +pub mod slot_params; pub mod snapshot_bank_utils; pub mod snapshot_controller; pub mod snapshot_minimizer; diff --git a/runtime/src/slot_params.rs b/runtime/src/slot_params.rs new file mode 100644 index 00000000000..13f3c6a26be --- /dev/null +++ b/runtime/src/slot_params.rs @@ -0,0 +1,162 @@ +use {solana_clock::Slot, solana_epoch_schedule::EpochSchedule}; + +pub const DEFAULT_MAX_ENTRY_BYTES_PER_SLOT: u64 = 20 * 1024 * 1024; // 20 MiB + +/// Runtime parameters tied to a slot-time regime. +/// +/// This intentionally groups values that need to move together when slot time +/// changes. For now, only the legacy baseline exists, so routing through this +/// type preserves current behavior while giving future feature-gated changes a +/// single table-shaped home. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct SlotParams { + pub(crate) ns_per_slot: u128, + pub(crate) slots_per_year: f64, + pub(crate) hashes_per_tick: Option, + pub(crate) max_block_units: u64, + pub(crate) max_writable_account_units: u64, + pub(crate) max_vote_units: u64, + pub(crate) max_block_accounts_data_size_delta: u64, + pub(crate) max_data_shreds_per_slot: u32, + pub(crate) max_code_shreds_per_slot: u32, + pub(crate) max_entry_bytes_per_slot: u64, + pub(crate) partitioned_epoch_rewards_stake_account_stores_per_block: u64, +} + +impl SlotParams { + /// Builds the slot-0 params baseline from genesis-derived Bank fields. + /// + /// Genesis can customize timing values. Bank construction can also + /// customize the partitioned-rewards write budget. Values without an + /// explicit construction-time knob retain the legacy baseline. + pub(crate) fn genesis_baseline( + ns_per_slot: u128, + slots_per_year: f64, + hashes_per_tick: Option, + partitioned_epoch_rewards_stake_account_stores_per_block: u64, + ) -> Self { + Self { + ns_per_slot, + slots_per_year, + hashes_per_tick, + partitioned_epoch_rewards_stake_account_stores_per_block, + ..LEGACY_SLOT_PARAMS + } + } + + /// Nanoseconds per slot for these params. + pub const fn ns_per_slot(&self) -> u128 { + self.ns_per_slot + } + + /// Slots per year for these params. + pub const fn slots_per_year(&self) -> f64 { + self.slots_per_year + } + + /// PoH hashes per tick for these params. + pub const fn hashes_per_tick(&self) -> Option { + self.hashes_per_tick + } + + /// Maximum data shred index capacity for these params. + pub const fn max_data_shreds_per_slot(&self) -> u32 { + self.max_data_shreds_per_slot + } + + /// Maximum coding shred index capacity for these params. + pub const fn max_code_shreds_per_slot(&self) -> u32 { + self.max_code_shreds_per_slot + } + + /// Maximum bytes that can be reserved for entries in one slot. + pub const fn max_entry_bytes_per_slot(&self) -> u64 { + self.max_entry_bytes_per_slot + } + + /// Number of stake account reward stores allowed in one block. + pub const fn partitioned_epoch_rewards_stake_account_stores_per_block(&self) -> u64 { + self.partitioned_epoch_rewards_stake_account_stores_per_block + } + + /// Returns the per-bank cost limits for these params. + pub const fn cost_limits(self) -> (u64, u64, u64, u64) { + ( + self.max_writable_account_units, + self.max_block_units, + self.max_vote_units, + self.max_block_accounts_data_size_delta, + ) + } +} + +pub(crate) const LEGACY_SLOT_PARAMS: SlotParams = SlotParams { + ns_per_slot: 400_000_000, + slots_per_year: 78_892_314.984, + hashes_per_tick: Some(62_500), + max_block_units: 60_000_000, + max_writable_account_units: 24_000_000, + max_vote_units: 36_000_000, + max_block_accounts_data_size_delta: 100_000_000, + max_data_shreds_per_slot: 32_768, + max_code_shreds_per_slot: 32_768, + max_entry_bytes_per_slot: 20 * 1024 * 1024, + partitioned_epoch_rewards_stake_account_stores_per_block: 4096, +}; + +/// Bank-local archive of slot-parameter transitions. +/// +/// This is rebuilt from feature accounts on startup instead of being serialized +/// into snapshots. The source of truth remains the feature set; this cache only +/// avoids repeatedly scanning, collecting, and sorting slot-time gates once they +/// are added. +#[derive(Clone, Debug)] +pub(crate) struct SlotParamsArchive { + /// Sorted `(effective_slot, params)` transitions, including baseline at slot 0. + /// + /// This is used when a bank, usually the root bank, must answer "which + /// parameters apply to some other slot?" Examples include shred filtering + /// for incoming shreds and inflation calculations that span historical slot + /// ranges. + param_transitions: Vec<(Slot, SlotParams)>, +} + +impl Default for SlotParamsArchive { + fn default() -> Self { + Self::new(&EpochSchedule::default(), LEGACY_SLOT_PARAMS) + } +} + +impl SlotParamsArchive { + /// Rebuilds slot-parameter transitions from the supplied baseline. + /// + /// `_epoch_schedule` is accepted now so the call sites already have the + /// right shape for delayed, epoch-boundary-effective feature transitions. + pub(crate) fn new(_epoch_schedule: &EpochSchedule, baseline_params: SlotParams) -> Self { + Self { + param_transitions: vec![(0, baseline_params)], + } + } + + /// Returns the baseline params supplied at genesis or snapshot restore. + pub(crate) fn baseline_params(&self) -> SlotParams { + self.param_transitions + .first() + .map(|(_, params)| *params) + .unwrap_or(LEGACY_SLOT_PARAMS) + } + + /// Returns the slot params effective at `slot`. + pub(crate) fn params_at_slot(&self, slot: Slot) -> SlotParams { + self.param_transitions + .iter() + .rev() + .find_map(|(effective_slot, params)| (*effective_slot <= slot).then_some(*params)) + .unwrap_or(LEGACY_SLOT_PARAMS) + } + + /// Returns sorted slot-parameter transitions. + pub(crate) fn param_transitions(&self) -> &[(Slot, SlotParams)] { + &self.param_transitions + } +} From 0b2fec10dab4767928972b27e281e233668fbd1b Mon Sep 17 00:00:00 2001 From: Alex Pyattaev Date: Thu, 28 May 2026 22:26:52 +0300 Subject: [PATCH 107/576] Bound dupshred notification channel (#12790) bound dupshred notification channel --- core/src/tvu.rs | 4 +++- core/src/window_service.rs | 2 +- gossip/src/duplicate_shred_handler.rs | 20 +++++++++++++------- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/core/src/tvu.rs b/core/src/tvu.rs index ac2c77faff9..d8a6bf7640f 100644 --- a/core/src/tvu.rs +++ b/core/src/tvu.rs @@ -410,7 +410,9 @@ impl Tvu { ); let (ancestor_duplicate_slots_sender, ancestor_duplicate_slots_receiver) = unbounded(); - let (duplicate_slots_sender, duplicate_slots_receiver) = unbounded(); + // This channel is used by both gossip and window service. Gossip will not fill this + // beyond 50%, while window_service will perform a blocking send. + let (duplicate_slots_sender, duplicate_slots_receiver) = bounded(2048); let (ancestor_hashes_replay_update_sender, ancestor_hashes_replay_update_receiver) = unbounded(); let (dumped_slots_sender, dumped_slots_receiver) = unbounded(); diff --git a/core/src/window_service.rs b/core/src/window_service.rs index 9ec0ad2c90d..7d1ff101c71 100644 --- a/core/src/window_service.rs +++ b/core/src/window_service.rs @@ -172,7 +172,7 @@ fn run_check_duplicate( if !migration_status.is_alpenglow_enabled() { // The state machine can be exited as soon as alpenglow is enabled. - // Notify duplicate consensus state machine + // Notify duplicate consensus state machine. If channel is full we wait. duplicate_slots_sender.send(shred_slot)?; } diff --git a/gossip/src/duplicate_shred_handler.rs b/gossip/src/duplicate_shred_handler.rs index d4b67c4a309..3389bac5f7f 100644 --- a/gossip/src/duplicate_shred_handler.rs +++ b/gossip/src/duplicate_shred_handler.rs @@ -131,10 +131,16 @@ impl DuplicateShredHandler { shred1.into_payload(), shred2.into_payload(), )?; - // Notify duplicate consensus state machine - self.duplicate_slots_sender - .send(slot) - .map_err(|_| Error::DuplicateSlotSenderFailure)?; + + // Notify duplicate consensus state machine. Drop if channel is over 50% full + // to avoid blocking replay. + if self.duplicate_slots_sender.len() * 2 + < self.duplicate_slots_sender.capacity().unwrap_or(usize::MAX) + { + self.duplicate_slots_sender + .try_send(slot) + .map_err(|_| Error::DuplicateSlotSenderFailure)?; + } } self.consumed.insert(slot, true); } @@ -213,7 +219,7 @@ mod tests { epoch_specs::TestEpochSpecs, protocol::DUPLICATE_SHRED_MAX_PAYLOAD_SIZE, }, - crossbeam_channel::unbounded, + crossbeam_channel::bounded, itertools::Itertools, solana_keypair::Keypair, solana_ledger::{ @@ -305,7 +311,7 @@ mod tests { let leader_schedule_cache = Arc::new(LeaderScheduleCache::new_from_bank( &bank_forks_arc.read().unwrap().working_bank(), )); - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); let start_slot: Slot = 10; let mut duplicate_shred_handler = DuplicateShredHandler::new( @@ -409,7 +415,7 @@ mod tests { let leader_schedule_cache = Arc::new(LeaderScheduleCache::new_from_bank( &bank_forks_arc.read().unwrap().working_bank(), )); - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); let mut duplicate_shred_handler = DuplicateShredHandler::new( blockstore.clone(), leader_schedule_cache, From d1f38f6b24659575c40e15c85c296cc4b34ab001 Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Thu, 28 May 2026 15:28:13 -0400 Subject: [PATCH 108/576] votor: fix standstill event slot when no finalization is present (#12807) --- core/src/bls_sigverify/bls_sigverifier.rs | 2 +- votor/src/consensus_pool_service.rs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/core/src/bls_sigverify/bls_sigverifier.rs b/core/src/bls_sigverify/bls_sigverifier.rs index fbdd629401f..87627252f9c 100644 --- a/core/src/bls_sigverify/bls_sigverifier.rs +++ b/core/src/bls_sigverify/bls_sigverifier.rs @@ -241,7 +241,7 @@ impl SigVerifier { } } ConsensusMessage::Certificate(cert) => { - if cert.cert_type.slot() <= root_slot { + if cert.cert_type.slot() < root_slot { self.stats.num_old_certs_received += 1; continue; } diff --git a/votor/src/consensus_pool_service.rs b/votor/src/consensus_pool_service.rs index 1e31b13b216..64d0756d655 100644 --- a/votor/src/consensus_pool_service.rs +++ b/votor/src/consensus_pool_service.rs @@ -296,7 +296,9 @@ impl ConsensusPoolService { // Genesis cert though. if kick_off_parent_ready { events.push(VotorEvent::Standstill( - consensus_pool.highest_finalized_slot().unwrap_or(0), + consensus_pool + .highest_finalized_slot() + .unwrap_or(ctx.sharable_banks.root().slot()), )); } stats.standstill = true; From 05cdbd1b36c461a7e08b2b70c4a6f66398cc2013 Mon Sep 17 00:00:00 2001 From: Akhilesh Singhania Date: Thu, 28 May 2026 21:43:50 +0200 Subject: [PATCH 109/576] BCL: Improved deadlock prevention in using the reward cert channels (#12763) --- core/src/block_creation_loop.rs | 128 +++------------ .../reward_certs_handler.rs | 147 ++++++++++++++++++ core/src/bls_sigverify/bls_sigverifier.rs | 3 +- core/src/bls_sigverify/bls_vote_sigverify.rs | 6 +- core/src/bls_sigverify/utils.rs | 7 +- core/src/lib.rs | 2 +- core/src/tvu.rs | 7 +- core/src/validator.rs | 15 +- votor-messages/src/reward_certificate.rs | 34 ---- votor/src/consensus_rewards.rs | 89 ++++++++--- votor/src/votor.rs | 10 +- 11 files changed, 257 insertions(+), 191 deletions(-) create mode 100644 core/src/block_creation_loop/reward_certs_handler.rs diff --git a/core/src/block_creation_loop.rs b/core/src/block_creation_loop.rs index cfe6947d9ca..1106dfceb13 100644 --- a/core/src/block_creation_loop.rs +++ b/core/src/block_creation_loop.rs @@ -7,17 +7,15 @@ use { crate::{ banking_trace::{BankingPacketSender, BankingTracer}, + block_creation_loop::reward_certs_handler::RewardCertsHandler, replay_stage::{Finalizer, ReplayStage}, }, - agave_votor::event::LeaderWindowInfo, + agave_votor::{consensus_rewards::BuildRewardCertsRespSucc, event::LeaderWindowInfo}, agave_votor_messages::{ consensus_message::Block, - reward_certificate::{ - BuildRewardCertsRequest, BuildRewardCertsRespSucc, BuildRewardCertsResponse, - NotarRewardCertificate, SkipRewardCertificate, - }, + reward_certificate::{NotarRewardCertificate, SkipRewardCertificate}, }, - crossbeam_channel::{Receiver, RecvTimeoutError, Sender, select_biased}, + crossbeam_channel::{Receiver, select_biased}, solana_clock::Slot, solana_entry::block_component::{ BlockFooterV1, GenesisCertificate, UpdateParentV1, VersionedBlockMarker, @@ -56,6 +54,7 @@ use { thiserror::Error, }; +pub(crate) mod reward_certs_handler; mod stats; /// Source of a leader-window notification consumed by BCL. @@ -115,10 +114,7 @@ pub struct BlockCreationLoopConfig { pub record_receiver_receiver: Receiver, pub optimistic_parent_receiver: Receiver, - /// Channel to send the request to build reward certs. - pub build_reward_certs_sender: Sender, - /// Channel to receive the built reward certs. - pub reward_certs_receiver: Receiver, + pub(crate) reward_certs_handler: RewardCertsHandler, /// Sender for packets to banking stage (used to re-inject transactions after sad leader handover). pub banking_stage_sender: BankingPacketSender, @@ -145,8 +141,7 @@ struct LeaderContext { slot_status_notifier: Option, banking_tracer: Arc, replay_highest_frozen: Arc, - build_reward_certs_sender: Sender, - reward_certs_receiver: Receiver, + reward_certs_handler: RewardCertsHandler, /// Banking-stage ingress used to reschedule transactions after sad handover. banking_stage_sender: BankingPacketSender, @@ -226,8 +221,7 @@ fn start_loop(config: BlockCreationLoopConfig) { replay_highest_frozen, highest_parent_ready, optimistic_parent_receiver, - build_reward_certs_sender, - reward_certs_receiver, + reward_certs_handler, highest_finalized, banking_stage_sender, } = config; @@ -276,8 +270,7 @@ fn start_loop(config: BlockCreationLoopConfig) { slot_status_notifier, banking_tracer, replay_highest_frozen, - build_reward_certs_sender, - reward_certs_receiver, + reward_certs_handler, banking_stage_sender, metrics: LoopMetrics::default(), slot_metrics: SlotMetrics::default(), @@ -629,10 +622,9 @@ fn record_and_complete_block( block_timer: &mut Instant, block_timeout: Duration, ) -> Result<(), PohRecorderError> { - drain_stale_reward_certs(ctx, bank_slot); - ctx.build_reward_certs_sender - .send(BuildRewardCertsRequest { bank_slot }) - .map_err(|_| PohRecorderError::ChannelDisconnected)?; + let reward_cert_request = ctx + .reward_certs_handler + .request_reward_certs(ctx.my_pubkey, bank_slot)?; let mut accumulated_txs = vec![]; let mut records_shutdown = false; let window_has_moved_on = loop { @@ -751,7 +743,9 @@ fn record_and_complete_block( bank.set_tick_height(max_tick_height - 1); let footer = { - let reward_certs = recv_reward_certs(ctx, bank_slot)?; + let reward_certs = ctx + .reward_certs_handler + .recv_reward_certs(ctx.my_pubkey, reward_cert_request)?; let BuildRewardCertsRespSucc { skip, notar, @@ -827,49 +821,6 @@ fn process_parent_ready( Ok(false) } -/// Drop reward-certificate responses left over from an older bank slot. -fn drain_stale_reward_certs(ctx: &LeaderContext, bank_slot: Slot) { - for reward_cert in ctx.reward_certs_receiver.try_iter() { - warn!( - "{}: dropping stale reward cert response for bank slot {}, before starting {bank_slot}", - ctx.my_pubkey, reward_cert.bank_slot - ); - } -} - -/// Wait for the reward-certificate response for `bank_slot`, ignoring stale -/// responses generated for banks that have already been abandoned. -fn recv_reward_certs( - ctx: &LeaderContext, - bank_slot: Slot, -) -> Result { - loop { - if ctx.exit.load(Ordering::Relaxed) { - return Err(PohRecorderError::ChannelDisconnected); - } - - match ctx - .reward_certs_receiver - .recv_timeout(Duration::from_millis(100)) - { - Ok(reward_certs) if reward_certs.bank_slot == bank_slot => { - break reward_certs.result.map_err(PohRecorderError::from); - } - Ok(reward_certs) => { - warn!( - "{}: ignoring stale reward cert response for bank slot {}, expected \ - {bank_slot}", - ctx.my_pubkey, reward_certs.bank_slot - ); - } - Err(RecvTimeoutError::Timeout) => continue, - Err(RecvTimeoutError::Disconnected) => { - return Err(PohRecorderError::ChannelDisconnected); - } - } - } -} - /// Stop record intake, drain any already-reserved records, and reset PoH after /// failing to complete the working bank. fn abort_failed_working_bank( @@ -1358,7 +1309,7 @@ mod tests { super::*, crate::banking_trace::BankingTracer, agave_banking_stage_ingress_types::BankingPacketReceiver, - agave_votor_messages::reward_certificate::{BuildRewardCertsRespError, RewardCertError}, + agave_votor::consensus_rewards::BuildRewardCertsResponse, crossbeam_channel::unbounded, solana_bls_signatures::{BLS_SIGNATURE_AFFINE_SIZE, Signature as BLSSignature}, solana_entry::{block_component::VersionedUpdateParent, entry_or_marker::EntryOrMarker}, @@ -1551,10 +1502,9 @@ mod tests { let (_record_sender, record_receiver) = record_channels(false); let (_leader_window_info_sender, leader_window_info_receiver) = unbounded(); - let (build_reward_certs_sender, _build_reward_certs_receiver) = unbounded(); - let (reward_certs_sender, reward_certs_receiver) = unbounded(); let (banking_stage_sender, _banking_stage_receiver) = BankingTracer::channel_for_test(); let bank_forks_controller = test_bank_forks_controller(bank_forks.clone()); + let (reward_certs_handler, _receiver) = RewardCertsHandler::new(); let mut ctx = LeaderContext { exit, @@ -1573,8 +1523,7 @@ mod tests { slot_status_notifier: None, banking_tracer: BankingTracer::new_disabled(), replay_highest_frozen: Arc::new(ReplayHighestFrozen::default()), - build_reward_certs_sender, - reward_certs_receiver, + reward_certs_handler, banking_stage_sender, metrics: LoopMetrics::default(), slot_metrics: SlotMetrics::default(), @@ -1619,27 +1568,6 @@ mod tests { assert!(ctx.bank_forks.read().unwrap().get(1).is_none()); assert!(ctx.record_receiver.is_shutdown()); assert!(ctx.record_receiver.is_safe_to_restart()); - - reward_certs_sender - .send(BuildRewardCertsResponse { - bank_slot: 1, - result: Err(BuildRewardCertsRespError::RewardCertTryNew( - RewardCertError::InvalidBitmap, - )), - }) - .unwrap(); - reward_certs_sender - .send(BuildRewardCertsResponse { - bank_slot: 2, - result: Ok(BuildRewardCertsRespSucc { - validators: vec![my_pubkey], - ..BuildRewardCertsRespSucc::default() - }), - }) - .unwrap(); - - let reward_certs = recv_reward_certs(&ctx, 2).unwrap(); - assert_eq!(reward_certs.validators, vec![my_pubkey]); } #[test] @@ -1684,12 +1612,11 @@ mod tests { let (_record_sender, record_receiver) = record_channels(false); let (_leader_window_info_sender, leader_window_info_receiver) = unbounded(); - let (build_reward_certs_sender, _build_reward_certs_receiver) = unbounded(); - let (_reward_certs_sender, reward_certs_receiver) = unbounded(); let (banking_stage_sender, _banking_stage_receiver) = BankingTracer::channel_for_test(); let mut genesis_cert = test_genesis_certificate(); genesis_cert.slot = parent_bank.slot(); let bank_forks_controller = test_bank_forks_controller(bank_forks.clone()); + let (reward_certs_handler, _receiver) = RewardCertsHandler::new(); let mut ctx = LeaderContext { exit, @@ -1708,8 +1635,7 @@ mod tests { slot_status_notifier: None, banking_tracer: BankingTracer::new_disabled(), replay_highest_frozen: Arc::new(ReplayHighestFrozen::default()), - build_reward_certs_sender, - reward_certs_receiver, + reward_certs_handler, banking_stage_sender, metrics: LoopMetrics::default(), slot_metrics: SlotMetrics::default(), @@ -1757,10 +1683,10 @@ mod tests { let (record_sender, record_receiver) = record_channels(false); let (_leader_window_info_sender, leader_window_info_receiver) = unbounded(); - let (build_reward_certs_sender, build_reward_certs_receiver) = unbounded(); - let (reward_certs_sender, reward_certs_receiver) = unbounded(); + let (reward_certs_sender, _reward_certs_receiver) = unbounded(); let (banking_stage_sender, _banking_stage_receiver) = BankingTracer::channel_for_test(); let bank_forks_controller = test_bank_forks_controller(bank_forks.clone()); + let (reward_certs_handler, build_reward_certs_receiver) = RewardCertsHandler::new(); let mut ctx = LeaderContext { exit, @@ -1779,8 +1705,7 @@ mod tests { slot_status_notifier: None, banking_tracer: BankingTracer::new_disabled(), replay_highest_frozen: Arc::new(ReplayHighestFrozen::default()), - build_reward_certs_sender, - reward_certs_receiver, + reward_certs_handler, banking_stage_sender, metrics: LoopMetrics::default(), slot_metrics: SlotMetrics::default(), @@ -1800,7 +1725,6 @@ mod tests { let delayed_reward = std::thread::spawn(move || { std::thread::sleep(Duration::from_millis(300)); let _ = reward_certs_sender.send(BuildRewardCertsResponse { - bank_slot: 1, result: Ok(BuildRewardCertsRespSucc::default()), }); }); @@ -1879,10 +1803,9 @@ mod tests { let (record_sender, record_receiver) = record_channels(false); let (leader_window_info_sender, leader_window_info_receiver) = unbounded(); - let (build_reward_certs_sender, _build_reward_certs_receiver) = unbounded(); - let (_reward_certs_sender, reward_certs_receiver) = unbounded(); let (banking_stage_sender, banking_stage_receiver) = BankingTracer::channel_for_test(); let bank_forks_controller = test_bank_forks_controller(bank_forks.clone()); + let (reward_certs_handler, _receiver) = RewardCertsHandler::new(); let mut ctx = LeaderContext { exit, @@ -1901,8 +1824,7 @@ mod tests { slot_status_notifier: None, banking_tracer: BankingTracer::new_disabled(), replay_highest_frozen: Arc::new(ReplayHighestFrozen::default()), - build_reward_certs_sender, - reward_certs_receiver, + reward_certs_handler, banking_stage_sender, metrics: LoopMetrics::default(), slot_metrics: SlotMetrics::default(), diff --git a/core/src/block_creation_loop/reward_certs_handler.rs b/core/src/block_creation_loop/reward_certs_handler.rs new file mode 100644 index 00000000000..6140b77ee64 --- /dev/null +++ b/core/src/block_creation_loop/reward_certs_handler.rs @@ -0,0 +1,147 @@ +use { + agave_votor::consensus_rewards::{ + BuildRewardCertsRequest, BuildRewardCertsRespSucc, BuildRewardCertsResponse, + }, + crossbeam_channel::{Receiver, Sender, TryRecvError, TrySendError, bounded}, + solana_clock::Slot, + solana_poh::poh_recorder::PohRecorderError, + solana_pubkey::Pubkey, +}; + +/// The request token to be used to receive responses on previously sent requests to build reward certs. +pub(super) struct RewardCertRequest { + reply_receiver: Receiver, +} + +/// Struct to handle requests to build rewards certs and receive the produced certs. +pub(crate) struct RewardCertsHandler { + sender: Sender, +} + +impl RewardCertsHandler { + pub(crate) fn new() -> (Self, Receiver) { + // If the rewards container is keeping up, we should only ever expect there to be 1 msg in + // flight. So using a fairly small capacity to allow the rewards container to catch up if + // it ever falls behind. + const CAPACITY: usize = 16; + + let (tx, rx) = bounded(CAPACITY); + (Self { sender: tx }, rx) + } + + /// Tries to send a message requesting a reward cert to be built. + /// + /// Does not block trying to send a message as we have a tight deadline to produce the block + /// and if the rewards container is not keeping up, then we would rather not include a rewards + /// cert than wait. + /// + /// Returns: + /// - `Ok(Some(RewardCertRequest))`` if sending succeeded. The `RewardCertRequest` can be + /// used to receive a response to the build the reward cert. + /// - `Ok(None)` if sending failed because the channel is full i.e. the rewards container is + /// not keeping up. + /// - `Err(PohRecorderError::ChannelDisconnected)` if the channel is to the rewards container + /// is disconnected. + pub(super) fn request_reward_certs( + &mut self, + my_pubkey: Pubkey, + bank_slot: Slot, + ) -> Result, PohRecorderError> { + let (reply_sender, reply_receiver) = bounded(1); + let request = BuildRewardCertsRequest { + bank_slot, + reply_sender, + }; + match self.sender.try_send(request) { + Ok(()) => Ok(Some(RewardCertRequest { reply_receiver })), + Err(TrySendError::Full(_)) => { + warn!( + "{my_pubkey} sending request to build reward cert for bank_slot={bank_slot} \ + failed, channel is full" + ); + Ok(None) + } + Err(TrySendError::Disconnected(_)) => Err(PohRecorderError::ChannelDisconnected), + } + } + + /// Tries to receive a response for an earlier request to build reward certs. + /// + /// Does not block trying to receive the response as we have a tight deadline to produce + /// the block and if the rewards container is not keeping up, we will rather not include any + /// reward certs than wait for it. + pub(super) fn recv_reward_certs( + &mut self, + my_pubkey: Pubkey, + request: Option, + ) -> Result { + match request { + None => Ok(BuildRewardCertsRespSucc::default()), + Some(request) => { + let msg = request.reply_receiver.try_recv(); + match msg { + Err(TryRecvError::Empty) => { + warn!("{my_pubkey} trying to receive cert failed, channel is empty"); + Ok(BuildRewardCertsRespSucc::default()) + } + Err(TryRecvError::Disconnected) => Err(PohRecorderError::ChannelDisconnected), + Ok(resp) => match resp.result { + Ok(res) => Ok(res), + Err(err) => { + error!("{my_pubkey} building reward cert failed with {err:?}"); + Ok(BuildRewardCertsRespSucc::default()) + } + }, + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_does_not_block() { + let my_pubkey = Pubkey::default(); + let (mut handler, _receiver) = RewardCertsHandler::new(); + handler.request_reward_certs(my_pubkey, 1).unwrap().unwrap(); + + let resp = handler.recv_reward_certs(my_pubkey, None).unwrap(); + assert_eq!(resp, BuildRewardCertsRespSucc::default()); + + { + let (_reply_sender, reply_receiver) = bounded(0); + let request = RewardCertRequest { reply_receiver }; + let resp = handler.recv_reward_certs(my_pubkey, Some(request)).unwrap(); + assert_eq!(resp, BuildRewardCertsRespSucc::default()); + } + } + + #[test] + fn test_basic_functionality() { + let my_pubkey = Pubkey::default(); + let (mut handler, receiver) = RewardCertsHandler::new(); + let request = handler.request_reward_certs(my_pubkey, 1).unwrap().unwrap(); + let validators = vec![Pubkey::new_unique(); 10]; + + let BuildRewardCertsRequest { + bank_slot: _, + reply_sender, + } = receiver.recv().unwrap(); + reply_sender + .send(BuildRewardCertsResponse { + result: Ok(BuildRewardCertsRespSucc { + skip: None, + notar: None, + validators: validators.clone(), + }), + }) + .unwrap(); + let resp = handler.recv_reward_certs(my_pubkey, Some(request)).unwrap(); + assert!(resp.skip.is_none()); + assert!(resp.notar.is_none()); + assert_eq!(resp.validators, validators); + } +} diff --git a/core/src/bls_sigverify/bls_sigverifier.rs b/core/src/bls_sigverify/bls_sigverifier.rs index 87627252f9c..4c605b4f362 100644 --- a/core/src/bls_sigverify/bls_sigverifier.rs +++ b/core/src/bls_sigverify/bls_sigverifier.rs @@ -10,13 +10,12 @@ use { crate::cluster_info_vote_listener::VerifiedVoterSlotsSender, agave_votor::{ consensus_metrics::ConsensusMetricsEventSender, - consensus_rewards::{self}, + consensus_rewards::{self, AddVoteMessage}, generated_cert_types::GeneratedCertTypes, }, agave_votor_messages::{ consensus_message::{CertificateType, ConsensusMessage, VoteMessage}, migration::MigrationStatus, - reward_certificate::AddVoteMessage, }, crossbeam_channel::{Receiver, RecvTimeoutError, Sender, TryRecvError}, rayon::{ThreadPool, ThreadPoolBuilder}, diff --git a/core/src/bls_sigverify/bls_vote_sigverify.rs b/core/src/bls_sigverify/bls_vote_sigverify.rs index 8b1b8c73f90..d44715c92d4 100644 --- a/core/src/bls_sigverify/bls_vote_sigverify.rs +++ b/core/src/bls_sigverify/bls_vote_sigverify.rs @@ -8,10 +8,12 @@ use { send_votes_to_metrics, send_votes_to_pool, send_votes_to_repair, send_votes_to_rewards, }, }, - agave_votor::{consensus_metrics::ConsensusMetricsEvent, consensus_rewards}, + agave_votor::{ + consensus_metrics::ConsensusMetricsEvent, + consensus_rewards::{self, AddVoteMessage}, + }, agave_votor_messages::{ consensus_message::{ConsensusMessage, VoteMessage}, - reward_certificate::AddVoteMessage, vote::Vote, }, rayon::{ diff --git a/core/src/bls_sigverify/utils.rs b/core/src/bls_sigverify/utils.rs index 720034a29d2..c996b1cf4b8 100644 --- a/core/src/bls_sigverify/utils.rs +++ b/core/src/bls_sigverify/utils.rs @@ -4,10 +4,11 @@ use { bls_sigverify::{errors::SigVerifyCertError, stats::SigVerifyCertStats}, cluster_info_vote_listener::VerifiedVoterSlotsSender, }, - agave_votor::consensus_metrics::{ConsensusMetricsEvent, ConsensusMetricsEventSender}, - agave_votor_messages::{ - consensus_message::ConsensusMessage, reward_certificate::AddVoteMessage, + agave_votor::{ + consensus_metrics::{ConsensusMetricsEvent, ConsensusMetricsEventSender}, + consensus_rewards::AddVoteMessage, }, + agave_votor_messages::consensus_message::ConsensusMessage, crossbeam_channel::{Sender, TrySendError}, solana_clock::Slot, solana_pubkey::Pubkey, diff --git a/core/src/lib.rs b/core/src/lib.rs index b0ec144ed7b..b3f6a506a2b 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -12,7 +12,7 @@ pub mod admin_rpc_post_init; pub mod banking_simulation; pub mod banking_stage; pub mod banking_trace; -mod block_creation_loop; +pub(crate) mod block_creation_loop; pub mod bls_sigverify; pub mod cluster_info_vote_listener; pub mod cluster_slots_service; diff --git a/core/src/tvu.rs b/core/src/tvu.rs index d8a6bf7640f..8a20447fcfb 100644 --- a/core/src/tvu.rs +++ b/core/src/tvu.rs @@ -30,6 +30,7 @@ use { }, agave_votor::{ consensus_metrics::MAX_IN_FLIGHT_CONSENSUS_EVENTS, + consensus_rewards::BuildRewardCertsRequest, event::{LatestSwitchRequest, LeaderWindowInfo, VotorEventReceiver, VotorEventSender}, generated_cert_types::GeneratedCertTypes, vote_history::VoteHistory, @@ -37,7 +38,6 @@ use { voting_service::{VotingService as BLSVotingService, VotingServiceOverride}, votor::{Votor, VotorConfig}, }, - agave_votor_messages::reward_certificate::{BuildRewardCertsRequest, BuildRewardCertsResponse}, crossbeam_channel::{Receiver, Sender, bounded, unbounded}, solana_client::connection_cache::ConnectionCache, solana_clock::Slot, @@ -190,7 +190,6 @@ pub struct AlpenglowInitializationState { pub voting_service_test_override: Option, // For rewards - pub reward_certs_sender: Sender, pub build_reward_certs_receiver: Receiver, } @@ -275,7 +274,6 @@ impl Tvu { voting_service_test_override, highest_finalized, build_reward_certs_receiver, - reward_certs_sender, } = votor_init; // streamer and sigverify for A2A BLS messages @@ -522,7 +520,6 @@ impl Tvu { event_sender: votor_event_sender.clone(), latest_switch_request: latest_switch_request.clone(), own_vote_sender: consensus_message_sender.clone(), - reward_certs_sender, repair_event_sender, event_receiver: votor_event_receiver, consensus_message_receiver, @@ -832,7 +829,6 @@ pub mod tests { thread::sleep(Duration::from_secs(1)); } }); - let (reward_certs_sender, _reward_certs_receiver) = bounded(1); let (_build_reward_certs_sender, build_reward_certs_receiver) = bounded(1); let (bank_forks_controller, bank_forks_controller_receiver) = BankForksControllerHandle::new(); @@ -911,7 +907,6 @@ pub mod tests { bank_forks_controller, bank_forks_controller_receiver, build_reward_certs_receiver, - reward_certs_sender, }, ) .expect("assume success"); diff --git a/core/src/validator.rs b/core/src/validator.rs index 8d4ae0752ae..34161549137 100644 --- a/core/src/validator.rs +++ b/core/src/validator.rs @@ -8,7 +8,10 @@ use { BankingStage, transaction_scheduler::scheduler_controller::SchedulerConfig, }, banking_trace::{self, BankingTracer, TraceError}, - block_creation_loop::{BlockCreationLoop, BlockCreationLoopConfig, ReplayHighestFrozen}, + block_creation_loop::{ + BlockCreationLoop, BlockCreationLoopConfig, ReplayHighestFrozen, + reward_certs_handler::RewardCertsHandler, + }, cluster_info_vote_listener::VoteTracker, completed_data_sets_service::CompletedDataSetsService, consensus::{ @@ -1491,10 +1494,8 @@ impl Validator { // small (but highly overprovisioned) number for performance and easier // debug if things go off the rails. let (optimistic_parent_sender, optimistic_parent_receiver) = bounded(100); - // There will only ever be a single msg in flight so bound channel for [`BuildRewardCertsRequest`] to 1 message. - let (build_reward_certs_sender, build_reward_certs_receiver) = bounded(1); - // There will only ever be a single msg in flight so bound channel for [`BuildRewardCertsResponse`] to 1 message. - let (reward_certs_sender, reward_certs_receiver) = bounded(1); + + let (reward_certs_handler, build_reward_certs_receiver) = RewardCertsHandler::new(); let banking_stage_sender_for_bcl = banking_tracer_channels.non_vote_sender.clone(); @@ -1515,8 +1516,7 @@ impl Validator { record_receiver_receiver, optimistic_parent_receiver: optimistic_parent_receiver.clone(), highest_finalized: highest_finalized.clone(), - build_reward_certs_sender, - reward_certs_receiver, + reward_certs_handler, banking_stage_sender: banking_stage_sender_for_bcl, }; let block_creation_loop = BlockCreationLoop::new(block_creation_loop_config); @@ -1672,7 +1672,6 @@ impl Validator { voting_service_test_override: config.voting_service_test_override.clone(), highest_finalized, build_reward_certs_receiver, - reward_certs_sender, }, ) .map_err(ValidatorError::Other)?; diff --git a/votor-messages/src/reward_certificate.rs b/votor-messages/src/reward_certificate.rs index c272d630999..7bee333eafd 100644 --- a/votor-messages/src/reward_certificate.rs +++ b/votor-messages/src/reward_certificate.rs @@ -1,11 +1,9 @@ //! Defines aggregates used for vote rewards. use { - crate::consensus_message::VoteMessage, solana_bls_signatures::SignatureCompressed as BLSSignatureCompressed, solana_clock::Slot, solana_hash::Hash, - solana_pubkey::Pubkey, solana_short_vec::ShortU16, solana_signer_store::EncodeError, thiserror::Error, @@ -117,30 +115,6 @@ impl NotarRewardCertificate { } } -/// Message to add votes to the rewards container. -#[derive(Debug)] -pub struct AddVoteMessage { - /// List of [`VoteMessage`]s. - pub votes: Vec, -} - -/// Request to build reward certificates. -pub struct BuildRewardCertsRequest { - /// The bank slot which will include the built reward certs. - pub bank_slot: Slot, -} - -/// Response when the reward certs are built successfully. -#[derive(Default)] -pub struct BuildRewardCertsRespSucc { - /// Skip reward certificate. None if no skip votes were registered. - pub skip: Option, - /// Notar reward certificate. None if no notar votes were registered. - pub notar: Option, - /// If at least one of the certs above is present, then this contains the slot for which the reward certs were built and the list of validators in the certs. - pub validators: Vec, -} - /// Error returned when build reward certs fails. #[derive(Debug, Error)] pub enum BuildRewardCertsRespError { @@ -151,11 +125,3 @@ pub enum BuildRewardCertsRespError { #[error("encode error {0:?}")] Encode(EncodeError), } - -/// Response to a [`BuildRewardCertsRequest`]. -pub struct BuildRewardCertsResponse { - /// The bank slot from the corresponding request. - pub bank_slot: Slot, - /// The result of building reward certs for `bank_slot`. - pub result: Result, -} diff --git a/votor/src/consensus_rewards.rs b/votor/src/consensus_rewards.rs index 30bd6ca10d3..cc72fd0719f 100644 --- a/votor/src/consensus_rewards.rs +++ b/votor/src/consensus_rewards.rs @@ -2,16 +2,17 @@ use { agave_votor_messages::{ consensus_message::VoteMessage, reward_certificate::{ - AddVoteMessage, BuildRewardCertsRequest, BuildRewardCertsRespError, - BuildRewardCertsRespSucc, BuildRewardCertsResponse, NUM_SLOTS_FOR_REWARD, + BuildRewardCertsRespError, NUM_SLOTS_FOR_REWARD, NotarRewardCertificate, + SkipRewardCertificate, }, vote::Vote, }, - crossbeam_channel::{Receiver, Sender, select_biased}, + crossbeam_channel::{Receiver, RecvError, Sender, select_biased}, entry::Entry, solana_clock::Slot, solana_gossip::cluster_info::ClusterInfo, solana_ledger::leader_schedule_cache::LeaderScheduleCache, + solana_pubkey::Pubkey, solana_runtime::{bank::Bank, bank_forks::SharableBanks}, std::{ collections::BTreeMap, @@ -70,8 +71,6 @@ struct ConsensusRewards { exit: Arc, /// Channel to receive messages to build reward certificates. build_reward_certs_receiver: Receiver, - /// Channel send the built reward certificates. - reward_certs_sender: Sender, /// Channel to receive verified votes. votes_receiver: Receiver, } @@ -84,7 +83,6 @@ impl ConsensusRewards { sharable_banks: SharableBanks, exit: Arc, build_reward_certs_receiver: Receiver, - reward_certs_sender: Sender, votes_receiver: Receiver, ) -> Self { Self { @@ -94,11 +92,37 @@ impl ConsensusRewards { sharable_banks, exit, build_reward_certs_receiver, - reward_certs_sender, votes_receiver, } } + fn handle_request( + &mut self, + msg: Result, + ) -> Result<(), ()> { + let my_pubkey = self.cluster_info.id(); + match msg { + Ok(BuildRewardCertsRequest { + bank_slot, + reply_sender, + }) => { + let resp = BuildRewardCertsResponse { + result: self.build_certs(bank_slot), + }; + let _ = reply_sender.send(resp).inspect_err(|_| { + info!( + "{my_pubkey}: channel to send reply for bank_slot={bank_slot} disconnected" + ); + }); + Ok(()) + } + Err(_) => { + error!("{my_pubkey}: build reward certs channel is disconnected; exiting."); + Err(()) + } + } + } + /// Runs a loop receiving and handling messages over different channels. fn run(&mut self) { while !self.exit.load(Ordering::Relaxed) { @@ -106,21 +130,8 @@ impl ConsensusRewards { // bias messages to build certificates as that is on the critical path select_biased! { recv(self.build_reward_certs_receiver) -> msg => { - match msg { - Ok(msg) => { - let resp = BuildRewardCertsResponse { - bank_slot: msg.bank_slot, - result: self.build_certs(msg.bank_slot), - }; - if self.reward_certs_sender.send(resp).is_err() { - error!("{my_pubkey}: cert sender channel is disconnected; exiting."); - break; - } - } - Err(_) => { - error!("{my_pubkey}: build reward certs channel is disconnected; exiting."); - break; - } + if let Err(()) = self.handle_request(msg) { + break; } } recv(self.votes_receiver) -> msg => { @@ -221,7 +232,6 @@ impl ConsensusRewardsService { exit: Arc, votes_receiver: Receiver, build_reward_certs_receiver: Receiver, - reward_certs_sender: Sender, ) -> Self { let handle = Builder::new() .name("solConsRew".to_string()) @@ -232,7 +242,6 @@ impl ConsensusRewardsService { sharable_banks, exit, build_reward_certs_receiver, - reward_certs_sender, votes_receiver, ) .run(); @@ -245,3 +254,35 @@ impl ConsensusRewardsService { self.handle.join() } } + +/// Request to build reward certificates. +pub struct BuildRewardCertsRequest { + /// The bank slot which will include the built reward certs. + pub bank_slot: Slot, + /// The channel on which to send the reply. + pub reply_sender: Sender, +} + +/// Response when the reward certs are built successfully. +#[derive(Default, Debug, PartialEq, Eq)] +pub struct BuildRewardCertsRespSucc { + /// Skip reward certificate. None if no skip votes were registered. + pub skip: Option, + /// Notar reward certificate. None if no notar votes were registered. + pub notar: Option, + /// If at least one of the certs above is present, then this contains the slot for which the reward certs were built and the list of validators in the certs. + pub validators: Vec, +} + +/// Response to a [`BuildRewardCertsRequest`]. +pub struct BuildRewardCertsResponse { + /// The result of building reward certs for `bank_slot`. + pub result: Result, +} + +/// Message to add votes to the rewards container. +#[derive(Debug)] +pub struct AddVoteMessage { + /// List of [`VoteMessage`]s. + pub votes: Vec, +} diff --git a/votor/src/votor.rs b/votor/src/votor.rs index 8b1d8f47030..99ed69be371 100644 --- a/votor/src/votor.rs +++ b/votor/src/votor.rs @@ -50,7 +50,7 @@ use { ConsensusMetrics, ConsensusMetricsEventReceiver, ConsensusMetricsEventSender, }, consensus_pool_service::{ConsensusPoolContext, ConsensusPoolService}, - consensus_rewards::ConsensusRewardsService, + consensus_rewards::{AddVoteMessage, BuildRewardCertsRequest, ConsensusRewardsService}, event::{ LatestSwitchRequest, LeaderWindowInfo, RepairEventSender, VotorEventReceiver, VotorEventSender, @@ -64,10 +64,7 @@ use { voting_service::BLSOp, voting_utils::VotingContext, }, - agave_votor_messages::{ - consensus_message::ConsensusMessage, - reward_certificate::{AddVoteMessage, BuildRewardCertsRequest, BuildRewardCertsResponse}, - }, + agave_votor_messages::consensus_message::ConsensusMessage, crossbeam_channel::{Receiver, Sender}, parking_lot::RwLock as PlRwLock, solana_clock::Slot, @@ -117,7 +114,6 @@ pub struct VotorConfig { pub highest_parent_ready: Arc>, pub event_sender: VotorEventSender, pub own_vote_sender: Sender>, - pub reward_certs_sender: Sender, pub repair_event_sender: RepairEventSender, pub latest_switch_request: LatestSwitchRequest, @@ -177,7 +173,6 @@ impl Votor { consensus_metrics_receiver, reward_votes_receiver, build_reward_certs_receiver, - reward_certs_sender, generated_cert_types, highest_finalized, bank_forks_controller, @@ -268,7 +263,6 @@ impl Votor { exit, reward_votes_receiver, build_reward_certs_receiver, - reward_certs_sender, ); Self { From d21b47738b68ae89779c9c53caea5b6cdc9e53a2 Mon Sep 17 00:00:00 2001 From: Akhilesh Singhania Date: Thu, 28 May 2026 21:57:05 +0200 Subject: [PATCH 110/576] Implements logic to pay alpenglow rewards during partitioned epoch rewards (#12317) --- programs/vote/src/vote_state/handler.rs | 2 +- runtime/src/alpenglow_epoch_type.rs | 15 + runtime/src/bank.rs | 42 + .../partitioned_epoch_rewards/calculation.rs | 65 +- runtime/src/bank_forks.rs | 2 +- .../block_component_processor/vote_reward.rs | 553 +++++++++++- .../vote_reward/migration_test.rs | 464 ++++++++++ runtime/src/genesis_utils.rs | 3 +- runtime/src/inflation_rewards/mod.rs | 292 +++++-- runtime/src/inflation_rewards/points.rs | 811 ++++++++++++++++-- runtime/src/lib.rs | 1 + 11 files changed, 2098 insertions(+), 152 deletions(-) create mode 100644 runtime/src/alpenglow_epoch_type.rs create mode 100644 runtime/src/block_component_processor/vote_reward/migration_test.rs diff --git a/programs/vote/src/vote_state/handler.rs b/programs/vote/src/vote_state/handler.rs index ec482cf6597..915c87c0053 100644 --- a/programs/vote/src/vote_state/handler.rs +++ b/programs/vote/src/vote_state/handler.rs @@ -277,7 +277,7 @@ impl VoteStateHandler { } } - pub(crate) fn epoch_credits_mut(&mut self) -> &mut Vec<(Epoch, u64, u64)> { + pub fn epoch_credits_mut(&mut self) -> &mut Vec<(Epoch, u64, u64)> { match &mut self.target_state { TargetVoteState::V4(v4) => &mut v4.epoch_credits, } diff --git a/runtime/src/alpenglow_epoch_type.rs b/runtime/src/alpenglow_epoch_type.rs new file mode 100644 index 00000000000..f516622d271 --- /dev/null +++ b/runtime/src/alpenglow_epoch_type.rs @@ -0,0 +1,15 @@ +use solana_clock::{Epoch, Slot}; + +#[derive(Debug)] +pub(crate) enum AlpenglowEpochType { + /// This is a full tower epoch. + Tower, + /// The epoch started in tower and then switched to alpenglow + MigrationEpoch { + num_tower_slots: Slot, + num_ag_slots: Slot, + migration_epoch: Epoch, + }, + /// This is a full alpenglow epoch + Alpenglow { migration_epoch: Epoch }, +} diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 9cc3c9de3f6..1c492ba4a4e 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -41,6 +41,7 @@ pub use { use { crate::{ account_saver::collect_accounts_to_store, + alpenglow_epoch_type::AlpenglowEpochType, bank::{ entry_bytes_budget::EntryBytesBudget, metrics::*, @@ -196,6 +197,7 @@ use { }, solana_vote_interface::state::VoteStateV4, std::{ + cmp::Ordering, collections::{HashMap, HashSet}, fmt, ops::AddAssign, @@ -6399,6 +6401,46 @@ impl Bank { solana_sha256_hasher::hashv(&[parent_block_id.as_ref(), bank.hash().as_ref()]); bank.set_block_id(Some(block_id)); } + + pub(crate) fn get_alpenglow_migration_slot(&self) -> Option { + let genesis_cert = self.get_alpenglow_genesis_certificate()?; + debug_assert!( + genesis_cert.cert_type.is_genesis(), + "cert_type={:?}", + genesis_cert.cert_type + ); + Some(genesis_cert.cert_type.slot()) + } + + /// Returns `AlpenglowEpochType` for the given `epoch`. + /// + /// Calling this function with an epoch >= `Bank::epoch` can return false information as it is + /// possible that we have not observed the genesis cert yet but will in the upcoming slots. + pub(crate) fn get_alpenglow_epoch_type(&self, epoch: Epoch) -> AlpenglowEpochType { + debug_assert!(epoch < self.epoch); + let Some(migration_slot) = self.get_alpenglow_migration_slot() else { + return AlpenglowEpochType::Tower; + }; + let migration_epoch = self.epoch_schedule.get_epoch(migration_slot); + match migration_epoch.cmp(&epoch) { + Ordering::Less => AlpenglowEpochType::Alpenglow { migration_epoch }, + Ordering::Greater => AlpenglowEpochType::Tower, + Ordering::Equal => { + let first_slot_in_epoch = + self.epoch_schedule.get_first_slot_in_epoch(migration_epoch); + // + 1 because the migration_slot is still tower. + let num_tower_slots = migration_slot - first_slot_in_epoch + 1; + let slots_in_epoch = self.epoch_schedule.get_slots_in_epoch(migration_epoch); + let num_ag_slots = slots_in_epoch - num_tower_slots; + assert_eq!(slots_in_epoch, num_tower_slots + num_ag_slots); + AlpenglowEpochType::MigrationEpoch { + num_tower_slots, + num_ag_slots, + migration_epoch, + } + } + } + } } impl InvokeContextCallback for Bank { diff --git a/runtime/src/bank/partitioned_epoch_rewards/calculation.rs b/runtime/src/bank/partitioned_epoch_rewards/calculation.rs index 0779177cece..b91b490b56b 100644 --- a/runtime/src/bank/partitioned_epoch_rewards/calculation.rs +++ b/runtime/src/bank/partitioned_epoch_rewards/calculation.rs @@ -7,13 +7,16 @@ use { StakeRewardCalculation, epoch_rewards_hasher::hash_rewards_into_partitions, }, crate::{ + alpenglow_epoch_type::AlpenglowEpochType, bank::{ RewardCalcTracer, RewardCalculationEvent, RewardsMetrics, fee_distribution::ExternalCollectorType, null_tracer, }, inflation_rewards::{ adjust_delegation_for_rent, - points::{CalculationEnvironment, DelegatedVoteState, PointValue, calculate_points}, + points::{ + CalculationEnvironment, DelegatedVoteState, PointValue, calculate_points_for_tower, + }, redeem_rewards, }, reward_info::RewardInfo, @@ -220,7 +223,7 @@ impl Bank { stake_history: &StakeHistory, stake_delegations: Vec<(&Pubkey, &StakeAccount)>, cached_vote_accounts: CachedVoteAccounts<'_>, - prev_epoch: Epoch, + rewarded_epoch: Epoch, reward_calc_tracer: Option, thread_pool: &ThreadPool, metrics: &mut RewardsMetrics, @@ -253,7 +256,7 @@ impl Bank { stake_history, stake_delegations, cached_vote_accounts, - prev_epoch, + rewarded_epoch, reward_calc_tracer, thread_pool, metrics, @@ -392,7 +395,7 @@ impl Bank { metrics: &mut RewardsMetrics, ) -> PartitionedRewardsCalculation { let capitalization = self.capitalization(); - let validator_rewards_lamports = + let epoch_inflation_rewards = self.calculate_epoch_inflation_rewards(capitalization, rewarded_epoch); // `distribution_epoch_vote_accounts` is the post-VAT-filter snapshot // produced upstream of this call (or unfiltered when VAT is off), @@ -410,7 +413,7 @@ impl Bank { stake_delegations, cached_vote_accounts, rewarded_epoch, - validator_rewards_lamports, + epoch_inflation_rewards, reward_calc_tracer, thread_pool, metrics, @@ -438,16 +441,18 @@ impl Bank { stake_delegations: Vec<(&'a Pubkey, &'a StakeAccount)>, cached_vote_accounts: CachedVoteAccounts<'_>, rewarded_epoch: Epoch, - rewards: u64, + epoch_inflation_rewards: u64, reward_calc_tracer: Option, thread_pool: &ThreadPool, metrics: &mut RewardsMetrics, ) -> Option { + let ag_epoch_type = self.get_alpenglow_epoch_type(rewarded_epoch); self.calculate_reward_points_partitioned( stake_history, &stake_delegations, &cached_vote_accounts, - rewards, + epoch_inflation_rewards, + &ag_epoch_type, thread_pool, metrics, ) @@ -459,6 +464,7 @@ impl Bank { cached_vote_accounts, rewarded_epoch, point_value.clone(), + &ag_epoch_type, thread_pool, reward_calc_tracer, metrics, @@ -515,6 +521,7 @@ impl Bank { delay_commission_updates: bool, commission_rate_in_basis_points: bool, adjust_delegations_for_rent: bool, + ag_epoch_type: &AlpenglowEpochType, custom_commission_collector: bool, ) -> Option { // curry closure to add the contextual stake_pubkey @@ -622,6 +629,8 @@ impl Bank { adjust_delegations_for_rent, }, reward_calc_tracer, + ag_epoch_type, + &self.epoch_stakes, current_lamports, minimum_lamports, ) { @@ -661,6 +670,7 @@ impl Bank { /// Calculates epoch rewards for stake/commission accounts /// Returns commission accounts, stake rewards, and the sum of all stake rewards in lamports + #[allow(clippy::too_many_arguments)] fn calculate_stake_rewards_and_commissions<'a>( &self, stake_history: &StakeHistory, @@ -668,6 +678,7 @@ impl Bank { cached_vote_accounts: CachedVoteAccounts<'_>, rewarded_epoch: Epoch, point_value: PointValue, + ag_epoch_type: &AlpenglowEpochType, thread_pool: &ThreadPool, reward_calc_tracer: Option, metrics: &mut RewardsMetrics, @@ -710,6 +721,7 @@ impl Bank { delay_commission_updates, commission_rate_in_basis_points, adjust_delegations_for_rent, + ag_epoch_type, custom_commission_collector, ); @@ -784,7 +796,8 @@ impl Bank { stake_history: &StakeHistory, stake_delegations: &Vec<(&'a Pubkey, &'a StakeAccount)>, cached_vote_accounts: &CachedVoteAccounts<'_>, - rewards: u64, + epoch_inflation_rewards: u64, + ag_epoch_type: &AlpenglowEpochType, thread_pool: &ThreadPool, metrics: &RewardsMetrics, ) -> Option { @@ -795,6 +808,24 @@ impl Bank { let solana_vote_program: Pubkey = solana_vote_program::id(); let new_warmup_cooldown_rate_epoch = self.new_warmup_cooldown_rate_epoch(); + match ag_epoch_type { + AlpenglowEpochType::Alpenglow { .. } => { + // In alpenglow, we do not need to compute `PointValue::points` as the final + // rewards are simply the total credits stored in the vote account. We just need + // to return a `Some` value with valid rewards. + return Some(PointValue { + rewards: epoch_inflation_rewards, + points: 0, + }); + } + AlpenglowEpochType::Tower => { + // For tower we need to compute the valid `PointValue::points`. + } + AlpenglowEpochType::MigrationEpoch { .. } => { + // For the migrating epoch, we need to compute the tower portion of `PointValue::points`. + } + } + let (points, measure_us) = measure_us!(thread_pool.install(|| { stake_delegations .par_iter() @@ -809,11 +840,12 @@ impl Bank { return 0; } - calculate_points( + calculate_points_for_tower( stake_account.stake_state(), DelegatedVoteState::from(vote_account.vote_state_view()), stake_history, new_warmup_cooldown_rate_epoch, + &self.epoch_stakes, ) .unwrap_or(0) }) @@ -821,7 +853,10 @@ impl Bank { })); metrics.calculate_points_us.fetch_add(measure_us, Relaxed); - (points > 0).then_some(PointValue { rewards, points }) + (points > 0).then_some(PointValue { + rewards: epoch_inflation_rewards, + points, + }) } /// If rewards are still active, recalculates partitioned stake rewards and @@ -860,6 +895,7 @@ impl Bank { // If rewards are active, the rewarded epoch is always the immediately // preceding epoch. let rewarded_epoch = self.epoch().saturating_sub(1); + let ag_epoch_type = self.get_alpenglow_epoch_type(rewarded_epoch); let point_value = PointValue { rewards: epoch_rewards_sysvar.total_rewards, @@ -893,6 +929,7 @@ impl Bank { cached_vote_accounts, rewarded_epoch, point_value, + &ag_epoch_type, thread_pool, null_tracer(), &mut RewardsMetrics::default(), // This is required, but not reporting anything at the moment @@ -1195,7 +1232,7 @@ mod tests { let expected_rewards = 100_000_000_000; let stakes = bank.stakes_cache.stakes(); - let rewarded_epoch = 1; + let rewarded_epoch = 0; let EpochRewardCalculateParamInfo { stake_history, stake_delegations, @@ -1258,6 +1295,7 @@ mod tests { &stake_delegations, &cached_vote_accounts, expected_rewards, + &AlpenglowEpochType::Tower, &thread_pool, &rewards_metrics, ); @@ -1291,6 +1329,7 @@ mod tests { &stake_delegations, &cached_vote_accounts, expected_rewards, + &AlpenglowEpochType::Tower, &thread_pool, &rewards_metrics, ); @@ -1341,6 +1380,7 @@ mod tests { cached_vote_accounts, rewarded_epoch, point_value, + &AlpenglowEpochType::Tower, &thread_pool, null_tracer(), &mut RewardsMetrics::default(), // This is required, but not reporting anything at the moment @@ -1978,6 +2018,7 @@ mod tests { cached_vote_accounts, rewarded_epoch, point_value, + &AlpenglowEpochType::Tower, &thread_pool, reward_calc_tracer, &mut rewards_metrics, @@ -2041,7 +2082,7 @@ mod tests { num_rewards_per_block, SLOTS_PER_EPOCH, ); - let rewarded_epoch = bank.epoch(); + let rewarded_epoch = bank.epoch() - 1; let thread_pool = ThreadPoolBuilder::new().num_threads(1).build().unwrap(); let mut rewards_metrics = RewardsMetrics::default(); diff --git a/runtime/src/bank_forks.rs b/runtime/src/bank_forks.rs index e44b31be1c5..c71ec0eca3d 100644 --- a/runtime/src/bank_forks.rs +++ b/runtime/src/bank_forks.rs @@ -1029,7 +1029,7 @@ mod tests { fn test_initialize_migration_status() { let ff_activation_slot = 5; let genesis_cert = Certificate { - cert_type: CertificateType::Finalize(1), + cert_type: CertificateType::Genesis(1, Hash::default()), signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), bitmap: vec![], }; diff --git a/runtime/src/block_component_processor/vote_reward.rs b/runtime/src/block_component_processor/vote_reward.rs index 7911ea87cca..f699aa6ab58 100644 --- a/runtime/src/block_component_processor/vote_reward.rs +++ b/runtime/src/block_component_processor/vote_reward.rs @@ -6,13 +6,19 @@ use { solana_clock::{Epoch, Slot}, solana_pubkey::Pubkey, solana_vote::vote_account::VoteAccount, - solana_vote_interface::state::{LandedVote, Lockout}, + solana_vote_interface::state::{LandedVote, Lockout, MAX_EPOCH_CREDITS_HISTORY}, solana_vote_program::vote_state::handler::VoteStateHandler, - std::collections::{HashMap, HashSet, VecDeque, hash_map::Entry}, + std::{ + collections::{HashMap, HashSet, VecDeque, hash_map::Entry}, + num::NonZero, + }, thiserror::Error, }; pub mod epoch_inflation_account_state; +mod migration_test; + +pub(crate) const AG_MIGRATION_EPOCH_CREDIT: (Epoch, u64, u64) = (Epoch::MAX, u64::MAX, u64::MAX); /// Different types of errors that can happen when calculating and paying voting reward. /// @@ -70,9 +76,15 @@ pub enum RewardStateError { reward_slot: Slot, current_slot: Slot, }, + #[error("genesis cert not found. reward_slot={reward_slot}; current_slot={current_slot}")] + GenesisCertNotFound { + reward_slot: Slot, + current_slot: Slot, + }, } /// Data needed to operate on `VoteStateHandler`. +#[derive(Debug)] struct VoteState { /// The pubkey of the vote account. vote_pubkey: Pubkey, @@ -173,6 +185,7 @@ struct RewardState<'a> { total_stake: u64, /// inflation state at `reward_slot`. epoch_inflation_state: EpochInflationState, + migration_epoch: Epoch, } impl<'a> RewardState<'a> { @@ -206,6 +219,11 @@ impl<'a> RewardState<'a> { current_slot, })? }; + let migration_epoch = + get_migration_epoch(bank).ok_or(RewardStateError::GenesisCertNotFound { + reward_slot, + current_slot, + })?; Ok(Self { current_epoch: bank.epoch(), reward_slot, @@ -215,6 +233,7 @@ impl<'a> RewardState<'a> { accounts, total_stake, epoch_inflation_state, + migration_epoch, }) } @@ -257,11 +276,14 @@ impl<'a> RewardState<'a> { self.update_votes(vote_state); let reward = self.calculate_reward(vote_state.vote_pubkey, accumulating_leader_reward)?; - if reward != 0 { - vote_state - .handler - .increment_credits(self.current_epoch, reward); - } + if let Some(reward) = NonZero::new(reward) { + increment_credits( + vote_state.handler.epoch_credits_mut(), + self.migration_epoch, + self.current_epoch, + reward, + ); + }; Ok(true) } else { Ok(false) @@ -270,17 +292,14 @@ impl<'a> RewardState<'a> { /// Assumes that `Self::update_account` was already called and pays the additional /// `leader_rewards` to the account. - #[must_use] - fn update_leader(&self, vote_state: &mut VoteState, leader_reward: u64) -> bool { + fn update_leader(&self, vote_state: &mut VoteState, leader_reward: NonZero) { debug_assert_eq!(vote_state.vote_pubkey, self.leader_vote_pubkey); - if leader_reward != 0 { - vote_state - .handler - .increment_credits(self.current_epoch, leader_reward); - true - } else { - false - } + increment_credits( + vote_state.handler.epoch_credits_mut(), + self.migration_epoch, + self.current_epoch, + leader_reward, + ); } } @@ -372,16 +391,18 @@ fn update_accounts( } // all validators have been processed, can pay leader rewards now. - if let Some(state) = &reward_state { + if let Some(state) = &reward_state + && let Some(leader_reward) = NonZero::new(leader_reward) + { match updated_accounts.entry(state.leader_vote_pubkey) { Entry::Occupied(e) => { - let _ = state.update_leader(e.into_mut(), leader_reward); + state.update_leader(e.into_mut(), leader_reward); } Entry::Vacant(e) => { if let Some(mut vote_state) = VoteState::try_new(vote_accounts, state.leader_vote_pubkey) - && state.update_leader(&mut vote_state, leader_reward) { + state.update_leader(&mut vote_state, leader_reward); e.insert(vote_state); } } @@ -472,26 +493,115 @@ fn calculate_reward( (validator_reward_lamports, leader_reward_lamports) } +fn get_migration_epoch(bank: &Bank) -> Option { + let migration_slot = bank.get_alpenglow_migration_slot()?; + let migration_epoch = bank.epoch_schedule.get_epoch(migration_slot); + Some(migration_epoch) +} + +fn ensure_marker(epoch_credits: &mut Vec<(Epoch, u64, u64)>) { + for elem in epoch_credits.iter().rev() { + if elem == &AG_MIGRATION_EPOCH_CREDIT { + return; + } + } + epoch_credits.push(AG_MIGRATION_EPOCH_CREDIT); +} + +fn increment_credits( + epoch_credits: &mut Vec<(Epoch, u64, u64)>, + migration_epoch: Epoch, + epoch: Epoch, + new_credits: NonZero, +) { + if epoch == migration_epoch { + ensure_marker(epoch_credits); + } + + let Some(entry) = epoch_credits.last_mut() else { + // no entries, insert a new entry and we are done. + epoch_credits.push((epoch, new_credits.get(), 0)); + return; + }; + + // Latest element is the marker, start a new entry. + if *entry == AG_MIGRATION_EPOCH_CREDIT { + // If there was a tower entry before, its final credits forms this entry's initial credits. + let len = epoch_credits.len(); + let final_tower_credits = if len >= 2 { + assert_ne!(epoch_credits[len - 2], AG_MIGRATION_EPOCH_CREDIT); + epoch_credits[len - 2].1 + } else { + 0 + }; + epoch_credits.push(( + epoch, + new_credits.get().saturating_add(final_tower_credits), + final_tower_credits, + )); + while epoch_credits.len() > MAX_EPOCH_CREDITS_HISTORY { + epoch_credits.remove(0); + } + return; + } + + let (entry_epoch, final_credits, initial_credits) = entry; + + // Latest element is the same epoch, simply increment final credits. + if *entry_epoch == epoch { + *final_credits = final_credits.saturating_add(new_credits.get()); + return; + } + + // Different epochs but the latest epoch didn't earn any credits, reuse the entry. + if final_credits == initial_credits { + *entry_epoch = epoch; + *final_credits = final_credits.saturating_add(new_credits.get()); + return; + } + + // Different epochs and the latest epoch earned credits, insert a new entry. + let entry = ( + epoch, + new_credits.get().saturating_add(*final_credits), + *final_credits, + ); + epoch_credits.push(entry); + + // maybe included a marker and a new entry above. So might have multiple entries to remove here. + while epoch_credits.len() > MAX_EPOCH_CREDITS_HISTORY { + epoch_credits.remove(0); + } +} + #[cfg(test)] mod tests { use { super::*, crate::{ + bank::VAT_TO_BURN_PER_EPOCH, bank_forks::BankForks, genesis_utils::{ - ValidatorVoteKeypairs, create_genesis_config_with_alpenglow_vote_accounts, + ValidatorVoteKeypairs, activate_all_features_alpenglow, + create_genesis_config_with_alpenglow_vote_accounts, + create_genesis_config_with_leader_ex, create_validator, }, + inflation_rewards::commission_split, + stake_utils, validated_block_finalization::ValidatedBlockFinalizationCert, }, + agave_feature_set::FeatureSet, agave_votor_messages::{ consensus_message::{Certificate, CertificateType}, reward_certificate::NUM_SLOTS_FOR_REWARD, }, bitvec::prelude::*, rand::seq::IndexedRandom, - solana_account::ReadableAccount, + solana_account::{Account, ReadableAccount, WritableAccount}, solana_bls_signatures::{BLS_SIGNATURE_AFFINE_SIZE, Signature as BLSSignature}, + solana_cluster_type::ClusterType, solana_epoch_schedule::EpochSchedule, + solana_fee_calculator::FeeRateGovernor, solana_genesis_config::GenesisConfig, solana_hash::Hash, solana_keypair::Keypair, @@ -500,7 +610,13 @@ mod tests { solana_rent::Rent, solana_signer::Signer, solana_signer_store::encode_base2, - std::sync::{Arc, RwLock}, + solana_stake_interface::state::StakeStateV2, + solana_vote_interface::state::{VoteStateV4, VoteStateVersions}, + std::{ + collections::HashMap, + sync::{Arc, RwLock}, + }, + test_case::test_matrix, }; fn new_bank_for_tests( @@ -512,7 +628,7 @@ mod tests { bank.wrap_with_bank_forks_for_tests() } - fn new_bank_from_parent(parent_bank: Arc, slot: Slot) -> Arc { + pub fn new_bank_from_parent(parent_bank: Arc, slot: Slot) -> Arc { let leader = *parent_bank.leader(); Arc::new(Bank::new_from_parent(parent_bank, leader, slot)) } @@ -877,4 +993,393 @@ mod tests { } } } + + pub fn set_commission( + genesis_config: &mut GenesisConfig, + validators: &[ValidatorVoteKeypairs], + commission_bps: u16, + ) { + for validator in validators { + let vote_pubkey = validator.vote_keypair.pubkey(); + let account = genesis_config.accounts.get_mut(&vote_pubkey).unwrap(); + let vote_state_versions = bincode::deserialize(&account.data).unwrap(); + let VoteStateVersions::V4(mut vote_state) = vote_state_versions else { + panic!(); + }; + vote_state.inflation_rewards_commission_bps = commission_bps; + VoteStateV4::serialize( + &VoteStateVersions::V4(vote_state), + account.data_as_mut_slice(), + ) + .unwrap(); + } + } + + struct State { + commission_bps: u16, + _bank_forks: Arc>, + validators: Vec, + /// key is validator pubkey, value is list of stakers. + stakers: HashMap>, + } + + impl State { + fn new( + num_validators: u64, + num_add_stakers: u64, + pay_leader: bool, + commission_bps: u16, + ) -> (Self, Arc) { + let lamports = LAMPORTS_PER_SOL * 20; + let mint_keypair = Keypair::new(); + let validators = (0..num_validators) + .map(|_| ValidatorVoteKeypairs::new_rand()) + .collect::>(); + let leader = if pay_leader { + let vote_pubkey = validators[0].vote_keypair.pubkey(); + let node_pubkey = validators[0].node_keypair.pubkey(); + SlotLeader { + id: node_pubkey, + vote_address: vote_pubkey, + } + } else { + SlotLeader::new_unique() + }; + let mut genesis_config = create_genesis_config_with_leader_ex( + lamports, + &mint_keypair.pubkey(), + &validators[0].node_keypair.pubkey(), + &validators[0].vote_keypair.pubkey(), + &validators[0].stake_keypair.pubkey(), + Some(validators[0].bls_keypair.public.to_bytes_compressed()), + lamports, + lamports, + FeeRateGovernor::new(0, 0), + Rent::default(), + ClusterType::Development, + &FeatureSet::all_enabled(), + vec![], + ); + genesis_config.epoch_schedule = EpochSchedule::without_warmup(); + activate_all_features_alpenglow(&mut genesis_config); + for (ind, keypair) in validators.iter().enumerate().skip(1) { + let node_pubkey = keypair.node_keypair.pubkey(); + let vote_pubkey = keypair.vote_keypair.pubkey(); + let stake_pubkey = keypair.stake_keypair.pubkey(); + let bls_pubkey = Some(keypair.bls_keypair.public.to_bytes_compressed()); + let lamports = lamports + ind as u64 * LAMPORTS_PER_SOL; + let accounts = create_validator( + &genesis_config.rent, + node_pubkey, + lamports, + vote_pubkey, + lamports, + stake_pubkey, + lamports, + bls_pubkey, + ) + .into_iter() + .map(|(pubkey, account)| (pubkey, Account::from(account))); + genesis_config.accounts.extend(accounts); + } + set_commission(&mut genesis_config, &validators, commission_bps); + + let vote_account = genesis_config + .accounts + .get(&validators[0].vote_keypair.pubkey()) + .unwrap() + .clone() + .into(); + + let staker_keypairs = (0..num_add_stakers) + .map(|_| Keypair::new()) + .collect::>(); + for (ind, keypair) in staker_keypairs.iter().enumerate() { + let stake_pubkey = keypair.pubkey(); + let account = Account::from(stake_utils::create_stake_account( + &stake_pubkey, + &validators[0].vote_keypair.pubkey(), + &vote_account, + &genesis_config.rent, + lamports + (ind as u64 + 1) * lamports, + )); + genesis_config.accounts.insert(stake_pubkey, account); + } + + let staker_pubkeys = { + let mut staker_pubkeys = validators + .iter() + .map(|keypair| { + ( + keypair.vote_keypair.pubkey(), + vec![keypair.stake_keypair.pubkey()], + ) + }) + .collect::>(); + for staker_keypair in &staker_keypairs { + let staker_pubkey = staker_keypair.pubkey(); + staker_pubkeys + .get_mut(&validators[0].vote_keypair.pubkey()) + .unwrap() + .push(staker_pubkey); + } + staker_pubkeys + }; + + let (bank_epoch0, bank_forks) = new_bank_for_tests(leader, &genesis_config); + assert_eq!(bank_epoch0.epoch(), 0); + + // need to bump epoch by 1 as epoch 0 is migration epoch + let first_slot_in_epoch1 = bank_epoch0 + .epoch_schedule + .get_first_slot_in_epoch(bank_epoch0.epoch() + 1); + let bank_epoch1 = new_bank_from_parent(bank_epoch0, first_slot_in_epoch1); + ( + Self { + commission_bps, + _bank_forks: bank_forks, + validators, + stakers: staker_pubkeys, + }, + bank_epoch1, + ) + } + + fn get_validator_stake(&self, bank: &Bank, pubkey: &Pubkey) -> u64 { + let rent_exempt_reserve = bank + .rent_collector() + .rent + .minimum_balance(StakeStateV2::size_of()); + self.stakers + .get(pubkey) + .unwrap() + .iter() + .map(|pubkey| { + let lamports = bank.get_account(pubkey).unwrap().lamports(); + lamports - rent_exempt_reserve + }) + .sum() + } + + /// returns (validator_rewards, leader_rewards) + fn get_rewards(&self, bank: &Bank, num_reward_slots: u64, pubkey: &Pubkey) -> (u64, u64) { + let epoch_state = EpochInflationAccountState::new_from_bank(bank) + .unwrap() + .get_epoch_state(bank.epoch()) + .unwrap(); + let total_stake = bank.epoch_stakes(bank.epoch()).unwrap().total_stake(); + let (validator_reward, leader_reward) = calculate_reward( + &epoch_state, + total_stake, + self.get_validator_stake(bank, pubkey), + ); + let vote_rewards = validator_reward * num_reward_slots; + let leader_reward = leader_reward * num_reward_slots; + (vote_rewards, leader_reward) + } + + fn get_initial_and_final_lamports( + &self, + reward_bank: &Bank, + payout_bank: &Bank, + pubkey: &Pubkey, + ) -> (u64, u64) { + assert!(payout_bank.slot() > reward_bank.slot()); + assert!(payout_bank.epoch() > reward_bank.epoch()); + let initial_lamports = reward_bank.get_account(pubkey).unwrap().lamports(); + let final_lamports = payout_bank.get_account(pubkey).unwrap().lamports(); + (initial_lamports, final_lamports) + } + + fn validate_stakers( + &self, + reward_bank: &Bank, + payout_bank: &Bank, + voter_pubkey: &Pubkey, + validator_reward: u64, + ) -> u64 { + assert!(payout_bank.slot() > reward_bank.slot()); + assert!(payout_bank.epoch() > reward_bank.epoch()); + let rent_exempt_reserve = reward_bank + .rent_collector() + .rent + .minimum_balance(StakeStateV2::size_of()); + let validator_stake = self.get_validator_stake(reward_bank, voter_pubkey); + let mut expected_validator_reward = 0; + for staker_pubkey in self.stakers.get(voter_pubkey).unwrap().iter() { + let (initial_lamports, final_lamports) = + self.get_initial_and_final_lamports(reward_bank, payout_bank, staker_pubkey); + if initial_lamports <= LAMPORTS_PER_SOL + rent_exempt_reserve { + continue; + } + let stake = initial_lamports - rent_exempt_reserve; + let stake_weighted_reward = validator_reward * stake / validator_stake; + let (voter_reward, staker_reward, is_split) = + commission_split(self.commission_bps, stake_weighted_reward); + assert!(is_split); + assert_eq!( + staker_reward, + final_lamports - initial_lamports, + "final={final_lamports}; initial={initial_lamports}" + ); + expected_validator_reward += voter_reward; + } + expected_validator_reward + } + + /// Returns leader_rewards + fn validate_voter_reward( + &self, + reward_bank: &Bank, + payout_bank: &Bank, + num_reward_slots: u64, + voter_pubkey: &Pubkey, + ) -> u64 { + assert!(payout_bank.slot() > reward_bank.slot()); + assert!(payout_bank.epoch() > reward_bank.epoch()); + + let (validator_reward, leader_reward) = + self.get_rewards(reward_bank, num_reward_slots, voter_pubkey); + + let expected_validator_reward = + self.validate_stakers(reward_bank, payout_bank, voter_pubkey, validator_reward); + + let (initial_validator_lamports, final_validator_lamports) = + self.get_initial_and_final_lamports(reward_bank, payout_bank, voter_pubkey); + assert_eq!( + expected_validator_reward, + final_validator_lamports + + VAT_TO_BURN_PER_EPOCH * (payout_bank.epoch() - reward_bank.epoch()) + - initial_validator_lamports + ); + leader_reward + } + + fn validate_leader_reward( + &self, + reward_bank: &Bank, + payout_bank: &Bank, + num_reward_slots: u64, + leader: Pubkey, + add_leader_reward: u64, + ) { + if !reward_bank.vote_accounts().contains_key(&leader) { + return; + } + assert!(payout_bank.slot() > reward_bank.slot()); + assert!(payout_bank.epoch() > reward_bank.epoch()); + + let (validator_reward, leader_reward) = + self.get_rewards(reward_bank, num_reward_slots, &leader); + let validator_reward = validator_reward + leader_reward + add_leader_reward; + + let expected_validator_reward = + self.validate_stakers(reward_bank, payout_bank, &leader, validator_reward); + + let (initial_validator_lamports, final_validator_lamports) = + self.get_initial_and_final_lamports(reward_bank, payout_bank, &leader); + assert_eq!( + expected_validator_reward, + final_validator_lamports + + VAT_TO_BURN_PER_EPOCH * (payout_bank.epoch() - reward_bank.epoch()) + - initial_validator_lamports + ); + } + + fn validate_rewards(&self, reward_bank: &Bank, payout_bank: &Bank, num_reward_slots: u64) { + assert_eq!(reward_bank.leader(), payout_bank.leader()); + let leader = reward_bank.leader().vote_address; + + let mut add_leader_reward = 0; + for voter_pubkey in self.stakers.keys() { + if voter_pubkey == &leader { + continue; + } + let leader_reward = self.validate_voter_reward( + reward_bank, + payout_bank, + num_reward_slots, + voter_pubkey, + ); + add_leader_reward += leader_reward; + } + self.validate_leader_reward( + reward_bank, + payout_bank, + num_reward_slots, + leader, + add_leader_reward, + ); + } + } + + fn reward_validators( + bank: Arc, + validators: &[ValidatorVoteKeypairs], + num_reward_slots: u64, + ) -> Arc { + let validators_to_reward = validators + .iter() + .map(|k| k.vote_keypair.pubkey()) + .collect::>() + .into_iter() + .collect::>(); + + let mut looping_bank = bank; + for _ in 0..num_reward_slots { + let reward_cert = ValidatedRewardCert::new_for_tests( + looping_bank.slot() - 100, + validators_to_reward.clone(), + ); + calc_vote_rewards_update_vote_states(&looping_bank, Some(reward_cert), None).unwrap(); + + let slot = looping_bank.slot() + 1; + looping_bank = new_bank_from_parent(looping_bank, slot); + } + looping_bank + } + + /// Progresses the bank a few times to pay out the rewards. + fn progress_bank_for_payout(mut bank: Arc) -> Arc { + for _ in 0..10 { + let slot = bank.slot() + 1; + bank = new_bank_from_parent(bank, slot); + } + bank + } + + fn test_vote_reward_payout_impl( + validators: &[ValidatorVoteKeypairs], + initial_bank: Arc, + num_reward_slots: u64, + ) -> Arc { + // bump slots a bit so that reward slots always land in the same epoch and after AG is activated if in migration epoch. + let slot = initial_bank.slot() + 100_000; + let initial_bank = new_bank_from_parent(initial_bank, slot); + let initial_bank_epoch = initial_bank.epoch(); + let rewarded_bank = reward_validators(initial_bank, validators, num_reward_slots); + assert_eq!(rewarded_bank.epoch(), initial_bank_epoch); + + let payout_epoch = rewarded_bank.epoch() + 1; + let payout_epoch_slot = rewarded_bank + .epoch_schedule + .get_first_slot_in_epoch(payout_epoch); + let payout_bank = new_bank_from_parent(rewarded_bank, payout_epoch_slot); + assert_eq!(payout_bank.epoch(), payout_epoch); + + let bank = progress_bank_for_payout(payout_bank); + assert_eq!(bank.epoch(), initial_bank_epoch + 1); + bank + } + + #[test_matrix([true, false], [1_000, 5_000], [0, 10])] + fn test_vote_reward_payout(pay_leader: bool, commission_bps: u16, num_add_stakers: u64) { + let num_validators = 2; + let num_reward_slots = 10; + let (state, initial_bank) = + State::new(num_validators, num_add_stakers, pay_leader, commission_bps); + let final_bank = + test_vote_reward_payout_impl(&state.validators, initial_bank.clone(), num_reward_slots); + state.validate_rewards(&initial_bank, &final_bank, num_reward_slots); + } } diff --git a/runtime/src/block_component_processor/vote_reward/migration_test.rs b/runtime/src/block_component_processor/vote_reward/migration_test.rs new file mode 100644 index 00000000000..986909437b3 --- /dev/null +++ b/runtime/src/block_component_processor/vote_reward/migration_test.rs @@ -0,0 +1,464 @@ +#![cfg(test)] +mod tests { + + use { + crate::{ + bank::{Bank, VAT_TO_BURN_PER_EPOCH}, + block_component_processor::vote_reward::{ + VoteState, increment_credits, + tests::{new_bank_from_parent, set_commission}, + }, + genesis_utils::{ + ValidatorVoteKeypairs, activate_all_features, create_genesis_config_with_leader_ex, + create_validator, + }, + inflation_rewards::commission_split, + stake_utils, + }, + agave_feature_set::FeatureSet, + agave_votor_messages::consensus_message::{Certificate, CertificateType}, + solana_account::{Account, ReadableAccount}, + solana_bls_signatures::{BLS_SIGNATURE_AFFINE_SIZE, Signature as BLSSignature}, + solana_cluster_type::ClusterType, + solana_epoch_schedule::EpochSchedule, + solana_fee_calculator::FeeRateGovernor, + solana_genesis_config::GenesisConfig, + solana_hash::Hash, + solana_keypair::Keypair, + solana_leader_schedule::SlotLeader, + solana_native_token::LAMPORTS_PER_SOL, + solana_pubkey::Pubkey, + solana_rent::Rent, + solana_signer::Signer, + solana_stake_interface::state::StakeStateV2, + std::{collections::HashMap, num::NonZero, sync::Arc}, + test_case::test_matrix, + }; + + fn new_bank_for_tests(leader: SlotLeader, genesis_config: &GenesisConfig) -> Bank { + let bank = Bank::new_with_paths_for_tests(genesis_config, None, vec![], Some(leader)); + assert_eq!(*bank.leader(), leader); + bank + } + + fn install_add_stakers( + genesis_config: &mut GenesisConfig, + validators: &[ValidatorVoteKeypairs], + num_add_stakers: u64, + lamports: u64, + ) -> HashMap> { + let mut ret = validators + .iter() + .map(|keypair| { + ( + keypair.vote_keypair.pubkey(), + vec![keypair.stake_keypair.pubkey()], + ) + }) + .collect::>(); + for validator in validators { + let voter_pubkey = validator.vote_keypair.pubkey(); + let vote_account = genesis_config + .accounts + .get(&voter_pubkey) + .unwrap() + .clone() + .into(); + + for i in 0..num_add_stakers { + let staker_keypair = Keypair::new(); + let staker_pubkey = staker_keypair.pubkey(); + let account = Account::from(stake_utils::create_stake_account( + &staker_pubkey, + &voter_pubkey, + &vote_account, + &genesis_config.rent, + lamports + (i + 1) * lamports, + )); + genesis_config.accounts.insert(staker_pubkey, account); + ret.get_mut(&voter_pubkey).unwrap().push(staker_pubkey); + } + } + ret + } + + enum PayType { + Both { + ag_credits: NonZero, + tower_credits: u64, + }, + Tower(u64), + None, + } + + impl PayType { + fn tower(&self) -> u64 { + match self { + Self::Both { tower_credits, .. } => *tower_credits, + Self::Tower(t) => *t, + Self::None => 0, + } + } + + fn ag(&self) -> Option> { + match self { + Self::Both { ag_credits, .. } => Some(*ag_credits), + Self::Tower(_) | Self::None => None, + } + } + } + + struct State { + pay_type: PayType, + commission_bps: u16, + validators: Vec, + /// key is validator pubkey, value is list of stakers. + stakers: HashMap>, + } + + impl State { + fn new( + pay_leader: bool, + num_validators: u64, + num_add_stakers: u64, + commission_bps: u16, + pay_type: PayType, + ) -> (Self, Bank) { + let lamports = LAMPORTS_PER_SOL * 20; + let mint_keypair = Keypair::new(); + let validators = (0..num_validators) + .map(|_| ValidatorVoteKeypairs::new_rand()) + .collect::>(); + let leader = if pay_leader { + let vote_pubkey = validators[0].vote_keypair.pubkey(); + let node_pubkey = validators[0].node_keypair.pubkey(); + SlotLeader { + id: node_pubkey, + vote_address: vote_pubkey, + } + } else { + SlotLeader::new_unique() + }; + let mut genesis_config = create_genesis_config_with_leader_ex( + lamports, + &mint_keypair.pubkey(), + &validators[0].node_keypair.pubkey(), + &validators[0].vote_keypair.pubkey(), + &validators[0].stake_keypair.pubkey(), + Some(validators[0].bls_keypair.public.to_bytes_compressed()), + lamports, + lamports, + FeeRateGovernor::new(0, 0), + Rent::default(), + ClusterType::Development, + &FeatureSet::all_enabled(), + vec![], + ); + genesis_config.epoch_schedule = EpochSchedule::without_warmup(); + activate_all_features(&mut genesis_config); + for (ind, keypair) in validators.iter().enumerate().skip(1) { + let node_pubkey = keypair.node_keypair.pubkey(); + let vote_pubkey = keypair.vote_keypair.pubkey(); + let stake_pubkey = keypair.stake_keypair.pubkey(); + let bls_pubkey = Some(keypair.bls_keypair.public.to_bytes_compressed()); + let lamports = lamports + ind as u64 * LAMPORTS_PER_SOL; + let accounts = create_validator( + &genesis_config.rent, + node_pubkey, + lamports, + vote_pubkey, + lamports, + stake_pubkey, + lamports, + bls_pubkey, + ) + .into_iter() + .map(|(pubkey, account)| (pubkey, Account::from(account))); + genesis_config.accounts.extend(accounts); + } + set_commission(&mut genesis_config, &validators, commission_bps); + + let stakers = + install_add_stakers(&mut genesis_config, &validators, num_add_stakers, lamports); + let bank_epoch0 = new_bank_for_tests(leader, &genesis_config); + assert_eq!(bank_epoch0.epoch(), 0); + + ( + Self { + pay_type, + commission_bps, + validators, + stakers, + }, + bank_epoch0, + ) + } + + fn add_tower_rewards(&self, bank: Arc) -> Arc { + let vote_accounts = bank.vote_accounts(); + let updated_accounts = self + .validators + .iter() + .map(|validator| { + let vote_pubkey = validator.vote_keypair.pubkey(); + let mut vote_state = VoteState::try_new(&vote_accounts, vote_pubkey).unwrap(); + vote_state + .handler + .increment_credits(bank.epoch(), self.pay_type.tower()); + vote_state.serialize().unwrap() + }) + .collect::>(); + bank.store_accounts((bank.slot(), updated_accounts.as_slice())); + let slot = bank.slot() + 10; + new_bank_from_parent(bank, slot) + } + + fn add_ag_rewards(&self, bank: Arc) -> Arc { + let vote_accounts = bank.vote_accounts(); + let updated_accounts = self + .validators + .iter() + .map(|validator| { + let vote_pubkey = validator.vote_keypair.pubkey(); + let mut vote_state = VoteState::try_new(&vote_accounts, vote_pubkey).unwrap(); + if let Some(ag_credits) = self.pay_type.ag() { + increment_credits( + vote_state.handler.epoch_credits_mut(), + bank.epoch(), + bank.epoch(), + ag_credits, + ); + } + vote_state.serialize().unwrap() + }) + .collect::>(); + bank.store_accounts((bank.slot(), updated_accounts.as_slice())); + let slot = bank.slot() + 10; + new_bank_from_parent(bank, slot) + } + + fn get_validator_stake(&self, bank: &Bank, pubkey: &Pubkey) -> u64 { + let rent_exempt_reserve = bank + .rent_collector() + .rent + .minimum_balance(StakeStateV2::size_of()); + self.stakers + .get(pubkey) + .unwrap() + .iter() + .map(|pubkey| { + let lamports = bank.get_account(pubkey).unwrap().lamports(); + lamports - rent_exempt_reserve + }) + .sum() + } + + fn calculate_tower_rewards( + &self, + payout_bank: &Bank, + reward_bank: &Bank, + ) -> HashMap { + if self.pay_type.tower() == 0 { + let mut ret = HashMap::new(); + for stakers in self.stakers.values() { + for staker in stakers { + ret.insert(*staker, 0); + } + } + return ret; + } + + let total_points = self + .validators + .iter() + .map(|validator| { + let vote_pubkey = validator.vote_keypair.pubkey(); + let validator_stake = self.get_validator_stake(reward_bank, &vote_pubkey); + let points = validator_stake * self.pay_type.tower(); + points as u128 + }) + .sum::(); + + let epoch_inflation = payout_bank.calculate_epoch_inflation_rewards( + reward_bank.capitalization(), + reward_bank.epoch(), + ); + let genesis_cert = payout_bank.get_alpenglow_genesis_certificate().unwrap(); + let first_slot_in_reward_epoch = payout_bank + .epoch_schedule + .get_first_slot_in_epoch(reward_bank.epoch()); + let num_tower_slots = genesis_cert.cert_type.slot() - first_slot_in_reward_epoch + 1; + let total_slots = reward_bank.epoch_schedule.slots_per_epoch; + + let rent_exempt_reserve = reward_bank + .rent_collector() + .rent + .minimum_balance(StakeStateV2::size_of()); + let mut ret = HashMap::new(); + for stakers in self.stakers.values() { + for staker in stakers { + let (initial_lamports, _final_lamports) = + self.get_initial_and_final_lamports(reward_bank, payout_bank, staker); + if initial_lamports <= LAMPORTS_PER_SOL + rent_exempt_reserve { + continue; + } + let stake = initial_lamports - rent_exempt_reserve; + let points = (stake * self.pay_type.tower()) as u128; + let reward = points + .checked_mul(u128::from(epoch_inflation)) + .unwrap() + .checked_div(total_points) + .unwrap() + .checked_mul(num_tower_slots as u128) + .unwrap() + .checked_div(total_slots as u128) + .unwrap() + .try_into() + .unwrap(); + ret.insert(*staker, reward); + } + } + ret + } + + fn get_initial_and_final_lamports( + &self, + reward_bank: &Bank, + payout_bank: &Bank, + pubkey: &Pubkey, + ) -> (u64, u64) { + assert!(payout_bank.slot() > reward_bank.slot()); + assert!(payout_bank.epoch() > reward_bank.epoch()); + let initial_lamports = reward_bank.get_account(pubkey).unwrap().lamports(); + let final_lamports = payout_bank.get_account(pubkey).unwrap().lamports(); + (initial_lamports, final_lamports) + } + + fn validate_stakers( + &self, + reward_bank: &Bank, + payout_bank: &Bank, + voter_pubkey: &Pubkey, + tower_rewards: &HashMap, + ) -> u64 { + assert!(payout_bank.slot() > reward_bank.slot()); + assert!(payout_bank.epoch() > reward_bank.epoch()); + let rent_exempt_reserve = reward_bank + .rent_collector() + .rent + .minimum_balance(StakeStateV2::size_of()); + let validator_stake = self.get_validator_stake(reward_bank, voter_pubkey); + let mut expected_validator_reward = 0; + for staker_pubkey in self.stakers.get(voter_pubkey).unwrap().iter() { + let (initial_lamports, final_lamports) = + self.get_initial_and_final_lamports(reward_bank, payout_bank, staker_pubkey); + if initial_lamports <= LAMPORTS_PER_SOL + rent_exempt_reserve { + continue; + } + let stake = initial_lamports - rent_exempt_reserve; + let stake_weighted_tower = *tower_rewards.get(staker_pubkey).unwrap(); + let stake_weighted_ag = + self.pay_type.ag().map(NonZero::get).unwrap_or(0) * stake / validator_stake; + let stake_weighted_reward = stake_weighted_tower + stake_weighted_ag; + let (voter_reward, staker_reward, is_split) = + commission_split(self.commission_bps, stake_weighted_reward); + assert!(is_split); + assert_eq!( + staker_reward, + final_lamports - initial_lamports, + "final={final_lamports}; initial={initial_lamports}" + ); + expected_validator_reward += voter_reward; + } + expected_validator_reward + } + + fn validate_validator_reward( + &self, + reward_bank: &Bank, + payout_bank: &Bank, + tower_rewards: &HashMap, + vote_pubkey: Pubkey, + ) { + let expected_validator_reward = + self.validate_stakers(reward_bank, payout_bank, &vote_pubkey, tower_rewards); + + let (initial_lamports, final_lamports) = + self.get_initial_and_final_lamports(reward_bank, payout_bank, &vote_pubkey); + let diff = final_lamports + + VAT_TO_BURN_PER_EPOCH * (payout_bank.epoch() - reward_bank.epoch()) + - initial_lamports; + assert_eq!(expected_validator_reward, diff); + } + + fn validate_rewards(&self, reward_bank: &Bank, payout_bank: &Bank) { + let tower_rewards = self.calculate_tower_rewards(payout_bank, reward_bank); + for validator in self.validators.iter() { + let vote_pubkey = validator.vote_keypair.pubkey(); + self.validate_validator_reward( + reward_bank, + payout_bank, + &tower_rewards, + vote_pubkey, + ); + } + } + } + + /// Progresses the bank a few times to pay out the rewards. + fn progress_bank_for_payout(mut bank: Arc) -> Arc { + for _ in 0..10 { + let slot = bank.slot() + 1; + bank = new_bank_from_parent(bank, slot); + } + bank + } + + #[test_matrix([true, false], [1_000, 5_000], [0, 10], [PayType::Both{ag_credits: NonZero::new(1023).unwrap(), tower_credits:532}, PayType::Tower(383), PayType::None])] + fn test_migration_epoch( + pay_leader: bool, + commission_bps: u16, + num_add_stakers: u64, + pay_type: PayType, + ) { + let num_validators = 5; + + let (state, mut bank_at_tower) = State::new( + pay_leader, + num_validators, + num_add_stakers, + commission_bps, + pay_type, + ); + bank_at_tower.activate_feature(&agave_feature_set::alpenglow::id()); + let (bank_at_tower, _bank_forks) = bank_at_tower.wrap_with_bank_forks_for_tests(); + + let first_slot_in_migration_epoch = bank_at_tower + .epoch_schedule + .get_first_slot_in_epoch(bank_at_tower.epoch() + 1); + let bank_at_migration0 = new_bank_from_parent(bank_at_tower, first_slot_in_migration_epoch); + + let bank_with_tower_rewards = state.add_tower_rewards(bank_at_migration0); + + let genesis_cert = Certificate { + cert_type: CertificateType::Genesis(bank_with_tower_rewards.slot(), Hash::default()), + signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), + bitmap: vec![], + }; + bank_with_tower_rewards.set_alpenglow_genesis_certificate(&genesis_cert); + let bank_with_genesis_cert_slot = bank_with_tower_rewards.slot() + 10_000; + let bank_with_genesis_cert = + new_bank_from_parent(bank_with_tower_rewards, bank_with_genesis_cert_slot); + + let bank_with_ag_rewards = state.add_ag_rewards(bank_with_genesis_cert); + let reward_epoch = bank_with_ag_rewards.epoch(); + + let first_slot_in_ag_epoch = bank_with_ag_rewards + .epoch_schedule + .get_first_slot_in_epoch(reward_epoch + 1); + let bank_at_ag = new_bank_from_parent(bank_with_ag_rewards.clone(), first_slot_in_ag_epoch); + let progressed_bank = progress_bank_for_payout(bank_at_ag); + + state.validate_rewards(&bank_with_ag_rewards, &progressed_bank); + } +} diff --git a/runtime/src/genesis_utils.rs b/runtime/src/genesis_utils.rs index 57cf44f1eb5..aac6fb1f5bb 100644 --- a/runtime/src/genesis_utils.rs +++ b/runtime/src/genesis_utils.rs @@ -84,6 +84,7 @@ pub const fn genesis_sysvar_and_builtin_program_lamports() -> u64 { + NUM_PRECOMPILES } +#[derive(Debug)] pub struct ValidatorVoteKeypairs { pub node_keypair: Keypair, pub vote_keypair: Keypair, @@ -384,7 +385,7 @@ pub fn bls_pubkey_to_compressed_bytes( bincode::serialize(&key).unwrap().try_into().unwrap() } -fn create_validator( +pub(crate) fn create_validator( rent: &Rent, node_pubkey: Pubkey, node_lamports: u64, diff --git a/runtime/src/inflation_rewards/mod.rs b/runtime/src/inflation_rewards/mod.rs index 570cae4115c..91db3995940 100644 --- a/runtime/src/inflation_rewards/mod.rs +++ b/runtime/src/inflation_rewards/mod.rs @@ -1,16 +1,19 @@ //! Information about stake and voter rewards based on stake state. - +#[cfg(feature = "dev-context-only-utils")] +use qualifier_attr::qualifiers; use { self::points::{ CalculatedStakePoints, CalculationEnvironment, DelegatedVoteState, InflationPointCalculationEvent, SkippedReason, calculate_stake_points_and_credits, }, + crate::{alpenglow_epoch_type::AlpenglowEpochType, epoch_stakes::VersionedEpochStakes}, solana_clock::Epoch, solana_instruction::error::InstructionError, solana_stake_interface::{ error::StakeError, state::{Delegation, Stake}, }, + std::collections::HashMap, }; pub mod points; @@ -34,6 +37,8 @@ pub(crate) fn redeem_rewards<'a>( vote_state: DelegatedVoteState, calculation_environment: CalculationEnvironment<'a>, inflation_point_calc_tracer: Option, + ag_epoch_type: &AlpenglowEpochType, + epoch_stakes: &HashMap, current_lamports: u64, minimum_lamports: u64, ) -> Result<(u64, u64, Stake), InstructionError> { @@ -74,6 +79,8 @@ pub(crate) fn redeem_rewards<'a>( vote_state, calculation_environment, inflation_point_calc_tracer, + ag_epoch_type, + epoch_stakes, current_lamports, minimum_lamports, ) { @@ -89,6 +96,8 @@ fn redeem_stake_rewards<'a>( vote_state: DelegatedVoteState, calculation_environment: CalculationEnvironment<'a>, inflation_point_calc_tracer: Option, + ag_epoch_type: &AlpenglowEpochType, + epoch_stakes: &HashMap, current_lamports: u64, minimum_lamports: u64, ) -> Option<(u64, u64)> { @@ -107,6 +116,8 @@ fn redeem_stake_rewards<'a>( vote_state, calculation_environment, inflation_point_calc_tracer.as_ref(), + ag_epoch_type, + epoch_stakes, ) .map(|calculated_stake_rewards| { if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer { @@ -190,6 +201,8 @@ fn calculate_stake_rewards<'a>( vote_state: DelegatedVoteState, calculation_environment: CalculationEnvironment<'a>, inflation_point_calc_tracer: Option, + ag_epoch_type: &AlpenglowEpochType, + epoch_stakes: &HashMap, ) -> Option { let CalculationEnvironment { stake_history, @@ -198,9 +211,11 @@ fn calculate_stake_rewards<'a>( rewarded_epoch, .. } = calculation_environment; + // ensure to run to trigger (optional) inflation_point_calc_tracer let CalculatedStakePoints { - points, + tower_points, + ag_points, new_credits_observed, mut force_credits_update_with_skipped_reward, } = calculate_stake_points_and_credits( @@ -209,6 +224,8 @@ fn calculate_stake_rewards<'a>( stake_history, inflation_point_calc_tracer.as_ref(), new_rate_activation_epoch, + ag_epoch_type, + epoch_stakes, ); // Drive credits_observed forward unconditionally when rewards are disabled @@ -234,25 +251,69 @@ fn calculate_stake_rewards<'a>( }); } - if points == 0 { - if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() { - inflation_point_calc_tracer(&SkippedReason::ZeroPoints.into()); + let rewards = match ag_epoch_type { + AlpenglowEpochType::Alpenglow { .. } => { + if ag_points == 0 { + if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() { + inflation_point_calc_tracer(&SkippedReason::ZeroPoints.into()); + } + return None; + } + // In alpenglow, `points` represents the actual reward that this `vote_state` earned. + ag_points } - return None; - } - if point_value.points == 0 { - if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() { - inflation_point_calc_tracer(&SkippedReason::ZeroPointValue.into()); + AlpenglowEpochType::Tower => { + if tower_points == 0 { + if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() { + inflation_point_calc_tracer(&SkippedReason::ZeroPoints.into()); + } + return None; + } + if point_value.points == 0 { + if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() { + inflation_point_calc_tracer(&SkippedReason::ZeroPointValue.into()); + } + return None; + } + // In tower, `points` still needs to be scaled by `point_value` to calculate this + // `vote_state` earned. + // The final unwrap is safe, as points_value.points is guaranteed to be non zero above. + tower_points + .checked_mul(u128::from(point_value.rewards)) + .expect("Rewards intermediate calculation should fit within u128") + .checked_div(point_value.points) + .unwrap() } - return None; - } - - // The final unwrap is safe, as points_value.points is guaranteed to be non zero above. - let rewards = points - .checked_mul(u128::from(point_value.rewards)) - .expect("Rewards intermediate calculation should fit within u128") - .checked_div(point_value.points) - .unwrap(); + AlpenglowEpochType::MigrationEpoch { + num_tower_slots, + num_ag_slots, + migration_epoch: _, + } => { + if tower_points == 0 && ag_points == 0 { + if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() { + inflation_point_calc_tracer(&SkippedReason::ZeroPoints.into()); + } + return None; + } + if ag_points == 0 && point_value.points == 0 { + if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() { + inflation_point_calc_tracer(&SkippedReason::ZeroPointValue.into()); + } + return None; + } + let total_slots = (num_tower_slots + num_ag_slots) as u128; + let tower_points = tower_points + .checked_mul(u128::from(point_value.rewards)) + .expect("Rewards intermediate calculation should fit within u128") + .checked_div(point_value.points) + .unwrap() + .checked_mul(*num_tower_slots as u128) + .unwrap() + .checked_div(total_slots) + .unwrap(); + tower_points + ag_points + } + }; let rewards = u64::try_from(rewards).expect("Rewards should fit within u64"); @@ -296,6 +357,7 @@ fn calculate_stake_rewards<'a>( /// indicate with false for was_split /// /// DEVELOPER NOTE: This function used to be a method on VoteState, but was moved here +#[cfg_attr(any(test, feature = "dev-context-only-utils"), qualifiers(pub(crate)))] fn commission_split(commission_bps: u16, on: u64) -> (u64, u64, bool) { const MAX_BPS: u16 = 10_000; const MAX_BPS_U128: u128 = MAX_BPS as u128; @@ -332,6 +394,7 @@ mod tests { use { self::points::{PointValue, null_tracer}, super::*, + crate::epoch_stakes::VersionedEpochStakes, proptest::prelude::*, solana_clock::Epoch, solana_native_token::LAMPORTS_PER_SOL, @@ -341,8 +404,10 @@ mod tests { stake_history::StakeHistory, state::{Delegation, StakeStateV2}, }, + solana_vote::vote_account::VoteAccount, solana_vote_program::vote_state::{VoteStateV4, handler::VoteStateHandler}, - test_case::test_case, + std::collections::HashMap, + test_case::{test_case, test_matrix}, }; fn new_stake( @@ -357,9 +422,34 @@ mod tests { } } - #[test_case(true; "adjust_delegations_for_rent")] - #[test_case(false; "no_adjust_delegations_for_rent")] - fn test_stake_state_redeem_rewards(adjust_delegations_for_rent: bool) { + /// Returns an instance of `AlpenglowEpochType`, epoch_stakes, total stake, and first AG epoch. + fn get_ag_epoch_type() -> ( + AlpenglowEpochType, + HashMap, + u64, + Epoch, + ) { + let total_stake = 1_000; + let vote_account = VoteAccount::new_random(); + let vote_account_hash_map = [(Pubkey::default(), (total_stake, vote_account))] + .into_iter() + .collect(); + let versioned_epoch_stakes = VersionedEpochStakes::new_for_tests(vote_account_hash_map, 0); + let epoch_stakes = (0..10) + .map(|epoch| (epoch, versioned_epoch_stakes.clone())) + .collect(); + let migration_epoch = 0; + let first_ag_epoch = migration_epoch + 1; + ( + AlpenglowEpochType::Alpenglow { migration_epoch }, + epoch_stakes, + total_stake, + first_ag_epoch, + ) + } + + #[test_matrix([true, false], [true, false])] + fn test_stake_state_redeem_rewards(adjust_delegations_for_rent: bool, ag_enabled: bool) { let mut vote_state = VoteStateHandler::new_v4(VoteStateV4::default()); // assume stake.stake() is right // bootstrap means fully-vested stake at epoch 0 @@ -374,6 +464,24 @@ mod tests { let new_rate_activation_epoch = None; let commission_rate_in_basis_points = true; + // epoch credits work differently in AG, so we need a multiplier to account for that. + let (ag_epoch_type, epoch_stakes, ag_total_stake_multiplier) = if ag_enabled { + let (ag_epoch_type, epoch_stakes, ag_total_stake_multiplier, _) = get_ag_epoch_type(); + (ag_epoch_type, epoch_stakes, ag_total_stake_multiplier) + } else { + (AlpenglowEpochType::Tower, HashMap::new(), 1) + }; + + let inc_credits = |handler: &mut VoteStateHandler, epoch: Epoch, credits: u64| { + if ag_enabled { + let (_, _, _, first_ag_epoch) = get_ag_epoch_type(); + handler + .increment_credits(epoch + first_ag_epoch, credits * ag_total_stake_multiplier); + } else { + handler.increment_credits(epoch, credits); + } + }; + let mut rent = Rent::default(); let minimum_balance = rent.minimum_balance(StakeStateV2::size_of()); @@ -402,14 +510,15 @@ mod tests { adjust_delegations_for_rent, }, null_tracer(), + &ag_epoch_type, + &epoch_stakes, stake_lamports + minimum_balance, new_minimum_balance, ) ); // put 2 credits in at epoch 0 - vote_state.increment_credits(0, 1); - vote_state.increment_credits(0, 1); + inc_credits(&mut vote_state, 0, 2); // this one should be able to collect exactly 2 assert_eq!( @@ -430,6 +539,8 @@ mod tests { adjust_delegations_for_rent, }, null_tracer(), + &ag_epoch_type, + &epoch_stakes, stake_lamports + minimum_balance, new_minimum_balance, ) @@ -439,11 +550,11 @@ mod tests { stake.delegation.stake, stake_lamports + (stake_lamports * 2) ); - assert_eq!(stake.credits_observed, 2); + assert_eq!(stake.credits_observed, 2 * ag_total_stake_multiplier); } - #[test] - fn test_stake_state_calculate_rewards() { + #[test_matrix([true, false])] + fn test_stake_state_calculate_rewards(ag_enabled: bool) { let mut vote_state = VoteStateHandler::new_v4(VoteStateV4::default()); // assume stake.stake() is right // bootstrap means fully-vested stake at epoch 0 @@ -454,6 +565,24 @@ mod tests { let commission_rate_in_basis_points = true; let adjust_delegations_for_rent = true; + // epoch credits work differently in AG, so we need a multiplier to account for that. + let (ag_epoch_type, epoch_stakes, ag_total_stake_multiplier) = if ag_enabled { + let (ag_epoch_type, epoch_stakes, ag_total_stake_multiplier, _) = get_ag_epoch_type(); + (ag_epoch_type, epoch_stakes, ag_total_stake_multiplier) + } else { + (AlpenglowEpochType::Tower, HashMap::new(), 1) + }; + + let inc_credits = |handler: &mut VoteStateHandler, epoch: Epoch, credits: u64| { + if ag_enabled { + let (_, _, _, first_ag_epoch) = get_ag_epoch_type(); + handler + .increment_credits(epoch + first_ag_epoch, credits * ag_total_stake_multiplier); + } else { + handler.increment_credits(epoch, credits); + } + }; + // this one can't collect now, credits_observed == vote_state.credits() assert_eq!( None, @@ -473,19 +602,20 @@ mod tests { adjust_delegations_for_rent, }, null_tracer(), + &ag_epoch_type, + &epoch_stakes ) ); // put 2 credits in at epoch 0 - vote_state.increment_credits(0, 1); - vote_state.increment_credits(0, 1); + inc_credits(&mut vote_state, 0, 2); // this one should be able to collect exactly 2 assert_eq!( Some(CalculatedStakeRewards { staker_rewards: stake.delegation.stake * 2, voter_rewards: 0, - new_credits_observed: 2, + new_credits_observed: 2 * ag_total_stake_multiplier, }), calculate_stake_rewards( &stake, @@ -503,16 +633,18 @@ mod tests { adjust_delegations_for_rent, }, null_tracer(), + &ag_epoch_type, + &epoch_stakes ) ); - stake.credits_observed = 1; + stake.credits_observed = ag_total_stake_multiplier; // this one should be able to collect exactly 1 (already observed one) assert_eq!( Some(CalculatedStakeRewards { staker_rewards: stake.delegation.stake, voter_rewards: 0, - new_credits_observed: 2, + new_credits_observed: 2 * ag_total_stake_multiplier, }), calculate_stake_rewards( &stake, @@ -530,19 +662,21 @@ mod tests { adjust_delegations_for_rent, }, null_tracer(), + &ag_epoch_type, + &epoch_stakes ) ); // put 1 credit in epoch 1 - vote_state.increment_credits(1, 1); + inc_credits(&mut vote_state, 1, 1); - stake.credits_observed = 2; + stake.credits_observed = 2 * ag_total_stake_multiplier; // this one should be able to collect the one just added assert_eq!( Some(CalculatedStakeRewards { staker_rewards: stake.delegation.stake, voter_rewards: 0, - new_credits_observed: 3, + new_credits_observed: 3 * ag_total_stake_multiplier, }), calculate_stake_rewards( &stake, @@ -560,17 +694,19 @@ mod tests { adjust_delegations_for_rent, }, null_tracer(), + &ag_epoch_type, + &epoch_stakes, ) ); // put 1 credit in epoch 2 - vote_state.increment_credits(2, 1); + inc_credits(&mut vote_state, 2, 1); // this one should be able to collect 2 now assert_eq!( Some(CalculatedStakeRewards { staker_rewards: stake.delegation.stake * 2, voter_rewards: 0, - new_credits_observed: 4, + new_credits_observed: 4 * ag_total_stake_multiplier, }), calculate_stake_rewards( &stake, @@ -588,6 +724,8 @@ mod tests { adjust_delegations_for_rent, }, null_tracer(), + &ag_epoch_type, + &epoch_stakes, ) ); @@ -600,7 +738,7 @@ mod tests { + stake.delegation.stake // epoch 1 + stake.delegation.stake, // epoch 2 voter_rewards: 0, - new_credits_observed: 4, + new_credits_observed: 4 * ag_total_stake_multiplier, }), calculate_stake_rewards( &stake, @@ -618,6 +756,8 @@ mod tests { adjust_delegations_for_rent, }, null_tracer(), + &ag_epoch_type, + &epoch_stakes, ) ); @@ -642,6 +782,8 @@ mod tests { adjust_delegations_for_rent, }, null_tracer(), + &ag_epoch_type, + &epoch_stakes, ) ); vote_state.set_inflation_rewards_commission_bps(9900); @@ -663,6 +805,8 @@ mod tests { adjust_delegations_for_rent, }, null_tracer(), + &ag_epoch_type, + &epoch_stakes, ) ); @@ -673,7 +817,7 @@ mod tests { Some(CalculatedStakeRewards { staker_rewards: 0, voter_rewards: 0, - new_credits_observed: 4, + new_credits_observed: 4 * ag_total_stake_multiplier, }), calculate_stake_rewards( &stake, @@ -691,17 +835,19 @@ mod tests { adjust_delegations_for_rent, }, null_tracer(), + &ag_epoch_type, + &epoch_stakes, ) ); // credits_observed remains at previous level when vote_state credits are // not advancing and inflation is disabled - stake.credits_observed = 4; + stake.credits_observed = 4 * ag_total_stake_multiplier; assert_eq!( Some(CalculatedStakeRewards { staker_rewards: 0, voter_rewards: 0, - new_credits_observed: 4, + new_credits_observed: 4 * ag_total_stake_multiplier, }), calculate_stake_rewards( &stake, @@ -719,13 +865,16 @@ mod tests { adjust_delegations_for_rent, }, null_tracer(), + &ag_epoch_type, + &epoch_stakes, ) ); assert_eq!( CalculatedStakePoints { - points: 0, - new_credits_observed: 4, + tower_points: 0, + ag_points: 0, + new_credits_observed: 4 * ag_total_stake_multiplier, force_credits_update_with_skipped_reward: false, }, calculate_stake_points_and_credits( @@ -733,18 +882,21 @@ mod tests { DelegatedVoteState::from(vote_state.as_ref_v4()), &StakeHistory::default(), null_tracer(), - None + None, + &ag_epoch_type, + &epoch_stakes, ) ); // credits_observed is auto-rewound when vote_state credits are assumed to have been // recreated - stake.credits_observed = 1000; + stake.credits_observed = 1000 * ag_total_stake_multiplier; // this is new behavior 1; return the post-recreation rewound credits from the vote account assert_eq!( CalculatedStakePoints { - points: 0, - new_credits_observed: 4, + tower_points: 0, + ag_points: 0, + new_credits_observed: 4 * ag_total_stake_multiplier, force_credits_update_with_skipped_reward: true, }, calculate_stake_points_and_credits( @@ -752,15 +904,18 @@ mod tests { DelegatedVoteState::from(vote_state.as_ref_v4()), &StakeHistory::default(), null_tracer(), - None + None, + &ag_epoch_type, + &epoch_stakes, ) ); // this is new behavior 2; don't hint when credits both from stake and vote are identical - stake.credits_observed = 4; + stake.credits_observed = 4 * ag_total_stake_multiplier; assert_eq!( CalculatedStakePoints { - points: 0, - new_credits_observed: 4, + tower_points: 0, + ag_points: 0, + new_credits_observed: 4 * ag_total_stake_multiplier, force_credits_update_with_skipped_reward: false, }, calculate_stake_points_and_credits( @@ -768,19 +923,21 @@ mod tests { DelegatedVoteState::from(vote_state.as_ref_v4()), &StakeHistory::default(), null_tracer(), - None + None, + &ag_epoch_type, + &epoch_stakes, ) ); // get rewards and credits observed when not the activation epoch vote_state.set_inflation_rewards_commission_bps(0); - stake.credits_observed = 3; + stake.credits_observed = 3 * ag_total_stake_multiplier; stake.delegation.activation_epoch = 1; assert_eq!( Some(CalculatedStakeRewards { staker_rewards: stake.delegation.stake, // epoch 2 voter_rewards: 0, - new_credits_observed: 4, + new_credits_observed: 4 * ag_total_stake_multiplier, }), calculate_stake_rewards( &stake, @@ -798,18 +955,20 @@ mod tests { adjust_delegations_for_rent, }, null_tracer(), + &ag_epoch_type, + &epoch_stakes ) ); // credits_observed is moved forward for the stake's activation epoch, // and no rewards are perceived stake.delegation.activation_epoch = 2; - stake.credits_observed = 3; + stake.credits_observed = 3 * ag_total_stake_multiplier; assert_eq!( Some(CalculatedStakeRewards { staker_rewards: 0, voter_rewards: 0, - new_credits_observed: 4, + new_credits_observed: 4 * ag_total_stake_multiplier, }), calculate_stake_rewards( &stake, @@ -827,6 +986,8 @@ mod tests { adjust_delegations_for_rent, }, null_tracer(), + &ag_epoch_type, + &epoch_stakes ) ); } @@ -864,6 +1025,8 @@ mod tests { adjust_delegations_for_rent, }, null_tracer(), + &AlpenglowEpochType::Tower, + &HashMap::new(), pre_lamports, new_minimum_balance, ); @@ -1122,11 +1285,13 @@ mod tests { adjust_delegations_for_rent, }, null_tracer(), + &AlpenglowEpochType::Tower, + &HashMap::new(), ); } - #[test] - fn test_stake_state_calculate_points_with_typical_values() { + #[test_matrix([true, false])] + fn test_stake_state_calculate_points_with_typical_values(ag_enabled: bool) { let vote_state = VoteStateHandler::new_v4(VoteStateV4::default()); // bootstrap means fully-vested stake at epoch 0 with @@ -1142,6 +1307,13 @@ mod tests { let commission_rate_in_basis_points = true; let adjust_delegations_for_rent = true; + let (ag_stake_state, epoch_stakes) = if ag_enabled { + let (state, epoch_stakes, _stake, _first_ag_epoch) = get_ag_epoch_type(); + (state, epoch_stakes) + } else { + (AlpenglowEpochType::Tower, HashMap::new()) + }; + // this one can't collect now, credits_observed == vote_state.credits() assert_eq!( None, @@ -1161,6 +1333,8 @@ mod tests { adjust_delegations_for_rent, }, null_tracer(), + &ag_stake_state, + &epoch_stakes ) ); } diff --git a/runtime/src/inflation_rewards/points.rs b/runtime/src/inflation_rewards/points.rs index bd91dd5d1b0..9fb39db56f9 100644 --- a/runtime/src/inflation_rewards/points.rs +++ b/runtime/src/inflation_rewards/points.rs @@ -1,6 +1,12 @@ //! Information about points calculation based on stake state. use { + crate::{ + alpenglow_epoch_type::AlpenglowEpochType, + block_component_processor::vote_reward::AG_MIGRATION_EPOCH_CREDIT, + epoch_stakes::VersionedEpochStakes, + }, + log::{error, trace}, solana_clock::Epoch, solana_instruction::error::InstructionError, solana_pubkey::Pubkey, @@ -9,7 +15,7 @@ use { state::{Delegation, Stake, StakeStateV2}, }, solana_vote::vote_state_view::VoteStateView, - std::cmp::Ordering, + std::{cmp::Ordering, collections::HashMap}, }; /// captures a rewards round as lamports to be awarded @@ -24,7 +30,8 @@ pub struct PointValue { #[derive(Debug, PartialEq, Eq)] pub(crate) struct CalculatedStakePoints { - pub(crate) points: u128, + pub(crate) tower_points: u128, + pub(crate) ag_points: u128, pub(crate) new_credits_observed: u64, pub(crate) force_credits_update_with_skipped_reward: bool, } @@ -94,31 +101,36 @@ impl<'a> From<&'a VoteStateView> for DelegatedVoteState<'a> { } } -pub(crate) fn calculate_points( +pub(crate) fn calculate_points_for_tower( stake_state: &StakeStateV2, vote_state: DelegatedVoteState, stake_history: &StakeHistory, new_rate_activation_epoch: Option, + epoch_stakes: &HashMap, ) -> Result { if let StakeStateV2::Stake(_meta, stake, _stake_flags) = stake_state { - Ok(calculate_stake_points( + Ok(calculate_stake_points_for_tower( stake, vote_state, stake_history, null_tracer(), new_rate_activation_epoch, + epoch_stakes, )) } else { Err(InstructionError::InvalidAccountData) } } -fn calculate_stake_points( +/// This function is used to calculate `PointValue::points` for tower epoch and tower portion of +/// the migration epoch. +fn calculate_stake_points_for_tower( stake: &Stake, vote_state: DelegatedVoteState, stake_history: &StakeHistory, inflation_point_calc_tracer: Option, new_rate_activation_epoch: Option, + epoch_stakes: &HashMap, ) -> u128 { calculate_stake_points_and_credits( stake, @@ -126,8 +138,238 @@ fn calculate_stake_points( stake_history, inflation_point_calc_tracer, new_rate_activation_epoch, + &AlpenglowEpochType::Tower, + epoch_stakes, ) - .points + .tower_points +} + +/// Returns the total stake delegated to `vote_pubkey` in the given `epoch`. +fn get_total_stake( + vote_pubkey: Pubkey, + epoch_stakes: &HashMap, + epoch: Epoch, +) -> Result { + let Some(rank_map) = epoch_stakes.get(&epoch).map(|e| e.bls_pubkey_to_rank_map()) else { + record_error(format!( + "epoch_stakes.get(epoch={epoch}) for vote_pubkey={vote_pubkey} failed" + )); + return Err(()); + }; + let Some(rank) = rank_map.get_rank_for_vote_pubkey(&vote_pubkey) else { + // this is benign and can happen if a staker stakes with a validator not + // in the validator set. + record_trace(format!( + "get_rank_for_vote_pubkey(vote_pubkey={vote_pubkey}) in epoch={epoch} failed" + )); + return Err(()); + }; + let Some(entry) = rank_map.get_pubkey_stake_entry(*rank as usize) else { + record_error(format!( + "get_pubkey_stake_entry(rank={rank}) for vote_pubkey={vote_pubkey} in epoch={epoch} \ + failed", + )); + return Err(()); + }; + Ok(entry.stake) +} + +fn record_error(msg: String) { + error!("{msg}"); + datapoint_error!( + "PER-total-stake-calculation-failure", + ("error", msg, String) + ); +} + +fn record_trace(msg: String) { + trace!("{msg}"); + datapoint_trace!( + "PER-total-stake-calculation-failure", + ("trace", msg, String) + ); +} + +/// Returns how many credits the stake actually earned based on what it has already observed and +/// the vote account has recorded. +/// +/// Also updates the `new_credits_observed` as a side effect. +fn calc_earned_credits( + stake: &Stake, + final_epoch_credits: u64, + initial_epoch_credits: u64, + new_credits_observed: &mut u64, +) -> u128 { + let credits_in_stake = stake.credits_observed; + + // figure out how much this stake has seen that + // for which the vote account has a record + let earned_credits = if credits_in_stake < initial_epoch_credits { + // the staker observed the entire epoch + final_epoch_credits - initial_epoch_credits + } else if credits_in_stake < final_epoch_credits { + // the staker registered sometime during the epoch, partial credit + final_epoch_credits - *new_credits_observed + } else { + // the staker has already observed or been redeemed this epoch + // or was activated after this epoch + 0 + }; + *new_credits_observed = (*new_credits_observed).max(final_epoch_credits); + u128::from(earned_credits) +} + +/// Calculates tower points. +/// +/// Returns (points calculated, new_credits_observed, +/// true if AG_MIGRATION_EPOCH_CREDIT was seen else false) +fn tower_epoch_credits_iter( + stake: &Stake, + epoch_credits_iter: impl Iterator, + stake_history: &StakeHistory, + inflation_point_calc_tracer: Option, + new_rate_activation_epoch: Option, +) -> (u128, u64, bool) { + let mut points = 0; + let credits_in_stake = stake.credits_observed; + let mut new_credits_observed = credits_in_stake; + let mut saw_marker = false; + + for entry in epoch_credits_iter { + if entry == AG_MIGRATION_EPOCH_CREDIT { + saw_marker = true; + break; + } + let (epoch, final_epoch_credits, initial_epoch_credits) = entry; + let earned_credits = calc_earned_credits( + stake, + final_epoch_credits, + initial_epoch_credits, + &mut new_credits_observed, + ); + #[allow(deprecated)] + let stake_amount = u128::from(stake.delegation.stake( + epoch, + stake_history, + new_rate_activation_epoch, + )); + + // finally calculate points for this epoch + let earned_points = stake_amount * earned_credits; + points += earned_points; + + if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() { + inflation_point_calc_tracer(&InflationPointCalculationEvent::CalculatedPoints( + epoch, + stake_amount, + earned_credits, + earned_points, + )); + } + } + (points, new_credits_observed, saw_marker) +} + +fn ag_epoch_credits_iter( + migration_epoch: Epoch, + mut saw_marker: bool, + stake: &Stake, + epoch_credits_iter: impl Iterator, + stake_history: &StakeHistory, + inflation_point_calc_tracer: Option, + new_rate_activation_epoch: Option, + epoch_stakes: &HashMap, +) -> Result<(u128, u64), CalculatedStakePoints> { + let mut points = 0; + let credits_in_stake = stake.credits_observed; + let mut new_credits_observed = credits_in_stake; + + for entry in epoch_credits_iter { + let (epoch, final_epoch_credits, initial_epoch_credits) = entry; + if epoch < migration_epoch { + continue; + } + if entry == AG_MIGRATION_EPOCH_CREDIT { + debug_assert!(!saw_marker); + saw_marker = true; + continue; + } + if epoch == migration_epoch && !saw_marker { + continue; + } + + let earned_credits = calc_earned_credits( + stake, + final_epoch_credits, + initial_epoch_credits, + &mut new_credits_observed, + ); + #[allow(deprecated)] + let stake_amount = u128::from(stake.delegation.stake( + epoch, + stake_history, + new_rate_activation_epoch, + )); + + let earned_points = { + if earned_credits == 0 { + earned_credits + } else { + let Ok(total_stake) = + get_total_stake(stake.delegation.voter_pubkey, epoch_stakes, epoch) + else { + return Err(CalculatedStakePoints { + tower_points: 0, + ag_points: 0, + new_credits_observed, + force_credits_update_with_skipped_reward: true, + }); + }; + earned_credits * stake_amount / total_stake as u128 + } + }; + points += earned_points; + if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() { + inflation_point_calc_tracer(&InflationPointCalculationEvent::CalculatedPoints( + epoch, + stake_amount, + earned_credits, + earned_points, + )); + } + } + Ok((points, new_credits_observed)) +} + +fn migrating_epoch_credits_iter( + migration_epoch: Epoch, + stake: &Stake, + mut epoch_credits_iter: impl Iterator, + stake_history: &StakeHistory, + inflation_point_calc_tracer: Option, + new_rate_activation_epoch: Option, + epoch_stakes: &HashMap, +) -> Result<(u128, u128, u64), CalculatedStakePoints> { + let (tower_points, tower_new_credits_observed, saw_marker) = tower_epoch_credits_iter( + stake, + epoch_credits_iter.by_ref(), + stake_history, + inflation_point_calc_tracer.as_ref(), + new_rate_activation_epoch, + ); + let (ag_points, ag_new_credits_observed) = ag_epoch_credits_iter( + migration_epoch, + saw_marker, + stake, + epoch_credits_iter, + stake_history, + inflation_point_calc_tracer, + new_rate_activation_epoch, + epoch_stakes, + )?; + + let new_credits_observed = tower_new_credits_observed.max(ag_new_credits_observed); + Ok((tower_points, ag_points, new_credits_observed)) } /// for a given stake and vote_state, calculate how many @@ -139,6 +381,8 @@ pub(crate) fn calculate_stake_points_and_credits( stake_history: &StakeHistory, inflation_point_calc_tracer: Option, new_rate_activation_epoch: Option, + ag_epoch_type: &AlpenglowEpochType, + epoch_stakes: &HashMap, ) -> CalculatedStakePoints { let credits_in_stake = stake.credits_observed; let credits_in_vote = vote_state.credits; @@ -167,7 +411,8 @@ pub(crate) fn calculate_stake_points_and_credits( // hint with true to indicate some exceptional credits handling is needed return CalculatedStakePoints { - points: 0, + tower_points: 0, + ag_points: 0, new_credits_observed: credits_in_vote, force_credits_update_with_skipped_reward: true, }; @@ -178,7 +423,8 @@ pub(crate) fn calculate_stake_points_and_credits( } // don't hint caller and return current value if credits remain unchanged (= delinquent) return CalculatedStakePoints { - points: 0, + tower_points: 0, + ag_points: 0, new_credits_observed: credits_in_stake, force_credits_update_with_skipped_reward: false, }; @@ -186,52 +432,53 @@ pub(crate) fn calculate_stake_points_and_credits( Ordering::Greater => {} } - let mut points = 0; - let mut new_credits_observed = credits_in_stake; - - for epoch_credits_item in vote_state.epoch_credits_iter { - let (epoch, final_epoch_credits, initial_epoch_credits) = epoch_credits_item; - #[allow(deprecated)] - let stake_amount = u128::from(stake.delegation.stake( - epoch, - stake_history, - new_rate_activation_epoch, - )); - - // figure out how much this stake has seen that - // for which the vote account has a record - let earned_credits = if credits_in_stake < initial_epoch_credits { - // the staker observed the entire epoch - final_epoch_credits - initial_epoch_credits - } else if credits_in_stake < final_epoch_credits { - // the staker registered sometime during the epoch, partial credit - final_epoch_credits - new_credits_observed - } else { - // the staker has already observed or been redeemed this epoch - // or was activated after this epoch - 0 - }; - let earned_credits = u128::from(earned_credits); - - // don't want to assume anything about order of the iterator... - new_credits_observed = new_credits_observed.max(final_epoch_credits); - - // finally calculate points for this epoch - let earned_points = stake_amount * earned_credits; - points += earned_points; - - if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() { - inflation_point_calc_tracer(&InflationPointCalculationEvent::CalculatedPoints( - epoch, - stake_amount, - earned_credits, - earned_points, - )); + let (tower_points, ag_points, new_credits_observed) = match ag_epoch_type { + AlpenglowEpochType::Tower => { + let (points, credits, _) = tower_epoch_credits_iter( + stake, + vote_state.epoch_credits_iter, + stake_history, + inflation_point_calc_tracer, + new_rate_activation_epoch, + ); + (points, 0, credits) } - } - + AlpenglowEpochType::MigrationEpoch { + migration_epoch, .. + } => { + match migrating_epoch_credits_iter( + *migration_epoch, + stake, + vote_state.epoch_credits_iter, + stake_history, + inflation_point_calc_tracer, + new_rate_activation_epoch, + epoch_stakes, + ) { + Ok(r) => r, + Err(e) => return e, + } + } + AlpenglowEpochType::Alpenglow { migration_epoch } => { + let (ag_points, credits) = match ag_epoch_credits_iter( + *migration_epoch, + false, + stake, + vote_state.epoch_credits_iter, + stake_history, + inflation_point_calc_tracer, + new_rate_activation_epoch, + epoch_stakes, + ) { + Ok(r) => r, + Err(e) => return e, + }; + (0, ag_points, credits) + } + }; CalculatedStakePoints { - points, + tower_points, + ag_points, new_credits_observed, force_credits_update_with_skipped_reward: false, } @@ -242,6 +489,7 @@ mod tests { use { super::*, solana_native_token::LAMPORTS_PER_SOL, + solana_vote::vote_account::VoteAccount, solana_vote_program::vote_state::{VoteStateV4, handler::VoteStateHandler}, }; @@ -289,13 +537,468 @@ mod tests { // no overflow on points assert_eq!( u128::from(stake.delegation.stake) * epoch_slots, - calculate_stake_points( + calculate_stake_points_for_tower( &stake, DelegatedVoteState::from(vote_state.as_ref_v4()), &StakeHistory::default(), null_tracer(), - None + None, + &HashMap::new() ) ); } + + #[test] + fn test_tower_epoch_credits_iter() { + let stake_lamports = 10_000_000 * LAMPORTS_PER_SOL; + let credits = 1235; + + let stake = new_stake( + stake_lamports, + &Pubkey::default(), + VoteStateHandler::new_v4(VoteStateV4::default()).as_ref_v4(), + u64::MAX, + ); + + let epoch_credits = vec![(0, credits, 0), (1, credits * 2, credits)]; + let mut epoch_credits_iter = epoch_credits.into_iter(); + let (points, new_credits, saw_marker) = tower_epoch_credits_iter( + &stake, + epoch_credits_iter.by_ref(), + &StakeHistory::default(), + null_tracer(), + None, + ); + assert_eq!(points, credits as u128 * stake_lamports as u128 * 2); + assert_eq!(new_credits, credits * 2); + assert_eq!(epoch_credits_iter.next(), None); + assert!(!saw_marker); + + let epoch_credits = vec![ + (0, credits, 0), + (1, credits * 2, credits), + AG_MIGRATION_EPOCH_CREDIT, + ]; + let mut epoch_credits_iter = epoch_credits.into_iter(); + let (points, new_credits, saw_marker) = tower_epoch_credits_iter( + &stake, + epoch_credits_iter.by_ref(), + &StakeHistory::default(), + null_tracer(), + None, + ); + assert_eq!(points, credits as u128 * stake_lamports as u128 * 2); + assert_eq!(new_credits, credits * 2); + assert_eq!(epoch_credits_iter.next(), None); + assert!(saw_marker); + + let epoch_credits = vec![ + (0, credits, 0), + (1, credits * 2, credits), + AG_MIGRATION_EPOCH_CREDIT, + (1, credits * 3, credits * 2), + ]; + let mut epoch_credits_iter = epoch_credits.into_iter(); + let (points, new_credits, saw_marker) = tower_epoch_credits_iter( + &stake, + epoch_credits_iter.by_ref(), + &StakeHistory::default(), + null_tracer(), + None, + ); + assert_eq!(points, credits as u128 * stake_lamports as u128 * 2); + assert_eq!(new_credits, credits * 2); + assert_eq!( + epoch_credits_iter.next().unwrap(), + (1, credits * 3, credits * 2) + ); + assert!(saw_marker); + } + + #[test] + fn test_ag_epoch_credits_iter() { + let stake_lamports = 10_000_000 * LAMPORTS_PER_SOL; + let total_stake = stake_lamports * 2; + let credits = 1235; + + let stake = new_stake( + stake_lamports, + &Pubkey::default(), + VoteStateHandler::new_v4(VoteStateV4::default()).as_ref_v4(), + u64::MAX, + ); + + let vote_account = VoteAccount::new_random(); + let vote_account_hash_map = [(Pubkey::default(), (total_stake, vote_account))] + .into_iter() + .collect(); + let versioned_epoch_stakes = VersionedEpochStakes::new_for_tests(vote_account_hash_map, 0); + let epoch_stakes = (0..10) + .map(|epoch| (epoch, versioned_epoch_stakes.clone())) + .collect(); + + for saw_marker in [true, false] { + let epoch_credits = vec![(0, credits, 0), (1, credits * 2, credits)]; + let (points, new_credits) = ag_epoch_credits_iter( + 2, + saw_marker, + &stake, + epoch_credits.into_iter(), + &StakeHistory::default(), + null_tracer(), + None, + &epoch_stakes, + ) + .unwrap(); + assert_eq!(points, 0); + assert_eq!(new_credits, 0); + } + + for saw_marker in [true, false] { + let epoch_credits = vec![(0, credits, 0), (1, credits * 2, credits)]; + let (points, new_credits) = ag_epoch_credits_iter( + 0, + saw_marker, + &stake, + epoch_credits.into_iter(), + &StakeHistory::default(), + null_tracer(), + None, + &epoch_stakes, + ) + .unwrap(); + if saw_marker { + assert_eq!( + points, + (credits as u128 * stake_lamports as u128 / total_stake as u128) * 2 + ); + } else { + assert_eq!( + points, + credits as u128 * stake_lamports as u128 / total_stake as u128 + ); + } + assert_eq!(new_credits, credits * 2); + } + + let epoch_credits = vec![ + (0, credits, 0), + AG_MIGRATION_EPOCH_CREDIT, + (0, credits * 2, credits), + (1, credits * 3, credits * 2), + ]; + let (points, new_credits) = ag_epoch_credits_iter( + 0, + false, + &stake, + epoch_credits.into_iter(), + &StakeHistory::default(), + null_tracer(), + None, + &epoch_stakes, + ) + .unwrap(); + assert_eq!( + points, + (credits as u128 * stake_lamports as u128 / total_stake as u128) * 2 + ); + assert_eq!(new_credits, credits * 3); + + for saw_marker in [true, false] { + let epoch_credits = vec![(1, credits, 0), (2, credits * 2, credits)]; + let (points, new_credits) = ag_epoch_credits_iter( + 0, + saw_marker, + &stake, + epoch_credits.into_iter(), + &StakeHistory::default(), + null_tracer(), + None, + &epoch_stakes, + ) + .unwrap(); + assert_eq!( + points, + (credits as u128 * stake_lamports as u128 / total_stake as u128) * 2 + ); + assert_eq!(new_credits, credits * 2); + } + } + + #[test] + fn test_migrating_epoch_credits_iter() { + let stake_lamports = 10_000_000 * LAMPORTS_PER_SOL; + let total_stake = stake_lamports * 2; + let credits = 1235; + + let stake = new_stake( + stake_lamports, + &Pubkey::default(), + VoteStateHandler::new_v4(VoteStateV4::default()).as_ref_v4(), + u64::MAX, + ); + + let vote_account = VoteAccount::new_random(); + let vote_account_hash_map = [(Pubkey::default(), (total_stake, vote_account))] + .into_iter() + .collect(); + let versioned_epoch_stakes = VersionedEpochStakes::new_for_tests(vote_account_hash_map, 0); + let epoch_stakes = (0..10) + .map(|epoch| (epoch, versioned_epoch_stakes.clone())) + .collect(); + + let epoch_credits = vec![(0, credits, 0), (1, credits * 2, credits)]; + let (tower_points, ag_points, new_credits) = migrating_epoch_credits_iter( + 2, + &stake, + epoch_credits.into_iter(), + &StakeHistory::default(), + null_tracer(), + None, + &epoch_stakes, + ) + .unwrap(); + assert_eq!(tower_points, credits as u128 * stake_lamports as u128 * 2); + assert_eq!(ag_points, 0); + assert_eq!(new_credits, credits * 2); + + let epoch_credits = vec![ + (0, credits, 0), + AG_MIGRATION_EPOCH_CREDIT, + (0, credits * 2, credits), + ]; + let (tower_points, ag_points, new_credits) = migrating_epoch_credits_iter( + 0, + &stake, + epoch_credits.into_iter(), + &StakeHistory::default(), + null_tracer(), + None, + &epoch_stakes, + ) + .unwrap(); + assert_eq!(tower_points, credits as u128 * stake_lamports as u128); + assert_eq!( + ag_points, + credits as u128 * stake_lamports as u128 / total_stake as u128 + ); + assert_eq!(new_credits, credits * 2); + + let epoch_credits = vec![AG_MIGRATION_EPOCH_CREDIT, (0, credits * 2, credits)]; + let (tower_points, ag_points, new_credits) = migrating_epoch_credits_iter( + 0, + &stake, + epoch_credits.into_iter(), + &StakeHistory::default(), + null_tracer(), + None, + &epoch_stakes, + ) + .unwrap(); + assert_eq!(tower_points, 0); + assert_eq!( + ag_points, + credits as u128 * stake_lamports as u128 / total_stake as u128 + ); + assert_eq!(new_credits, credits * 2); + } + + #[test] + fn test_changing_total_stake() { + let pubkey = Pubkey::new_unique(); + let vote_account = VoteAccount::new_random(); + let staker_delegation = LAMPORTS_PER_SOL; + let epoch0_validator_stake = staker_delegation * 5; + let epoch1_validator_stake = epoch0_validator_stake * 3; + let epoch2_validator_stake = epoch0_validator_stake / 3; + + let epoch0_vote_accounts = [(pubkey, (epoch0_validator_stake, vote_account.clone()))] + .into_iter() + .collect(); + let versioned_epoch_stakes0 = VersionedEpochStakes::new_for_tests(epoch0_vote_accounts, 0); + + let epoch1_vote_accounts = [(pubkey, (epoch1_validator_stake, vote_account.clone()))] + .into_iter() + .collect(); + let versioned_epoch_stakes1 = VersionedEpochStakes::new_for_tests(epoch1_vote_accounts, 0); + + let epoch2_vote_accounts = [(pubkey, (epoch2_validator_stake, vote_account.clone()))] + .into_iter() + .collect(); + let versioned_epoch_stakes2 = VersionedEpochStakes::new_for_tests(epoch2_vote_accounts, 0); + + let epoch_stakes = [ + (0, versioned_epoch_stakes0), + (1, versioned_epoch_stakes1), + (2, versioned_epoch_stakes2), + ] + .into_iter() + .collect(); + let stake = Stake { + delegation: Delegation { + voter_pubkey: pubkey, + stake: staker_delegation, + activation_epoch: u64::MAX, + deactivation_epoch: u64::MAX, + ..Default::default() + }, + credits_observed: 0, + }; + + let credits = 1235; + let epoch_credits = vec![ + (0, credits, 0), + AG_MIGRATION_EPOCH_CREDIT, + (0, credits * 2, credits), + (1, credits * 3, credits * 2), + (2, credits * 4, credits * 3), + ]; + let (points, new_credits) = ag_epoch_credits_iter( + 0, + false, + &stake, + epoch_credits.into_iter(), + &StakeHistory::default(), + null_tracer(), + None, + &epoch_stakes, + ) + .unwrap(); + assert_eq!(new_credits, credits * 4); + let epoch0_expected = credits * staker_delegation / epoch0_validator_stake; + let epoch1_expected = credits * staker_delegation / epoch1_validator_stake; + let epoch2_expected = credits * staker_delegation / epoch2_validator_stake; + let expected_points = epoch0_expected + epoch1_expected + epoch2_expected; + assert_eq!(points, expected_points as u128); + } + + #[test] + fn test_stake_activating_deactivating() { + let stake_lamports = 10_000_000 * LAMPORTS_PER_SOL; + let credits = 1235; + + for (activation_epoch, deactivation_epoch) in [(0, u64::MAX), (u64::MAX, 0)] { + let stake = Stake { + delegation: Delegation { + voter_pubkey: Pubkey::default(), + stake: stake_lamports, + activation_epoch, + deactivation_epoch, + ..Default::default() + }, + credits_observed: 0, + }; + let epoch_credits = vec![(0, credits, 0), (1, credits * 2, credits)]; + let mut epoch_credits_iter = epoch_credits.into_iter(); + let (points, new_credits, saw_marker) = tower_epoch_credits_iter( + &stake, + epoch_credits_iter.by_ref(), + &StakeHistory::default(), + null_tracer(), + None, + ); + assert_eq!(points, credits as u128 * stake_lamports as u128); + assert_eq!(new_credits, credits * 2); + assert_eq!(epoch_credits_iter.next(), None); + assert!(!saw_marker); + } + + for (activation_epoch, deactivation_epoch) in [(0, u64::MAX), (u64::MAX, 0)] { + let stake = Stake { + delegation: Delegation { + voter_pubkey: Pubkey::default(), + stake: stake_lamports, + activation_epoch, + deactivation_epoch, + ..Default::default() + }, + credits_observed: 0, + }; + + let total_stake = stake_lamports * 2; + let vote_account = VoteAccount::new_random(); + let vote_account_hash_map = [(Pubkey::default(), (total_stake, vote_account))] + .into_iter() + .collect(); + let versioned_epoch_stakes = + VersionedEpochStakes::new_for_tests(vote_account_hash_map, 0); + let epoch_stakes = (0..10) + .map(|epoch| (epoch, versioned_epoch_stakes.clone())) + .collect(); + + let epoch_credits = vec![ + (0, credits, 0), + AG_MIGRATION_EPOCH_CREDIT, + (0, credits * 2, credits), + (1, credits * 3, credits * 2), + ]; + let (points, new_credits) = ag_epoch_credits_iter( + 0, + false, + &stake, + epoch_credits.into_iter(), + &StakeHistory::default(), + null_tracer(), + None, + &epoch_stakes, + ) + .unwrap(); + assert_eq!( + points, + credits as u128 * stake_lamports as u128 / total_stake as u128 + ); + assert_eq!(new_credits, credits * 3); + } + + for (activation_epoch, deactivation_epoch) in [(0, u64::MAX), (u64::MAX, 0)] { + let stake = Stake { + delegation: Delegation { + voter_pubkey: Pubkey::default(), + stake: stake_lamports, + activation_epoch, + deactivation_epoch, + ..Default::default() + }, + credits_observed: 0, + }; + + let total_stake = stake_lamports * 2; + let vote_account = VoteAccount::new_random(); + let vote_account_hash_map = [(Pubkey::default(), (total_stake, vote_account))] + .into_iter() + .collect(); + let versioned_epoch_stakes = + VersionedEpochStakes::new_for_tests(vote_account_hash_map, 0); + let epoch_stakes = (0..10) + .map(|epoch| (epoch, versioned_epoch_stakes.clone())) + .collect(); + + let epoch_credits = vec![ + (0, credits, 0), + AG_MIGRATION_EPOCH_CREDIT, + (0, credits * 2, credits), + (1, credits * 3, credits * 2), + ]; + let (tower_points, ag_points, new_credits) = migrating_epoch_credits_iter( + 0, + &stake, + epoch_credits.into_iter(), + &StakeHistory::default(), + null_tracer(), + None, + &epoch_stakes, + ) + .unwrap(); + if activation_epoch == 0 { + assert_eq!(tower_points, 0); + } else { + assert_eq!(tower_points, credits as u128 * stake_lamports as u128); + } + assert_eq!( + ag_points, + credits as u128 * stake_lamports as u128 / total_stake as u128 + ); + assert_eq!(new_credits, credits * 3); + } + } } diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 300e3d2c1ff..2bfa1afd769 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -7,6 +7,7 @@ mod account_saver; #[cfg(feature = "dev-context-only-utils")] pub mod account_saver; pub mod accounts_background_service; +mod alpenglow_epoch_type; pub mod bank; pub mod bank_client; pub mod bank_forks; From 6f0143053aa3c417d87b4a470d7cb4679d2622a6 Mon Sep 17 00:00:00 2001 From: Trent Nelson <490004+t-nelson@users.noreply.github.com> Date: Thu, 28 May 2026 14:45:57 -0600 Subject: [PATCH 111/576] chore(sec pol): discourage reporting multiple findings in the same ghsa (#12800) --- SECURITY.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/SECURITY.md b/SECURITY.md index 2aa306418ce..27e1ec92a16 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -14,6 +14,10 @@ Provide a helpful title, detailed description of the vulnerability and an exploi proof-of-concept. Speculative submissions without proof-of-concept will be closed with no further consideration. +Create one GHSA per finding. GHSAs reporting multiple findings will be closed as +invalid. Such reports will not be eligible for bug bounties and will not hold a +position in duplicate report standings. + For vulnerabilities regarding SPL programs, please refer to the repositories and their associated security policy within the [solana-program organization](https://github.com/solana-program). @@ -196,6 +200,7 @@ one week in order to be eligible for a bounty * In the stable and beta branches, vulnerabilities are eligible for bounty upon merge * Submissions _MUST_ include an exploit proof-of-concept to be considered eligible +* Only reports describing a [single finding](#reporting) will be considered eligible * The participant submitting the bug report shall follow the process outlined within this document * Valid exploits can be eligible even if they are not successfully executed on a public cluster * Multiple submissions for the same class of exploit are still eligible for compensation, though may be compensated at a lower rate, however these will be assessed on a case-by-case basis From 58c231ea297b2c160e5968f0518b01e7bf7e838f Mon Sep 17 00:00:00 2001 From: Alex Pyattaev Date: Thu, 28 May 2026 23:56:17 +0300 Subject: [PATCH 112/576] Refactor: use existing sigverify worker stats reporting infrastructure (#12786) use existing sigverify worker stats reporting infrastructure --- core/src/banking_stage.rs | 4 +- core/src/banking_trace.rs | 80 +++----------------------- core/src/block_creation_loop.rs | 3 + core/src/cluster_info_vote_listener.rs | 37 +++++++++++- core/src/sigverify.rs | 26 ++++++++- core/src/sigverify_stage.rs | 18 ++++++ 6 files changed, 90 insertions(+), 78 deletions(-) diff --git a/core/src/banking_stage.rs b/core/src/banking_stage.rs index dcc74b7247b..96373d3a12e 100644 --- a/core/src/banking_stage.rs +++ b/core/src/banking_stage.rs @@ -1351,7 +1351,9 @@ mod tests { }) .unwrap() }) - .for_each(|handle| handle.join().unwrap()); + .for_each(|handle| { + handle.join().unwrap(); + }); banking_stage.join().unwrap(); exit.store(true, Ordering::Relaxed); diff --git a/core/src/banking_trace.rs b/core/src/banking_trace.rs index a2867cfabd6..2850de5c575 100644 --- a/core/src/banking_trace.rs +++ b/core/src/banking_trace.rs @@ -7,16 +7,14 @@ use { serde::{Deserialize, Serialize}, solana_clock::Slot, solana_hash::Hash, - solana_metrics::datapoint_info, solana_streamer::{evicting_sender::EvictingSender, streamer::ChannelSend}, - solana_time_utils::timestamp, std::{ fs::{create_dir_all, remove_dir_all}, io::{self, Write}, path::PathBuf, sync::{ Arc, - atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering}, + atomic::{AtomicBool, Ordering}, }, thread::{self, JoinHandle, sleep}, time::{Duration, SystemTime}, @@ -32,9 +30,6 @@ const VOTE_CHANNEL_CAPACITY: usize = 1024 * 8; /// Larger than the vote channel to absorb bursty TPU load. const NON_VOTE_CHANNEL_CAPACITY: usize = 1024 * 16; -/// Period between metric emissions per channel from inside `TracedSender::send`. -const STATS_REPORT_INTERVAL: Duration = Duration::from_secs(5); - pub type BankingPacketSender = TracedSender; pub type TracerThreadResult = Result<(), TraceError>; pub type TracerThread = Option>; @@ -98,17 +93,6 @@ pub enum ChannelLabel { Dummy, } -impl ChannelLabel { - fn metric_name(&self) -> &'static str { - match self { - ChannelLabel::NonVote => "banking_trace_channel_non_vote", - ChannelLabel::TpuVote => "banking_trace_channel_tpu_vote", - ChannelLabel::GossipVote => "banking_trace_channel_gossip_vote", - ChannelLabel::Dummy => "banking_trace_channel_dummy", - } - } -} - struct RollingConditionGrouped { basic: RollingConditionBasic, tried_rollover_after_opened: bool, @@ -394,22 +378,9 @@ impl BankingTracer { pub struct TracedSender { label: ChannelLabel, sender: EvictingSender, - stats: Arc, active_tracer: Option, } -#[derive(Default)] -struct TracedSenderStats { - /// Total number of batches sent downstream. - sent: AtomicU64, - /// Max channel length observed since last report. - max_len: AtomicUsize, - /// Number of batches the EvictingSender dropped due to a full channel since last report. - eviction_drops: AtomicU64, - /// `solana_time_utils::timestamp` of the last report. - last_report_ms: AtomicU64, -} - impl TracedSender { fn new( label: ChannelLabel, @@ -419,12 +390,14 @@ impl TracedSender { Self { label, sender, - stats: Arc::new(TracedSenderStats::default()), active_tracer, } } - pub fn send(&self, batch: BankingPacketBatch) -> Result<(), SendError> { + /// Send a batch on the channel. This may evict an existing batch to make + /// room; in that case `Ok(n)` is returned where `n` is the number of + /// evicted packets. On channel disconnect returns `Err(SendError)`. + pub fn send(&self, batch: BankingPacketBatch) -> Result> { if let Some(ActiveTracer { trace_sender, exit }) = &self.active_tracer { if !exit.load(Ordering::Relaxed) { // Ignore errors in sending to tracer - it is a non-critical component. @@ -434,23 +407,11 @@ impl TracedSender { )); } } - let (result, sent) = match self.sender.try_send(batch) { - // New batch queued; nothing was evicted. - Ok(()) => (Ok(()), self.stats.sent.fetch_add(1, Ordering::Relaxed)), - // EvictingSender popped the oldest to make room. - Err(TrySendError::Full(_)) => { - self.stats.eviction_drops.fetch_add(1, Ordering::Relaxed); - (Ok(()), 0) - } - Err(TrySendError::Disconnected(b)) => (Err(SendError(b)), 0), - }; - let len = self.sender.len(); - self.stats.max_len.fetch_max(len, Ordering::Relaxed); - // if we've made reasonable progress, or dropped packets - if sent % 128 == 0 { - self.maybe_report_stats(); + match self.sender.try_send(batch) { + Ok(()) => Ok(0), + Err(TrySendError::Full(b)) => Ok(b.iter().map(|pb| pb.len()).sum()), + Err(TrySendError::Disconnected(b)) => Err(SendError(b)), } - result } pub fn len(&self) -> usize { @@ -460,29 +421,6 @@ impl TracedSender { pub fn is_empty(&self) -> bool { self.len() == 0 } - - fn maybe_report_stats(&self) { - let now_ms = timestamp(); - let last = self.stats.last_report_ms.load(Ordering::Relaxed); - if Duration::from_millis(now_ms.saturating_sub(last)) < STATS_REPORT_INTERVAL { - return; - } - if self - .stats - .last_report_ms - .compare_exchange(last, now_ms, Ordering::Relaxed, Ordering::Relaxed) - .is_err() - { - return; - } - let max_len = self.stats.max_len.swap(0, Ordering::Relaxed); - let eviction_drops = self.stats.eviction_drops.swap(0, Ordering::Relaxed); - datapoint_info!( - self.label.metric_name(), - ("max_len", max_len as i64, i64), - ("eviction_drops", eviction_drops as i64, i64), - ); - } } #[cfg(any(test, feature = "dev-context-only-utils"))] diff --git a/core/src/block_creation_loop.rs b/core/src/block_creation_loop.rs index 1106dfceb13..a1f07c8ce5f 100644 --- a/core/src/block_creation_loop.rs +++ b/core/src/block_creation_loop.rs @@ -966,6 +966,9 @@ fn handle_parent_ready( let batch: PacketBatch = packets.into(); let banking_packet_batch = Arc::new(vec![batch]); ctx.banking_stage_sender + // technically this send can evict to make room (which may drop a few packets) + // but this should (hopefully) not be significant amounts since we are evicting + // at most 1 batch. .send(banking_packet_batch) .map_err(|_| PohRecorderError::RescheduleTransactionsError(slot))?; } diff --git a/core/src/cluster_info_vote_listener.rs b/core/src/cluster_info_vote_listener.rs index 8845029334c..2c95f696d70 100644 --- a/core/src/cluster_info_vote_listener.rs +++ b/core/src/cluster_info_vote_listener.rs @@ -19,7 +19,6 @@ use { solana_hash::Hash, solana_ledger::blockstore::Blockstore, solana_measure::measure::Measure, - solana_metrics::inc_new_counter_debug, solana_perf::packet::{self, PacketBatch}, solana_pubkey::Pubkey, solana_rpc::{ @@ -497,15 +496,47 @@ impl ClusterInfoVoteListener { verified_packets_sender: BankingPacketSender, verified_vote_transactions_sender: VerifiedVoteTransactionsSender, ) -> Result<()> { + #[derive(Default)] + struct Stats { + received_count: usize, + banking_channel_max_len: usize, + banking_channel_eviction_drops: usize, + } + const STATS_REPORT_INTERVAL: Duration = Duration::from_secs(1); let mut cursor = Cursor::default(); + let mut last_report = Instant::now(); + let mut stats = Stats::default(); while !exit.load(Ordering::Relaxed) { let votes = cluster_info.get_votes(&mut cursor); - inc_new_counter_debug!("cluster_info_vote_listener-recv_count", votes.len()); if !votes.is_empty() { + stats.received_count += votes.len(); let (vote_txs, packets) = Self::verify_votes(votes, &mut gossip_sigverify_handle, &sharable_banks)?; verified_vote_transactions_sender.send(vote_txs)?; - verified_packets_sender.send(BankingPacketBatch::new(packets))?; + // Sample backlog before the push. + stats.banking_channel_max_len = stats + .banking_channel_max_len + .max(verified_packets_sender.len()); + stats.banking_channel_eviction_drops += + verified_packets_sender.send(BankingPacketBatch::new(packets))?; + } + if last_report.elapsed() >= STATS_REPORT_INTERVAL { + datapoint_info!( + "cluster_info_vote_listener", + ("received_count", stats.received_count as i64, i64), + ( + "banking_channel_max_len", + stats.banking_channel_max_len as i64, + i64 + ), + ( + "banking_channel_eviction_drops", + stats.banking_channel_eviction_drops as i64, + i64 + ), + ); + stats = Stats::default(); + last_report = Instant::now(); } sleep(Duration::from_millis(GOSSIP_SLEEP_MILLIS)); } diff --git a/core/src/sigverify.rs b/core/src/sigverify.rs index 0fb7b9779a9..ec9f691d14f 100644 --- a/core/src/sigverify.rs +++ b/core/src/sigverify.rs @@ -43,6 +43,10 @@ pub(crate) struct SigVerifyWorkerStats { pub(crate) total_dedup_time_us: Arc, pub(crate) total_valid_packets: Arc, pub(crate) total_verify_time_us: Arc, + /// Max occupancy of the banking_stage channel sampled immediately before each send. + pub(crate) max_pre_send_len: Arc, + /// Count of sends where the EvictingSender had to drop a batch to make room. + pub(crate) eviction_drops: Arc, } #[derive(Clone)] @@ -292,12 +296,28 @@ impl SigVerifyWorkerPool { .fetch_add(verify_time_us as usize, Ordering::Relaxed); let banking_packet_batch = BankingPacketBatch::new(vec![batch]); - if let Err(err) = state + // Sample backlog before the push: measures consumer health without + // including this batch's own contribution. + state + .stats + .max_pre_send_len + .fetch_max(state.banking_stage_sender.len(), Ordering::Relaxed); + match state .banking_stage_sender .send(banking_packet_batch.clone()) { - error!("sigverify send failed: {err:?}"); - return false; + Ok(0) => {} // avoid poking atomics if nothing was evicted (typical case) + Ok(evicted) => { + // record evicted amount into metrics + state + .stats + .eviction_drops + .fetch_add(evicted, Ordering::Relaxed); + } + Err(err) => { + error!("sigverify send to banking failed: {err:?}"); + return false; + } } if should_forward { Self::try_forward(forward_stage_sender, banking_packet_batch, is_tpu_vote); diff --git a/core/src/sigverify_stage.rs b/core/src/sigverify_stage.rs index 4186c2ec862..16204d335b6 100644 --- a/core/src/sigverify_stage.rs +++ b/core/src/sigverify_stage.rs @@ -61,6 +61,10 @@ struct SigVerifierStats { total_valid_packets: Arc, total_dedup_time_us: Arc, total_verify_time_us: Arc, + /// Max occupancy of the banking_stage output channel since last report. + max_pre_send_len: Arc, + /// Count of sends in which the EvictingSender had to drop a batch. + eviction_drops: Arc, } struct ServicerState { @@ -115,6 +119,16 @@ impl SigVerifierStats { self.total_verify_time_us.swap(0, Ordering::Relaxed) as i64, i64 ), + ( + "max_pre_send_len", + self.max_pre_send_len.swap(0, Ordering::Relaxed) as i64, + i64 + ), + ( + "eviction_drops", + self.eviction_drops.swap(0, Ordering::Relaxed) as i64, + i64 + ), ); } } @@ -157,6 +171,8 @@ impl SigVerifyStage { total_dedup_time_us: non_vote_stats.total_dedup_time_us.clone(), total_valid_packets: non_vote_stats.total_valid_packets.clone(), total_verify_time_us: non_vote_stats.total_verify_time_us.clone(), + max_pre_send_len: non_vote_stats.max_pre_send_len.clone(), + eviction_drops: non_vote_stats.eviction_drops.clone(), }, ), SigVerifyWorkerState::new( @@ -169,6 +185,8 @@ impl SigVerifyStage { total_dedup_time_us: tpu_vote_stats.total_dedup_time_us.clone(), total_valid_packets: tpu_vote_stats.total_valid_packets.clone(), total_verify_time_us: tpu_vote_stats.total_verify_time_us.clone(), + max_pre_send_len: tpu_vote_stats.max_pre_send_len.clone(), + eviction_drops: tpu_vote_stats.eviction_drops.clone(), }, ), ); From 509af33d261cd5d974834513161f660ba16ee3fb Mon Sep 17 00:00:00 2001 From: Rory Harris Date: Thu, 28 May 2026 14:03:58 -0700 Subject: [PATCH 113/576] Gate full snapshot clean activity behind full snapshot occurring (#12744) * Gate full snapshot clean activity behind full snapshot occurring * Deleted comment --- accounts-db/src/accounts_db.rs | 43 +++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/accounts-db/src/accounts_db.rs b/accounts-db/src/accounts_db.rs index df104075953..f2bf8eb6524 100644 --- a/accounts-db/src/accounts_db.rs +++ b/accounts-db/src/accounts_db.rs @@ -963,6 +963,10 @@ pub struct AccountsDb { /// for incremental snapshot support. zero_lamport_accounts_to_purge_after_full_snapshot: DashSet<(Slot, Pubkey)>, + /// Set by `set_latest_full_snapshot_slot` when the snapshot advances. Read and cleared by + /// clean + latest_full_snapshot_slot_advanced_since_clean: AtomicBool, + /// GeyserPlugin accounts update notifier accounts_update_notifier: Option, @@ -1147,6 +1151,7 @@ impl AccountsDb { is_bank_drop_callback_enabled: AtomicBool::default(), dirty_stores: DashMap::default(), zero_lamport_accounts_to_purge_after_full_snapshot: DashSet::default(), + latest_full_snapshot_slot_advanced_since_clean: AtomicBool::default(), accounts_file_provider: AccountsFileProvider::default(), latest_full_snapshot_slot: SeqLock::new(None), best_ancient_slots_to_shrink: RwLock::default(), @@ -1593,28 +1598,32 @@ impl AccountsDb { timings.delta_key_count = Self::count_pubkeys(&candidates); - // Check if we should purge any of the - // zero_lamport_accounts_to_purge_later, based on the - // latest_full_snapshot_slot. - let latest_full_snapshot_slot = self.latest_full_snapshot_slot(); - assert!( - latest_full_snapshot_slot.is_some() + debug_assert!( + self.latest_full_snapshot_slot().is_some() || self .zero_lamport_accounts_to_purge_after_full_snapshot .is_empty(), "if snapshots are disabled, then zero_lamport_accounts_to_purge_later should always \ be empty" ); - if let Some(latest_full_snapshot_slot) = latest_full_snapshot_slot { - self.zero_lamport_accounts_to_purge_after_full_snapshot - .retain(|(slot, pubkey)| { - let is_candidate_for_clean = - max_slot_inclusive >= *slot && latest_full_snapshot_slot >= *slot; - if is_candidate_for_clean { - insert_candidate(*pubkey, true); - } - !is_candidate_for_clean - }); + + // Cleaning up zero lamport accounts is gated by a full snapshot because they need to be retained + // for incremental snapshots. Once a snapshot occurs, drain the list + if self + .latest_full_snapshot_slot_advanced_since_clean + .swap(false, Ordering::Acquire) + { + if let Some(latest_full_snapshot_slot) = self.latest_full_snapshot_slot() { + self.zero_lamport_accounts_to_purge_after_full_snapshot + .retain(|(slot, pubkey)| { + let is_candidate_for_clean = + max_slot_inclusive >= *slot && latest_full_snapshot_slot >= *slot; + if is_candidate_for_clean { + insert_candidate(*pubkey, true); + } + !is_candidate_for_clean + }); + } } (candidates, min_dirty_slot) @@ -6029,6 +6038,8 @@ impl AccountsDb { /// Sets the latest full snapshot slot to `slot` pub fn set_latest_full_snapshot_slot(&self, slot: Slot) { *self.latest_full_snapshot_slot.lock_write() = Some(slot); + self.latest_full_snapshot_slot_advanced_since_clean + .store(true, Ordering::Release); } fn generate_index_for_slot<'a>( From 46f52e8399710b4f665b15c7b2ed94e8260c9aa4 Mon Sep 17 00:00:00 2001 From: Rocker Zhang Date: Fri, 29 May 2026 05:12:51 +0800 Subject: [PATCH 114/576] cli: format unmatched pubkeys plainly in 'solana stakes' error message (#12794) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit process_show_stakes' error path previously rendered the set of unmatched vote-account pubkeys with HashSet's debug format, producing output like "Failed to retrieve matching vote account for {\"\"}." — the {"..."} wrapping looks like a JSON object or set literal, and the order varies between runs because HashSet iteration is unstable. Collect the pubkeys into a sorted Vec and join them with ", " so the error prints the pubkeys plainly and deterministically. --- cli/src/cluster_query.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cli/src/cluster_query.rs b/cli/src/cluster_query.rs index c70fb8baaaa..8076c5c6ab4 100644 --- a/cli/src/cluster_query.rs +++ b/cli/src/cluster_query.rs @@ -1603,8 +1603,11 @@ pub async fn process_show_stakes( .collect(); if !pubkeys.is_empty() { + let mut pubkeys: Vec = pubkeys.into_iter().collect(); + pubkeys.sort(); return Err(CliError::RpcRequestError(format!( - "Failed to retrieve matching vote account for {pubkeys:?}." + "Failed to retrieve matching vote account for {}.", + pubkeys.join(", ") )) .into()); } From 60a149a9a9860b038a5e4da626d15b67f3307293 Mon Sep 17 00:00:00 2001 From: Andrew Fitzgerald Date: Thu, 28 May 2026 16:57:53 -0500 Subject: [PATCH 115/576] Revert "banking-trace: use bounded channel (#12778)" (#12812) This reverts commit c8e7e1728faba3ec43317c058bb5031a730fe7bc. --- core/src/banking_trace.rs | 27 ++++++++++++++++----------- streamer/src/evicting_sender.rs | 11 +---------- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/core/src/banking_trace.rs b/core/src/banking_trace.rs index 2850de5c575..e01f10b0da8 100644 --- a/core/src/banking_trace.rs +++ b/core/src/banking_trace.rs @@ -2,7 +2,9 @@ use { agave_banking_stage_ingress_types::{BankingPacketBatch, BankingPacketReceiver}, bincode::serialize_into, chrono::{DateTime, Local}, - crossbeam_channel::{Receiver, SendError, TryRecvError, TrySendError, bounded}, + crossbeam_channel::{ + Receiver, SendError, Sender, TryRecvError, TrySendError, bounded, unbounded, + }, rolling_file::{RollingCondition, RollingConditionBasic, RollingFileAppender}, serde::{Deserialize, Serialize}, solana_clock::Slot, @@ -59,12 +61,13 @@ pub const DISABLED_BAKING_TRACE_DIR: DirByteLimit = 0; pub const BANKING_TRACE_DIR_DEFAULT_BYTE_LIMIT: DirByteLimit = TRACE_FILE_DEFAULT_ROTATE_BYTE_THRESHOLD * TRACE_FILE_ROTATE_COUNT; -#[derive(Clone)] +#[derive(Clone, Debug)] struct ActiveTracer { - trace_sender: EvictingSender, + trace_sender: Sender, exit: Arc, } +#[derive(Debug)] pub struct BankingTracer { active_tracer: Option, } @@ -211,9 +214,7 @@ impl BankingTracer { )); } - const TRACING_CHANNEL_CAPACITY: usize = 50_000; - let (trace_sender, trace_receiver) = - EvictingSender::new_bounded(TRACING_CHANNEL_CAPACITY); + let (trace_sender, trace_receiver) = unbounded(); let file_appender = Self::create_file_appender(path, rotate_threshold_size)?; @@ -400,11 +401,15 @@ impl TracedSender { pub fn send(&self, batch: BankingPacketBatch) -> Result> { if let Some(ActiveTracer { trace_sender, exit }) = &self.active_tracer { if !exit.load(Ordering::Relaxed) { - // Ignore errors in sending to tracer - it is a non-critical component. - let _ = trace_sender.try_send(TimedTracedEvent( - SystemTime::now(), - TracedEvent::PacketBatch(self.label, BankingPacketBatch::clone(&batch)), - )); + trace_sender + .send(TimedTracedEvent( + SystemTime::now(), + TracedEvent::PacketBatch(self.label, BankingPacketBatch::clone(&batch)), + )) + .map_err(|err| { + error!("unexpected error when tracing a banking event...: {err:?}"); + SendError(BankingPacketBatch::clone(&batch)) + })?; } } match self.sender.try_send(batch) { diff --git a/streamer/src/evicting_sender.rs b/streamer/src/evicting_sender.rs index 4a5ae056eb8..847952a684c 100644 --- a/streamer/src/evicting_sender.rs +++ b/streamer/src/evicting_sender.rs @@ -4,21 +4,12 @@ use { }; /// A sender implementation that evicts the oldest message when the channel is full. +#[derive(Clone)] pub struct EvictingSender { sender: Sender, receiver: Receiver, } -// Manual implementation of Clone since `T` is not required to implement Clone. -impl Clone for EvictingSender { - fn clone(&self) -> Self { - Self { - sender: self.sender.clone(), - receiver: self.receiver.clone(), - } - } -} - impl EvictingSender { /// Create a new evicting sender with provided sender, receiver. #[inline] From e8c9872f3e63b0b8c9f07f23a0ab38ccfb9ef7a3 Mon Sep 17 00:00:00 2001 From: Brennan Date: Thu, 28 May 2026 15:07:03 -0700 Subject: [PATCH 116/576] Test fix: fund vote account (#12815) --- runtime/src/bank/partitioned_epoch_rewards/calculation.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/runtime/src/bank/partitioned_epoch_rewards/calculation.rs b/runtime/src/bank/partitioned_epoch_rewards/calculation.rs index b91b490b56b..d1029a7acbd 100644 --- a/runtime/src/bank/partitioned_epoch_rewards/calculation.rs +++ b/runtime/src/bank/partitioned_epoch_rewards/calculation.rs @@ -3505,6 +3505,9 @@ mod tests { ); let pre_balance = bank.get_balance(&collector_into_vote_address); assert_ne!(pre_balance, 0); + // Fund the converted vote account enough to pass VAT filtering. + let pre_balance = + pre_balance.max(bank.get_minimum_balance_for_rent_exemption(VoteStateV4::size_of())); // Transform the collector into a vote account, see that all rewards // are burned for this epoch From 963314bb1ad0550ba1614b60045945a397fd578a Mon Sep 17 00:00:00 2001 From: Trent Nelson <490004+t-nelson@users.noreply.github.com> Date: Thu, 28 May 2026 16:58:23 -0600 Subject: [PATCH 117/576] chore(quic): document connection abuse mitigation strategy (#12813) the code is clanker bait --- streamer/src/nonblocking/quic.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/streamer/src/nonblocking/quic.rs b/streamer/src/nonblocking/quic.rs index 396d6397349..d21d95b53c2 100644 --- a/streamer/src/nonblocking/quic.rs +++ b/streamer/src/nonblocking/quic.rs @@ -329,6 +329,16 @@ where } if let Ok(Some(incoming)) = timeout_connection { + // our connection/handshake abuse mitigation policy is one of shed + // fast and bound resource consumption. attempting to be "smarter" + // before a peer has asserted control over their ip address by + // completing the retry challenge creates a scenario whereby peers + // can attack one another via ip spoofing. employ the following + // * limit duration of in-flight connection attempts with a timeout + // * protect against connection attempt bursts with a global rate-limiter + // * rate-limit abusive peers by (control-asserted) ip + // * cap total connections per-peer/ip + stats .total_incoming_connection_attempts .fetch_add(1, Ordering::Relaxed); From 5d1f86071487312e05c6d8e0dc974f88d0bbdab8 Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Thu, 28 May 2026 20:30:23 -0400 Subject: [PATCH 118/576] flh: gate UpdateParent on first slot in leader window (#12803) --- core/src/replay_stage/tests.rs | 58 +++++++++++++++++++++--- core/src/replay_stage/update_parent.rs | 7 ++- ledger/src/blockstore.rs | 15 +++++- ledger/src/blockstore/error.rs | 2 + ledger/src/blockstore/tests.rs | 47 +++++++++++++++---- ledger/src/blockstore_processor.rs | 33 +++++++++++--- runtime/src/block_component_processor.rs | 42 +++++++++++++---- 7 files changed, 170 insertions(+), 34 deletions(-) diff --git a/core/src/replay_stage/tests.rs b/core/src/replay_stage/tests.rs index 318271cd33a..1ad2143df2b 100644 --- a/core/src/replay_stage/tests.rs +++ b/core/src/replay_stage/tests.rs @@ -2828,8 +2828,8 @@ fn test_update_parent_restart() { let bank0 = bank_forks.read().unwrap().get(0).unwrap(); // Slot 4: 5 shreds, replay_fec_set_index=32; 5 < 32 so cleared. - // Slot 5: 40 shreds, replay_fec_set_index=32; 40 >= 32 so skipped. - for (slot, shreds) in [(4, 5), (5, 40)] { + // Slot 8: 40 shreds, replay_fec_set_index=32; 40 >= 32 so skipped. + for (slot, shreds) in [(4, 5), (8, 40)] { let bank = Bank::new_from_parent(bank0.clone(), SlotLeader::default(), slot); bank_forks.write().unwrap().insert(bank); let p = ForkProgress::new(Hash::default(), Some(0), None, 0, 0, None); @@ -2838,7 +2838,7 @@ fn test_update_parent_restart() { } let (tx, rx) = crossbeam_channel::unbounded(); - for slot in [4, 5] { + for slot in [4, 8] { let parent_block_id = Hash::new_unique(); insert_update_parent_slot( &blockstore, @@ -2866,7 +2866,7 @@ fn test_update_parent_restart() { ); assert!(progress.get(&4).is_none()); // cleared: 5 < 32 - assert!(progress.get(&5).is_some()); // skipped: 40 >= 32 + assert!(progress.get(&8).is_some()); // skipped: 40 >= 32 assert_eq!( replay_vote_receiver.try_recv(), Ok(ReplayVoteMessage::InvalidBank { @@ -2891,7 +2891,7 @@ fn test_headerless_update_parent() { .. } = vote_simulator; let my_pubkey = Pubkey::new_unique(); - let slot = 1; + let slot = 4; let migration_status = post_migration_status_for_tests(); let footer_marker = || { @@ -3000,6 +3000,50 @@ fn test_update_parent_tower_gated() { assert!(bank_forks.read().unwrap().get(slot).is_some()); } +#[test] +fn test_update_parent_interrupt_ignores_non_first_leader_window_slot() { + let ReplayBlockstoreComponents { + blockstore, + vote_simulator, + .. + } = replay_blockstore_components(Some(tr(0) / tr(1)), 1, None::); + let VoteSimulator { + bank_forks, + mut progress, + .. + } = vote_simulator; + + let slot = 1; + let mut meta = blockstore.meta(slot).unwrap().unwrap(); + meta.parent_slot = Some(0); + meta.parent_block_id = Hash::new_unique(); + meta.replay_fec_set_index = 10; + blockstore.put_meta(slot, &meta).unwrap(); + + let p = ForkProgress::new(Hash::default(), Some(0), None, 0, 0, None); + p.replay_progress.write().unwrap().num_shreds = 5; + progress.insert(slot, p); + + let (tx, rx) = crossbeam_channel::unbounded(); + tx.send(UpdateParentSignal { slot }).unwrap(); + + let (replay_vote_sender, _replay_vote_receiver) = unbounded(); + let mut async_verification_freelist = Vec::new(); + handle_update_parent_interrupts( + &Pubkey::new_unique(), + &blockstore, + &bank_forks, + &mut progress, + &mut async_verification_freelist, + &rx, + &replay_vote_sender, + &post_migration_status_for_tests(), + ); + + assert!(progress.get(&slot).is_some()); + assert!(bank_forks.read().unwrap().get(slot).is_some()); +} + #[test] fn test_update_parent_keeps_hard() { let ReplayBlockstoreComponents { @@ -3172,7 +3216,7 @@ fn test_before_update_soft_dead() { .. } = vote_simulator; - let slot = 1; + let slot = 4; let bank0 = bank_forks.read().unwrap().get(0).unwrap(); let bank = Bank::new_from_parent(bank0, SlotLeader::default(), slot); let bank = bank_forks.write().unwrap().insert(bank); @@ -3403,7 +3447,7 @@ fn test_skip_own_update_full() { let my_pubkey = *validator_keypairs.keys().next().unwrap(); let root_bank = bank_forks.read().unwrap().root_bank(); let leader_schedule_cache = Arc::new(LeaderScheduleCache::new_from_bank(&root_bank)); - let slot = 2; + let slot = 4; let replay_fec_set_index = 32; insert_update_parent_slot( diff --git a/core/src/replay_stage/update_parent.rs b/core/src/replay_stage/update_parent.rs index b8158a5f1e2..0cae13e1e6a 100644 --- a/core/src/replay_stage/update_parent.rs +++ b/core/src/replay_stage/update_parent.rs @@ -21,7 +21,10 @@ use { blockstore_processor::AsyncVerificationProgress, }, solana_pubkey::Pubkey, - solana_runtime::{bank::Bank, bank_forks::BankForks, vote_sender_types::ReplayVoteSender}, + solana_runtime::{ + bank::Bank, bank_forks::BankForks, leader_schedule_utils::leader_slot_index, + vote_sender_types::ReplayVoteSender, + }, std::{ collections::BTreeSet, sync::{Arc, RwLock}, @@ -42,7 +45,7 @@ pub(super) enum ChildBankReplayStart { /// Return the UpdateParent replay offset recorded in SlotMeta, if it is /// usable from the currently rooted fork. fn update_parent_replay_offset(slot: Slot, slot_meta: &SlotMeta, root: Slot) -> Option { - if !slot_meta.has_update_parent() { + if !slot_meta.has_update_parent() || leader_slot_index(slot) != 0 { return None; } let parent_slot = slot_meta.parent_slot?; diff --git a/ledger/src/blockstore.rs b/ledger/src/blockstore.rs index 2809bbe7bb2..92eb271ab1b 100644 --- a/ledger/src/blockstore.rs +++ b/ledger/src/blockstore.rs @@ -50,7 +50,7 @@ use { solana_measure::{measure::Measure, measure_us}, solana_metrics::datapoint_error, solana_pubkey::Pubkey, - solana_runtime::bank::Bank, + solana_runtime::{bank::Bank, leader_schedule_utils::leader_slot_index}, solana_sha256_hasher::hashv, solana_signature::Signature, solana_signer::Signer, @@ -495,6 +495,14 @@ impl ParentInfo { self.replay_fec_set_index > 0 } + fn validate_update_parent_slot(&self, slot: Slot) -> Result<()> { + if self.has_update_parent() && leader_slot_index(slot) != 0 { + return Err(BlockstoreError::UpdateParentNotFirstInLeaderWindow(slot)); + } + + Ok(()) + } + /// True when this metadata came from the slot's block header. fn populated_from_block_header(&self) -> bool { self.replay_fec_set_index == 0 @@ -1150,6 +1158,11 @@ impl Blockstore { return Ok(()); }; + if let Err(err) = new_parent_info.validate_update_parent_slot(slot) { + self.mark_invalid_parent_info_dead(write_batch, new_parent_info, slot, location, &err)?; + return Err(err); + } + let max_root = self.max_root(); if let Err(err) = new_parent_info.validate_shred_parent(slot, shred_parent_slot, max_root) { self.mark_invalid_parent_info_dead(write_batch, new_parent_info, slot, location, &err)?; diff --git a/ledger/src/blockstore/error.rs b/ledger/src/blockstore/error.rs index bdae6108629..e97bbdbee8a 100644 --- a/ledger/src/blockstore/error.rs +++ b/ledger/src/blockstore/error.rs @@ -90,6 +90,8 @@ pub enum BlockstoreError { update_parent_slot: Slot, shred_parent_slot: Slot, }, + #[error("update parent is only valid in the first slot of a leader window for slot {0}")] + UpdateParentNotFirstInLeaderWindow(Slot), #[error("unexpected block component")] UnexpectedBlockComponent, #[error("multiple update parents for slot {0}")] diff --git a/ledger/src/blockstore/tests.rs b/ledger/src/blockstore/tests.rs index 970abc63403..34a8a15bde4 100644 --- a/ledger/src/blockstore/tests.rs +++ b/ledger/src/blockstore/tests.rs @@ -6974,32 +6974,59 @@ fn test_invalid_update_parent_parent_info_marks_dead() { } } +#[test] +fn test_update_parent_non_first_leader_window_marks_dead() { + let ledger_path = get_tmp_ledger_path_auto_delete!(); + let blockstore = Blockstore::open(ledger_path.path()).unwrap(); + + let slot = 10; + let shred_parent_slot = 5; + let update_parent_slot = 3; + let mut shreds = create_block_header_shreds(slot, shred_parent_slot, Hash::new_unique()); + shreds.extend(create_update_parent_shreds_with_shred_parent( + slot, + shred_parent_slot, + update_parent_slot, + Hash::new_unique(), + 32, + true, + )); + + blockstore.insert_shreds(shreds, None, true).unwrap(); + + let meta = blockstore.meta(slot).unwrap().unwrap(); + assert!(blockstore.is_dead(slot)); + assert_eq!(meta.parent_slot, Some(shred_parent_slot)); + assert!(!meta.has_update_parent()); +} + #[test] fn test_block_header_followed_by_update_parent() { let ledger_path = get_tmp_ledger_path_auto_delete!(); let blockstore = Blockstore::open(ledger_path.path()).unwrap(); + let slot = 12; let parent_5_id = Hash::new_unique(); blockstore - .insert_shreds(create_block_header_shreds(10, 5, parent_5_id), None, true) + .insert_shreds(create_block_header_shreds(slot, 5, parent_5_id), None, true) .unwrap(); - assert_eq!(blockstore.meta(10).unwrap().unwrap().parent_slot, Some(5)); - verify_next_slots(&blockstore, 5, &[10]); + assert_eq!(blockstore.meta(slot).unwrap().unwrap().parent_slot, Some(5)); + verify_next_slots(&blockstore, 5, &[slot]); let parent_3_id = Hash::new_unique(); blockstore .insert_shreds( - create_update_parent_shreds_with_shred_parent(10, 5, 3, parent_3_id, 32, true), + create_update_parent_shreds_with_shred_parent(slot, 5, 3, parent_3_id, 32, true), None, true, ) .unwrap(); - assert_eq!(blockstore.meta(10).unwrap().unwrap().parent_slot, Some(3)); + assert_eq!(blockstore.meta(slot).unwrap().unwrap().parent_slot, Some(3)); let parent_info = blockstore - .get_parent_info(10, BlockLocation::Original) + .get_parent_info(slot, BlockLocation::Original) .unwrap() .unwrap(); assert_eq!(parent_info.parent_slot, 3); @@ -7007,7 +7034,7 @@ fn test_block_header_followed_by_update_parent() { assert!(parent_info.has_update_parent()); verify_next_slots(&blockstore, 5, &[]); - verify_next_slots(&blockstore, 3, &[10]); + verify_next_slots(&blockstore, 3, &[slot]); } #[test] @@ -7015,7 +7042,7 @@ fn test_post_update_orig_after() { let ledger_path = get_tmp_ledger_path_auto_delete!(); let blockstore = Blockstore::open(ledger_path.path()).unwrap(); - let slot = 90; + let slot = 92; let original_parent = 85; let update_parent = 80; blockstore @@ -7072,7 +7099,7 @@ fn test_update_parent_shred_parent(update_parent_first: bool) { let ledger_path = get_tmp_ledger_path_auto_delete!(); let blockstore = Blockstore::open(ledger_path.path()).unwrap(); - let slot = 90; + let slot = 92; let original_parent = 85; let update_parent = 80; let bad_shred_parent = 84; @@ -7134,7 +7161,7 @@ fn test_marker_boundary_ooo() { let ledger_path = get_tmp_ledger_path_auto_delete!(); let blockstore = Blockstore::open(ledger_path.path()).unwrap(); - let slot = 93; + let slot = 96; let original_parent = 88; let update_parent = 80; blockstore diff --git a/ledger/src/blockstore_processor.rs b/ledger/src/blockstore_processor.rs index d9db5d8dd5a..7f4843497d2 100644 --- a/ledger/src/blockstore_processor.rs +++ b/ledger/src/blockstore_processor.rs @@ -39,6 +39,7 @@ use { commitment::VOTE_THRESHOLD_SIZE, dependency_tracker::DependencyTracker, installed_scheduler_pool::BankWithScheduler, + leader_schedule_utils::leader_slot_index, prioritization_fee_cache::PrioritizationFeeCache, runtime_config::RuntimeConfig, snapshot_controller::SnapshotController, @@ -1808,6 +1809,7 @@ fn confirm_slot_with_components( // UpdateParent as its first parent marker. From-shred-zero replay still // requires a block header before UpdateParent. let replay_starts_at_update_parent = migration_status.should_allow_fast_leader_handover(slot) + && leader_slot_index(slot) == 0 && blockstore .meta(slot) .expect("Blockstore operations must succeed") @@ -2456,7 +2458,9 @@ fn load_frozen_forks( // Live replay restarts UpdateParent slots from the marker's FEC set. // Startup replay must use the same offset or a restarted validator can // execute the obsolete optimistic-parent prefix. - if migration_status.should_allow_fast_leader_handover(slot) && meta.has_update_parent() + if migration_status.should_allow_fast_leader_handover(slot) + && leader_slot_index(slot) == 0 + && meta.has_update_parent() { progress.num_shreds = u64::from(meta.replay_fec_set_index); } @@ -2710,6 +2714,7 @@ pub fn check_chained_block_id( let slot = bank.slot(); if migration_status.should_allow_fast_leader_handover(slot) + && leader_slot_index(slot) == 0 && blockstore .meta(slot) .expect("Blockstore operations must succeed") @@ -6365,11 +6370,11 @@ pub mod tests { )); // Case 4: UpdateParent metadata does not bypass Tower validation. - insert_shreds_with_chained_merkle_root(13, 0, Hash::new_unique()); - let mut meta = blockstore.meta(13).unwrap().unwrap(); + insert_shreds_with_chained_merkle_root(16, 0, Hash::new_unique()); + let mut meta = blockstore.meta(16).unwrap().unwrap(); meta.replay_fec_set_index = 32; - blockstore.put_meta(13, &meta).unwrap(); - let child_bank = Bank::new_from_parent(parent_bank.clone(), SlotLeader::default(), 13); + blockstore.put_meta(16, &meta).unwrap(); + let child_bank = Bank::new_from_parent(parent_bank.clone(), SlotLeader::default(), 16); assert!(matches!( check_chained_block_id(&blockstore, &child_bank, &MigrationStatus::default()), ChainedBlockIdCheck::Mismatch @@ -6386,7 +6391,23 @@ pub mod tests { ChainedBlockIdCheck::Pass )); - // Case 6: Parent has no shreds (get_block_merkle_root returns Err) — + // Case 6: Non-first-window UpdateParent metadata does not bypass + // chained block ID validation. + insert_shreds_with_chained_merkle_root(13, 0, Hash::new_unique()); + let mut meta = blockstore.meta(13).unwrap().unwrap(); + meta.replay_fec_set_index = 32; + blockstore.put_meta(13, &meta).unwrap(); + let child_bank = Bank::new_from_parent(parent_bank.clone(), SlotLeader::default(), 13); + assert!(matches!( + check_chained_block_id( + &blockstore, + &child_bank, + &MigrationStatus::post_migration_status() + ), + ChainedBlockIdCheck::Mismatch + )); + + // Case 7: Parent has no shreds (get_block_merkle_root returns Err) — // should return Pass regardless of chained merkle root. let no_shreds_parent_bank = Arc::new(Bank::new_from_parent( parent_bank, diff --git a/runtime/src/block_component_processor.rs b/runtime/src/block_component_processor.rs index 15f01bdf755..a76cf232532 100644 --- a/runtime/src/block_component_processor.rs +++ b/runtime/src/block_component_processor.rs @@ -4,6 +4,7 @@ use { block_component_processor::vote_reward::{ CalcVoteRewardUpdateVoteStatesError, calc_vote_rewards_update_vote_states, }, + leader_schedule_utils::leader_slot_index, validated_block_finalization::{ BlockFinalizationCertError, ValidatedBlockFinalizationCert, }, @@ -70,6 +71,8 @@ pub enum BlockComponentProcessorError { NanosecondClockOutOfBounds, #[error("Spurious update parent")] SpuriousUpdateParent, + #[error("UpdateParent marker is only valid in the first slot of a leader window: slot {0}")] + UpdateParentNotFirstInLeaderWindow(Slot), #[error( "UpdateParent cannot be the initial parent marker unless replay starts at UpdateParent" )] @@ -102,7 +105,8 @@ impl BlockComponentProcessorError { | BlockComponentProcessorError::GenesisCertificateFailedVerification | BlockComponentProcessorError::MissingBlockFooter | BlockComponentProcessorError::MultipleUpdateParents - | BlockComponentProcessorError::SpuriousUpdateParent => false, + | BlockComponentProcessorError::SpuriousUpdateParent + | BlockComponentProcessorError::UpdateParentNotFirstInLeaderWindow(_) => false, } } } @@ -207,7 +211,7 @@ impl BlockComponentProcessor { ), BlockMarkerV1::UpdateParent(update_parent) if markers_fully_enabled => { - self.on_update_parent(update_parent.inner(), allow_initial_update_parent) + self.on_update_parent(slot, update_parent.inner(), allow_initial_update_parent) } // Any other combination means we saw a marker too early @@ -405,6 +409,7 @@ impl BlockComponentProcessor { fn on_update_parent( &mut self, + slot: Slot, update_parent: &VersionedUpdateParent, allow_initial_update_parent: bool, ) -> Result<(), BlockComponentProcessorError> { @@ -412,6 +417,10 @@ impl BlockComponentProcessor { return Err(BlockComponentProcessorError::MultipleUpdateParents); } + if leader_slot_index(slot) != 0 { + return Err(BlockComponentProcessorError::UpdateParentNotFirstInLeaderWindow(slot)); + } + if !self.has_header && !allow_initial_update_parent { return Err(BlockComponentProcessorError::UnexpectedInitialUpdateParent); } @@ -1301,12 +1310,27 @@ mod tests { }); assert!(matches!( - processor.on_update_parent(&update_parent, false), + processor.on_update_parent(4, &update_parent, false), Err(BlockComponentProcessorError::UnexpectedInitialUpdateParent) )); assert!(processor.update_parent.is_none()); } + #[test] + fn test_update_parent_rejects_non_first_leader_window_slot() { + let mut processor = BlockComponentProcessor::default(); + let update_parent = VersionedUpdateParent::V1(UpdateParentV1 { + new_parent_slot: 0, + new_parent_block_id: Hash::default(), + }); + + assert!(matches!( + processor.on_update_parent(5, &update_parent, true), + Err(BlockComponentProcessorError::UpdateParentNotFirstInLeaderWindow(5)) + )); + assert!(processor.update_parent.is_none()); + } + #[test] fn test_initial_up_ok() { let mut processor = BlockComponentProcessor::default(); @@ -1315,7 +1339,7 @@ mod tests { new_parent_block_id: Hash::default(), }); - processor.on_update_parent(&update_parent, true).unwrap(); + processor.on_update_parent(4, &update_parent, true).unwrap(); assert!(processor.update_parent.is_some()); } @@ -1338,7 +1362,7 @@ mod tests { }); assert!(matches!( - processor.on_update_parent(&update_parent, false), + processor.on_update_parent(4, &update_parent, false), Err(BlockComponentProcessorError::AbandonedBank(_)) )); } @@ -1352,11 +1376,11 @@ mod tests { }); // First should succeed - processor.on_update_parent(&update_parent, true).unwrap(); + processor.on_update_parent(4, &update_parent, true).unwrap(); // Second should fail assert!(matches!( - processor.on_update_parent(&update_parent, true), + processor.on_update_parent(4, &update_parent, true), Err(BlockComponentProcessorError::MultipleUpdateParents) )); } @@ -1366,6 +1390,7 @@ mod tests { let mut processor = BlockComponentProcessor::default(); processor .on_update_parent( + 4, &VersionedUpdateParent::V1(UpdateParentV1 { new_parent_slot: 0, new_parent_block_id: Hash::default(), @@ -1391,10 +1416,11 @@ mod tests { let migration_status = MigrationStatus::post_migration_status(); let mut processor = BlockComponentProcessor::default(); let (parent, bank_forks) = create_test_bank(); - let bank = create_child_bank(&bank_forks, &parent, 1); + let bank = create_child_bank(&bank_forks, &parent, 4); processor .on_update_parent( + bank.slot(), &VersionedUpdateParent::V1(UpdateParentV1 { new_parent_slot: 0, new_parent_block_id: Hash::default(), From df6a7501c0f01df0adf1bb1313bc0d97c1c4b06a Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Thu, 28 May 2026 22:25:48 -0400 Subject: [PATCH 119/576] bank: properly populate the clock sysvar in alpenglow (#12814) --- runtime/src/bank.rs | 42 +++++++++++++++++++--- runtime/src/bank/sysvar_cache.rs | 61 ++++++++++++++++++++++++++++++-- svm/src/transaction_processor.rs | 43 +++++++++++++++++++++- 3 files changed, 139 insertions(+), 7 deletions(-) diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 1c492ba4a4e..a19a61a7c4a 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -1955,8 +1955,15 @@ impl Bank { let (_, update_sysvars_time_us) = measure_us!({ self.update_slot_hashes(); self.update_stake_history(Some(parent_epoch)); - // Keep setting the tower clock sysvar till we have not migrated to Alpenglow. - if self.get_alpenglow_genesis_certificate().is_none() { + + if self.get_alpenglow_genesis_certificate().is_some() { + // Alpenglow banks have the timestamp populated via the footer + // We only populate the slot here + self.update_clock_slot_for_alpenglow(); + } else { + // PoH banks have the timestamp and slot populated at the beginning + // Note: The first alpenglow bank will have the timestamp populated + // here at the beginning as well as at the end via the footer - this is intentional. self.update_clock(Some(parent_epoch)); } self.update_last_restart_slot() @@ -2455,6 +2462,31 @@ impl Bank { }); } + /// In Alpenglow the clock sysvar's timestamp is populated from the block footer. + /// The timestamp value on the block footer is used as an estimate for when the block *ended*. + /// This is applied at the end of execution on the bank for use in the child. + /// + /// However we still need to update the slot and epoch fields for the clock sysvar at the *start* + /// of the bank, as transactions executing in this bank need to be able to read these values. + /// This function updates the slot and epoch fields while preserving the timestamp fields from the parent + /// bank's footer. + fn update_clock_slot_for_alpenglow(&self) { + let clock = self.clock(); + let clock = sysvar::clock::Clock { + slot: self.slot, + epoch: self.epoch_schedule().get_epoch(self.slot), + leader_schedule_epoch: self.epoch_schedule().get_leader_schedule_epoch(self.slot), + epoch_start_timestamp: clock.epoch_start_timestamp, + unix_timestamp: clock.unix_timestamp, + }; + self.update_sysvar_account(&sysvar::clock::id(), |account| { + create_account( + &clock, + self.inherit_specially_retained_account_fields(account), + ) + }); + } + pub fn update_last_restart_slot(&self) { // First, see what the currently stored last restart slot is. let current_last_restart_slot = self @@ -2502,9 +2534,8 @@ impl Bank { }); // Simply force fill sysvar cache rather than checking which sysvar was // actually updated since tests don't need to be optimized for performance. - self.transaction_processor.reset_sysvar_cache(); self.transaction_processor - .fill_missing_sysvar_cache_entries(self); + .reset_and_fill_sysvar_cache_entries(self); } fn update_slot_history(&self) { @@ -3279,6 +3310,9 @@ impl Bank { alpenclock_acct.set_data_from_slice(&data); self.store_account_and_update_capitalization(&NANOSECOND_CLOCK_ACCOUNT, &alpenclock_acct); + + self.transaction_processor + .reset_and_fill_sysvar_cache_entries(self); } /// Get the nanosecond clock value. Returns `None` if the nanosecond clock has not been diff --git a/runtime/src/bank/sysvar_cache.rs b/runtime/src/bank/sysvar_cache.rs index 92257bbefcb..3840bd5505e 100644 --- a/runtime/src/bank/sysvar_cache.rs +++ b/runtime/src/bank/sysvar_cache.rs @@ -4,8 +4,12 @@ use super::Bank; #[cfg(test)] mod tests { use { - super::*, crate::inflation_rewards::points::PointValue, - solana_genesis_config::create_genesis_config, solana_leader_schedule::SlotLeader, + super::*, + crate::{ + genesis_utils::activate_all_features_alpenglow, inflation_rewards::points::PointValue, + }, + solana_genesis_config::create_genesis_config, + solana_leader_schedule::SlotLeader, solana_sysvar::epoch_rewards::EpochRewards, }; @@ -161,4 +165,57 @@ mod tests { expected_epoch_rewards, ); } + + #[test] + fn test_alpenglow_clock_cache_updates_before_and_after_footer() { + let (mut genesis_config, _mint_keypair) = create_genesis_config(100_000); + activate_all_features_alpenglow(&mut genesis_config); + let (bank0, bank_forks) = + Bank::new_for_tests(&genesis_config).wrap_with_bank_forks_for_tests(); + assert!(bank0.get_alpenglow_genesis_certificate().is_some()); + + let parent_clock = bank0.clock(); + let bank1_slot = bank0.slot() + 1; + let bank1 = Bank::new_from_parent_with_bank_forks( + bank_forks.as_ref(), + bank0, + SlotLeader::default(), + bank1_slot, + ); + + let pre_footer_clock = bank1.clock(); + assert_eq!(pre_footer_clock.slot, bank1_slot); + assert_eq!(pre_footer_clock.unix_timestamp, parent_clock.unix_timestamp); + assert_eq!( + pre_footer_clock.epoch_start_timestamp, + parent_clock.epoch_start_timestamp + ); + + let cached_pre_footer_clock = { + let sysvar_cache = bank1.transaction_processor.sysvar_cache(); + sysvar_cache.get_clock().unwrap() + }; + assert_eq!(cached_pre_footer_clock.slot, bank1_slot); + assert_eq!( + cached_pre_footer_clock.unix_timestamp, + parent_clock.unix_timestamp + ); + + let footer_timestamp_nanos = parent_clock + .unix_timestamp + .saturating_add(42) + .saturating_mul(1_000_000_000); + bank1.update_clock_from_footer(footer_timestamp_nanos); + let footer_timestamp = footer_timestamp_nanos / 1_000_000_000; + + assert_eq!(bank1.clock().slot, bank1_slot); + assert_eq!(bank1.clock().unix_timestamp, footer_timestamp); + + let cached_post_footer_clock = { + let sysvar_cache = bank1.transaction_processor.sysvar_cache(); + sysvar_cache.get_clock().unwrap() + }; + assert_eq!(cached_post_footer_clock.slot, bank1_slot); + assert_eq!(cached_post_footer_clock.unix_timestamp, footer_timestamp); + } } diff --git a/svm/src/transaction_processor.rs b/svm/src/transaction_processor.rs index 568fe285858..006445a7566 100644 --- a/svm/src/transaction_processor.rs +++ b/svm/src/transaction_processor.rs @@ -1185,6 +1185,22 @@ impl TransactionBatchProcessor { callbacks: &CB, ) { let mut sysvar_cache = self.sysvar_cache.write().unwrap(); + Self::fill_missing_sysvar_cache_entries_from_accounts(&mut sysvar_cache, callbacks); + } + + pub fn reset_and_fill_sysvar_cache_entries( + &self, + callbacks: &CB, + ) { + let mut sysvar_cache = self.sysvar_cache.write().unwrap(); + sysvar_cache.reset(); + Self::fill_missing_sysvar_cache_entries_from_accounts(&mut sysvar_cache, callbacks); + } + + fn fill_missing_sysvar_cache_entries_from_accounts( + sysvar_cache: &mut SysvarCache, + callbacks: &CB, + ) { sysvar_cache.fill_missing_entries(|pubkey, set_sysvar| { if let Some((account, _slot)) = callbacks.get_account_shared_data(pubkey) { set_sysvar(account.data()); @@ -1893,6 +1909,31 @@ mod tests { let transaction_processor = TransactionBatchProcessor::::default(); // Fill the sysvar cache transaction_processor.fill_missing_sysvar_cache_entries(&mock_bank); + + let updated_clock = Clock { + slot: 6, + epoch_start_timestamp: 7, + epoch: 8, + leader_schedule_epoch: 9, + unix_timestamp: 10, + }; + let updated_clock_account = create_account_shared_data_for_test(&updated_clock); + mock_bank + .account_shared_data + .write() + .unwrap() + .insert(sysvar::clock::id(), updated_clock_account); + transaction_processor.reset_and_fill_sysvar_cache_entries(&mock_bank); + { + let sysvar_cache = transaction_processor.sysvar_cache.read().unwrap(); + assert_eq!( + sysvar_cache + .get_clock() + .expect("clock sysvar missing in cache"), + updated_clock.clone().into() + ); + } + // Reset the sysvar cache transaction_processor.reset_sysvar_cache(); @@ -1918,7 +1959,7 @@ mod tests { assert_eq!( cached_clock.expect("clock sysvar missing in cache"), - clock.into() + updated_clock.into() ); assert_eq!( cached_epoch_schedule.expect("epoch_schedule sysvar missing in cache"), From c2885840fc0f67411988865afa7ba4914de589ff Mon Sep 17 00:00:00 2001 From: Kamil Skalski Date: Fri, 29 May 2026 05:20:16 +0200 Subject: [PATCH 120/576] chore(rust): bump toolchain to 1.96 and nightly to 2026-04-11 (#12808) --- ci/rust-version.sh | 2 +- rust-toolchain.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ci/rust-version.sh b/ci/rust-version.sh index 1dc16a74575..71826e89376 100644 --- a/ci/rust-version.sh +++ b/ci/rust-version.sh @@ -29,7 +29,7 @@ fi if [[ -n ${RUST_NIGHTLY_VERSION:-} ]]; then nightly_version="$RUST_NIGHTLY_VERSION" else - nightly_version=2026-02-28 + nightly_version=2026-04-11 fi diff --git a/rust-toolchain.toml b/rust-toolchain.toml index f25b5b1407b..0f87b448033 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.95.0" +channel = "1.96.0" From 7e0670fa5a0481b79c39aeb48f746aa8351cbd88 Mon Sep 17 00:00:00 2001 From: Kamil Skalski Date: Fri, 29 May 2026 05:23:26 +0200 Subject: [PATCH 121/576] feat(snapshots): replace hardlinking with storages map (#12655) * feat(snapshots): replace hardlinking with storages list * Wipe account paths only on boot from archive path * fix: remove stale storages as files * Add migration impl * disable_remove when serialize_snapshot + should_finalize * Add validation that symlink target is within account_paths * Wipe previous state before loading from genesis * Rename to account_run_paths in context with account_snapshot_dirs * fix(ledger-tool): remove unconditional accounts clean * Disable remove_on_drop for storages loaded in ledger-tool * Save storages_list during migration * Update import in test * Keep using hardlinks constant * Clarify scenario justifying save of storages_list --- Cargo.lock | 1 - accounts-db/src/account_storage_entry.rs | 6 + accounts-db/src/accounts_file.rs | 8 + accounts-db/src/append_vec.rs | 7 + core/src/snapshot_packager_service.rs | 17 +- core/src/validator.rs | 34 +- dev-bins/Cargo.lock | 1 - ledger-tool/src/ledger_utils.rs | 54 +- ledger/src/bank_forks_utils.rs | 29 +- programs/sbf/Cargo.lock | 1 - runtime/Cargo.toml | 1 - runtime/src/serde_snapshot.rs | 2 + runtime/src/serde_snapshot/storages_list.rs | 84 +++ runtime/src/snapshot_bank_utils.rs | 235 ++++---- runtime/src/snapshot_utils.rs | 607 ++++++++++---------- snapshots/src/error.rs | 36 +- snapshots/src/paths.rs | 1 + 17 files changed, 618 insertions(+), 506 deletions(-) create mode 100644 runtime/src/serde_snapshot/storages_list.rs diff --git a/Cargo.lock b/Cargo.lock index da7fbffdf86..5504fe6603d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10050,7 +10050,6 @@ dependencies = [ "static_assertions", "strum", "strum_macros", - "symlink", "tempfile", "test-case", "thiserror 2.0.18", diff --git a/accounts-db/src/account_storage_entry.rs b/accounts-db/src/account_storage_entry.rs index 99fa2bbbd4b..2c5ec696c65 100644 --- a/accounts-db/src/account_storage_entry.rs +++ b/accounts-db/src/account_storage_entry.rs @@ -211,6 +211,12 @@ impl AccountStorageEntry { self.accounts.flush() } + /// Detach the on-disk file from this storage's lifetime; see + /// [`AccountsFile::disable_remove_on_drop`]. + pub fn disable_remove_on_drop(&self) { + self.accounts.disable_remove_on_drop(); + } + pub(crate) fn add_accounts(&self, num_accounts: usize, num_bytes: usize) { self.num_alive_accounts .fetch_add(num_accounts, Ordering::Release); diff --git a/accounts-db/src/accounts_file.rs b/accounts-db/src/accounts_file.rs index 3f72b556813..8dfa8eaa7d3 100644 --- a/accounts-db/src/accounts_file.rs +++ b/accounts-db/src/accounts_file.rs @@ -76,6 +76,14 @@ impl AccountsFile { } } + /// Detach the on-disk file from this storage's lifetime; see + /// [`AppendVec::disable_remove_on_drop`]. + pub fn disable_remove_on_drop(&self) { + match self { + Self::AppendVec(av) => av.disable_remove_on_drop(), + } + } + /// Return the total number of bytes of the zero lamport single ref accounts in the storage. /// Those bytes are "dead" and can be shrunk away. pub(crate) fn dead_bytes_due_to_zero_lamport_single_ref(&self, count: usize) -> usize { diff --git a/accounts-db/src/append_vec.rs b/accounts-db/src/append_vec.rs index 7f7521252e6..ef0c354d817 100644 --- a/accounts-db/src/append_vec.rs +++ b/accounts-db/src/append_vec.rs @@ -283,6 +283,13 @@ impl AppendVec { Ok(()) } + /// Detach the on-disk file from this AppendVec's lifetime so dropping the AppendVec no + /// longer removes the file. Used when ownership of the file is being handed off (e.g. to a + /// bank snapshot's storages list that needs the file to outlive validator exit). + pub fn disable_remove_on_drop(&self) { + self.remove_file_on_drop.store(false, Ordering::Release); + } + /// Return AppendVec opened in read-only file-io mode or `None` if it already is such pub(crate) fn reopen_as_readonly_file_io(&self) -> Option { if matches!(self.read_write_state, ReadWriteState::ReadOnly) { diff --git a/core/src/snapshot_packager_service.rs b/core/src/snapshot_packager_service.rs index cff47b2f30b..063bbf34522 100644 --- a/core/src/snapshot_packager_service.rs +++ b/core/src/snapshot_packager_service.rs @@ -286,6 +286,11 @@ impl SnapshotPackagerService { // the "storages flushed" file, so return early. return; } + // Pin the storage file so it outlives the validator-exit Drop chain (which would + // otherwise remove it via AppendVec::drop) and is available for fastboot on restart. + // The next startup opens a fresh AppendVec over the file; that one defaults back to + // removing-on-drop, so normal runtime cleanup (shrink, clean) still applies later. + storage.disable_remove_on_drop(); } info!("Flushing account storages... Done in {:?}", start.elapsed()); @@ -294,21 +299,21 @@ impl SnapshotPackagerService { snapshot_slot, ); - info!("Hard linking account storages..."); + info!("Writing account storages list..."); let start = Instant::now(); - let result = snapshot_utils::hard_link_storages_to_snapshot( + let result = snapshot_utils::write_storages_list_to_snapshot( &bank_snapshot_dir, - snapshot_slot, &snapshot_storages, + &io_setup, ); if let Err(err) = result { - warn!("Failed to hard link account storages: {err}"); - // If hard linking the storages failed, we do *NOT* want to mark the bank snapshot as + warn!("Failed to write account storages list: {err}"); + // If writing the storages list failed, we do *NOT* want to mark the bank snapshot as // loadable so return early. return; } info!( - "Hard linking account storages... Done in {:?}", + "Writing account storages list... Done in {:?}", start.elapsed(), ); diff --git a/core/src/validator.rs b/core/src/validator.rs index 34161549137..e1f13c48420 100644 --- a/core/src/validator.rs +++ b/core/src/validator.rs @@ -42,14 +42,14 @@ use { voting_service::VotingServiceOverride, }, agave_xdp::transmitter::{Transmitter, TransmitterBuilder}, - anyhow::{Context, Result, anyhow}, + anyhow::{Result, anyhow}, crossbeam_channel::{Receiver, bounded, unbounded}, serde::{Deserialize, Serialize}, solana_account::ReadableAccount, solana_accounts_db::{ accounts_db::{ACCOUNTS_DB_CONFIG_FOR_TESTING, AccountsDbConfig}, accounts_update_notifier_interface::AccountsUpdateNotifier, - utils::{move_and_async_delete_path_contents, validate_account_paths_for_direct_io}, + utils::validate_account_paths_for_direct_io, }, solana_client::connection_cache::{ConnectionCache, Protocol}, solana_clock::Slot, @@ -135,7 +135,7 @@ use { runtime_config::RuntimeConfig, snapshot_bank_utils, snapshot_controller::SnapshotController, - snapshot_utils::{self, clean_orphaned_account_snapshot_dirs}, + snapshot_utils, }, solana_send_transaction_service::send_transaction_service::Config as SendTransactionServiceConfig, solana_shred_version::compute_shred_version, @@ -826,29 +826,18 @@ impl Validator { let genesis_config = load_genesis(config, ledger_path)?; metrics_config_sanity_check(genesis_config.cluster_type)?; - info!("Validating and cleaning accounts paths..."); + info!("Validating accounts paths..."); *start_progress.write().unwrap() = ValidatorStartProgress::CleaningAccounts; - let mut timer = Measure::start("validate_and_clean_accounts_paths"); + let mut timer = Measure::start("validate_account_paths"); validate_account_paths(config)?; - cleanup_accounts_paths(config); timer.stop(); - info!("Validating and cleaning accounts paths done. {timer}"); + info!("Validating accounts paths done. {timer}"); snapshot_utils::purge_incomplete_bank_snapshots(&config.snapshot_config.bank_snapshots_dir); snapshot_utils::purge_old_bank_snapshots_at_startup( &config.snapshot_config.bank_snapshots_dir, ); - info!("Cleaning orphaned account snapshot directories..."); - let mut timer = Measure::start("clean_orphaned_account_snapshot_dirs"); - clean_orphaned_account_snapshot_dirs( - &config.snapshot_config.bank_snapshots_dir, - &config.account_snapshot_paths, - ) - .context("failed to clean orphaned account snapshot directories")?; - timer.stop(); - info!("Cleaning orphaned account snapshot directories done. {timer}"); - // token used to cancel tpu-client-next, streamer and BLS streamer. let cancel = CancellationToken::new(); { @@ -2313,6 +2302,11 @@ fn load_blockstore( let (bank_forks, starting_snapshot_hashes) = bank_from_snapshot_opt .unwrap_or_else(|| { + // Clean run from genesis — must not use any existing state from previous runs. + bank_forks_utils::discard_previous_run_state( + &config.snapshot_config.bank_snapshots_dir, + &config.account_paths, + ); bank_forks_utils::load_bank_forks_from_genesis( genesis_config, &blockstore, @@ -2958,12 +2952,6 @@ fn get_stake_percent_in_gossip(bank: &Bank, cluster_info: &ClusterInfo, log: boo online_stake_percentage as u64 } -fn cleanup_accounts_paths(config: &ValidatorConfig) { - for account_path in &config.account_paths { - move_and_async_delete_path_contents(account_path); - } -} - fn validate_account_paths(config: &ValidatorConfig) -> std::io::Result<()> { validate_account_paths_for_direct_io( config.snapshot_config.use_direct_io, diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index ed9d13a06c3..137b4b21490 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -8433,7 +8433,6 @@ dependencies = [ "static_assertions", "strum", "strum_macros", - "symlink", "tempfile", "thiserror 2.0.18", "wincode", diff --git a/ledger-tool/src/ledger_utils.rs b/ledger-tool/src/ledger_utils.rs index 144fffce662..2ae71be1303 100644 --- a/ledger-tool/src/ledger_utils.rs +++ b/ledger-tool/src/ledger_utils.rs @@ -10,10 +10,7 @@ use { log::*, solana_accounts_db::{ accounts_db::TOTAL_IO_URING_BUFFERS_SIZE_LIMIT, - utils::{ - create_all_accounts_run_and_snapshot_dirs, move_and_async_delete_path_contents, - validate_account_paths_for_direct_io, - }, + utils::{create_all_accounts_run_and_snapshot_dirs, validate_account_paths_for_direct_io}, }, solana_clock::Slot, solana_core::{ @@ -35,7 +32,6 @@ use { leader_schedule_cache::LeaderScheduleCache, use_snapshot_archives_at_startup::UseSnapshotArchivesAtStartup, }, - solana_measure::measure_time, solana_pubkey::Pubkey, solana_rpc::transaction_status_service::TransactionStatusService, solana_runtime::{ @@ -45,7 +41,7 @@ use { }, bank_forks::BankForks, snapshot_controller::SnapshotController, - snapshot_utils::{self, clean_orphaned_account_snapshot_dirs}, + snapshot_utils, }, solana_transaction::versioned::VersionedTransaction, solana_unified_scheduler_pool::DefaultSchedulerPool, @@ -81,9 +77,6 @@ const PROCESS_SLOTS_HELP_STRING: &str = #[derive(Error, Debug)] pub(crate) enum LoadAndProcessLedgerError { - #[error("failed to clean orphaned account snapshot directories: {0}")] - CleanOrphanedAccountSnapshotDirectories(#[source] std::io::Error), - #[error("failed to create all run and snapshot directories: {0}")] CreateAllAccountsRunAndSnapshotDirectories(#[source] std::io::Error), @@ -279,26 +272,8 @@ pub fn load_and_process_ledger( ) .map_err(LoadAndProcessLedgerError::ValidateAccountPaths)?; - let (_, measure_clean_account_paths) = measure_time!( - account_paths.iter().for_each(|path| { - if path.exists() { - info!("Cleaning contents of account path: {}", path.display()); - move_and_async_delete_path_contents(path); - } - }), - "Cleaning account paths" - ); - info!("{measure_clean_account_paths}"); - snapshot_utils::purge_incomplete_bank_snapshots(&snapshot_config.bank_snapshots_dir); - info!("Cleaning contents of account snapshot paths: {account_snapshot_paths:?}"); - clean_orphaned_account_snapshot_dirs( - &snapshot_config.bank_snapshots_dir, - &account_snapshot_paths, - ) - .map_err(LoadAndProcessLedgerError::CleanOrphanedAccountSnapshotDirectories)?; - let geyser_plugin_active = arg_matches.is_present("geyser_plugin_config"); let (accounts_update_notifier, transaction_notifier) = if geyser_plugin_active { let geyser_config_files = values_t_or_exit!(arg_matches, "geyser_plugin_config", String) @@ -375,6 +350,11 @@ pub fn load_and_process_ledger( ) .transpose() .unwrap_or_else(|| { + // Clean run from genesis — must not use any existing state from previous runs. + bank_forks_utils::discard_previous_run_state( + &snapshot_config.bank_snapshots_dir, + &account_paths, + ); bank_forks_utils::load_bank_forks_from_genesis( genesis_config, &blockstore, @@ -387,6 +367,26 @@ pub fn load_and_process_ledger( ) }) .map_err(LoadAndProcessLedgerError::LoadBankForks)?; + + // With hard-linking gone, an AppendVec's Drop removes the only copy of the storage file by + // default. For a fastboot load we don't want that — the files belong to the validator's + // local state and need to outlive this process. Storages ledger-tool creates itself (archive + // or genesis load) can be cleaned up normally. We detect fastboot via the bank snapshot dir + // at the root slot: archive/genesis paths went through `discard_previous_run_state`, which + // purges those dirs, so its presence here means we loaded from it. + { + let root_bank = bank_forks.read().unwrap().root_bank(); + let loaded_from_fastboot = snapshot_config + .bank_snapshots_dir + .join(root_bank.slot().to_string()) + .is_dir(); + if loaded_from_fastboot { + for storage in root_bank.get_snapshot_storages(None) { + storage.disable_remove_on_drop(); + } + } + } + let leader_schedule_cache = LeaderScheduleCache::new_from_bank(&bank_forks.read().unwrap().root_bank()); diff --git a/ledger/src/bank_forks_utils.rs b/ledger/src/bank_forks_utils.rs index ce06cd87582..37c67c22510 100644 --- a/ledger/src/bank_forks_utils.rs +++ b/ledger/src/bank_forks_utils.rs @@ -17,11 +17,14 @@ use { snapshot_hash::{FullSnapshotHash, IncrementalSnapshotHash, StartingSnapshotHashes}, }, log::*, - solana_accounts_db::accounts_update_notifier_interface::AccountsUpdateNotifier, + solana_accounts_db::{ + accounts_update_notifier_interface::AccountsUpdateNotifier, + utils::move_and_async_delete_path_contents, + }, solana_genesis_config::GenesisConfig, solana_runtime::{bank_forks::BankForks, snapshot_bank_utils, snapshot_utils}, std::{ - path::PathBuf, + path::{Path, PathBuf}, sync::{Arc, RwLock, atomic::AtomicBool}, }, thiserror::Error, @@ -60,6 +63,19 @@ pub enum BankForksUtilsError { pub type BankAndHashes = (Arc>, Option); +/// Tear down state left over from a previous run: purges old bank snapshots, wipes the account +/// run dirs, and wipes the legacy snapshot sibling trees. +/// +/// Call from any load path that commits to NOT fastbooting (archive load, genesis boot) — +/// i.e. anywhere the existing storages must not be loaded. +pub fn discard_previous_run_state(bank_snapshots_dir: &Path, account_run_paths: &[PathBuf]) { + snapshot_utils::purge_all_bank_snapshots(bank_snapshots_dir); + for account_run_path in account_run_paths { + move_and_async_delete_path_contents(account_run_path); + } + snapshot_utils::wipe_account_snapshot_dirs(account_run_paths); +} + /// Load the banks via genesis pub fn load_bank_forks_from_genesis( genesis_config: &GenesisConfig, @@ -209,11 +225,10 @@ pub fn try_load_bank_forks_from_snapshot( path: fastboot_snapshot.snapshot_path(), })? } else { - // Given that we are going to boot from an archive, the append vecs held in the snapshot dirs for fast-boot should - // be released. They will be released by the account_background_service anyway. But in the case of the account_paths - // using memory-mounted file system, they are not released early enough to give space for the new append-vecs from - // the archives, causing the out-of-memory problem. So, purge the snapshot dirs upfront before loading from the archive. - snapshot_utils::purge_all_bank_snapshots(&snapshot_config.bank_snapshots_dir); + // Committed to loading from a snapshot archive — the existing storages a previous run + // left around (kept for fastboot) are now orphans, and the archive will be extracted + // into the (cleared) run dirs. + discard_previous_run_state(&snapshot_config.bank_snapshots_dir, account_paths); snapshot_bank_utils::bank_from_snapshot_archives( account_paths, diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index b5c31ff5174..6af6c28e796 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -8240,7 +8240,6 @@ dependencies = [ "static_assertions", "strum", "strum_macros", - "symlink", "tempfile", "thiserror 2.0.18", "wincode", diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index c60a7011285..d7e446904a3 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -178,7 +178,6 @@ spl-generic-token = { workspace = true } static_assertions = { workspace = true } strum = { workspace = true, features = ["derive"] } strum_macros = { workspace = true } -symlink = { workspace = true } tempfile = { workspace = true } thiserror = { workspace = true } wincode = { workspace = true } diff --git a/runtime/src/serde_snapshot.rs b/runtime/src/serde_snapshot.rs index 6b0af01e8a0..a719ef3bbc1 100644 --- a/runtime/src/serde_snapshot.rs +++ b/runtime/src/serde_snapshot.rs @@ -66,6 +66,7 @@ use { mod obsolete_accounts; mod status_cache; mod storage; +mod storages_list; mod tests; mod types; mod utils; @@ -74,6 +75,7 @@ pub(crate) use { obsolete_accounts::{SerdeObsoleteAccounts, SerdeObsoleteAccountsMap}, status_cache::{deserialize_status_cache, serialize_status_cache}, storage::{SerializableAccountStorageEntry, SerializedAccountsFileId}, + storages_list::{StorageListItem, StoragesList}, }; const MAX_STREAM_SIZE: usize = 32 * 1024 * 1024 * 1024; diff --git a/runtime/src/serde_snapshot/storages_list.rs b/runtime/src/serde_snapshot/storages_list.rs new file mode 100644 index 00000000000..75aebdbb448 --- /dev/null +++ b/runtime/src/serde_snapshot/storages_list.rs @@ -0,0 +1,84 @@ +use { + solana_accounts_db::{account_storage_entry::AccountStorageEntry, accounts_db::AccountsFileId}, + solana_clock::Slot, + std::{collections::HashSet, sync::Arc}, + wincode::{SchemaRead, SchemaWrite}, +}; + +/// Identifies a storage file belonging to a bank snapshot, by `(slot, id)`. +/// +/// The file is local-only (never archived) and gated behind `SNAPSHOT_FASTBOOT_VERSION`, so the +/// on-disk encoding can use `AccountsFileId` (`u32`) directly without worrying about +/// forward-compat with potential future widening. +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, SchemaRead, SchemaWrite)] +pub struct StorageListItem { + pub slot: Slot, + pub id: AccountsFileId, +} + +/// On-disk format of the storages list file. +/// +/// Written next to the bank snapshot on graceful exit to record which storages in the account +/// run dirs make up the snapshot; read on startup to prune anything else before fastboot. +#[derive(Debug, SchemaRead, SchemaWrite)] +pub struct StoragesList { + list: Vec, +} + +impl StoragesList { + pub fn new_from_storages(snapshot_storages: &[Arc]) -> Self { + Self::from_items( + snapshot_storages + .iter() + .map(|storage| StorageListItem { + slot: storage.slot(), + id: storage.id(), + }) + .collect(), + ) + } + + /// Build a `StoragesList` from an existing `Vec` of items. + pub fn from_items(list: Vec) -> Self { + Self { list } + } + + pub fn into_slot_file_id_set(self) -> HashSet<(Slot, AccountsFileId)> { + self.list + .into_iter() + .map(|item| (item.slot, item.id)) + .collect() + } +} + +#[cfg(test)] +mod tests { + use { + super::*, + crate::serde_snapshot::{deserialize_wincode_from, serialize_into}, + std::io::Cursor, + }; + + #[test] + fn test_roundtrip_storages_list() { + let list = StoragesList { + list: vec![ + StorageListItem { slot: 7, id: 42 }, + StorageListItem { slot: 11, id: 13 }, + StorageListItem { + slot: Slot::MAX, + id: AccountsFileId::MAX, + }, + ], + }; + let expected_set: HashSet<(Slot, AccountsFileId)> = + HashSet::from([(7, 42), (11, 13), (Slot::MAX, AccountsFileId::MAX)]); + + let mut buf = Vec::new(); + serialize_into(Cursor::new(&mut buf), &list).unwrap(); + + let decoded: StoragesList = deserialize_wincode_from(Cursor::new(&buf)).unwrap(); + assert_eq!(decoded.into_slot_file_id_set(), expected_set); + } +} diff --git a/runtime/src/snapshot_bank_utils.rs b/runtime/src/snapshot_bank_utils.rs index 8347308f96c..0f21833b661 100644 --- a/runtime/src/snapshot_bank_utils.rs +++ b/runtime/src/snapshot_bank_utils.rs @@ -25,7 +25,7 @@ use { rebuild_storages_from_snapshot_dir, verify_and_unarchive_snapshots, }, }, - agave_fs::{dirs, io_setup::IoSetupState}, + agave_fs::io_setup::IoSetupState, agave_snapshots::{ SnapshotArchiveKind, SnapshotKind, error::{ @@ -376,11 +376,9 @@ pub fn bank_from_snapshot_dir( bank_snapshot.snapshot_dir.display() ); - // Clear the contents of the account paths run directories. When constructing the bank, the appendvec - // files will be extracted from the snapshot hardlink directories into these run/ directories. - for path in account_paths { - dirs::remove_dir_contents(path); - } + // Storages from this snapshot already live under `account_paths`; the storages list + // inside the bank snapshot dir tells us which ones belong to it, and stale files + // (e.g. from later slots) are pruned by `rebuild_storages_from_snapshot_dir`. let next_append_vec_id = Arc::new(AtomicAccountsFileId::new(0)); @@ -837,9 +835,8 @@ mod tests { genesis_utils::{GenesisConfigInfo, create_genesis_config_with_leader}, snapshot_package::BankSnapshotPackage, snapshot_utils::{ - clean_orphaned_account_snapshot_dirs, create_tmp_accounts_dir_for_tests, - get_bank_snapshots, get_highest_bank_snapshot, get_highest_loadable_bank_snapshot, - purge_all_bank_snapshots, purge_bank_snapshot, + create_tmp_accounts_dir_for_tests, get_bank_snapshots, get_highest_bank_snapshot, + get_highest_loadable_bank_snapshot, purge_all_bank_snapshots, purge_bank_snapshot, purge_bank_snapshots_older_than_slot, purge_incomplete_bank_snapshots, purge_old_bank_snapshots, purge_old_bank_snapshots_at_startup, snapshot_storage_rebuilder::get_slot_and_append_vec_id, @@ -850,7 +847,10 @@ mod tests { SnapshotVersion, error::VerifySlotDeltasError, paths::get_bank_snapshot_dir, }, semver::Version, - solana_accounts_db::accounts_db::ACCOUNTS_DB_CONFIG_FOR_TESTING, + solana_accounts_db::{ + accounts_db::{ACCOUNTS_DB_CONFIG_FOR_TESTING, AccountsFileId}, + accounts_file::AccountsFile, + }, solana_hash::Hash, solana_keypair::Keypair, solana_native_token::LAMPORTS_PER_SOL, @@ -886,7 +886,7 @@ mod tests { genesis_config: &GenesisConfig, bank_snapshots_dir: impl AsRef, num_total: usize, - should_flush_and_hard_link_storages: bool, + should_finalize: bool, ) -> Arc { let (bank0, bank_forks) = Bank::new_for_tests(genesis_config).wrap_with_bank_forks_for_tests(); @@ -906,7 +906,7 @@ mod tests { &bank_snapshots_dir, &bank, SnapshotVersion::default(), - should_flush_and_hard_link_storages, + should_finalize, ) .unwrap(); } @@ -915,7 +915,7 @@ mod tests { } /// Creates a bank snapshot from a bank, regardless of state. The Bank will be frozen during - /// the process. Passing in should_flush_and_hard_link_storages as true will create a + /// the process. Passing in should_finalize as true will create a /// a fastbootable bank snapshot /// /// Requires: @@ -925,7 +925,7 @@ mod tests { bank_snapshots_dir: impl AsRef, bank: &Bank, snapshot_version: SnapshotVersion, - should_flush_and_hard_link_storages: bool, + should_finalize: bool, ) -> agave_snapshots::Result<()> { assert!(bank.is_complete()); assert!(bank.block_id().is_some()); @@ -948,7 +948,7 @@ mod tests { snapshot_version, bank_snapshot_package, snapshot_storages.as_slice(), - should_flush_and_hard_link_storages, + should_finalize, &IoSetupState::default(), )?; @@ -1673,7 +1673,7 @@ mod tests { } #[test] - fn test_bank_snapshot_dir_accounts_hardlinks() { + fn test_bank_snapshot_dir_storages_list() { let bank = Bank::new_for_tests(&GenesisConfig::default()); bank.fill_bank_with_ticks_for_tests(); bank.set_block_id(Some(Hash::default())); @@ -1687,25 +1687,13 @@ mod tests { ) .unwrap(); - let accounts_hardlinks_dir = get_bank_snapshot_dir(&bank_snapshots_dir, bank.slot()) - .join(snapshot_paths::SNAPSHOT_ACCOUNTS_HARDLINKS); - assert!(fs::metadata(&accounts_hardlinks_dir).is_ok()); - - let mut hardlink_dirs = Vec::new(); - // This directory contain symlinks to all accounts snapshot directories. - for entry in fs::read_dir(accounts_hardlinks_dir).unwrap() { - let entry = entry.unwrap(); - let symlink = entry.path(); - let dst_path = fs::read_link(symlink).unwrap(); - assert!(fs::metadata(&dst_path).is_ok()); - hardlink_dirs.push(dst_path); - } - let bank_snapshot_dir = get_bank_snapshot_dir(&bank_snapshots_dir, bank.slot()); - assert!(purge_bank_snapshot(bank_snapshot_dir).is_ok()); + let storages_list_file = + bank_snapshot_dir.join(snapshot_paths::SNAPSHOT_STORAGES_LIST_FILENAME); + assert!(fs::metadata(&storages_list_file).is_ok()); - // When the bank snapshot is removed, all the snapshot hardlink directories should be removed. - assert!(hardlink_dirs.iter().all(|dir| fs::metadata(dir).is_err())); + assert!(purge_bank_snapshot(&bank_snapshot_dir).is_ok()); + assert!(fs::metadata(&bank_snapshot_dir).is_err()); } /// Test versioning when fastbooting @@ -1765,14 +1753,14 @@ mod tests { #[test_case(false)] #[test_case(true)] - fn test_get_highest_bank_snapshot(should_flush_and_hard_link_storages: bool) { + fn test_get_highest_bank_snapshot(should_finalize: bool) { let genesis_config = GenesisConfig::default(); let bank_snapshots_dir = tempfile::TempDir::new().unwrap(); let _bank = create_snapshot_dirs_for_tests( &genesis_config, &bank_snapshots_dir, 4, - should_flush_and_hard_link_storages, + should_finalize, ); let snapshot = get_highest_bank_snapshot(&bank_snapshots_dir).unwrap(); @@ -1803,83 +1791,16 @@ mod tests { assert_eq!(snapshot.slot, 1); } - #[test] - fn test_clean_orphaned_account_snapshot_dirs() { - let genesis_config = GenesisConfig::default(); - let bank_snapshots_dir = tempfile::TempDir::new().unwrap(); - let _bank = create_snapshot_dirs_for_tests(&genesis_config, &bank_snapshots_dir, 2, true); - - let snapshot_dir_slot_2 = bank_snapshots_dir.path().join("2"); - let accounts_link_dir_slot_2 = - snapshot_dir_slot_2.join(snapshot_paths::SNAPSHOT_ACCOUNTS_HARDLINKS); - - // the symlinks point to the account snapshot hardlink directories /snapshot// for slot 2 - // get them via read_link - let hardlink_dirs_slot_2: Vec = fs::read_dir(accounts_link_dir_slot_2) - .unwrap() - .map(|entry| { - let symlink = entry.unwrap().path(); - fs::read_link(symlink).unwrap() - }) - .collect(); - - // remove the bank snapshot directory for slot 2, so the account snapshot slot 2 directories become orphaned - fs::remove_dir_all(snapshot_dir_slot_2).unwrap(); - - // verify the orphaned account snapshot hardlink directories are still there - assert!( - hardlink_dirs_slot_2 - .iter() - .all(|dir| fs::metadata(dir).is_ok()) - ); - - let account_snapshot_paths: Vec = hardlink_dirs_slot_2 - .iter() - .map(|dir| dir.parent().unwrap().parent().unwrap().to_path_buf()) - .collect(); - // clean the orphaned hardlink directories - clean_orphaned_account_snapshot_dirs(&bank_snapshots_dir, &account_snapshot_paths).unwrap(); - - // verify the hardlink directories are gone - assert!( - hardlink_dirs_slot_2 - .iter() - .all(|dir| fs::metadata(dir).is_err()) - ); - } - - // Ensure that `clean_orphaned_account_snapshot_dirs()` works correctly for bank snapshots - // that *do not* hard link the storages into their staging dir. - #[test] - fn test_clean_orphaned_account_snapshot_dirs_no_hard_link() { - let genesis_config = GenesisConfig::default(); - let bank_snapshots_dir = tempfile::TempDir::new().unwrap(); - let _bank = create_snapshot_dirs_for_tests(&genesis_config, &bank_snapshots_dir, 2, false); - - // Ensure the bank snapshot dir does exist. - let bank_snapshot_dir = get_bank_snapshot_dir(&bank_snapshots_dir, 2); - assert!(fs::exists(&bank_snapshot_dir).unwrap()); - - // Ensure the accounts hard links dir does *not* exist for this bank snapshot - // (since we asked create_snapshot_dirs_for_tests() to *not* hard link). - let bank_snapshot_accounts_hard_link_dir = - bank_snapshot_dir.join(snapshot_paths::SNAPSHOT_ACCOUNTS_HARDLINKS); - assert!(!fs::exists(&bank_snapshot_accounts_hard_link_dir).unwrap()); - - // Now make sure clean_orphaned_account_snapshot_dirs() doesn't error. - clean_orphaned_account_snapshot_dirs(&bank_snapshots_dir, &[]).unwrap(); - } - #[test_case(false)] #[test_case(true)] - fn test_purge_incomplete_bank_snapshots(should_flush_and_hard_link_storages: bool) { + fn test_purge_incomplete_bank_snapshots(should_finalize: bool) { let genesis_config = GenesisConfig::default(); let bank_snapshots_dir = tempfile::TempDir::new().unwrap(); let _bank = create_snapshot_dirs_for_tests( &genesis_config, &bank_snapshots_dir, 2, - should_flush_and_hard_link_storages, + should_finalize, ); // remove the "version" files so the snapshots will be purged @@ -2097,7 +2018,7 @@ mod tests { bank2.set_block_id(Some(Hash::default())); assert_eq!(bank2.get_balance(&key2.pubkey()), 0); - // Take a bank snapshot, passing `true` for `should_flush_and_hard_link_storages`. + // Take a bank snapshot, passing `true` for `should_finalize`. // This ensures that `serialize_snapshot` performs all necessary steps to create // a snapshot that supports fastbooting. create_bank_snapshot_from_bank( @@ -2180,7 +2101,7 @@ mod tests { bank.fill_bank_with_ticks_for_tests(); bank.set_block_id(Some(Hash::default())); - // Take a bank snapshot, passing `true` for `should_flush_and_hard_link_storages`. + // Take a bank snapshot, passing `true` for `should_finalize`. // This ensures that `serialize_snapshot` performs all necessary steps to create // a snapshot that supports fastbooting. create_bank_snapshot_from_bank( @@ -2225,6 +2146,92 @@ mod tests { assert_eq!(max_id, next_id - 1); } + /// Drop a stale `.` file into the account_paths run dir before calling + /// `bank_from_snapshot_dir`, and verify that the fastboot rebuild path removes it (because + /// the `(slot, id)` pair isn't in the storages list) while keeping the snapshot's own + /// storage files in place and producing a working bank. + #[test] + fn test_bank_from_snapshot_dir_prunes_stale_storage() { + let GenesisConfigInfo { genesis_config, .. } = create_genesis_config_with_leader( + 1_000_000 * LAMPORTS_PER_SOL, + &Pubkey::new_unique(), + 1_000_000 * LAMPORTS_PER_SOL, + ); + let bank_snapshots_dir = tempfile::TempDir::new().unwrap(); + let bank = Bank::new_for_tests(&genesis_config); + bank.fill_bank_with_ticks_for_tests(); + bank.set_block_id(Some(Hash::default())); + + create_bank_snapshot_from_bank( + &bank_snapshots_dir, + &bank, + SnapshotVersion::default(), + true, + ) + .unwrap(); + let bank_snapshot = get_highest_bank_snapshot(&bank_snapshots_dir).unwrap(); + let account_paths = &bank.rc.accounts.accounts_db.paths; + + // Collect the legit paths (created by `create_bank_snapshot_from_bank` above) before + // we drop any stale files, so we can assert they survive the prune. + let pre_load_legit_files: Vec<_> = account_paths + .iter() + .flat_map(|account_path| { + fs::read_dir(account_path) + .unwrap() + .map(|e| e.unwrap().path()) + }) + .collect(); + assert!( + !pre_load_legit_files.is_empty(), + "expected at least one accounts storage file" + ); + + // Pick a `(slot, id)` that the snapshot doesn't reference. The snapshot only covers + // storages up to `bank.slot()`, so a far-future slot is guaranteed to be stale. + let stale_slot = bank.slot() + 1_000; + let stale_id = AccountsFileId::MAX; + let stale_files: Vec<_> = account_paths + .iter() + .map(|account_path| { + let stale = account_path.join(AccountsFile::file_name(stale_slot, stale_id)); + fs::write(&stale, b"junk").unwrap(); + stale + }) + .collect(); + + let bank_constructed = bank_from_snapshot_dir( + account_paths, + &bank_snapshot, + &genesis_config, + &RuntimeConfig::default(), + None, + None, + None, + false, + ACCOUNTS_DB_CONFIG_FOR_TESTING, + None, + Arc::default(), + ) + .unwrap(); + assert_eq!(bank_constructed, bank); + + for stale in stale_files { + assert!( + !stale.exists(), + "stale storage file '{}' should have been pruned", + stale.display(), + ); + } + for legit in pre_load_legit_files { + assert!( + legit.exists(), + "snapshot storage file '{}' should have survived the prune", + legit.display(), + ); + } + } + /// Ensure bank_from_snapshot_dir() catches a snapshot with incorrect capitalization. #[test] fn test_bank_from_snapshot_dir_bad_capitalization() { @@ -2280,14 +2287,14 @@ mod tests { #[test_case(false)] #[test_case(true)] - fn test_purge_all_bank_snapshots(should_flush_and_hard_link_storages: bool) { + fn test_purge_all_bank_snapshots(should_finalize: bool) { let genesis_config = GenesisConfig::default(); let bank_snapshots_dir = tempfile::TempDir::new().unwrap(); let _bank = create_snapshot_dirs_for_tests( &genesis_config, &bank_snapshots_dir, 10, - should_flush_and_hard_link_storages, + should_finalize, ); // Keep bank in this scope so that its account_paths tmp dirs are not released, and purge_all_bank_snapshots // can clear the account hardlinks correctly. @@ -2299,14 +2306,14 @@ mod tests { #[test_case(false)] #[test_case(true)] - fn test_purge_old_bank_snapshots(should_flush_and_hard_link_storages: bool) { + fn test_purge_old_bank_snapshots(should_finalize: bool) { let genesis_config = GenesisConfig::default(); let bank_snapshots_dir = tempfile::TempDir::new().unwrap(); let _bank = create_snapshot_dirs_for_tests( &genesis_config, &bank_snapshots_dir, 10, - should_flush_and_hard_link_storages, + should_finalize, ); // Keep bank in this scope so that its account_paths tmp dirs are not released, and purge_old_bank_snapshots // can clear the account hardlinks correctly. @@ -2326,7 +2333,7 @@ mod tests { #[test_case(false)] #[test_case(true)] - fn test_purge_bank_snapshots_older_than_slot(should_flush_and_hard_link_storages: bool) { + fn test_purge_bank_snapshots_older_than_slot(should_finalize: bool) { let genesis_config = GenesisConfig::default(); let bank_snapshots_dir = tempfile::TempDir::new().unwrap(); @@ -2335,7 +2342,7 @@ mod tests { &genesis_config, &bank_snapshots_dir, 9, - should_flush_and_hard_link_storages, + should_finalize, ); let bank_snapshots_before = get_bank_snapshots(&bank_snapshots_dir); @@ -2359,7 +2366,7 @@ mod tests { #[test_case(false)] #[test_case(true)] - fn test_purge_old_bank_snapshots_at_startup(should_flush_and_hard_link_storages: bool) { + fn test_purge_old_bank_snapshots_at_startup(should_finalize: bool) { let genesis_config = GenesisConfig::default(); let bank_snapshots_dir = tempfile::TempDir::new().unwrap(); @@ -2368,7 +2375,7 @@ mod tests { &genesis_config, &bank_snapshots_dir, 9, - should_flush_and_hard_link_storages, + should_finalize, ); purge_old_bank_snapshots_at_startup(&bank_snapshots_dir); diff --git a/runtime/src/snapshot_utils.rs b/runtime/src/snapshot_utils.rs index e5268c7c6f7..073e1cbd89f 100644 --- a/runtime/src/snapshot_utils.rs +++ b/runtime/src/snapshot_utils.rs @@ -6,7 +6,7 @@ use { serde_snapshot::{ self, AccountsDbFields, ExtraFieldsToSerialize, SerdeObsoleteAccountsMap, SerializableAccountStorageEntry, SnapshotAccountsDbFields, SnapshotBankFields, - SnapshotStreams, + SnapshotStreams, StorageListItem, StoragesList, }, snapshot_package::BankSnapshotPackage, snapshot_utils::snapshot_storage_rebuilder::{ @@ -14,7 +14,7 @@ use { }, }, agave_fs::{ - FileInfo, + FileInfo, FileSize, buffered_reader::large_file_buf_reader, buffered_writer::{SizeLimitedWriter, large_file_buf_writer}, io_setup::IoSetupState, @@ -22,9 +22,7 @@ use { agave_snapshots::{ ArchiveFormat, Result, SnapshotArchiveKind, SnapshotVersion, archive_snapshot, error::{ - AddBankSnapshotError, GetSnapshotAccountsHardLinkDirError, - HardLinkStoragesToSnapshotError, SnapshotError, SnapshotFastbootError, - SnapshotNewFromDirError, + AddBankSnapshotError, SnapshotError, SnapshotFastbootError, SnapshotNewFromDirError, }, paths::{self as snapshot_paths, incremental_snapshot_archives_iter}, snapshot_archive_info::{ @@ -42,9 +40,11 @@ use { solana_accounts_db::{ account_storage::AccountStorageMap, account_storage_entry::AccountStorageEntry, - accounts_db::AtomicAccountsFileId, - accounts_file::AccountsFile, - utils::{ACCOUNTS_RUN_DIR, ACCOUNTS_SNAPSHOT_DIR, move_and_async_delete_path}, + accounts_db::{AccountsFileId, AtomicAccountsFileId}, + utils::{ + ACCOUNTS_RUN_DIR, ACCOUNTS_SNAPSHOT_DIR, move_and_async_delete_path, + move_and_async_delete_path_contents, + }, }, solana_clock::Slot, solana_measure::{measure::Measure, measure_time, measure_us}, @@ -71,10 +71,15 @@ pub mod snapshot_storage_rebuilder; /// Limit is set assuming 24 bytes per entry, 5% of 10 billion accounts /// = 500 million entries * 24 bytes = 12 GB pub const MAX_OBSOLETE_ACCOUNTS_FILE_SIZE: u64 = 1024 * 1024 * 1024 * 12; // 12 GB +/// Limit the size of the storages list file. +/// Each `(slot, id)` entry encodes to 12 bytes; 100 MiB covers ~8.7 million entries, well past +/// any realistic storage count. +pub const MAX_STORAGES_LIST_FILE_SIZE: u64 = 100 * 1024 * 1024; // 100 MiB pub const MAX_SNAPSHOT_DATA_FILE_SIZE: u64 = 32 * 1024 * 1024 * 1024; // 32 GiB const MAX_SNAPSHOT_VERSION_FILE_SIZE: u64 = 8; // byte -/// Buffer size that allows several concurrent reads using default io-uring reader read size (1MiB) -const OBSOLETE_ACCOUNTS_READ_BUF_SIZE: usize = 4 * 1024 * 1024; +/// Buffer size for reading auxiliary per-snapshot files (obsolete accounts, storages list). +/// Sized to allow several concurrent reads at the default io-uring reader read size (1MiB). +const AUX_SNAPSHOT_FILE_READ_BUF_SIZE: usize = 4 * 1024 * 1024; // Snapshot Fastboot Version History // Legacy - No fastboot version file, storages flushed file presence determines if snapshot is loadable @@ -82,7 +87,13 @@ const OBSOLETE_ACCOUNTS_READ_BUF_SIZE: usize = 4 * 1024 * 1024; // 2.0.0 - Obsolete Accounts File added, storages flushed file not written anymore // Snapshots created with version 2.0.0 will not fastboot to older versions // Snapshots created with versions <2.0.0 will fastboot to version 2.0.0 -const SNAPSHOT_FASTBOOT_VERSION: Version = Version::new(2, 0, 0); +// 3.0.0 - Storages List file added, replaces the per-storage hardlink dirs. +// 3.0.0 validators can still fastboot from 2.0.0 snapshots: the legacy hardlinks are +// migrated back into the account run dirs at load time (see `migrate_legacy_hardlinks`), +// and the next teardown writes the new-format storages list. +// Note: 2.0.0 validators cannot fastboot from 3.0.0 snapshots because the per-storage +// hardlink dirs they rely on are no longer written; they must fall back to archive. +const SNAPSHOT_FASTBOOT_VERSION: Version = Version::new(3, 0, 0); /// Information about a bank snapshot. Namely the slot of the bank, the path to the snapshot, and /// the kind of the snapshot. @@ -252,71 +263,6 @@ pub(crate) struct StorageAndNextAccountsFileId { pub next_append_vec_id: AtomicAccountsFileId, } -/// The account snapshot directories under /snapshot/ contain account files hardlinked -/// from /run taken at snapshot time. They are referenced by the symlinks from the -/// bank snapshot dir snapshot//accounts_hardlinks/. We observed that sometimes the bank snapshot dir -/// could be deleted but the account snapshot directories were left behind, possibly by some manual operations -/// or some legacy code not using the symlinks to clean up the account snapshot hardlink directories. -/// This function cleans up any account snapshot directories that are no longer referenced by the bank -/// snapshot dirs, to ensure proper snapshot operations. -pub fn clean_orphaned_account_snapshot_dirs( - bank_snapshots_dir: impl AsRef, - account_snapshot_paths: &[PathBuf], -) -> io::Result<()> { - // Create the HashSet of the account snapshot hardlink directories referenced by the snapshot dirs. - // This is used to clean up any hardlinks that are no longer referenced by the snapshot dirs. - let mut account_snapshot_dirs_referenced = HashSet::new(); - let snapshots = get_bank_snapshots(bank_snapshots_dir); - for snapshot in snapshots { - let account_hardlinks_dir = snapshot - .snapshot_dir - .join(snapshot_paths::SNAPSHOT_ACCOUNTS_HARDLINKS); - // loop through entries in the snapshot_hardlink_dir, read the symlinks, add the target to the HashSet - let Ok(read_dir) = fs::read_dir(&account_hardlinks_dir) else { - // The bank snapshot may not have a hard links dir with the storages. - // This is fine, and happens for bank snapshots we do *not* fastboot from. - // In this case, log it and go to the next bank snapshot. - debug!( - "failed to read account hardlinks dir '{}'", - account_hardlinks_dir.display(), - ); - continue; - }; - for entry in read_dir { - let path = entry?.path(); - let target = fs::read_link(&path).map_err(|err| { - IoError::other(format!( - "failed to read symlink '{}': {err}", - path.display(), - )) - })?; - account_snapshot_dirs_referenced.insert(target); - } - } - - // loop through the account snapshot hardlink directories, if the directory is not in the account_snapshot_dirs_referenced set, delete it - for account_snapshot_path in account_snapshot_paths { - let read_dir = fs::read_dir(account_snapshot_path).map_err(|err| { - IoError::other(format!( - "failed to read account snapshot dir '{}': {err}", - account_snapshot_path.display(), - )) - })?; - for entry in read_dir { - let path = entry?.path(); - if !account_snapshot_dirs_referenced.contains(&path) { - info!( - "Removing orphaned account snapshot hardlink directory '{}'...", - path.display() - ); - move_and_async_delete_path(&path); - } - } - } - - Ok(()) -} - /// Purges incomplete bank snapshots pub fn purge_incomplete_bank_snapshots(bank_snapshots_dir: impl AsRef) { let Ok(read_dir_iter) = std::fs::read_dir(&bank_snapshots_dir) else { @@ -423,10 +369,17 @@ fn is_bank_snapshot_loadable( fn is_snapshot_fastboot_compatible( version: &Version, ) -> std::result::Result { - if version.major <= SNAPSHOT_FASTBOOT_VERSION.major { - Ok(true) - } else { - Err(SnapshotFastbootError::IncompatibleVersion(version.clone())) + match version.major { + // Current format: storages list lives next to the bank snapshot file. + 3 => Ok(true), + // Legacy format: per-storage hardlink dirs. `rebuild_storages_from_snapshot_dir` + // migrates them to the new format at load time. + 2 => Ok(true), + v if v > SNAPSHOT_FASTBOOT_VERSION.major => { + Err(SnapshotFastbootError::IncompatibleVersion(version.clone())) + } + // Older format we no longer know how to load — fall back to archive. + _ => Ok(false), } } @@ -533,7 +486,7 @@ pub fn serialize_snapshot( snapshot_version: SnapshotVersion, bank_snapshot_package: BankSnapshotPackage, snapshot_storages: &[Arc], - should_flush_and_hard_link_storages: bool, + should_finalize: bool, io_setup: &IoSetupState, ) -> Result { let BankSnapshotPackage { @@ -604,19 +557,18 @@ pub fn serialize_snapshot( .map_err(|err| AddBankSnapshotError::WriteSnapshotVersionFile(err, version_path))? ); - let (flush_storages_us, hard_link_storages_us, serialize_obsolete_accounts_us) = - if should_flush_and_hard_link_storages { + let (flush_storages_us, serialize_obsolete_accounts_us, write_storages_list_us) = + if should_finalize { let flush_measure = Measure::start(""); for storage in snapshot_storages { storage.flush().map_err(|err| { AddBankSnapshotError::FlushStorage(err, storage.path().to_path_buf()) })?; + // We're about to mark this snapshot fastboot-loadable. Pin the storage + // file so it outlives the validator-exit Drop chain. + storage.disable_remove_on_drop(); } let flush_us = flush_measure.end_as_us(); - let (_, hard_link_us) = measure_us!( - hard_link_storages_to_snapshot(&bank_snapshot_dir, slot, snapshot_storages) - .map_err(AddBankSnapshotError::HardLinkStorages)? - ); let (_, serialize_obsolete_accounts_us) = measure_us!({ write_obsolete_accounts_to_snapshot( @@ -628,13 +580,22 @@ pub fn serialize_snapshot( .map_err(|err| AddBankSnapshotError::SerializeObsoleteAccounts(Box::new(err)))? }); + let (_, write_storages_list_us) = measure_us!( + write_storages_list_to_snapshot( + &bank_snapshot_dir, + snapshot_storages, + io_setup, + ) + .map_err(|err| AddBankSnapshotError::WriteStoragesList(Box::new(err)))? + ); + mark_bank_snapshot_as_loadable(&bank_snapshot_dir) .map_err(AddBankSnapshotError::MarkSnapshotLoadable)?; ( Some(flush_us), - Some(hard_link_us), Some(serialize_obsolete_accounts_us), + Some(write_storages_list_us), ) } else { (None, None, None) @@ -649,8 +610,8 @@ pub fn serialize_snapshot( ("bank_size", bank_snapshot_consumed_size, i64), ("status_cache_size", status_cache_consumed_size, i64), ("flush_storages_us", flush_storages_us, Option), - ("hard_link_storages_us", hard_link_storages_us, Option), ("serialize_obsolete_accounts_us", serialize_obsolete_accounts_us, Option), + ("write_storages_list_us", write_storages_list_us, Option), ("bank_serialize_us", bank_serialize.as_us(), i64), ("status_cache_serialize_us", status_cache_serialize_us, i64), ("write_version_file_us", write_version_file_us, i64), @@ -775,7 +736,7 @@ fn deserialize_obsolete_accounts( .join(snapshot_paths::SNAPSHOT_OBSOLETE_ACCOUNTS_FILENAME); let obsolete_accounts_reader = ReadAdapter::new(large_file_buf_reader( &obsolete_accounts_path, - OBSOLETE_ACCOUNTS_READ_BUF_SIZE, + AUX_SNAPSHOT_FILE_READ_BUF_SIZE, &IoSetupState::default(), )?); // If the file is too large return error @@ -795,6 +756,62 @@ fn deserialize_obsolete_accounts( )?) } +pub fn write_storages_list_to_snapshot( + bank_snapshot_dir: impl AsRef, + snapshot_storages: &[Arc], + io_setup: &IoSetupState, +) -> Result { + let storages_list = StoragesList::new_from_storages(snapshot_storages); + serialize_storages_list_to_snapshot(bank_snapshot_dir, storages_list, io_setup) +} + +fn serialize_storages_list_to_snapshot( + bank_snapshot_dir: impl AsRef, + storages_list: StoragesList, + io_setup: &IoSetupState, +) -> Result { + let storages_list_path = bank_snapshot_dir + .as_ref() + .join(snapshot_paths::SNAPSHOT_STORAGES_LIST_FILENAME); + let mut file_stream = SizeLimitedWriter::new( + large_file_buf_writer(&storages_list_path, io_setup)?, + MAX_STORAGES_LIST_FILE_SIZE, + ); + serde_snapshot::serialize_into(&mut file_stream, &storages_list).map_err(|err| { + IoError::other(format!( + "unable to serialize storages list to file '{}': {err}", + storages_list_path.display(), + )) + })?; + Ok(file_stream.bytes_written()) +} + +fn deserialize_storages_list( + storages_list_path: &Path, + maximum_storages_list_file_size: u64, +) -> Result { + let storages_list_reader = ReadAdapter::new(large_file_buf_reader( + storages_list_path, + AUX_SNAPSHOT_FILE_READ_BUF_SIZE, + &IoSetupState::default(), + )?); + // If the file is too large return error + let storages_list_file_metadata = fs::metadata(storages_list_path)?; + if storages_list_file_metadata.len() > maximum_storages_list_file_size { + let error_message = format!( + "too large storages list file to deserialize: '{}' has {} bytes (max size is \ + {maximum_storages_list_file_size} bytes)", + storages_list_path.display(), + storages_list_file_metadata.len(), + ); + return Err(IoError::other(error_message).into()); + } + + Ok(serde_snapshot::deserialize_wincode_from( + storages_list_reader, + )?) +} + pub fn serialize_snapshot_data_file( data_file_path: &Path, io_setup: &IoSetupState, @@ -961,111 +978,6 @@ fn check_deserialize_file_consumed( Ok(()) } -/// Return account path from the appendvec path after checking its format. -fn get_account_path_from_appendvec_path(appendvec_path: &Path) -> Option { - let run_path = appendvec_path.parent()?; - let run_file_name = run_path.file_name()?; - // All appendvec files should be under /run/. - // When generating the bank snapshot directory, they are hardlinked to /snapshot// - if run_file_name != ACCOUNTS_RUN_DIR { - error!( - "The account path {} does not have run/ as its immediate parent directory.", - run_path.display() - ); - return None; - } - let account_path = run_path.parent()?; - Some(account_path.to_path_buf()) -} - -/// From an appendvec path, derive the snapshot hardlink path. If the corresponding snapshot hardlink -/// directory does not exist, create it. -fn get_snapshot_accounts_hardlink_dir( - appendvec_path: &Path, - bank_slot: Slot, - account_paths: &mut HashSet, - hardlinks_dir: impl AsRef, -) -> std::result::Result { - let account_path = get_account_path_from_appendvec_path(appendvec_path).ok_or_else(|| { - GetSnapshotAccountsHardLinkDirError::GetAccountPath(appendvec_path.to_path_buf()) - })?; - - let snapshot_hardlink_dir = account_path - .join(ACCOUNTS_SNAPSHOT_DIR) - .join(bank_slot.to_string()); - - // Use the hashset to track, to avoid checking the file system. Only set up the hardlink directory - // and the symlink to it at the first time of seeing the account_path. - if !account_paths.contains(&account_path) { - let idx = account_paths.len(); - debug!( - "for appendvec_path {}, create hard-link path {}", - appendvec_path.display(), - snapshot_hardlink_dir.display() - ); - fs::create_dir_all(&snapshot_hardlink_dir).map_err(|err| { - GetSnapshotAccountsHardLinkDirError::CreateSnapshotHardLinkDir( - err, - snapshot_hardlink_dir.clone(), - ) - })?; - let symlink_path = hardlinks_dir.as_ref().join(format!("account_path_{idx}")); - symlink::symlink_dir(&snapshot_hardlink_dir, &symlink_path).map_err(|err| { - GetSnapshotAccountsHardLinkDirError::SymlinkSnapshotHardLinkDir { - source: err, - original: snapshot_hardlink_dir.clone(), - link: symlink_path, - } - })?; - account_paths.insert(account_path); - }; - - Ok(snapshot_hardlink_dir) -} - -/// Hard-link the files from accounts/ to snapshot//accounts/ -/// This keeps the appendvec files alive and with the bank snapshot. The slot and id -/// in the file names are also updated in case its file is a recycled one with inconsistent slot -/// and id. -pub fn hard_link_storages_to_snapshot( - bank_snapshot_dir: impl AsRef, - bank_slot: Slot, - snapshot_storages: &[Arc], -) -> std::result::Result<(), HardLinkStoragesToSnapshotError> { - let accounts_hardlinks_dir = bank_snapshot_dir - .as_ref() - .join(snapshot_paths::SNAPSHOT_ACCOUNTS_HARDLINKS); - fs::create_dir_all(&accounts_hardlinks_dir).map_err(|err| { - HardLinkStoragesToSnapshotError::CreateAccountsHardLinksDir( - err, - accounts_hardlinks_dir.clone(), - ) - })?; - - let mut account_paths: HashSet = HashSet::new(); - for storage in snapshot_storages { - let storage_path = storage.accounts.path(); - let snapshot_hardlink_dir = get_snapshot_accounts_hardlink_dir( - storage_path, - bank_slot, - &mut account_paths, - &accounts_hardlinks_dir, - )?; - // The appendvec could be recycled, so its filename may not be consistent to the slot and id. - // Use the storage slot and id to compose a consistent file name for the hard-link file. - let hardlink_filename = AccountsFile::file_name(storage.slot(), storage.id()); - let hard_link_path = snapshot_hardlink_dir.join(hardlink_filename); - fs::hard_link(storage_path, &hard_link_path).map_err(|err| { - HardLinkStoragesToSnapshotError::HardLinkStorage( - err, - storage_path.to_path_buf(), - hard_link_path, - ) - })?; - } - Ok(()) -} - /// Unarchives the given full and incremental snapshot archives, as long as they are compatible. pub fn verify_and_unarchive_snapshots( bank_snapshots_dir: impl AsRef, @@ -1412,6 +1324,147 @@ fn spawn_streaming_snapshot_dir_files( (file_receiver, handle) } +/// Migrates a legacy (2.0.0) bank snapshot's hardlink-based storages into the new format. +/// +/// Walks `/accounts_hardlinks/`, follows each symlink to its +/// `/snapshot//` target, and renames each storage file there back into +/// `/run/`. Writes the derived storages list into the bank snapshot dir so the +/// load path can always read it from disk. Tears down the legacy directories (the +/// `accounts_hardlinks/` symlink dir and the whole `/snapshot/` tree) on success +/// so subsequent restarts go through the normal new-format path. +fn migrate_legacy_hardlinks(bank_snapshot_dir: &Path, account_run_paths: &[PathBuf]) -> Result<()> { + let accounts_hardlinks_dir = + bank_snapshot_dir.join(snapshot_paths::SNAPSHOT_ACCOUNTS_HARDLINKS); + let mut items: Vec = Vec::new(); + + for entry in fs::read_dir(&accounts_hardlinks_dir).map_err(|err| { + IoError::other(format!( + "failed to read legacy accounts hardlinks dir '{}': {err}", + accounts_hardlinks_dir.display(), + )) + })? { + let symlink_path = entry?.path(); + let snapshot_slot_dir = fs::read_link(&symlink_path).map_err(|err| { + IoError::other(format!( + "failed to read symlink '{}': {err}", + symlink_path.display(), + )) + })?; + // snapshot_slot_dir = `/snapshot//`. The account run dir is its + // grandparent + `run` (i.e. `/run`). + let run_dir = snapshot_slot_dir + .parent() + .and_then(Path::parent) + .ok_or_else(|| { + IoError::other(format!( + "invalid legacy hardlink target '{}'", + snapshot_slot_dir.display(), + )) + })? + .join(ACCOUNTS_RUN_DIR); + // The legacy snapshot was taken against the account paths in use at the time. If + // those have changed (e.g. the operator reconfigured `account_paths` while upgrading + // Agave), the run dir we just derived isn't one we're loading into — bail rather than + // silently writing files into a location nobody's reading from. + if !account_run_paths.contains(&run_dir) { + return Err(IoError::other(format!( + "legacy hardlink target '{}' points to run dir '{}' which is not in the current \ + account paths ({:?}); the account paths configuration has changed since this \ + snapshot was taken — load from a snapshot archive instead", + snapshot_slot_dir.display(), + run_dir.display(), + account_run_paths, + )) + .into()); + } + + for file_entry in fs::read_dir(&snapshot_slot_dir).map_err(|err| { + IoError::other(format!( + "failed to read legacy hardlink dir '{}': {err}", + snapshot_slot_dir.display(), + )) + })? { + let src = file_entry?.path(); + let Some(name) = src.file_name().and_then(|n| n.to_str()) else { + continue; + }; + let (slot, id) = get_slot_and_append_vec_id(name)?; + let dest = run_dir.join(name); + fs::rename(&src, &dest).map_err(|err| { + IoError::other(format!( + "failed to migrate legacy storage from '{}' to '{}': {err}", + src.display(), + dest.display(), + )) + })?; + items.push(StorageListItem { + slot, + id: id as AccountsFileId, + }); + } + } + + // Persist the derived list now: migration is destructive (it removes the legacy hardlinks + // below), and writing the storages list is what actually brings the bank snapshot to + // fastboot version >=3 compatibility. Doing it here means the snapshot stays loadable even + // if the validator never performs a proper teardown (e.g. crashes). + serialize_storages_list_to_snapshot( + bank_snapshot_dir, + StoragesList::from_items(items), + &IoSetupState::default(), + )?; + + // Tear down the legacy state so we don't repeat this migration: drop the bank snapshot's + // `accounts_hardlinks/` symlink dir and wipe each `/snapshot/` tree (catches + // both the per-slot dirs we just emptied and any orphans from older purged snapshots). + fs::remove_dir_all(&accounts_hardlinks_dir).map_err(|err| { + IoError::other(format!( + "failed to remove legacy accounts hardlinks dir '{}': {err}", + accounts_hardlinks_dir.display(), + )) + })?; + wipe_account_snapshot_dirs(account_run_paths); + + // Bump the fastboot version so subsequent loads take the normal 3.0+ path instead of + // re-running the migration (which would fail now that the hardlinks dir is gone). + mark_bank_snapshot_as_loadable(bank_snapshot_dir)?; + + Ok(()) +} + +/// Removes storage files from `account_paths` whose `(slot, id)` pair isn't listed in the +/// storages list (i.e. they don't belong to the snapshot being loaded). Files whose names +/// don't parse as `.` storage filenames are left alone. +fn prune_stale_storages(account_paths: &[PathBuf], storages_list: StoragesList) -> Result<()> { + let expected_storages = storages_list.into_slot_file_id_set(); + for account_path in account_paths { + let read_dir = fs::read_dir(account_path).map_err(|err| { + IoError::other(format!( + "failed to read account path '{}': {err}", + account_path.display(), + )) + })?; + for entry in read_dir { + let path = entry?.path(); + let Some(name) = path.file_name().and_then(|n| n.to_str()) else { + continue; + }; + let Ok((slot, id)) = get_slot_and_append_vec_id(name) else { + // Not a storage file name — leave it alone. + continue; + }; + if !expected_storages.contains(&(slot, id as AccountsFileId)) { + info!( + "Removing stale storage file '{}' not in storages list", + path.display(), + ); + fs::remove_file(&path)? + } + } + } + Ok(()) +} + /// Performs the common tasks when deserializing a snapshot /// /// Handles reading the snapshot file and version file, @@ -1426,8 +1479,6 @@ pub(crate) fn rebuild_storages_from_snapshot_dir( AccountsDbFields, )> { let bank_snapshot_dir = &snapshot_info.snapshot_dir; - let accounts_hardlinks = bank_snapshot_dir.join(snapshot_paths::SNAPSHOT_ACCOUNTS_HARDLINKS); - let account_run_paths: HashSet<_> = HashSet::from_iter(account_paths); // With fastboot_version >= 2, obsolete accounts are tracked and stored in the snapshot // Even if obsolete accounts are not enabled, the snapshot may still contain obsolete accounts @@ -1445,56 +1496,22 @@ pub(crate) fn rebuild_storages_from_snapshot_dir( )) })?; - let read_dir = fs::read_dir(&accounts_hardlinks).map_err(|err| { - IoError::other(format!( - "failed to read accounts hardlinks dir '{}': {err}", - accounts_hardlinks.display(), - )) - })?; - for dir_entry in read_dir { - let symlink_path = dir_entry?.path(); - // The symlink point to /snapshot/ which contain the account files hardlinks - // The corresponding run path should be /run/ - let account_snapshot_path = fs::read_link(&symlink_path).map_err(|err| { - IoError::other(format!( - "failed to read symlink '{}': {err}", - symlink_path.display(), - )) - })?; - let account_run_path = account_snapshot_path - .parent() - .ok_or_else(|| SnapshotError::InvalidAccountPath(account_snapshot_path.clone()))? - .parent() - .ok_or_else(|| SnapshotError::InvalidAccountPath(account_snapshot_path.clone()))? - .join(ACCOUNTS_RUN_DIR); - if !account_run_paths.contains(&account_run_path) { - // The appendvec from the bank snapshot storage does not match any of the provided account_paths set. - // The account paths have changed so the snapshot is no longer usable. - return Err(SnapshotError::AccountPathsMismatch); - } - // Generate hard-links to make the account files available in the main accounts/, and let the new appendvec - // paths be in accounts/ - let read_dir = fs::read_dir(&account_snapshot_path).map_err(|err| { - IoError::other(format!( - "failed to read account snapshot dir '{}': {err}", - account_snapshot_path.display(), - )) - })?; - for file in read_dir { - let file_path = file?.path(); - let file_name = file_path - .file_name() - .ok_or_else(|| SnapshotError::InvalidAppendVecPath(file_path.to_path_buf()))?; - let dest_path = account_run_path.join(file_name); - fs::hard_link(&file_path, &dest_path).map_err(|err| { - IoError::other(format!( - "failed to hard link from '{}' to '{}': {err}", - file_path.display(), - dest_path.display(), - )) - })?; - } + // The bank snapshot lists the storage files belonging to it. Anything else in the account + // paths is from a later (post-snapshot) slot and must be removed before we load — the + // lt hash check at startup verifies the surviving storages. + let storages_list_path = + bank_snapshot_dir.join(snapshot_paths::SNAPSHOT_STORAGES_LIST_FILENAME); + if !storages_list_path.exists() { + // Legacy (2.0.0) bank snapshot: storages live as hardlinks under + // `/snapshot//`, with symlinks in + // `/accounts_hardlinks/` tying them to the bank snapshot. Move the + // files back into `/run/` and write out the storages list so the load + // path below can read it like any other 3.0+ snapshot. + migrate_legacy_hardlinks(bank_snapshot_dir, account_paths)?; } + let storages_list = + deserialize_storages_list(&storages_list_path, MAX_STORAGES_LIST_FILE_SIZE)?; + prune_stale_storages(account_paths, storages_list)?; let snapshot_file_path = snapshot_info.snapshot_path(); let snapshot_version_path = bank_snapshot_dir.join(snapshot_paths::SNAPSHOT_VERSION_FILENAME); @@ -1717,6 +1734,23 @@ pub enum VerifyBank { NonDeterministic, } +/// For each account run dir, wipes the sibling `snapshot/` dir. +/// +/// Validator account paths are laid out as a parent containing two siblings: `run/` (the live +/// storage files) and `snapshot/` (legacy per-slot hardlink dirs, pre-3.0). `account_run_paths` +/// here holds the `run/` paths, so for each entry we walk up to the parent and wipe the +/// `snapshot/` sibling. Nothing new is written under `snapshot/` anymore, so any content is +/// either legacy hardlink dirs (written by pre-3.0 validators during fastboot) or orphans +/// from purged bank snapshots. Used by the legacy-hardlink migration path and the +/// archive-load path to drop that leftover state. +pub fn wipe_account_snapshot_dirs(account_run_paths: &[PathBuf]) { + for account_run_path in account_run_paths { + if let Some(parent) = account_run_path.parent() { + move_and_async_delete_path_contents(parent.join(ACCOUNTS_SNAPSHOT_DIR)); + } + } +} + /// Purges all bank snapshots pub fn purge_all_bank_snapshots(bank_snapshots_dir: impl AsRef) { let bank_snapshots = get_bank_snapshots(&bank_snapshots_dir); @@ -1773,12 +1807,14 @@ fn purge_bank_snapshots<'a>(bank_snapshots: impl IntoIterator) -> Result<()> { const FN_ERR: &str = "failed to purge bank snapshot"; + // Migration: snapshots written by pre-storages-list versions kept an `accounts_hardlinks/` + // subdir of symlinks pointing at hardlink dirs under `/snapshot//`. + // Follow them so the hardlink dirs don't outlive the owning bank snapshot when we purge at + // runtime (startup-time cleanup catches any leftovers). let accounts_hardlinks_dir = bank_snapshot_dir .as_ref() .join(snapshot_paths::SNAPSHOT_ACCOUNTS_HARDLINKS); if accounts_hardlinks_dir.is_dir() { - // This directory contain symlinks to all accounts snapshot directories. - // They should all be removed. let read_dir = fs::read_dir(&accounts_hardlinks_dir).map_err(|err| { IoError::other(format!( "{FN_ERR}: failed to read accounts hardlinks dir '{}': {err}", @@ -1848,7 +1884,7 @@ mod tests { }, assert_matches::assert_matches, bincode::{deserialize_from, serialize_into}, - solana_accounts_db::accounts_file::AccountsFileProvider, + solana_accounts_db::accounts_file::{AccountsFile, AccountsFileProvider}, solana_hash::Hash, std::{convert::TryFrom, mem::size_of}, tempfile::NamedTempFile, @@ -2621,49 +2657,6 @@ mod tests { assert!(remaining_incremental_snapshot_archives.is_empty()); } - #[test] - fn test_get_snapshot_accounts_hardlink_dir() { - let slot: Slot = 1; - - let mut account_paths_set: HashSet = HashSet::new(); - - let bank_snapshots_dir_tmp = tempfile::TempDir::new().unwrap(); - let bank_snapshot_dir = bank_snapshots_dir_tmp.path().join(slot.to_string()); - let accounts_hardlinks_dir = - bank_snapshot_dir.join(snapshot_paths::SNAPSHOT_ACCOUNTS_HARDLINKS); - fs::create_dir_all(&accounts_hardlinks_dir).unwrap(); - - let (_tmp_dir, accounts_dir) = create_tmp_accounts_dir_for_tests(); - let appendvec_filename = format!("{slot}.0"); - let appendvec_path = accounts_dir.join(appendvec_filename); - - let ret = get_snapshot_accounts_hardlink_dir( - &appendvec_path, - slot, - &mut account_paths_set, - &accounts_hardlinks_dir, - ); - assert!(ret.is_ok()); - - let wrong_appendvec_path = appendvec_path - .parent() - .unwrap() - .parent() - .unwrap() - .join(appendvec_path.file_name().unwrap()); - let ret = get_snapshot_accounts_hardlink_dir( - &wrong_appendvec_path, - slot, - &mut account_paths_set, - accounts_hardlinks_dir, - ); - - assert_matches!( - ret, - Err(GetSnapshotAccountsHardLinkDirError::GetAccountPath(_)) - ); - } - #[test] fn test_get_snapshot_file_kind() { assert_eq!(None, get_snapshot_file_kind("file.txt")); @@ -2858,4 +2851,34 @@ mod tests { // scenario 9: all good assert!(is_bank_snapshot_complete(bank_snapshot_dir)); } + + #[test] + fn test_prune_stale_storages() { + let account_path = tempfile::TempDir::new().unwrap(); + // Files that belong to the snapshot. + let keep_a = account_path.path().join(AccountsFile::file_name(100, 1)); + let keep_b = account_path.path().join(AccountsFile::file_name(200, 2)); + // A stale storage file that should be removed. + let stale = account_path.path().join(AccountsFile::file_name(300, 3)); + // A non-storage filename — should be left alone. + let untouched = account_path.path().join("something_else.txt"); + for path in [&keep_a, &keep_b, &stale, &untouched] { + fs::write(path, b"x").unwrap(); + } + + let storages_list = StoragesList::from_items(vec![ + StorageListItem { slot: 100, id: 1 }, + StorageListItem { slot: 200, id: 2 }, + ]); + prune_stale_storages( + std::slice::from_ref(&account_path.path().to_path_buf()), + storages_list, + ) + .unwrap(); + + assert!(keep_a.exists(), "expected storage file was deleted"); + assert!(keep_b.exists(), "expected storage file was deleted"); + assert!(!stale.exists(), "stale storage file was not removed"); + assert!(untouched.exists(), "non-storage file was wrongly removed"); + } } diff --git a/snapshots/src/error.rs b/snapshots/src/error.rs index 3ea464d31ff..de2bb906ab6 100644 --- a/snapshots/src/error.rs +++ b/snapshots/src/error.rs @@ -199,9 +199,6 @@ pub enum AddBankSnapshotError { #[error("failed to flush storage '{1}': {0}")] FlushStorage(#[source] AccountsFileError, PathBuf), - #[error("failed to hard link storages: {0}")] - HardLinkStorages(#[source] HardLinkStoragesToSnapshotError), - #[error("failed to serialize bank: {0}")] SerializeBank(#[source] Box), @@ -211,6 +208,9 @@ pub enum AddBankSnapshotError { #[error("failed to serialize obsolete accounts: {0}")] SerializeObsoleteAccounts(#[source] Box), + #[error("failed to write storages list: {0}")] + WriteStoragesList(#[source] Box), + #[error("failed to write snapshot version file '{1}': {0}")] WriteSnapshotVersionFile(#[source] io::Error, PathBuf), @@ -275,33 +275,3 @@ pub enum ArchiveSnapshotPackageError { #[error("failed to initialize file buffered reader: {0}")] StorageFileBufReaderError(#[source] io::Error), } - -/// Errors that can happen in `hard_link_storages_to_snapshot()` -#[derive(Error, Debug)] -pub enum HardLinkStoragesToSnapshotError { - #[error("failed to create accounts hard links dir '{1}': {0}")] - CreateAccountsHardLinksDir(#[source] io::Error, PathBuf), - - #[error("failed to get the snapshot's accounts hard link dir: {0}")] - GetSnapshotHardLinksDir(#[from] GetSnapshotAccountsHardLinkDirError), - - #[error("failed to hard link storage from '{1}' to '{2}': {0}")] - HardLinkStorage(#[source] io::Error, PathBuf, PathBuf), -} - -/// Errors that can happen in `get_snapshot_accounts_hardlink_dir()` -#[derive(Error, Debug)] -pub enum GetSnapshotAccountsHardLinkDirError { - #[error("invalid account storage path '{0}'")] - GetAccountPath(PathBuf), - - #[error("failed to create the snapshot hard link dir '{1}': {0}")] - CreateSnapshotHardLinkDir(#[source] io::Error, PathBuf), - - #[error("failed to symlink snapshot hard link dir '{link}' to '{original}': {source}")] - SymlinkSnapshotHardLinkDir { - source: io::Error, - original: PathBuf, - link: PathBuf, - }, -} diff --git a/snapshots/src/paths.rs b/snapshots/src/paths.rs index ad1c634e59f..3598f28d2e5 100644 --- a/snapshots/src/paths.rs +++ b/snapshots/src/paths.rs @@ -22,6 +22,7 @@ pub const SNAPSHOT_STATUS_CACHE_FILENAME: &str = "status_cache"; pub const SNAPSHOT_VERSION_FILENAME: &str = "version"; pub const SNAPSHOT_FASTBOOT_VERSION_FILENAME: &str = "fastboot_version"; pub const SNAPSHOT_ACCOUNTS_HARDLINKS: &str = "accounts_hardlinks"; +pub const SNAPSHOT_STORAGES_LIST_FILENAME: &str = "storages_list"; pub const SNAPSHOT_ARCHIVE_DOWNLOAD_DIR: &str = "remote"; pub const SNAPSHOT_OBSOLETE_ACCOUNTS_FILENAME: &str = "obsolete_accounts"; /// When a snapshot is taken of a bank, the state is serialized under this directory. From 3c01f9ce54499772fac6ce929c08476543b3d81c Mon Sep 17 00:00:00 2001 From: ripatel-fd Date: Fri, 29 May 2026 04:52:01 -0500 Subject: [PATCH 122/576] status cache: use fixed message hash offset (#12743) For new slot deltas, use the first 20 bytes of the hash instead (key_index=0), which is equivalent to BLAKE3-160(b"solana-tx-message-v1" + serialized_message). The status cache keys transactions by a random 20 byte subslice of the 32 byte message hash (key_index). This behavior has no cryptographic or performance benefit and only complicates code, and is thus removed. For old/existing slot deltas, reuse the existing key_index to keep compatibility with older snapshots. Once the entire network upgrades to Agave v4.2, all snapshots will use key_index=0, allowing us to clean up the random subslice code entirely. Co-authored-by: Richard Patel --- runtime/src/status_cache.rs | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/runtime/src/status_cache.rs b/runtime/src/status_cache.rs index a783fe5cd2b..3859d6e5781 100644 --- a/runtime/src/status_cache.rs +++ b/runtime/src/status_cache.rs @@ -1,5 +1,7 @@ #[cfg(feature = "shuttle-test")] use shuttle::sync::{Arc, Mutex}; +#[cfg(not(feature = "shuttle-test"))] +use std::sync::{Arc, Mutex}; use { ahash::{HashMap, HashMapExt as _}, log::*, @@ -12,11 +14,6 @@ use { num::{NonZero, NonZeroUsize}, }, }; -#[cfg(not(feature = "shuttle-test"))] -use { - rand::{Rng, rng}, - std::sync::{Arc, Mutex}, -}; // The maximum number of entries to store in the cache. This is the same as the number of recent // blockhashes because we automatically reject txs that use older blockhashes so we don't need to @@ -220,15 +217,10 @@ impl StatusCache { let max_key_index = key.as_ref().len().saturating_sub(CACHED_KEY_SIZE + 1); // Get the cache entry for this blockhash. - let (max_slot, key_index, hash_map) = - self.cache.entry(*transaction_blockhash).or_insert_with(|| { - // DFS tests need deterministic behavior - #[cfg(feature = "shuttle-test")] - let key_index = 0; - #[cfg(not(feature = "shuttle-test"))] - let key_index = rng().random_range(0..max_key_index + 1); - (slot, key_index, HashMap::new()) - }); + let (max_slot, key_index, hash_map) = self + .cache + .entry(*transaction_blockhash) + .or_insert_with(|| (slot, 0, HashMap::new())); // Update the max slot observed to contain txs using this blockhash. *max_slot = std::cmp::max(slot, *max_slot); From ea95ec412d76b8c10e7afa7a708feca310c63f44 Mon Sep 17 00:00:00 2001 From: Andrew Fitzgerald Date: Fri, 29 May 2026 08:01:27 -0500 Subject: [PATCH 123/576] banking-trace: use bounded channel (#12816) --- core/src/banking_trace.rs | 32 +++++++++++++------------------- streamer/src/evicting_sender.rs | 11 ++++++++++- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/core/src/banking_trace.rs b/core/src/banking_trace.rs index e01f10b0da8..f6c245d50ad 100644 --- a/core/src/banking_trace.rs +++ b/core/src/banking_trace.rs @@ -2,9 +2,7 @@ use { agave_banking_stage_ingress_types::{BankingPacketBatch, BankingPacketReceiver}, bincode::serialize_into, chrono::{DateTime, Local}, - crossbeam_channel::{ - Receiver, SendError, Sender, TryRecvError, TrySendError, bounded, unbounded, - }, + crossbeam_channel::{Receiver, SendError, TryRecvError, TrySendError, bounded}, rolling_file::{RollingCondition, RollingConditionBasic, RollingFileAppender}, serde::{Deserialize, Serialize}, solana_clock::Slot, @@ -61,13 +59,12 @@ pub const DISABLED_BAKING_TRACE_DIR: DirByteLimit = 0; pub const BANKING_TRACE_DIR_DEFAULT_BYTE_LIMIT: DirByteLimit = TRACE_FILE_DEFAULT_ROTATE_BYTE_THRESHOLD * TRACE_FILE_ROTATE_COUNT; -#[derive(Clone, Debug)] +#[derive(Clone)] struct ActiveTracer { - trace_sender: Sender, + trace_sender: EvictingSender, exit: Arc, } -#[derive(Debug)] pub struct BankingTracer { active_tracer: Option, } @@ -214,7 +211,9 @@ impl BankingTracer { )); } - let (trace_sender, trace_receiver) = unbounded(); + const TRACING_CHANNEL_CAPACITY: usize = 50_000; + let (trace_sender, trace_receiver) = + EvictingSender::new_bounded(TRACING_CHANNEL_CAPACITY); let file_appender = Self::create_file_appender(path, rotate_threshold_size)?; @@ -253,9 +252,8 @@ impl BankingTracer { fn trace_event(&self, on_trace: impl Fn() -> TimedTracedEvent) { if let Some(ActiveTracer { trace_sender, exit }) = &self.active_tracer { if !exit.load(Ordering::Relaxed) { - trace_sender - .send(on_trace()) - .expect("active tracer thread unless exited"); + // Ignore errors in sending to tracer - it is a non-critical component. + let _ = trace_sender.try_send(on_trace()); } } } @@ -401,15 +399,11 @@ impl TracedSender { pub fn send(&self, batch: BankingPacketBatch) -> Result> { if let Some(ActiveTracer { trace_sender, exit }) = &self.active_tracer { if !exit.load(Ordering::Relaxed) { - trace_sender - .send(TimedTracedEvent( - SystemTime::now(), - TracedEvent::PacketBatch(self.label, BankingPacketBatch::clone(&batch)), - )) - .map_err(|err| { - error!("unexpected error when tracing a banking event...: {err:?}"); - SendError(BankingPacketBatch::clone(&batch)) - })?; + // Ignore errors in sending to tracer - it is a non-critical component. + let _ = trace_sender.try_send(TimedTracedEvent( + SystemTime::now(), + TracedEvent::PacketBatch(self.label, BankingPacketBatch::clone(&batch)), + )); } } match self.sender.try_send(batch) { diff --git a/streamer/src/evicting_sender.rs b/streamer/src/evicting_sender.rs index 847952a684c..4a5ae056eb8 100644 --- a/streamer/src/evicting_sender.rs +++ b/streamer/src/evicting_sender.rs @@ -4,12 +4,21 @@ use { }; /// A sender implementation that evicts the oldest message when the channel is full. -#[derive(Clone)] pub struct EvictingSender { sender: Sender, receiver: Receiver, } +// Manual implementation of Clone since `T` is not required to implement Clone. +impl Clone for EvictingSender { + fn clone(&self) -> Self { + Self { + sender: self.sender.clone(), + receiver: self.receiver.clone(), + } + } +} + impl EvictingSender { /// Create a new evicting sender with provided sender, receiver. #[inline] From 6a8fc78368e24a46485dbd0d56c9a7add05f2bd3 Mon Sep 17 00:00:00 2001 From: Kamil Skalski Date: Fri, 29 May 2026 15:50:52 +0200 Subject: [PATCH 124/576] perf(snapshots): avoid cloning results in status_cache (de)ser (#12820) --- runtime/src/serde_snapshot/status_cache.rs | 39 +++++++++++++--------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/runtime/src/serde_snapshot/status_cache.rs b/runtime/src/serde_snapshot/status_cache.rs index 00642f6fe9f..dff59bc38cc 100644 --- a/runtime/src/serde_snapshot/status_cache.rs +++ b/runtime/src/serde_snapshot/status_cache.rs @@ -52,7 +52,10 @@ pub fn serialize_status_cache( .map(|(key_slice, result)| { ( *key_slice, - result.clone().map_err(SerdeTransactionError::from), + result + .as_ref() + .map(|_| ()) + .map_err(SerdeTransactionError::from), ) }) .collect::>(), @@ -83,21 +86,21 @@ pub fn deserialize_status_cache( .deserialize_from(stream)?; let slot_deltas = snapshot_slot_deltas - .iter() + .into_iter() .map(|slot_delta| { let status_map = slot_delta .2 - .iter() + .into_iter() .map(|(key, value)| { ( - *key, + key, ( value.0, value .1 - .iter() + .into_iter() .map(|(key_slice, result)| { - (*key_slice, result.clone().map_err(TransactionError::from)) + (key_slice, result.map_err(TransactionError::from)) }) .collect::>(), ), @@ -161,8 +164,8 @@ enum SerdeTransactionError { CommitCancelled, } -impl From for SerdeTransactionError { - fn from(err: TransactionError) -> Self { +impl From<&TransactionError> for SerdeTransactionError { + fn from(err: &TransactionError) -> Self { match err { TransactionError::AccountInUse => Self::AccountInUse, TransactionError::AccountLoadedTwice => Self::AccountLoadedTwice, @@ -172,7 +175,9 @@ impl From for SerdeTransactionError { TransactionError::InvalidAccountForFee => Self::InvalidAccountForFee, TransactionError::AlreadyProcessed => Self::AlreadyProcessed, TransactionError::BlockhashNotFound => Self::BlockhashNotFound, - TransactionError::InstructionError(i, inner) => Self::InstructionError(i, inner.into()), + TransactionError::InstructionError(i, inner) => { + Self::InstructionError(*i, inner.into()) + } TransactionError::CallChainTooDeep => Self::CallChainTooDeep, TransactionError::MissingSignatureForFee => Self::MissingSignatureForFee, TransactionError::InvalidAccountIndex => Self::InvalidAccountIndex, @@ -204,9 +209,11 @@ impl From for SerdeTransactionError { TransactionError::WouldExceedAccountDataTotalLimit => { Self::WouldExceedAccountDataTotalLimit } - TransactionError::DuplicateInstruction(i) => Self::DuplicateInstruction(i), + TransactionError::DuplicateInstruction(i) => Self::DuplicateInstruction(*i), TransactionError::InsufficientFundsForRent { account_index } => { - Self::InsufficientFundsForRent { account_index } + Self::InsufficientFundsForRent { + account_index: *account_index, + } } TransactionError::MaxLoadedAccountsDataSizeExceeded => { Self::MaxLoadedAccountsDataSizeExceeded @@ -216,7 +223,9 @@ impl From for SerdeTransactionError { } TransactionError::ResanitizationNeeded => Self::ResanitizationNeeded, TransactionError::ProgramExecutionTemporarilyRestricted { account_index } => { - Self::ProgramExecutionTemporarilyRestricted { account_index } + Self::ProgramExecutionTemporarilyRestricted { + account_index: *account_index, + } } TransactionError::UnbalancedTransaction => Self::UnbalancedTransaction, TransactionError::ProgramCacheHitMaxLimit => Self::ProgramCacheHitMaxLimit, @@ -429,8 +438,8 @@ impl From for InstructionError { } } -impl From for SerdeInstructionError { - fn from(err: InstructionError) -> Self { +impl From<&InstructionError> for SerdeInstructionError { + fn from(err: &InstructionError) -> Self { match err { InstructionError::GenericError => Self::GenericError, InstructionError::InvalidArgument => Self::InvalidArgument, @@ -458,7 +467,7 @@ impl From for SerdeInstructionError { InstructionError::AccountBorrowFailed => Self::AccountBorrowFailed, InstructionError::AccountBorrowOutstanding => Self::AccountBorrowOutstanding, InstructionError::DuplicateAccountOutOfSync => Self::DuplicateAccountOutOfSync, - InstructionError::Custom(n) => Self::Custom(n), + InstructionError::Custom(n) => Self::Custom(*n), InstructionError::InvalidError => Self::InvalidError, InstructionError::ExecutableDataModified => Self::ExecutableDataModified, InstructionError::ExecutableLamportChange => Self::ExecutableLamportChange, From dd9012b545c7ecd555a07702eb02560e9fbf0445 Mon Sep 17 00:00:00 2001 From: Mykola Dzham Date: Fri, 29 May 2026 15:55:04 +0200 Subject: [PATCH 125/576] ci: Specify user-agent for crates.io curl (#12825) Set the user agent the same as in #12775 --- ci/publish-crate.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ci/publish-crate.sh b/ci/publish-crate.sh index 908e69275bc..651405ddf96 100755 --- a/ci/publish-crate.sh +++ b/ci/publish-crate.sh @@ -8,8 +8,10 @@ source ci/rust-version.sh stable is_crate_version_uploaded() { name=$1 version=$2 - curl https://crates.io/api/v1/crates/${name}/${version} | \ - python3 -c "import sys,json; print('version' in json.load(sys.stdin));" + # crates.io API docs state to include a user-agent header: + # https://crates.io/data-access#api + curl --user-agent 'Anza (https://github.com/anza-xyz/agave)' https://crates.io/api/v1/crates/${name}/${version} | + python3 -c "import sys,json; print('version' in json.load(sys.stdin));" } # Only package/publish if this is a tagged release From ecb270e22e8cfd9cbbe0c88f6c8b94da71f30d6a Mon Sep 17 00:00:00 2001 From: Callum McIntyre Date: Fri, 29 May 2026 15:11:57 +0100 Subject: [PATCH 126/576] fix: v1 transactions over 1232 bytes (#12597) * fix: v1 transactions over 1232 bytes * Refactor so we check for v1 prefix before decoding base64 * fail on oversize legacy base64 txs before decoding --- core/src/validator.rs | 2 + rpc/src/rpc.rs | 135 ++++++++++++++++++++++++++++++++++++------ 2 files changed, 119 insertions(+), 18 deletions(-) diff --git a/core/src/validator.rs b/core/src/validator.rs index e1f13c48420..b9d56918f77 100644 --- a/core/src/validator.rs +++ b/core/src/validator.rs @@ -596,6 +596,8 @@ impl ValidatorTpuConfig { let tpu_quic_server_config = SwQosQuicStreamerConfig { quic_streamer_config: QuicStreamerConfig { max_connections_per_ipaddr_per_min: 32, + stream_receive_window_size: solana_message::v1::MAX_TRANSACTION_SIZE as u32, + max_stream_data_bytes: solana_message::v1::MAX_TRANSACTION_SIZE as u32, ..Default::default() }, qos_config: SwQosConfig::default(), diff --git a/rpc/src/rpc.rs b/rpc/src/rpc.rs index 52dc7167838..61f7b33116c 100644 --- a/rpc/src/rpc.rs +++ b/rpc/src/rpc.rs @@ -4368,8 +4368,19 @@ fn rpc_perf_sample_from_perf_sample(slot: u64, sample: PerfSample) -> RpcPerfSam } } +// Base58 encoding is deprecated, not increased for v1 transactions const MAX_BASE58_SIZE: usize = 1683; // Golden, bump if PACKET_DATA_SIZE changes -const MAX_BASE64_SIZE: usize = 1644; // Golden, bump if PACKET_DATA_SIZE changes +// Cap base64 based on the largest supported transaction version, +// which is v1 (4096 bytes). +const MAX_BASE64_SIZE: usize = 5464; // ceil(4096 / 3) * 4 +// Legacy/v0 transactions are still capped at PACKET_DATA_SIZE (1232 bytes). +const MAX_BASE64_LEGACY_SIZE: usize = 1644; // ceil(1232 / 3) * 4 + +// V1+ messages start with 0x81 (or higher). In base64 the top 6 bits of 0x81 +// Comparing the first two chars against "gQ" detects V1+ before decoding. +// Revisit before V3 which will break this shortcut +const V1_BASE64_PREFIX_LOWER_BOUND: &[u8] = b"gQ"; + fn decode_and_deserialize( encoded: String, encoding: TransactionBinaryEncoding, @@ -4377,7 +4388,7 @@ fn decode_and_deserialize( where T: for<'a> wincode::SchemaRead<'a, wincode::config::DefaultConfig, Dst = T>, { - let wire_output = match encoding { + let (wire_output, max_raw_size) = match encoding { TransactionBinaryEncoding::Base58 => { inc_new_counter_info!("rpc-base58_encoded_tx", 1); if encoded.len() > MAX_BASE58_SIZE { @@ -4389,32 +4400,44 @@ where PACKET_DATA_SIZE, ))); } - bs58::decode(encoded) + let bytes = bs58::decode(encoded) .into_vec() - .map_err(|e| Error::invalid_params(format!("invalid base58 encoding: {e:?}")))? + .map_err(|e| Error::invalid_params(format!("invalid base58 encoding: {e:?}")))?; + // base58 is deprecated, unchanged max size + (bytes, PACKET_DATA_SIZE) } TransactionBinaryEncoding::Base64 => { inc_new_counter_info!("rpc-base64_encoded_tx", 1); - if encoded.len() > MAX_BASE64_SIZE { + let (max_encoded_size, max_raw_size) = if encoded + .as_bytes() + .get(..2) + .is_some_and(|p| p >= V1_BASE64_PREFIX_LOWER_BOUND) + { + (MAX_BASE64_SIZE, solana_message::v1::MAX_TRANSACTION_SIZE) + } else { + (MAX_BASE64_LEGACY_SIZE, PACKET_DATA_SIZE) + }; + if encoded.len() > max_encoded_size { return Err(Error::invalid_params(format!( "base64 encoded {} too large: {} bytes (max: encoded/raw {}/{})", type_name::(), encoded.len(), - MAX_BASE64_SIZE, - PACKET_DATA_SIZE, + max_encoded_size, + max_raw_size, ))); } - BASE64_STANDARD + let bytes = BASE64_STANDARD .decode(encoded) - .map_err(|e| Error::invalid_params(format!("invalid base64 encoding: {e:?}")))? + .map_err(|e| Error::invalid_params(format!("invalid base64 encoding: {e:?}")))?; + (bytes, max_raw_size) } }; - if wire_output.len() > PACKET_DATA_SIZE { + if wire_output.len() > max_raw_size { return Err(Error::invalid_params(format!( "decoded {} too large: {} bytes (max: {} bytes)", type_name::(), wire_output.len(), - PACKET_DATA_SIZE + max_raw_size ))); } @@ -9113,17 +9136,21 @@ pub mod tests { #[test] fn test_worst_case_encoded_tx_goldens() { - let ff_tx = vec![0xffu8; PACKET_DATA_SIZE]; - let tx58 = bs58::encode(&ff_tx).into_string(); + // Base58 is capped at the legacy PACKET_DATA_SIZE since base58 encoding + // is deprecated and not extended to v1-sized transactions. + let ff_tx58_input = vec![0xffu8; PACKET_DATA_SIZE]; + let tx58 = bs58::encode(&ff_tx58_input).into_string(); assert_eq!(tx58.len(), MAX_BASE58_SIZE); - let tx64 = BASE64_STANDARD.encode(&ff_tx); + + // Base64 is sized for the largest supported transaction version (v1). + let ff_tx64_input = vec![0xffu8; solana_message::v1::MAX_TRANSACTION_SIZE]; + let tx64 = BASE64_STANDARD.encode(&ff_tx64_input); assert_eq!(tx64.len(), MAX_BASE64_SIZE); } #[test] - fn test_decode_and_deserialize_too_large_payloads_fail() { - // +2 because +1 still fits in base64 encoded worst-case - let too_big = PACKET_DATA_SIZE + 2; + fn test_decode_and_deserialize_too_large_encoded_base58_payload_fails() { + let too_big = PACKET_DATA_SIZE + 1; let tx_ser = vec![0xffu8; too_big]; let tx58 = bs58::encode(&tx_ser).into_string(); @@ -9136,18 +9163,50 @@ pub mod tests { encoded/raw {MAX_BASE58_SIZE}/{PACKET_DATA_SIZE})", )) ); + } + + #[test] + fn test_decode_and_deserialize_too_large_encoded_base64_v1_payload_fails() { + // +3 because +2 still fits in base64 encoded worst-case + let too_big = solana_message::v1::MAX_TRANSACTION_SIZE + 3; + let mut tx_ser = vec![0xffu8; too_big]; + tx_ser[0] = solana_message::v1::V1_PREFIX; + + let tx64 = BASE64_STANDARD.encode(&tx_ser); + let tx64_len = tx64.len(); + assert_eq!( + decode_and_deserialize::(tx64, TransactionBinaryEncoding::Base64) + .unwrap_err(), + Error::invalid_params(format!( + "base64 encoded solana_transaction::Transaction too large: {tx64_len} bytes (max: \ + encoded/raw {MAX_BASE64_SIZE}/{})", + solana_message::v1::MAX_TRANSACTION_SIZE, + )) + ); + } + + #[test] + fn test_decode_and_deserialize_too_large_encoded_base64_legacy_payload_fails() { + // Legacy/v0 payloads must bail out before decoding when they exceed the + // legacy base64 cap, even if they would fit under the v1 cap. + let too_big = PACKET_DATA_SIZE + 3; + let tx_ser = vec![0x00u8; too_big]; let tx64 = BASE64_STANDARD.encode(&tx_ser); let tx64_len = tx64.len(); + assert!(tx64_len > MAX_BASE64_LEGACY_SIZE && tx64_len < MAX_BASE64_SIZE); assert_eq!( decode_and_deserialize::(tx64, TransactionBinaryEncoding::Base64) .unwrap_err(), Error::invalid_params(format!( "base64 encoded solana_transaction::Transaction too large: {tx64_len} bytes (max: \ - encoded/raw {MAX_BASE64_SIZE}/{PACKET_DATA_SIZE})", + encoded/raw {MAX_BASE64_LEGACY_SIZE}/{PACKET_DATA_SIZE})", )) ); + } + #[test] + fn test_decode_and_deserialize_too_large_payloads_fail() { let too_big = PACKET_DATA_SIZE + 1; let tx_ser = vec![0x00u8; too_big]; let tx58 = bs58::encode(&tx_ser).into_string(); @@ -9209,6 +9268,46 @@ pub mod tests { ); } + #[test] + fn test_decode_and_deserialize_v1_sized_payload_passes_size_checks() { + // V1-prefixed payload at the V1 max size. Random bytes will fail + // wincode deserialization, but should pass the size gates. + let mut tx_ser = vec![0xffu8; solana_message::v1::MAX_TRANSACTION_SIZE]; + tx_ser[0] = solana_message::v1::V1_PREFIX; + + let tx64 = BASE64_STANDARD.encode(&tx_ser); + let err = + decode_and_deserialize::(tx64, TransactionBinaryEncoding::Base64) + .unwrap_err(); + assert_eq!(err.code, ErrorCode::InvalidParams); + assert!( + err.message.starts_with( + "failed to deserialize solana_transaction::versioned::VersionedTransaction" + ), + "unexpected error: {}", + err.message + ); + } + + #[test] + fn test_decode_and_deserialize_v1_oversize_payload_fails() { + // V1-prefixed payload one byte over the V1 max. + let too_big = solana_message::v1::MAX_TRANSACTION_SIZE + 1; + let mut tx_ser = vec![0xffu8; too_big]; + tx_ser[0] = solana_message::v1::V1_PREFIX; + + let tx64 = BASE64_STANDARD.encode(&tx_ser); + assert_eq!( + decode_and_deserialize::(tx64, TransactionBinaryEncoding::Base64) + .unwrap_err(), + Error::invalid_params(format!( + "decoded solana_transaction::versioned::VersionedTransaction too large: {too_big} \ + bytes (max: {} bytes)", + solana_message::v1::MAX_TRANSACTION_SIZE, + )) + ); + } + #[test] fn test_sanitize_unsanitary() { let unsanitary_tx58 = "ju9xZWuDBX4pRxX2oZkTjxU5jB4SSTgEGhX8bQ8PURNzyzqKMPPpNvWihx8zUe\ From d30026ad9bb08920f71529d80eba2cc60f2b5674 Mon Sep 17 00:00:00 2001 From: Brooks Date: Fri, 29 May 2026 10:27:28 -0400 Subject: [PATCH 127/576] Refactors Bank::store_accounts() (#12834) --- runtime/src/bank.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index a19a61a7c4a..29765942231 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -4571,7 +4571,6 @@ impl Bank { assert!(!self.freeze_started()); let mut m = Measure::start("stakes_cache.check_and_store"); let new_warmup_cooldown_rate_epoch = self.new_warmup_cooldown_rate_epoch(); - (0..accounts.len()).for_each(|i| { accounts.account(i, |account| { self.stakes_cache.check_and_store( @@ -4581,10 +4580,7 @@ impl Bank { ) }) }); - self.update_bank_hash_stats(&accounts); - self.rc - .accounts - .store_accounts_par(accounts, None, &self.ancestors); + self.store_accounts_without_stakes_cache(accounts); m.stop(); self.rc .accounts From 2ec7234327ab69cea06f7090a2f801af94ce0093 Mon Sep 17 00:00:00 2001 From: Rocker Zhang Date: Fri, 29 May 2026 22:46:27 +0800 Subject: [PATCH 128/576] core/bls-sigverify: retain root_slot certs in verified cache (follow-up to #12807) (#12819) #12807 fixed the incoming cert filter so that certificates at slot == root_slot are no longer dropped on ingest, but the cache prune in maybe_prune_caches still used > root_slot and evicted the just-verified root_slot cert from verified_certs. If the same cert arrived again (e.g. from a slow peer) the dedup check would miss and the cert would be re-verified. Align the prune to >= root_slot, matching the sibling generated_cert_types::prune (votor/src/generated_cert_types.rs) and the post-#12807 ingest filter. --- core/src/bls_sigverify/bls_sigverifier.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/bls_sigverify/bls_sigverifier.rs b/core/src/bls_sigverify/bls_sigverifier.rs index 4c605b4f362..a795f5277e9 100644 --- a/core/src/bls_sigverify/bls_sigverifier.rs +++ b/core/src/bls_sigverify/bls_sigverifier.rs @@ -199,7 +199,7 @@ impl SigVerifier { fn maybe_prune_caches(&mut self, root_slot: Slot) { if self.last_checked_root_slot < root_slot { self.last_checked_root_slot = root_slot; - self.verified_certs.retain(|cert| cert.slot() > root_slot); + self.verified_certs.retain(|cert| cert.slot() >= root_slot); } } From 9935173129023063f92f7b93b18e3a3678a18272 Mon Sep 17 00:00:00 2001 From: Gabriele Picco Date: Fri, 29 May 2026 16:48:10 +0200 Subject: [PATCH 129/576] rpc-client: Add method to get latest blockhash with response context (notably `context.slot`) (#12826) Add RpcClient method to get latest blockhash with response context `get_latest_blockhash_with_commitment` discards the `context.slot` from the `getLatestBlockhash` RPC response, so callers that need the blockhash and the slot it was observed at had to fall back to a raw `send`. Add `get_latest_blockhash_with_commitment_and_context`, which returns the full `RpcResult` envelope (context plus the parsed blockhash and last valid block height), and route the existing method through it to avoid duplicating the parse logic. --- CHANGELOG.md | 2 ++ rpc-client/src/nonblocking/rpc_client.rs | 38 ++++++++++++++++++++---- rpc-client/src/rpc_client.rs | 36 ++++++++++++++++++++++ 3 files changed, 70 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index edcf8a1498e..666b200a255 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ Release channels have their own copy of this changelog: ### RPC #### Breaking #### Changes +* Added `RpcClient::get_latest_blockhash_with_commitment_and_context`, which returns the + `getLatestBlockhash` response together with its context (notably `context.slot`). ### Validator #### Breaking #### Deprecations diff --git a/rpc-client/src/nonblocking/rpc_client.rs b/rpc-client/src/nonblocking/rpc_client.rs index 19077bffba7..1449265d328 100644 --- a/rpc-client/src/nonblocking/rpc_client.rs +++ b/rpc-client/src/nonblocking/rpc_client.rs @@ -4942,20 +4942,46 @@ impl RpcClient { &self, commitment: CommitmentConfig, ) -> ClientResult<(Hash, u64)> { - let RpcBlockhash { - blockhash, - last_valid_block_height, + Ok(self + .get_latest_blockhash_with_commitment_and_context(commitment) + .await? + .value) + } + + /// Returns the most recent blockhash and last valid block height along with the response + /// context, which includes the slot at which the node observed the blockhash. + /// + /// # RPC Reference + /// + /// This method corresponds directly to the [`getLatestBlockhash`] RPC method and uses the + /// provided [commitment level][cl]. + /// + /// [cl]: https://solana.com/docs/rpc#configuring-state-commitment + /// [`getLatestBlockhash`]: https://solana.com/docs/rpc/http/getlatestblockhash + pub async fn get_latest_blockhash_with_commitment_and_context( + &self, + commitment: CommitmentConfig, + ) -> RpcResult<(Hash, u64)> { + let Response { + context, + value: + RpcBlockhash { + blockhash, + last_valid_block_height, + }, } = self .send::>(RpcRequest::GetLatestBlockhash, json!([commitment])) - .await? - .value; + .await?; let blockhash = blockhash.parse().map_err(|_| { ClientError::new_with_request( RpcError::ParseError("Hash".to_string()).into(), RpcRequest::GetLatestBlockhash, ) })?; - Ok((blockhash, last_valid_block_height)) + Ok(Response { + context, + value: (blockhash, last_valid_block_height), + }) } /// Checks whether a blockhash is still valid for submitting transactions. diff --git a/rpc-client/src/rpc_client.rs b/rpc-client/src/rpc_client.rs index 422c8c98ee9..a34cb86733b 100644 --- a/rpc-client/src/rpc_client.rs +++ b/rpc-client/src/rpc_client.rs @@ -4211,6 +4211,25 @@ impl RpcClient { self.invoke((self.rpc_client.as_ref()).get_latest_blockhash_with_commitment(commitment)) } + /// Returns the most recent blockhash and last valid block height along with the response + /// context, which includes the slot at which the node observed the blockhash. + /// + /// # RPC Reference + /// + /// This method corresponds directly to the [`getLatestBlockhash`] RPC method and uses the + /// provided [commitment level][cl]. + /// + /// [cl]: https://solana.com/docs/rpc#configuring-state-commitment + /// [`getLatestBlockhash`]: https://solana.com/docs/rpc/http/getlatestblockhash + pub fn get_latest_blockhash_with_commitment_and_context( + &self, + commitment: CommitmentConfig, + ) -> RpcResult<(Hash, u64)> { + self.invoke( + (self.rpc_client.as_ref()).get_latest_blockhash_with_commitment_and_context(commitment), + ) + } + /// Checks whether a blockhash is still valid for submitting transactions. /// /// # RPC Reference @@ -5124,4 +5143,21 @@ mod tests { let fee: u64 = rpc_client.get_fee_for_message(&message).unwrap(); assert_eq!(fee, 42); } + + #[test] + fn test_get_latest_blockhash_with_commitment_and_context() { + let rpc_client = RpcClient::new_mock("succeeds".to_string()); + let expected_blockhash: Hash = PUBKEY.parse().unwrap(); + + let response = rpc_client + .get_latest_blockhash_with_commitment_and_context(CommitmentConfig::default()) + .unwrap(); + assert_eq!(response.context.slot, 1); + assert_eq!(response.value, (expected_blockhash, 1234)); + + let value = rpc_client + .get_latest_blockhash_with_commitment(CommitmentConfig::default()) + .unwrap(); + assert_eq!(value, response.value); + } } From ac2392c100cfb28c051124b4194fb8fc343e4bc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Mei=C3=9Fner?= Date: Fri, 29 May 2026 11:10:02 -0400 Subject: [PATCH 130/576] Refactor - Adds `account_owner` check to `ProgramCacheEntry::eq()` (#12804) Adds account_owner check to ProgramCacheEntry::eq(). --- program-runtime/src/program_cache_entry.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/program-runtime/src/program_cache_entry.rs b/program-runtime/src/program_cache_entry.rs index 80f9cd8a746..815b093be4b 100644 --- a/program-runtime/src/program_cache_entry.rs +++ b/program-runtime/src/program_cache_entry.rs @@ -173,6 +173,7 @@ impl PartialEq for ProgramCacheEntry { fn eq(&self, other: &Self) -> bool { self.effective_slot == other.effective_slot && self.deployment_slot == other.deployment_slot + && self.account_owner == other.account_owner && self.is_tombstone() == other.is_tombstone() } } From 7238f33d020d05f6be51f97dde2ee2c9bf02292a Mon Sep 17 00:00:00 2001 From: Akhilesh Singhania Date: Fri, 29 May 2026 17:28:36 +0200 Subject: [PATCH 131/576] cli: retrieves alpenglow genesis cert (#12735) --- Cargo.lock | 5 +++ cli-output/Cargo.toml | 2 + cli-output/src/cli_output.rs | 53 ++++++++++++++++++++++++ cli/Cargo.toml | 1 + cli/src/cli.rs | 5 +++ cli/src/cluster_query.rs | 46 ++++++++++++++++++++ dev-bins/Cargo.lock | 4 ++ programs/sbf/Cargo.lock | 4 ++ rpc-client-types/src/request.rs | 2 + rpc-client/Cargo.toml | 2 + rpc-client/src/mock_sender.rs | 11 +++++ rpc-client/src/nonblocking/rpc_client.rs | 25 +++++++++++ rpc-client/src/rpc_client.rs | 22 ++++++++++ rpc/src/rpc.rs | 14 +++++++ 14 files changed, 196 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 5504fe6603d..ffbb64ca990 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7445,6 +7445,7 @@ dependencies = [ "solana-sha256-hasher", "solana-signature", "solana-signer", + "solana-signer-store", "solana-slot-history", "solana-stake-interface", "solana-syscalls", @@ -7484,6 +7485,7 @@ dependencies = [ "Inflector", "agave-reserved-account-keys", "base64 0.22.1", + "bitvec", "chrono", "clap 2.33.3", "console 0.16.3", @@ -7496,6 +7498,7 @@ dependencies = [ "solana-account 4.3.0", "solana-account-decoder", "solana-bincode", + "solana-bls-signatures", "solana-clap-utils", "solana-cli-config", "solana-clock", @@ -9760,6 +9763,7 @@ dependencies = [ name = "solana-rpc-client" version = "4.2.0-alpha.0" dependencies = [ + "agave-votor-messages", "assert_matches", "async-trait", "base64 0.22.1", @@ -9779,6 +9783,7 @@ dependencies = [ "solana-account 4.3.0", "solana-account-decoder", "solana-account-decoder-client-types", + "solana-bls-signatures", "solana-clock", "solana-commitment-config", "solana-epoch-info", diff --git a/cli-output/Cargo.toml b/cli-output/Cargo.toml index ead6be574cc..eddbe7522cf 100644 --- a/cli-output/Cargo.toml +++ b/cli-output/Cargo.toml @@ -19,6 +19,7 @@ agave-unstable-api = [] Inflector = { workspace = true } agave-reserved-account-keys = { workspace = true } base64 = { workspace = true } +bitvec = { workspace = true } chrono = { workspace = true, features = ["default", "serde"] } clap = { version = "2.33.0", default-features = false, features = ["suggestions"] } console = { workspace = true } @@ -31,6 +32,7 @@ serde_json = { workspace = true } solana-account = { workspace = true } solana-account-decoder = { workspace = true } solana-bincode = { workspace = true } +solana-bls-signatures = { workspace = true } solana-clap-utils = { workspace = true } solana-cli-config = { workspace = true } solana-clock = { workspace = true } diff --git a/cli-output/src/cli_output.rs b/cli-output/src/cli_output.rs index 21a9f2d1da6..430eab193b1 100644 --- a/cli-output/src/cli_output.rs +++ b/cli-output/src/cli_output.rs @@ -11,6 +11,7 @@ use { }, }, base64::{Engine, prelude::BASE64_STANDARD}, + bitvec::vec::BitVec, chrono::{Local, TimeZone, Utc}, clap::ArgMatches, console::{Emoji, style}, @@ -22,6 +23,7 @@ use { UiAccountEncoding, UiDataSliceConfig, encode_ui_account, parse_account_data::AccountAdditionalDataV3, parse_token::UiTokenAccount, }, + solana_bls_signatures::Signature as BLSSignature, solana_clap_utils::keypair::SignOnly, solana_clock::{Epoch, Slot, UnixTimestamp}, solana_epoch_info::EpochInfo, @@ -1911,6 +1913,57 @@ impl From<&LandedVote> for CliLandedVote { } } +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CliAgGenesisInfoPayload { + /// Epoch in which the alpenglow feature was activated. + pub epoch: Epoch, + /// Slot in which migration to alpenglow happened. + pub slot: Slot, + /// Hash of the block in which migration to alpenglow happened. + pub block_id: Hash, + /// Bitvec of validators that signed the block. + pub bitvec: BitVec, + /// The aggregate signature of the validators. + pub signature: BLSSignature, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[allow(clippy::large_enum_variant)] +pub enum CliAgGenesisInfo { + Tower, + Ag(CliAgGenesisInfoPayload), +} + +impl QuietDisplay for CliAgGenesisInfo {} +impl VerboseDisplay for CliAgGenesisInfo {} + +impl fmt::Display for CliAgGenesisInfo { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Tower => writeln!(f, "still running tower"), + Self::Ag(payload) => { + let CliAgGenesisInfoPayload { + epoch, + slot, + block_id: block_hash, + bitvec, + signature, + } = payload; + writeln!(f, "Alpenglow genesis information:")?; + writeln!(f, " Feature flag activation: Epoch {epoch}")?; + writeln!(f, " Genesis Block - Slot {slot}, Block ID {block_hash}")?; + writeln!( + f, + " Genesis Vote - {} validators participated, Signature {signature}", + bitvec.count_ones(), + ) + } + } + } +} + #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CliBlockTime { diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 9c01f273b10..ff398bbb5d2 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -80,6 +80,7 @@ solana-sbpf = { workspace = true, features = ["jit"] } solana-sdk-ids = { workspace = true } solana-signature = { workspace = true } solana-signer = { workspace = true } +solana-signer-store = { workspace = true } solana-slot-history = { workspace = true } solana-stake-interface = { workspace = true } solana-syscalls = { workspace = true } diff --git a/cli/src/cli.rs b/cli/src/cli.rs index a7e0fa3fe3a..fbcf97d2e4e 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -449,6 +449,7 @@ pub enum CliCommand { signature: Signature, message: OffchainMessage, }, + GetAgGenesisInfo, } #[derive(Debug, PartialEq)] @@ -490,6 +491,8 @@ pub enum CliError { KeypairFileNotFound(String), #[error("Invalid signature")] InvalidSignature, + #[error("Invalid alpenglow genesis cert")] + InvalidAgGenesisCert, } impl From> for CliError { @@ -617,6 +620,7 @@ pub fn parse_command( } ("epoch", Some(matches)) => parse_get_epoch(matches), ("epoch-info", Some(matches)) => parse_get_epoch_info(matches), + ("alpenglow-genesis-info", Some(matches)) => parse_get_ag_genesis_info(matches), ("feature", Some(matches)) => { parse_feature_subcommand(matches, default_signer, wallet_manager) } @@ -1726,6 +1730,7 @@ pub async fn process_command(config: &CliConfig<'_>) -> ProcessResult { signature, message, } => process_verify_offchain_signature(config, signer_pubkey, signature, message), + CliCommand::GetAgGenesisInfo => process_get_ag_genesis_info(&rpc_client, config).await, } } diff --git a/cli/src/cluster_query.rs b/cli/src/cluster_query.rs index 8076c5c6ab4..72d22aacbd4 100644 --- a/cli/src/cluster_query.rs +++ b/cli/src/cluster_query.rs @@ -3,6 +3,7 @@ use { cli::{CliCommand, CliCommandInfo, CliConfig, CliError, ProcessResult}, feature::get_feature_activation_epoch, }, + agave_votor_messages::consensus_message::Certificate, clap::{App, AppSettings, Arg, ArgMatches, SubCommand, value_t, value_t_or_exit}, console::style, serde::{Deserialize, Serialize}, @@ -40,6 +41,7 @@ use { }, solana_sdk_ids::sysvar::{self, stake_history}, solana_signature::Signature, + solana_signer_store::{Decoded, decode}, solana_slot_history::{self as slot_history, SlotHistory}, solana_stake_interface::{self as stake, stake_history::StakeHistory, state::StakeStateV2}, solana_system_interface::MAX_PERMITTED_DATA_LENGTH, @@ -189,6 +191,11 @@ impl ClusterQuerySubCommands for App<'_, '_> { .about("Get information about the current epoch") .alias("get-epoch-info"), ) + .subcommand( + SubCommand::with_name("alpenglow-genesis-info") + .about("Get info about the Alpenglow genesis cert") + .alias("get-alpenglow-genesis-info"), + ) .subcommand( SubCommand::with_name("genesis-hash") .about("Get the genesis hash") @@ -505,6 +512,12 @@ pub fn parse_get_epoch(_matches: &ArgMatches<'_>) -> Result) -> Result { + Ok(CliCommandInfo::without_signers( + CliCommand::GetAgGenesisInfo, + )) +} + pub fn parse_get_epoch_info(_matches: &ArgMatches<'_>) -> Result { Ok(CliCommandInfo::without_signers(CliCommand::GetEpochInfo)) } @@ -1072,6 +1085,39 @@ pub async fn process_get_epoch(rpc_client: &RpcClient, _config: &CliConfig<'_>) Ok(epoch_info.epoch.to_string()) } +pub async fn process_get_ag_genesis_info( + rpc_client: &RpcClient, + config: &CliConfig<'_>, +) -> ProcessResult { + let cert = rpc_client.get_ag_genesis_cert().await?; + let ag_genesis_info = match cert { + None => CliAgGenesisInfo::Tower, + Some(Certificate { + cert_type, + signature, + bitmap, + }) => { + debug_assert!(cert_type.is_genesis()); + let epoch_schedule = rpc_client.get_epoch_schedule().await?; + let epoch = epoch_schedule.get_epoch(cert_type.slot()); + const MAX_VALIDATORS: usize = 4096; + let Decoded::Base2(bitvec) = decode(&bitmap, MAX_VALIDATORS) + .map_err(|_| Box::new(CliError::InvalidAgGenesisCert))? + else { + return Err(Box::new(CliError::InvalidAgGenesisCert)); + }; + CliAgGenesisInfo::Ag(CliAgGenesisInfoPayload { + epoch, + slot: cert_type.slot(), + block_id: cert_type.to_block().unwrap().1, + bitvec, + signature, + }) + } + }; + Ok(config.output_format.formatted_string(&ag_genesis_info)) +} + pub async fn process_get_epoch_info( rpc_client: &RpcClient, config: &CliConfig<'_>, diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 137b4b21490..5e8d80d5772 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -6421,6 +6421,7 @@ dependencies = [ "Inflector", "agave-reserved-account-keys", "base64 0.22.1", + "bitvec", "chrono", "clap", "console 0.16.3", @@ -6433,6 +6434,7 @@ dependencies = [ "solana-account 4.3.0", "solana-account-decoder", "solana-bincode", + "solana-bls-signatures", "solana-clap-utils", "solana-cli-config", "solana-clock", @@ -8219,6 +8221,7 @@ dependencies = [ name = "solana-rpc-client" version = "4.2.0-alpha.0" dependencies = [ + "agave-votor-messages", "async-trait", "base64 0.22.1", "bincode", @@ -8234,6 +8237,7 @@ dependencies = [ "solana-account 4.3.0", "solana-account-decoder", "solana-account-decoder-client-types", + "solana-bls-signatures", "solana-clock", "solana-commitment-config", "solana-epoch-info", diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index 6af6c28e796..4f80fb6920d 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -6268,6 +6268,7 @@ dependencies = [ "Inflector", "agave-reserved-account-keys", "base64 0.22.1", + "bitvec", "chrono", "clap", "console 0.16.3", @@ -6280,6 +6281,7 @@ dependencies = [ "solana-account 4.3.0", "solana-account-decoder", "solana-bincode", + "solana-bls-signatures", "solana-clap-utils", "solana-cli-config", "solana-clock", @@ -8026,6 +8028,7 @@ dependencies = [ name = "solana-rpc-client" version = "4.2.0-alpha.0" dependencies = [ + "agave-votor-messages", "async-trait", "base64 0.22.1", "bincode", @@ -8041,6 +8044,7 @@ dependencies = [ "solana-account 4.3.0", "solana-account-decoder", "solana-account-decoder-client-types", + "solana-bls-signatures", "solana-clock", "solana-commitment-config", "solana-epoch-info", diff --git a/rpc-client-types/src/request.rs b/rpc-client-types/src/request.rs index 6593dff3e25..439ebb7de9c 100644 --- a/rpc-client-types/src/request.rs +++ b/rpc-client-types/src/request.rs @@ -48,6 +48,7 @@ pub enum RpcRequest { GetSlot, GetSlotLeader, GetSlotLeaders, + GetAgGenesisCert, GetStorageTurn, GetStorageTurnRate, GetSlotsPerSegment, @@ -114,6 +115,7 @@ impl RpcRequest { RpcRequest::GetSlot => "getSlot", RpcRequest::GetSlotLeader => "getSlotLeader", RpcRequest::GetSlotLeaders => "getSlotLeaders", + RpcRequest::GetAgGenesisCert => "getAgGenesisCert", RpcRequest::GetStakeMinimumDelegation => "getStakeMinimumDelegation", RpcRequest::GetStorageTurn => "getStorageTurn", RpcRequest::GetStorageTurnRate => "getStorageTurnRate", diff --git a/rpc-client/Cargo.toml b/rpc-client/Cargo.toml index 2eb096d3f73..a8261b3626a 100644 --- a/rpc-client/Cargo.toml +++ b/rpc-client/Cargo.toml @@ -20,6 +20,7 @@ agave-unstable-api = [] spinner = ["dep:indicatif"] [dependencies] +agave-votor-messages = { workspace = true } async-trait = { workspace = true } base64 = { workspace = true } bincode = { workspace = true } @@ -35,6 +36,7 @@ serde_json = { workspace = true } solana-account = { workspace = true } solana-account-decoder = { workspace = true } solana-account-decoder-client-types = { workspace = true, features = ["zstd"] } +solana-bls-signatures = { workspace = true } solana-clock = { workspace = true } solana-commitment-config = { workspace = true } solana-epoch-info = { workspace = true, features = ["serde"] } diff --git a/rpc-client/src/mock_sender.rs b/rpc-client/src/mock_sender.rs index 8d5db296f5e..09611dda908 100644 --- a/rpc-client/src/mock_sender.rs +++ b/rpc-client/src/mock_sender.rs @@ -2,15 +2,18 @@ use { crate::rpc_sender::*, + agave_votor_messages::consensus_message::{Certificate, CertificateType}, async_trait::async_trait, base64::{Engine, prelude::BASE64_STANDARD}, serde_json::{Number, Value, json}, solana_account_decoder_client_types::{ UiAccount, UiAccountData, UiAccountEncoding, token::UiTokenAmount, }, + solana_bls_signatures::{BLS_SIGNATURE_AFFINE_SIZE, Signature as BLSSignature}, solana_clock::{Slot, UnixTimestamp}, solana_epoch_info::EpochInfo, solana_epoch_schedule::EpochSchedule, + solana_hash::Hash, solana_instruction::{TRANSACTION_LEVEL_STACK_HEIGHT, error::InstructionError}, solana_message::MessageHeader, solana_pubkey::Pubkey, @@ -171,6 +174,14 @@ impl RpcSender for MockSender { block_height: 34, transaction_count: Some(123), })?, + "getAgGenesisCert" => { + let cert = Certificate { + cert_type: CertificateType::Genesis(0, Hash::default()), + signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), + bitmap: Vec::default(), + }; + serde_json::to_value(Some(cert))? + } "getSignatureStatuses" => { let status: TransactionResult<()> = if self.url == "account_in_use" { Err(TransactionError::AccountInUse) diff --git a/rpc-client/src/nonblocking/rpc_client.rs b/rpc-client/src/nonblocking/rpc_client.rs index 1449265d328..4dfad7f528b 100644 --- a/rpc-client/src/nonblocking/rpc_client.rs +++ b/rpc-client/src/nonblocking/rpc_client.rs @@ -19,6 +19,7 @@ use { }, rpc_sender::*, }, + agave_votor_messages::consensus_message::Certificate, base64::{Engine, prelude::BASE64_STANDARD}, bincode::serialize, futures::join, @@ -2096,6 +2097,30 @@ impl RpcClient { }) } + /// Returns Alpenglow's genesis certificate. + /// + /// # RPC Reference + /// + /// This method corresponds directly to the [`getAgGenesisCert`] RPC method. + /// + /// [`getAgGenesisCert`]: https://solana.com/docs/rpc/http/getaggenesiscert + /// + /// # Examples + /// + /// ``` + /// # use solana_rpc_client_api::client_error::Error; + /// # use solana_rpc_client::nonblocking::rpc_client::RpcClient; + /// # futures::executor::block_on(async { + /// # let rpc_client = RpcClient::new_mock("succeeds".to_string()); + /// let cert = rpc_client.get_ag_genesis_cert().await?; + /// # Ok::<(), Error>(()) + /// # })?; + /// # Ok::<(), Error>(()) + /// ``` + pub async fn get_ag_genesis_cert(&self) -> ClientResult> { + self.send(RpcRequest::GetAgGenesisCert, Value::Null).await + } + /// Get block production for the current epoch. /// /// # RPC Reference diff --git a/rpc-client/src/rpc_client.rs b/rpc-client/src/rpc_client.rs index a34cb86733b..f034c335f4c 100644 --- a/rpc-client/src/rpc_client.rs +++ b/rpc-client/src/rpc_client.rs @@ -17,6 +17,7 @@ use { nonblocking::{self, rpc_client::get_rpc_request_str}, rpc_sender::*, }, + agave_votor_messages::consensus_message::Certificate, serde::Serialize, serde_json::Value, solana_account::{Account, ReadableAccount}, @@ -1828,6 +1829,27 @@ impl RpcClient { self.invoke((self.rpc_client.as_ref()).get_slot_leaders(start_slot, limit)) } + /// Returns Alpenglow's genesis certificate. + /// + /// # RPC Reference + /// + /// This method corresponds directly to the [`getAgGenesisCert`] RPC method. + /// + /// [`getAgGenesisCert`]: https://solana.com/docs/rpc/http/getaggenesiscert + /// + /// # Examples + /// + /// ``` + /// # use solana_rpc_client_api::client_error::Error; + /// # use solana_rpc_client::rpc_client::RpcClient; + /// # let rpc_client = RpcClient::new_mock("succeeds".to_string()); + /// let cert = rpc_client.get_ag_genesis_cert()?; + /// # Ok::<(), Error>(()) + /// ``` + pub fn get_ag_genesis_cert(&self) -> ClientResult> { + self.invoke((self.rpc_client.as_ref()).get_ag_genesis_cert()) + } + /// Get block production for the current epoch. /// /// # RPC Reference diff --git a/rpc/src/rpc.rs b/rpc/src/rpc.rs index 61f7b33116c..1fb0866ec98 100644 --- a/rpc/src/rpc.rs +++ b/rpc/src/rpc.rs @@ -8,6 +8,7 @@ use { parsed_token_accounts::*, rpc_cache::LargestAccountsCache, rpc_health::*, }, agave_snapshots::{paths as snapshot_paths, snapshot_config::SnapshotConfig}, + agave_votor_messages::consensus_message::Certificate, base64::{Engine, prelude::BASE64_STANDARD}, crossbeam_channel::{Receiver, Sender, unbounded}, jsonrpc_core::{ @@ -920,6 +921,11 @@ impl JsonRpcRequestProcessor { bank.epoch_schedule().clone() } + pub fn get_ag_genesis_cert(&self) -> Option { + let bank = self.bank(Some(CommitmentConfig::finalized())); + bank.get_alpenglow_genesis_certificate() + } + pub fn get_balance( &self, pubkey: &Pubkey, @@ -3029,6 +3035,9 @@ pub mod rpc_bank { limit: u64, ) -> Result>; + #[rpc(meta, name = "getAgGenesisCert")] + fn get_ag_genesis_cert(&self, meta: Self::Metadata) -> Result>; + #[rpc(meta, name = "getBlockProduction")] fn get_block_production( &self, @@ -3104,6 +3113,11 @@ pub mod rpc_bank { .collect()) } + fn get_ag_genesis_cert(&self, meta: Self::Metadata) -> Result> { + debug!("get_ag_genesis_cert rpc request received"); + Ok(meta.get_ag_genesis_cert()) + } + fn get_block_production( &self, meta: Self::Metadata, From 9376529ed5577a106ee4d41f11b2d57914e4a3d4 Mon Sep 17 00:00:00 2001 From: sam0x17 Date: Fri, 29 May 2026 12:59:35 -0400 Subject: [PATCH 132/576] rpc: fix WS pubsub subscription cap bypass via duplicate params (#12594) The configured --rpc-pubsub-max-active-subscriptions cap was enforced against the count of unique SubscriptionParams entries, so opening N WebSocket connections that subscribed with identical params (e.g. slotSubscribe) consumed only one slot. Each duplicate holder still participated in downstream broadcast fanout, exceeding the operator-set global limit advertised by the CLI help. Track live subscriber holders in a dedicated AtomicUsize and enforce the cap against that counter instead. A SubscriberCountGuard RAII handle reserves a slot on subscribe() via an atomic check-and-increment and releases it when the issued SubscriptionToken drops, so the existing per-param upstream deduplication optimization is preserved while duplicate subscribers each consume their own slot. SubscriberCountGuard is intentionally not Clone and SubscriptionToken no longer derives Clone, so duplicate holders must always go through try_reserve and cannot bypass the cap. Counter accesses use Relaxed ordering since the atomic is used purely for limiting, not memory synchronization. --- rpc/src/rpc_subscription_tracker.rs | 168 +++++++++++++++++++++++----- 1 file changed, 138 insertions(+), 30 deletions(-) diff --git a/rpc/src/rpc_subscription_tracker.rs b/rpc/src/rpc_subscription_tracker.rs index 838c712502d..cf6e313a9d1 100644 --- a/rpc/src/rpc_subscription_tracker.rs +++ b/rpc/src/rpc_subscription_tracker.rs @@ -19,7 +19,7 @@ use { fmt, sync::{ Arc, RwLock, Weak, - atomic::{AtomicU64, Ordering}, + atomic::{AtomicU64, AtomicUsize, Ordering}, }, }, thiserror::Error, @@ -186,6 +186,10 @@ struct SubscriptionControlInner { subscriptions: DashMap, next_id: AtomicU64, max_active_subscriptions: usize, + // Number of live `SubscriptionToken`s issued by `subscribe()`. Counts + // duplicate subscribers to the same params separately so the configured + // cap is enforced per subscriber, not per deduplicated upstream stream. + subscriber_count: AtomicUsize, sender: crossbeam_channel::Sender, broadcast_sender: broadcast::Sender, counter: TokenCounter, @@ -201,6 +205,7 @@ impl SubscriptionControl { subscriptions: DashMap::new(), next_id: AtomicU64::new(0), max_active_subscriptions, + subscriber_count: AtomicUsize::new(0), sender, broadcast_sender, counter: TokenCounter::new("rpc_pubsub_total_subscriptions"), @@ -216,54 +221,69 @@ impl SubscriptionControl { "Total existing subscriptions: {}", self.0.subscriptions.len() ); - let count = self.0.subscriptions.len(); - let create_token_and_weak_ref = |id, params| { - let token = SubscriptionToken( - Arc::new(SubscriptionTokenInner { - control: Arc::clone(&self.0), - params, - id, - }), - self.0.counter.create_token(), - ); - let weak_ref = WeakSubscriptionTokenRef(Arc::downgrade(&token.0), token.0.id); - (token, weak_ref) + // Reserve a subscriber slot up front. The cap is per live token holder, + // not per deduplicated upstream stream, so duplicate subscribes to the + // same params each consume their own slot. + let subscriber_guard = SubscriberCountGuard::try_reserve(&self.0).ok_or_else(|| { + inc_new_counter_info!("rpc-subscription-refused-limit-reached", 1); + Error::TooManySubscriptions + })?; + + let create_inner = |id, params| { + Arc::new(SubscriptionTokenInner { + control: Arc::clone(&self.0), + params, + id, + }) }; match self.0.subscriptions.entry(params) { DashEntry::Occupied(mut entry) => match entry.get().0.upgrade() { - Some(token_ref) => Ok(SubscriptionToken(token_ref, self.0.counter.create_token())), + Some(inner) => Ok(SubscriptionToken( + inner, + self.0.counter.create_token(), + subscriber_guard, + )), // This means the last Arc for this Weak pointer entered the drop just before us, // but could not remove the entry since we are holding the write lock. // See `Drop` implementation for `SubscriptionTokenInner` for further info. None => { - let (token, weak_ref) = - create_token_and_weak_ref(entry.get().1, entry.key().clone()); + let inner = create_inner(entry.get().1, entry.key().clone()); + let weak_ref = WeakSubscriptionTokenRef(Arc::downgrade(&inner), inner.id); entry.insert(weak_ref); - Ok(token) + Ok(SubscriptionToken( + inner, + self.0.counter.create_token(), + subscriber_guard, + )) } }, DashEntry::Vacant(entry) => { - if count >= self.0.max_active_subscriptions { - inc_new_counter_info!("rpc-subscription-refused-limit-reached", 1); - return Err(Error::TooManySubscriptions); - } let id = SubscriptionId::from(self.0.next_id.fetch_add(1, Ordering::AcqRel)); - let (token, weak_ref) = create_token_and_weak_ref(id, entry.key().clone()); + let inner = create_inner(id, entry.key().clone()); + let weak_ref = WeakSubscriptionTokenRef(Arc::downgrade(&inner), id); let _ = self .0 .sender - .send(NotificationEntry::Subscribed(token.0.params.clone(), id).into()); + .send(NotificationEntry::Subscribed(inner.params.clone(), id).into()); entry.insert(weak_ref); datapoint_info!( "rpc-subscription", ("total", self.0.subscriptions.len(), i64) ); - Ok(token) + Ok(SubscriptionToken( + inner, + self.0.counter.create_token(), + subscriber_guard, + )) } } } + pub fn subscriber_total(&self) -> usize { + self.0.subscriber_count.load(Ordering::Relaxed) + } + pub fn total(&self) -> usize { self.0.subscriptions.len() } @@ -577,11 +597,36 @@ impl Drop for SubscriptionTokenInner { } } -// allowing dead code here to appease clippy, but unsure if/how the CounterToken is actually used. -// further investigation would be necessary before removing -#[allow(dead_code)] -#[derive(Clone)] -pub struct SubscriptionToken(Arc, CounterToken); +// RAII guard for one subscriber slot. The slot is reserved atomically against +// the configured cap by `try_reserve` (returns `None` if the cap is reached) +// and released on drop. Intentionally not `Clone`: each holder must go through +// `try_reserve` so duplicates cannot bypass the cap. +struct SubscriberCountGuard(Arc); + +impl SubscriberCountGuard { + fn try_reserve(control: &Arc) -> Option { + let max = control.max_active_subscriptions; + control + .subscriber_count + .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| { + (current < max).then_some(current + 1) + }) + .ok()?; + Some(Self(Arc::clone(control))) + } +} + +impl Drop for SubscriberCountGuard { + fn drop(&mut self) { + self.0.subscriber_count.fetch_sub(1, Ordering::Relaxed); + } +} + +pub struct SubscriptionToken( + Arc, + #[allow(dead_code)] CounterToken, + #[allow(dead_code)] SubscriberCountGuard, +); impl SubscriptionToken { pub fn id(&self) -> SubscriptionId { @@ -655,12 +700,75 @@ mod tests { control.assert_unsubscribed(&SubscriptionParams::Slot, 0); } + #[test] + fn recreates_inner_when_weak_upgrade_fails() { + // Exercise the race-window branch in `subscribe()` where a stale + // `WeakSubscriptionTokenRef` is still in the DashMap because the + // previous holder's `SubscriptionTokenInner::drop` is blocked on the + // entry lock. A fresh subscribe must recreate the inner and reuse the + // stored SubscriptionId rather than allocating a new one. + let (sender, receiver) = crossbeam_channel::unbounded(); + let (broadcast_sender, _broadcast_receiver) = broadcast::channel(42); + let control = SubscriptionControl::new(2, sender, broadcast_sender); + + let stored_id = SubscriptionId::from(42); + let dead_weak = { + let arc = Arc::new(SubscriptionTokenInner { + control: Arc::clone(&control.0), + params: SubscriptionParams::Slot, + id: stored_id, + }); + let weak = Arc::downgrade(&arc); + WeakSubscriptionTokenRef(weak, stored_id) + // `arc` drops here; its Drop sees Vacant and logs a warning. + }; + control + .0 + .subscriptions + .insert(SubscriptionParams::Slot, dead_weak); + + let token = control.subscribe(SubscriptionParams::Slot).unwrap(); + assert_eq!(token.id(), stored_id); + assert_eq!(control.total(), 1); + assert_eq!(control.subscriber_total(), 1); + // Drain notifications emitted by the test (Drop-on-vacant warning is + // log-only; the only entry should be the Subscribed from the recreate). + while receiver.try_recv().is_ok() {} + drop(token); + } + + #[test] + fn duplicate_params_consume_separate_subscriber_slots() { + // Cap of 1 must reject a second subscriber even when params match an + // existing subscription (GHSA: duplicate-params cap bypass). + let (sender, _receiver) = crossbeam_channel::unbounded(); + let (broadcast_sender, _broadcast_receiver) = broadcast::channel(42); + let control = SubscriptionControl::new(1, sender, broadcast_sender); + + let token1 = control.subscribe(SubscriptionParams::Slot).unwrap(); + assert_eq!(control.total(), 1); + assert_eq!(control.subscriber_total(), 1); + + assert!(matches!( + control.subscribe(SubscriptionParams::Slot), + Err(Error::TooManySubscriptions) + )); + assert_eq!(control.subscriber_total(), 1); + + drop(token1); + // After the only holder drops, a fresh subscriber should fit again. + let token2 = control.subscribe(SubscriptionParams::Slot).unwrap(); + assert_eq!(control.subscriber_total(), 1); + drop(token2); + assert_eq!(control.subscriber_total(), 0); + } + #[test] fn notify_subscribe_multiple() { let control = ControlWrapper::new(); let token1 = control.control.subscribe(SubscriptionParams::Slot).unwrap(); control.assert_subscribed(&SubscriptionParams::Slot, 0); - let token2 = token1.clone(); + let token2 = control.control.subscribe(SubscriptionParams::Slot).unwrap(); drop(token1); let token3 = control.control.subscribe(SubscriptionParams::Slot).unwrap(); drop(token3); From 766428eaa24bfa7cb2122a6326f57f220f485a94 Mon Sep 17 00:00:00 2001 From: Akhilesh Singhania Date: Fri, 29 May 2026 19:27:25 +0200 Subject: [PATCH 133/576] moves `Certificate` and `CertificateType` into their own module (#12840) move certificate and certificatetype into its own module --- bls-cert-verify/benches/cert_verify.rs | 3 +- bls-cert-verify/src/cert_verify.rs | 2 +- cli/src/cluster_query.rs | 2 +- core/src/bls_sigverify/bls_cert_sigverify.rs | 3 +- core/src/bls_sigverify/bls_sigverifier.rs | 6 +- core/src/replay_stage/tests.rs | 2 +- core/src/validator.rs | 2 +- entry/src/block_component.rs | 2 +- ledger/src/blockstore_processor.rs | 2 +- rpc-client/src/mock_sender.rs | 2 +- rpc-client/src/nonblocking/rpc_client.rs | 2 +- rpc-client/src/rpc_client.rs | 2 +- rpc/src/rpc.rs | 2 +- runtime/src/bank.rs | 4 +- runtime/src/bank_forks.rs | 2 +- runtime/src/block_component_processor.rs | 3 +- .../block_component_processor/vote_reward.rs | 2 +- .../vote_reward/migration_test.rs | 2 +- runtime/src/genesis_utils.rs | 3 +- runtime/src/validated_block_finalization.rs | 2 +- votor-messages/src/certificate.rs | 208 ++++++++++++++++++ votor-messages/src/consensus_message.rs | 194 +--------------- votor-messages/src/lib.rs | 1 + votor-messages/src/migration.rs | 3 +- votor/src/common.rs | 2 +- votor/src/consensus_pool.rs | 3 +- .../src/consensus_pool/certificate_builder.rs | 8 +- votor/src/consensus_pool/stats.rs | 2 +- votor/src/consensus_pool_service.rs | 6 +- votor/src/generated_cert_types.rs | 4 +- votor/src/voting_service.rs | 5 +- 31 files changed, 259 insertions(+), 227 deletions(-) create mode 100644 votor-messages/src/certificate.rs diff --git a/bls-cert-verify/benches/cert_verify.rs b/bls-cert-verify/benches/cert_verify.rs index 761dc124a64..a0824700aa1 100644 --- a/bls-cert-verify/benches/cert_verify.rs +++ b/bls-cert-verify/benches/cert_verify.rs @@ -2,7 +2,8 @@ use { agave_bls_cert_verify::cert_verify::{aggregate_pubkeys, collect_pubkeys, verify_certificate}, agave_votor::consensus_pool::certificate_builder::CertificateBuilder, agave_votor_messages::{ - consensus_message::{Certificate, CertificateType, VoteMessage}, + certificate::{Certificate, CertificateType}, + consensus_message::VoteMessage, vote::Vote, }, bitvec::vec::BitVec, diff --git a/bls-cert-verify/src/cert_verify.rs b/bls-cert-verify/src/cert_verify.rs index e01cf4b987c..9505b58979f 100644 --- a/bls-cert-verify/src/cert_verify.rs +++ b/bls-cert-verify/src/cert_verify.rs @@ -2,7 +2,7 @@ use qualifier_attr::qualifiers; use { agave_votor_messages::{ - consensus_message::{Certificate, CertificateType}, + certificate::{Certificate, CertificateType}, vote::Vote, }, bitvec::vec::BitVec, diff --git a/cli/src/cluster_query.rs b/cli/src/cluster_query.rs index 72d22aacbd4..e09364339c5 100644 --- a/cli/src/cluster_query.rs +++ b/cli/src/cluster_query.rs @@ -3,7 +3,7 @@ use { cli::{CliCommand, CliCommandInfo, CliConfig, CliError, ProcessResult}, feature::get_feature_activation_epoch, }, - agave_votor_messages::consensus_message::Certificate, + agave_votor_messages::certificate::Certificate, clap::{App, AppSettings, Arg, ArgMatches, SubCommand, value_t, value_t_or_exit}, console::style, serde::{Deserialize, Serialize}, diff --git a/core/src/bls_sigverify/bls_cert_sigverify.rs b/core/src/bls_sigverify/bls_cert_sigverify.rs index ba1fa66dbcc..2dc83daf07c 100644 --- a/core/src/bls_sigverify/bls_cert_sigverify.rs +++ b/core/src/bls_sigverify/bls_cert_sigverify.rs @@ -3,7 +3,8 @@ use { crate::bls_sigverify::{bls_sigverifier::NUM_SLOTS_FOR_VERIFY, utils::send_certs_to_pool}, agave_bls_cert_verify::cert_verify::Error as BlsCertVerifyError, agave_votor_messages::{ - consensus_message::{Certificate, CertificateType, ConsensusMessage}, + certificate::{Certificate, CertificateType}, + consensus_message::ConsensusMessage, fraction::Fraction, }, crossbeam_channel::Sender, diff --git a/core/src/bls_sigverify/bls_sigverifier.rs b/core/src/bls_sigverify/bls_sigverifier.rs index a795f5277e9..903a4c59a8d 100644 --- a/core/src/bls_sigverify/bls_sigverifier.rs +++ b/core/src/bls_sigverify/bls_sigverifier.rs @@ -14,7 +14,8 @@ use { generated_cert_types::GeneratedCertTypes, }, agave_votor_messages::{ - consensus_message::{CertificateType, ConsensusMessage, VoteMessage}, + certificate::CertificateType, + consensus_message::{ConsensusMessage, VoteMessage}, migration::MigrationStatus, }, crossbeam_channel::{Receiver, RecvTimeoutError, Sender, TryRecvError}, @@ -337,7 +338,8 @@ mod tests { consensus_pool::certificate_builder::CertificateBuilder, }, agave_votor_messages::{ - consensus_message::{Certificate, CertificateType, ConsensusMessage, VoteMessage}, + certificate::{Certificate, CertificateType}, + consensus_message::{ConsensusMessage, VoteMessage}, vote::Vote, }, bitvec::prelude::{BitVec, Lsb0}, diff --git a/core/src/replay_stage/tests.rs b/core/src/replay_stage/tests.rs index 1ad2143df2b..1acb582e5a9 100644 --- a/core/src/replay_stage/tests.rs +++ b/core/src/replay_stage/tests.rs @@ -11,7 +11,7 @@ use { replay_stage::ReplayStage, vote_simulator::{self, VoteSimulator}, }, - agave_votor_messages::consensus_message::{Certificate, CertificateType}, + agave_votor_messages::certificate::{Certificate, CertificateType}, blockstore_processor::{ ConfirmationProgress, ProcessOptions, confirm_full_slot, fill_blockstore_slot_with_ticks, process_bank_0, diff --git a/core/src/validator.rs b/core/src/validator.rs index b9d56918f77..5ba50b62f23 100644 --- a/core/src/validator.rs +++ b/core/src/validator.rs @@ -3012,7 +3012,7 @@ mod tests { #[test] fn active_vote_account_exists_in_bank_alpenglow_checks_genesis_certificate_and_votes() { use { - agave_votor_messages::consensus_message::{Certificate, CertificateType}, + agave_votor_messages::certificate::{Certificate, CertificateType}, solana_account::{AccountSharedData, state_traits::StateMut}, solana_bls_signatures::{BLS_SIGNATURE_AFFINE_SIZE, Signature as BLSSignature}, }; diff --git a/entry/src/block_component.rs b/entry/src/block_component.rs index d0c4b018b9b..7e489bfb13f 100644 --- a/entry/src/block_component.rs +++ b/entry/src/block_component.rs @@ -132,7 +132,7 @@ use { crate::entry::{Entry, MaxDataShredsLen}, agave_votor_messages::{ - consensus_message::{Certificate, CertificateType}, + certificate::{Certificate, CertificateType}, reward_certificate::{NotarRewardCertificate, SkipRewardCertificate}, }, solana_bls_signatures::{ diff --git a/ledger/src/blockstore_processor.rs b/ledger/src/blockstore_processor.rs index 7f4843497d2..6be891fa904 100644 --- a/ledger/src/blockstore_processor.rs +++ b/ledger/src/blockstore_processor.rs @@ -2961,7 +2961,7 @@ pub mod tests { }, shred::{ProcessShredsStats, ReedSolomonCache, Shred, Shredder}, }, - agave_votor_messages::consensus_message::{Certificate, CertificateType}, + agave_votor_messages::certificate::{Certificate, CertificateType}, assert_matches::assert_matches, rand::{Rng, rng}, rayon::ThreadPoolBuilder, diff --git a/rpc-client/src/mock_sender.rs b/rpc-client/src/mock_sender.rs index 09611dda908..ba5ebe246fe 100644 --- a/rpc-client/src/mock_sender.rs +++ b/rpc-client/src/mock_sender.rs @@ -2,7 +2,7 @@ use { crate::rpc_sender::*, - agave_votor_messages::consensus_message::{Certificate, CertificateType}, + agave_votor_messages::certificate::{Certificate, CertificateType}, async_trait::async_trait, base64::{Engine, prelude::BASE64_STANDARD}, serde_json::{Number, Value, json}, diff --git a/rpc-client/src/nonblocking/rpc_client.rs b/rpc-client/src/nonblocking/rpc_client.rs index 4dfad7f528b..43a3eb874f2 100644 --- a/rpc-client/src/nonblocking/rpc_client.rs +++ b/rpc-client/src/nonblocking/rpc_client.rs @@ -19,7 +19,7 @@ use { }, rpc_sender::*, }, - agave_votor_messages::consensus_message::Certificate, + agave_votor_messages::certificate::Certificate, base64::{Engine, prelude::BASE64_STANDARD}, bincode::serialize, futures::join, diff --git a/rpc-client/src/rpc_client.rs b/rpc-client/src/rpc_client.rs index f034c335f4c..c17944d9e64 100644 --- a/rpc-client/src/rpc_client.rs +++ b/rpc-client/src/rpc_client.rs @@ -17,7 +17,7 @@ use { nonblocking::{self, rpc_client::get_rpc_request_str}, rpc_sender::*, }, - agave_votor_messages::consensus_message::Certificate, + agave_votor_messages::certificate::Certificate, serde::Serialize, serde_json::Value, solana_account::{Account, ReadableAccount}, diff --git a/rpc/src/rpc.rs b/rpc/src/rpc.rs index 1fb0866ec98..08b0eb8a77e 100644 --- a/rpc/src/rpc.rs +++ b/rpc/src/rpc.rs @@ -8,7 +8,7 @@ use { parsed_token_accounts::*, rpc_cache::LargestAccountsCache, rpc_health::*, }, agave_snapshots::{paths as snapshot_paths, snapshot_config::SnapshotConfig}, - agave_votor_messages::consensus_message::Certificate, + agave_votor_messages::certificate::Certificate, base64::{Engine, prelude::BASE64_STANDARD}, crossbeam_channel::{Receiver, Sender, unbounded}, jsonrpc_core::{ diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 29765942231..70a02b297d3 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -79,9 +79,7 @@ use { agave_precompiles::{get_precompile, get_precompiles, is_precompile}, agave_reserved_account_keys::ReservedAccountKeys, agave_snapshots::snapshot_hash::SnapshotHash, - agave_votor_messages::{ - consensus_message::Certificate, migration::GENESIS_CERTIFICATE_ACCOUNT, - }, + agave_votor_messages::{certificate::Certificate, migration::GENESIS_CERTIFICATE_ACCOUNT}, ahash::AHashSet, dashmap::DashMap, log::*, diff --git a/runtime/src/bank_forks.rs b/runtime/src/bank_forks.rs index c71ec0eca3d..bea4575e6e7 100644 --- a/runtime/src/bank_forks.rs +++ b/runtime/src/bank_forks.rs @@ -787,7 +787,7 @@ mod tests { }, agave_feature_set::FeatureSet, agave_votor_messages::{ - consensus_message::{Certificate, CertificateType}, + certificate::{Certificate, CertificateType}, migration::{GENESIS_CERTIFICATE_ACCOUNT, MIGRATION_SLOT_OFFSET}, }, assert_matches::assert_matches, diff --git a/runtime/src/block_component_processor.rs b/runtime/src/block_component_processor.rs index a76cf232532..5fca47fb5b9 100644 --- a/runtime/src/block_component_processor.rs +++ b/runtime/src/block_component_processor.rs @@ -11,7 +11,8 @@ use { validated_reward_certificate::{Error as ValidatedRewardCertError, ValidatedRewardCert}, }, agave_votor_messages::{ - consensus_message::{Certificate, ConsensusMessage}, + certificate::Certificate, + consensus_message::ConsensusMessage, fraction::Fraction, migration::{GENESIS_VOTE_THRESHOLD, MigrationStatus}, }, diff --git a/runtime/src/block_component_processor/vote_reward.rs b/runtime/src/block_component_processor/vote_reward.rs index f699aa6ab58..23f3b90e780 100644 --- a/runtime/src/block_component_processor/vote_reward.rs +++ b/runtime/src/block_component_processor/vote_reward.rs @@ -592,7 +592,7 @@ mod tests { }, agave_feature_set::FeatureSet, agave_votor_messages::{ - consensus_message::{Certificate, CertificateType}, + certificate::{Certificate, CertificateType}, reward_certificate::NUM_SLOTS_FOR_REWARD, }, bitvec::prelude::*, diff --git a/runtime/src/block_component_processor/vote_reward/migration_test.rs b/runtime/src/block_component_processor/vote_reward/migration_test.rs index 986909437b3..c7f81acd176 100644 --- a/runtime/src/block_component_processor/vote_reward/migration_test.rs +++ b/runtime/src/block_component_processor/vote_reward/migration_test.rs @@ -16,7 +16,7 @@ mod tests { stake_utils, }, agave_feature_set::FeatureSet, - agave_votor_messages::consensus_message::{Certificate, CertificateType}, + agave_votor_messages::certificate::{Certificate, CertificateType}, solana_account::{Account, ReadableAccount}, solana_bls_signatures::{BLS_SIGNATURE_AFFINE_SIZE, Signature as BLSSignature}, solana_cluster_type::ClusterType, diff --git a/runtime/src/genesis_utils.rs b/runtime/src/genesis_utils.rs index aac6fb1f5bb..d6dbca2c338 100644 --- a/runtime/src/genesis_utils.rs +++ b/runtime/src/genesis_utils.rs @@ -9,7 +9,8 @@ use { agave_feature_set::{FEATURE_NAMES, FeatureSet}, agave_votor_messages::{ self, - consensus_message::{BLS_KEYPAIR_DERIVE_SEED, Certificate, CertificateType}, + certificate::{Certificate, CertificateType}, + consensus_message::BLS_KEYPAIR_DERIVE_SEED, migration::GENESIS_CERTIFICATE_ACCOUNT, }, bincode::serialize, diff --git a/runtime/src/validated_block_finalization.rs b/runtime/src/validated_block_finalization.rs index ca11b582374..93e16b2b547 100644 --- a/runtime/src/validated_block_finalization.rs +++ b/runtime/src/validated_block_finalization.rs @@ -7,7 +7,7 @@ use { crate::bank::Bank, agave_votor_messages::{ - consensus_message::{Certificate, CertificateType}, + certificate::{Certificate, CertificateType}, fraction::Fraction, }, log::warn, diff --git a/votor-messages/src/certificate.rs b/votor-messages/src/certificate.rs new file mode 100644 index 00000000000..73476453252 --- /dev/null +++ b/votor-messages/src/certificate.rs @@ -0,0 +1,208 @@ +//! Definitions of Alpenglow consensus certificates + +use { + crate::{ + consensus_message::Block, + fraction::Fraction, + migration::GENESIS_VOTE_THRESHOLD, + vote::{Vote, VoteType}, + }, + serde::{Deserialize, Serialize}, + solana_bls_signatures::Signature as BLSSignature, + solana_clock::Slot, + solana_hash::Hash, + wincode::{SchemaRead, SchemaWrite, pod_wrapper}, +}; + +// Use `BLSSignature` directly once `BLSSignature` wincode support +// is released in solana-sdk. +pod_wrapper! { + unsafe struct PodBLSSignature(BLSSignature); +} + +/// The actual certificate with the aggregate signature and bitmap for which validators are included in the aggregate. +/// BLS vote message, we need rank to look up pubkey +#[cfg_attr( + feature = "frozen-abi", + derive(AbiExample), + frozen_abi(digest = "Aijp38PLH2Y9rwxXeAdRXvCisL9Fnx6spwnKGY55WcuU") +)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, SchemaWrite, SchemaRead)] +pub struct Certificate { + /// The certificate type. + pub cert_type: CertificateType, + /// The aggregate signature. + #[wincode(with = "PodBLSSignature")] + pub signature: BLSSignature, + /// A rank bitmap for validators' signatures included in the aggregate. + /// See solana-signer-store for encoding format. + pub bitmap: Vec, +} + +/// The different types of certificates and their relevant state. +#[cfg_attr( + feature = "frozen-abi", + derive(AbiExample, AbiEnumVisitor), + frozen_abi(digest = "CazjewshYYizgQuCgBBRv6gzasJpUvFVKoSeEirWRKgA") +)] +#[derive( + Debug, + Copy, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + Deserialize, + Serialize, + SchemaWrite, + SchemaRead, +)] +pub enum CertificateType { + /// Finalize certificate + Finalize(Slot), + /// Fast finalize certificate + FinalizeFast(Slot, Hash), + /// Notarize certificate + Notarize(Slot, Hash), + /// Notarize fallback certificate + NotarizeFallback(Slot, Hash), + /// Skip certificate + Skip(Slot), + /// Genesis certificate + Genesis(Slot, Hash), +} + +impl CertificateType { + /// Get the slot of the certificate + pub fn slot(&self) -> Slot { + match self { + CertificateType::Finalize(slot) + | CertificateType::FinalizeFast(slot, _) + | CertificateType::Notarize(slot, _) + | CertificateType::NotarizeFallback(slot, _) + | CertificateType::Genesis(slot, _) + | CertificateType::Skip(slot) => *slot, + } + } + + /// Is this a fast finalize certificate? + pub fn is_fast_finalization(&self) -> bool { + matches!(self, Self::FinalizeFast(_, _)) + } + + /// Is this a finalize / fast finalize certificate? + pub fn is_finalization(&self) -> bool { + matches!(self, Self::Finalize(_) | Self::FinalizeFast(_, _)) + } + + /// Is this a slow finalize certificate? + pub fn is_slow_finalization(&self) -> bool { + matches!(self, Self::Finalize(_)) + } + + /// Is this a notarize certificate? + pub fn is_notarize(&self) -> bool { + matches!(self, Self::Notarize(_, _)) + } + + /// Is this a notarize fallback certificate? + pub fn is_notarize_fallback(&self) -> bool { + matches!(self, Self::NotarizeFallback(_, _)) + } + + /// Is this a skip certificate? + pub fn is_skip(&self) -> bool { + matches!(self, Self::Skip(_)) + } + + /// Is this a genesis certificate? + pub fn is_genesis(&self) -> bool { + matches!(self, Self::Genesis(_, _)) + } + + /// Gets the block associated with this certificate, if present + pub fn to_block(self) -> Option { + match self { + CertificateType::Finalize(_) | CertificateType::Skip(_) => None, + CertificateType::Notarize(slot, block_id) + | CertificateType::NotarizeFallback(slot, block_id) + | CertificateType::Genesis(slot, block_id) + | CertificateType::FinalizeFast(slot, block_id) => Some((slot, block_id)), + } + } + + /// Reconstructs the single source `Vote` payload for this certificate. + /// + /// This method is used primarily by the signature verifier. For + /// certificates formed by aggregating a single type of vote + /// (e.g., a `Notarize` certificate from `Notarize` votes), this function + /// reconstructs the canonical message payload that was signed by validators. + /// + /// For `NotarizeFallback` and `Skip` certificates, this function returns the + /// appropriate payload *only* if the certificate was formed from a single + /// vote type (e.g., exclusively from `Notarize` or `Skip` votes). For + /// certificates formed from a mix of two vote types, use the `to_source_votes` + /// function. + pub fn to_source_vote(self) -> Vote { + match self { + Self::Notarize(slot, block_id) + | Self::FinalizeFast(slot, block_id) + | Self::NotarizeFallback(slot, block_id) => Vote::new_notarization_vote(slot, block_id), + Self::Finalize(slot) => Vote::new_finalization_vote(slot), + Self::Skip(slot) => Vote::new_skip_vote(slot), + Self::Genesis(slot, block_id) => Vote::new_genesis_vote(slot, block_id), + } + } + + /// Reconstructs the two distinct source `Vote` payloads for this certificate. + /// + /// This method is primarily used by the signature verifier for certificates that + /// can be formed by aggregating two different types of votes. For example, a + /// `NotarizeFallback` certificate accepts both `Notarize` and `NotarizeFallback`. + /// + /// It reconstructs both potential message payloads that were signed by validators, which + /// the verifier uses to check the single aggregate signature. + pub fn to_source_votes(self) -> Option<(Vote, Vote)> { + match self { + Self::NotarizeFallback(slot, block_id) => { + let vote1 = Vote::new_notarization_vote(slot, block_id); + let vote2 = Vote::new_notarization_fallback_vote(slot, block_id); + Some((vote1, vote2)) + } + Self::Skip(slot) => { + let vote1 = Vote::new_skip_vote(slot); + let vote2 = Vote::new_skip_fallback_vote(slot); + Some((vote1, vote2)) + } + // Other certificate types do not use Base3 encoding. + _ => None, + } + } + + /// Returns the stake fraction required for certificate completion and the + /// `VoteType`s that contribute to this certificate. + /// + /// Must be in sync with `Vote::to_cert_types` + pub const fn limits_and_vote_types(&self) -> (Fraction, &'static [VoteType]) { + match self { + CertificateType::Notarize(_, _) => { + (Fraction::from_percentage(60), &[VoteType::Notarize]) + } + CertificateType::NotarizeFallback(_, _) => ( + Fraction::from_percentage(60), + &[VoteType::Notarize, VoteType::NotarizeFallback], + ), + CertificateType::FinalizeFast(_, _) => { + (Fraction::from_percentage(80), &[VoteType::Notarize]) + } + CertificateType::Finalize(_) => (Fraction::from_percentage(60), &[VoteType::Finalize]), + CertificateType::Skip(_) => ( + Fraction::from_percentage(60), + &[VoteType::Skip, VoteType::SkipFallback], + ), + CertificateType::Genesis(_, _) => (GENESIS_VOTE_THRESHOLD, &[VoteType::Genesis]), + } + } +} diff --git a/votor-messages/src/consensus_message.rs b/votor-messages/src/consensus_message.rs index c82ed3a4a1e..c29aed7659a 100644 --- a/votor-messages/src/consensus_message.rs +++ b/votor-messages/src/consensus_message.rs @@ -1,9 +1,8 @@ //! Put Alpenglow consensus messages here so all clients can agree on the format. use { crate::{ - fraction::Fraction, - migration::GENESIS_VOTE_THRESHOLD, - vote::{Vote, VoteType}, + certificate::{Certificate, CertificateType}, + vote::Vote, }, serde::{Deserialize, Serialize}, solana_bls_signatures::Signature as BLSSignature, @@ -40,198 +39,11 @@ pub struct VoteMessage { pub rank: u16, } -/// The different types of certificates and their relevant state. -#[cfg_attr( - feature = "frozen-abi", - derive(AbiExample, AbiEnumVisitor), - frozen_abi(digest = "CazjewshYYizgQuCgBBRv6gzasJpUvFVKoSeEirWRKgA") -)] -#[derive( - Debug, - Copy, - Clone, - PartialEq, - Eq, - PartialOrd, - Ord, - Hash, - Deserialize, - Serialize, - SchemaWrite, - SchemaRead, -)] -pub enum CertificateType { - /// Finalize certificate - Finalize(Slot), - /// Fast finalize certificate - FinalizeFast(Slot, Hash), - /// Notarize certificate - Notarize(Slot, Hash), - /// Notarize fallback certificate - NotarizeFallback(Slot, Hash), - /// Skip certificate - Skip(Slot), - /// Genesis certificate - Genesis(Slot, Hash), -} - -impl CertificateType { - /// Get the slot of the certificate - pub fn slot(&self) -> Slot { - match self { - CertificateType::Finalize(slot) - | CertificateType::FinalizeFast(slot, _) - | CertificateType::Notarize(slot, _) - | CertificateType::NotarizeFallback(slot, _) - | CertificateType::Genesis(slot, _) - | CertificateType::Skip(slot) => *slot, - } - } - - /// Is this a fast finalize certificate? - pub fn is_fast_finalization(&self) -> bool { - matches!(self, Self::FinalizeFast(_, _)) - } - - /// Is this a finalize / fast finalize certificate? - pub fn is_finalization(&self) -> bool { - matches!(self, Self::Finalize(_) | Self::FinalizeFast(_, _)) - } - - /// Is this a slow finalize certificate? - pub fn is_slow_finalization(&self) -> bool { - matches!(self, Self::Finalize(_)) - } - - /// Is this a notarize certificate? - pub fn is_notarize(&self) -> bool { - matches!(self, Self::Notarize(_, _)) - } - - /// Is this a notarize fallback certificate? - pub fn is_notarize_fallback(&self) -> bool { - matches!(self, Self::NotarizeFallback(_, _)) - } - - /// Is this a skip certificate? - pub fn is_skip(&self) -> bool { - matches!(self, Self::Skip(_)) - } - - /// Is this a genesis certificate? - pub fn is_genesis(&self) -> bool { - matches!(self, Self::Genesis(_, _)) - } - - /// Gets the block associated with this certificate, if present - pub fn to_block(self) -> Option { - match self { - CertificateType::Finalize(_) | CertificateType::Skip(_) => None, - CertificateType::Notarize(slot, block_id) - | CertificateType::NotarizeFallback(slot, block_id) - | CertificateType::Genesis(slot, block_id) - | CertificateType::FinalizeFast(slot, block_id) => Some((slot, block_id)), - } - } - - /// Reconstructs the single source `Vote` payload for this certificate. - /// - /// This method is used primarily by the signature verifier. For - /// certificates formed by aggregating a single type of vote - /// (e.g., a `Notarize` certificate from `Notarize` votes), this function - /// reconstructs the canonical message payload that was signed by validators. - /// - /// For `NotarizeFallback` and `Skip` certificates, this function returns the - /// appropriate payload *only* if the certificate was formed from a single - /// vote type (e.g., exclusively from `Notarize` or `Skip` votes). For - /// certificates formed from a mix of two vote types, use the `to_source_votes` - /// function. - pub fn to_source_vote(self) -> Vote { - match self { - Self::Notarize(slot, block_id) - | Self::FinalizeFast(slot, block_id) - | Self::NotarizeFallback(slot, block_id) => Vote::new_notarization_vote(slot, block_id), - Self::Finalize(slot) => Vote::new_finalization_vote(slot), - Self::Skip(slot) => Vote::new_skip_vote(slot), - Self::Genesis(slot, block_id) => Vote::new_genesis_vote(slot, block_id), - } - } - - /// Reconstructs the two distinct source `Vote` payloads for this certificate. - /// - /// This method is primarily used by the signature verifier for certificates that - /// can be formed by aggregating two different types of votes. For example, a - /// `NotarizeFallback` certificate accepts both `Notarize` and `NotarizeFallback`. - /// - /// It reconstructs both potential message payloads that were signed by validators, which - /// the verifier uses to check the single aggregate signature. - pub fn to_source_votes(self) -> Option<(Vote, Vote)> { - match self { - Self::NotarizeFallback(slot, block_id) => { - let vote1 = Vote::new_notarization_vote(slot, block_id); - let vote2 = Vote::new_notarization_fallback_vote(slot, block_id); - Some((vote1, vote2)) - } - Self::Skip(slot) => { - let vote1 = Vote::new_skip_vote(slot); - let vote2 = Vote::new_skip_fallback_vote(slot); - Some((vote1, vote2)) - } - // Other certificate types do not use Base3 encoding. - _ => None, - } - } - - /// Returns the stake fraction required for certificate completion and the - /// `VoteType`s that contribute to this certificate. - /// - /// Must be in sync with `Vote::to_cert_types` - pub const fn limits_and_vote_types(&self) -> (Fraction, &'static [VoteType]) { - match self { - CertificateType::Notarize(_, _) => { - (Fraction::from_percentage(60), &[VoteType::Notarize]) - } - CertificateType::NotarizeFallback(_, _) => ( - Fraction::from_percentage(60), - &[VoteType::Notarize, VoteType::NotarizeFallback], - ), - CertificateType::FinalizeFast(_, _) => { - (Fraction::from_percentage(80), &[VoteType::Notarize]) - } - CertificateType::Finalize(_) => (Fraction::from_percentage(60), &[VoteType::Finalize]), - CertificateType::Skip(_) => ( - Fraction::from_percentage(60), - &[VoteType::Skip, VoteType::SkipFallback], - ), - CertificateType::Genesis(_, _) => (GENESIS_VOTE_THRESHOLD, &[VoteType::Genesis]), - } - } -} - -/// The actual certificate with the aggregate signature and bitmap for which validators are included in the aggregate. -/// BLS vote message, we need rank to look up pubkey -#[cfg_attr( - feature = "frozen-abi", - derive(AbiExample), - frozen_abi(digest = "B5NsoWZr2Lpbbjqj8udwEKvae6bA37Pm4R92udZHxwfU") -)] -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, SchemaWrite, SchemaRead)] -pub struct Certificate { - /// The certificate type. - pub cert_type: CertificateType, - /// The aggregate signature. - #[wincode(with = "PodBLSSignature")] - pub signature: BLSSignature, - /// A rank bitmap for validators' signatures included in the aggregate. - /// See solana-signer-store for encoding format. - pub bitmap: Vec, -} - /// A consensus message sent between validators. #[cfg_attr( feature = "frozen-abi", derive(AbiExample, AbiEnumVisitor), - frozen_abi(digest = "BdKT6dbkLnTeGNMS8XtQkg6HTeHSQ6Z41Btc1rJ117PB") + frozen_abi(digest = "FJKdeeziPmtNpqrV7Wk3tVeHS6CzWWLvkX3MdgYXoKwk") )] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, SchemaWrite, SchemaRead)] #[allow(clippy::large_enum_variant)] diff --git a/votor-messages/src/lib.rs b/votor-messages/src/lib.rs index caa77e7f33b..c3d78d143a4 100644 --- a/votor-messages/src/lib.rs +++ b/votor-messages/src/lib.rs @@ -3,6 +3,7 @@ #![cfg_attr(feature = "frozen-abi", feature(min_specialization))] #![deny(missing_docs)] +pub mod certificate; pub mod consensus_message; pub mod fraction; pub mod migration; diff --git a/votor-messages/src/migration.rs b/votor-messages/src/migration.rs index 55fd71bb841..f0db953e503 100644 --- a/votor-messages/src/migration.rs +++ b/votor-messages/src/migration.rs @@ -44,7 +44,8 @@ //! - When in `FullAlpenglowEpoch` we completely shutdown these TowerBFT threads (AncestorHashesService and ClusterSlotsService) use { crate::{ - consensus_message::{Block, Certificate, CertificateType}, + certificate::{Certificate, CertificateType}, + consensus_message::Block, fraction::Fraction, }, log::*, diff --git a/votor/src/common.rs b/votor/src/common.rs index 1749480f4b6..cecb01253ca 100644 --- a/votor/src/common.rs +++ b/votor/src/common.rs @@ -1,6 +1,6 @@ use { agave_votor_messages::{ - consensus_message::CertificateType, + certificate::CertificateType, fraction::Fraction, vote::{Vote, VoteType}, }, diff --git a/votor/src/consensus_pool.rs b/votor/src/consensus_pool.rs index b3c854bb7a7..bb0f149e328 100644 --- a/votor/src/consensus_pool.rs +++ b/votor/src/consensus_pool.rs @@ -16,7 +16,8 @@ use { generated_cert_types::GeneratedCertTypes, }, agave_votor_messages::{ - consensus_message::{Block, Certificate, CertificateType, ConsensusMessage, VoteMessage}, + certificate::{Certificate, CertificateType}, + consensus_message::{Block, ConsensusMessage, VoteMessage}, fraction::Fraction, migration::MigrationStatus, vote::{Vote, VoteType}, diff --git a/votor/src/consensus_pool/certificate_builder.rs b/votor/src/consensus_pool/certificate_builder.rs index 9b367be4b75..e688ec62293 100644 --- a/votor/src/consensus_pool/certificate_builder.rs +++ b/votor/src/consensus_pool/certificate_builder.rs @@ -1,5 +1,8 @@ use { - agave_votor_messages::consensus_message::{Certificate, CertificateType, VoteMessage}, + agave_votor_messages::{ + certificate::{Certificate, CertificateType}, + consensus_message::VoteMessage, + }, bitvec::prelude::*, solana_bls_signatures::{BlsError, SignatureProjective}, solana_signer_store::{EncodeError, encode_base2, encode_base3}, @@ -271,8 +274,7 @@ mod tests { use { super::*, agave_votor_messages::{ - consensus_message::{CertificateType, VoteMessage}, - vote::Vote, + certificate::CertificateType, consensus_message::VoteMessage, vote::Vote, }, solana_bls_signatures::{ BLS_SIGNATURE_AFFINE_SIZE, Keypair as BLSKeypair, PreparedHashedMessage, diff --git a/votor/src/consensus_pool/stats.rs b/votor/src/consensus_pool/stats.rs index 610b8bd4310..c704af2a783 100644 --- a/votor/src/consensus_pool/stats.rs +++ b/votor/src/consensus_pool/stats.rs @@ -1,5 +1,5 @@ use { - agave_votor_messages::{consensus_message::CertificateType, vote::VoteType}, + agave_votor_messages::{certificate::CertificateType, vote::VoteType}, solana_metrics::datapoint_info, std::time::{Duration, Instant}, }; diff --git a/votor/src/consensus_pool_service.rs b/votor/src/consensus_pool_service.rs index 64d0756d655..9a1f3cf5310 100644 --- a/votor/src/consensus_pool_service.rs +++ b/votor/src/consensus_pool_service.rs @@ -15,7 +15,8 @@ use { voting_service::BLSOp, }, agave_votor_messages::{ - consensus_message::{Block, Certificate, ConsensusMessage}, + certificate::Certificate, + consensus_message::{Block, ConsensusMessage}, migration::MigrationStatus, }, crossbeam_channel::{Receiver, Sender, TrySendError, select}, @@ -589,7 +590,8 @@ mod tests { super::*, crate::tests::get_cluster_info, agave_votor_messages::{ - consensus_message::{BLS_KEYPAIR_DERIVE_SEED, CertificateType, VoteMessage}, + certificate::CertificateType, + consensus_message::{BLS_KEYPAIR_DERIVE_SEED, VoteMessage}, vote::Vote, }, crossbeam_channel::unbounded, diff --git a/votor/src/generated_cert_types.rs b/votor/src/generated_cert_types.rs index 97f7a073fca..639076c6d9f 100644 --- a/votor/src/generated_cert_types.rs +++ b/votor/src/generated_cert_types.rs @@ -1,8 +1,8 @@ #[cfg(feature = "dev-context-only-utils")] use qualifier_attr::qualifiers; use { - agave_votor_messages::consensus_message::CertificateType, parking_lot::RwLock, - solana_clock::Slot, std::collections::HashSet, + agave_votor_messages::certificate::CertificateType, parking_lot::RwLock, solana_clock::Slot, + std::collections::HashSet, }; /// A simple container that allows the consensus pool to communicate with the bls sigverifier diff --git a/votor/src/voting_service.rs b/votor/src/voting_service.rs index 35976ce2663..4dbb3278814 100644 --- a/votor/src/voting_service.rs +++ b/votor/src/voting_service.rs @@ -3,7 +3,7 @@ use { staked_validators_cache::StakedValidatorsCache, vote_history_storage::{SavedVoteHistoryVersions, VoteHistoryStorage}, }, - agave_votor_messages::consensus_message::{Certificate, ConsensusMessage}, + agave_votor_messages::{certificate::Certificate, consensus_message::ConsensusMessage}, crossbeam_channel::Receiver, solana_client::connection_cache::ConnectionCache, solana_clock::Slot, @@ -251,7 +251,8 @@ mod tests { NullVoteHistoryStorage, SavedVoteHistory, SavedVoteHistoryVersions, }, agave_votor_messages::{ - consensus_message::{Certificate, CertificateType, ConsensusMessage, VoteMessage}, + certificate::{Certificate, CertificateType}, + consensus_message::{ConsensusMessage, VoteMessage}, vote::Vote, }, solana_bls_signatures::{BLS_SIGNATURE_AFFINE_SIZE, Signature as BLSSignature}, From b5bf557b8670e4e9d31a4eedd1a4afd1ef1fefd1 Mon Sep 17 00:00:00 2001 From: Rory Harris Date: Fri, 29 May 2026 13:08:15 -0700 Subject: [PATCH 134/576] Check cache in get_account_at_slot (#12845) --- accounts-db/src/accounts_db/tests.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/accounts-db/src/accounts_db/tests.rs b/accounts-db/src/accounts_db/tests.rs index b4275cffbd0..62ed5df3cca 100644 --- a/accounts-db/src/accounts_db/tests.rs +++ b/accounts-db/src/accounts_db/tests.rs @@ -3465,6 +3465,11 @@ impl AccountsDb { } fn get_account_at_slot(&self, pubkey: &Pubkey, slot: Slot) -> Option { + // Check the cache for the pubkey first + if let Some(cached) = self.accounts_cache.load(slot, pubkey) { + return Some(cached.account.clone()); + } + // Add the slot to ancestors so unrooted slots will be selected let mut ancestors = Ancestors::default(); ancestors.insert(slot); From 75c2617e29de6773fb08072cfa0e5dda9e607b2f Mon Sep 17 00:00:00 2001 From: Andrew Fitzgerald Date: Fri, 29 May 2026 16:56:11 -0500 Subject: [PATCH 135/576] ChannelSend: remove blocking send (#12832) --- streamer/src/evicting_sender.rs | 7 +------ streamer/src/streamer.rs | 7 ------- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/streamer/src/evicting_sender.rs b/streamer/src/evicting_sender.rs index 4a5ae056eb8..9b7d0a5fa40 100644 --- a/streamer/src/evicting_sender.rs +++ b/streamer/src/evicting_sender.rs @@ -1,6 +1,6 @@ use { crate::streamer::ChannelSend, - crossbeam_channel::{Receiver, SendError, Sender, TryRecvError, TrySendError, bounded}, + crossbeam_channel::{Receiver, Sender, TryRecvError, TrySendError, bounded}, }; /// A sender implementation that evicts the oldest message when the channel is full. @@ -38,11 +38,6 @@ impl ChannelSend for EvictingSender where T: Send + 'static, { - #[inline] - fn send(&self, msg: T) -> std::result::Result<(), SendError> { - self.sender.send(msg) - } - fn try_send(&self, msg: T) -> std::result::Result<(), TrySendError> { let Err(e) = self.sender.try_send(msg) else { return Ok(()); diff --git a/streamer/src/streamer.rs b/streamer/src/streamer.rs index 7bed9a26f5f..d1dcdf3eecf 100644 --- a/streamer/src/streamer.rs +++ b/streamer/src/streamer.rs @@ -40,8 +40,6 @@ use { }; pub trait ChannelSend: Send + 'static { - fn send(&self, msg: T) -> std::result::Result<(), SendError>; - fn try_send(&self, msg: T) -> std::result::Result<(), TrySendError>; fn is_empty(&self) -> bool; @@ -53,11 +51,6 @@ impl ChannelSend for Sender where T: Send + 'static, { - #[inline] - fn send(&self, msg: T) -> std::result::Result<(), SendError> { - self.send(msg) - } - #[inline] fn try_send(&self, msg: T) -> std::result::Result<(), TrySendError> { self.try_send(msg) From 9ba058c5ef4a465456f0761ecc401c0ac4a8e2fe Mon Sep 17 00:00:00 2001 From: Brennan Date: Fri, 29 May 2026 16:02:41 -0700 Subject: [PATCH 136/576] AG PoH Speed Check Disable (#12853) --- core/src/validator.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/validator.rs b/core/src/validator.rs index 5ba50b62f23..8fe389acb19 100644 --- a/core/src/validator.rs +++ b/core/src/validator.rs @@ -933,7 +933,7 @@ impl Validator { let migration_status = bank_forks.read().unwrap().migration_status(); - if !config.no_poh_speed_test { + if !config.no_poh_speed_test && !migration_status.is_alpenglow_enabled() { check_poh_speed(&bank_forks.read().unwrap().root_bank(), None)?; } From dc3e807efed2db3e7682cace887a270f2e1cf8a2 Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Sat, 30 May 2026 23:46:04 -0400 Subject: [PATCH 137/576] replay: plug in bank switch metrics (#12860) --- core/src/replay_stage.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/replay_stage.rs b/core/src/replay_stage.rs index 05b6c5d3d0e..889293bf44b 100644 --- a/core/src/replay_stage.rs +++ b/core/src/replay_stage.rs @@ -2499,6 +2499,7 @@ impl ReplayStage { // Switch the blockstore data atomically, handles slot meta chaining so generate new bank forks can proceed blockstore.switch_block_from_alternate(slot, location)?; + progress.increment_num_bank_switches(slot); info!("{my_pubkey}: Switched {slot} from {location:?}"); } From a2f3e672153996d668d75b8c1479ac3701a26b4c Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Sat, 30 May 2026 23:55:19 -0400 Subject: [PATCH 138/576] replay: root progress map normally (#12861) --- core/src/replay_stage.rs | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/core/src/replay_stage.rs b/core/src/replay_stage.rs index 889293bf44b..e00af997cfa 100644 --- a/core/src/replay_stage.rs +++ b/core/src/replay_stage.rs @@ -1055,8 +1055,6 @@ impl ReplayStage { &optimistic_parent_receiver, &replay_highest_frozen, &mut highest_frozen_slot, - &mut progress, - did_complete_bank, ); let mut process_switch_bank_events_time = Measure::start("process_switch_bank_events_time"); @@ -1579,7 +1577,6 @@ impl ReplayStage { Ok(Self { t_replay }) } - #[allow(clippy::too_many_arguments)] fn alpenglow_handle_newly_frozen_banks( new_frozen_slots: &[Slot], migration_status: &MigrationStatus, @@ -1590,8 +1587,6 @@ impl ReplayStage { optimistic_parent_receiver: &Receiver, replay_highest_frozen: &ReplayHighestFrozen, highest_frozen_slot: &mut Slot, - progress: &mut ProgressMap, - did_complete_bank: bool, ) { let flh_candidate_banks = { let bank_forks_r = bank_forks.read().unwrap(); @@ -1621,10 +1616,6 @@ impl ReplayStage { replay_highest_frozen.freeze_notification.notify_one(); } } - if did_complete_bank { - let bank_forks_r = bank_forks.read().unwrap(); - progress.handle_new_root(&bank_forks_r); - } } /// Enables alpenglow @@ -5038,7 +5029,7 @@ impl ReplayStage { async_verification_freelist: &mut Vec, ) -> Result<(), TryRecvError> { if let Some(command) = bank_forks_controller_receiver.take_set_root_command() { - Self::process_set_root_command(command, context, my_pubkey); + Self::process_set_root_command(command, context, my_pubkey, progress); } loop { @@ -5056,6 +5047,7 @@ impl ReplayStage { command: SetRootCommand, context: &ProcessBankForksContext, my_pubkey: &Pubkey, + progress: &mut ProgressMap, ) { let SetRootCommand { parent_slot, @@ -5074,7 +5066,7 @@ impl ReplayStage { &context.bank_forks, context.rpc_subscriptions.as_deref(), my_pubkey, - |_| {}, + |bank_forks| progress.handle_new_root(bank_forks), ); } From 0211338e699d765d3f892371f22fcd00bc5f236e Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Sun, 31 May 2026 12:56:28 -0400 Subject: [PATCH 139/576] rpc: perf fix for solana stakes (#12863) --- cli/src/cluster_query.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cli/src/cluster_query.rs b/cli/src/cluster_query.rs index e09364339c5..bd46fa2209f 100644 --- a/cli/src/cluster_query.rs +++ b/cli/src/cluster_query.rs @@ -1700,9 +1700,11 @@ pub async fn process_show_stakes( .await?; let stake_history_account = rpc_client.get_account(&stake_history::id()).await?; let clock_account = rpc_client.get_account(&sysvar::clock::id()).await?; + let rent_account = rpc_client.get_account(&sysvar::rent::id()).await?; let clock: Clock = from_account(&clock_account).ok_or_else(|| { CliError::RpcRequestError("Failed to deserialize clock sysvar".to_string()) })?; + let rent: Rent = rent_account.deserialize_data()?; let stake_history: StakeHistory = bincode::deserialize(&stake_history_account.data).map_err(|_| { CliError::RpcRequestError("Failed to deserialize stake history".to_string()) @@ -1721,9 +1723,7 @@ pub async fn process_show_stakes( Ensure that the account was fetched using a binary encoding.", ); if let Ok(stake_state) = stake_account.state() { - let rent_exempt_balance = rpc_client - .get_minimum_balance_for_rent_exemption(stake_account.data.len()) - .await?; + let rent_exempt_balance = rent.minimum_balance(stake_account.data.len()).max(1); match stake_state { StakeStateV2::Initialized(_) if vote_account_pubkeys.is_empty() => { From 86d9afb527df091ee669e1c20607c1ed4871120f Mon Sep 17 00:00:00 2001 From: Brennan Date: Sun, 31 May 2026 10:00:06 -0700 Subject: [PATCH 140/576] Epoch rewards AG fix (#12846) --- runtime/src/bank.rs | 4 +- runtime/src/bank/tests.rs | 8 +- .../block_component_processor/vote_reward.rs | 14 +++- .../epoch_inflation_account_state.rs | 82 ++++++++++++++----- 4 files changed, 79 insertions(+), 29 deletions(-) diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 70a02b297d3..17000d38807 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -1823,10 +1823,10 @@ impl Bank { // the vote reward account state should be created at the epoch boundary in which we // activate alpenglow as it will need info from the previous epoch. if self.feature_set.snapshot().alpenglow { + let epoch_start_capitalization = parent_capitalization; EpochInflationAccountState::new_epoch_update_account( self, - parent_epoch, - parent_capitalization, + epoch_start_capitalization, epoch_validator_rewards, ); } diff --git a/runtime/src/bank/tests.rs b/runtime/src/bank/tests.rs index 14e9d350479..98bb3310ba2 100644 --- a/runtime/src/bank/tests.rs +++ b/runtime/src/bank/tests.rs @@ -5257,9 +5257,9 @@ fn test_bank_hash_consistency(deprecate_rent_exemption_threshold: bool) { assert_eq!( bank.hash().to_string(), if deprecate_rent_exemption_threshold { - "9ycftRwjpQ17PrhnwhGPbVzDKd3Q9BybmLYU8UD1Pg1T" + "F4ns41hFn3ignVHqaVehaTZ8LMUxPaELAgvN8iTcowDD" } else { - "4XzjZMjhP9s8iBeFQsnHFwaQ991dpiBGHEkzj1ifAcpS" + "FyxeAqHjieaKMA6mLK3yfSD46Xcw2U6hzKfyeu1qVuCD" }, ); } @@ -5268,9 +5268,9 @@ fn test_bank_hash_consistency(deprecate_rent_exemption_threshold: bool) { assert_eq!( bank.hash().to_string(), if deprecate_rent_exemption_threshold { - "7sbqfkN4W3PkBREyduDc3R2eiKu68JKYRXZt6gCWNt4N" + "sF1tuUv3r5JCr9smYyhm9Qv27f5jcoJ75LnoLGV5Scd" } else { - "ArB3XNVLJsyV7AtrG3C3Bj4akb8s7AzpS5rHJnqGW5sw" + "DrpEs59czmKpLQvR9kBjSmWJUYMNKDdQ4Zsyu4WjfoPb" }, ); break; diff --git a/runtime/src/block_component_processor/vote_reward.rs b/runtime/src/block_component_processor/vote_reward.rs index 23f3b90e780..0cd732c96ff 100644 --- a/runtime/src/block_component_processor/vote_reward.rs +++ b/runtime/src/block_component_processor/vote_reward.rs @@ -700,13 +700,14 @@ mod tests { } fn calc_reward_for_test( - prev_bank: &Bank, bank: &Bank, + epoch_start_capitalization: u64, + reward_epoch: Epoch, total_stake: u64, stake_voted: u64, ) -> (u64, u64) { let epoch_inflation = - bank.calculate_epoch_inflation_rewards(prev_bank.capitalization(), prev_bank.epoch()); + bank.calculate_epoch_inflation_rewards(epoch_start_capitalization, reward_epoch); let numerator = epoch_inflation as u128 * stake_voted as u128; let denominator = bank.epoch_schedule.slots_per_epoch as u128 * total_stake as u128; let reward: u64 = (numerator / denominator).try_into().unwrap(); @@ -778,8 +779,15 @@ mod tests { .epoch_stakes_from_slot(reward_slot) .unwrap() .total_stake(); + let reward_epoch = bank.epoch_schedule.get_epoch(reward_slot); let (expected_validator_reward, expected_leader_reward_per_validator) = - calc_reward_for_test(&prev_bank, &bank, total_stake, per_validator_stake); + calc_reward_for_test( + &bank, + prev_bank.capitalization(), + reward_epoch, + total_stake, + per_validator_stake, + ); if *validator != leader_vote_pubkey { assert_eq!(got_reward, expected_validator_reward); } diff --git a/runtime/src/block_component_processor/vote_reward/epoch_inflation_account_state.rs b/runtime/src/block_component_processor/vote_reward/epoch_inflation_account_state.rs index ebe78660437..1c6c95d4de5 100644 --- a/runtime/src/block_component_processor/vote_reward/epoch_inflation_account_state.rs +++ b/runtime/src/block_component_processor/vote_reward/epoch_inflation_account_state.rs @@ -37,13 +37,12 @@ pub(super) struct EpochInflationState { impl EpochInflationState { fn new_from_bank( bank: &Bank, - prev_epoch: Epoch, - prev_epoch_capitalization: u64, + epoch_start_capitalization: u64, additional_validator_rewards: u64, ) -> Self { let max_possible_validator_reward = bank.calculate_epoch_inflation_rewards( - prev_epoch_capitalization + additional_validator_rewards, - prev_epoch, + epoch_start_capitalization + additional_validator_rewards, + bank.epoch(), ); EpochInflationState { max_possible_validator_reward, @@ -121,15 +120,13 @@ impl EpochInflationAccountState { /// compute the vote rewards. pub(crate) fn new_epoch_update_account( bank: &Bank, - prev_epoch: Epoch, - prev_epoch_capitalization: u64, + epoch_start_capitalization: u64, additional_validator_rewards: u64, ) { let prev = Self::new_from_bank(bank).map(|s| s.current); let current = EpochInflationState::new_from_bank( bank, - prev_epoch, - prev_epoch_capitalization, + epoch_start_capitalization, additional_validator_rewards, ); let state = Self { prev, current }; @@ -162,7 +159,9 @@ mod tests { }, }, rand::Rng, + solana_epoch_schedule::EpochSchedule, solana_genesis_config::GenesisConfig, + solana_inflation::Inflation, solana_leader_schedule::SlotLeader, std::sync::Arc, }; @@ -221,24 +220,67 @@ mod tests { assert_eq!(bank_epoch_1.epoch(), 1); assert_eq!(bank_epoch_2.epoch(), 2); - let expected_prev = EpochInflationState::new_from_bank( - &bank_epoch_1, - bank_epoch_0.epoch(), - bank_epoch_0.capitalization(), - 0, - ); - let expected_current = EpochInflationState::new_from_bank( - &bank_epoch_2, - bank_epoch_1.epoch(), - bank_epoch_1.capitalization(), - 0, - ); + let expected_prev = + EpochInflationState::new_from_bank(&bank_epoch_1, bank_epoch_0.capitalization(), 0); + let expected_current = + EpochInflationState::new_from_bank(&bank_epoch_2, bank_epoch_1.capitalization(), 0); let EpochInflationAccountState { current, prev } = EpochInflationAccountState::new_from_bank(&bank_epoch_2).unwrap(); assert_eq!(current, expected_current); assert_eq!(prev.unwrap(), expected_prev); } + #[test] + fn new_epoch_update_account_use_current_reward_budget() { + let GenesisConfigInfo { + mut genesis_config, .. + } = create_genesis_config(1_000_000_000); + genesis_config.inflation = Inflation::full(); + // Warmup is necessary here to give epoch 0 and epoch 1 different reward + // budgets. + genesis_config.epoch_schedule = EpochSchedule::custom(64, 64, true); + + let bank_forks = BankForks::new_rw_arc(Bank::new_for_tests(&genesis_config)); + let bank_epoch_0 = bank_forks.read().unwrap().root_bank(); + let epoch_1_first_slot = bank_epoch_0.epoch_schedule().get_first_slot_in_epoch(1); + let bank_epoch_1 = Bank::new_from_parent_with_bank_forks( + bank_forks.as_ref(), + bank_epoch_0.clone(), + SlotLeader::new_unique(), + epoch_1_first_slot, + ); + + assert_eq!(bank_epoch_0.epoch(), 0); + assert_eq!(bank_epoch_1.epoch(), 1); + assert_ne!( + bank_epoch_1.epoch_schedule().get_slots_in_epoch(0), + bank_epoch_1.epoch_schedule().get_slots_in_epoch(1) + ); + + let epoch_start_capitalization = bank_epoch_0.capitalization(); + let expected_current_epoch_rewards = bank_epoch_1 + .calculate_epoch_inflation_rewards(epoch_start_capitalization, bank_epoch_1.epoch()); + assert_ne!( + expected_current_epoch_rewards, + bank_epoch_1.calculate_epoch_inflation_rewards( + epoch_start_capitalization, + bank_epoch_0.epoch() + ) + ); + + EpochInflationAccountState::new_epoch_update_account( + &bank_epoch_1, + epoch_start_capitalization, + 0, + ); + let state = EpochInflationAccountState::new_from_bank(&bank_epoch_1).unwrap(); + assert_eq!(state.current.epoch, bank_epoch_1.epoch()); + assert_eq!( + state.current.max_possible_validator_reward, + expected_current_epoch_rewards + ); + } + #[test] fn handles_prefunded_account() { let GenesisConfigInfo { From 902625fb2cba41a6da48be2b35a58dc089a5dd98 Mon Sep 17 00:00:00 2001 From: steviez Date: Sun, 31 May 2026 13:24:04 -0500 Subject: [PATCH 141/576] streamer: Remove percentage crate dependency (#12851) We have the technology to do multiplication internally; no need to add the risk/overhead of another dependency for this --- Cargo.lock | 1 - dev-bins/Cargo.lock | 1 - programs/sbf/Cargo.lock | 1 - streamer/Cargo.toml | 1 - streamer/src/nonblocking/stream_throttle.rs | 15 +++++++-------- streamer/src/nonblocking/swqos.rs | 8 +++----- 6 files changed, 10 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ffbb64ca990..a2600a65b85 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10522,7 +10522,6 @@ dependencies = [ "nix", "num_cpus", "pem", - "percentage", "quinn", "rand 0.9.4", "rustls", diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 5e8d80d5772..37496d7a628 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -8829,7 +8829,6 @@ dependencies = [ "nix", "num_cpus", "pem", - "percentage", "quinn", "rand 0.9.4", "rustls", diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index 4f80fb6920d..ef2f98b67b8 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -9350,7 +9350,6 @@ dependencies = [ "nix", "num_cpus", "pem", - "percentage", "quinn", "rand 0.9.4", "rustls", diff --git a/streamer/Cargo.toml b/streamer/Cargo.toml index 9800d5b33aa..e33dda5a727 100644 --- a/streamer/Cargo.toml +++ b/streamer/Cargo.toml @@ -33,7 +33,6 @@ log = { workspace = true } nix = { workspace = true, features = ["net", "poll", "signal"] } num_cpus = { workspace = true } pem = { workspace = true } -percentage = { workspace = true } quinn = { workspace = true } rand = { workspace = true } rustls = { workspace = true } diff --git a/streamer/src/nonblocking/stream_throttle.rs b/streamer/src/nonblocking/stream_throttle.rs index f4056b673ed..413139234b1 100644 --- a/streamer/src/nonblocking/stream_throttle.rs +++ b/streamer/src/nonblocking/stream_throttle.rs @@ -3,7 +3,6 @@ use { nonblocking::{qos::OpaqueStreamerCounter, quic::ConnectionPeerType}, quic::StreamerStats, }, - percentage::Percentage, std::{ sync::{ Arc, RwLock, @@ -16,8 +15,8 @@ use { /// Max TPS allowed for unstaked connection const MAX_UNSTAKED_TPS: u64 = 200; -/// Expected % of max TPS to be consumed by unstaked connections -const EXPECTED_UNSTAKED_STREAMS_PERCENT: u64 = 20; +/// Expected fraction of max TPS to be consumed by unstaked connections +const EXPECTED_UNSTAKED_STREAMS_RATIO: f64 = 0.20; pub const STREAM_THROTTLING_INTERVAL_MS: u64 = 100; pub const STREAM_THROTTLING_INTERVAL: Duration = @@ -30,7 +29,7 @@ const STREAM_LOAD_EMA_INTERVAL_MS: u64 = 5; // before throttling activates. const STREAM_LOAD_EMA_INTERVAL_COUNT: u64 = 40; -const STAKED_THROTTLING_ON_LOAD_THRESHOLD_PERCENT: u64 = 95; +const STAKED_THROTTLING_ON_LOAD_THRESHOLD_RATIO: f64 = 0.95; pub(crate) struct StakedStreamLoadEMA { current_load_ema: AtomicU64, @@ -53,7 +52,7 @@ impl StakedStreamLoadEMA { let allow_unstaked_streams = max_unstaked_connections > 0; let max_staked_load_in_ms = if allow_unstaked_streams { max_streams_per_ms - - Percentage::from(EXPECTED_UNSTAKED_STREAMS_PERCENT).apply_to(max_streams_per_ms) + - ((EXPECTED_UNSTAKED_STREAMS_RATIO * (max_streams_per_ms as f64)) as u64) } else { max_streams_per_ms }; @@ -68,9 +67,9 @@ impl StakedStreamLoadEMA { 0 }; - let staked_throttling_on_load_threshold = - Percentage::from(STAKED_THROTTLING_ON_LOAD_THRESHOLD_PERCENT) - .apply_to(max_staked_load_in_ema_interval); + let staked_throttling_on_load_threshold = (STAKED_THROTTLING_ON_LOAD_THRESHOLD_RATIO + * (max_staked_load_in_ema_interval as f64)) + as u64; Self { current_load_ema: AtomicU64::default(), diff --git a/streamer/src/nonblocking/swqos.rs b/streamer/src/nonblocking/swqos.rs index c84e6b2a025..8ec246f5c60 100644 --- a/streamer/src/nonblocking/swqos.rs +++ b/streamer/src/nonblocking/swqos.rs @@ -20,7 +20,6 @@ use { }, streamer::StakedNodes, }, - percentage::Percentage, quinn::{Connection, VarInt}, solana_time_utils as timing, std::{ @@ -246,10 +245,9 @@ impl SwQos { stats: Arc, ) { if unstaked_connection_table.total_size >= max_unstaked_connections { - const PRUNE_TABLE_TO_PERCENTAGE: u8 = 90; - let max_percentage_full = Percentage::from(PRUNE_TABLE_TO_PERCENTAGE); - - let max_connections = max_percentage_full.apply_to(max_unstaked_connections); + // Prune the connection table down to 90% capacity + const PRUNE_TABLE_RATIO: f64 = 0.90; + let max_connections = (PRUNE_TABLE_RATIO * (max_unstaked_connections as f64)) as usize; let num_pruned = unstaked_connection_table.prune_oldest(max_connections); stats .num_evictions_unstaked From af0858cbd1442f4b9e0b5e29d9b3e7af5f38a3b9 Mon Sep 17 00:00:00 2001 From: Kamil Skalski Date: Mon, 1 Jun 2026 11:08:10 +0200 Subject: [PATCH 142/576] chore(deps): bump solana-message from 4.1.1 to 4.2.0 (#12793) * chore(deps): bump solana-message from 4.1.1 to 4.2.0 Bumps [solana-message](https://github.com/anza-xyz/solana-sdk) from 4.1.1 to 4.2.0. - [Release notes](https://github.com/anza-xyz/solana-sdk/releases) - [Commits](https://github.com/anza-xyz/solana-sdk/compare/message@v4.1.1...hash@v4.2.0) --- updated-dependencies: - dependency-name: solana-message dependency-version: 4.2.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * Update all workspaces * fix deprecated message serialize call * Update offset in test to accomodate new format --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- dev-bins/Cargo.lock | 4 ++-- install/Cargo.toml | 2 +- keygen/Cargo.toml | 2 +- programs/sbf/Cargo.lock | 4 ++-- transaction-view/src/instructions_frame.rs | 4 ++-- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a2600a65b85..99ca8f13195 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8979,9 +8979,9 @@ dependencies = [ [[package]] name = "solana-message" -version = "4.1.1" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a634d1db65a393d4e87ec49ef97c8dfb12201e2ba9e5faf87ff34e632f29e509" +checksum = "a81a5ae2aa5c27925de70bc94fc42f1ca80a46c951ae101b419aa4eac9abb56a" dependencies = [ "blake3", "lazy_static", diff --git a/Cargo.toml b/Cargo.toml index 8fc83172c10..638efe31c17 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -418,7 +418,7 @@ solana-loader-v4-interface = "3.1.0" solana-local-cluster = { path = "local-cluster", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-measure = { path = "measure", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-merkle-tree = { path = "merkle-tree", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-message = "4.1.1" +solana-message = "4.2.0" solana-metrics = { path = "metrics", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-msg = "3.1.0" solana-native-token = "3.0.0" diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 37496d7a628..d96f24e1976 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -7598,9 +7598,9 @@ dependencies = [ [[package]] name = "solana-message" -version = "4.1.1" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a634d1db65a393d4e87ec49ef97c8dfb12201e2ba9e5faf87ff34e632f29e509" +checksum = "a81a5ae2aa5c27925de70bc94fc42f1ca80a46c951ae101b419aa4eac9abb56a" dependencies = [ "blake3", "lazy_static", diff --git a/install/Cargo.toml b/install/Cargo.toml index af061341bbf..f7ea930d771 100644 --- a/install/Cargo.toml +++ b/install/Cargo.toml @@ -43,7 +43,7 @@ solana-clap-utils = { workspace = true } solana-config-interface = { version = "=2.0.0", features = ["bincode"] } solana-hash = "=4.3.0" solana-keypair = "=3.1.2" -solana-message = "=4.1.1" +solana-message = "=4.2.0" solana-pubkey = { version = "=4.2.0", default-features = false } solana-rpc-client = { workspace = true } solana-sha256-hasher = { workspace = true } diff --git a/keygen/Cargo.toml b/keygen/Cargo.toml index 9531942ccca..aa270a8a7cb 100644 --- a/keygen/Cargo.toml +++ b/keygen/Cargo.toml @@ -35,7 +35,7 @@ solana-cli-config = { workspace = true } solana-derivation-path = { workspace = true } solana-instruction = { version = "=3.4.0", features = ["bincode"] } solana-keypair = "=3.1.2" -solana-message = { version = "=4.1.1", features = ["wincode"] } +solana-message = { version = "=4.2.0", features = ["wincode"] } solana-pubkey = { version = "=4.2.0", default-features = false } solana-remote-wallet = { workspace = true } solana-seed-derivable = { workspace = true } diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index ef2f98b67b8..ae8774f9302 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -7364,9 +7364,9 @@ dependencies = [ [[package]] name = "solana-message" -version = "4.1.1" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a634d1db65a393d4e87ec49ef97c8dfb12201e2ba9e5faf87ff34e632f29e509" +checksum = "a81a5ae2aa5c27925de70bc94fc42f1ca80a46c951ae101b419aa4eac9abb56a" dependencies = [ "blake3", "lazy_static", diff --git a/transaction-view/src/instructions_frame.rs b/transaction-view/src/instructions_frame.rs index 12fdf5f8c5f..4a91437a1f1 100644 --- a/transaction-view/src/instructions_frame.rs +++ b/transaction-view/src/instructions_frame.rs @@ -558,9 +558,9 @@ mod tests { ..solana_message::v1::Message::default() }; - let serialized = solana_message::v1::serialize(&message); + let serialized = solana_message::v1::Message::serialize(&message); - let mut offset = 41; // instruction headers starts at offset 41 for no config, 0 addresses, per spec + let mut offset = 42; // 1-byte V1_PREFIX + 41-byte header (no config, 0 addresses), per spec let instructions_frame = InstructionsFrame::try_new_for_v1(&serialized, &mut offset, 2).unwrap(); From e65fcda48062837a498f70ffe10e3c7927bedfeb Mon Sep 17 00:00:00 2001 From: Alex Pyattaev Date: Mon, 1 Jun 2026 12:20:37 +0300 Subject: [PATCH 143/576] chore: rm last unbounded channel in gossip (#12869) --- gossip/src/cluster_info.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gossip/src/cluster_info.rs b/gossip/src/cluster_info.rs index 1a35aba1f92..d33483482d5 100644 --- a/gossip/src/cluster_info.rs +++ b/gossip/src/cluster_info.rs @@ -2740,7 +2740,7 @@ mod tests { .collect(); let recycler = PacketBatchRecycler::default(); let packets = { - let (sender, receiver) = crossbeam_channel::unbounded(); + let (sender, receiver) = crossbeam_channel::bounded(1024); cluster_info.handle_batch_ping_messages( remote_nodes.iter().map(|(_, socket)| socket).zip(pings), &recycler, From 309e874c917caf7d53d736cfb21c34438137c927 Mon Sep 17 00:00:00 2001 From: Alex Pyattaev Date: Mon, 1 Jun 2026 13:46:22 +0300 Subject: [PATCH 144/576] refactor: move get_remote_pubkey to tls-utils (#12805) --- Cargo.lock | 1 + dev-bins/Cargo.lock | 1 + programs/sbf/Cargo.lock | 1 + streamer/src/nonblocking/quic.rs | 13 +------------ tls-utils/Cargo.toml | 1 + tls-utils/src/tls_certificates.rs | 16 ++++++++++++++++ 6 files changed, 21 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 99ca8f13195..50194df2838 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10916,6 +10916,7 @@ checksum = "0ced92c60aa76ec4780a9d93f3bd64dfa916e1b998eacc6f1c110f3f444f02c9" name = "solana-tls-utils" version = "4.2.0-alpha.0" dependencies = [ + "quinn", "rustls", "solana-keypair", "solana-pubkey 4.2.0", diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index d96f24e1976..a9cc5d5f9ed 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -9161,6 +9161,7 @@ checksum = "0ced92c60aa76ec4780a9d93f3bd64dfa916e1b998eacc6f1c110f3f444f02c9" name = "solana-tls-utils" version = "4.2.0-alpha.0" dependencies = [ + "quinn", "rustls", "solana-keypair", "solana-pubkey 4.2.0", diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index ae8774f9302..228b630c9ad 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -9684,6 +9684,7 @@ checksum = "0ced92c60aa76ec4780a9d93f3bd64dfa916e1b998eacc6f1c110f3f444f02c9" name = "solana-tls-utils" version = "4.2.0-alpha.0" dependencies = [ + "quinn", "rustls", "solana-keypair", "solana-pubkey 4.2.0", diff --git a/streamer/src/nonblocking/quic.rs b/streamer/src/nonblocking/quic.rs index d21d95b53c2..dc589f3fdc8 100644 --- a/streamer/src/nonblocking/quic.rs +++ b/streamer/src/nonblocking/quic.rs @@ -22,7 +22,7 @@ use { solana_packet::Meta, solana_perf::packet::{BytesPacket, PacketBatch}, solana_pubkey::Pubkey, - solana_tls_utils::get_pubkey_from_tls_certificate, + solana_tls_utils::get_remote_pubkey, std::{ array, fmt, iter::repeat_with, @@ -413,17 +413,6 @@ where tasks.wait().await; } -pub fn get_remote_pubkey(connection: &Connection) -> Option { - // Use the client cert only if it is self signed and the chain length is 1. - connection - .peer_identity()? - .downcast::>() - .ok() - .filter(|certs| certs.len() == 1)? - .first() - .and_then(get_pubkey_from_tls_certificate) -} - pub fn get_connection_stake( connection: &Connection, staked_nodes: &RwLock, diff --git a/tls-utils/Cargo.toml b/tls-utils/Cargo.toml index 6afd2b351f3..d0a76f2d775 100644 --- a/tls-utils/Cargo.toml +++ b/tls-utils/Cargo.toml @@ -13,6 +13,7 @@ edition = { workspace = true } agave-unstable-api = [] [dependencies] +quinn = { workspace = true } rustls = { workspace = true, features = ["ring"] } solana-keypair = { workspace = true } solana-pubkey = { workspace = true } diff --git a/tls-utils/src/tls_certificates.rs b/tls-utils/src/tls_certificates.rs index 4963003e406..3f8544f86a7 100644 --- a/tls-utils/src/tls_certificates.rs +++ b/tls-utils/src/tls_certificates.rs @@ -127,6 +127,22 @@ pub fn get_pubkey_from_tls_certificate( } } +/// Recover the peer's Solana pubkey from a `quinn::Connection`. Accepts +/// only a self-signed cert chain of length 1 (matches the cert shape +/// produced by [`new_dummy_x509_certificate`]). +/// +/// Single source of truth for anything doing QUIC and using +/// Solana validator identities for auth. +pub fn get_remote_pubkey(connection: &quinn::Connection) -> Option { + connection + .peer_identity()? + .downcast::>() + .ok() + .filter(|certs| certs.len() == 1)? + .first() + .and_then(get_pubkey_from_tls_certificate) +} + /// Translate a SocketAddr into a valid SNI for the purposes of QUIC connection /// /// We do not actually check if the server holds a cert for this server_name From f8bc56ec839edc6a81facb10bace51e2634badd9 Mon Sep 17 00:00:00 2001 From: Yihau Chen Date: Mon, 1 Jun 2026 19:46:45 +0800 Subject: [PATCH 145/576] ci: improve docker building process (#12821) * check sccache * check nextest * check grcov * don't store checksum * use --ignore-missing --- ci/docker/Dockerfile | 10 +++++++++- ci/docker/build.sh | 1 + ci/docker/env.sh | 2 ++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/ci/docker/Dockerfile b/ci/docker/Dockerfile index dc939da4398..cb451c320e8 100644 --- a/ci/docker/Dockerfile +++ b/ci/docker/Dockerfile @@ -6,6 +6,7 @@ ARG \ RUST_NIGHTLY_VERSION= \ NODE_MAJOR= \ SCCACHE_VERSION= \ + NEXTEST_VERSION= \ GRCOV_VERSION= \ CODECOV_VERSION= @@ -26,6 +27,7 @@ RUN \ if [ -z "$RUST_NIGHTLY_VERSION" ]; then echo "ERROR: The RUST_NIGHTLY_VERSION argument is required!" && exit 1; fi && \ if [ -z "$NODE_MAJOR" ]; then echo "ERROR: The NODE_MAJOR argument is required!" && exit 1; fi && \ if [ -z "$SCCACHE_VERSION" ]; then echo "ERROR: The SCCACHE_VERSION argument is required!" && exit 1; fi && \ + if [ -z "$NEXTEST_VERSION" ]; then echo "ERROR: The NEXTEST_VERSION argument is required!" && exit 1; fi && \ if [ -z "$GRCOV_VERSION" ]; then echo "ERROR: The GRCOV_VERSION argument is required!" && exit 1; fi && \ if [ -z "$CODECOV_VERSION" ]; then echo "ERROR: The CODECOV_VERSION argument is required!" && exit 1; fi && \ apt-get update && \ @@ -98,12 +100,17 @@ RUN \ rm -rf $CARGO_HOME/registry && \ # sccache curl -LOsS "https://github.com/mozilla/sccache/releases/download/$SCCACHE_VERSION/sccache-$SCCACHE_VERSION-x86_64-unknown-linux-musl.tar.gz" && \ + echo "$(curl -LsS "https://github.com/mozilla/sccache/releases/download/$SCCACHE_VERSION/sccache-$SCCACHE_VERSION-x86_64-unknown-linux-musl.tar.gz.sha256") sccache-$SCCACHE_VERSION-x86_64-unknown-linux-musl.tar.gz" | sha256sum -c - && \ tar -xzf "sccache-$SCCACHE_VERSION-x86_64-unknown-linux-musl.tar.gz" && \ mv "sccache-$SCCACHE_VERSION-x86_64-unknown-linux-musl"/sccache "$CARGO_HOME/bin/" && \ rm "sccache-$SCCACHE_VERSION-x86_64-unknown-linux-musl.tar.gz" && \ rm -rf "sccache-$SCCACHE_VERSION-x86_64-unknown-linux-musl" && \ # nextest - curl -LsSf https://get.nexte.st/latest/linux | tar zxf - -C "$CARGO_HOME/bin" && \ + curl -LOsS "https://github.com/nextest-rs/nextest/releases/download/cargo-nextest-$NEXTEST_VERSION/cargo-nextest-$NEXTEST_VERSION-x86_64-unknown-linux-musl.tar.gz" && \ + curl -LsS "https://github.com/nextest-rs/nextest/releases/download/cargo-nextest-$NEXTEST_VERSION/cargo-nextest-$NEXTEST_VERSION-x86_64-unknown-linux-musl.sha256" | sha256sum -c - && \ + tar -xzf "cargo-nextest-$NEXTEST_VERSION-x86_64-unknown-linux-musl.tar.gz" && \ + mv ./cargo-nextest "$CARGO_HOME/bin/" && \ + rm "cargo-nextest-$NEXTEST_VERSION-x86_64-unknown-linux-musl.tar.gz" && \ # nodejs sudo mkdir -p /etc/apt/keyrings && \ curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \ @@ -119,6 +126,7 @@ RUN \ chmod -R a+w /.npm && \ # grcov curl -LOsS "https://github.com/mozilla/grcov/releases/download/$GRCOV_VERSION/grcov-x86_64-unknown-linux-musl.tar.bz2" && \ + curl -LsS "https://github.com/mozilla/grcov/releases/download/$GRCOV_VERSION/checksums.sha256" | sha256sum -c --ignore-missing - && \ tar -xf grcov-x86_64-unknown-linux-musl.tar.bz2 && \ mv ./grcov $CARGO_HOME/bin && \ rm grcov-x86_64-unknown-linux-musl.tar.bz2 && \ diff --git a/ci/docker/build.sh b/ci/docker/build.sh index 1593d3406a1..6d31f301672 100755 --- a/ci/docker/build.sh +++ b/ci/docker/build.sh @@ -21,6 +21,7 @@ docker build "${platform[@]}" \ --build-arg "RUST_NIGHTLY_VERSION=${CI_DOCKER_ARG_RUST_NIGHTLY_VERSION}" \ --build-arg "NODE_MAJOR=${CI_DOCKER_ARG_NODE_MAJOR}" \ --build-arg "SCCACHE_VERSION=${CI_DOCKER_ARG_SCCACHE_VERSION}" \ + --build-arg "NEXTEST_VERSION=${CI_DOCKER_ARG_NEXTEST_VERSION}" \ --build-arg "GRCOV_VERSION=${CI_DOCKER_ARG_GRCOV_VERSION}" \ --build-arg "CODECOV_VERSION=${CI_DOCKER_ARG_CODECOV_VERSION}" \ -t "$CI_DOCKER_IMAGE" . diff --git a/ci/docker/env.sh b/ci/docker/env.sh index fda575ccc85..da1d6ce8e1a 100755 --- a/ci/docker/env.sh +++ b/ci/docker/env.sh @@ -15,6 +15,7 @@ export CI_DOCKER_ARG_RUST_VERSION="${rust_stable}" export CI_DOCKER_ARG_RUST_NIGHTLY_VERSION="${rust_nightly}" export CI_DOCKER_ARG_NODE_MAJOR=24 export CI_DOCKER_ARG_SCCACHE_VERSION=v0.9.1 +export CI_DOCKER_ARG_NEXTEST_VERSION=0.9.137 export CI_DOCKER_ARG_GRCOV_VERSION=v0.8.18 export CI_DOCKER_ARG_CODECOV_VERSION=v0.8.0 @@ -25,6 +26,7 @@ hash_vars=( "${CI_DOCKER_ARG_RUST_NIGHTLY_VERSION}" "${CI_DOCKER_ARG_NODE_MAJOR}" "${CI_DOCKER_ARG_SCCACHE_VERSION}" + "${CI_DOCKER_ARG_NEXTEST_VERSION}" "${CI_DOCKER_ARG_GRCOV_VERSION}" "${CI_DOCKER_ARG_CODECOV_VERSION}" ) From d5e1896411b652d1087b8e2d92ec3ffa2429c43b Mon Sep 17 00:00:00 2001 From: Akhilesh Singhania Date: Mon, 1 Jun 2026 15:26:42 +0200 Subject: [PATCH 146/576] genesis_utils: Inserts a valid genesis cert bitmap (#12874) Inserts a valid genesis cert bitmap --- dev-bins/Cargo.lock | 1 + programs/sbf/Cargo.lock | 1 + runtime/Cargo.toml | 1 + runtime/src/genesis_utils.rs | 4 +++- 4 files changed, 6 insertions(+), 1 deletion(-) diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index a9cc5d5f9ed..113b136a0eb 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -8331,6 +8331,7 @@ dependencies = [ "assert_matches", "base64 0.22.1", "bincode", + "bitvec", "bytemuck", "crossbeam-channel", "dashmap", diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index 228b630c9ad..8f0434feb4d 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -8138,6 +8138,7 @@ dependencies = [ "assert_matches", "base64 0.22.1", "bincode", + "bitvec", "bytemuck", "crossbeam-channel", "dashmap", diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index d7e446904a3..3d98a6e644a 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -65,6 +65,7 @@ arrayref = { workspace = true } assert_matches = { workspace = true } base64 = { workspace = true } bincode = { workspace = true } +bitvec = { workspace = true } bytemuck = { workspace = true } crossbeam-channel = { workspace = true } dashmap = { workspace = true, features = ["rayon", "raw-api", "serde"] } diff --git a/runtime/src/genesis_utils.rs b/runtime/src/genesis_utils.rs index d6dbca2c338..cdc620a7496 100644 --- a/runtime/src/genesis_utils.rs +++ b/runtime/src/genesis_utils.rs @@ -14,6 +14,7 @@ use { migration::GENESIS_CERTIFICATE_ACCOUNT, }, bincode::serialize, + bitvec::vec::BitVec, log::*, solana_account::{Account, AccountSharedData, ReadableAccount, state_traits::StateMut}, solana_bls_signatures::{ @@ -34,6 +35,7 @@ use { solana_sdk_ids::{stake as stake_program, sysvar}, solana_seed_derivable::SeedDerivable, solana_signer::Signer, + solana_signer_store::encode_base2, solana_stake_interface::state::{Authorized, Lockup, Meta, StakeStateV2}, solana_system_interface::program as system_program, solana_sysvar::{ @@ -325,7 +327,7 @@ pub fn activate_all_features_alpenglow(genesis_config: &mut GenesisConfig) { let cert = Certificate { cert_type: CertificateType::Genesis(0, Hash::default()), signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), - bitmap: Vec::default(), + bitmap: encode_base2(&BitVec::new()).unwrap(), }; let cert_size = bincode::serialized_size(&cert).unwrap(); let lamports = Rent::default().minimum_balance(cert_size as usize); From 69ce7147ea3dfc54ec3e77bc5c70df44b74791c5 Mon Sep 17 00:00:00 2001 From: Kamil Skalski Date: Mon, 1 Jun 2026 15:38:33 +0200 Subject: [PATCH 147/576] fix(fs): allow repeated IoUringFileWriter flush (#12873) --- fs/src/io_uring/file_writer.rs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/fs/src/io_uring/file_writer.rs b/fs/src/io_uring/file_writer.rs index 46ae5efa1a2..50baa5c6b4c 100644 --- a/fs/src/io_uring/file_writer.rs +++ b/fs/src/io_uring/file_writer.rs @@ -99,16 +99,13 @@ impl Write for IoUringFileWriter<'_> { } } - /// Flush can only be called once. - /// Flush will close the underlying file. + /// Flush will close the underlying file. Writes are not allowed after flush, + /// and subsequent flushes do nothing. fn flush(&mut self) -> Result<()> { - // @TODO -- once file creator supports multiple partial writes we - // can support multiple flushes + // Since `write` is disallowed after finalization, no new data can arrive + // once we've flushed and this call can be safely ignored. if self.finalized { - return Err(io::Error::new( - io::ErrorKind::Unsupported, - "file writer has already been finalized by flush", - )); + return Ok(()); } // Handle the case where we are flushing after the buffer has already been dispatched during a write. @@ -217,7 +214,7 @@ mod tests { } #[test] - fn test_allow_only_one_flush() { + fn test_no_writes_after_flush_but_flush_is_idempotent() { let temp_dir = tempfile::tempdir().unwrap(); let dir = Arc::new(File::open(temp_dir.path()).unwrap()); let file_path = temp_dir.path().join("test.txt"); @@ -228,7 +225,8 @@ mod tests { let mut io_uring_file_writer = IoUringFileWriter::new(io_uring_file_creator, file_path.clone(), 0o666, dir).unwrap(); io_uring_file_writer.flush().unwrap(); + // Writes are rejected after flush, but a subsequent flush is a no-op and succeeds. assert!(io_uring_file_writer.write(&[0]).is_err()); - assert!(io_uring_file_writer.flush().is_err()); + io_uring_file_writer.flush().unwrap(); } } From 06e0f490d1cf36852e624f028b87f2f37800a30a Mon Sep 17 00:00:00 2001 From: Brennan Date: Mon, 1 Jun 2026 06:48:46 -0700 Subject: [PATCH 148/576] Slot range duration plumbing (#12857) slot range duration --- core/src/block_creation_loop.rs | 15 ++---- runtime/src/bank.rs | 18 ++++--- runtime/src/bank/tests.rs | 6 +++ runtime/src/block_component_processor.rs | 63 +++++++++--------------- runtime/src/slot_params.rs | 54 ++++++++++++++++---- 5 files changed, 91 insertions(+), 65 deletions(-) diff --git a/core/src/block_creation_loop.rs b/core/src/block_creation_loop.rs index a1f07c8ce5f..9e1387b4808 100644 --- a/core/src/block_creation_loop.rs +++ b/core/src/block_creation_loop.rs @@ -469,18 +469,14 @@ fn select_freshest_window( /// Clamps the block producer timestamp to ensure that the leader produces a timestamp that conforms /// to Alpenglow clock bounds. fn skew_block_producer_time_nanos( - parent_slot: Slot, parent_time_nanos: i64, - working_bank_slot: Slot, working_bank_time_nanos: i64, - ns_per_slot: u64, + elapsed_slot_duration_nanos: u128, ) -> i64 { let (min_working_bank_time, max_working_bank_time) = BlockComponentProcessor::nanosecond_time_bounds( - parent_slot, parent_time_nanos, - working_bank_slot, - ns_per_slot, + elapsed_slot_duration_nanos, ); working_bank_time_nanos @@ -509,14 +505,13 @@ fn produce_block_footer( .get_nanosecond_clock() .unwrap_or_else(|| bank.clock().unix_timestamp.saturating_mul(1_000_000_000)); let parent_slot = parent_bank.slot(); - let ns_per_slot = u64::try_from(bank.ns_per_slot_at_slot(slot)).unwrap_or(u64::MAX); + let elapsed_slot_duration_nanos = + bank.slot_range_duration_nanos(parent_slot.saturating_add(1), slot); block_producer_time_nanos = skew_block_producer_time_nanos( - parent_slot, parent_time_nanos, - slot, block_producer_time_nanos, - ns_per_slot, + elapsed_slot_duration_nanos, ); } diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 17000d38807..913a9d84379 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -2755,22 +2755,28 @@ impl Bank { let mut duration = 0.0; for (effective_slot, effective_params) in self.slot_params.param_transitions() { - if *effective_slot <= start_slot { - params = *effective_params; + if effective_slot <= start_slot { + params = effective_params; continue; } - if *effective_slot >= end_slot { + if effective_slot >= end_slot { break; } - duration += (*effective_slot - cursor) as f64 / params.slots_per_year(); - cursor = *effective_slot; - params = *effective_params; + duration += (effective_slot - cursor) as f64 / params.slots_per_year(); + cursor = effective_slot; + params = effective_params; } duration + (end_slot - cursor) as f64 / params.slots_per_year() } + /// Returns the exact wall-clock duration in nanoseconds for `start_slot..=end_slot`. + pub fn slot_range_duration_nanos(&self, start_slot: Slot, end_slot: Slot) -> u128 { + self.slot_params + .slot_range_duration_nanos(start_slot, end_slot) + } + pub fn epoch_duration_in_years(&self, epoch: Epoch) -> f64 { // period: time that has passed as a fraction of a year, basically the length of // an epoch as a fraction of a year diff --git a/runtime/src/bank/tests.rs b/runtime/src/bank/tests.rs index 98bb3310ba2..0352704a421 100644 --- a/runtime/src/bank/tests.rs +++ b/runtime/src/bank/tests.rs @@ -6656,6 +6656,12 @@ fn test_slot_params_use_genesis_baseline_without_features() { bank.slot_range_duration_in_years(0, 64).to_bits(), (64.0 / genesis_slots_per_year).to_bits() ); + assert_eq!(bank.slot_range_duration_nanos(1, 0), 0); + assert_eq!(bank.slot_range_duration_nanos(0, 0), genesis_ns_per_slot); + assert_eq!( + bank.slot_range_duration_nanos(0, 63), + 64 * genesis_ns_per_slot + ); assert_eq!( bank.partitioned_rewards_stake_account_stores_per_block, stake_account_stores_per_block diff --git a/runtime/src/block_component_processor.rs b/runtime/src/block_component_processor.rs index 5fca47fb5b9..42da04a0d2e 100644 --- a/runtime/src/block_component_processor.rs +++ b/runtime/src/block_component_processor.rs @@ -456,10 +456,11 @@ impl BlockComponentProcessor { let current_time_nanos = Self::block_producer_time_nanos_as_i64(footer.block_producer_time_nanos)?; let current_slot = bank.slot(); - let ns_per_slot = bank.ns_per_slot.try_into().expect("ns_per_slot overflow"); + let elapsed_slot_duration_nanos = + bank.slot_range_duration_nanos(parent_slot.saturating_add(1), current_slot); let (lower_bound_nanos, upper_bound_nanos) = - Self::nanosecond_time_bounds(parent_slot, parent_time_nanos, current_slot, ns_per_slot); + Self::nanosecond_time_bounds(parent_time_nanos, elapsed_slot_duration_nanos); let is_valid = lower_bound_nanos <= current_time_nanos && current_time_nanos <= upper_bound_nanos; @@ -483,26 +484,24 @@ impl BlockComponentProcessor { .map_err(|_| BlockComponentProcessorError::NanosecondClockOutOfBounds) } - /// Given the parent slot, parent time, slot, and default nanoseconds per - /// slot, calculate the lower and upper bounds for the block producer time. - /// We return (lower_bound, upper_bound), where both bounds are inclusive. - /// I.e., the working bank time is valid if lower_bound <= working_bank_time - /// <= upper_bound. + /// Given a parent time and elapsed slot duration, calculates inclusive + /// block producer timestamp bounds. + /// + /// `parent_time_nanos` describes the parent bank's nanosecond clock. + /// `elapsed_slot_duration_nanos` is the summed duration for all skipped + /// and working slots after the parent. The returned `(lower_bound, + /// upper_bound)` accepts timestamps where + /// `lower_bound <= working_bank_time <= upper_bound`. /// /// Refer to /// https://github.com/solana-foundation/solana-improvement-documents/pull/363 /// for details on the bounds calculation. pub fn nanosecond_time_bounds( - parent_slot: Slot, parent_time_nanos: i64, - slot: Slot, - ns_per_slot: u64, + elapsed_slot_duration_nanos: u128, ) -> (i64, i64) { - let diff_slots = slot.saturating_sub(parent_slot); - let min_working_bank_time = parent_time_nanos.saturating_add(1); - let max_working_bank_time_offset = u128::from(diff_slots) - .saturating_mul(u128::from(ns_per_slot)) + let max_working_bank_time_offset = elapsed_slot_duration_nanos .saturating_mul(2) .min(i64::MAX as u128) as i64; let max_working_bank_time = parent_time_nanos.saturating_add(max_working_bank_time_offset); @@ -1086,14 +1085,13 @@ mod tests { let parent_slot = parent.slot(); let parent_time_nanos = parent.clock().unix_timestamp.saturating_mul(1_000_000_000); let current_slot = bank.slot(); - let ns_per_slot = bank.ns_per_slot.try_into().expect("ns_per_slot overflow"); + let elapsed_slot_duration_nanos = + bank.slot_range_duration_nanos(parent_slot.saturating_add(1), current_slot); // Use a timestamp in the middle of the valid range let (lower_bound, upper_bound) = BlockComponentProcessor::nanosecond_time_bounds( - parent_slot, parent_time_nanos, - current_slot, - ns_per_slot, + elapsed_slot_duration_nanos, ); let footer_time_nanos = (lower_bound + upper_bound) / 2; let expected_time_secs = footer_time_nanos / 1_000_000_000; @@ -1136,13 +1134,11 @@ mod tests { parent.update_clock_from_footer(parent_time_nanos); let bank: Arc = create_child_bank(&bank_forks, &parent, slot_gap); - let ns_per_slot = bank.ns_per_slot.try_into().expect("ns_per_slot overflow"); + let elapsed_slot_duration_nanos = bank.slot_range_duration_nanos(1, slot_gap); let (lower_bound, upper_bound) = BlockComponentProcessor::nanosecond_time_bounds( - 0, parent_time_nanos, - slot_gap, - ns_per_slot, + elapsed_slot_duration_nanos, ); let footer_time_nanos = timestamp_fn(parent_time_nanos, lower_bound, upper_bound); @@ -1235,18 +1231,14 @@ mod tests { // Helper function to test nanosecond_time_bounds calculation fn test_nanosecond_time_bounds_helper( - parent_slot: u64, parent_time_nanos: i64, - working_slot: u64, - ns_per_slot: u64, + elapsed_slot_duration_nanos: u128, expected_lower: i64, expected_upper: i64, ) { let (lower, upper) = BlockComponentProcessor::nanosecond_time_bounds( - parent_slot, parent_time_nanos, - working_slot, - ns_per_slot, + elapsed_slot_duration_nanos, ); assert_eq!(lower, expected_lower); @@ -1264,10 +1256,8 @@ mod tests { let working_slot = 15; let slot_delta = working_slot - parent_slot; test_nanosecond_time_bounds_helper( - parent_slot, parent_time, - working_slot, - DEFAULT_NS_PER_SLOT, + u128::from(slot_delta).saturating_mul(u128::from(DEFAULT_NS_PER_SLOT)), parent_time + 1, parent_time + (2 * DEFAULT_NS_PER_SLOT * slot_delta) as i64, ); @@ -1282,21 +1272,14 @@ mod tests { // Note: In this case, lower > upper, so no timestamp would be valid // This is expected since we shouldn't have the same slot for parent and working bank let parent_time = 1_000_000_000_000; - test_nanosecond_time_bounds_helper( - 10, - parent_time, - 10, - DEFAULT_NS_PER_SLOT, - parent_time + 1, - parent_time, - ); + test_nanosecond_time_bounds_helper(parent_time, 0, parent_time + 1, parent_time); } #[test] fn test_nanosecond_time_bounds_saturates_upper_bound() { let parent_time = i64::MAX - 5; let (lower, upper) = - BlockComponentProcessor::nanosecond_time_bounds(0, parent_time, u64::MAX, u64::MAX); + BlockComponentProcessor::nanosecond_time_bounds(parent_time, u128::MAX); assert_eq!(lower, parent_time + 1); assert_eq!(upper, i64::MAX); diff --git a/runtime/src/slot_params.rs b/runtime/src/slot_params.rs index 13f3c6a26be..0e25bb31ec9 100644 --- a/runtime/src/slot_params.rs +++ b/runtime/src/slot_params.rs @@ -1,4 +1,11 @@ -use {solana_clock::Slot, solana_epoch_schedule::EpochSchedule}; +use { + solana_clock::Slot, + solana_epoch_schedule::EpochSchedule, + std::{ + collections::BTreeMap, + ops::Bound::{Excluded, Included}, + }, +}; pub const DEFAULT_MAX_ENTRY_BYTES_PER_SLOT: u64 = 20 * 1024 * 1024; // 20 MiB @@ -118,7 +125,7 @@ pub(crate) struct SlotParamsArchive { /// parameters apply to some other slot?" Examples include shred filtering /// for incoming shreds and inflation calculations that span historical slot /// ranges. - param_transitions: Vec<(Slot, SlotParams)>, + param_transitions: BTreeMap, } impl Default for SlotParamsArchive { @@ -134,14 +141,14 @@ impl SlotParamsArchive { /// right shape for delayed, epoch-boundary-effective feature transitions. pub(crate) fn new(_epoch_schedule: &EpochSchedule, baseline_params: SlotParams) -> Self { Self { - param_transitions: vec![(0, baseline_params)], + param_transitions: BTreeMap::from([(0, baseline_params)]), } } /// Returns the baseline params supplied at genesis or snapshot restore. pub(crate) fn baseline_params(&self) -> SlotParams { self.param_transitions - .first() + .first_key_value() .map(|(_, params)| *params) .unwrap_or(LEGACY_SLOT_PARAMS) } @@ -149,14 +156,43 @@ impl SlotParamsArchive { /// Returns the slot params effective at `slot`. pub(crate) fn params_at_slot(&self, slot: Slot) -> SlotParams { self.param_transitions - .iter() - .rev() - .find_map(|(effective_slot, params)| (*effective_slot <= slot).then_some(*params)) + .range(..=slot) + .next_back() + .map(|(_, params)| *params) .unwrap_or(LEGACY_SLOT_PARAMS) } /// Returns sorted slot-parameter transitions. - pub(crate) fn param_transitions(&self) -> &[(Slot, SlotParams)] { - &self.param_transitions + pub(crate) fn param_transitions(&self) -> impl Iterator + '_ { + self.param_transitions + .iter() + .map(|(slot, params)| (*slot, *params)) + } + + /// Returns the exact wall-clock duration in nanoseconds for + /// `start_slot..=end_slot`. + pub(crate) fn slot_range_duration_nanos(&self, start_slot: Slot, end_slot: Slot) -> u128 { + if start_slot > end_slot { + return 0; + } + + let mut cursor = start_slot; + let mut params = self.params_at_slot(start_slot); + let mut duration = 0u128; + + for (&effective_slot, &effective_params) in self + .param_transitions + .range((Excluded(start_slot), Included(end_slot))) + { + duration = duration.saturating_add( + u128::from(effective_slot.saturating_sub(cursor)) + .saturating_mul(params.ns_per_slot()), + ); + cursor = effective_slot; + params = effective_params; + } + + let remaining_slots = u128::from(end_slot.saturating_sub(cursor)) + 1; + duration.saturating_add(remaining_slots.saturating_mul(params.ns_per_slot())) } } From bc0075cad647a4f857313ad1c710a222bdc80728 Mon Sep 17 00:00:00 2001 From: Yihau Chen Date: Mon, 1 Jun 2026 21:52:33 +0800 Subject: [PATCH 149/576] ci: have GitHub Actions only check repository owner (#12831) --- .github/workflows/benchmark.yml | 2 +- .github/workflows/bump-version.yml | 2 +- .github/workflows/cargo.yml | 4 ++-- .github/workflows/crate-check.yml | 2 +- .github/workflows/docs.yml | 6 +++--- .github/workflows/downstream-project-anchor.yml | 2 +- .github/workflows/downstream-project-spl-nightly.yml | 2 +- .github/workflows/downstream-project-spl.yml | 6 +++--- .github/workflows/manage-stale-issues.yml | 2 +- .github/workflows/release.yml | 4 ++-- .github/workflows/trigger-buildkite-pipeline.yml | 4 ++-- 11 files changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 269f4090a44..99b4093a1ac 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -6,7 +6,7 @@ on: jobs: benchmark: - if: github.repository == 'anza-xyz/agave' + if: github.repository_owner == 'anza-xyz' name: benchmark runs-on: benchmark strategy: diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml index a494b72e612..6c76ae94229 100644 --- a/.github/workflows/bump-version.yml +++ b/.github/workflows/bump-version.yml @@ -17,7 +17,7 @@ on: jobs: version-bump: - if: github.repository == 'anza-xyz/agave' + if: github.repository_owner == 'anza-xyz' runs-on: ubuntu-24.04 steps: - name: Checkout code diff --git a/.github/workflows/cargo.yml b/.github/workflows/cargo.yml index 2bc5d32447c..506bd063b89 100644 --- a/.github/workflows/cargo.yml +++ b/.github/workflows/cargo.yml @@ -30,7 +30,7 @@ env: jobs: clippy-nightly: - if: github.repository == 'anza-xyz/agave' + if: github.repository_owner == 'anza-xyz' strategy: matrix: os: @@ -69,7 +69,7 @@ jobs: clippy-nightly-windows-2025: name: clippy-nightly (windows-2025) - if: github.repository == 'anza-xyz/agave' && github.event_name == 'push' + if: github.repository_owner == 'anza-xyz' && github.event_name == 'push' runs-on: windows-2025 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 diff --git a/.github/workflows/crate-check.yml b/.github/workflows/crate-check.yml index 33ea169a70d..419f19b0298 100644 --- a/.github/workflows/crate-check.yml +++ b/.github/workflows/crate-check.yml @@ -15,7 +15,7 @@ on: jobs: check: name: crate check - if: github.repository == 'anza-xyz/agave' + if: github.repository_owner == 'anza-xyz' runs-on: ubuntu-22.04 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 9f2c2da9ef3..7fef5aafd94 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -14,7 +14,7 @@ on: jobs: check: - if: github.repository == 'anza-xyz/agave' + if: github.repository_owner == 'anza-xyz' outputs: continue: ${{ steps.check.outputs.need_to_build }} runs-on: ubuntu-22.04 @@ -70,7 +70,7 @@ jobs: needs: - check if: > - github.repository == 'anza-xyz/agave' && + github.repository_owner == 'anza-xyz' && needs.check.outputs.continue == 1 runs-on: ubuntu-22.04 steps: @@ -106,7 +106,7 @@ jobs: needs: - build if: > - github.repository == 'anza-xyz/agave' && + github.repository_owner == 'anza-xyz' && github.event_name == 'push' runs-on: ubuntu-22.04 steps: diff --git a/.github/workflows/downstream-project-anchor.yml b/.github/workflows/downstream-project-anchor.yml index 6dbc469abd7..be76e10ea2f 100644 --- a/.github/workflows/downstream-project-anchor.yml +++ b/.github/workflows/downstream-project-anchor.yml @@ -36,7 +36,7 @@ env: jobs: test: - #if: github.repository == 'anza-xyz/agave' + #if: github.repository_owner == 'anza-xyz' if: false # restore once all program-specific repos are ready runs-on: ubuntu-24.04 strategy: diff --git a/.github/workflows/downstream-project-spl-nightly.yml b/.github/workflows/downstream-project-spl-nightly.yml index ace52d430f3..687ae0f2ab3 100644 --- a/.github/workflows/downstream-project-spl-nightly.yml +++ b/.github/workflows/downstream-project-spl-nightly.yml @@ -6,7 +6,7 @@ on: jobs: main: - if: github.repository == 'anza-xyz/agave' + if: github.repository_owner == 'anza-xyz' strategy: fail-fast: false matrix: diff --git a/.github/workflows/downstream-project-spl.yml b/.github/workflows/downstream-project-spl.yml index 36551cf7118..508dc6d8921 100644 --- a/.github/workflows/downstream-project-spl.yml +++ b/.github/workflows/downstream-project-spl.yml @@ -36,7 +36,7 @@ env: jobs: check: - #if: github.repository == 'anza-xyz/agave' + #if: github.repository_owner == 'anza-xyz' if: false runs-on: ubuntu-24.04 timeout-minutes: 60 @@ -75,7 +75,7 @@ jobs: cargo check test_cli: - #if: github.repository == 'anza-xyz/agave' + #if: github.repository_owner == 'anza-xyz' if: false runs-on: ubuntu-24.04 timeout-minutes: 60 @@ -106,7 +106,7 @@ jobs: cargo test --manifest-path clients/cli/Cargo.toml cargo-test-sbf: - #if: github.repository == 'anza-xyz/agave' + #if: github.repository_owner == 'anza-xyz' if: false runs-on: ubuntu-24.04 timeout-minutes: 60 diff --git a/.github/workflows/manage-stale-issues.yml b/.github/workflows/manage-stale-issues.yml index 4f12c2f19af..bd1381b90f0 100644 --- a/.github/workflows/manage-stale-issues.yml +++ b/.github/workflows/manage-stale-issues.yml @@ -21,7 +21,7 @@ jobs: # Forks do not need to run this, especially on cron schedule. if: > github.event_name != 'schedule' - || github.repository == 'anza-xyz/agave' + || github.repository_owner == 'anza-xyz' runs-on: ubuntu-24.04 steps: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index af6cf27929e..f815d0bc745 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,7 +7,7 @@ on: jobs: trigger-buildkite-pipeline: - if: github.repository == 'anza-xyz/agave' + if: github.repository_owner == 'anza-xyz' runs-on: ubuntu-24.04 steps: - name: Trigger a Buildkite Build @@ -21,7 +21,7 @@ jobs: message: ":github: Triggered from a GitHub Action" draft-release: - if: github.repository == 'anza-xyz/agave' + if: github.repository_owner == 'anza-xyz' runs-on: ubuntu-24.04 steps: - name: Create Release diff --git a/.github/workflows/trigger-buildkite-pipeline.yml b/.github/workflows/trigger-buildkite-pipeline.yml index 90e530edcab..a9080b10c3e 100644 --- a/.github/workflows/trigger-buildkite-pipeline.yml +++ b/.github/workflows/trigger-buildkite-pipeline.yml @@ -20,7 +20,7 @@ concurrency: jobs: member_check: runs-on: ubuntu-24.04 - if: github.repository == 'anza-xyz/agave' && github.event.action != 'labeled' + if: github.repository_owner == 'anza-xyz' && github.event.action != 'labeled' outputs: should_trigger: ${{ steps.check.outputs.should_trigger }} permissions: @@ -51,7 +51,7 @@ jobs: label_check: runs-on: ubuntu-24.04 - if: github.repository == 'anza-xyz/agave' && github.event.action == 'labeled' && github.event.label.name == 'CI' + if: github.repository_owner == 'anza-xyz' && github.event.action == 'labeled' && github.event.label.name == 'CI' outputs: should_trigger: ${{ steps.check.outputs.should_trigger }} permissions: From ca07a0946e4d2847a9cf5e6f0ad7b80727d33a60 Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Mon, 1 Jun 2026 10:47:25 -0400 Subject: [PATCH 150/576] repair: fix set-identity bug on ping challenges (#12862) --- core/src/shred_fetch_stage.rs | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/core/src/shred_fetch_stage.rs b/core/src/shred_fetch_stage.rs index 780463e5a37..6425cda59b8 100644 --- a/core/src/shred_fetch_stage.rs +++ b/core/src/shred_fetch_stage.rs @@ -3,7 +3,6 @@ use { crate::repair::{repair_service::OutstandingShredRepairs, serve_repair::ServeRepair}, solana_gossip::cluster_info::ClusterInfo, - solana_keypair::Keypair, solana_ledger::shred::{ self, filter::{ShredFilterContext, TurbineMode}, @@ -65,7 +64,6 @@ impl ShredFetchStage { shred_version, Some(turbine_mode), ); - let repair_keypair = repair_context.map(RepairContext::keypair); for mut packet_batch in recvr { shred_filter_ctx.maybe_update(sharable_banks.root()); @@ -73,15 +71,13 @@ impl ShredFetchStage { if let Some(repair_context) = repair_context { debug_assert_eq!(flags, PacketFlags::REPAIR); - debug_assert!(repair_keypair.is_some()); - if let Some(ref keypair) = repair_keypair { - ServeRepair::handle_repair_response_pings( - &repair_context.repair_socket, - keypair, - &mut packet_batch, - &mut shred_filter_ctx.stats, - ); - } + let keypair = repair_context.cluster_info.keypair(); + ServeRepair::handle_repair_response_pings( + &repair_context.repair_socket, + &keypair, + &mut packet_batch, + &mut shred_filter_ctx.stats, + ); // Discard packets if repair nonce does not verify. let now = solana_time_utils::timestamp(); let mut outstanding_repair_requests = @@ -251,12 +247,6 @@ impl ShredFetchStage { } } -impl RepairContext { - fn keypair(&self) -> Arc { - self.cluster_info.keypair() - } -} - // Returns false if repair nonce is invalid and packet should be discarded. #[must_use] fn verify_repair_nonce( From 3bd0fa80438a868f2ec3b4cf32c2c979d0b3fbf5 Mon Sep 17 00:00:00 2001 From: Akhilesh Singhania Date: Mon, 1 Jun 2026 17:12:00 +0200 Subject: [PATCH 151/576] BCL: testing: removes outdated part of test (#12878) --- core/src/block_creation_loop.rs | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/core/src/block_creation_loop.rs b/core/src/block_creation_loop.rs index 9e1387b4808..c28f4f202fc 100644 --- a/core/src/block_creation_loop.rs +++ b/core/src/block_creation_loop.rs @@ -1307,7 +1307,6 @@ mod tests { super::*, crate::banking_trace::BankingTracer, agave_banking_stage_ingress_types::BankingPacketReceiver, - agave_votor::consensus_rewards::BuildRewardCertsResponse, crossbeam_channel::unbounded, solana_bls_signatures::{BLS_SIGNATURE_AFFINE_SIZE, Signature as BLSSignature}, solana_entry::{block_component::VersionedUpdateParent, entry_or_marker::EntryOrMarker}, @@ -1681,10 +1680,9 @@ mod tests { let (record_sender, record_receiver) = record_channels(false); let (_leader_window_info_sender, leader_window_info_receiver) = unbounded(); - let (reward_certs_sender, _reward_certs_receiver) = unbounded(); let (banking_stage_sender, _banking_stage_receiver) = BankingTracer::channel_for_test(); let bank_forks_controller = test_bank_forks_controller(bank_forks.clone()); - let (reward_certs_handler, build_reward_certs_receiver) = RewardCertsHandler::new(); + let (reward_certs_handler, _build_reward_certs_receiver) = RewardCertsHandler::new(); let mut ctx = LeaderContext { exit, @@ -1720,13 +1718,6 @@ mod tests { )) .unwrap(); - let delayed_reward = std::thread::spawn(move || { - std::thread::sleep(Duration::from_millis(300)); - let _ = reward_certs_sender.send(BuildRewardCertsResponse { - result: Ok(BuildRewardCertsRespSucc::default()), - }); - }); - let start = Instant::now(); let result = record_and_complete_block(&mut ctx, 1, None, &mut Instant::now(), Duration::ZERO); @@ -1736,15 +1727,6 @@ mod tests { assert!(ctx.bank_forks.read().unwrap().get(1).is_none()); assert!(ctx.record_receiver.is_shutdown()); assert!(ctx.record_receiver.is_safe_to_restart()); - assert_eq!( - build_reward_certs_receiver - .recv_timeout(Duration::from_secs(1)) - .unwrap() - .bank_slot, - 1 - ); - - delayed_reward.join().unwrap(); } #[test] From df6aa9cb3fa08d16e392f76cc16a76e04a6a902d Mon Sep 17 00:00:00 2001 From: Quentin Kniep Date: Mon, 1 Jun 2026 18:27:10 +0200 Subject: [PATCH 152/576] fix: potential wrong repair response due to swapped-out block location (#12791) --- ledger/src/blockstore.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/ledger/src/blockstore.rs b/ledger/src/blockstore.rs index 92eb271ab1b..72c1b23c01a 100644 --- a/ledger/src/blockstore.rs +++ b/ledger/src/blockstore.rs @@ -998,11 +998,14 @@ impl Blockstore { }; // Block was full above, get_block_location returned Some, but may have - // been purged by BlockstoreCleanupService in between. - let Some(dmm) = self.get_double_merkle_meta_maybe_populate_proofs(slot, location)? else { - return Ok(None); - }; - Ok(Some((dmm, location))) + // been purged by BlockstoreCleanupService in between, or swapped out. + if let Some(dmm) = self.get_double_merkle_meta_maybe_populate_proofs(slot, location)? + && dmm.double_merkle_root == block_id + { + Ok(Some((dmm, location))) + } else { + Ok(None) + } } /// Gets the double merkle meta for the given block. From 2832b413b1bb0bcf1a8f0700399c4da7588ac676 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Mei=C3=9Fner?= Date: Mon, 1 Jun 2026 12:37:03 -0400 Subject: [PATCH 153/576] Refactor - `Bank::prune_program_cache()` (#12838) * Removes slot and epoch parameters from Bank::prune_program_cache(). * Use the actual new environment, not the estimate, in Bank::prune_program_cache(). --- ledger/src/blockstore_processor.rs | 6 +----- program-runtime/src/loaded_programs.rs | 6 +++--- runtime/src/bank.rs | 19 +++++++++++-------- runtime/src/bank_forks.rs | 2 +- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/ledger/src/blockstore_processor.rs b/ledger/src/blockstore_processor.rs index 6be891fa904..b3d096c9d18 100644 --- a/ledger/src/blockstore_processor.rs +++ b/ledger/src/blockstore_processor.rs @@ -2580,11 +2580,7 @@ fn load_frozen_forks( root = new_root_bank.slot(); leader_schedule_cache.set_root(new_root_bank); - new_root_bank.prune_program_cache( - root, - new_root_bank.epoch(), - &bank_forks.read().unwrap(), - ); + new_root_bank.prune_program_cache(&bank_forks.read().unwrap()); let _ = bank_forks .write() .unwrap() diff --git a/program-runtime/src/loaded_programs.rs b/program-runtime/src/loaded_programs.rs index 84d06a5a50c..93728ebbde5 100644 --- a/program-runtime/src/loaded_programs.rs +++ b/program-runtime/src/loaded_programs.rs @@ -492,7 +492,7 @@ impl ProgramCache { pub fn prune( &mut self, new_root_slot: Slot, - upcoming_environment: Option, + new_environment: Option, fork_graph: &FG, ) { match &mut self.index { @@ -539,8 +539,8 @@ impl ProgramCache { }) .filter(|entry| { // Remove outdated environment of previous feature set - if let Some(upcoming_environment) = upcoming_environment.as_ref() - && !Self::matches_environment(entry, upcoming_environment) + if let Some(new_environment) = new_environment.as_ref() + && !Self::matches_environment(entry, new_environment) { self.stats .prunes_environment diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 913a9d84379..133a82b789d 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -1646,23 +1646,26 @@ impl Bank { } } - pub fn prune_program_cache( - &self, - new_root_slot: Slot, - new_root_epoch: Epoch, - bank_forks: &BankForks, - ) { + pub fn prune_program_cache(&self, bank_forks: &BankForks) { let upcoming_environment = self .transaction_processor .epoch_boundary_preparation .write() .unwrap() - .reroot(new_root_epoch); + .reroot(self.epoch()); self.transaction_processor .global_program_cache .write() .unwrap() - .prune(new_root_slot, upcoming_environment, bank_forks); + .prune( + self.slot(), + upcoming_environment.map(|_| { + ProgramRuntimeEnvironment::clone( + &self.transaction_processor.program_runtime_environment, + ) + }), + bank_forks, + ); } pub fn prune_program_cache_by_deployment_slot(&self, deployment_slot: Slot) { diff --git a/runtime/src/bank_forks.rs b/runtime/src/bank_forks.rs index bea4575e6e7..40328c97a49 100644 --- a/runtime/src/bank_forks.rs +++ b/runtime/src/bank_forks.rs @@ -540,7 +540,7 @@ impl BankForks { pub fn prune_program_cache(&self, root: Slot) { if let Some(root_bank) = self.banks.get(&root) { - root_bank.prune_program_cache(root, root_bank.epoch(), self); + root_bank.prune_program_cache(self); } } From 1363a45487591901c0b90ce5acc062144557ea40 Mon Sep 17 00:00:00 2001 From: Quentin Kniep Date: Mon, 1 Jun 2026 18:49:34 +0200 Subject: [PATCH 154/576] fix: consider Genesis cert in safe-to-notar (#12884) --- votor/src/consensus_pool.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/votor/src/consensus_pool.rs b/votor/src/consensus_pool.rs index bb0f149e328..fe9fd43ce6d 100644 --- a/votor/src/consensus_pool.rs +++ b/votor/src/consensus_pool.rs @@ -593,6 +593,9 @@ impl ConsensusPool { || self .completed_certificates .contains_key(&CertificateType::FinalizeFast(slot, block_id)) + || self + .completed_certificates + .contains_key(&CertificateType::Genesis(slot, block_id)) } /// Takes the pending safe-to-notar blocks that need parent verification. From f73431d794143cd40676fc34696ebcf5b9142929 Mon Sep 17 00:00:00 2001 From: Rory Harris Date: Mon, 1 Jun 2026 11:07:14 -0700 Subject: [PATCH 155/576] Update the lt hash from index to also updates that are in the cache but not in the index (#12844) * Update the lt hash for tests to include the cache * Update in response to feedback: Remove test function and fold logic into mainline function --- accounts-db/src/accounts_db.rs | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/accounts-db/src/accounts_db.rs b/accounts-db/src/accounts_db.rs index f2bf8eb6524..0d9a4e892a5 100644 --- a/accounts-db/src/accounts_db.rs +++ b/accounts-db/src/accounts_db.rs @@ -4855,6 +4855,7 @@ impl AccountsDb { /// /// Only intended to be called at startup (or by tests). /// Only intended to be used while testing the experimental accumulator hash. + /// NOT safe to call concurrently with flush operations pub fn calculate_accounts_lt_hash_at_startup_from_index( &self, ancestors: &Ancestors, @@ -4866,7 +4867,7 @@ impl AccountsDb { // A different parallel implementation that iterated over the bins *sequentially* and then // hashed the accounts *within* a bin in parallel took about 600 seconds. That impl uses // less memory, as only a single index bin is loaded into mem at a time. - let lt_hash = self + let mut lt_hash = self .accounts_index .account_maps .par_iter() @@ -4904,6 +4905,36 @@ impl AccountsDb { accum }); + let cache_lt_hash = { + let mut cache_lt_hash = LtHash::identity(); + for pubkey in self.accounts_cache.cached_pubkeys().iter() { + // mix out whatever older version the index walk produced (if any) + self.accounts_index.get_with_and_then( + pubkey, + ancestors, + false, + |(slot, account_info)| { + self.get_account_accessor(slot, pubkey, &account_info.storage_location()) + .get_loaded_account(|loaded_account| { + cache_lt_hash + .mix_out(&Self::lt_hash_account(&loaded_account, pubkey).0); + }); + }, + ); + // mix in the cache version + if let Some((account, _slot)) = self.load( + ancestors, + pubkey, + LoadHint::FixedMaxRoot, + PopulateReadCache::False, + ) { + cache_lt_hash.mix_in(&Self::lt_hash_account(&account, pubkey).0); + } + } + cache_lt_hash + }; + lt_hash.mix_in(&cache_lt_hash); + AccountsLtHash(lt_hash) } From ae4568d2f898e9cba6ad828a1bf0b61bd8cebc18 Mon Sep 17 00:00:00 2001 From: Brennan Date: Mon, 1 Jun 2026 11:40:23 -0700 Subject: [PATCH 156/576] Remove hashes per tick for ledger (#12887) --- ledger/src/blockstore.rs | 13 +++---------- ledger/src/blockstore/tests.rs | 9 --------- ledger/src/blockstore_processor.rs | 3 +-- 3 files changed, 4 insertions(+), 21 deletions(-) diff --git a/ledger/src/blockstore.rs b/ledger/src/blockstore.rs index 72c1b23c01a..693b6c5780d 100644 --- a/ledger/src/blockstore.rs +++ b/ledger/src/blockstore.rs @@ -396,13 +396,6 @@ impl SlotMetaWorkingSetEntry { } } -pub(crate) fn hashes_per_tick_for_ledger(genesis_config: &GenesisConfig) -> u64 { - let Some(hashes_per_tick) = genesis_config.poh_config.hashes_per_tick else { - return 0; - }; - hashes_per_tick -} - #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub(crate) struct ParentInfo { /// Parent slot to expose through SlotMeta and replay. @@ -6157,9 +6150,9 @@ pub fn create_new_ledger( }, )?; let ticks_per_slot = genesis_config.ticks_per_slot; - // Slot-0 tick entries are created before a Bank exists, so derive the - // effective hashes-per-tick directly from genesis feature state here. - let hashes_per_tick = hashes_per_tick_for_ledger(genesis_config); + // Slot-0 tick entries are created before a Bank exists, so use the + // genesis PoH config directly. + let hashes_per_tick = genesis_config.poh_config.hashes_per_tick.unwrap_or(0); let entries = create_ticks(ticks_per_slot, hashes_per_tick, genesis_config.hash()); let last_hash = entries.last().unwrap().hash; let version = solana_shred_version::version_from_hash(&last_hash); diff --git a/ledger/src/blockstore/tests.rs b/ledger/src/blockstore/tests.rs index 34a8a15bde4..e778893abe7 100644 --- a/ledger/src/blockstore/tests.rs +++ b/ledger/src/blockstore/tests.rs @@ -192,15 +192,6 @@ fn data_shreds(shreds: Vec) -> Vec { shreds.into_iter().filter(Shred::is_data).collect() } -#[test] -fn test_hashes_per_tick_for_ledger() { - let mut genesis_config = GenesisConfig::default(); - assert_eq!(hashes_per_tick_for_ledger(&genesis_config), 0); - - genesis_config.poh_config.hashes_per_tick = Some(2); - assert_eq!(hashes_per_tick_for_ledger(&genesis_config), 2); -} - #[test] fn test_create_new_ledger() { agave_logger::setup(); diff --git a/ledger/src/blockstore_processor.rs b/ledger/src/blockstore_processor.rs index b3d096c9d18..ced44cd56bf 100644 --- a/ledger/src/blockstore_processor.rs +++ b/ledger/src/blockstore_processor.rs @@ -2950,7 +2950,6 @@ pub mod tests { use { super::*, crate::{ - blockstore::hashes_per_tick_for_ledger, blockstore_options::{AccessType, BlockstoreOptions}, genesis_utils::{ GenesisConfigInfo, create_genesis_config, create_genesis_config_with_leader, @@ -3854,7 +3853,7 @@ pub mod tests { entries.push(entry); } - let hashes_per_tick = hashes_per_tick_for_ledger(&genesis_config); + let hashes_per_tick = genesis_config.poh_config.hashes_per_tick.unwrap_or(0); let remaining_hashes = hashes_per_tick - entries.len() as u64; let tick_entry = next_entry_mut(&mut last_entry_hash, remaining_hashes, vec![]); entries.push(tick_entry); From e5b0f856828e8b110c0e0328646e0e40fa6affcf Mon Sep 17 00:00:00 2001 From: Quentin Kniep Date: Mon, 1 Jun 2026 20:44:14 +0200 Subject: [PATCH 157/576] fix(repair): include FEC set count in double-Merkle leaf (#12506) --- core/src/repair/block_id_repair_service.rs | 6 +- core/src/repair/serve_repair.rs | 55 ++++++++++++++++++- ledger/src/blockstore.rs | 7 ++- ledger/src/blockstore/tests.rs | 7 ++- .../broadcast_stage/standard_broadcast_run.rs | 16 ++++-- 5 files changed, 80 insertions(+), 11 deletions(-) diff --git a/core/src/repair/block_id_repair_service.rs b/core/src/repair/block_id_repair_service.rs index 28a3c099d6e..81433086489 100644 --- a/core/src/repair/block_id_repair_service.rs +++ b/core/src/repair/block_id_repair_service.rs @@ -1404,7 +1404,11 @@ mod tests { // Create valid merkle tree for the response let fec_set_roots: Vec = (0..fec_set_count).map(|_| Hash::new_unique()).collect(); - let parent_info_leaf = hashv(&[&parent_slot.to_le_bytes(), parent_block_id.as_ref()]); + let parent_info_leaf = hashv(&[ + &parent_slot.to_le_bytes(), + parent_block_id.as_ref(), + &fec_set_count.to_le_bytes(), + ]); let mut leaves = fec_set_roots.clone(); leaves.push(parent_info_leaf); let (block_id, proofs) = build_merkle_tree(&leaves); diff --git a/core/src/repair/serve_repair.rs b/core/src/repair/serve_repair.rs index 1e172e6de4d..3d6e00c789d 100644 --- a/core/src/repair/serve_repair.rs +++ b/core/src/repair/serve_repair.rs @@ -289,8 +289,11 @@ impl RequestResponse for BlockIdRepairType { return false; } - let parent_info_leaf = - hashv(&[&parent_slot.to_le_bytes(), parent_block_id.as_ref()]); + let parent_info_leaf = hashv(&[ + &parent_slot.to_le_bytes(), + parent_block_id.as_ref(), + &fec_set_count.to_le_bytes(), + ]); merkle_tree::verify_merkle_proof( parent_info_leaf, *fec_set_count as usize, @@ -3209,4 +3212,52 @@ mod tests { "pong for original ping must still be valid — token was not overwritten" ); } + + #[test] + fn test_verify_fec_set_count_non_malleable() { + let parent_slot = 99u64; + let parent_block_id = Hash::new_unique(); + let fec_set_count: u32 = 2; // even => total leaves = 3, last leaf duplicated + let fec_set_roots: Vec = (0..fec_set_count).map(|_| Hash::new_unique()).collect(); + let real_parent_leaf = hashv(&[ + &parent_slot.to_le_bytes(), + parent_block_id.as_ref(), + &fec_set_count.to_le_bytes(), + ]); + let mut leaves: Vec = fec_set_roots; + leaves.push(real_parent_leaf); + let tree = + merkle_tree::MerkleTree::try_new_with_len(leaves.iter().copied().map(Ok), leaves.len()) + .unwrap(); + let block_id = *tree.root(); + let real_parent_proof: Vec = tree + .make_merkle_proof(fec_set_count as usize, leaves.len()) + .flat_map(|entry| entry.unwrap().iter().copied()) + .collect(); + + let request = BlockIdRepairType::ParentAndFecSetCount { + slot: 100, + block_id, + }; + + // honest response verifies + assert!( + request.verify_response(&BlockIdRepairResponse::ParentFecSetCount { + fec_set_count, + parent_info: (parent_slot, parent_block_id), + parent_proof: real_parent_proof.clone(), + }) + ); + + // Attack: claim N+1 and reuse the honest proof. The padded tree puts + // `real_parent_leaf` at both positions N and N+1, so without binding + // `fec_set_count` into the leaf this proof would verify. + assert!( + !request.verify_response(&BlockIdRepairResponse::ParentFecSetCount { + fec_set_count: fec_set_count + 1, + parent_info: (parent_slot, parent_block_id), + parent_proof: real_parent_proof.clone(), + }) + ); + } } diff --git a/ledger/src/blockstore.rs b/ledger/src/blockstore.rs index 693b6c5780d..65bb30cdf90 100644 --- a/ledger/src/blockstore.rs +++ b/ledger/src/blockstore.rs @@ -1889,10 +1889,15 @@ impl Blockstore { .map_err(|_| shred::Error::InvalidMerkleRoot) .and_then(|mr| mr.ok_or(shred::Error::InvalidMerkleRoot)) }) - // Add parent info as the last leaf + // Add parent info as the last leaf. The `fec_set_count` is bound + // into this leaf so that an adversary cannot convince a verifier + // that the count is off-by-one when the number of FEC sets is + // even (which makes the total leaf count odd and causes the last + // leaf to be hashed with itself during tree construction). .chain(std::iter::once(Ok(hashv(&[ &parent_slot.to_le_bytes(), parent_block_id.as_ref(), + &fec_set_count.to_le_bytes(), ])))); MerkleTree::try_new_with_len(merkle_tree_leaves, fec_set_count as usize + 1) diff --git a/ledger/src/blockstore/tests.rs b/ledger/src/blockstore/tests.rs index e778893abe7..73074e9f55e 100644 --- a/ledger/src/blockstore/tests.rs +++ b/ledger/src/blockstore/tests.rs @@ -6646,7 +6646,12 @@ fn test_get_double_merkle_root(use_alternate_location: bool) { } } - let parent_info_hash = hashv(&[&parent_slot.to_le_bytes(), parent_block_id.as_ref()]); + let fec_set_count = u32::try_from(fec_set_roots.len()).unwrap(); + let parent_info_hash = hashv(&[ + &parent_slot.to_le_bytes(), + parent_block_id.as_ref(), + &fec_set_count.to_le_bytes(), + ]); let merkle_tree_leaves: Vec<_> = fec_set_roots .iter() .copied() diff --git a/turbine/src/broadcast_stage/standard_broadcast_run.rs b/turbine/src/broadcast_stage/standard_broadcast_run.rs index d13b951dc34..4a6e78af7a5 100644 --- a/turbine/src/broadcast_stage/standard_broadcast_run.rs +++ b/turbine/src/broadcast_stage/standard_broadcast_run.rs @@ -288,10 +288,11 @@ impl StandardBroadcastRun { } /// Leaf that binds the current replay parent into the double-merkle block id. - fn parent_info_leaf(&self) -> Hash { + fn parent_info_leaf(&self, fec_set_count: u32) -> Hash { hashv(&[ &self.parent_for_double_merkle.0.to_le_bytes(), self.parent_for_double_merkle.1.as_bytes(), + &fec_set_count.to_le_bytes(), ]) } @@ -482,15 +483,16 @@ impl StandardBroadcastRun { .should_use_double_merkle_block_id(bank.slot()) { // Block id is the double merkle root - let fec_set_count = self.double_merkle_leaves.len(); + let fec_set_count = u32::try_from(self.double_merkle_leaves.len()).unwrap(); // Add the final leaf (parent info) - self.double_merkle_leaves.push(self.parent_info_leaf()); + self.double_merkle_leaves + .push(self.parent_info_leaf(fec_set_count)); // Compute the double merkle root // Blockstore population of the DoubleMerkleMeta happens asynchronously when shreds are inserted let merkle_tree = MerkleTree::try_new_with_len( self.double_merkle_leaves.drain(..).map(Ok), - fec_set_count + 1, + fec_set_count as usize + 1, ) .expect("Double merkle tree construction cannot fail"); *merkle_tree.root() @@ -1286,11 +1288,13 @@ mod test { bs.parent_for_double_merkle, (new_parent_slot, new_parent_block_id) ); + let fec_set_count = 3u32; assert_eq!( - bs.parent_info_leaf(), + bs.parent_info_leaf(fec_set_count), hashv(&[ &new_parent_slot.to_le_bytes(), - new_parent_block_id.as_bytes() + new_parent_block_id.as_bytes(), + &fec_set_count.to_le_bytes(), ]) ); From ea84ae3ab8e96b74870cd85792a9b06d33039bdb Mon Sep 17 00:00:00 2001 From: Quentin Kniep Date: Mon, 1 Jun 2026 21:04:31 +0200 Subject: [PATCH 158/576] fix: consider snapshot root in safe-to-notar (#12888) --- votor/src/consensus_pool.rs | 14 ++------------ votor/src/consensus_pool/parent_ready_tracker.rs | 9 +++++++++ 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/votor/src/consensus_pool.rs b/votor/src/consensus_pool.rs index fe9fd43ce6d..6a68d0234ce 100644 --- a/votor/src/consensus_pool.rs +++ b/votor/src/consensus_pool.rs @@ -584,18 +584,8 @@ impl ConsensusPool { /// Checks if a specific block has a NotarizeFallback certificate (or stronger). /// This is used for verifying that an intrawindow block's parent has been certified. pub(crate) fn block_has_notar_fallback_or_stronger(&self, block: Block) -> bool { - let (slot, block_id) = block; - self.completed_certificates - .contains_key(&CertificateType::NotarizeFallback(slot, block_id)) - || self - .completed_certificates - .contains_key(&CertificateType::Notarize(slot, block_id)) - || self - .completed_certificates - .contains_key(&CertificateType::FinalizeFast(slot, block_id)) - || self - .completed_certificates - .contains_key(&CertificateType::Genesis(slot, block_id)) + self.parent_ready_tracker + .has_notar_fallback_or_stronger(block) } /// Takes the pending safe-to-notar blocks that need parent verification. diff --git a/votor/src/consensus_pool/parent_ready_tracker.rs b/votor/src/consensus_pool/parent_ready_tracker.rs index 971ef42d46e..5c5f1729135 100644 --- a/votor/src/consensus_pool/parent_ready_tracker.rs +++ b/votor/src/consensus_pool/parent_ready_tracker.rs @@ -231,6 +231,15 @@ impl ParentReadyTracker { } } + /// Returns whether the block is notarized or notarized-fallback + /// + /// This includes any special cases like: genesis, snapshot root, migration Genesis cert + pub(super) fn has_notar_fallback_or_stronger(&self, block @ (slot, _): Block) -> bool { + self.slot_statuses + .get(&slot) + .is_some_and(|ss| ss.notar_fallbacks.contains(&block)) + } + fn highest_parent_ready(&self) -> Slot { self.highest_with_parent_ready } From 951c32e2822821327570a455e701723e25813213 Mon Sep 17 00:00:00 2001 From: Akhilesh Singhania Date: Mon, 1 Jun 2026 21:53:51 +0200 Subject: [PATCH 159/576] Combines all the reward certs related modules inside BCL (#12876) --- core/Cargo.toml | 2 + core/src/block_creation_loop.rs | 79 +++-- .../rewards/certs_builder.rs | 164 ++++++++++ .../rewards/certs_builder}/entry.rs | 7 +- .../certs_builder}/entry/notar_entry.rs | 5 +- .../certs_builder}/entry/partial_cert.rs | 2 +- .../certs_requestor.rs} | 68 ++--- core/src/block_creation_loop/rewards/mod.rs | 4 + .../block_creation_loop/rewards/msg_types.rs | 49 +++ .../rewards/reward_certs_service.rs | 118 +++++++ core/src/bls_sigverify/bls_sigverifier.rs | 12 +- core/src/bls_sigverify/bls_vote_sigverify.rs | 19 +- core/src/bls_sigverify/utils.rs | 6 +- core/src/tvu.rs | 17 +- core/src/validator.rs | 14 +- dev-bins/Cargo.lock | 2 + programs/sbf/Cargo.lock | 2 + votor/src/consensus_rewards.rs | 288 ------------------ votor/src/lib.rs | 1 - votor/src/votor.rs | 16 - 20 files changed, 456 insertions(+), 419 deletions(-) create mode 100644 core/src/block_creation_loop/rewards/certs_builder.rs rename {votor/src/consensus_rewards => core/src/block_creation_loop/rewards/certs_builder}/entry.rs (97%) rename {votor/src/consensus_rewards => core/src/block_creation_loop/rewards/certs_builder}/entry/notar_entry.rs (97%) rename {votor/src/consensus_rewards => core/src/block_creation_loop/rewards/certs_builder}/entry/partial_cert.rs (98%) rename core/src/block_creation_loop/{reward_certs_handler.rs => rewards/certs_requestor.rs} (65%) create mode 100644 core/src/block_creation_loop/rewards/mod.rs create mode 100644 core/src/block_creation_loop/rewards/msg_types.rs create mode 100644 core/src/block_creation_loop/rewards/reward_certs_service.rs delete mode 100644 votor/src/consensus_rewards.rs diff --git a/core/Cargo.toml b/core/Cargo.toml index a18977b66d8..bf171f15265 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -60,6 +60,7 @@ arrayvec = { workspace = true } assert_matches = { workspace = true } async-trait = { workspace = true } bincode = { workspace = true } +bitvec = { workspace = true } bytes = { workspace = true } chrono = { workspace = true, features = ["default", "serde"] } crossbeam-channel = { workspace = true } @@ -139,6 +140,7 @@ solana-sha256-hasher = { workspace = true } solana-shred-version = { workspace = true } solana-signature = { workspace = true } solana-signer = { workspace = true } +solana-signer-store = { workspace = true } solana-slot-hashes = { workspace = true } solana-slot-history = { workspace = true } solana-streamer = { workspace = true } diff --git a/core/src/block_creation_loop.rs b/core/src/block_creation_loop.rs index c28f4f202fc..47c3b30dc34 100644 --- a/core/src/block_creation_loop.rs +++ b/core/src/block_creation_loop.rs @@ -7,15 +7,19 @@ use { crate::{ banking_trace::{BankingPacketSender, BankingTracer}, - block_creation_loop::reward_certs_handler::RewardCertsHandler, + block_creation_loop::rewards::{ + certs_requestor::CertsRequestor, + msg_types::{AddVoteMessage, RewardRespSucc}, + reward_certs_service::RewardCertsService, + }, replay_stage::{Finalizer, ReplayStage}, }, - agave_votor::{consensus_rewards::BuildRewardCertsRespSucc, event::LeaderWindowInfo}, + agave_votor::event::LeaderWindowInfo, agave_votor_messages::{ consensus_message::Block, reward_certificate::{NotarRewardCertificate, SkipRewardCertificate}, }, - crossbeam_channel::{Receiver, select_biased}, + crossbeam_channel::{Receiver, Sender, select_biased}, solana_clock::Slot, solana_entry::block_component::{ BlockFooterV1, GenesisCertificate, UpdateParentV1, VersionedBlockMarker, @@ -33,7 +37,7 @@ use { solana_rpc::{rpc_subscriptions::RpcSubscriptions, slot_status_notifier::SlotStatusNotifier}, solana_runtime::{ bank::{Bank, NewBankOptions}, - bank_forks::BankForks, + bank_forks::{BankForks, SharableBanks}, bank_forks_controller::{BankForksController, BankForksControllerError}, block_component_processor::BlockComponentProcessor, leader_schedule_utils::{last_of_consecutive_leader_slots, leader_slot_index}, @@ -54,7 +58,7 @@ use { thiserror::Error, }; -pub(crate) mod reward_certs_handler; +pub(crate) mod rewards; mod stats; /// Source of a leader-window notification consumed by BCL. @@ -66,25 +70,39 @@ enum ParentSource { } pub struct BlockCreationLoop { - thread: JoinHandle<()>, + t_block_creation_loop: JoinHandle<()>, + reward_certs_service: RewardCertsService, } impl BlockCreationLoop { - pub fn new(config: BlockCreationLoopConfig) -> Self { - let thread = Builder::new() + pub fn new(config: BlockCreationLoopConfig) -> (Self, Sender) { + let (reward_certs_service, certs_requestor, votes_sender) = RewardCertsService::new( + config.cluster_info.clone(), + config.leader_schedule_cache.clone(), + config.sharable_banks.clone(), + config.exit.clone(), + ); + let t_block_creation_loop = Builder::new() .name("solBlkCreatLoop".to_string()) .spawn(move || { info!("BlockCreationLoop has started"); - start_loop(config); + start_loop(config, certs_requestor); info!("BlockCreationLoop has stopped"); }) .unwrap(); - Self { thread } + ( + Self { + t_block_creation_loop, + reward_certs_service, + }, + votes_sender, + ) } pub fn join(self) -> thread::Result<()> { - self.thread.join() + self.t_block_creation_loop.join()?; + self.reward_certs_service.join() } } @@ -93,6 +111,7 @@ pub struct BlockCreationLoopConfig { // Shared state pub bank_forks: Arc>, + pub sharable_banks: SharableBanks, pub bank_forks_controller: Arc, pub blockstore: Arc, pub cluster_info: Arc, @@ -114,8 +133,6 @@ pub struct BlockCreationLoopConfig { pub record_receiver_receiver: Receiver, pub optimistic_parent_receiver: Receiver, - pub(crate) reward_certs_handler: RewardCertsHandler, - /// Sender for packets to banking stage (used to re-inject transactions after sad leader handover). pub banking_stage_sender: BankingPacketSender, } @@ -141,7 +158,7 @@ struct LeaderContext { slot_status_notifier: Option, banking_tracer: Arc, replay_highest_frozen: Arc, - reward_certs_handler: RewardCertsHandler, + reward_certs_requestor: CertsRequestor, /// Banking-stage ingress used to reschedule transactions after sad handover. banking_stage_sender: BankingPacketSender, @@ -204,7 +221,7 @@ enum StartLeaderError { /// The `votor::consensus_pool_service` tracks when it is our leader window, and /// communicates the skip timer and parent slot for our window. This loop takes the responsibility /// of creating our `NUM_CONSECUTIVE_LEADER_SLOTS` blocks and finishing them within the required timeout. -fn start_loop(config: BlockCreationLoopConfig) { +fn start_loop(config: BlockCreationLoopConfig, reward_certs_requestor: CertsRequestor) { let BlockCreationLoopConfig { exit, bank_forks, @@ -221,9 +238,9 @@ fn start_loop(config: BlockCreationLoopConfig) { replay_highest_frozen, highest_parent_ready, optimistic_parent_receiver, - reward_certs_handler, highest_finalized, banking_stage_sender, + sharable_banks: _, } = config; // Similar to Votor, if this loop dies kill the validator @@ -270,7 +287,7 @@ fn start_loop(config: BlockCreationLoopConfig) { slot_status_notifier, banking_tracer, replay_highest_frozen, - reward_certs_handler, + reward_certs_requestor, banking_stage_sender, metrics: LoopMetrics::default(), slot_metrics: SlotMetrics::default(), @@ -618,8 +635,9 @@ fn record_and_complete_block( block_timeout: Duration, ) -> Result<(), PohRecorderError> { let reward_cert_request = ctx - .reward_certs_handler - .request_reward_certs(ctx.my_pubkey, bank_slot)?; + .reward_certs_requestor + .request_reward_certs(ctx.my_pubkey, bank_slot) + .map_err(|()| PohRecorderError::ChannelDisconnected)?; let mut accumulated_txs = vec![]; let mut records_shutdown = false; let window_has_moved_on = loop { @@ -739,9 +757,10 @@ fn record_and_complete_block( let footer = { let reward_certs = ctx - .reward_certs_handler - .recv_reward_certs(ctx.my_pubkey, reward_cert_request)?; - let BuildRewardCertsRespSucc { + .reward_certs_requestor + .recv_reward_certs(ctx.my_pubkey, reward_cert_request) + .map_err(|()| PohRecorderError::ChannelDisconnected)?; + let RewardRespSucc { skip, notar, validators: _, @@ -1501,7 +1520,7 @@ mod tests { let (_leader_window_info_sender, leader_window_info_receiver) = unbounded(); let (banking_stage_sender, _banking_stage_receiver) = BankingTracer::channel_for_test(); let bank_forks_controller = test_bank_forks_controller(bank_forks.clone()); - let (reward_certs_handler, _receiver) = RewardCertsHandler::new(); + let (reward_certs_requestor, _receiver) = CertsRequestor::new(); let mut ctx = LeaderContext { exit, @@ -1520,7 +1539,7 @@ mod tests { slot_status_notifier: None, banking_tracer: BankingTracer::new_disabled(), replay_highest_frozen: Arc::new(ReplayHighestFrozen::default()), - reward_certs_handler, + reward_certs_requestor, banking_stage_sender, metrics: LoopMetrics::default(), slot_metrics: SlotMetrics::default(), @@ -1613,7 +1632,7 @@ mod tests { let mut genesis_cert = test_genesis_certificate(); genesis_cert.slot = parent_bank.slot(); let bank_forks_controller = test_bank_forks_controller(bank_forks.clone()); - let (reward_certs_handler, _receiver) = RewardCertsHandler::new(); + let (reward_certs_requestor, _receiver) = CertsRequestor::new(); let mut ctx = LeaderContext { exit, @@ -1632,7 +1651,7 @@ mod tests { slot_status_notifier: None, banking_tracer: BankingTracer::new_disabled(), replay_highest_frozen: Arc::new(ReplayHighestFrozen::default()), - reward_certs_handler, + reward_certs_requestor, banking_stage_sender, metrics: LoopMetrics::default(), slot_metrics: SlotMetrics::default(), @@ -1682,7 +1701,7 @@ mod tests { let (_leader_window_info_sender, leader_window_info_receiver) = unbounded(); let (banking_stage_sender, _banking_stage_receiver) = BankingTracer::channel_for_test(); let bank_forks_controller = test_bank_forks_controller(bank_forks.clone()); - let (reward_certs_handler, _build_reward_certs_receiver) = RewardCertsHandler::new(); + let (reward_certs_requestor, _reward_request_receiver) = CertsRequestor::new(); let mut ctx = LeaderContext { exit, @@ -1701,7 +1720,7 @@ mod tests { slot_status_notifier: None, banking_tracer: BankingTracer::new_disabled(), replay_highest_frozen: Arc::new(ReplayHighestFrozen::default()), - reward_certs_handler, + reward_certs_requestor, banking_stage_sender, metrics: LoopMetrics::default(), slot_metrics: SlotMetrics::default(), @@ -1785,7 +1804,7 @@ mod tests { let (leader_window_info_sender, leader_window_info_receiver) = unbounded(); let (banking_stage_sender, banking_stage_receiver) = BankingTracer::channel_for_test(); let bank_forks_controller = test_bank_forks_controller(bank_forks.clone()); - let (reward_certs_handler, _receiver) = RewardCertsHandler::new(); + let (reward_certs_requestor, _receiver) = CertsRequestor::new(); let mut ctx = LeaderContext { exit, @@ -1804,7 +1823,7 @@ mod tests { slot_status_notifier: None, banking_tracer: BankingTracer::new_disabled(), replay_highest_frozen: Arc::new(ReplayHighestFrozen::default()), - reward_certs_handler, + reward_certs_requestor, banking_stage_sender, metrics: LoopMetrics::default(), slot_metrics: SlotMetrics::default(), diff --git a/core/src/block_creation_loop/rewards/certs_builder.rs b/core/src/block_creation_loop/rewards/certs_builder.rs new file mode 100644 index 00000000000..4c3b88db754 --- /dev/null +++ b/core/src/block_creation_loop/rewards/certs_builder.rs @@ -0,0 +1,164 @@ +use { + crate::block_creation_loop::rewards::msg_types::{ + RewardRequest, RewardRespSucc, RewardResponse, + }, + agave_votor_messages::{ + consensus_message::VoteMessage, + reward_certificate::{BuildRewardCertsRespError, NUM_SLOTS_FOR_REWARD}, + vote::Vote, + }, + crossbeam_channel::RecvError, + entry::Entry, + solana_clock::Slot, + solana_gossip::cluster_info::ClusterInfo, + solana_ledger::leader_schedule_cache::LeaderScheduleCache, + solana_runtime::bank::Bank, + std::{collections::BTreeMap, sync::Arc}, +}; + +mod entry; + +/// Returns [`false`] if the rewards container is not interested in the [`VoteMessage`]. +/// Returns [`true`] if the rewards container might be interested in the [`VoteMessage`]. +pub fn wants_vote( + cluster_info: &ClusterInfo, + leader_schedule: &LeaderScheduleCache, + root_slot: Slot, + vote: &VoteMessage, +) -> bool { + match vote.vote { + Vote::Notarize(_) | Vote::Skip(_) => (), + Vote::Finalize(_) + | Vote::NotarizeFallback(_) + | Vote::SkipFallback(_) + | Vote::Genesis(_) => return false, + } + let vote_slot = vote.vote.slot(); + if vote_slot.saturating_add(NUM_SLOTS_FOR_REWARD) <= root_slot { + return false; + } + let my_pubkey = cluster_info.id(); + let Some(leader) = + leader_schedule.slot_leader_at(vote_slot.saturating_add(NUM_SLOTS_FOR_REWARD), None) + else { + return false; + }; + if leader.id != my_pubkey { + return false; + } + true +} + +/// Container to store state needed to generate reward certificates. +pub(super) struct CertsBuilder { + /// Per [`Slot`], stores skip and notar votes. + votes: BTreeMap, + /// Stores the latest pubkey for the current node. + cluster_info: Arc, + /// Stores the leader schedules. + leader_schedule: Arc, +} + +impl CertsBuilder { + /// Constructs a new instance of [`CertsBuilder`]. + pub(super) fn new( + cluster_info: Arc, + leader_schedule: Arc, + ) -> Self { + Self { + votes: BTreeMap::default(), + cluster_info, + leader_schedule, + } + } + + /// Builds reward certificates. + fn build_certs( + &mut self, + bank_slot: Slot, + ) -> Result { + let Some(reward_slot) = bank_slot.checked_sub(NUM_SLOTS_FOR_REWARD) else { + return Ok(RewardRespSucc::default()); + }; + // we assume that the block creation loop will only ever request to build reward certs in a + // strictly increasing order so we can drop older state + self.votes = self.votes.split_off(&reward_slot); + match self.votes.remove(&reward_slot) { + None => Ok(RewardRespSucc::default()), + Some(entry) => entry.build_certs(reward_slot), + } + } + + pub(super) fn build_request( + &mut self, + request: Result, + ) -> Result<(), ()> { + let my_pubkey = self.cluster_info.id(); + match request { + Ok(RewardRequest { + bank_slot, + reply_sender, + }) => { + let resp = RewardResponse { + result: self.build_certs(bank_slot), + }; + let _ = reply_sender.send(resp).inspect_err(|_| { + info!( + "{my_pubkey}: channel to send reply for bank_slot={bank_slot} disconnected" + ); + }); + Ok(()) + } + Err(_) => { + error!("{my_pubkey}: build reward certs channel is disconnected; exiting."); + Err(()) + } + } + } + + /// Returns [`true`] if the rewards container is interested in this vote else [`false`]. + fn wants_vote(&self, root_slot: Slot, vote: &VoteMessage) -> bool { + if !wants_vote(&self.cluster_info, &self.leader_schedule, root_slot, vote) { + return false; + } + let Some(entry) = self.votes.get(&vote.vote.slot()) else { + return true; + }; + entry.wants_vote(vote) + } + + /// Adds received [`VoteMessage`] from other validators. + pub(super) fn add_vote(&mut self, root_bank: &Bank, vote: &VoteMessage) { + let slot = vote.vote.slot(); + let Some(rank_map) = root_bank.get_rank_map(slot) else { + warn!( + "failed to look up rank_map for slot {slot} using bank for slot {}", + root_bank.slot() + ); + return; + }; + let max_validators = rank_map.len(); + let root_slot = root_bank.slot(); + // drop state that is too old based on how the root slot has progressed + // TODO: if this actually purges state, that probably indicates that the leader missed its + // window. We should have a metric for this. + self.votes = self + .votes + .split_off(&root_slot.saturating_sub(NUM_SLOTS_FOR_REWARD)); + + if !self.wants_vote(root_slot, vote) { + return; + } + match self + .votes + .entry(vote.vote.slot()) + .or_insert(Entry::new(max_validators)) + .add_vote(rank_map, vote) + { + Ok(()) => (), + Err(e) => { + warn!("Adding vote {vote:?} failed with {e}"); + } + } + } +} diff --git a/votor/src/consensus_rewards/entry.rs b/core/src/block_creation_loop/rewards/certs_builder/entry.rs similarity index 97% rename from votor/src/consensus_rewards/entry.rs rename to core/src/block_creation_loop/rewards/certs_builder/entry.rs index b3ed4881236..b9ffc8e1172 100644 --- a/votor/src/consensus_rewards/entry.rs +++ b/core/src/block_creation_loop/rewards/certs_builder/entry.rs @@ -1,5 +1,6 @@ use { - super::{BuildRewardCertsRespError, BuildRewardCertsRespSucc}, + super::BuildRewardCertsRespError, + crate::block_creation_loop::rewards::msg_types::RewardRespSucc, agave_votor_messages::{ consensus_message::VoteMessage, reward_certificate::SkipRewardCertificate, vote::Vote, }, @@ -81,7 +82,7 @@ impl Entry { pub(super) fn build_certs( self, reward_slot: Slot, - ) -> Result { + ) -> Result { let notar = self.notar.build_cert(reward_slot)?; let skip = match self.skip.build_sig_bitmap() { Err(e) => match e { @@ -107,7 +108,7 @@ impl Entry { } }; - Ok(BuildRewardCertsRespSucc { + Ok(RewardRespSucc { skip, notar, validators, diff --git a/votor/src/consensus_rewards/entry/notar_entry.rs b/core/src/block_creation_loop/rewards/certs_builder/entry/notar_entry.rs similarity index 97% rename from votor/src/consensus_rewards/entry/notar_entry.rs rename to core/src/block_creation_loop/rewards/certs_builder/entry/notar_entry.rs index 9315e6d25dc..be9d0c91538 100644 --- a/votor/src/consensus_rewards/entry/notar_entry.rs +++ b/core/src/block_creation_loop/rewards/certs_builder/entry/notar_entry.rs @@ -3,8 +3,7 @@ use { super::{AddVoteError, BuildSigBitmapError, partial_cert::PartialCert}, - crate::consensus_rewards::BuildRewardCertsRespError, - agave_votor_messages::reward_certificate::NotarRewardCertificate, + agave_votor_messages::reward_certificate::{BuildRewardCertsRespError, NotarRewardCertificate}, solana_bls_signatures::Signature as BLSSignature, solana_clock::Slot, solana_hash::Hash, @@ -95,7 +94,7 @@ impl NotarEntry { mod tests { use { super::*, - crate::consensus_rewards::entry::tests::{ + crate::block_creation_loop::rewards::certs_builder::entry::tests::{ get_rank_map_keypairs, new_vote, validate_bitmap, }, agave_votor_messages::{consensus_message::VoteMessage, vote::Vote}, diff --git a/votor/src/consensus_rewards/entry/partial_cert.rs b/core/src/block_creation_loop/rewards/certs_builder/entry/partial_cert.rs similarity index 98% rename from votor/src/consensus_rewards/entry/partial_cert.rs rename to core/src/block_creation_loop/rewards/certs_builder/entry/partial_cert.rs index a8fbc51b775..4083489e2ad 100644 --- a/votor/src/consensus_rewards/entry/partial_cert.rs +++ b/core/src/block_creation_loop/rewards/certs_builder/entry/partial_cert.rs @@ -104,7 +104,7 @@ impl PartialCert { mod tests { use { super::*, - crate::consensus_rewards::entry::tests::{ + crate::block_creation_loop::rewards::certs_builder::entry::tests::{ get_rank_map_keypairs, new_vote, validate_bitmap, }, agave_votor_messages::{consensus_message::VoteMessage, vote::Vote}, diff --git a/core/src/block_creation_loop/reward_certs_handler.rs b/core/src/block_creation_loop/rewards/certs_requestor.rs similarity index 65% rename from core/src/block_creation_loop/reward_certs_handler.rs rename to core/src/block_creation_loop/rewards/certs_requestor.rs index 6140b77ee64..f62dc46ada2 100644 --- a/core/src/block_creation_loop/reward_certs_handler.rs +++ b/core/src/block_creation_loop/rewards/certs_requestor.rs @@ -1,32 +1,26 @@ use { - agave_votor::consensus_rewards::{ - BuildRewardCertsRequest, BuildRewardCertsRespSucc, BuildRewardCertsResponse, + crate::block_creation_loop::rewards::msg_types::{ + RewardRequest, RewardRequestToken, RewardRespSucc, }, crossbeam_channel::{Receiver, Sender, TryRecvError, TrySendError, bounded}, solana_clock::Slot, - solana_poh::poh_recorder::PohRecorderError, solana_pubkey::Pubkey, }; -/// The request token to be used to receive responses on previously sent requests to build reward certs. -pub(super) struct RewardCertRequest { - reply_receiver: Receiver, -} - /// Struct to handle requests to build rewards certs and receive the produced certs. -pub(crate) struct RewardCertsHandler { - sender: Sender, +pub(crate) struct CertsRequestor { + req_sender: Sender, } -impl RewardCertsHandler { - pub(crate) fn new() -> (Self, Receiver) { +impl CertsRequestor { + pub(crate) fn new() -> (Self, Receiver) { // If the rewards container is keeping up, we should only ever expect there to be 1 msg in // flight. So using a fairly small capacity to allow the rewards container to catch up if // it ever falls behind. const CAPACITY: usize = 16; let (tx, rx) = bounded(CAPACITY); - (Self { sender: tx }, rx) + (Self { req_sender: tx }, rx) } /// Tries to send a message requesting a reward cert to be built. @@ -40,20 +34,19 @@ impl RewardCertsHandler { /// used to receive a response to the build the reward cert. /// - `Ok(None)` if sending failed because the channel is full i.e. the rewards container is /// not keeping up. - /// - `Err(PohRecorderError::ChannelDisconnected)` if the channel is to the rewards container - /// is disconnected. - pub(super) fn request_reward_certs( + /// - `Err(())` if the channel to the rewards container is disconnected. + pub(crate) fn request_reward_certs( &mut self, my_pubkey: Pubkey, bank_slot: Slot, - ) -> Result, PohRecorderError> { + ) -> Result, ()> { let (reply_sender, reply_receiver) = bounded(1); - let request = BuildRewardCertsRequest { + let request = RewardRequest { bank_slot, reply_sender, }; - match self.sender.try_send(request) { - Ok(()) => Ok(Some(RewardCertRequest { reply_receiver })), + match self.req_sender.try_send(request) { + Ok(()) => Ok(Some(RewardRequestToken { reply_receiver })), Err(TrySendError::Full(_)) => { warn!( "{my_pubkey} sending request to build reward cert for bank_slot={bank_slot} \ @@ -61,7 +54,7 @@ impl RewardCertsHandler { ); Ok(None) } - Err(TrySendError::Disconnected(_)) => Err(PohRecorderError::ChannelDisconnected), + Err(TrySendError::Disconnected(_)) => Err(()), } } @@ -70,26 +63,26 @@ impl RewardCertsHandler { /// Does not block trying to receive the response as we have a tight deadline to produce /// the block and if the rewards container is not keeping up, we will rather not include any /// reward certs than wait for it. - pub(super) fn recv_reward_certs( + pub(crate) fn recv_reward_certs( &mut self, my_pubkey: Pubkey, - request: Option, - ) -> Result { + request: Option, + ) -> Result { match request { - None => Ok(BuildRewardCertsRespSucc::default()), + None => Ok(RewardRespSucc::default()), Some(request) => { let msg = request.reply_receiver.try_recv(); match msg { Err(TryRecvError::Empty) => { warn!("{my_pubkey} trying to receive cert failed, channel is empty"); - Ok(BuildRewardCertsRespSucc::default()) + Ok(RewardRespSucc::default()) } - Err(TryRecvError::Disconnected) => Err(PohRecorderError::ChannelDisconnected), + Err(TryRecvError::Disconnected) => Err(()), Ok(resp) => match resp.result { Ok(res) => Ok(res), Err(err) => { error!("{my_pubkey} building reward cert failed with {err:?}"); - Ok(BuildRewardCertsRespSucc::default()) + Ok(RewardRespSucc::default()) } }, } @@ -100,39 +93,40 @@ impl RewardCertsHandler { #[cfg(test)] mod tests { - use super::*; + + use {super::*, crate::block_creation_loop::rewards::msg_types::RewardResponse}; #[test] fn test_does_not_block() { let my_pubkey = Pubkey::default(); - let (mut handler, _receiver) = RewardCertsHandler::new(); + let (mut handler, _receiver) = CertsRequestor::new(); handler.request_reward_certs(my_pubkey, 1).unwrap().unwrap(); let resp = handler.recv_reward_certs(my_pubkey, None).unwrap(); - assert_eq!(resp, BuildRewardCertsRespSucc::default()); + assert_eq!(resp, RewardRespSucc::default()); { let (_reply_sender, reply_receiver) = bounded(0); - let request = RewardCertRequest { reply_receiver }; + let request = RewardRequestToken { reply_receiver }; let resp = handler.recv_reward_certs(my_pubkey, Some(request)).unwrap(); - assert_eq!(resp, BuildRewardCertsRespSucc::default()); + assert_eq!(resp, RewardRespSucc::default()); } } #[test] fn test_basic_functionality() { let my_pubkey = Pubkey::default(); - let (mut handler, receiver) = RewardCertsHandler::new(); + let (mut handler, receiver) = CertsRequestor::new(); let request = handler.request_reward_certs(my_pubkey, 1).unwrap().unwrap(); let validators = vec![Pubkey::new_unique(); 10]; - let BuildRewardCertsRequest { + let RewardRequest { bank_slot: _, reply_sender, } = receiver.recv().unwrap(); reply_sender - .send(BuildRewardCertsResponse { - result: Ok(BuildRewardCertsRespSucc { + .send(RewardResponse { + result: Ok(RewardRespSucc { skip: None, notar: None, validators: validators.clone(), diff --git a/core/src/block_creation_loop/rewards/mod.rs b/core/src/block_creation_loop/rewards/mod.rs new file mode 100644 index 00000000000..a7941cb856d --- /dev/null +++ b/core/src/block_creation_loop/rewards/mod.rs @@ -0,0 +1,4 @@ +pub(crate) mod certs_builder; +pub(super) mod certs_requestor; +pub(crate) mod msg_types; +pub(crate) mod reward_certs_service; diff --git a/core/src/block_creation_loop/rewards/msg_types.rs b/core/src/block_creation_loop/rewards/msg_types.rs new file mode 100644 index 00000000000..e57776bd311 --- /dev/null +++ b/core/src/block_creation_loop/rewards/msg_types.rs @@ -0,0 +1,49 @@ +use { + agave_votor_messages::{ + consensus_message::VoteMessage, + reward_certificate::{ + BuildRewardCertsRespError, NotarRewardCertificate, SkipRewardCertificate, + }, + }, + crossbeam_channel::{Receiver, Sender}, + solana_clock::Slot, + solana_pubkey::Pubkey, +}; + +/// Request to build reward certificates. +pub(crate) struct RewardRequest { + /// The bank slot which will include the built reward certs. + pub(crate) bank_slot: Slot, + /// The channel on which to send the reply. + pub(super) reply_sender: Sender, +} + +/// Response when the reward certs are built successfully. +#[derive(Default, Debug, PartialEq, Eq)] +pub(crate) struct RewardRespSucc { + /// Skip reward certificate. None if no skip votes were registered. + pub(crate) skip: Option, + /// Notar reward certificate. None if no notar votes were registered. + pub(crate) notar: Option, + /// If at least one of the certs above is present, then this contains the slot for which the + /// reward certs were built and the list of validators in the certs. + pub(crate) validators: Vec, +} + +/// Response to a [`RewardRequest`]. +pub(crate) struct RewardResponse { + /// The result of building reward certs for `bank_slot`. + pub(crate) result: Result, +} + +/// Message to add votes to the rewards container. +#[derive(Debug)] +pub struct AddVoteMessage { + /// List of [`VoteMessage`]s. + pub(crate) votes: Vec, +} + +/// The request token to be used to receive responses on previously sent requests to build reward certs. +pub(crate) struct RewardRequestToken { + pub(super) reply_receiver: Receiver, +} diff --git a/core/src/block_creation_loop/rewards/reward_certs_service.rs b/core/src/block_creation_loop/rewards/reward_certs_service.rs new file mode 100644 index 00000000000..c642cdd0661 --- /dev/null +++ b/core/src/block_creation_loop/rewards/reward_certs_service.rs @@ -0,0 +1,118 @@ +use { + crate::{ + block_creation_loop::rewards::{ + certs_builder::CertsBuilder, + certs_requestor::CertsRequestor, + msg_types::{AddVoteMessage, RewardRequest}, + }, + tvu::MAX_ALPENGLOW_PACKET_NUM, + }, + crossbeam_channel::{Receiver, Sender, bounded, select_biased}, + solana_gossip::cluster_info::ClusterInfo, + solana_ledger::leader_schedule_cache::LeaderScheduleCache, + solana_runtime::bank_forks::SharableBanks, + std::{ + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, + thread::{self, Builder, JoinHandle}, + time::Duration, + }, +}; + +pub(crate) struct RewardCertsService { + handler: JoinHandle<()>, +} + +impl RewardCertsService { + pub(crate) fn new( + cluster_info: Arc, + leader_schedule: Arc, + sharable_banks: SharableBanks, + exit: Arc, + ) -> (Self, CertsRequestor, Sender) { + let (votes_sender, votes_receiver) = bounded(MAX_ALPENGLOW_PACKET_NUM); + let (certs_requestor, req_receiver) = CertsRequestor::new(); + let builder = CertsBuilder::new(cluster_info.clone(), leader_schedule); + let ctx = Context::new( + exit, + cluster_info, + votes_receiver, + req_receiver, + sharable_banks, + builder, + ); + let handler = Builder::new() + .name("solConsRew".to_string()) + .spawn(move || { + ctx.run(); + }) + .unwrap(); + (Self { handler }, certs_requestor, votes_sender) + } + + pub(crate) fn join(self) -> thread::Result<()> { + self.handler.join() + } +} + +struct Context { + exit: Arc, + cluster_info: Arc, + votes_receiver: Receiver, + req_receiver: Receiver, + sharable_banks: SharableBanks, + builder: CertsBuilder, +} + +impl Context { + fn new( + exit: Arc, + cluster_info: Arc, + votes_receiver: Receiver, + req_receiver: Receiver, + sharable_banks: SharableBanks, + builder: CertsBuilder, + ) -> Self { + Self { + exit, + cluster_info, + votes_receiver, + req_receiver, + sharable_banks, + builder, + } + } + + fn run(mut self) { + while !self.exit.load(Ordering::Relaxed) { + let my_pubkey = self.cluster_info.id(); + // bias messages to build certificates as that is on the critical path + select_biased! { + recv(self.req_receiver) -> msg => { + if let Err(()) = self.builder.build_request(msg) { + break; + } + } + recv(self.votes_receiver) -> msg => { + match msg { + Ok(msg) => { + let bank = self.sharable_banks.root(); + for vote in msg.votes { + self.builder.add_vote(&bank, &vote); + } + } + Err(_) => { + error!("{my_pubkey}: votes receiver channel is disconnected; exiting."); + break; + } + } + } + default(Duration::from_secs(1)) => { + continue; + } + } + } + } +} diff --git a/core/src/bls_sigverify/bls_sigverifier.rs b/core/src/bls_sigverify/bls_sigverifier.rs index 903a4c59a8d..72189b2229c 100644 --- a/core/src/bls_sigverify/bls_sigverifier.rs +++ b/core/src/bls_sigverify/bls_sigverifier.rs @@ -7,11 +7,12 @@ use { errors::SigVerifyError, stats::SigVerifierStats, }, - crate::cluster_info_vote_listener::VerifiedVoterSlotsSender, + crate::{ + block_creation_loop::rewards::{certs_builder::wants_vote, msg_types::AddVoteMessage}, + cluster_info_vote_listener::VerifiedVoterSlotsSender, + }, agave_votor::{ - consensus_metrics::ConsensusMetricsEventSender, - consensus_rewards::{self, AddVoteMessage}, - generated_cert_types::GeneratedCertTypes, + consensus_metrics::ConsensusMetricsEventSender, generated_cert_types::GeneratedCertTypes, }, agave_votor_messages::{ certificate::CertificateType, @@ -285,8 +286,7 @@ impl SigVerifier { if vote.vote.slot() > root_slot { return ret; } - if consensus_rewards::wants_vote(&self.cluster_info, &self.leader_schedule, root_slot, vote) - { + if wants_vote(&self.cluster_info, &self.leader_schedule, root_slot, vote) { return ret; } self.stats.num_old_votes_received += 1; diff --git a/core/src/bls_sigverify/bls_vote_sigverify.rs b/core/src/bls_sigverify/bls_vote_sigverify.rs index d44715c92d4..af298633217 100644 --- a/core/src/bls_sigverify/bls_vote_sigverify.rs +++ b/core/src/bls_sigverify/bls_vote_sigverify.rs @@ -2,16 +2,17 @@ use qualifier_attr::qualifiers; use { super::{errors::SigVerifyVoteError, stats::SigVerifyVoteStats}, - crate::bls_sigverify::{ - bls_sigverifier::{BAN_TIMEOUT, NUM_SLOTS_FOR_VERIFY, SigVerifierChannels}, - utils::{ - send_votes_to_metrics, send_votes_to_pool, send_votes_to_repair, send_votes_to_rewards, + crate::{ + block_creation_loop::rewards::msg_types::AddVoteMessage, + bls_sigverify::{ + bls_sigverifier::{BAN_TIMEOUT, NUM_SLOTS_FOR_VERIFY, SigVerifierChannels}, + utils::{ + send_votes_to_metrics, send_votes_to_pool, send_votes_to_repair, + send_votes_to_rewards, + }, }, }, - agave_votor::{ - consensus_metrics::ConsensusMetricsEvent, - consensus_rewards::{self, AddVoteMessage}, - }, + agave_votor::consensus_metrics::ConsensusMetricsEvent, agave_votor_messages::{ consensus_message::{ConsensusMessage, VoteMessage}, vote::Vote, @@ -139,7 +140,7 @@ fn process_verified_votes( let mut votes_for_metrics = Vec::with_capacity(verified_votes.len()); for vote in verified_votes { let vote_message = vote.vote_message; - if consensus_rewards::wants_vote( + if crate::block_creation_loop::rewards::certs_builder::wants_vote( cluster_info, leader_schedule, root_bank.slot(), diff --git a/core/src/bls_sigverify/utils.rs b/core/src/bls_sigverify/utils.rs index c996b1cf4b8..cd6776b46ec 100644 --- a/core/src/bls_sigverify/utils.rs +++ b/core/src/bls_sigverify/utils.rs @@ -1,13 +1,11 @@ use { super::{errors::SigVerifyVoteError, stats::SigVerifyVoteStats}, crate::{ + block_creation_loop::rewards::msg_types::AddVoteMessage, bls_sigverify::{errors::SigVerifyCertError, stats::SigVerifyCertStats}, cluster_info_vote_listener::VerifiedVoterSlotsSender, }, - agave_votor::{ - consensus_metrics::{ConsensusMetricsEvent, ConsensusMetricsEventSender}, - consensus_rewards::AddVoteMessage, - }, + agave_votor::consensus_metrics::{ConsensusMetricsEvent, ConsensusMetricsEventSender}, agave_votor_messages::consensus_message::ConsensusMessage, crossbeam_channel::{Sender, TrySendError}, solana_clock::Slot, diff --git a/core/src/tvu.rs b/core/src/tvu.rs index 8a20447fcfb..f1853c5e114 100644 --- a/core/src/tvu.rs +++ b/core/src/tvu.rs @@ -5,7 +5,7 @@ use { crate::{ admin_rpc_post_init::{KeyUpdaterType, KeyUpdaters}, banking_trace::BankingTracer, - block_creation_loop::ReplayHighestFrozen, + block_creation_loop::{ReplayHighestFrozen, rewards::msg_types::AddVoteMessage}, bls_sigverify::bls_sigverifier::{self, SigVerifierChannels, SigVerifierContext}, cluster_info_vote_listener::{ DuplicateConfirmedSlotsReceiver, GossipVerifiedVoteHashReceiver, @@ -30,7 +30,6 @@ use { }, agave_votor::{ consensus_metrics::MAX_IN_FLIGHT_CONSENSUS_EVENTS, - consensus_rewards::BuildRewardCertsRequest, event::{LatestSwitchRequest, LeaderWindowInfo, VotorEventReceiver, VotorEventSender}, generated_cert_types::GeneratedCertTypes, vote_history::VoteHistory, @@ -98,7 +97,7 @@ use { const CHANNEL_SIZE_RETRANSMIT_INGRESS: usize = 16 * 1024; /// The maximum number of alpenglow packets that can be processed in a single batch -const MAX_ALPENGLOW_PACKET_NUM: usize = 10_000; +pub(crate) const MAX_ALPENGLOW_PACKET_NUM: usize = 10_000; /// The maximum number of distinct bls messages that can be sent in a single batch. /// This is overprovisioned to account for standstill scenarios, where a large amount /// of votes / certificate need to be refreshed. @@ -188,9 +187,6 @@ pub struct AlpenglowInitializationState { // For BLS voting service pub bls_connection_cache: Arc, pub voting_service_test_override: Option, - - // For rewards - pub build_reward_certs_receiver: Receiver, } impl Tvu { @@ -245,6 +241,7 @@ impl Tvu { slot_status_notifier: Option, vote_connection_cache: Arc, votor_init: AlpenglowInitializationState, + reward_votes_sender: Sender, ) -> Result { let migration_status = bank_forks.read().unwrap().migration_status(); @@ -273,13 +270,11 @@ impl Tvu { bls_connection_cache, voting_service_test_override, highest_finalized, - build_reward_certs_receiver, } = votor_init; // streamer and sigverify for A2A BLS messages let (consensus_message_sender, consensus_message_receiver) = bounded(MAX_ALPENGLOW_PACKET_NUM); - let (reward_votes_sender, reward_votes_receiver) = bounded(MAX_ALPENGLOW_PACKET_NUM); let (consensus_metrics_sender, consensus_metrics_receiver) = bounded(MAX_IN_FLIGHT_CONSENSUS_EVENTS); let generated_cert_types = Arc::new(GeneratedCertTypes::default()); @@ -524,8 +519,6 @@ impl Tvu { event_receiver: votor_event_receiver, consensus_message_receiver, consensus_metrics_receiver, - reward_votes_receiver, - build_reward_certs_receiver, }; let votor = Votor::new(votor_config); @@ -829,10 +822,10 @@ pub mod tests { thread::sleep(Duration::from_secs(1)); } }); - let (_build_reward_certs_sender, build_reward_certs_receiver) = bounded(1); let (bank_forks_controller, bank_forks_controller_receiver) = BankForksControllerHandle::new(); let bank_forks_controller = Arc::new(bank_forks_controller); + let (reward_votes_sender, _reward_votes_receiver) = unbounded(); let tvu = Tvu::new( &vote_keypair.pubkey(), @@ -906,8 +899,8 @@ pub mod tests { highest_finalized: Arc::new(RwLock::new(None)), bank_forks_controller, bank_forks_controller_receiver, - build_reward_certs_receiver, }, + reward_votes_sender, ) .expect("assume success"); exit.store(true, Ordering::Relaxed); diff --git a/core/src/validator.rs b/core/src/validator.rs index 8fe389acb19..446ded0fb32 100644 --- a/core/src/validator.rs +++ b/core/src/validator.rs @@ -8,10 +8,7 @@ use { BankingStage, transaction_scheduler::scheduler_controller::SchedulerConfig, }, banking_trace::{self, BankingTracer, TraceError}, - block_creation_loop::{ - BlockCreationLoop, BlockCreationLoopConfig, ReplayHighestFrozen, - reward_certs_handler::RewardCertsHandler, - }, + block_creation_loop::{BlockCreationLoop, BlockCreationLoopConfig, ReplayHighestFrozen}, cluster_info_vote_listener::VoteTracker, completed_data_sets_service::CompletedDataSetsService, consensus::{ @@ -1486,8 +1483,6 @@ impl Validator { // debug if things go off the rails. let (optimistic_parent_sender, optimistic_parent_receiver) = bounded(100); - let (reward_certs_handler, build_reward_certs_receiver) = RewardCertsHandler::new(); - let banking_stage_sender_for_bcl = banking_tracer_channels.non_vote_sender.clone(); let block_creation_loop_config = BlockCreationLoopConfig { @@ -1507,10 +1502,11 @@ impl Validator { record_receiver_receiver, optimistic_parent_receiver: optimistic_parent_receiver.clone(), highest_finalized: highest_finalized.clone(), - reward_certs_handler, banking_stage_sender: banking_stage_sender_for_bcl, + sharable_banks: bank_forks.read().unwrap().sharable_banks(), }; - let block_creation_loop = BlockCreationLoop::new(block_creation_loop_config); + let (block_creation_loop, reward_votes_sender) = + BlockCreationLoop::new(block_creation_loop_config); assert_eq!( blockstore.get_new_shred_signals_len(), @@ -1662,8 +1658,8 @@ impl Validator { bls_connection_cache, voting_service_test_override: config.voting_service_test_override.clone(), highest_finalized, - build_reward_certs_receiver, }, + reward_votes_sender, ) .map_err(ValidatorError::Other)?; diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 113b136a0eb..8cdc34c9910 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -6646,6 +6646,7 @@ dependencies = [ "assert_matches", "async-trait", "bincode", + "bitvec", "bytes", "chrono", "crossbeam-channel", @@ -6719,6 +6720,7 @@ dependencies = [ "solana-shred-version", "solana-signature", "solana-signer", + "solana-signer-store", "solana-slot-hashes", "solana-slot-history", "solana-streamer", diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index 8f0434feb4d..f2e27c0d824 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -6493,6 +6493,7 @@ dependencies = [ "assert_matches", "async-trait", "bincode", + "bitvec", "bytes", "chrono", "crossbeam-channel", @@ -6566,6 +6567,7 @@ dependencies = [ "solana-shred-version", "solana-signature", "solana-signer", + "solana-signer-store", "solana-slot-hashes", "solana-slot-history", "solana-streamer", diff --git a/votor/src/consensus_rewards.rs b/votor/src/consensus_rewards.rs deleted file mode 100644 index cc72fd0719f..00000000000 --- a/votor/src/consensus_rewards.rs +++ /dev/null @@ -1,288 +0,0 @@ -use { - agave_votor_messages::{ - consensus_message::VoteMessage, - reward_certificate::{ - BuildRewardCertsRespError, NUM_SLOTS_FOR_REWARD, NotarRewardCertificate, - SkipRewardCertificate, - }, - vote::Vote, - }, - crossbeam_channel::{Receiver, RecvError, Sender, select_biased}, - entry::Entry, - solana_clock::Slot, - solana_gossip::cluster_info::ClusterInfo, - solana_ledger::leader_schedule_cache::LeaderScheduleCache, - solana_pubkey::Pubkey, - solana_runtime::{bank::Bank, bank_forks::SharableBanks}, - std::{ - collections::BTreeMap, - sync::{ - Arc, - atomic::{AtomicBool, Ordering}, - }, - thread::{self, Builder, JoinHandle}, - time::Duration, - }, -}; - -mod entry; - -/// Returns [`false`] if the rewards container is not interested in the [`VoteMessage`]. -/// Returns [`true`] if the rewards container might be interested in the [`VoteMessage`]. -pub fn wants_vote( - cluster_info: &ClusterInfo, - leader_schedule: &LeaderScheduleCache, - root_slot: Slot, - vote: &VoteMessage, -) -> bool { - match vote.vote { - Vote::Notarize(_) | Vote::Skip(_) => (), - Vote::Finalize(_) - | Vote::NotarizeFallback(_) - | Vote::SkipFallback(_) - | Vote::Genesis(_) => return false, - } - let vote_slot = vote.vote.slot(); - if vote_slot.saturating_add(NUM_SLOTS_FOR_REWARD) <= root_slot { - return false; - } - let my_pubkey = cluster_info.id(); - let Some(leader) = - leader_schedule.slot_leader_at(vote_slot.saturating_add(NUM_SLOTS_FOR_REWARD), None) - else { - return false; - }; - if leader.id != my_pubkey { - return false; - } - true -} - -/// Container to store state needed to generate reward certificates. -struct ConsensusRewards { - /// Per [`Slot`], stores skip and notar votes. - votes: BTreeMap, - /// Stores the latest pubkey for the current node. - cluster_info: Arc, - /// Stores the leader schedules. - leader_schedule: Arc, - sharable_banks: SharableBanks, - /// Flag to indicate when the channel receiving loop should exit. - exit: Arc, - /// Channel to receive messages to build reward certificates. - build_reward_certs_receiver: Receiver, - /// Channel to receive verified votes. - votes_receiver: Receiver, -} - -impl ConsensusRewards { - /// Constructs a new instance of [`ConsensusRewards`]. - fn new( - cluster_info: Arc, - leader_schedule: Arc, - sharable_banks: SharableBanks, - exit: Arc, - build_reward_certs_receiver: Receiver, - votes_receiver: Receiver, - ) -> Self { - Self { - votes: BTreeMap::default(), - cluster_info, - leader_schedule, - sharable_banks, - exit, - build_reward_certs_receiver, - votes_receiver, - } - } - - fn handle_request( - &mut self, - msg: Result, - ) -> Result<(), ()> { - let my_pubkey = self.cluster_info.id(); - match msg { - Ok(BuildRewardCertsRequest { - bank_slot, - reply_sender, - }) => { - let resp = BuildRewardCertsResponse { - result: self.build_certs(bank_slot), - }; - let _ = reply_sender.send(resp).inspect_err(|_| { - info!( - "{my_pubkey}: channel to send reply for bank_slot={bank_slot} disconnected" - ); - }); - Ok(()) - } - Err(_) => { - error!("{my_pubkey}: build reward certs channel is disconnected; exiting."); - Err(()) - } - } - } - - /// Runs a loop receiving and handling messages over different channels. - fn run(&mut self) { - while !self.exit.load(Ordering::Relaxed) { - let my_pubkey = self.cluster_info.id(); - // bias messages to build certificates as that is on the critical path - select_biased! { - recv(self.build_reward_certs_receiver) -> msg => { - if let Err(()) = self.handle_request(msg) { - break; - } - } - recv(self.votes_receiver) -> msg => { - match msg { - Ok(msg) => { - let bank = self.sharable_banks.root(); - for vote in msg.votes { - self.add_vote(&bank, &vote); - } - } - Err(_) => { - error!("{my_pubkey}: votes receiver channel is disconnected; exiting."); - break; - } - } - } - default(Duration::from_secs(1)) => { - continue; - } - } - } - } - - /// Returns [`true`] if the rewards container is interested in this vote else [`false`]. - fn wants_vote(&self, root_slot: Slot, vote: &VoteMessage) -> bool { - if !wants_vote(&self.cluster_info, &self.leader_schedule, root_slot, vote) { - return false; - } - let Some(entry) = self.votes.get(&vote.vote.slot()) else { - return true; - }; - entry.wants_vote(vote) - } - - /// Adds received [`VoteMessage`] from other validators. - fn add_vote(&mut self, root_bank: &Bank, vote: &VoteMessage) { - let slot = vote.vote.slot(); - let Some(rank_map) = root_bank.get_rank_map(slot) else { - warn!( - "failed to look up rank_map for slot {slot} using bank for slot {}", - root_bank.slot() - ); - return; - }; - let max_validators = rank_map.len(); - let root_slot = root_bank.slot(); - // drop state that is too old based on how the root slot has progressed - // TODO: if this actually purges state, that probably indicates that the leader missed its - // window. We should have a metric for this. - self.votes = self - .votes - .split_off(&root_slot.saturating_sub(NUM_SLOTS_FOR_REWARD)); - - if !self.wants_vote(root_slot, vote) { - return; - } - match self - .votes - .entry(vote.vote.slot()) - .or_insert(Entry::new(max_validators)) - .add_vote(rank_map, vote) - { - Ok(()) => (), - Err(e) => { - warn!("Adding vote {vote:?} failed with {e}"); - } - } - } - - /// Builds reward certificates. - fn build_certs( - &mut self, - bank_slot: Slot, - ) -> Result { - let Some(reward_slot) = bank_slot.checked_sub(NUM_SLOTS_FOR_REWARD) else { - return Ok(BuildRewardCertsRespSucc::default()); - }; - // we assume that the block creation loop will only ever request to build reward certs in a strictly increasing order so we can drop older state - self.votes = self.votes.split_off(&reward_slot); - match self.votes.remove(&reward_slot) { - None => Ok(BuildRewardCertsRespSucc::default()), - Some(entry) => entry.build_certs(reward_slot), - } - } -} - -/// Service to run the consensus reward container in a dedicated thread. -pub struct ConsensusRewardsService { - handle: JoinHandle<()>, -} - -impl ConsensusRewardsService { - /// Creates a new instance of [`ConsensusRewardsService`]. - pub fn new( - cluster_info: Arc, - leader_schedule: Arc, - sharable_banks: SharableBanks, - exit: Arc, - votes_receiver: Receiver, - build_reward_certs_receiver: Receiver, - ) -> Self { - let handle = Builder::new() - .name("solConsRew".to_string()) - .spawn(move || { - ConsensusRewards::new( - cluster_info, - leader_schedule, - sharable_banks, - exit, - build_reward_certs_receiver, - votes_receiver, - ) - .run(); - }) - .unwrap(); - Self { handle } - } - - pub fn join(self) -> thread::Result<()> { - self.handle.join() - } -} - -/// Request to build reward certificates. -pub struct BuildRewardCertsRequest { - /// The bank slot which will include the built reward certs. - pub bank_slot: Slot, - /// The channel on which to send the reply. - pub reply_sender: Sender, -} - -/// Response when the reward certs are built successfully. -#[derive(Default, Debug, PartialEq, Eq)] -pub struct BuildRewardCertsRespSucc { - /// Skip reward certificate. None if no skip votes were registered. - pub skip: Option, - /// Notar reward certificate. None if no notar votes were registered. - pub notar: Option, - /// If at least one of the certs above is present, then this contains the slot for which the reward certs were built and the list of validators in the certs. - pub validators: Vec, -} - -/// Response to a [`BuildRewardCertsRequest`]. -pub struct BuildRewardCertsResponse { - /// The result of building reward certs for `bank_slot`. - pub result: Result, -} - -/// Message to add votes to the rewards container. -#[derive(Debug)] -pub struct AddVoteMessage { - /// List of [`VoteMessage`]s. - pub votes: Vec, -} diff --git a/votor/src/lib.rs b/votor/src/lib.rs index 5cb854b7c87..e3cf0a52fa9 100644 --- a/votor/src/lib.rs +++ b/votor/src/lib.rs @@ -9,7 +9,6 @@ pub mod common; pub mod consensus_metrics; pub mod consensus_pool; mod consensus_pool_service; -pub mod consensus_rewards; pub mod event; mod event_handler; pub mod generated_cert_types; diff --git a/votor/src/votor.rs b/votor/src/votor.rs index 99ed69be371..5b980ed4b29 100644 --- a/votor/src/votor.rs +++ b/votor/src/votor.rs @@ -50,7 +50,6 @@ use { ConsensusMetrics, ConsensusMetricsEventReceiver, ConsensusMetricsEventSender, }, consensus_pool_service::{ConsensusPoolContext, ConsensusPoolService}, - consensus_rewards::{AddVoteMessage, BuildRewardCertsRequest, ConsensusRewardsService}, event::{ LatestSwitchRequest, LeaderWindowInfo, RepairEventSender, VotorEventReceiver, VotorEventSender, @@ -121,8 +120,6 @@ pub struct VotorConfig { pub event_receiver: VotorEventReceiver, pub consensus_message_receiver: Receiver>, pub consensus_metrics_receiver: ConsensusMetricsEventReceiver, - pub reward_votes_receiver: Receiver, - pub build_reward_certs_receiver: Receiver, } /// Context shared with block creation, replay, gossip, banking stage etc @@ -141,7 +138,6 @@ pub struct Votor { event_handler: EventHandler, consensus_pool_service: ConsensusPoolService, timer_manager: Arc>, - consensus_rewards_service: ConsensusRewardsService, metrics: JoinHandle<()>, } @@ -171,8 +167,6 @@ impl Votor { consensus_message_receiver, consensus_metrics_sender, consensus_metrics_receiver, - reward_votes_receiver, - build_reward_certs_receiver, generated_cert_types, highest_finalized, bank_forks_controller, @@ -256,19 +250,10 @@ impl Votor { ); let event_handler = EventHandler::new(event_handler_context); let consensus_pool_service = ConsensusPoolService::new(consensus_pool_context); - let consensus_rewards_service = ConsensusRewardsService::new( - cluster_info, - leader_schedule_cache, - sharable_banks, - exit, - reward_votes_receiver, - build_reward_certs_receiver, - ); Self { event_handler, consensus_pool_service, - consensus_rewards_service, timer_manager, metrics, } @@ -276,7 +261,6 @@ impl Votor { pub fn join(self) -> thread::Result<()> { self.consensus_pool_service.join()?; - self.consensus_rewards_service.join()?; // Loop till we manage to unwrap the Arc and then we can join. let mut timer_manager = self.timer_manager; From 8842e2c734fbc9cef756122684fd0a856ce4aebe Mon Sep 17 00:00:00 2001 From: Nikita Belenkov Date: Mon, 1 Jun 2026 16:19:53 -0400 Subject: [PATCH 160/576] ci: adding 7 days delay to all dependabot updates (#12889) adding 7 days delay to all deps --- .github/dependabot.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 42f7002869a..200c581e0ab 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,6 +8,8 @@ updates: time: "08:00" timezone: Etc/UTC open-pull-requests-limit: 6 + cooldown: + default-days: 7 # split from root directory schedule to avoid duplicate checks - package-ecosystem: cargo @@ -20,6 +22,8 @@ updates: time: "14:00" timezone: Etc/UTC open-pull-requests-limit: 6 + cooldown: + default-days: 7 - package-ecosystem: "npm" directory: "/docs" @@ -42,3 +46,5 @@ updates: time: "08:00" timezone: Etc/UTC open-pull-requests-limit: 3 + cooldown: + default-days: 7 From 76c17f0a976f7b6d8a4737890ae1f12fcfd31aed Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Mon, 1 Jun 2026 16:42:55 -0400 Subject: [PATCH 161/576] bank: update hashes_per_tick to None for Alpenglow banks (#12782) --- core/src/block_creation_loop.rs | 16 +++- entry/src/poh.rs | 4 + local-cluster/src/local_cluster.rs | 1 + local-cluster/tests/local_cluster.rs | 6 +- poh/src/poh_recorder.rs | 94 ++++++++++++++++++------ poh/src/poh_service.rs | 2 +- runtime/src/bank.rs | 26 +++---- runtime/src/block_component_processor.rs | 1 + runtime/src/genesis_utils.rs | 3 + 9 files changed, 110 insertions(+), 43 deletions(-) diff --git a/core/src/block_creation_loop.rs b/core/src/block_creation_loop.rs index 47c3b30dc34..60bfb623983 100644 --- a/core/src/block_creation_loop.rs +++ b/core/src/block_creation_loop.rs @@ -1265,6 +1265,13 @@ fn create_and_insert_leader_bank( &parent_bank.hash(), ); + // If this is the first alpenglow block, set PoH to low power mode. + // This is persisted in the recorder when we set bank. + // Replayers will update hashes_per_tick when they process the genesis certificate block marker. + if should_include_genesis_certificate(parent_slot, &ctx.genesis_cert) { + tpu_bank.set_hashes_per_tick(None); + } + // Insert the bank let tpu_bank = ctx.bank_forks_controller.insert_bank(tpu_bank)?; @@ -1298,7 +1305,7 @@ fn maybe_include_genesis_certificate( parent_slot: Slot, ctx: &LeaderContext, ) -> Result<(), PohRecorderError> { - if parent_slot != ctx.genesis_cert.slot || parent_slot == 0 { + if !should_include_genesis_certificate(parent_slot, &ctx.genesis_cert) { return Ok(()); } @@ -1320,6 +1327,13 @@ fn maybe_include_genesis_certificate( Ok(()) } +fn should_include_genesis_certificate( + parent_slot: Slot, + genesis_cert: &GenesisCertificate, +) -> bool { + parent_slot == genesis_cert.slot && parent_slot != 0 +} + #[cfg(test)] mod tests { use { diff --git a/entry/src/poh.rs b/entry/src/poh.rs index 7008a215bbe..bdf6db6b130 100644 --- a/entry/src/poh.rs +++ b/entry/src/poh.rs @@ -52,6 +52,10 @@ impl Poh { self.hashes_per_tick } + pub fn hashes_per_tick_config(&self) -> Option { + (self.hashes_per_tick != LOW_POWER_MODE).then_some(self.hashes_per_tick) + } + pub fn target_poh_time(&self, target_ns_per_tick: u64) -> Instant { assert!(self.hashes_per_tick > 0); let offset_tick_ns = target_ns_per_tick * self.tick_number; diff --git a/local-cluster/src/local_cluster.rs b/local-cluster/src/local_cluster.rs index 96d722b7319..543e2ce99a9 100644 --- a/local-cluster/src/local_cluster.rs +++ b/local-cluster/src/local_cluster.rs @@ -205,6 +205,7 @@ impl LocalCluster { } pub fn new_alpenglow(config: &mut ClusterConfig, socket_addr_space: SocketAddrSpace) -> Self { + config.poh_config.hashes_per_tick = None; Self::init(config, socket_addr_space, AlpenglowMode::Enabled) } diff --git a/local-cluster/tests/local_cluster.rs b/local-cluster/tests/local_cluster.rs index 14b38494231..f394e9a5c0a 100644 --- a/local-cluster/tests/local_cluster.rs +++ b/local-cluster/tests/local_cluster.rs @@ -16,9 +16,7 @@ use { solana_account::AccountSharedData, solana_accounts_db::utils::create_accounts_run_and_snapshot_dirs, solana_client_traits::AsyncClient, - solana_clock::{ - self as clock, DEFAULT_SLOTS_PER_EPOCH, DEFAULT_TICKS_PER_SLOT, MAX_PROCESSING_AGE, Slot, - }, + solana_clock::{DEFAULT_SLOTS_PER_EPOCH, DEFAULT_TICKS_PER_SLOT, MAX_PROCESSING_AGE, Slot}, solana_cluster_type::ClusterType, solana_commitment_config::CommitmentConfig, solana_core::{ @@ -5858,7 +5856,7 @@ fn test_alpenglow_nodes_basic(num_nodes: usize, num_offline_nodes: usize) { stakers_slot_offset: MINIMUM_SLOTS_PER_EPOCH * 2, poh_config: PohConfig { target_tick_duration: PohConfig::default().target_tick_duration, - hashes_per_tick: Some(clock::DEFAULT_HASHES_PER_TICK), + hashes_per_tick: None, target_tick_count: None, }, ..ClusterConfig::default() diff --git a/poh/src/poh_recorder.rs b/poh/src/poh_recorder.rs index b7585871dfa..58b897df2ac 100644 --- a/poh/src/poh_recorder.rs +++ b/poh/src/poh_recorder.rs @@ -218,11 +218,6 @@ pub struct PohRecorder { // Allocation to hold PohEntrys recorded into PoHStream. entries: Vec, - - /// When alpenglow is enabled there will be no ticks apart from a final one - /// to complete the block. This tick will not be verified, and we use this - /// flag to unset hashes_per_tick - alpenglow_enabled: bool, } impl PohRecorder { @@ -305,7 +300,6 @@ impl PohRecorder { last_reported_slot_for_pending_fork: Arc::default(), is_exited, entries: Vec::with_capacity(64), - alpenglow_enabled: false, }, working_bank_receiver, ) @@ -479,18 +473,17 @@ impl PohRecorder { trace!("new working bank"); assert_eq!(working_bank.bank.ticks_per_slot(), self.ticks_per_slot()); let mut tick_height = self.tick_height(); - if let Some(hashes_per_tick) = *working_bank.bank.hashes_per_tick() { - if self.poh.lock().unwrap().hashes_per_tick() != hashes_per_tick { - // We must clear/reset poh when changing hashes per tick because it's - // possible there are ticks in the cache created with the old hashes per - // tick value that would get flushed later. This would corrupt the leader's - // block and it would be disregarded by the network. - info!( - "resetting poh due to hashes per tick change detected at {}", - working_bank.bank.slot() - ); - tick_height = self.reset_poh(working_bank.bank.clone(), false); - } + let hashes_per_tick = working_bank.bank.hashes_per_tick(); + if self.poh.lock().unwrap().hashes_per_tick_config() != hashes_per_tick { + // We must clear/reset poh when changing hashes per tick because it's + // possible there are ticks in the cache created with the old hashes per + // tick value that would get flushed later. This would corrupt the leader's + // block and it would be disregarded by the network. + info!( + "resetting poh due to hashes per tick change detected at {}", + working_bank.bank.slot() + ); + tick_height = self.reset_poh(working_bank.bank.clone(), false); } let leader_state = self.shared_leader_state.load(); @@ -568,11 +561,7 @@ impl PohRecorder { #[must_use] fn reset_poh(&mut self, reset_bank: Arc, reset_start_bank: bool) -> u64 { let blockhash = reset_bank.last_blockhash(); - let hashes_per_tick = if self.alpenglow_enabled { - None - } else { - *reset_bank.hashes_per_tick() - }; + let hashes_per_tick = reset_bank.hashes_per_tick(); let poh_hash = { let mut poh = self.poh.lock().unwrap(); poh.reset(blockhash, hashes_per_tick); @@ -1083,7 +1072,6 @@ impl PohRecorder { pub fn enable_alpenglow(&mut self) { info!("Enabling Alpenglow, migrating poh to low power mode"); - self.alpenglow_enabled = true; self.tick_cache = vec![]; { let mut poh = self.poh.lock().unwrap(); @@ -1499,6 +1487,64 @@ mod tests { assert!(!poh_recorder.tick_cache.is_empty()); } + #[test] + fn test_set_bank_resets_to_low_power_mode() { + let ledger_path = get_tmp_ledger_path_auto_delete!(); + let blockstore = Arc::new( + Blockstore::open(ledger_path.path()).expect("Expected to be able to open ledger"), + ); + let GenesisConfigInfo { + mut genesis_config, .. + } = create_genesis_config(2); + genesis_config.poh_config.hashes_per_tick = Some(4); + let (parent, bank_forks) = + Bank::new_for_tests(&genesis_config).wrap_with_bank_forks_for_tests(); + let child = Bank::new_from_parent_with_bank_forks( + bank_forks.as_ref(), + parent.clone(), + SlotLeader::default(), + parent.slot() + 1, + ); + child.set_hashes_per_tick(None); + child.set_tick_height(child.max_tick_height() - 1); + + let prev_hash = parent.last_blockhash(); + let (mut poh_recorder, _entry_receiver) = PohRecorder::new( + parent.tick_height(), + prev_hash, + parent.clone(), + Some((child.slot(), child.slot())), + parent.ticks_per_slot(), + blockstore, + &Arc::new(LeaderScheduleCache::new_from_bank(&parent)), + &genesis_config.poh_config, + Arc::new(AtomicBool::default()), + ); + assert_eq!( + poh_recorder.poh.lock().unwrap().hashes_per_tick_config(), + Some(4) + ); + + poh_recorder.set_bank_for_test(child.clone()); + assert_eq!( + poh_recorder.poh.lock().unwrap().hashes_per_tick_config(), + None + ); + + let bank_to_freeze = child.clone(); + let freezer = std::thread::spawn(move || { + while bank_to_freeze.tick_height() < bank_to_freeze.max_tick_height() { + std::hint::spin_loop(); + } + bank_to_freeze.freeze(); + }); + poh_recorder + .tick_alpenglow(child.max_tick_height(), test_footer()) + .unwrap(); + freezer.join().unwrap(); + assert!(!poh_recorder.has_bank()); + } + #[test] fn test_poh_recorder_tick_verifies() { // Setup diff --git a/poh/src/poh_service.rs b/poh/src/poh_service.rs index 5686afea25e..80b9719e746 100644 --- a/poh/src/poh_service.rs +++ b/poh/src/poh_service.rs @@ -729,7 +729,7 @@ mod tests { PohConfig::default().target_tick_duration.as_micros() as u64; let target_tick_duration = Duration::from_micros(default_target_tick_duration); let poh_config = PohConfig { - hashes_per_tick: *bank.hashes_per_tick(), + hashes_per_tick: bank.hashes_per_tick(), target_tick_duration, target_tick_count: None, }; diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 133a82b789d..ca2f1774025 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -671,7 +671,7 @@ impl PartialEq for Bank { && signature_count.load(Relaxed) == other.signature_count.load(Relaxed) && capitalization.load(Relaxed) == other.capitalization.load(Relaxed) && max_tick_height == &other.max_tick_height - && hashes_per_tick == &other.hashes_per_tick + && *hashes_per_tick.read().unwrap() == *other.hashes_per_tick.read().unwrap() && ticks_per_slot == &other.ticks_per_slot && ns_per_slot == &other.ns_per_slot && genesis_creation_time == &other.genesis_creation_time @@ -866,7 +866,7 @@ pub struct Bank { max_tick_height: u64, /// The number of hashes in each tick. None value means hashing is disabled. - hashes_per_tick: Option, + hashes_per_tick: RwLock>, /// The number of ticks in each slot. ticks_per_slot: u64, @@ -1180,7 +1180,7 @@ impl Bank { signature_count: AtomicU64::default(), capitalization: AtomicU64::default(), max_tick_height: u64::default(), - hashes_per_tick: Option::::default(), + hashes_per_tick: RwLock::default(), ticks_per_slot: u64::default(), ns_per_slot: u128::default(), genesis_creation_time: UnixTimestamp::default(), @@ -1411,7 +1411,7 @@ impl Bank { partitioned_rewards_stake_account_stores_per_block: parent .partitioned_rewards_stake_account_stores_per_block, // TODO: clean this up, so much special-case copying... - hashes_per_tick: parent.hashes_per_tick, + hashes_per_tick: RwLock::new(parent.hashes_per_tick()), ticks_per_slot: parent.ticks_per_slot, ns_per_slot: parent.ns_per_slot, genesis_creation_time: parent.genesis_creation_time, @@ -2112,7 +2112,7 @@ impl Bank { signature_count: AtomicU64::new(fields.signature_count), capitalization: AtomicU64::new(fields.capitalization), max_tick_height: fields.max_tick_height, - hashes_per_tick: fields.hashes_per_tick, + hashes_per_tick: RwLock::new(fields.hashes_per_tick), ticks_per_slot: fields.ticks_per_slot, ns_per_slot: fields.ns_per_slot, genesis_creation_time: fields.genesis_creation_time, @@ -2255,7 +2255,7 @@ impl Bank { signature_count: self.signature_count.load(Relaxed), capitalization: self.capitalization.load(Relaxed), max_tick_height: self.max_tick_height, - hashes_per_tick: self.hashes_per_tick, + hashes_per_tick: *self.hashes_per_tick.read().unwrap(), ticks_per_slot: self.ticks_per_slot, ns_per_slot: self.ns_per_slot, genesis_creation_time: self.genesis_creation_time, @@ -3091,7 +3091,7 @@ impl Bank { genesis_config.fee_rate_governor.lamports_per_signature, ); - self.hashes_per_tick = genesis_config.hashes_per_tick(); + self.hashes_per_tick = RwLock::new(genesis_config.hashes_per_tick()); self.ticks_per_slot = genesis_config.ticks_per_slot(); self.ns_per_slot = genesis_config.ns_per_slot(); self.genesis_creation_time = genesis_config.creation_time; @@ -3166,8 +3166,8 @@ impl Bank { self.rent_collector.rent.burn_percent = burn_percent; } - pub fn set_hashes_per_tick(&mut self, hashes_per_tick: Option) { - self.hashes_per_tick = hashes_per_tick; + pub fn set_hashes_per_tick(&self, hashes_per_tick: Option) { + *self.hashes_per_tick.write().unwrap() = hashes_per_tick; } /// Return the last block hash registered. @@ -5472,8 +5472,8 @@ impl Bank { } /// Return the number of hashes per tick - pub fn hashes_per_tick(&self) -> &Option { - &self.hashes_per_tick + pub fn hashes_per_tick(&self) -> Option { + *self.hashes_per_tick.read().unwrap() } /// Return the number of ticks per slot @@ -6575,7 +6575,7 @@ impl Bank { bank.signature_count = AtomicU64::new(fields.signature_count); bank.capitalization = AtomicU64::new(fields.capitalization); bank.max_tick_height = fields.max_tick_height; - bank.hashes_per_tick = fields.hashes_per_tick; + bank.hashes_per_tick = RwLock::new(fields.hashes_per_tick); bank.ticks_per_slot = fields.ticks_per_slot; bank.ns_per_slot = fields.ns_per_slot; bank.genesis_creation_time = fields.genesis_creation_time; @@ -6606,7 +6606,7 @@ impl Bank { bank.refresh_slot_params_with_baseline(SlotParams::genesis_baseline( bank.ns_per_slot, bank.slots_per_year, - bank.hashes_per_tick, + bank.hashes_per_tick(), bank.partitioned_rewards_stake_account_stores_per_block, )); diff --git a/runtime/src/block_component_processor.rs b/runtime/src/block_component_processor.rs index 42da04a0d2e..e286755611f 100644 --- a/runtime/src/block_component_processor.rs +++ b/runtime/src/block_component_processor.rs @@ -245,6 +245,7 @@ impl BlockComponentProcessor { let genesis_cert = Certificate::from(genesis_cert); Self::verify_genesis_certificate(&bank, &genesis_cert)?; bank.set_alpenglow_genesis_certificate(&genesis_cert); + bank.set_hashes_per_tick(None); if migration_status.is_alpenglow_enabled() { // We participated in the migration, nothing to do diff --git a/runtime/src/genesis_utils.rs b/runtime/src/genesis_utils.rs index cdc620a7496..92b7bad9b11 100644 --- a/runtime/src/genesis_utils.rs +++ b/runtime/src/genesis_utils.rs @@ -322,6 +322,9 @@ pub fn create_genesis_config_with_leader_with_mint_keypair( pub fn activate_all_features_alpenglow(genesis_config: &mut GenesisConfig) { do_activate_all_features::(genesis_config); + // PoH is in low power mode + genesis_config.poh_config.hashes_per_tick = None; + // This is a dev cluster with alpenglow enabled at genesis. We don't want to test the migration pathway // so we add a fake genesis certificate. let cert = Certificate { From ce0aca9c230e84b2894bca7c8ef91bacff5bd446 Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Mon, 1 Jun 2026 17:52:26 -0400 Subject: [PATCH 162/576] votor: properly handle initial parent ready after a restart (#12811) * votor: properly handle initial parent ready after a restart * pr feedback: produce block, comment on highest parent ready choice --- votor/src/consensus_pool.rs | 39 ++-- .../consensus_pool/parent_ready_tracker.rs | 77 +++++-- votor/src/consensus_pool_service.rs | 195 +++++++++++++++--- votor/src/event_handler.rs | 41 +++- votor/src/timer_manager.rs | 7 +- votor/src/timer_manager/timers.rs | 3 +- votor/src/vote_history.rs | 24 ++- votor/src/votor.rs | 2 + 8 files changed, 299 insertions(+), 89 deletions(-) diff --git a/votor/src/consensus_pool.rs b/votor/src/consensus_pool.rs index 6a68d0234ce..6c9bbb2823d 100644 --- a/votor/src/consensus_pool.rs +++ b/votor/src/consensus_pool.rs @@ -7,7 +7,7 @@ use { Stake, conflicting_types, vote_to_cert_types, }, consensus_pool::{ - parent_ready_tracker::ParentReadyTracker, + parent_ready_tracker::{ParentReady, ParentReadyTracker}, slot_stake_counters::SlotStakeCounters, stats::ConsensusPoolStats, vote_pool::{DuplicateBlockVotePool, SimpleVotePool, VotePool}, @@ -123,8 +123,8 @@ pub(crate) struct ConsensusPool { stats: ConsensusPoolStats, /// Slot stake counters, used to calculate safe_to_notar and safe_to_skip slot_stake_counters_map: BTreeMap, - /// Stores details about the genesis vote during the migration - migration_status: Option>, + /// Stores details about the genesis vote during the migration. + migration_status: Arc, /// Pending safe-to-notar blocks for intrawindow slots that need parent verification. /// These are blocks that have reached the safe-to-notar threshold but are not the /// first block in their leader window. They need to be verified that their parent @@ -135,26 +135,15 @@ pub(crate) struct ConsensusPool { } impl ConsensusPool { - pub(crate) fn new_from_root_bank_pre_migration( + pub(crate) fn new( cluster_info: Arc, - bank: &Bank, + root: &Bank, generated_cert_types: Arc, migration_status: Arc, + initial_parent_ready: ParentReady, ) -> Self { - let mut pool = Self::new_from_root_bank(cluster_info, bank, generated_cert_types); - pool.migration_status = Some(migration_status); - pool - } - - pub fn new_from_root_bank( - cluster_info: Arc, - bank: &Bank, - generated_cert_types: Arc, - ) -> Self { - // To account for genesis and snapshots we allow default block id until - // block id can be serialized as part of the snapshot - let root_block = (bank.slot(), bank.block_id().unwrap_or_default()); - let parent_ready_tracker = ParentReadyTracker::new(cluster_info.clone(), root_block); + let parent_ready_tracker = + ParentReadyTracker::new(cluster_info.clone(), root.slot(), initial_parent_ready); Self { cluster_info, @@ -164,7 +153,7 @@ impl ConsensusPool { parent_ready_tracker, stats: ConsensusPoolStats::default(), slot_stake_counters_map: BTreeMap::new(), - migration_status: None, + migration_status, generated_cert_types, pending_safe_to_notar: vec![], last_pruned_slot: 0, @@ -373,9 +362,7 @@ impl ConsensusPool { } } CertificateType::Genesis(slot, block_id) => { - if let Some(ref migration_status) = self.migration_status { - migration_status.set_genesis_certificate(cert); - } + self.migration_status.set_genesis_certificate(cert); // The genesis block is automatically certified self.parent_ready_tracker .add_new_notar_fallback_or_stronger((slot, block_id), events); @@ -755,12 +742,16 @@ mod tests { let root_bank = bank_forks.read().unwrap().root_bank(); let generated_cert_types = Arc::new(GeneratedCertTypes::default()); let cluster_info = get_cluster_info(Keypair::new()); + let root_block = (root_bank.slot(), root_bank.block_id().unwrap_or_default()); + let initial_parent_ready = (root_bank.slot().checked_add(1).unwrap(), root_block); Self { validators: validator_keypairs, - pool: ConsensusPool::new_from_root_bank( + pool: ConsensusPool::new( cluster_info, &root_bank, generated_cert_types.clone(), + Arc::new(MigrationStatus::post_migration_status()), + initial_parent_ready, ), bank_forks, generated_cert_types, diff --git a/votor/src/consensus_pool/parent_ready_tracker.rs b/votor/src/consensus_pool/parent_ready_tracker.rs index 5c5f1729135..93c6a45b200 100644 --- a/votor/src/consensus_pool/parent_ready_tracker.rs +++ b/votor/src/consensus_pool/parent_ready_tracker.rs @@ -22,6 +22,8 @@ use { std::{collections::HashMap, sync::Arc}, }; +pub(crate) type ParentReady = (Slot, Block); + #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub(crate) enum BlockProductionParent { MissedWindow, @@ -63,30 +65,49 @@ struct ParentReadyStatus { } impl ParentReadyTracker { - /// Creates a new tracker with the root bank as implicitely notarized fallback - pub(super) fn new(cluster_info: Arc, root_block @ (root_slot, _): Block) -> Self { + /// Creates a new tracker with the selected startup parent-ready state. + pub(super) fn new( + cluster_info: Arc, + root: Slot, + (slot, parent_block @ (parent_slot, _)): ParentReady, + ) -> Self { + debug_assert!(parent_slot < slot); let mut slot_statuses = HashMap::new(); + // Parent block is notarize fallback slot_statuses.insert( - root_slot, + parent_slot, ParentReadyStatus { skip: false, - notar_fallbacks: vec![root_block], + notar_fallbacks: vec![parent_block], parents_ready: vec![], }, ); + // Intermediate blocks have skips + for s in (parent_slot.checked_add(1).unwrap())..slot { + slot_statuses.insert( + s, + ParentReadyStatus { + skip: true, + notar_fallbacks: vec![], + parents_ready: vec![parent_block], + }, + ); + } + // Initial slot is parent ready slot_statuses.insert( - root_slot.saturating_add(1), + slot, ParentReadyStatus { skip: false, notar_fallbacks: vec![], - parents_ready: vec![root_block], + parents_ready: vec![parent_block], }, ); + Self { cluster_info: DebugIgnore(cluster_info), slot_statuses, - root: root_slot, - highest_with_parent_ready: root_slot.saturating_add(1), + root: parent_slot.min(root), + highest_with_parent_ready: slot.max(root), } } @@ -257,11 +278,22 @@ mod tests { solana_keypair::Keypair, }; + fn root_parent_ready(root_block @ (root_slot, _): Block) -> ParentReady { + (root_slot.saturating_add(1), root_block) + } + + fn new_tracker( + cluster_info: Arc, + root_block @ (root_slot, _): Block, + ) -> ParentReadyTracker { + ParentReadyTracker::new(cluster_info, root_slot, root_parent_ready(root_block)) + } + #[test] fn basic() { let cluster_info = get_cluster_info(Keypair::new()); let genesis = Block::default(); - let mut tracker = ParentReadyTracker::new(cluster_info, genesis); + let mut tracker = new_tracker(cluster_info, genesis); let mut events = vec![]; for i in 1..2 * NUM_CONSECUTIVE_LEADER_SLOTS.get() as Slot { @@ -276,7 +308,7 @@ mod tests { fn skips() { let cluster_info = get_cluster_info(Keypair::new()); let genesis = Block::default(); - let mut tracker = ParentReadyTracker::new(cluster_info, genesis); + let mut tracker = new_tracker(cluster_info, genesis); let mut events = vec![]; let block = (1, Hash::new_unique()); @@ -294,7 +326,7 @@ mod tests { fn out_of_order() { let cluster_info = get_cluster_info(Keypair::new()); let genesis = Block::default(); - let mut tracker = ParentReadyTracker::new(cluster_info, genesis); + let mut tracker = new_tracker(cluster_info, genesis); let mut events = vec![]; let block = (1, Hash::new_unique()); @@ -315,7 +347,7 @@ mod tests { let cluster_info = get_cluster_info(Keypair::new()); let root_slot = 2147; let root_block = (root_slot, Hash::new_unique()); - let mut tracker = ParentReadyTracker::new(cluster_info, root_block); + let mut tracker = new_tracker(cluster_info, root_block); let mut events = vec![]; assert!(tracker.parent_ready(root_slot + 1, root_block)); @@ -339,11 +371,26 @@ mod tests { assert_eq!(tracker.highest_parent_ready(), root_slot + 5); } + #[test] + fn restored_parent_ready() { + let cluster_info = get_cluster_info(Keypair::new()); + let root_slot = 4; + let restored = (13, Hash::new_unique()); + let tracker = ParentReadyTracker::new(cluster_info, root_slot, (16, restored)); + + assert!(tracker.parent_ready(16, restored)); + assert_eq!(tracker.highest_parent_ready(), 16); + assert_eq!( + tracker.block_production_parent(16), + BlockProductionParent::Parent(restored) + ); + } + #[test] fn highest_parent_ready_out_of_order() { let cluster_info = get_cluster_info(Keypair::new()); let genesis = Block::default(); - let mut tracker = ParentReadyTracker::new(cluster_info, genesis); + let mut tracker = new_tracker(cluster_info, genesis); let mut events = vec![]; assert_eq!(tracker.highest_parent_ready(), 1); @@ -366,7 +413,7 @@ mod tests { fn missed_window() { let cluster_info = get_cluster_info(Keypair::new()); let genesis = Block::default(); - let mut tracker = ParentReadyTracker::new(cluster_info, genesis); + let mut tracker = new_tracker(cluster_info, genesis); let mut events = vec![]; assert_eq!(tracker.highest_parent_ready(), 1); assert_eq!( @@ -397,7 +444,7 @@ mod tests { fn pick_more_skips() { let cluster_info = get_cluster_info(Keypair::new()); let genesis = Block::default(); - let mut tracker = ParentReadyTracker::new(cluster_info, genesis); + let mut tracker = new_tracker(cluster_info, genesis); let mut events = vec![]; for i in 1..=10 { diff --git a/votor/src/consensus_pool_service.rs b/votor/src/consensus_pool_service.rs index 9a1f3cf5310..c4c3a0a6aac 100644 --- a/votor/src/consensus_pool_service.rs +++ b/votor/src/consensus_pool_service.rs @@ -8,7 +8,8 @@ use { commitment::{CommitmentAggregationData, CommitmentType, update_commitment_cache}, common::DELTA_STANDSTILL, consensus_pool::{ - AddVoteError, ConsensusPool, parent_ready_tracker::BlockProductionParent, + AddVoteError, ConsensusPool, + parent_ready_tracker::{BlockProductionParent, ParentReady}, }, event::{LeaderWindowInfo, RepairEvent, RepairEventSender, VotorEvent, VotorEventSender}, generated_cert_types::GeneratedCertTypes, @@ -52,6 +53,7 @@ pub(crate) struct ConsensusPoolContext { pub(crate) blockstore: Arc, pub(crate) sharable_banks: SharableBanks, pub(crate) leader_schedule_cache: Arc, + pub(crate) vote_history_highest_parent_ready: Option<(Slot, Block)>, pub(crate) consensus_message_receiver: Receiver>, @@ -224,27 +226,60 @@ impl ConsensusPoolService { Err(()) } + /// Finds the initial parent ready that we should use for instantiating the ConsensusPool or kicking off votor + /// The max of genesis block, root block, or the restored parent ready from vote history + fn initial_parent_ready( + genesis_block: Option, + root_block @ (root_slot, _): Block, + vote_history_highest_parent_ready: Option<(Slot, Block)>, + ) -> ParentReady { + let Some(genesis_block) = genesis_block else { + // Alpenglow is not yet enabled, start with just the root + return (root_slot.checked_add(1).unwrap(), root_block); + }; + + let initial_block @ (initial_slot, _) = genesis_block.max(root_block); + let initial_parent_ready = (initial_slot.checked_add(1).unwrap(), initial_block); + + if let Some(restored @ (restored_slot, _)) = vote_history_highest_parent_ready + && restored_slot > initial_parent_ready.0 + { + restored + } else { + initial_parent_ready + } + } + + fn root_block(root_bank: &Bank) -> Block { + ( + root_bank.slot(), + root_bank + .block_id() + // Once SIMD-0333 is active we can hard unwrap here + .unwrap_or_default(), + ) + } + // Main loop for the certificate pool service, it only exits when any channel is disconnected fn consensus_pool_ingest_loop(mut ctx: ConsensusPoolContext) -> Result<(), ()> { let mut events = vec![]; let root_bank = ctx.sharable_banks.root(); - // Unlike the other votor threads, consensus pool starts even before alpenglow is enabled - // As it is required to track the Genesis Vote. - let mut consensus_pool = if ctx.migration_status.is_alpenglow_enabled() { - ConsensusPool::new_from_root_bank( - ctx.cluster_info.clone(), - &root_bank, - ctx.generated_cert_types.clone(), - ) - } else { - ConsensusPool::new_from_root_bank_pre_migration( - ctx.cluster_info.clone(), - &root_bank, - ctx.generated_cert_types.clone(), - ctx.migration_status.clone(), - ) - }; + let initial_parent_ready = Self::initial_parent_ready( + ctx.migration_status.genesis_block(), + Self::root_block(&root_bank), + ctx.vote_history_highest_parent_ready, + ); + + // Unlike the other votor threads, consensus pool starts even before Alpenglow is enabled + // because it must track the genesis vote. + let mut consensus_pool = ConsensusPool::new( + ctx.cluster_info.clone(), + &root_bank, + ctx.generated_cert_types.clone(), + ctx.migration_status.clone(), + initial_parent_ready, + ); info!("{}: Certificate pool loop starting", ctx.cluster_info.id()); let mut stats = ConsensusPoolServiceStats::new(); @@ -264,23 +299,18 @@ impl ConsensusPoolService { // Kick off parent ready event, this either happens: // - When we first migrate to alpenglow from TowerBFT - kick off with genesis block // - If we startup post alpenglow migration - kick off with root block + // - If restored vote history is farther ahead, resume from its highest parent-ready if !kick_off_parent_ready && ctx.migration_status.is_alpenglow_enabled() { - let genesis_block = ctx - .migration_status - .genesis_block() - .expect("Alpenglow is enabled"); let root_bank = ctx.sharable_banks.root(); - // can expect once we have block id in snapshots (SIMD-0333) - let root_block = (root_bank.slot(), root_bank.block_id().unwrap_or_default()); - let kick_off_block @ (kick_off_slot, _) = genesis_block.max(root_block); - let start_slot = kick_off_slot.checked_add(1).unwrap(); - - events.push(VotorEvent::ParentReady { - slot: start_slot, - parent_block: kick_off_block, - }); - highest_parent_ready = start_slot; + let (slot, parent_block) = Self::initial_parent_ready( + ctx.migration_status.genesis_block(), + Self::root_block(&root_bank), + ctx.vote_history_highest_parent_ready, + ); + events.push(VotorEvent::ParentReady { slot, parent_block }); kick_off_parent_ready = true; + // Intentionally do not increment `highest_parent_ready` in case + // this is a cluster restart and we are the very first leader. } Self::add_produce_block_event( @@ -661,10 +691,14 @@ mod tests { let root_bank = sharable_banks.root(); let cluster_info = get_cluster_info(my_keypair.insecure_clone()); - let consensus_pool = ConsensusPool::new_from_root_bank( + let root_block = (root_bank.slot(), root_bank.block_id().unwrap_or_default()); + let initial_parent_ready = (root_bank.slot().checked_add(1).unwrap(), root_block); + let consensus_pool = ConsensusPool::new( cluster_info.clone(), &root_bank, Arc::new(GeneratedCertTypes::default()), + Arc::new(MigrationStatus::post_migration_status()), + initial_parent_ready, ); let my_vote_pubkey = Pubkey::new_unique(); @@ -892,6 +926,7 @@ mod tests { blockstore: ctx.blockstore.clone(), sharable_banks: ctx.sharable_banks.clone(), leader_schedule_cache: ctx.leader_schedule_cache.clone(), + vote_history_highest_parent_ready: None, consensus_message_receiver: crossbeam_channel::unbounded().1, bls_sender: ctx.bls_sender.clone(), event_sender: crossbeam_channel::unbounded().0, @@ -928,6 +963,102 @@ mod tests { ); } + #[test] + fn test_can_produce_window_immediately_on_restart() { + let ctx = TestContext::default(); + let (repair_event_sender, _repair_event_receiver) = unbounded(); + let (event_sender, event_receiver) = unbounded(); + let (consensus_message_sender, consensus_message_receiver) = unbounded(); + + let root_bank = ctx.sharable_banks.root(); + let next_leader_slot = ctx + .leader_schedule_cache + .next_leader_slot(&ctx.my_pubkey, 0, &root_bank, None, 1000000) + .expect("Should find a leader slot") + .0; + let restored_parent_ready = ( + next_leader_slot, + (next_leader_slot.checked_sub(1).unwrap(), Hash::new_unique()), + ); + let exit = ctx.exit.clone(); + + let pool_ctx = ConsensusPoolContext { + exit: exit.clone(), + migration_status: Arc::new(MigrationStatus::post_migration_status()), + generated_cert_types: Arc::new(GeneratedCertTypes::default()), + cluster_info: ctx.cluster_info.clone(), + my_vote_pubkey: ctx.my_vote_pubkey, + blockstore: ctx.blockstore.clone(), + sharable_banks: ctx.sharable_banks.clone(), + leader_schedule_cache: ctx.leader_schedule_cache.clone(), + vote_history_highest_parent_ready: Some(restored_parent_ready), + consensus_message_receiver, + bls_sender: ctx.bls_sender.clone(), + event_sender, + commitment_sender: ctx.commitment_sender.clone(), + highest_finalized: ctx.highest_finalized.clone(), + repair_event_sender, + }; + + let handle = + thread::spawn(move || ConsensusPoolService::consensus_pool_ingest_loop(pool_ctx)); + + let deadline = Instant::now() + Duration::from_secs(5); + let mut saw_parent_ready = false; + let mut saw_produce_window = false; + while Instant::now() < deadline && !saw_produce_window { + let timeout = deadline.saturating_duration_since(Instant::now()); + let Ok(event) = event_receiver.recv_timeout(timeout) else { + break; + }; + + match event { + VotorEvent::ParentReady { slot, parent_block } => { + saw_parent_ready |= (slot, parent_block) == restored_parent_ready; + } + VotorEvent::ProduceWindow(LeaderWindowInfo { start_slot, .. }) => { + saw_produce_window |= start_slot == restored_parent_ready.0; + } + _ => {} + } + } + + exit.store(true, Ordering::Relaxed); + drop(consensus_message_sender); + let _ = handle.join().unwrap(); + + assert!( + saw_parent_ready, + "Should have received kick-off ParentReady" + ); + assert!( + saw_produce_window, + "Should have received ProduceWindow for kick-off ParentReady" + ); + } + + #[test] + fn test_kick_off_parent_ready_uses_restored_vote_history() { + let genesis_block = Some((10, Hash::new_unique())); + let root_block = (12, Hash::new_unique()); + assert_eq!( + ConsensusPoolService::initial_parent_ready(genesis_block, root_block, None), + (13, root_block) + ); + + let restored = (16, (15, Hash::new_unique())); + assert_eq!( + ConsensusPoolService::initial_parent_ready(genesis_block, root_block, Some(restored)), + restored + ); + + let stale = (12, (11, Hash::new_unique())); + assert_eq!( + ConsensusPoolService::initial_parent_ready(genesis_block, root_block, Some(stale)), + (13, root_block) + ); + } + #[test] fn test_send_certificates() { let ctx = TestContext::default(); diff --git a/votor/src/event_handler.rs b/votor/src/event_handler.rs index c86d434f465..229b2a1f8de 100644 --- a/votor/src/event_handler.rs +++ b/votor/src/event_handler.rs @@ -221,18 +221,18 @@ impl EventHandler { request_switch(&ctx.latest_switch_request, *my_pubkey, parent_block); } - let should_set_timeouts = vctx.vote_history.add_parent_ready(slot, parent_block); + vctx.vote_history.add_parent_ready(slot, parent_block); Self::check_pending_blocks(my_pubkey, &mut local_context.pending_blocks, vctx, votes)?; - if should_set_timeouts { - let root_bank = vctx.sharable_banks.root(); - let delta_block = Duration::from_nanos_u128(root_bank.ns_per_slot_at_slot(slot)); - let delta_first_slice = delta_block; - timer_manager.write().set_timeouts( - slot, - local_context.standstill_slot, - delta_first_slice, - delta_block, - ); + let root_bank = vctx.sharable_banks.root(); + let delta_block = Duration::from_nanos_u128(root_bank.ns_per_slot_at_slot(slot)); + let delta_first_slice = delta_block; + let timeout_inserted = timer_manager.write().set_timeouts( + slot, + local_context.standstill_slot, + delta_first_slice, + delta_block, + ); + if timeout_inserted { local_context.stats.timeout_set = local_context.stats.timeout_set.saturating_add(1); } @@ -1488,6 +1488,25 @@ mod tests { test_context.check_for_commitment(CommitmentType::Notarize, slot); } + #[test] + fn test_restored_parent_ready_sets_timeout() { + let mut test_context = setup(); + let slot = 4; + let parent_block = (3, Hash::new_unique()); + + assert!( + test_context + .voting_context + .vote_history + .add_parent_ready(slot, parent_block) + ); + assert!(!test_context.timer_manager.read().is_timeout_set(slot)); + + test_context.send_parent_ready_event(slot, parent_block); + test_context.check_timeout_set(slot); + assert_eq!(test_context.local_context.stats.timeout_set, 1); + } + #[test] fn test_received_block_notarized_and_timeout() { // Test block notarized event will trigger Finalize vote when all conditions are met diff --git a/votor/src/timer_manager.rs b/votor/src/timer_manager.rs index 251f8e65827..e3b8ce252e8 100644 --- a/votor/src/timer_manager.rs +++ b/votor/src/timer_manager.rs @@ -63,14 +63,14 @@ impl TimerManager { standstill_slot: Option, delta_first_slice: Duration, delta_block: Duration, - ) { + ) -> bool { self.timers.write().set_timeouts( slot, Instant::now(), standstill_slot, delta_first_slice, delta_block, - ); + ) } pub(crate) fn join(self) { @@ -103,7 +103,8 @@ mod tests { let delta_first_slice = delta_block; let slot = 52; let start = Instant::now(); - timer_manager.set_timeouts(slot, None, delta_first_slice, delta_block); + assert!(timer_manager.set_timeouts(slot, None, delta_first_slice, delta_block)); + assert!(!timer_manager.set_timeouts(slot, None, delta_first_slice, delta_block)); // Should see two timeouts at delta_block and DELTA_TIMEOUT let mut timeouts_received = 0; while timeouts_received < 2 && Instant::now().duration_since(start) < Duration::from_secs(2) diff --git a/votor/src/timer_manager/timers.rs b/votor/src/timer_manager/timers.rs index 950852abe41..1fd9846a820 100644 --- a/votor/src/timer_manager/timers.rs +++ b/votor/src/timer_manager/timers.rs @@ -205,7 +205,7 @@ impl Timers { standstill_slot: Option, delta_first_slice: Duration, delta_block: Duration, - ) { + ) -> bool { assert!(delta_first_slice <= delta_block); assert_eq!(self.heap.len(), self.timers.len()); let timeout_config = TimeoutConfig { @@ -225,6 +225,7 @@ impl Timers { }); self.stats .incr_timeout_count_with_heap_size(self.heap.len(), new_timer_inserted); + new_timer_inserted } /// Call to make progress on the timer states. If there are still active diff --git a/votor/src/vote_history.rs b/votor/src/vote_history.rs index 0615775a8b9..fe3f1d9a3a2 100644 --- a/votor/src/vote_history.rs +++ b/votor/src/vote_history.rs @@ -212,8 +212,7 @@ impl VoteHistory { /// Add a new parent ready slot /// - /// Returns true if the insertion was successful and this was the - /// first parent ready for this slot, indicating we should set timeouts. + /// Returns true if this was the first parent ready for this slot. pub fn add_parent_ready(&mut self, slot: Slot, parent: Block) -> bool { if slot < self.root { return false; @@ -231,7 +230,18 @@ impl VoteHistory { } pub fn highest_parent_ready_slot(&self) -> Option { - self.parent_ready_slots.keys().max().copied() + self.highest_parent_ready().map(|(slot, _)| slot) + } + + pub fn highest_parent_ready(&self) -> Option<(Slot, Block)> { + let (&slot, parents) = self + .parent_ready_slots + .iter() + .max_by_key(|(slot, _)| *slot)?; + + // Choose the parent that maximizes the amount of slots skipped. + // This matches the block production parent selection logic in the `ParentReadyTracker`. + parents.iter().min().copied().map(|parent| (slot, parent)) } /// Sets the new root slot and cleans up outdated slots < `root` @@ -496,6 +506,14 @@ mod test { assert!(vote_history.is_parent_ready(2, &block_id_2_1)); assert!(vote_history.is_parent_ready(2, &block_id_0)); assert_eq!(vote_history.highest_parent_ready_slot(), Some(2)); + let expected_parent = [block_id_2_0, block_id_2_1, block_id_0] + .into_iter() + .min() + .unwrap(); + assert_eq!( + vote_history.highest_parent_ready(), + Some((2, expected_parent)) + ); // Adding a parent ready for slot before root silently returns false assert!(!vote_history.add_parent_ready(1, block_id_0)); diff --git a/votor/src/votor.rs b/votor/src/votor.rs index 5b980ed4b29..42ae859199e 100644 --- a/votor/src/votor.rs +++ b/votor/src/votor.rs @@ -174,6 +174,7 @@ impl Votor { let migration_status = bank_forks.read().unwrap().migration_status(); let identity_keypair = cluster_info.keypair(); + let vote_history_highest_parent_ready = vote_history.highest_parent_ready(); // Get the sharable root bank let sharable_banks = bank_forks.read().unwrap().sharable_banks(); @@ -235,6 +236,7 @@ impl Votor { blockstore, sharable_banks: sharable_banks.clone(), leader_schedule_cache: leader_schedule_cache.clone(), + vote_history_highest_parent_ready, consensus_message_receiver, bls_sender, event_sender, From 1f59b9374b4a8ad21775b532f40a57d2a9318d37 Mon Sep 17 00:00:00 2001 From: Joe C Date: Tue, 2 Jun 2026 07:21:11 +0800 Subject: [PATCH 163/576] svm: conformance: add elf loading harness (#12881) * svm: conformance: repackage instr harness * svm: conformance: add elf loading harness * use xxhash --- Cargo.lock | 7 + Cargo.toml | 1 + programs/sbf/tests/programs.rs | 3 +- svm/Cargo.toml | 2 + svm/src/conformance/elf_loader.rs | 202 +++++++++++++++++++++ svm/src/conformance/{ => instr}/context.rs | 67 +------ svm/src/conformance/instr/effects.rs | 60 ++++++ svm/src/conformance/{ => instr}/harness.rs | 16 +- svm/src/conformance/instr/mod.rs | 5 + svm/src/conformance/mod.rs | 5 +- 10 files changed, 295 insertions(+), 73 deletions(-) create mode 100644 svm/src/conformance/elf_loader.rs rename svm/src/conformance/{ => instr}/context.rs (54%) create mode 100644 svm/src/conformance/instr/effects.rs rename svm/src/conformance/{ => instr}/harness.rs (96%) create mode 100644 svm/src/conformance/instr/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 50194df2838..cb2c62c2389 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10620,6 +10620,7 @@ dependencies = [ "spl-token-interface", "test-case", "thiserror 2.0.18", + "xxhash-rust", ] [[package]] @@ -13376,6 +13377,12 @@ dependencies = [ "rustix 0.38.39", ] +[[package]] +name = "xxhash-rust" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" + [[package]] name = "yansi" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index 638efe31c17..a1744628870 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -563,6 +563,7 @@ winapi = "0.3.8" wincode = { version = "0.5.5", features = ["derive"] } winreg = "0.55" x509-parser = "0.18.1" +xxhash-rust = "0.8.5" zstd = "0.13.3" [profile.dev] diff --git a/programs/sbf/tests/programs.rs b/programs/sbf/tests/programs.rs index 28128588ace..1b11cfe7b64 100644 --- a/programs/sbf/tests/programs.rs +++ b/programs/sbf/tests/programs.rs @@ -78,8 +78,7 @@ use { solana_program_runtime::sysvar_cache::SysvarCache, solana_sdk_ids::sysvar::rent, solana_svm::conformance::{ - context::InstrContext, - harness::execute_instr, + instr::{context::InstrContext, harness::execute_instr}, programs::{ add_program_to_program_cache, keyed_account_for_system_program, new_program_cache_with_builtins, diff --git a/svm/Cargo.toml b/svm/Cargo.toml index 0190e853111..53889e64cfe 100644 --- a/svm/Cargo.toml +++ b/svm/Cargo.toml @@ -25,6 +25,7 @@ conformance = [ "dep:bincode", "dep:prost", "dep:protosol", + "dep:xxhash-rust", "dev-context-only-utils", "solana-instruction-error/serde", "solana-pubkey/default", @@ -111,6 +112,7 @@ solana-transaction-context = { workspace = true } solana-transaction-error = { workspace = true } spl-generic-token = { workspace = true } thiserror = { workspace = true } +xxhash-rust = { workspace = true, optional = true, features = ["xxh64"] } [dev-dependencies] assert_matches = { workspace = true } diff --git a/svm/src/conformance/elf_loader.rs b/svm/src/conformance/elf_loader.rs new file mode 100644 index 00000000000..9302db6c832 --- /dev/null +++ b/svm/src/conformance/elf_loader.rs @@ -0,0 +1,202 @@ +//! ELF loader conformance harness. + +use { + crate::conformance::feature_set::feature_set_from_proto, + prost::Message, + protosol::protos::{ + ElfLoaderCtx as ProtoElfLoaderCtx, ElfLoaderEffects as ProtoElfLoaderEffects, + }, + solana_compute_budget::compute_budget::ComputeBudget, + solana_program_runtime::solana_sbpf::{ + ebpf, + elf::{ElfError, Executable}, + }, + solana_syscalls::create_program_runtime_environment, + std::{collections::BTreeSet, ffi::c_int}, + xxhash_rust::xxh64::xxh64, +}; + +pub fn execute_elf_loader(input: &ProtoElfLoaderCtx) -> ProtoElfLoaderEffects { + let feature_set = input + .features + .as_ref() + .map(feature_set_from_proto) + .unwrap_or_default() + .runtime_features(); + let simd_0268_active = feature_set.raise_cpi_nesting_limit_to_8; + let compute_budget = ComputeBudget::new_with_defaults(simd_0268_active); + + let program_runtime_environment = create_program_runtime_environment( + &feature_set, + &compute_budget.to_budget(), + input.deploy_checks, + std::env::var("ENABLE_VM_TRACING").is_ok(), + ) + .unwrap(); + + let executable = match Executable::load(&input.elf_data, (*program_runtime_environment).clone()) + { + Ok(executable) => executable, + Err(err) => { + return ProtoElfLoaderEffects { + err_code: elf_err_to_num(&err) as u32, + ..Default::default() + }; + } + }; + + let (text_vaddr, text_bytes) = executable.get_text_bytes(); + let calldests: Vec = executable + .get_function_registry() + .iter() + .map(|(_, (_, address))| address as u64) + .collect::>() + .into_iter() + .collect(); + + ProtoElfLoaderEffects { + err_code: 0, + rodata_hash: fd_hash_without_seed(executable.get_ro_section()), + entry_pc: executable.get_entrypoint_instruction_offset() as u64, + text_off: text_vaddr.saturating_sub(ebpf::MM_BYTECODE_START), + text_cnt: (text_bytes.len() / 8) as u64, + calldests_hash: fd_hash_u64_without_seed(&calldests), + } +} + +fn elf_err_to_num(error: &ElfError) -> u8 { + match error { + ElfError::FailedToParse(_) => 1, + ElfError::EntrypointOutOfBounds => 2, + ElfError::InvalidEntrypoint => 3, + ElfError::FailedToGetSection(_) => 4, + ElfError::UnresolvedSymbol(_, _, _) => 5, + ElfError::SectionNotFound(_) => 6, + ElfError::RelativeJumpOutOfBounds(_) => 7, + ElfError::SymbolHashCollision(_) => 8, + ElfError::WrongEndianess => 9, + ElfError::WrongAbi => 10, + ElfError::WrongMachine => 11, + ElfError::WrongClass => 12, + ElfError::NotOneTextSection => 13, + ElfError::WritableSectionNotSupported(_) => 14, + ElfError::AddressOutsideLoadableSection(_) => 15, + ElfError::InvalidVirtualAddress(_) => 16, + ElfError::UnknownRelocation(_) => 17, + ElfError::FailedToReadRelocationInfo => 18, + ElfError::WrongType => 19, + ElfError::UnknownSymbol(_) => 20, + ElfError::ValueOutOfBounds => 21, + ElfError::UnsupportedSBPFVersion => 22, + ElfError::InvalidProgramHeader => 23, + } +} + +fn fd_hash_without_seed(buf: &[u8]) -> u64 { + fd_hash(0, buf) +} + +fn fd_hash_u64_without_seed(buf: &[u64]) -> u64 { + let bytes = unsafe { + std::slice::from_raw_parts(buf.as_ptr().cast::(), std::mem::size_of_val(buf)) + }; + fd_hash(0, bytes) +} + +/// Hashing used to summarize binary blobs (e.g. ELF sections) for conformance +/// comparisons. Firedancer's `fd_hash` is XXH64. +fn fd_hash(seed: u64, buf: &[u8]) -> u64 { + xxh64(buf, seed) +} + +/// # Safety +/// +/// `in_ptr` must point to `in_sz` initialized bytes. `out_ptr` must point +/// to a writable buffer of at least `*out_psz` bytes. On return, `*out_psz` +/// is updated to the number of bytes written. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn sol_compat_elf_loader_v1( + out_ptr: *mut u8, + out_psz: *mut u64, + in_ptr: *mut u8, + in_sz: u64, +) -> c_int { + if in_ptr.is_null() || in_sz == 0 { + return 0; + } + if out_psz.is_null() || out_ptr.is_null() { + return 0; + } + let in_slice = unsafe { std::slice::from_raw_parts(in_ptr, in_sz as usize) }; + let Ok(ctx) = ProtoElfLoaderCtx::decode(in_slice) else { + return 0; + }; + + let effects = execute_elf_loader(&ctx); + + let out_slice = unsafe { std::slice::from_raw_parts_mut(out_ptr, (*out_psz) as usize) }; + let out_vec = effects.encode_to_vec(); + if out_vec.len() > out_slice.len() { + return 0; + } + out_slice[..out_vec.len()].copy_from_slice(&out_vec); + unsafe { *out_psz = out_vec.len() as u64 }; + + 1 +} + +#[cfg(test)] +mod tests { + use { + super::*, agave_feature_set::enable_sbpf_v3_deployment_and_execution, + protosol::protos::FeatureSet as ProtoFeatureSet, + }; + + const NOOP_ALIGNED: &[u8] = + include_bytes!("../../../programs/bpf_loader/test_elfs/out/noop_aligned.so"); + const NOOP_UNALIGNED: &[u8] = + include_bytes!("../../../programs/bpf_loader/test_elfs/out/noop_unaligned.so"); + const SBPFV3_RETURN_OK: &[u8] = + include_bytes!("../../../programs/bpf_loader/test_elfs/out/sbpfv3_return_ok.so"); + + fn assert_loads_ok(elf: &[u8], deploy_checks: bool, features: Option) { + let effects = execute_elf_loader(&ProtoElfLoaderCtx { + features, + elf_data: elf.to_vec(), + deploy_checks, + }); + assert_eq!(effects.err_code, 0); + assert!(effects.text_cnt > 0); + } + + #[test] + fn test_load_noop_aligned() { + assert_loads_ok(NOOP_ALIGNED, false, None); + } + + #[test] + fn test_load_noop_unaligned_with_deploy_checks() { + assert_loads_ok(NOOP_UNALIGNED, true, None); + } + + #[test] + fn test_load_sbpf_v3_with_feature_enabled() { + // The v3 ELF only loads when the SBPF v3 feature is active. + let v3 = enable_sbpf_v3_deployment_and_execution::id(); + let features = ProtoFeatureSet { + features: vec![u64::from_le_bytes(v3.to_bytes()[..8].try_into().unwrap())], + }; + assert_loads_ok(SBPFV3_RETURN_OK, false, Some(features)); + } + + #[test] + fn test_fd_hash_matches_xxh64() { + // Reference values from Firedancer's `fd_hash` (XXH64). The empty-input, + // seed-0 case is the canonical XXH64 test vector. + let long: Vec = (0u8..100).collect(); + assert_eq!(fd_hash(0, &[]), 0xef46db3751d8e999); + assert_eq!(fd_hash(0, b"hello world"), 0x45ab6734b21e6968); + assert_eq!(fd_hash(42, b"hello world"), 0x69c2b68f9d9352a1); + assert_eq!(fd_hash(0, &long), 0x6ac1e58032166597); + } +} diff --git a/svm/src/conformance/context.rs b/svm/src/conformance/instr/context.rs similarity index 54% rename from svm/src/conformance/context.rs rename to svm/src/conformance/instr/context.rs index 035a1eb494c..bbc6a6bf846 100644 --- a/svm/src/conformance/context.rs +++ b/svm/src/conformance/instr/context.rs @@ -1,20 +1,15 @@ -//! Instruction context and effects (input + output). +//! Instruction context (input). #[cfg(feature = "conformance")] use { - super::{ - account_state::{account_from_proto, account_to_proto}, - feature_set::feature_set_from_proto, - }, - protosol::protos::{InstrContext as ProtoInstrContext, InstrEffects as ProtoInstrEffects}, + crate::conformance::{account_state::account_from_proto, feature_set::feature_set_from_proto}, + protosol::protos::InstrContext as ProtoInstrContext, solana_instruction::AccountMeta, }; use { - solana_account::Account, - solana_instruction::{Instruction, error::InstructionError}, + solana_account::Account, solana_instruction::Instruction, solana_program_runtime::execution_budget::DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT, - solana_pubkey::Pubkey, - solana_svm_feature_set::SVMFeatureSet, + solana_pubkey::Pubkey, solana_svm_feature_set::SVMFeatureSet, }; /// Inputs to a single instruction. @@ -94,55 +89,3 @@ impl From for InstrContext { } } } - -/// Represents the effects of a single instruction. -pub struct InstrEffects { - pub result: Option, - pub custom_err: Option, - pub resulting_accounts: Vec<(Pubkey, Account)>, - pub cu_avail: u64, - pub return_data: Vec, - pub logs: Vec, -} - -impl InstrEffects { - /// Returns the resulting account for the given pubkey, if it exists. - pub fn get_account(&self, pubkey: &Pubkey) -> Option<&Account> { - self.resulting_accounts - .iter() - .find(|(pk, _)| pk == pubkey) - .map(|(_, acc)| acc) - } -} - -#[cfg(feature = "conformance")] -impl From for ProtoInstrEffects { - fn from(value: InstrEffects) -> Self { - let InstrEffects { - result, - custom_err, - resulting_accounts, - cu_avail, - return_data, - .. - } = value; - - Self { - result: result - .as_ref() - .map(|error| { - let serialized_err = bincode::serialize(error).unwrap(); - i32::from_le_bytes((&serialized_err[0..4]).try_into().unwrap()) - .saturating_add(1) - }) - .unwrap_or_default(), - custom_err: custom_err.unwrap_or_default(), - modified_accounts: resulting_accounts - .into_iter() - .map(account_to_proto) - .collect(), - cu_avail, - return_data, - } - } -} diff --git a/svm/src/conformance/instr/effects.rs b/svm/src/conformance/instr/effects.rs new file mode 100644 index 00000000000..0d7b64e9e86 --- /dev/null +++ b/svm/src/conformance/instr/effects.rs @@ -0,0 +1,60 @@ +//! Instruction effects (output). + +#[cfg(feature = "conformance")] +use { + crate::conformance::account_state::account_to_proto, + protosol::protos::InstrEffects as ProtoInstrEffects, +}; +use {solana_account::Account, solana_instruction::error::InstructionError, solana_pubkey::Pubkey}; + +/// Represents the effects of a single instruction. +pub struct InstrEffects { + pub result: Option, + pub custom_err: Option, + pub resulting_accounts: Vec<(Pubkey, Account)>, + pub cu_avail: u64, + pub return_data: Vec, + pub logs: Vec, +} + +impl InstrEffects { + /// Returns the resulting account for the given pubkey, if it exists. + pub fn get_account(&self, pubkey: &Pubkey) -> Option<&Account> { + self.resulting_accounts + .iter() + .find(|(pk, _)| pk == pubkey) + .map(|(_, acc)| acc) + } +} + +#[cfg(feature = "conformance")] +impl From for ProtoInstrEffects { + fn from(value: InstrEffects) -> Self { + let InstrEffects { + result, + custom_err, + resulting_accounts, + cu_avail, + return_data, + .. + } = value; + + Self { + result: result + .as_ref() + .map(|error| { + let serialized_err = bincode::serialize(error).unwrap(); + i32::from_le_bytes((&serialized_err[0..4]).try_into().unwrap()) + .saturating_add(1) + }) + .unwrap_or_default(), + custom_err: custom_err.unwrap_or_default(), + modified_accounts: resulting_accounts + .into_iter() + .map(account_to_proto) + .collect(), + cu_avail, + return_data, + } + } +} diff --git a/svm/src/conformance/harness.rs b/svm/src/conformance/instr/harness.rs similarity index 96% rename from svm/src/conformance/harness.rs rename to svm/src/conformance/instr/harness.rs index fb3bf2eb673..9512d633e12 100644 --- a/svm/src/conformance/harness.rs +++ b/svm/src/conformance/instr/harness.rs @@ -1,7 +1,7 @@ //! Conformance harness. use { - super::context::{InstrContext, InstrEffects}, + super::{context::InstrContext, effects::InstrEffects}, crate::message_processor::process_message, solana_compute_budget::compute_budget::ComputeBudget, solana_instruction::error::InstructionError, @@ -24,7 +24,9 @@ use { }; #[cfg(feature = "conformance")] use { - super::programs::{fill_program_cache_from_accounts, new_program_cache_with_builtins}, + crate::conformance::programs::{ + fill_program_cache_from_accounts, new_program_cache_with_builtins, + }, agave_feature_set::FeatureSet, agave_precompiles::{get_precompile, is_precompile}, prost::Message, @@ -281,11 +283,11 @@ pub unsafe extern "C" fn sol_compat_instr_execute_v1( #[cfg(test)] mod tests { use { - super::{ - super::programs::{add_program_to_program_cache, new_program_cache_with_builtins}, - *, + super::*, + crate::conformance::programs::{ + add_program_to_program_cache, keyed_account_for_system_program, + new_program_cache_with_builtins, }, - crate::conformance::programs::keyed_account_for_system_program, solana_account::Account, solana_instruction::Instruction, solana_rent::Rent, @@ -296,7 +298,7 @@ mod tests { }; const NOOP_ELF: &[u8] = - include_bytes!("../../../programs/bpf_loader/test_elfs/out/noop_aligned.so"); + include_bytes!("../../../../programs/bpf_loader/test_elfs/out/noop_aligned.so"); const FROM_BASE_LAMPORTS: u64 = 5_000; const TO_BASE_LAMPORTS: u64 = 1_000; diff --git a/svm/src/conformance/instr/mod.rs b/svm/src/conformance/instr/mod.rs new file mode 100644 index 00000000000..585f5d4b20e --- /dev/null +++ b/svm/src/conformance/instr/mod.rs @@ -0,0 +1,5 @@ +//! Instruction conformance harness. + +pub mod context; +pub mod effects; +pub mod harness; diff --git a/svm/src/conformance/mod.rs b/svm/src/conformance/mod.rs index f6b06301478..ff12bbcbcc4 100644 --- a/svm/src/conformance/mod.rs +++ b/svm/src/conformance/mod.rs @@ -2,8 +2,9 @@ #[cfg(feature = "conformance")] pub mod account_state; -pub mod context; +#[cfg(feature = "conformance")] +pub mod elf_loader; #[cfg(feature = "conformance")] pub mod feature_set; -pub mod harness; +pub mod instr; pub mod programs; From ac5319a2daf4dca0ea423632f01bac7a2c8a670a Mon Sep 17 00:00:00 2001 From: Brennan Date: Mon, 1 Jun 2026 16:38:09 -0700 Subject: [PATCH 164/576] Clock use dynamic slot range (#12886) --- runtime/src/bank.rs | 12 +++++-- runtime/src/stake_weighted_timestamp.rs | 47 ++++++++++++++----------- 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index ca2f1774025..f6a2bb30881 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -2879,14 +2879,22 @@ impl Bank { (slot_delta <= slots_per_epoch) .then_some((*pubkey, (last_timestamp.slot, last_timestamp.timestamp))) }); - let slot_duration = Duration::from_nanos(self.ns_per_slot as u64); + let elapsed_slot_duration = |from_slot: Slot, to_slot: Slot| { + if from_slot >= to_slot { + Duration::ZERO + } else { + Duration::from_nanos_u128( + self.slot_range_duration_nanos(from_slot.saturating_add(1), to_slot), + ) + } + }; let epoch = self.epoch_schedule().get_epoch(self.slot()); let stakes = self.epoch_vote_accounts(epoch)?; let stake_weighted_timestamp = calculate_stake_weighted_timestamp( recent_timestamps, stakes, self.slot(), - slot_duration, + elapsed_slot_duration, epoch_start_timestamp, max_allowable_drift, ); diff --git a/runtime/src/stake_weighted_timestamp.rs b/runtime/src/stake_weighted_timestamp.rs index 24fd608c468..62fef9ebf86 100644 --- a/runtime/src/stake_weighted_timestamp.rs +++ b/runtime/src/stake_weighted_timestamp.rs @@ -27,7 +27,7 @@ pub(crate) fn calculate_stake_weighted_timestamp( unique_timestamps: I, stakes: &HashMap, slot: Slot, - slot_duration: Duration, + elapsed_slot_duration: impl Fn(Slot, Slot) -> Duration, epoch_start_timestamp: Option<(Slot, UnixTimestamp)>, max_allowable_drift: MaxAllowableDrift, ) -> Option @@ -40,7 +40,7 @@ where let mut total_stake: u128 = 0; for (vote_pubkey, slot_timestamp) in unique_timestamps { let (timestamp_slot, timestamp) = slot_timestamp.borrow(); - let offset = slot_duration.saturating_mul(slot.saturating_sub(*timestamp_slot) as u32); + let offset = elapsed_slot_duration(*timestamp_slot, slot); let estimate = timestamp.saturating_add(offset.as_secs() as i64); let stake = stakes .get(vote_pubkey.borrow()) @@ -67,8 +67,7 @@ where } // Bound estimate by `max_allowable_drift` since the start of the epoch if let Some((epoch_start_slot, epoch_start_timestamp)) = epoch_start_timestamp { - let poh_estimate_offset = - slot_duration.saturating_mul(slot.saturating_sub(epoch_start_slot) as u32); + let poh_estimate_offset = elapsed_slot_duration(epoch_start_slot, slot); let estimate_offset = Duration::from_secs((estimate as u64).saturating_sub(epoch_start_timestamp as u64)); let max_allowable_drift_fast = @@ -103,6 +102,12 @@ pub mod tests { solana_native_token::LAMPORTS_PER_SOL, }; + fn constant_elapsed_slot_duration(slot_duration: Duration) -> impl Fn(Slot, Slot) -> Duration { + move |from_slot, to_slot| { + slot_duration.saturating_mul(to_slot.saturating_sub(from_slot) as u32) + } + } + #[test] fn test_calculate_stake_weighted_timestamp_uses_median() { let recent_timestamp: UnixTimestamp = 1_578_909_061; @@ -166,7 +171,7 @@ pub mod tests { &unique_timestamps, &stakes, slot as Slot, - slot_duration, + constant_elapsed_slot_duration(slot_duration), None, max_allowable_drift, ) @@ -189,7 +194,7 @@ pub mod tests { &unique_timestamps, &stakes, slot as Slot, - slot_duration, + constant_elapsed_slot_duration(slot_duration), None, max_allowable_drift, ) @@ -212,7 +217,7 @@ pub mod tests { &unique_timestamps, &stakes, slot as Slot, - slot_duration, + constant_elapsed_slot_duration(slot_duration), None, max_allowable_drift, ) @@ -260,7 +265,7 @@ pub mod tests { &unique_timestamps, &stakes, slot as Slot, - slot_duration, + constant_elapsed_slot_duration(slot_duration), None, max_allowable_drift, ) @@ -297,7 +302,7 @@ pub mod tests { &unique_timestamps, &stakes, slot as Slot, - slot_duration, + constant_elapsed_slot_duration(slot_duration), None, max_allowable_drift, ) @@ -363,7 +368,7 @@ pub mod tests { &unique_timestamps, &stakes, slot as Slot, - slot_duration, + constant_elapsed_slot_duration(slot_duration), Some((0, epoch_start_timestamp)), max_allowable_drift, ) @@ -384,7 +389,7 @@ pub mod tests { &unique_timestamps, &stakes, slot as Slot, - slot_duration, + constant_elapsed_slot_duration(slot_duration), Some((0, epoch_start_timestamp)), max_allowable_drift, ) @@ -405,7 +410,7 @@ pub mod tests { &unique_timestamps, &stakes, slot as Slot, - slot_duration, + constant_elapsed_slot_duration(slot_duration), Some((0, epoch_start_timestamp)), max_allowable_drift, ) @@ -425,7 +430,7 @@ pub mod tests { &unique_timestamps, &stakes, slot as Slot, - slot_duration, + constant_elapsed_slot_duration(slot_duration), Some((0, epoch_start_timestamp)), max_allowable_drift, ) @@ -509,7 +514,7 @@ pub mod tests { &unique_timestamps, &stakes, slot as Slot, - slot_duration, + constant_elapsed_slot_duration(slot_duration), Some((0, epoch_start_timestamp)), allowable_drift_25, ) @@ -520,7 +525,7 @@ pub mod tests { &unique_timestamps, &stakes, slot as Slot, - slot_duration, + constant_elapsed_slot_duration(slot_duration), Some((0, epoch_start_timestamp)), allowable_drift_50, ) @@ -550,7 +555,7 @@ pub mod tests { &unique_timestamps, &stakes, slot as Slot, - slot_duration, + constant_elapsed_slot_duration(slot_duration), Some((0, epoch_start_timestamp)), allowable_drift_25, ) @@ -561,7 +566,7 @@ pub mod tests { &unique_timestamps, &stakes, slot as Slot, - slot_duration, + constant_elapsed_slot_duration(slot_duration), Some((0, epoch_start_timestamp)), allowable_drift_50, ) @@ -641,7 +646,7 @@ pub mod tests { &unique_timestamps, &stakes, slot as Slot, - slot_duration, + constant_elapsed_slot_duration(slot_duration), Some((0, epoch_start_timestamp)), max_allowable_drift, ) @@ -671,7 +676,7 @@ pub mod tests { &unique_timestamps, &stakes, slot as Slot, - slot_duration, + constant_elapsed_slot_duration(slot_duration), Some((0, epoch_start_timestamp)), max_allowable_drift, ) @@ -701,7 +706,7 @@ pub mod tests { &unique_timestamps, &stakes, slot as Slot, - slot_duration, + constant_elapsed_slot_duration(slot_duration), Some((0, epoch_start_timestamp)), max_allowable_drift, ) @@ -767,7 +772,7 @@ pub mod tests { &unique_timestamps, &stakes, slot as Slot, - slot_duration, + constant_elapsed_slot_duration(slot_duration), Some((0, epoch_start_timestamp)), max_allowable_drift, ) From 2ad0f1d5f5431581ccb4aa278f70dde5164b772f Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Mon, 1 Jun 2026 20:27:06 -0400 Subject: [PATCH 165/576] startup: reconcile vote history with hard forks / blockstore root (#12842) * startup: reconcile vote history with hard forks / blockstore root * pr feedback --- core/src/consensus.rs | 2 + core/src/validator.rs | 334 ++++++++++++++++-------- programs/vote/src/vote_state/handler.rs | 2 +- validator/src/admin_rpc_service.rs | 9 +- 4 files changed, 235 insertions(+), 112 deletions(-) diff --git a/core/src/consensus.rs b/core/src/consensus.rs index 81be5122aa6..670353faced 100644 --- a/core/src/consensus.rs +++ b/core/src/consensus.rs @@ -1733,6 +1733,7 @@ impl TowerError { #[derive(Debug)] pub enum ExternalRootSource { Tower(Slot), + VoteHistory(Slot), HardFork(Slot), } @@ -1740,6 +1741,7 @@ impl ExternalRootSource { fn root(&self) -> Slot { match self { ExternalRootSource::Tower(slot) => *slot, + ExternalRootSource::VoteHistory(slot) => *slot, ExternalRootSource::HardFork(slot) => *slot, } } diff --git a/core/src/validator.rs b/core/src/validator.rs index 446ded0fb32..24f28dfcb21 100644 --- a/core/src/validator.rs +++ b/core/src/validator.rs @@ -34,7 +34,7 @@ use { snapshot_config::SnapshotConfig, snapshot_hash::StartingSnapshotHashes, }, agave_votor::{ - vote_history::VoteHistory, + vote_history::{VoteHistory, VoteHistoryError}, vote_history_storage::{NullVoteHistoryStorage, VoteHistoryStorage}, voting_service::VotingServiceOverride, }, @@ -1533,19 +1533,14 @@ impl Validator { exit.clone(), ); - let tower = match process_blockstore.process_to_create_tower() { - Ok(tower) => { - info!("Tower state: {tower:?}"); - tower - } - Err(e) => { - warn!("Unable to retrieve tower: {e:?} creating default tower...."); - Tower::default() - } - }; - // Future upstream PR will handle reconciliation of VoteHistory against hard forks - let vote_history = - restore_vote_history(config, &bank_forks, &cluster_info.id(), vote_account); + let (tower, vote_history) = process_blockstore.process().map_err(|e| { + ValidatorError::Other(format!( + "Unable to restore Tower or VoteHistory and either --require-tower was specified \ + or --do-not-require-vote-history was not specified. Aborting {e}" + )) + })?; + info!("Tower state: {tower:?}, Vote History state: {vote_history:?}"); + migration_status.log_phase(); let outstanding_repair_requests = @@ -1993,26 +1988,43 @@ fn active_vote_account_exists_in_bank(bank: &Bank, vote_account: &Pubkey) -> boo false } -pub fn active_vote_account_exists_in_bank_alpenglow(bank: &Bank, vote_account: &Pubkey) -> bool { +/// Should we require that a vote history file is present +pub fn should_require_vote_history_file( + bank: &Bank, + vote_account: &Pubkey, + identity: &Pubkey, +) -> bool { let Some(genesis_certificate) = bank.get_alpenglow_genesis_certificate() else { + // Vote history is only used when Alpenglow is active return false; }; + let Some(Ok(vote_state)) = bank .get_account(vote_account) .map(|acct| acct.deserialize_data()) else { + // Must have a vote account return false; }; + let Ok(vote_state_handler) = VoteStateHandler::try_new_from_vote_state_versions(vote_state) else { return false; }; + if vote_state_handler.node_pubkey() != identity { + // We are starting up or set-identity with a dummy keypair + // We don't need to require the vote history file + return false; + } + let Some(last_voted_slot) = vote_state_handler.last_voted_slot() else { + // New vote account return false; }; let genesis_slot = genesis_certificate.cert_type.slot(); + // We've voted past the alpenglow genesis last_voted_slot > genesis_slot } @@ -2025,21 +2037,18 @@ fn restore_vote_history( match VoteHistory::restore(config.vote_history_storage.as_ref(), identity) { Ok(vote_history) => vote_history, Err(err) => { - let voting_has_been_active = { + let should_require_vote_history = { let bank_forks = bank_forks.read().unwrap(); - active_vote_account_exists_in_bank_alpenglow( - &bank_forks.working_bank(), - vote_account, - ) + should_require_vote_history_file(&bank_forks.working_bank(), vote_account, identity) }; - if config.require_vote_history && voting_has_been_active { + if config.require_vote_history && should_require_vote_history { panic!( "Unable to retrieve vote history for identity {identity}. The vote account \ {vote_account} has prior Alpenglow votes. If this is intentional, use \ --do-not-require-vote-history: {err:?}" ); } - if err.is_file_missing() && !voting_has_been_active { + if err.is_file_missing() && !should_require_vote_history { info!( "Ignoring expected failed vote history restore because this vote account has \ not voted before" @@ -2178,6 +2187,75 @@ fn post_process_restored_tower( Ok(restored_tower) } +fn post_process_restored_vote_history( + mut vote_history: VoteHistory, + validator_identity: &Pubkey, + config: &ValidatorConfig, + bank_forks: &BankForks, +) -> Result { + let mut should_require_vote_history = config.require_vote_history; + + let restored_vote_history = { + let root_bank = bank_forks.root_bank(); + + if vote_history.root() < root_bank.slot() { + // Vote history is old, update + vote_history.set_root(root_bank.slot()); + } + + if let Some(hard_fork_restart_slot) = + maybe_cluster_restart_with_hard_fork(config, root_bank.slot()) + { + // intentionally fail to restore vote_history; we're supposedly in a new hard fork; past + // out-of-chain votor state doesn't make sense at all + // what if --wait-for-supermajority again if the validator restarted? + let message = format!( + "Hard fork is detected; discarding vote_history restoration result: \ + {vote_history:?}" + ); + datapoint_error!("vote_history_error", ("error", message, String),); + error!("{message}"); + + // unconditionally relax vote_history requirement + should_require_vote_history = false; + Err(VoteHistoryError::HardFork(hard_fork_restart_slot)) + } else if let Some(warp_slot) = config.warp_slot { + // unconditionally relax vote_history requirement + should_require_vote_history = false; + Err(VoteHistoryError::HardFork(warp_slot)) + } else { + Ok(vote_history) + } + }; + + let restored_vote_history = match restored_vote_history { + Ok(vote_history) => vote_history, + Err(err) => { + if !err.is_file_missing() { + datapoint_error!( + "vote_history_error", + ( + "error", + format!("Unable to restore vote_history: {err}"), + String + ), + ); + } + if should_require_vote_history { + return Err(format!( + "Requested mandatory vote_history restore failed: {err}. Ensure that the vote \ + history storage file has been copied to the correct directory. Aborting" + )); + } + error!("Rebuilding an empty vote_history from root slot due to failed restore: {err}"); + + VoteHistory::new(*validator_identity, bank_forks.root()) + } + }; + + Ok(restored_vote_history) +} + fn load_genesis( config: &ValidatorConfig, ledger_path: &Path, @@ -2371,6 +2449,7 @@ pub struct ProcessBlockStore<'a> { snapshot_controller: &'a SnapshotController, config: &'a ValidatorConfig, tower: Option, + vote_history: Option, } impl<'a> ProcessBlockStore<'a> { @@ -2405,94 +2484,119 @@ impl<'a> ProcessBlockStore<'a> { snapshot_controller, config, tower: None, + vote_history: None, } } - pub(crate) fn process(&mut self) -> Result<(), String> { - if self.tower.is_none() { - let previous_start_process = *self.start_progress.read().unwrap(); - *self.start_progress.write().unwrap() = ValidatorStartProgress::LoadingLedger; - - let exit = Arc::new(AtomicBool::new(false)); - if let Ok(Some(max_slot)) = self.blockstore.highest_slot() { - let bank_forks = self.bank_forks.clone(); - let exit = exit.clone(); - let start_progress = self.start_progress.clone(); - - let _ = Builder::new() - .name("solRptLdgrStat".to_string()) - .spawn(move || { - while !exit.load(Ordering::Relaxed) { - let slot = bank_forks.read().unwrap().working_bank().slot(); - *start_progress.write().unwrap() = - ValidatorStartProgress::ProcessingLedger { slot, max_slot }; - thread::sleep(Duration::from_secs(2)); - } - }) - .unwrap(); - } - blockstore_processor::process_blockstore_from_root( - self.blockstore, - self.bank_forks, - self.leader_schedule_cache, - self.process_options, - self.transaction_status_sender, - self.entry_notification_sender, - Some(self.snapshot_controller), - ) - .map_err(|err| { - exit.store(true, Ordering::Relaxed); - format!("Failed to load ledger: {err:?}") - })?; - exit.store(true, Ordering::Relaxed); + pub(crate) fn process(&mut self) -> Result<(Tower, VoteHistory), String> { + if let (Some(tower), Some(vote_history)) = (self.tower.as_ref(), self.vote_history.as_ref()) + { + return Ok((tower.clone(), vote_history.clone())); + } - if let Some(blockstore_root_scan) = self.blockstore_root_scan.take() { - blockstore_root_scan.join(); - } + // This means we have not fully processed blockstore yet. Attempt to load and process + let previous_start_process = *self.start_progress.read().unwrap(); + *self.start_progress.write().unwrap() = ValidatorStartProgress::LoadingLedger; - self.tower = Some({ - let restored_tower = Tower::restore(self.config.tower_storage.as_ref(), self.id); - if let Ok(tower) = &restored_tower { - // reconciliation attempt 1 of 2 with tower - reconcile_blockstore_roots_with_external_source( - ExternalRootSource::Tower(tower.root()), - self.blockstore, - &mut self.original_blockstore_root, - ) - .map_err(|err| format!("Failed to reconcile blockstore with tower: {err:?}"))?; - } + let exit = Arc::new(AtomicBool::new(false)); + if let Ok(Some(max_slot)) = self.blockstore.highest_slot() { + let bank_forks = self.bank_forks.clone(); + let exit = exit.clone(); + let start_progress = self.start_progress.clone(); + + let _ = Builder::new() + .name("solRptLdgrStat".to_string()) + .spawn(move || { + while !exit.load(Ordering::Relaxed) { + let slot = bank_forks.read().unwrap().working_bank().slot(); + *start_progress.write().unwrap() = + ValidatorStartProgress::ProcessingLedger { slot, max_slot }; + thread::sleep(Duration::from_secs(2)); + } + }) + .unwrap(); + } - post_process_restored_tower( - restored_tower, - self.id, - self.vote_account, - self.config, - &self.bank_forks.read().unwrap(), - )? - }); + blockstore_processor::process_blockstore_from_root( + self.blockstore, + self.bank_forks, + self.leader_schedule_cache, + self.process_options, + self.transaction_status_sender, + self.entry_notification_sender, + Some(self.snapshot_controller), + ) + .map_err(|err| { + exit.store(true, Ordering::Relaxed); + format!("Failed to load ledger: {err:?}") + })?; + exit.store(true, Ordering::Relaxed); - if let Some(hard_fork_restart_slot) = maybe_cluster_restart_with_hard_fork( - self.config, - self.bank_forks.read().unwrap().root(), - ) { - // reconciliation attempt 2 of 2 with hard fork - // this should be #2 because hard fork root > tower root in almost all cases + if let Some(blockstore_root_scan) = self.blockstore_root_scan.take() { + blockstore_root_scan.join(); + } + + // Load and post process tower + let tower = { + let restored_tower = Tower::restore(self.config.tower_storage.as_ref(), self.id); + if let Ok(tower) = &restored_tower { + // reconciliation attempt 1 of 2 with tower reconcile_blockstore_roots_with_external_source( - ExternalRootSource::HardFork(hard_fork_restart_slot), + ExternalRootSource::Tower(tower.root()), self.blockstore, &mut self.original_blockstore_root, ) - .map_err(|err| format!("Failed to reconcile blockstore with hard fork: {err:?}"))?; + .map_err(|err| format!("Failed to reconcile blockstore with tower: {err:?}"))?; } - *self.start_progress.write().unwrap() = previous_start_process; + post_process_restored_tower( + restored_tower, + self.id, + self.vote_account, + self.config, + &self.bank_forks.read().unwrap(), + )? + }; + + // Load and post process vote history + let vote_history = { + let vote_history = + restore_vote_history(self.config, self.bank_forks, self.id, self.vote_account); + // reconciliation attempt 1 of 2 with vote history + reconcile_blockstore_roots_with_external_source( + ExternalRootSource::VoteHistory(vote_history.root()), + self.blockstore, + &mut self.original_blockstore_root, + ) + .map_err(|err| format!("Failed to reconcile blockstore with vote history: {err:?}"))?; + + post_process_restored_vote_history( + vote_history, + self.id, + self.config, + &self.bank_forks.read().unwrap(), + )? + }; + + if let Some(hard_fork_restart_slot) = maybe_cluster_restart_with_hard_fork( + self.config, + self.bank_forks.read().unwrap().root(), + ) { + // reconciliation attempt 2 of 2 with hard fork + // it is intentional that we do this second, as having the hard fork root < tower/vote_history root + // is invalid! This means we've hard forked and missed a finalized slot + reconcile_blockstore_roots_with_external_source( + ExternalRootSource::HardFork(hard_fork_restart_slot), + self.blockstore, + &mut self.original_blockstore_root, + ) + .map_err(|err| format!("Failed to reconcile blockstore with hard fork: {err:?}"))?; } - Ok(()) - } - pub(crate) fn process_to_create_tower(mut self) -> Result { - self.process()?; - Ok(self.tower.unwrap()) + *self.start_progress.write().unwrap() = previous_start_process; + self.tower = Some(tower.clone()); + self.vote_history = Some(vote_history.clone()); + Ok((tower, vote_history)) } } @@ -3006,7 +3110,7 @@ mod tests { }; #[test] - fn active_vote_account_exists_in_bank_alpenglow_checks_genesis_certificate_and_votes() { + fn test_should_require_vote_history_file() { use { agave_votor_messages::certificate::{Certificate, CertificateType}, solana_account::{AccountSharedData, state_traits::StateMut}, @@ -3016,17 +3120,22 @@ mod tests { let genesis_config = create_genesis_config(1_000_000).0; let bank = Bank::new_for_tests(&genesis_config); let vote_account_pubkey = Pubkey::new_unique(); + let identity = Pubkey::new_unique(); assert!(!active_vote_account_exists_in_bank( &bank, &vote_account_pubkey )); - assert!(!active_vote_account_exists_in_bank_alpenglow( + assert!(!should_require_vote_history_file( &bank, - &vote_account_pubkey + &vote_account_pubkey, + &identity, )); - let mut vote_state = VoteStateV4::default(); + let mut vote_state = VoteStateV4 { + node_pubkey: identity, + ..VoteStateV4::default() + }; let mut vote_account = AccountSharedData::new(1, VoteStateV4::size_of(), &solana_vote_program::id()); vote_account @@ -3037,9 +3146,10 @@ mod tests { &bank, &vote_account_pubkey )); - assert!(!active_vote_account_exists_in_bank_alpenglow( + assert!(!should_require_vote_history_file( &bank, - &vote_account_pubkey + &vote_account_pubkey, + &identity, )); vote_state.votes.push_back(LandedVote { @@ -3054,9 +3164,10 @@ mod tests { &bank, &vote_account_pubkey )); - assert!(!active_vote_account_exists_in_bank_alpenglow( + assert!(!should_require_vote_history_file( &bank, - &vote_account_pubkey + &vote_account_pubkey, + &identity, )); bank.set_alpenglow_genesis_certificate(&Certificate { @@ -3064,9 +3175,10 @@ mod tests { signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), bitmap: vec![], }); - assert!(!active_vote_account_exists_in_bank_alpenglow( + assert!(!should_require_vote_history_file( &bank, - &vote_account_pubkey + &vote_account_pubkey, + &identity, )); vote_state.votes.push_back(LandedVote { @@ -3078,9 +3190,17 @@ mod tests { .set_state(&VoteStateVersions::new_v4(vote_state)) .unwrap(); bank.store_account(&vote_account_pubkey, &vote_account); - assert!(active_vote_account_exists_in_bank_alpenglow( + assert!(should_require_vote_history_file( &bank, - &vote_account_pubkey + &vote_account_pubkey, + &identity, + )); + + // Use an unstaked identity + assert!(!should_require_vote_history_file( + &bank, + &vote_account_pubkey, + &Pubkey::new_unique(), )); } diff --git a/programs/vote/src/vote_state/handler.rs b/programs/vote/src/vote_state/handler.rs index 915c87c0053..7f7ccc474d6 100644 --- a/programs/vote/src/vote_state/handler.rs +++ b/programs/vote/src/vote_state/handler.rs @@ -163,7 +163,7 @@ impl VoteStateHandler { } } - pub(crate) fn node_pubkey(&self) -> &Pubkey { + pub fn node_pubkey(&self) -> &Pubkey { match &self.target_state { TargetVoteState::V4(v4) => &v4.node_pubkey, } diff --git a/validator/src/admin_rpc_service.rs b/validator/src/admin_rpc_service.rs index ce30415e205..f45c6e81c0f 100644 --- a/validator/src/admin_rpc_service.rs +++ b/validator/src/admin_rpc_service.rs @@ -22,7 +22,7 @@ use { repair::repair_service, validator::{ BlockProductionMethod, SchedulerPacing, TransactionStructure, ValidatorStartProgress, - active_vote_account_exists_in_bank_alpenglow, + should_require_vote_history_file, }, }, solana_geyser_plugin_manager::GeyserPluginManagerRequest, @@ -913,14 +913,15 @@ impl AdminRpcImpl { } if require_vote_history { - let voting_has_been_active = { + let should_require_vote_history = { let bank_forks = post_init.bank_forks.read().unwrap(); - active_vote_account_exists_in_bank_alpenglow( + should_require_vote_history_file( &bank_forks.root_bank(), &post_init.vote_account, + &identity_keypair.pubkey(), ) }; - if voting_has_been_active { + if should_require_vote_history { let _ = VoteHistory::restore( meta.vote_history_storage.as_ref(), &identity_keypair.pubkey(), From 201524615456c70597ead679eab1b2f0c130bb81 Mon Sep 17 00:00:00 2001 From: Kamil Skalski Date: Tue, 2 Jun 2026 06:15:51 +0200 Subject: [PATCH 166/576] refactor(snapshots): remove thread pool from snapshot storage rebuilder (#12823) * refactor(snapshots): remove thread pool from snapshot storage rebuilder * Propagate and check file creator send errors together with rebuilder errors --- Cargo.lock | 1 - dev-bins/Cargo.lock | 1 - programs/sbf/Cargo.lock | 1 - runtime/Cargo.toml | 1 - runtime/src/serde_snapshot.rs | 8 +- .../src/serde_snapshot/obsolete_accounts.rs | 21 +- runtime/src/serde_snapshot/tests.rs | 15 +- runtime/src/snapshot_utils.rs | 75 ++++--- .../snapshot_storage_rebuilder.rs | 194 ++++-------------- snapshots/src/hardened_unpack.rs | 8 +- snapshots/src/unarchive.rs | 35 ++-- 11 files changed, 130 insertions(+), 230 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cb2c62c2389..383f98039bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9953,7 +9953,6 @@ dependencies = [ "mockall", "num-derive", "num-traits", - "num_cpus", "percentage", "proptest", "qualifier_attr", diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 8cdc34c9910..6c9e996849f 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -8344,7 +8344,6 @@ dependencies = [ "mockall", "num-derive", "num-traits", - "num_cpus", "percentage", "qualifier_attr", "rand 0.9.4", diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index f2e27c0d824..1c9e62adc82 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -8151,7 +8151,6 @@ dependencies = [ "mockall", "num-derive", "num-traits", - "num_cpus", "percentage", "qualifier_attr", "rand 0.9.4", diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 3d98a6e644a..0f4c7984e33 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -76,7 +76,6 @@ log = { workspace = true } mockall = { workspace = true } num-derive = { workspace = true } num-traits = { workspace = true } -num_cpus = { workspace = true } percentage = { workspace = true } qualifier_attr = { workspace = true } rand = { workspace = true } diff --git a/runtime/src/serde_snapshot.rs b/runtime/src/serde_snapshot.rs index a719ef3bbc1..ef0ade509d2 100644 --- a/runtime/src/serde_snapshot.rs +++ b/runtime/src/serde_snapshot.rs @@ -50,7 +50,7 @@ use { result::Result, sync::{ Arc, - atomic::{AtomicBool, AtomicUsize, Ordering}, + atomic::{AtomicBool, Ordering}, }, thread, time::Instant, @@ -905,7 +905,7 @@ pub(crate) fn remap_append_vec_file( old_append_vec_id: SerializedAccountsFileId, append_vec_file_info: FileInfo, next_append_vec_id: &AtomicAccountsFileId, - num_collisions: &AtomicUsize, + num_collisions: &mut usize, ) -> io::Result<(AccountsFileId, FileInfo)> { #[cfg(all(target_os = "linux", target_env = "gnu"))] let append_vec_path_cstr = cstring_from_path(&append_vec_file_info.path)?; @@ -957,7 +957,7 @@ pub(crate) fn remap_append_vec_file( // If we made it this far, a file exists at the new path. Record the collision // and try again. - num_collisions.fetch_add(1, Ordering::Relaxed); + *num_collisions += 1; }; // Only rename the file if the new ID is actually different from the original. In the target_os @@ -985,7 +985,7 @@ pub(crate) fn remap_and_reconstruct_single_storage( current_len: usize, append_vec_file_info: FileInfo, next_append_vec_id: &AtomicAccountsFileId, - num_collisions: &AtomicUsize, + num_collisions: &mut usize, ) -> Result, SnapshotError> { let (remapped_append_vec_id, remapped_append_vec_file_info) = remap_append_vec_file( slot, diff --git a/runtime/src/serde_snapshot/obsolete_accounts.rs b/runtime/src/serde_snapshot/obsolete_accounts.rs index 39faab8f052..25fe6aeff98 100644 --- a/runtime/src/serde_snapshot/obsolete_accounts.rs +++ b/runtime/src/serde_snapshot/obsolete_accounts.rs @@ -1,6 +1,5 @@ use { crate::serde_snapshot::SerializedAccountsFileId, - dashmap::DashMap, rayon::iter::{IntoParallelIterator, ParallelIterator}, serde::Serialize, solana_accounts_db::{ @@ -8,7 +7,7 @@ use { account_storage_entry::AccountStorageEntry, accounts_db::AccountsFileId, }, solana_clock::Slot, - std::sync::Arc, + std::{collections::HashMap, sync::Arc}, wincode::{SchemaRead, SchemaWrite}, }; @@ -116,7 +115,7 @@ impl SerdeObsoleteAccountsMap { SerdeObsoleteAccountsMap { map } } - pub(crate) fn into_dashmap(self) -> DashMap { + pub(crate) fn into_hashmap(self) -> HashMap { self.map.into_iter().collect() } } @@ -139,7 +138,7 @@ mod test { num_obsolete_accounts_per_storage: usize, ) { // Create a set of obsolete accounts - let obsolete_accounts = DashMap::::new(); + let mut obsolete_accounts = HashMap::::new(); for slot in 1..=num_storages { let obsolete_accounts_list = ObsoleteAccounts { accounts: (0..num_obsolete_accounts_per_storage) @@ -157,15 +156,13 @@ mod test { // Convert the obsolete accounts into a SerdeObsoleteAccountsMap let map = obsolete_accounts .iter() - .map(|entry| { + .map(|(slot, accounts)| { let serde_obsolete_accounts = SerdeObsoleteAccounts { - id: *entry.key() as SerializedAccountsFileId, + id: *slot as SerializedAccountsFileId, bytes: num_obsolete_accounts_per_storage as u64 * 1000, - accounts: SerdeObsoleteAccounts::items_from_obsolete_accounts( - entry.value().clone(), - ), + accounts: SerdeObsoleteAccounts::items_from_obsolete_accounts(accounts.clone()), }; - (*entry.key(), serde_obsolete_accounts) + (*slot, serde_obsolete_accounts) }) .collect(); let obsolete_accounts_map = SerdeObsoleteAccountsMap { map }; @@ -178,7 +175,7 @@ mod test { let cursor = Cursor::new(buf.as_slice()); let deserialized_obsolete_accounts: SerdeObsoleteAccountsMap = deserialize_wincode_from(cursor).unwrap(); - let map = deserialized_obsolete_accounts.into_dashmap(); + let mut map = deserialized_obsolete_accounts.into_hashmap(); // Verify the deserialized data matches the original obsolete accounts assert_eq!(map.len(), obsolete_accounts.len()); @@ -186,7 +183,7 @@ mod test { let deserialized_obsolete_accounts = map.remove(&slot).unwrap(); assert_eq!( obsolete_accounts, - deserialized_obsolete_accounts.1.into_tuple().0 + deserialized_obsolete_accounts.into_tuple().0 ); } } diff --git a/runtime/src/serde_snapshot/tests.rs b/runtime/src/serde_snapshot/tests.rs index c91798d3325..0137b5cbad0 100644 --- a/runtime/src/serde_snapshot/tests.rs +++ b/runtime/src/serde_snapshot/tests.rs @@ -36,10 +36,7 @@ mod serde_snapshot_tests { io::{self, BufReader, Cursor, Read, Write}, ops::RangeFull, path::{Path, PathBuf}, - sync::{ - Arc, - atomic::{AtomicUsize, Ordering}, - }, + sync::{Arc, atomic::Ordering}, }, tempfile::TempDir, test_case::test_case, @@ -851,18 +848,18 @@ mod serde_snapshot_tests { become_ungovernable(tmp.path()); let next_append_vec_id = AtomicAccountsFileId::new(next_id as u32); - let num_collisions = AtomicUsize::new(0); + let mut num_collisions = 0; let (remapped_id, remapped_file_info) = remap_append_vec_file( 123, old_id, old_file_info, &next_append_vec_id, - &num_collisions, + &mut num_collisions, ) .unwrap(); assert_eq!(remapped_id as usize, expected_remapped_id); assert_eq!(&remapped_file_info.path, &expected_remapped_path); - assert_eq!(num_collisions.load(Ordering::Relaxed), expected_collisions); + assert_eq!(num_collisions, expected_collisions); } #[test] @@ -880,13 +877,13 @@ mod serde_snapshot_tests { // In remap_append_vec_file() we want to handle EEXIST (collisions), but we want to return all // other errors let next_append_vec_id = AtomicAccountsFileId::new(457); - let num_collisions = AtomicUsize::new(0); + let mut num_collisions = 0; remap_append_vec_file( 123, 456, original_file_info, &next_append_vec_id, - &num_collisions, + &mut num_collisions, ) .unwrap(); } diff --git a/runtime/src/snapshot_utils.rs b/runtime/src/snapshot_utils.rs index 073e1cbd89f..5636a95b7a7 100644 --- a/runtime/src/snapshot_utils.rs +++ b/runtime/src/snapshot_utils.rs @@ -1246,7 +1246,6 @@ fn unarchive_snapshot( io_setup, ); - let num_rebuilder_threads = num_cpus::get_physical().saturating_sub(1).max(1); let snapshot_result = snapshot_fields_from_files(&file_receiver).and_then( |SnapshotFieldsBundle { snapshot_version, @@ -1258,11 +1257,9 @@ fn unarchive_snapshot( let snapshot_storage_lengths = accounts_db_fields.get_storage_lengths_for_snapshot_slots(base_slot)?; let (storage, measure_untar) = measure_time!( - SnapshotStorageRebuilder::spawn_rebuilder_threads( + SnapshotStorageRebuilder::rebuild_storages( snapshot_storage_lengths, - append_vec_files, - file_receiver, - num_rebuilder_threads, + append_vec_files.into_iter().chain(file_receiver), next_append_vec_id, SnapshotFrom::Archive, None, @@ -1285,8 +1282,15 @@ fn unarchive_snapshot( }) }, ); - unarchive_handle.join().unwrap()?; - snapshot_result + // Producer errors are usually the root cause (no files -> no reception). + let unarchive_result = unarchive_handle.join().expect("must join unarchive thread"); + match (unarchive_result, snapshot_result) { + // Rebuilder closed the receiver early; the producer's send failure is just the + // downstream symptom — surface the rebuilder's error instead. + (Err(SnapshotError::CrossbeamSend(_)), snap @ Err(_)) => snap, + (Err(err), _) => Err(err), + (Ok(()), snap) => snap, + } }) } @@ -1521,30 +1525,35 @@ pub(crate) fn rebuild_storages_from_snapshot_dir( account_paths, ); - let SnapshotFieldsBundle { - bank_fields, - accounts_db_fields, - append_vec_files, - .. - } = snapshot_fields_from_files(&file_receiver)?; - - let num_rebuilder_threads = num_cpus::get_physical().saturating_sub(1).max(1); + let snapshot_result = snapshot_fields_from_files(&file_receiver).and_then( + |SnapshotFieldsBundle { + bank_fields, + accounts_db_fields, + append_vec_files, + .. + }| { + let snapshot_storage_lengths = + accounts_db_fields.get_storage_lengths_for_snapshot_slots(None)?; + let storage = SnapshotStorageRebuilder::rebuild_storages( + snapshot_storage_lengths, + append_vec_files.into_iter().chain(file_receiver), + next_append_vec_id, + SnapshotFrom::Dir, + obsolete_accounts, + )?; + Ok((storage, bank_fields, accounts_db_fields)) + }, + ); - let snapshot_storage_lengths = - accounts_db_fields.get_storage_lengths_for_snapshot_slots(None)?; - let storage = SnapshotStorageRebuilder::spawn_rebuilder_threads( - snapshot_storage_lengths, - append_vec_files, - file_receiver, - num_rebuilder_threads, - next_append_vec_id, - SnapshotFrom::Dir, - obsolete_accounts, - )?; - stream_files_handle - .join() - .expect("should join file stream thread")?; - Ok((storage, bank_fields, accounts_db_fields)) + // Producer errors are usually the root cause (no files -> no reception). + let stream_files_result = stream_files_handle.join().expect("must join dir thread"); + match (stream_files_result, snapshot_result) { + // Rebuilder closed the receiver early; the producer's send failure is just the + // downstream symptom — surface the rebuilder's error instead. + (Err(SnapshotError::CrossbeamSend(_)), snap @ Err(_)) => snap, + (Err(err), _) => Err(err), + (Ok(()), snap) => snap, + } } /// Reads the `snapshot_version` from a file. Before opening the file, its size @@ -2705,14 +2714,14 @@ mod tests { .unwrap(); // Deserialize - let deserialized_accounts = + let mut deserialized_accounts = deserialize_obsolete_accounts(bank_snapshot_dir, MAX_OBSOLETE_ACCOUNTS_FILE_SIZE) .unwrap() - .into_dashmap(); + .into_hashmap(); // Verify for storage in &snapshot_storages { - let obsolete_accounts = deserialized_accounts.remove(&storage.slot()).unwrap().1; + let obsolete_accounts = deserialized_accounts.remove(&storage.slot()).unwrap(); assert!(obsolete_accounts.into_tuple().2 == 0); } } diff --git a/runtime/src/snapshot_utils/snapshot_storage_rebuilder.rs b/runtime/src/snapshot_utils/snapshot_storage_rebuilder.rs index 4e21cf4f88b..a03af4b726c 100644 --- a/runtime/src/snapshot_utils/snapshot_storage_rebuilder.rs +++ b/runtime/src/snapshot_utils/snapshot_storage_rebuilder.rs @@ -7,13 +7,7 @@ use { remap_and_reconstruct_single_storage, }, agave_fs::FileInfo, - crossbeam_channel::{Receiver, Sender, select, unbounded}, - dashmap::DashMap, log::*, - rayon::{ - ThreadPool, ThreadPoolBuilder, - iter::{IntoParallelIterator, ParallelIterator}, - }, solana_accounts_db::{ account_storage::AccountStorageMap, accounts_db::{AccountsFileId, AtomicAccountsFileId}, @@ -23,139 +17,77 @@ use { collections::HashMap, path::PathBuf, str::FromStr as _, - sync::{ - Arc, - atomic::{AtomicUsize, Ordering}, - }, - time::Instant, + sync::{Arc, atomic::Ordering}, + time::{Duration, Instant}, }, }; +const PROGRESS_LOG_INTERVAL: Duration = Duration::from_secs(2); + /// Stores state for rebuilding snapshot storages #[derive(Debug)] pub(crate) struct SnapshotStorageRebuilder { - /// Receiver for unpacked snapshot storage files - file_receiver: Receiver, - /// Number of threads to rebuild with - num_threads: usize, /// Snapshot storage lengths - from the snapshot file snapshot_storage_lengths: HashMap, /// Container for storing rebuilt snapshot storages storage: AccountStorageMap, /// Tracks next append_vec_id next_append_vec_id: Arc, - /// Tracker for number of processed slots - processed_slot_count: AtomicUsize, /// Tracks the number of collisions in AccountsFileId - num_collisions: AtomicUsize, + num_collisions: usize, + /// Number of storage slots that have been rebuilt so far + processed_slot_count: usize, /// Rebuild from the snapshot files or archives snapshot_from: SnapshotFrom, /// obsolete accounts for all storages - obsolete_accounts: DashMap, + obsolete_accounts: HashMap, } impl SnapshotStorageRebuilder { - /// Create the SnapshotStorageRebuilder for storing state during rebuilding - /// - pre-allocates data for storage file infos - fn new( - file_receiver: Receiver, - num_threads: usize, - next_append_vec_id: Arc, + /// Rebuild snapshot storages on the current thread by consuming `files`. + pub(crate) fn rebuild_storages( snapshot_storage_lengths: HashMap, + files: impl IntoIterator, + next_append_vec_id: Arc, snapshot_from: SnapshotFrom, obsolete_accounts: Option, - ) -> Self { - let storage = AccountStorageMap::with_capacity(snapshot_storage_lengths.len()); - Self { - file_receiver, - num_threads, + ) -> Result { + let mut rebuilder = Self { + storage: AccountStorageMap::with_capacity(snapshot_storage_lengths.len()), snapshot_storage_lengths, - storage, next_append_vec_id, - processed_slot_count: AtomicUsize::new(0), - num_collisions: AtomicUsize::new(0), + num_collisions: 0, + processed_slot_count: 0, snapshot_from, obsolete_accounts: obsolete_accounts - .map(|map| map.into_dashmap()) + .map(|map| map.into_hashmap()) .unwrap_or_default(), - } - } - - /// Synchronously spawns threads to rebuild snapshot storages returning when they complete - /// - /// Spawn threads for processing buffered append_vec_files, and then received files - pub(crate) fn spawn_rebuilder_threads( - snapshot_storage_lengths: HashMap, - append_vec_files: Vec, - file_receiver: Receiver, - num_threads: usize, - next_append_vec_id: Arc, - snapshot_from: SnapshotFrom, - obsolete_accounts: Option, - ) -> Result { - let rebuilder = Arc::new(SnapshotStorageRebuilder::new( - file_receiver, - num_threads, - next_append_vec_id, - snapshot_storage_lengths, - snapshot_from, - obsolete_accounts, - )); - - let thread_pool = rebuilder.build_thread_pool(); + }; - if snapshot_from == SnapshotFrom::Archive { - // Synchronously process buffered append_vec_files - thread_pool.install(|| rebuilder.process_buffered_files(append_vec_files))?; - } + let mut last_log_time = Instant::now(); - // Asynchronously spawn threads to process received append_vec_files - let (exit_sender, exit_receiver) = unbounded(); - for _ in 0..rebuilder.num_threads { - Self::spawn_receiver_thread(&thread_pool, exit_sender.clone(), rebuilder.clone()); + for file_info in files { + rebuilder.process_append_vec_file(file_info)?; + let now = Instant::now(); + if now.duration_since(last_log_time) >= PROGRESS_LOG_INTERVAL { + rebuilder.log_progress(); + last_log_time = now; + } } - drop(exit_sender); // drop otherwise loop below will never end - // wait for asynchronous threads to complete - rebuilder.wait_for_completion(exit_receiver)?; - Ok(Arc::try_unwrap(rebuilder).unwrap().storage) + Ok(rebuilder.storage) } - /// Processes buffered append_vec_files - fn process_buffered_files(&self, append_vec_files: Vec) -> Result<(), SnapshotError> { - append_vec_files - .into_par_iter() - .map(|file_info| self.process_append_vec_file(file_info)) - .collect::>() + fn log_progress(&self) { + info!( + "rebuilt storages for {}/{} slots with {} collisions", + self.processed_slot_count, + self.snapshot_storage_lengths.len(), + self.num_collisions, + ); } - /// Spawn a single thread to process received append_vec_files - fn spawn_receiver_thread( - thread_pool: &ThreadPool, - exit_sender: Sender>, - rebuilder: Arc, - ) { - thread_pool.spawn(move || { - for file_info in rebuilder.file_receiver.iter() { - match rebuilder.process_append_vec_file(file_info) { - Ok(_) => {} - Err(err) => { - exit_sender - .send(Err(err)) - .expect("sender should be connected"); - return; - } - } - } - - exit_sender - .send(Ok(())) - .expect("sender should be connected"); - }) - } - - /// Process an append_vec_file - fn process_append_vec_file(&self, file_info: FileInfo) -> Result<(), SnapshotError> { + fn process_append_vec_file(&mut self, file_info: FileInfo) -> Result<(), SnapshotError> { let filename = file_info.path.file_name().unwrap().to_str().unwrap(); if let Ok((slot, append_vec_id)) = get_slot_and_append_vec_id(filename) { if self.snapshot_from == SnapshotFrom::Dir { @@ -167,13 +99,17 @@ impl SnapshotStorageRebuilder { .fetch_max((append_vec_id + 1) as AccountsFileId, Ordering::Relaxed); } self.process_complete_slot(slot, file_info)?; - self.processed_slot_count.fetch_add(1, Ordering::AcqRel); + self.processed_slot_count += 1; } Ok(()) } /// Process a slot that has received all storage entries - fn process_complete_slot(&self, slot: Slot, file_info: FileInfo) -> Result<(), SnapshotError> { + fn process_complete_slot( + &mut self, + slot: Slot, + file_info: FileInfo, + ) -> Result<(), SnapshotError> { let filename = file_info.path.file_name().unwrap().to_str().unwrap(); let (_, old_append_vec_id) = get_slot_and_append_vec_id(filename)?; let Some(¤t_len) = self.snapshot_storage_lengths.get(&slot) else { @@ -189,7 +125,7 @@ impl SnapshotStorageRebuilder { current_len, file_info, &self.next_append_vec_id, - &self.num_collisions, + &mut self.num_collisions, )?, SnapshotFrom::Dir => reconstruct_single_storage( &slot, @@ -198,7 +134,7 @@ impl SnapshotStorageRebuilder { old_append_vec_id as AccountsFileId, self.obsolete_accounts .remove(&slot) - .map(|(_, accounts)| accounts.into_tuple()), + .map(|accounts| accounts.into_tuple()), )?, }; @@ -213,48 +149,6 @@ impl SnapshotStorageRebuilder { Ok(()) } } - - /// Wait for the completion of the rebuilding threads - fn wait_for_completion( - &self, - exit_receiver: Receiver>, - ) -> Result<(), SnapshotError> { - let num_slots = self.snapshot_storage_lengths.len(); - let mut last_log_time = Instant::now(); - loop { - select! { - recv(exit_receiver) -> maybe_exit_signal => { - match maybe_exit_signal { - Ok(Ok(_)) => continue, // thread exited successfully - Ok(Err(err)) => { // thread exited with error - return Err(err); - } - Err(_) => break, // all threads have exited - channel disconnected - } - } - default(std::time::Duration::from_millis(100)) => { - let now = Instant::now(); - if now.duration_since(last_log_time).as_millis() >= 2000 { - let num_processed_slots = self.processed_slot_count.load(Ordering::Relaxed); - let num_collisions = self.num_collisions.load(Ordering::Relaxed); - info!("rebuilt storages for {num_processed_slots}/{num_slots} slots with {num_collisions} collisions"); - last_log_time = now; - } - } - } - } - - Ok(()) - } - - /// Builds thread pool to rebuild with - fn build_thread_pool(&self) -> ThreadPool { - ThreadPoolBuilder::default() - .thread_name(|i| format!("solRbuildSnap{i:02}")) - .num_threads(self.num_threads) - .build() - .expect("new rayon threadpool") - } } /// Get the slot and append vec id from the filename diff --git a/snapshots/src/hardened_unpack.rs b/snapshots/src/hardened_unpack.rs index 475213e5d8f..b8b717e49a3 100644 --- a/snapshots/src/hardened_unpack.rs +++ b/snapshots/src/hardened_unpack.rs @@ -87,7 +87,7 @@ enum UnpackPath<'a> { #[allow(clippy::arithmetic_side_effects)] fn unpack_archive<'a, C>( input: impl Read, - mut file_creator: Box, + mut file_creator: Box, apparent_limit_size: u64, actual_limit_size: u64, limit_count: u64, @@ -315,7 +315,7 @@ fn validate_inside_dst(dst: &Path, file_dst: &Path) -> Result { /// sends entry file paths through the `sender` channel pub(super) fn streaming_unpack_snapshot( input: impl Read, - file_creator: Box, + file_creator: Box, ledger_dir: &Path, account_paths: &[PathBuf], ) -> Result<()> { @@ -324,7 +324,7 @@ pub(super) fn streaming_unpack_snapshot( fn unpack_snapshot_with_processors( input: impl Read, - file_creator: Box, + file_creator: Box, ledger_dir: &Path, account_paths: &[PathBuf], mut accounts_path_processor: F, @@ -417,7 +417,7 @@ fn is_valid_snapshot_archive_entry(parts: &[&str], kind: tar::EntryType) -> bool pub(super) fn unpack_genesis( input: impl Read, - file_creator: Box, + file_creator: Box, unpack_dir: &Path, max_genesis_archive_unpacked_size: u64, ) -> Result<()> { diff --git a/snapshots/src/unarchive.rs b/snapshots/src/unarchive.rs index ed76cdf9aa6..e85312d815f 100644 --- a/snapshots/src/unarchive.rs +++ b/snapshots/src/unarchive.rs @@ -1,15 +1,17 @@ use { crate::{ ArchiveFormat, ArchiveFormatDecompressor, + error::SnapshotError, hardened_unpack::{self, UnpackError}, }, agave_fs::{FileInfo, buffered_reader, file_io::file_creator, io_setup::IoSetupState}, bzip2::bufread::BzDecoder, - crossbeam_channel::Sender, + crossbeam_channel::{SendError, Sender}, std::{ fs, io::{self, BufRead, BufReader}, path::{Path, PathBuf}, + sync::OnceLock, thread::{self, Scope, ScopedJoinHandle}, time::Instant, }, @@ -33,8 +35,10 @@ pub fn streaming_unarchive_snapshot<'scope, 'env: 'scope>( snapshot_archive_path: PathBuf, archive_format: ArchiveFormat, io_setup: &'env IoSetupState, -) -> ScopedJoinHandle<'scope, Result<(), UnpackError>> { +) -> ScopedJoinHandle<'scope, Result<(), SnapshotError>> { let do_unpack = move |archive_path: &Path| { + let first_failed_send = OnceLock::::new(); + let first_failed_send_ref = &first_failed_send; let (decompressor, file_creator) = { // Bound the buffers based on input archive size (decompression multiplies content size, // but buffering more than origin isn't necessary). @@ -47,15 +51,15 @@ pub fn streaming_unarchive_snapshot<'scope, 'env: 'scope>( ( decompressor, file_creator(write_buf_size, io_setup, move |file_info| { - let result = file_sender.send(file_info); - if let Err(err) = result { - panic!( - "failed to send path '{}' from unpacker to rebuilder: {err}", - err.0.path.display(), - ); + match file_sender.send(file_info) { + // Channel owns the file now — don't pass it back for closing. + Ok(()) => None, + Err(SendError(FileInfo { file, path, .. })) => { + let _ = first_failed_send_ref.set(path); + // Hand the file back so the creator closes it. + Some(file) + } } - // Don't pass `File` back to file creator, so it's not closed (owned by channel now) - None })?, ) }; @@ -66,13 +70,16 @@ pub fn streaming_unarchive_snapshot<'scope, 'env: 'scope>( ledger_dir.as_path(), &account_paths, ) + .map(|()| first_failed_send.into_inner()) }; - thread::Builder::new() .name("solTarUnpack".to_string()) - .spawn_scoped(scope, move || { - do_unpack(&snapshot_archive_path) - .map_err(|err| UnpackError::Unpack(Box::new(err), snapshot_archive_path)) + .spawn_scoped(scope, move || -> Result<(), SnapshotError> { + match do_unpack(&snapshot_archive_path) { + Err(err) => Err(UnpackError::Unpack(Box::new(err), snapshot_archive_path).into()), + Ok(Some(path)) => Err(SnapshotError::CrossbeamSend(SendError(path))), + Ok(None) => Ok(()), + } }) .unwrap() } From c519954467c40dbf289b70075316f98bdc347e38 Mon Sep 17 00:00:00 2001 From: Jon C Date: Tue, 2 Jun 2026 11:33:49 +0200 Subject: [PATCH 167/576] SIMD-0232: Add feature key (#12796) #### Problem Now that #12550 has landed, everything is in place for SIMD-0232 to be enabled, but it still doesn't have a feature key. #### Summary of changes Add a real feature key! --- feature-set/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature-set/src/lib.rs b/feature-set/src/lib.rs index dba7ac117c0..611e4480ad9 100644 --- a/feature-set/src/lib.rs +++ b/feature-set/src/lib.rs @@ -1425,7 +1425,7 @@ pub mod commission_rate_in_basis_points { } pub mod custom_commission_collector { - solana_pubkey::declare_id!("CustomCommissionCo11ector111111111111111111"); + solana_pubkey::declare_id!("3HcSrCTGXTUnrTueHi4DAwNuMxZSsm5xui2Ax3mgxHqf"); } pub mod enable_bls12_381_syscall { From be1c3a10bde255bd7c2b3e03bd31fa5d7842f576 Mon Sep 17 00:00:00 2001 From: Philipp Beyrl Date: Tue, 2 Jun 2026 12:43:04 +0200 Subject: [PATCH 168/576] pubsub-client: mark account_subscribe doctest as no_run (#12288) `cargo test --doc -p solana-pubsub-client` fails with a runtime panic: thread 'main' panicked at rustls-0.23.40/src/crypto/mod.rs:249:14: Could not automatically determine the process-level CryptoProvider from Rustls crate features. Call CryptoProvider::install_default() before this point to select a provider manually, or make sure exactly one of the 'aws-lc-rs' and 'ring' features is enabled. The doctest at `pubsub-client/src/pubsub_client.rs:47` is opened with plain ` ``` ` (rather than ` ```no_run `) and contains the rustdoc- convention trailing hidden invocation `# get_account_updates(...)` that actually executes the example at test time. The example calls `PubsubClient::account_subscribe` against `wss://api.devnet.solana.com/`, which initializes a rustls connection. Rustls 0.23+ panics when no CryptoProvider is configured, and the workspace-root `Cargo.toml` declares rustls with `default-features = false` and only the `std` feature, so neither `aws-lc-rs` nor `ring` is enabled. The package-scoped doctest fails because no other crate enables a rustls CryptoProvider feature in the dependency graph for this single-crate build. Broader workspace doctest runs (`ci/test-docs.sh`, which executes `cargo test --all --doc --exclude solana-local-cluster`) can hide the panic through cargo's feature unification, since other crates in the workspace bring in a CryptoProvider feature transitively. However, even when the panic is hidden, the example still performs real network I/O at test time. Empirically, `cargo test --doc -p solana-pubsub-client -p solana-rpc-client-api` does not panic but blocks for over 60 seconds attempting the actual WebSocket connection to devnet. The example is intended to demonstrate the API, not to actually connect, so marking it `no_run` is the right fix regardless of which build mode triggers the symptom: the doctest still compiles (so the API surface is validated) but no longer attempts a real network connection. --- pubsub-client/src/pubsub_client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubsub-client/src/pubsub_client.rs b/pubsub-client/src/pubsub_client.rs index 37bd276d7f6..df0a57210d0 100644 --- a/pubsub-client/src/pubsub_client.rs +++ b/pubsub-client/src/pubsub_client.rs @@ -44,7 +44,7 @@ //! This example subscribes to account events and then loops forever receiving //! them. //! -//! ``` +//! ```no_run //! use anyhow::Result; //! use solana_commitment_config::CommitmentConfig; //! use solana_pubkey::Pubkey; From 07f8ec0c7f7c62466706cc45aba5f2ce0b519a4e Mon Sep 17 00:00:00 2001 From: Yihau Chen Date: Tue, 2 Jun 2026 20:19:57 +0800 Subject: [PATCH 169/576] ci: clean up buildkite env in docker run (#12893) * ci: clean up buildkite env in docker run * fake change * Revert "fake change" This reverts commit 34957c6c0d84bb194fb3bfbfb7aec5c5d0ea4b7f. --- ci/docker-run.sh | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/ci/docker-run.sh b/ci/docker-run.sh index 4effcbeec8b..9ff494c7947 100755 --- a/ci/docker-run.sh +++ b/ci/docker-run.sh @@ -43,6 +43,12 @@ ARGS=( if [[ -n $CI ]]; then if [[ -n $BUILDKITE ]]; then + ARGS+=( + --env BUILDKITE + --env BUILDKITE_AGENT_ACCESS_TOKEN + --env BUILDKITE_JOB_ID + ) + # I hate buildkite-esque echo is leaking into this generic shell wrapper. # but it's easiest to notify to users, and properly guarded under $BUILDKITE_ env # (2 is chosen for third time's the charm). @@ -112,11 +118,6 @@ fi # Environment variables to propagate into the container ARGS+=( - --env BUILDKITE - --env BUILDKITE_AGENT_ACCESS_TOKEN - --env BUILDKITE_JOB_ID - --env BUILDKITE_PARALLEL_JOB - --env BUILDKITE_PARALLEL_JOB_COUNT --env CI --env CI_BRANCH --env CI_BASE_BRANCH From 6d188f36e2b985462972cc12f421e181991e2eb9 Mon Sep 17 00:00:00 2001 From: Rocker Zhang Date: Tue, 2 Jun 2026 20:36:01 +0800 Subject: [PATCH 170/576] core/bls-sigverify: votes channel-full does blocking send too (follow-up to #12685) (#12855) #12685 fixed send_certs_to_pool to fall back to a blocking send when the channel_to_pool is full, per #12660: a dropped cert that the verifier cached would be re-suppressed on retry, potentially preventing finalization. send_votes_to_pool writes to the same channel but still silently dropped on Full; this PR mirrors the cert fix onto the votes path. Also: - Both send_certs_to_pool and send_votes_to_pool now increment pool_sent on the blocking-send success branch, so the counter reflects every message that made it onto the channel (fast path or blocking). - Add the symmetric pool_outstanding_msgs field to SigVerifyVoteStats (declaration, merge, report). - Update test_blssigverifier_send_packets_channel_full for the new no-drop contract: drain in a background thread so the blocking send completes; assert both messages are delivered and the channel has no leftovers (try_recv -> Empty inside the drain thread). --- core/src/bls_sigverify/bls_sigverifier.rs | 31 ++++++++++++++++++----- core/src/bls_sigverify/stats.rs | 8 +++++- core/src/bls_sigverify/utils.rs | 20 ++++++++++++--- 3 files changed, 47 insertions(+), 12 deletions(-) diff --git a/core/src/bls_sigverify/bls_sigverifier.rs b/core/src/bls_sigverify/bls_sigverifier.rs index 72189b2229c..129c8dd8980 100644 --- a/core/src/bls_sigverify/bls_sigverifier.rs +++ b/core/src/bls_sigverify/bls_sigverifier.rs @@ -655,16 +655,33 @@ mod tests { ctx.verifier .verify_and_send_batches(messages_to_batches(std::slice::from_ref(&msg1))) .unwrap(); + + // The cap-1 channel is now full. The second send hits Full and falls + // back to a blocking send (see `send_votes_to_pool`); drain in a + // background thread so the blocking send can complete. + let pool_receiver = ctx.pool_receiver.clone(); + let drain = std::thread::spawn(move || { + let m1 = pool_receiver.recv().expect("recv msg1"); + let m2 = pool_receiver.recv().expect("recv msg2"); + // No leftover messages on the channel after both deliveries. + assert!(matches!( + pool_receiver.try_recv(), + Err(crossbeam_channel::TryRecvError::Empty) + )); + (m1, m2) + }); + ctx.verifier - .verify_and_send_batches(messages_to_batches(&[msg2])) + .verify_and_send_batches(messages_to_batches(std::slice::from_ref(&msg2))) .unwrap(); - // We failed to send the second message because the channel is full. - let msgs = ctx.pool_receiver.try_iter().flatten().collect::>(); - assert_eq!(msgs.len(), 1); - assert_eq!(msgs, vec![msg1]); - assert_eq!(ctx.verifier.stats.vote_stats.pool_sent, 1); - assert_eq!(ctx.verifier.stats.vote_stats.pool_channel_full, 1); + let (m1_recv, m2_recv) = drain.join().expect("drain joined"); + // Both messages were eventually delivered (no silent drop). + assert_eq!(m1_recv, vec![msg1]); + assert_eq!(m2_recv, vec![msg2]); + // pool_sent counts every message that made it onto the channel, + // whether via try_send or the blocking fallback. + assert_eq!(ctx.verifier.stats.vote_stats.pool_sent, 2); } #[test] diff --git a/core/src/bls_sigverify/stats.rs b/core/src/bls_sigverify/stats.rs index 6a5c0317773..9a787f448a1 100644 --- a/core/src/bls_sigverify/stats.rs +++ b/core/src/bls_sigverify/stats.rs @@ -304,9 +304,11 @@ pub(super) struct SigVerifyVoteStats { pub(super) rewards_sent: u64, /// Number of times the channel to rewards was full. pub(super) rewards_channel_full: u64, + /// How many messages are outstanding on the channel. + pub(super) pool_outstanding_msgs: u64, /// Number of votes sent successfully over the channel to consensus pool. pub(super) pool_sent: u64, - /// Number of times the channel to consensus pool was full. + /// Number of times the channel to consensus pool was full and we resorted to blocking send. pub(super) pool_channel_full: u64, /// Number of votes sent successfully over the channel to repair. pub(super) repair_sent: u64, @@ -337,6 +339,7 @@ impl SigVerifyVoteStats { rewards_channel_full, repair_sent, repair_channel_full, + pool_outstanding_msgs, pool_sent, pool_channel_full, fn_verify_and_send_votes_stats, @@ -354,6 +357,7 @@ impl SigVerifyVoteStats { self.rewards_channel_full += rewards_channel_full; self.repair_sent += repair_sent; self.repair_channel_full += repair_channel_full; + self.pool_outstanding_msgs += pool_outstanding_msgs; self.pool_sent += pool_sent; self.pool_channel_full += pool_channel_full; self.fn_verify_and_send_votes_stats @@ -377,6 +381,7 @@ impl SigVerifyVoteStats { rewards_channel_full, repair_sent, repair_channel_full, + pool_outstanding_msgs, pool_sent, pool_channel_full, fn_verify_and_send_votes_stats, @@ -396,6 +401,7 @@ impl SigVerifyVoteStats { ("rewards_channel_full", *rewards_channel_full, i64), ("repair_sent", *repair_sent, i64), ("repair_channel_full", *repair_channel_full, i64), + ("pool_outstanding_msgs", *pool_outstanding_msgs, i64), ("pool_sent", *pool_sent, i64), ("pool_channel_full", *pool_channel_full, i64), ( diff --git a/core/src/bls_sigverify/utils.rs b/core/src/bls_sigverify/utils.rs index cd6776b46ec..0e51cb117f7 100644 --- a/core/src/bls_sigverify/utils.rs +++ b/core/src/bls_sigverify/utils.rs @@ -52,23 +52,34 @@ pub(super) fn send_votes_to_rewards( } } +/// Sends the `votes` to the consensus pool. If the channel is full, then does a +/// blocking send. pub(super) fn send_votes_to_pool( votes: Vec, channel: &Sender>, stats: &mut SigVerifyVoteStats, ) -> Result<(), SigVerifyVoteError> { - let len = votes.len(); - if len == 0 { + if votes.is_empty() { return Ok(()); } + let len = votes.len(); match channel.try_send(votes) { Ok(()) => { stats.pool_sent += len as u64; + stats.pool_outstanding_msgs = channel.len() as u64; Ok(()) } - Err(TrySendError::Full(_)) => { + Err(TrySendError::Full(msgs)) => { stats.pool_channel_full += 1; - Ok(()) + error!("votes channel to consensus pool is full. Doing a blocking send."); + match channel.send(msgs) { + Ok(()) => { + info!("votes channel to consensus pool has space again"); + stats.pool_sent += len as u64; + Ok(()) + } + Err(_) => Err(SigVerifyVoteError::ConsensusPoolChannelDisconnected), + } } Err(TrySendError::Disconnected(_)) => { Err(SigVerifyVoteError::ConsensusPoolChannelDisconnected) @@ -120,6 +131,7 @@ pub(super) fn send_certs_to_pool( match channel_to_pool.send(msgs) { Ok(()) => { info!("certs channel to consensus pool has space again"); + stats.pool_sent += len as u64; Ok(()) } Err(_) => Err(SigVerifyCertError::ConsensusPoolChannelDisconnected), From 45d43c31a09e01d818724c45dc96189d9b295cb3 Mon Sep 17 00:00:00 2001 From: Kamil Skalski Date: Tue, 2 Jun 2026 15:16:22 +0200 Subject: [PATCH 171/576] perf(snapshots): switch status cache serialization to wincode (#12880) --- runtime/src/serde_snapshot/status_cache.rs | 90 ++++++++++------------ runtime/src/snapshot_utils.rs | 8 +- snapshots/src/error.rs | 3 + 3 files changed, 51 insertions(+), 50 deletions(-) diff --git a/runtime/src/serde_snapshot/status_cache.rs b/runtime/src/serde_snapshot/status_cache.rs index dff59bc38cc..f84da15d3a6 100644 --- a/runtime/src/serde_snapshot/status_cache.rs +++ b/runtime/src/serde_snapshot/status_cache.rs @@ -5,15 +5,15 @@ use shuttle::sync::Mutex; #[cfg(not(feature = "shuttle-test"))] use std::sync::Mutex; use { - crate::{bank::BankSlotDelta, snapshot_utils, status_cache::KeySlice}, + crate::{bank::BankSlotDelta, serde_snapshot, snapshot_utils, status_cache::KeySlice}, agave_fs::io_setup::IoSetupState, - bincode::{self, Options as _}, - serde::{Deserialize, Serialize}, + serde::Serialize, solana_clock::Slot, solana_hash::Hash, solana_instruction::error::InstructionError, solana_transaction_error::TransactionError, std::{collections::HashMap, path::Path, sync::Arc}, + wincode::{SchemaRead, SchemaWrite}, }; #[cfg_attr( @@ -30,46 +30,43 @@ type SerdeStatus = ahash::HashMap)>; pub fn serialize_status_cache( slot_deltas: &[BankSlotDelta], status_cache_path: &Path, + io_setup: &IoSetupState, ) -> agave_snapshots::Result { - snapshot_utils::serialize_snapshot_data_file( - status_cache_path, - &IoSetupState::default(), - |stream| { - let snapshot_slot_deltas = slot_deltas - .iter() - .map(|slot_delta| { - let status_map = slot_delta.2.lock().unwrap(); - let snapshot_status_map = status_map - .iter() - .map(|(key, value)| { + snapshot_utils::serialize_snapshot_data_file(status_cache_path, io_setup, |stream| { + let snapshot_slot_deltas = slot_deltas + .iter() + .map(|slot_delta| { + let status_map = slot_delta.2.lock().unwrap(); + let snapshot_status_map = status_map + .iter() + .map(|(key, value)| { + ( + *key, ( - *key, - ( - value.0, - value - .1 - .iter() - .map(|(key_slice, result)| { - ( - *key_slice, - result - .as_ref() - .map(|_| ()) - .map_err(SerdeTransactionError::from), - ) - }) - .collect::>(), - ), - ) - }) - .collect::>(); - (slot_delta.0, slot_delta.1, snapshot_status_map) - }) - .collect::>(); - bincode::serialize_into(stream, &snapshot_slot_deltas)?; - Ok(()) - }, - ) + value.0, + value + .1 + .iter() + .map(|(key_slice, result)| { + ( + *key_slice, + result + .as_ref() + .map(|_| ()) + .map_err(SerdeTransactionError::from), + ) + }) + .collect::>(), + ), + ) + }) + .collect::>(); + (slot_delta.0, slot_delta.1, snapshot_status_map) + }) + .collect::>(); + serde_snapshot::serialize_into(stream, &snapshot_slot_deltas)?; + Ok(()) + }) } /// Deserializes the status cache from file at `status_cache_path` @@ -79,11 +76,8 @@ pub fn deserialize_status_cache( status_cache_path: &Path, ) -> agave_snapshots::Result> { snapshot_utils::deserialize_snapshot_data_file(status_cache_path, |stream| { - let snapshot_slot_deltas: Vec = bincode::options() - .with_limit(snapshot_utils::MAX_SNAPSHOT_DATA_FILE_SIZE) - .with_fixint_encoding() - .allow_trailing_bytes() - .deserialize_from(stream)?; + let snapshot_slot_deltas: Vec = + serde_snapshot::deserialize_wincode_from(stream)?; let slot_deltas = snapshot_slot_deltas .into_iter() @@ -121,7 +115,7 @@ pub fn deserialize_status_cache( frozen_abi(digest = "5pMgydVNgsYbg64Trhjxbftsug5La7fRDmooyrsHd4wy"), derive(AbiExample, AbiEnumVisitor) )] -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Eq, Clone, Serialize, SchemaRead, SchemaWrite)] enum SerdeTransactionError { AccountInUse, AccountLoadedTwice, @@ -308,7 +302,7 @@ impl From for TransactionError { /// contains a string. #[cfg_attr(test, derive(strum_macros::FromRepr, strum_macros::EnumIter))] #[cfg_attr(feature = "frozen-abi", derive(AbiExample, AbiEnumVisitor))] -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Eq, Clone, Serialize, SchemaRead, SchemaWrite)] enum SerdeInstructionError { GenericError, InvalidArgument, diff --git a/runtime/src/snapshot_utils.rs b/runtime/src/snapshot_utils.rs index 5636a95b7a7..7e9bc3d5bff 100644 --- a/runtime/src/snapshot_utils.rs +++ b/runtime/src/snapshot_utils.rs @@ -547,8 +547,12 @@ pub fn serialize_snapshot( let status_cache_path = bank_snapshot_dir.join(snapshot_paths::SNAPSHOT_STATUS_CACHE_FILENAME); let (status_cache_consumed_size, status_cache_serialize_us) = measure_us!( - serde_snapshot::serialize_status_cache(status_cache_slot_deltas, &status_cache_path) - .map_err(|err| AddBankSnapshotError::SerializeStatusCache(Box::new(err)))? + serde_snapshot::serialize_status_cache( + status_cache_slot_deltas, + &status_cache_path, + io_setup, + ) + .map_err(|err| AddBankSnapshotError::SerializeStatusCache(Box::new(err)))? ); let version_path = bank_snapshot_dir.join(snapshot_paths::SNAPSHOT_VERSION_FILENAME); diff --git a/snapshots/src/error.rs b/snapshots/src/error.rs index de2bb906ab6..7c04fceeef4 100644 --- a/snapshots/src/error.rs +++ b/snapshots/src/error.rs @@ -24,6 +24,9 @@ pub enum SnapshotError { #[error("deserialization error: {0}")] DeserializeWincode(#[from] wincode::ReadError), + #[error("serialization error: {0}")] + SerializeWincode(#[from] wincode::WriteError), + #[error("crossbeam send error: {0}")] CrossbeamSend(#[from] crossbeam_channel::SendError), From b558b294a8cc2beaa057c3b5ad0485102702b0fa Mon Sep 17 00:00:00 2001 From: steviez Date: Tue, 2 Jun 2026 09:13:57 -0500 Subject: [PATCH 172/576] chore: Remove instances of &Arc> (#12894) Using &Arc is a bit of an anti-pattern; functions can either take an Arc if they need the Arc or take &T if they just need the ref --- banks-server/src/banks_server.rs | 2 +- core/src/replay_stage.rs | 8 ++++---- core/src/replay_stage/update_parent.rs | 4 ++-- core/src/sample_performance_service.rs | 4 +--- core/src/tvu.rs | 6 +++--- core/src/validator.rs | 6 +++--- rpc/src/optimistically_confirmed_bank_tracker.rs | 2 +- rpc/src/rpc.rs | 6 +++--- rpc/src/rpc_service.rs | 11 +++++------ rpc/src/rpc_subscriptions.rs | 4 ++-- runtime/src/block_component_processor.rs | 2 +- .../src/send_transaction_service.rs | 8 ++++---- 12 files changed, 30 insertions(+), 33 deletions(-) diff --git a/banks-server/src/banks_server.rs b/banks-server/src/banks_server.rs index a918e59fb6c..4335c21a0f9 100644 --- a/banks-server/src/banks_server.rs +++ b/banks-server/src/banks_server.rs @@ -506,7 +506,7 @@ pub async fn start_tcp_server( let client = create_client(None, tpu_addr, exit.clone()); SendTransactionService::new( - &bank_forks, + bank_forks.clone(), receiver, client, Config { diff --git a/core/src/replay_stage.rs b/core/src/replay_stage.rs index e00af997cfa..458b6c68773 100644 --- a/core/src/replay_stage.rs +++ b/core/src/replay_stage.rs @@ -1783,7 +1783,7 @@ impl ReplayStage { tower_storage: &dyn TowerStorage, node_pubkey: &Pubkey, vote_account: &Pubkey, - bank_forks: &Arc>, + bank_forks: &RwLock, ) -> Result { let tower = Tower::restore(tower_storage, node_pubkey).and_then(|restored_tower| { let root_bank = bank_forks.read().unwrap().root_bank(); @@ -2826,7 +2826,7 @@ impl ReplayStage { #[allow(clippy::too_many_arguments)] fn maybe_start_leader( my_pubkey: &Pubkey, - bank_forks: &Arc>, + bank_forks: &RwLock, poh_recorder: &Arc>, poh_controller: &mut PohController, leader_schedule_cache: &Arc, @@ -3006,7 +3006,7 @@ impl ReplayStage { fn handle_votable_bank( bank: &Arc, switch_fork_decision: &SwitchForkDecision, - bank_forks: &Arc>, + bank_forks: &RwLock, tower: &mut Tower, progress: &mut ProgressMap, vote_account_pubkey: &Pubkey, @@ -5313,7 +5313,7 @@ impl ReplayStage { fn log_heaviest_fork_failures( heaviest_fork_failures: &Vec, - bank_forks: &Arc>, + bank_forks: &RwLock, tower: &Tower, progress: &ProgressMap, ancestors: &HashMap>, diff --git a/core/src/replay_stage/update_parent.rs b/core/src/replay_stage/update_parent.rs index 0cae13e1e6a..5580914e1c3 100644 --- a/core/src/replay_stage/update_parent.rs +++ b/core/src/replay_stage/update_parent.rs @@ -300,7 +300,7 @@ pub(super) fn handle_abandoned_bank( bank: &Arc, bank_slot: Slot, update_parent: &VersionedUpdateParent, - bank_forks: &Arc>, + bank_forks: &RwLock, progress: &mut ProgressMap, async_verification_freelist: &mut Vec, duplicate_slots_to_repair: &mut DuplicateSlotsToRepair, @@ -367,7 +367,7 @@ pub(super) fn handle_abandoned_bank( // parent by generate_new_bank_forks on the next iteration. ReplayStage::clear_banks( &BTreeSet::from([bank_slot]), - bank_forks.as_ref(), + bank_forks, progress, async_verification_freelist, ); diff --git a/core/src/sample_performance_service.rs b/core/src/sample_performance_service.rs index 27237f6e224..fee61278526 100644 --- a/core/src/sample_performance_service.rs +++ b/core/src/sample_performance_service.rs @@ -20,12 +20,10 @@ pub struct SamplePerformanceService { impl SamplePerformanceService { pub fn new( - bank_forks: &Arc>, + bank_forks: Arc>, blockstore: Arc, exit: Arc, ) -> Self { - let bank_forks = bank_forks.clone(); - let thread_hdl = Builder::new() .name("solSamplePerf".to_string()) .spawn(move || { diff --git a/core/src/tvu.rs b/core/src/tvu.rs index f1853c5e114..dd32fcd3bf0 100644 --- a/core/src/tvu.rs +++ b/core/src/tvu.rs @@ -200,7 +200,7 @@ impl Tvu { pub fn new( vote_account: &Pubkey, authorized_voter_keypairs: Arc>>>, - bank_forks: &Arc>, + bank_forks: Arc>, cluster_info: &Arc, sockets: TvuSockets, blockstore: Arc, @@ -621,7 +621,7 @@ impl Tvu { ); let epoch_specs: Box = - Box::new(EpochSpecs::from(bank_forks.clone())); + Box::new(EpochSpecs::from(bank_forks)); let duplicate_shred_listener = DuplicateShredListener::new( exit, @@ -830,7 +830,7 @@ pub mod tests { let tvu = Tvu::new( &vote_keypair.pubkey(), Arc::new(RwLock::new(vec![Arc::new(vote_keypair)])), - &bank_forks, + bank_forks.clone(), &cref1, TvuSockets { repair: target1.sockets.repair, diff --git a/core/src/validator.rs b/core/src/validator.rs index 24f28dfcb21..1b06947e411 100644 --- a/core/src/validator.rs +++ b/core/src/validator.rs @@ -1356,7 +1356,7 @@ impl Validator { let sample_performance_service = if config.rpc_config.enable_rpc_transaction_history { Some(SamplePerformanceService::new( - &bank_forks, + bank_forks.clone(), blockstore.clone(), exit.clone(), )) @@ -1579,7 +1579,7 @@ impl Validator { let tvu = Tvu::new( vote_account, authorized_voter_keypairs, - &bank_forks, + bank_forks.clone(), &cluster_info, TvuSockets { repair: node.sockets.repair.try_clone().unwrap(), @@ -2030,7 +2030,7 @@ pub fn should_require_vote_history_file( fn restore_vote_history( config: &ValidatorConfig, - bank_forks: &Arc>, + bank_forks: &RwLock, identity: &Pubkey, vote_account: &Pubkey, ) -> VoteHistory { diff --git a/rpc/src/optimistically_confirmed_bank_tracker.rs b/rpc/src/optimistically_confirmed_bank_tracker.rs index 77db08aeffa..e1115955e17 100644 --- a/rpc/src/optimistically_confirmed_bank_tracker.rs +++ b/rpc/src/optimistically_confirmed_bank_tracker.rs @@ -34,7 +34,7 @@ pub struct OptimisticallyConfirmedBank { } impl OptimisticallyConfirmedBank { - pub fn locked_from_bank_forks_root(bank_forks: &Arc>) -> Arc> { + pub fn locked_from_bank_forks_root(bank_forks: &RwLock) -> Arc> { Arc::new(RwLock::new(Self { bank: bank_forks.read().unwrap().root_bank(), })) diff --git a/rpc/src/rpc.rs b/rpc/src/rpc.rs index 08b0eb8a77e..a520366e99b 100644 --- a/rpc/src/rpc.rs +++ b/rpc/src/rpc.rs @@ -481,7 +481,7 @@ impl JsonRpcRequestProcessor { let client = create_client_for_tests(runtime.handle().clone(), my_tpu_address, None, 1); SendTransactionService::new( - &bank_forks, + bank_forks.clone(), transaction_receiver, client, SendTransactionServiceConfig { @@ -6958,7 +6958,7 @@ pub mod tests { let client = create_client_for_tests(runtime.handle().clone(), my_tpu_address, None, 1); SendTransactionService::new( - &bank_forks, + bank_forks.clone(), receiver, client, SendTransactionServiceConfig { @@ -7270,7 +7270,7 @@ pub mod tests { ); SendTransactionService::new( - &bank_forks, + bank_forks, receiver, client, SendTransactionServiceConfig { diff --git a/rpc/src/rpc_service.rs b/rpc/src/rpc_service.rs index 29b50357666..10f9c1c3fd8 100644 --- a/rpc/src/rpc_service.rs +++ b/rpc/src/rpc_service.rs @@ -394,7 +394,7 @@ impl RequestMiddleware for RpcRequestMiddleware { } if let Some(path) = match_supply_path(request.uri().path()) { - process_rest(&self.bank_forks, path) + process_rest(self.bank_forks.clone(), path) } else if self.is_file_get_path(request.uri().path()) { self.process_file_get(request.uri().path()) } else if request.uri().path() == "/health" { @@ -433,7 +433,7 @@ async fn calculate_circulating_supply_async(bank: &Arc) -> Result>, path: &str) -> Option { +async fn handle_rest(bank_forks: &RwLock, path: &str) -> Option { match path { "/v0/circulating-supply" => { let bank = bank_forks.read().unwrap().root_bank(); @@ -452,8 +452,7 @@ async fn handle_rest(bank_forks: &Arc>, path: &str) -> Option< } } -fn process_rest(bank_forks: &Arc>, path: &str) -> RequestMiddlewareAction { - let bank_forks = bank_forks.clone(); +fn process_rest(bank_forks: Arc>, path: &str) -> RequestMiddlewareAction { let path = path.to_string(); RequestMiddlewareAction::Respond { @@ -692,7 +691,7 @@ impl JsonRpcService { ); let _send_transaction_service = Arc::new(SendTransactionService::new( - &bank_forks, + bank_forks.clone(), receiver, client.clone(), send_transaction_service_config, @@ -723,7 +722,7 @@ impl JsonRpcService { let request_middleware = RpcRequestMiddleware::new( ledger_path, snapshot_config, - bank_forks.clone(), + bank_forks, health.clone(), ); let server = ServerBuilder::with_meta_extractor( diff --git a/rpc/src/rpc_subscriptions.rs b/rpc/src/rpc_subscriptions.rs index 37604500dcd..93919f8cd55 100644 --- a/rpc/src/rpc_subscriptions.rs +++ b/rpc/src/rpc_subscriptions.rs @@ -136,7 +136,7 @@ impl std::fmt::Debug for NotificationEntry { fn check_commitment_and_notify( params: &P, subscription: &SubscriptionInfo, - bank_forks: &Arc>, + bank_forks: &RwLock, slot: Slot, bank_method: B, filter_results: F, @@ -895,7 +895,7 @@ impl RpcSubscriptions { fn notify_watchers( max_complete_transaction_status_slot: Arc, subscriptions: &HashMap>, - bank_forks: &Arc>, + bank_forks: &RwLock, blockstore: &Blockstore, commitment_slots: &CommitmentSlots, notifier: &RpcNotifier, diff --git a/runtime/src/block_component_processor.rs b/runtime/src/block_component_processor.rs index e286755611f..1a14d98f598 100644 --- a/runtime/src/block_component_processor.rs +++ b/runtime/src/block_component_processor.rs @@ -559,7 +559,7 @@ mod tests { } fn create_child_bank( - bank_forks: &Arc>, + bank_forks: &RwLock, parent: &Arc, slot: u64, ) -> Arc { diff --git a/send-transaction-service/src/send_transaction_service.rs b/send-transaction-service/src/send_transaction_service.rs index 42bce3f0898..f5a969ab6f0 100644 --- a/send-transaction-service/src/send_transaction_service.rs +++ b/send-transaction-service/src/send_transaction_service.rs @@ -158,7 +158,7 @@ pub const MAX_RETRY_SLEEP_MS: u64 = 1000; impl SendTransactionService { pub fn new( - bank_forks: &Arc>, + bank_forks: Arc>, receiver: Receiver, client: Client, config: Config, @@ -178,7 +178,7 @@ impl SendTransactionService { ); let retry_thread = Self::retry_thread( - bank_forks.clone(), + bank_forks, client, retry_transactions, config, @@ -561,7 +561,7 @@ mod test { create_client_for_tests(Handle::current(), "127.0.0.1:0".parse().unwrap(), None, 1); let send_transaction_service = SendTransactionService::new( - &bank_forks, + bank_forks, receiver, client.clone(), Config { @@ -598,7 +598,7 @@ mod test { let client = create_client_for_tests(Handle::current(), "127.0.0.1:0".parse().unwrap(), None, 1); let _send_transaction_service = SendTransactionService::new( - &bank_forks, + bank_forks, receiver, client.clone(), Config { From 20a2a3ef333752a167b1d2836418648560dec0fe Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Tue, 2 Jun 2026 13:18:29 -0400 Subject: [PATCH 173/576] startup: bubble up error when vote_history_file is not present (#12905) --- core/src/validator.rs | 13 +++++++------ validator/src/admin_rpc_service.rs | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/core/src/validator.rs b/core/src/validator.rs index 1b06947e411..0a79970b8f4 100644 --- a/core/src/validator.rs +++ b/core/src/validator.rs @@ -2033,20 +2033,21 @@ fn restore_vote_history( bank_forks: &RwLock, identity: &Pubkey, vote_account: &Pubkey, -) -> VoteHistory { +) -> Result { match VoteHistory::restore(config.vote_history_storage.as_ref(), identity) { - Ok(vote_history) => vote_history, + Ok(vote_history) => Ok(vote_history), Err(err) => { let should_require_vote_history = { let bank_forks = bank_forks.read().unwrap(); should_require_vote_history_file(&bank_forks.working_bank(), vote_account, identity) }; if config.require_vote_history && should_require_vote_history { - panic!( + return Err(format!( "Unable to retrieve vote history for identity {identity}. The vote account \ {vote_account} has prior Alpenglow votes. If this is intentional, use \ --do-not-require-vote-history: {err:?}" - ); + ) + .to_string()); } if err.is_file_missing() && !should_require_vote_history { info!( @@ -2056,7 +2057,7 @@ fn restore_vote_history( } else { warn!("Unable to retrieve vote history: {err:?} creating default vote history..."); } - VoteHistory::new(*identity, 0) + Ok(VoteHistory::new(*identity, 0)) } } } @@ -2561,7 +2562,7 @@ impl<'a> ProcessBlockStore<'a> { // Load and post process vote history let vote_history = { let vote_history = - restore_vote_history(self.config, self.bank_forks, self.id, self.vote_account); + restore_vote_history(self.config, self.bank_forks, self.id, self.vote_account)?; // reconciliation attempt 1 of 2 with vote history reconcile_blockstore_roots_with_external_source( ExternalRootSource::VoteHistory(vote_history.root()), diff --git a/validator/src/admin_rpc_service.rs b/validator/src/admin_rpc_service.rs index f45c6e81c0f..13366d1598e 100644 --- a/validator/src/admin_rpc_service.rs +++ b/validator/src/admin_rpc_service.rs @@ -916,7 +916,7 @@ impl AdminRpcImpl { let should_require_vote_history = { let bank_forks = post_init.bank_forks.read().unwrap(); should_require_vote_history_file( - &bank_forks.root_bank(), + &bank_forks.working_bank(), &post_init.vote_account, &identity_keypair.pubkey(), ) From 3deb741d0a07c4e2a88a53e66eb02a5be7816d40 Mon Sep 17 00:00:00 2001 From: febo Date: Tue, 2 Jun 2026 21:00:40 +0100 Subject: [PATCH 174/576] Add memo program v4 (#12658) * Add memo v4 * Add memo binary * Update lock files * Fix tests --- Cargo.lock | 1 + cli-output/src/display.rs | 6 ++++-- cli/src/memo.rs | 2 +- dev-bins/Cargo.lock | 1 + faucet/src/faucet.rs | 4 ++-- fetch-spl.sh | 1 + program-binaries/src/lib.rs | 8 ++++++++ program-binaries/src/programs/spl_memo-4.0.0.so | Bin 0 -> 2304 bytes program-test/Cargo.toml | 1 + program-test/tests/spl.rs | 6 ++++++ programs/sbf/Cargo.lock | 1 + transaction-status/benches/extract_memos.rs | 7 ++++--- transaction-status/src/extract_memos.rs | 9 +++++++++ transaction-status/src/parse_instruction.rs | 16 ++++++++++++++++ 14 files changed, 55 insertions(+), 8 deletions(-) create mode 100644 program-binaries/src/programs/spl_memo-4.0.0.so diff --git a/Cargo.lock b/Cargo.lock index 383f98039bd..0a157a844d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9517,6 +9517,7 @@ dependencies = [ "solana-transaction-error", "solana-vote-program", "spl-generic-token", + "spl-memo-interface", "test-case", "thiserror 2.0.18", "tokio", diff --git a/cli-output/src/display.rs b/cli-output/src/display.rs index cd0ea7f096c..054c725f477 100644 --- a/cli-output/src/display.rs +++ b/cli-output/src/display.rs @@ -18,7 +18,9 @@ use { Rewards, UiReturnDataEncoding, UiTransactionReturnData, UiTransactionStatusMeta, }, solana_transaction_status_client_types::UiTransactionError, - spl_memo_interface::{v1::id as spl_memo_v1_id, v3::id as spl_memo_v3_id}, + spl_memo_interface::{ + v1::id as spl_memo_v1_id, v3::id as spl_memo_v3_id, v4::id as spl_memo_v4_id, + }, std::{collections::HashMap, fmt, io, time::Duration}, }; @@ -40,7 +42,7 @@ impl Default for BuildBalanceMessageConfig { } fn is_memo_program(k: &Pubkey) -> bool { - *k == spl_memo_v1_id() || *k == spl_memo_v3_id() + *k == spl_memo_v1_id() || *k == spl_memo_v3_id() || *k == spl_memo_v4_id() } pub fn build_balance_message_with_config( diff --git a/cli/src/memo.rs b/cli/src/memo.rs index a6455e4802c..d242e2d39aa 100644 --- a/cli/src/memo.rs +++ b/cli/src/memo.rs @@ -1,4 +1,4 @@ -use {solana_instruction::Instruction, solana_pubkey::Pubkey, spl_memo_interface::v3::id}; +use {solana_instruction::Instruction, solana_pubkey::Pubkey, spl_memo_interface::v4::id}; pub trait WithMemo { fn with_memo>(self, memo: Option) -> Self; diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 6c9e996849f..f841720a9a6 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -8015,6 +8015,7 @@ dependencies = [ "solana-transaction-error", "solana-vote-program", "spl-generic-token", + "spl-memo-interface", "thiserror 2.0.18", "tokio", ] diff --git a/faucet/src/faucet.rs b/faucet/src/faucet.rs index 5a03a58e2c4..e446a0bebb4 100644 --- a/faucet/src/faucet.rs +++ b/faucet/src/faucet.rs @@ -202,7 +202,7 @@ impl Faucet { ) ); let memo_instruction = Instruction { - program_id: spl_memo_interface::v3::id(), + program_id: spl_memo_interface::v4::id(), accounts: vec![], data: memo.as_bytes().to_vec(), }; @@ -675,7 +675,7 @@ mod tests { assert_eq!(tx.signatures.len(), 1); assert_eq!( message.account_keys, - vec![mint_pubkey, spl_memo_interface::v3::id()] + vec![mint_pubkey, spl_memo_interface::v4::id()] ); assert_eq!(message.recent_blockhash, blockhash); diff --git a/fetch-spl.sh b/fetch-spl.sh index da2bf2c2f2d..d87508a0ea8 100755 --- a/fetch-spl.sh +++ b/fetch-spl.sh @@ -43,6 +43,7 @@ add_spl_program_to_fetch token 1.0.0-rc.1 TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623 add_spl_program_to_fetch token-2022 10.0.0 TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb BPFLoaderUpgradeab1e11111111111111111111111 add_spl_program_to_fetch memo 1.0.0 Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo BPFLoader1111111111111111111111111111111111 add_spl_program_to_fetch memo 3.0.0 MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr BPFLoader2111111111111111111111111111111111 +add_spl_program_to_fetch memo 4.0.0 Memo4c2pN8afCj432Lb7RMVKi9PbQnnW7ewFFaV3oAH BPFLoaderUpgradeab1e11111111111111111111111 add_spl_program_to_fetch associated-token-account 1.1.2 ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL BPFLoader2111111111111111111111111111111111 add_spl_program_to_fetch feature-proposal 1.0.0 Feat1YXHhH6t1juaWF74WLcfv4XoNocjXA6sPWHNgAse BPFLoader2111111111111111111111111111111111 diff --git a/program-binaries/src/lib.rs b/program-binaries/src/lib.rs index f2693b550a9..d427caf242e 100644 --- a/program-binaries/src/lib.rs +++ b/program-binaries/src/lib.rs @@ -15,6 +15,9 @@ mod spl_memo_1_0 { mod spl_memo_3_0 { solana_pubkey::declare_id!("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"); } +mod spl_memo_4_0 { + solana_pubkey::declare_id!("Memo4c2pN8afCj432Lb7RMVKi9PbQnnW7ewFFaV3oAH"); +} static SPL_PROGRAMS: &[(Pubkey, Pubkey, &[u8])] = &[ ( @@ -37,6 +40,11 @@ static SPL_PROGRAMS: &[(Pubkey, Pubkey, &[u8])] = &[ solana_sdk_ids::bpf_loader::ID, include_bytes!("programs/spl_memo-3.0.0.so"), ), + ( + spl_memo_4_0::ID, + solana_sdk_ids::bpf_loader_upgradeable::ID, + include_bytes!("programs/spl_memo-4.0.0.so"), + ), ( spl_generic_token::associated_token_account::ID, solana_sdk_ids::bpf_loader::ID, diff --git a/program-binaries/src/programs/spl_memo-4.0.0.so b/program-binaries/src/programs/spl_memo-4.0.0.so new file mode 100644 index 0000000000000000000000000000000000000000..04ba8f2e56e184710caccd3772c6c66cd4e72ca7 GIT binary patch literal 2304 zcmbtV%}*0i5Fc9VcI!tKtq=~(t`!^*)JW4IeN zULlfrbVQljIT968xBcH-3(bogp0+9(KL_?BewoOo^BbyY05%l&Kq&8n>a@ z#aQU#e!tTX^D1XQIBKaN{8a2$6!9`B+;)PnB=YpAAmS`>h|`R_ z9LG{Y3X_RR0W3a?!~9P9-y>)yxVIq04}QRL@q0SkP8K!W}0Z%W{ zFzU{A1ci0Q(~55eJ)XO7te1W%`~~e>I3oNgmkH;Gub%o6O)~E1ccjO6elv=l z`3|6-eQeBB=e*7`2I~?p^5hU{<(%*vu{eEV9`Pa%d^bSz-`P)`+n(1vD=}GLP2LhQ zy;8LA$@arp*|7aP3#DptK3fPbiTsq>>2*ZMl%Li4*0g9=<xF(tk(*iM2vW25 z{e+%?>^stHh{jbBvj#%XKzbbr^AB(M@8e2mHIOa2Ug!|Zd9?OWW*_;PedJWEHtWG+ zZK2wvMy)(ku9armi^Ye92gSg#m11RXF_@uAjU%a^?<$Xcx#jdu3vT>&7r)z~k5qO# z_T4hqEG{=GHy>0R%{t}k#WL0+2r3Tqm4!LVHSV!OGhbkx+I+s5*MR;L5FQ@ux7FRS z{ohkRRtIx?UhPo}ko`*xZ7yV2eAyb9}`(mmw<1xw|Q = (0..64).map(|_| Pubkey::new_unique()).collect(); - account_keys[62] = spl_memo_interface::v1::id(); - account_keys[63] = spl_memo_interface::v3::id(); + account_keys[61] = spl_memo_interface::v1::id(); + account_keys[62] = spl_memo_interface::v3::id(); + account_keys[63] = spl_memo_interface::v4::id(); let memo = "Test memo"; let instructions: Vec<_> = (0..20) .map(|i| CompiledInstruction { - program_id_index: 62 + (i % 2), + program_id_index: 61 + (i % 3), accounts: vec![], data: memo.as_bytes().to_vec(), }) diff --git a/transaction-status/src/extract_memos.rs b/transaction-status/src/extract_memos.rs index 81480b01c85..a77fd5ff729 100644 --- a/transaction-status/src/extract_memos.rs +++ b/transaction-status/src/extract_memos.rs @@ -71,6 +71,7 @@ fn extract_memos_inner( KeyType::Unknown(program_id) => { if **program_id == spl_memo_interface::v1::id() || **program_id == spl_memo_interface::v3::id() + || **program_id == spl_memo_interface::v4::id() { account_keys[index] = KeyType::MemoProgram; Some(&ix.data) @@ -95,9 +96,11 @@ mod test { let another_program_id = Pubkey::new_unique(); let memo0 = "Test memo"; let memo1 = "🦖"; + let memo2 = "Yet another memo"; let expected_memos = vec![ format!("[{}] {}", memo0.len(), memo0), format!("[{}] {}", memo1.len(), memo1), + format!("[{}] {}", memo2.len(), memo2), ]; let memo_instructions = vec![ CompiledInstruction { @@ -115,12 +118,18 @@ mod test { accounts: vec![], data: memo1.as_bytes().to_vec(), }, + CompiledInstruction { + program_id_index: 4, + accounts: vec![], + data: memo2.as_bytes().to_vec(), + }, ]; let static_keys = vec![ fee_payer, spl_memo_interface::v1::id(), another_program_id, spl_memo_interface::v3::id(), + spl_memo_interface::v4::id(), ]; let account_keys = AccountKeys::new(&static_keys, None); diff --git a/transaction-status/src/parse_instruction.rs b/transaction-status/src/parse_instruction.rs index db9aa47a353..ed3bc3996df 100644 --- a/transaction-status/src/parse_instruction.rs +++ b/transaction-status/src/parse_instruction.rs @@ -36,6 +36,7 @@ static PARSABLE_PROGRAM_IDS: std::sync::LazyLock Date: Tue, 2 Jun 2026 15:02:36 -0700 Subject: [PATCH 175/576] Create a set of slots with zero lamport accounts to shrink after full snapshot (#12665) * Revisit all stores since last snapshot and promote them to shrink * Updates in response to feedback: - Naming of variables - Corrupted test comment * Switching to use explicit initializer to avoid race condition * Resolve clippy issue * Update bracing on comment --- accounts-db/src/accounts_db.rs | 79 +++++++++++++++++++++++++++- accounts-db/src/accounts_db/tests.rs | 72 +++++++++++++++++++++++++ ledger/src/bank_forks_utils.rs | 7 +++ 3 files changed, 156 insertions(+), 2 deletions(-) diff --git a/accounts-db/src/accounts_db.rs b/accounts-db/src/accounts_db.rs index 0d9a4e892a5..546c5a5b759 100644 --- a/accounts-db/src/accounts_db.rs +++ b/accounts-db/src/accounts_db.rs @@ -821,6 +821,8 @@ struct CleanKeyTimings { delta_key_count: u64, dirty_pubkeys_count: u64, oldest_dirty_slot: Slot, + zero_lamport_single_ref_slots_added_to_shrink_count: u64, + zero_lamport_sweep_us: u64, } pub fn get_temp_accounts_paths(count: u32) -> io::Result<(Vec, Vec)> { @@ -989,6 +991,10 @@ pub struct AccountsDb { /// Note, this is None if we're told to *not* take snapshots latest_full_snapshot_slot: SeqLock>, + /// The full snapshot slot we last swept for zero-lamport-single-ref shrink + /// eligibility. + last_swept_full_snapshot_slot: AtomicU64, + /// These are the ancient storages that could be valuable to /// shrink, sorted by amount of dead bytes. The elements /// are sorted from the largest dead bytes to the smallest. @@ -1154,6 +1160,7 @@ impl AccountsDb { latest_full_snapshot_slot_advanced_since_clean: AtomicBool::default(), accounts_file_provider: AccountsFileProvider::default(), latest_full_snapshot_slot: SeqLock::new(None), + last_swept_full_snapshot_slot: AtomicU64::new(0), best_ancient_slots_to_shrink: RwLock::default(), }; @@ -1607,8 +1614,9 @@ impl AccountsDb { be empty" ); - // Cleaning up zero lamport accounts is gated by a full snapshot because they need to be retained - // for incremental snapshots. Once a snapshot occurs, drain the list + // Cleaning up zero lamport accounts is gated by a full snapshot because they need to be + // retained for incremental snapshots. Once a full snapshot occurs, drain the list and + // search for newly shrinkable storages. if self .latest_full_snapshot_slot_advanced_since_clean .swap(false, Ordering::Acquire) @@ -1623,12 +1631,58 @@ impl AccountsDb { } !is_candidate_for_clean }); + + let last_swept_full_snapshot_slot = + self.last_swept_full_snapshot_slot.load(Ordering::Relaxed); + let (added_to_shrink_count, sweep_us) = + measure_us!(self.check_shrink_eligibility_after_snapshot( + last_swept_full_snapshot_slot, + latest_full_snapshot_slot + )); + timings.zero_lamport_single_ref_slots_added_to_shrink_count += + added_to_shrink_count; + timings.zero_lamport_sweep_us += sweep_us; } } (candidates, min_dirty_slot) } + /// Loop through slots in `[last_swept_full_snapshot_slot + 1, latest_full_snapshot_slot]` and + /// check each storage to determine if it is eligible for shrink, since zero-lamport single-ref + /// accounts become shrinkable after a full snapshot advances past their slot. Advances the + /// `last_swept_full_snapshot_slot` to `latest_full_snapshot_slot` on completion. + /// + /// Returns the count of storages that were added to the shrink candidates set + fn check_shrink_eligibility_after_snapshot( + &self, + last_swept_full_snapshot_slot: Slot, + latest_full_snapshot_slot: Slot, + ) -> u64 { + let start = last_swept_full_snapshot_slot.saturating_add(1); + + let mut added_to_shrink_count = 0; + // Held for the full scan. Safe because the only paths that take this lock in production + // validator code run in earlier/later phases of the same AccountsBackgroundService + // iteration, never concurrently with clean_accounts. + let mut shrink_candidates = self.shrink_candidate_slots.lock().unwrap(); + for slot in start..=latest_full_snapshot_slot { + if let Some(store) = self.storage.get_slot_storage_entry(slot) + && self.is_shrinking_productive(&store) + && self.is_candidate_for_shrink(&store) + { + if shrink_candidates.insert(slot) { + added_to_shrink_count += 1; + } + } + } + drop(shrink_candidates); + + self.last_swept_full_snapshot_slot + .store(latest_full_snapshot_slot, Ordering::Relaxed); + added_to_shrink_count + } + /// called with cli argument to verify refcounts are correct on all accounts /// this is very slow /// this function will call Rayon par_iter, so you will want to have thread pool installed if @@ -2072,6 +2126,12 @@ impl AccountsDb { ("delta_insert_us", key_timings.delta_insert_us, i64), ("delta_key_count", key_timings.delta_key_count, i64), ("dirty_pubkeys_count", key_timings.dirty_pubkeys_count, i64), + ( + "zero_lamport_single_ref_slots_added_to_shrink_count", + key_timings.zero_lamport_single_ref_slots_added_to_shrink_count, + i64 + ), + ("zero_lamport_sweep_us", key_timings.zero_lamport_sweep_us, i64), ("useful_keys", useful_accum.load(Ordering::Relaxed), i64), ("total_keys_count", num_candidates, i64), ("retained_keys_count", retained_keys_count, i64), @@ -6073,6 +6133,21 @@ impl AccountsDb { .store(true, Ordering::Release); } + /// Marks slots <= slot as already swept for zero-lamport-single-ref shrink eligibility + pub fn set_last_swept_full_snapshot_slot(&self, slot: Slot) { + // Prior to setting this, the latest full snapshot slot must be set, and + // last_swept_full_snapshot_slot value must be less than or equal to it. + assert!( + self.latest_full_snapshot_slot() + .is_some_and(|snapshot_slot| slot <= snapshot_slot), + "last swept full snapshot slot {slot} cannot be greater than latest full snapshot \ + slot {:?}", + self.latest_full_snapshot_slot() + ); + self.last_swept_full_snapshot_slot + .store(slot, Ordering::Relaxed); + } + fn generate_index_for_slot<'a>( &self, reader: &mut impl RequiredLenBufFileRead<'a>, diff --git a/accounts-db/src/accounts_db/tests.rs b/accounts-db/src/accounts_db/tests.rs index 62ed5df3cca..edbb836fd7b 100644 --- a/accounts-db/src/accounts_db/tests.rs +++ b/accounts-db/src/accounts_db/tests.rs @@ -3601,6 +3601,78 @@ define_accounts_db_test!( } ); +/// When the full snapshot advances past slots that still hold zero-lamport single-ref +/// accounts, the next clean's range sweep must re-queue those slots for shrink so the +/// zero lamport single ref accounts can be removed by shrink. +#[test_case(false; "without_last_swept_set_queues_both_slots")] +#[test_case(true; "with_last_swept_set_skips_only_at_last_swept")] +fn test_zero_lamport_single_ref_resweep_respects_last_swept(set_last_swept: bool) { + let db = AccountsDb::new_single_for_tests(); + + let one_lamport_account = AccountSharedData::new(1, 0, AccountSharedData::default().owner()); + let zero_lamport_account = AccountSharedData::new(0, 0, AccountSharedData::default().owner()); + let slot_at_last_swept = 2; + let slot_above_last_swept = 3; + let full_snapshot_slot = 4; + + // Seed the snapshot at slot 0 to avoid cleaning zero lamport single ref accounts + db.set_latest_full_snapshot_slot(0); + + // slot 1: older non-zero versions of both keys. Both entries are overwritten by the + // stores below, so slot1 is reclaimed + let key_zero_at_last_swept = Pubkey::new_unique(); + let key_zero_above_last_swept = Pubkey::new_unique(); + db.store_for_tests(( + 1, + &[ + (&key_zero_at_last_swept, &one_lamport_account), + (&key_zero_above_last_swept, &one_lamport_account), + ][..], + )); + db.add_root_and_flush_write_cache(1); + + // slot 2: zero-lamport account plus an unrelated live account so the storage still + // has alive bytes now that key_zero is a single-ref zero-lamport. + db.store_for_tests(( + slot_at_last_swept, + &[ + (&key_zero_at_last_swept, &zero_lamport_account), + (&Pubkey::new_unique(), &one_lamport_account), + ][..], + )); + db.add_root_and_flush_write_cache(slot_at_last_swept); + + // slot 3: same pattern, but for a key whose slot sits *above* the seeded + // last-swept slot, so it must be queued in both variants — proving the sweep + // walks past the last-swept slot. + db.store_for_tests(( + slot_above_last_swept, + &[ + (&key_zero_above_last_swept, &zero_lamport_account), + (&Pubkey::new_unique(), &one_lamport_account), + ][..], + )); + db.add_root_and_flush_write_cache(slot_above_last_swept); + + // Optionally mark slot 2 as already swept. `set_last_swept_full_snapshot_slot` + // requires `last_swept <= latest`, so advance latest to slot 2 first (it gets + // advanced again to `full_snapshot_slot` below). + if set_last_swept { + db.set_latest_full_snapshot_slot(slot_at_last_swept); + db.set_last_swept_full_snapshot_slot(slot_at_last_swept); + } + + // Advance the snapshot past both ZLSR slots and clean + // The sweep range is (0, 4] when not set, queueing both slot 2 and slot 3. + // The sweep range is (2, 4] when set, queueing only slot 3. + db.set_latest_full_snapshot_slot(full_snapshot_slot); + db.clean_accounts(Some(full_snapshot_slot), false); + + let queued = db.shrink_candidate_slots.lock().unwrap(); + assert_eq!(queued.contains(&slot_at_last_swept), !set_last_swept); + assert!(queued.contains(&slot_above_last_swept)); +} + fn setup_accounts_db_cache_clean( num_slots: usize, scan_slot: Option, diff --git a/ledger/src/bank_forks_utils.rs b/ledger/src/bank_forks_utils.rs index 37c67c22510..a5162c4015d 100644 --- a/ledger/src/bank_forks_utils.rs +++ b/ledger/src/bank_forks_utils.rs @@ -267,6 +267,13 @@ pub fn try_load_bank_forks_from_snapshot( .accounts .accounts_db .set_latest_full_snapshot_slot(full_snapshot_archive_info.slot()); + // Set the last swept slot so the first full snapshot only triggers + // cleaning of zero lamport single ref accounts between the previous + // full snapshot and the new full snapshot + bank.rc + .accounts + .accounts_db + .set_last_swept_full_snapshot_slot(full_snapshot_archive_info.slot()); } else { assert!( bank.rc From 3826da5781de71c8a6908e2382d8b22c4b3ad9a8 Mon Sep 17 00:00:00 2001 From: Joe C Date: Wed, 3 Jun 2026 07:03:09 +0800 Subject: [PATCH 176/576] svm: conformance: add vm serialization harness (#12891) * svm: conformance: extract callbacks into shared module * svm: conformance: extract fd_hash into shared module * svm: conformance: extract invocation setup helpers * svm: conformance: add serialization harness --- svm/src/conformance/callback.rs | 39 ++++ svm/src/conformance/elf_loader.rs | 34 +--- svm/src/conformance/fd_hash.rs | 35 ++++ svm/src/conformance/instr/harness.rs | 122 +++--------- svm/src/conformance/mod.rs | 6 + svm/src/conformance/serialization.rs | 272 +++++++++++++++++++++++++++ svm/src/conformance/setup.rs | 94 +++++++++ 7 files changed, 481 insertions(+), 121 deletions(-) create mode 100644 svm/src/conformance/callback.rs create mode 100644 svm/src/conformance/fd_hash.rs create mode 100644 svm/src/conformance/serialization.rs create mode 100644 svm/src/conformance/setup.rs diff --git a/svm/src/conformance/callback.rs b/svm/src/conformance/callback.rs new file mode 100644 index 00000000000..af32c24f355 --- /dev/null +++ b/svm/src/conformance/callback.rs @@ -0,0 +1,39 @@ +//! Invoke-context callbacks shared by the conformance harnesses. + +use solana_svm_callback::InvokeContextCallback; +#[cfg(feature = "conformance")] +use { + agave_feature_set::FeatureSet, + agave_precompiles::{get_precompile, is_precompile}, + solana_precompile_error::PrecompileError, + solana_pubkey::Pubkey, +}; + +/// Default callback. No precompile support. +pub struct DefaultCallback; + +impl InvokeContextCallback for DefaultCallback {} + +/// Conformance callback. Full precompile support across all features. +#[cfg(feature = "conformance")] +pub struct ConformanceCallback; + +#[cfg(feature = "conformance")] +impl InvokeContextCallback for ConformanceCallback { + fn is_precompile(&self, program_id: &Pubkey) -> bool { + is_precompile(program_id, |_| true) + } + + fn process_precompile( + &self, + program_id: &Pubkey, + data: &[u8], + instruction_datas: Vec<&[u8]>, + ) -> Result<(), PrecompileError> { + if let Some(precompile) = get_precompile(program_id, |_| true) { + precompile.verify(data, &instruction_datas, &FeatureSet::all_enabled()) + } else { + Err(PrecompileError::InvalidPublicKey) + } + } +} diff --git a/svm/src/conformance/elf_loader.rs b/svm/src/conformance/elf_loader.rs index 9302db6c832..501c73512c1 100644 --- a/svm/src/conformance/elf_loader.rs +++ b/svm/src/conformance/elf_loader.rs @@ -1,7 +1,10 @@ //! ELF loader conformance harness. use { - crate::conformance::feature_set::feature_set_from_proto, + crate::conformance::{ + fd_hash::{fd_hash_u64_without_seed, fd_hash_without_seed}, + feature_set::feature_set_from_proto, + }, prost::Message, protosol::protos::{ ElfLoaderCtx as ProtoElfLoaderCtx, ElfLoaderEffects as ProtoElfLoaderEffects, @@ -13,7 +16,6 @@ use { }, solana_syscalls::create_program_runtime_environment, std::{collections::BTreeSet, ffi::c_int}, - xxhash_rust::xxh64::xxh64, }; pub fn execute_elf_loader(input: &ProtoElfLoaderCtx) -> ProtoElfLoaderEffects { @@ -92,23 +94,6 @@ fn elf_err_to_num(error: &ElfError) -> u8 { } } -fn fd_hash_without_seed(buf: &[u8]) -> u64 { - fd_hash(0, buf) -} - -fn fd_hash_u64_without_seed(buf: &[u64]) -> u64 { - let bytes = unsafe { - std::slice::from_raw_parts(buf.as_ptr().cast::(), std::mem::size_of_val(buf)) - }; - fd_hash(0, bytes) -} - -/// Hashing used to summarize binary blobs (e.g. ELF sections) for conformance -/// comparisons. Firedancer's `fd_hash` is XXH64. -fn fd_hash(seed: u64, buf: &[u8]) -> u64 { - xxh64(buf, seed) -} - /// # Safety /// /// `in_ptr` must point to `in_sz` initialized bytes. `out_ptr` must point @@ -188,15 +173,4 @@ mod tests { }; assert_loads_ok(SBPFV3_RETURN_OK, false, Some(features)); } - - #[test] - fn test_fd_hash_matches_xxh64() { - // Reference values from Firedancer's `fd_hash` (XXH64). The empty-input, - // seed-0 case is the canonical XXH64 test vector. - let long: Vec = (0u8..100).collect(); - assert_eq!(fd_hash(0, &[]), 0xef46db3751d8e999); - assert_eq!(fd_hash(0, b"hello world"), 0x45ab6734b21e6968); - assert_eq!(fd_hash(42, b"hello world"), 0x69c2b68f9d9352a1); - assert_eq!(fd_hash(0, &long), 0x6ac1e58032166597); - } } diff --git a/svm/src/conformance/fd_hash.rs b/svm/src/conformance/fd_hash.rs new file mode 100644 index 00000000000..a624df1a7c9 --- /dev/null +++ b/svm/src/conformance/fd_hash.rs @@ -0,0 +1,35 @@ +//! Hashing used to summarize binary blobs (e.g. ELF sections, serialized VM +//! memory) for conformance comparisons. + +use xxhash_rust::xxh64::xxh64; + +pub fn fd_hash_without_seed(buf: &[u8]) -> u64 { + fd_hash(0, buf) +} + +pub fn fd_hash_u64_without_seed(buf: &[u64]) -> u64 { + let bytes = unsafe { + std::slice::from_raw_parts(buf.as_ptr().cast::(), std::mem::size_of_val(buf)) + }; + fd_hash(0, bytes) +} + +pub fn fd_hash(seed: u64, buf: &[u8]) -> u64 { + xxh64(buf, seed) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_fd_hash_matches_xxh64() { + // Reference values from Firedancer's `fd_hash` (XXH64). The empty-input, + // seed-0 case is the canonical XXH64 test vector. + let long: Vec = (0u8..100).collect(); + assert_eq!(fd_hash(0, &[]), 0xef46db3751d8e999); + assert_eq!(fd_hash(0, b"hello world"), 0x45ab6734b21e6968); + assert_eq!(fd_hash(42, b"hello world"), 0x69c2b68f9d9352a1); + assert_eq!(fd_hash(0, &long), 0x6ac1e58032166597); + } +} diff --git a/svm/src/conformance/instr/harness.rs b/svm/src/conformance/instr/harness.rs index 9512d633e12..a929756b62f 100644 --- a/svm/src/conformance/instr/harness.rs +++ b/svm/src/conformance/instr/harness.rs @@ -2,69 +2,39 @@ use { super::{context::InstrContext, effects::InstrEffects}, - crate::message_processor::process_message, + crate::{ + conformance::{ + callback::DefaultCallback, + setup::{compile_transaction_context, program_runtime_environments, recent_blockhash}, + }, + message_processor::process_message, + }, solana_compute_budget::compute_budget::ComputeBudget, solana_instruction::error::InstructionError, solana_program_runtime::{ - invoke_context::{EnvironmentConfig, InvokeContext, mock_compile_message}, - loaded_programs::{ - ProgramCacheForTxBatch, ProgramRuntimeEnvironment, ProgramRuntimeEnvironments, - }, + invoke_context::{EnvironmentConfig, InvokeContext}, + loaded_programs::ProgramCacheForTxBatch, sysvar_cache::SysvarCache, }, solana_pubkey::Pubkey, solana_svm_callback::InvokeContextCallback, solana_svm_log_collector::LogCollector, solana_svm_timings::ExecuteTimings, - solana_svm_transaction::svm_message::SVMStaticMessage, - solana_syscalls::create_program_runtime_environment, - solana_transaction_context::transaction::TransactionContext, solana_transaction_error::TransactionError, std::rc::Rc, }; #[cfg(feature = "conformance")] use { - crate::conformance::programs::{ - fill_program_cache_from_accounts, new_program_cache_with_builtins, + crate::conformance::{ + callback::ConformanceCallback, + programs::{fill_program_cache_from_accounts, new_program_cache_with_builtins}, + setup::sysvar_cache_from_accounts, }, - agave_feature_set::FeatureSet, - agave_precompiles::{get_precompile, is_precompile}, prost::Message, protosol::protos::{InstrContext as ProtoInstrContext, InstrEffects as ProtoInstrEffects}, - solana_account::ReadableAccount, - solana_precompile_error::PrecompileError, std::ffi::c_int, }; -/// Default callback. No precompile support. -struct DefaultCallback; - -impl InvokeContextCallback for DefaultCallback {} - -/// Conformance callback. Full precompile support across all features. -#[cfg(feature = "conformance")] -struct ConformanceCallback; - -#[cfg(feature = "conformance")] -impl InvokeContextCallback for ConformanceCallback { - fn is_precompile(&self, program_id: &Pubkey) -> bool { - is_precompile(program_id, |_| true) - } - - fn process_precompile( - &self, - program_id: &Pubkey, - data: &[u8], - instruction_datas: Vec<&[u8]>, - ) -> Result<(), PrecompileError> { - if let Some(precompile) = get_precompile(program_id, |_| true) { - precompile.verify(data, &instruction_datas, &FeatureSet::all_enabled()) - } else { - Err(PrecompileError::InvalidPublicKey) - } - } -} - /// Execute a single instruction against the Solana VM with the default /// (no-precompile) callback. pub fn execute_instr( @@ -99,45 +69,27 @@ pub fn execute_instr_with_callback( .expect("program not loaded in cache") .account_owner(); - let (sanitized_message, transaction_accounts) = - mock_compile_message(&input.instruction, &input.accounts, program_id, &loader_key); - - let mut transaction_context = TransactionContext::new( - transaction_accounts, + let (sanitized_message, mut transaction_context) = compile_transaction_context( + &input.instruction, + &input.accounts, + program_id, + &loader_key, + &compute_budget, (*rent).clone(), - compute_budget.max_instruction_stack_depth, - compute_budget.max_instruction_trace_length, - sanitized_message.num_instructions(), ); - let program_runtime_environment = create_program_runtime_environment( - &input.feature_set, - &compute_budget.to_budget(), - false, /* deployment */ - false, /* debugging_features */ - ) - .unwrap(); + let runtime_environments = program_runtime_environments(&input.feature_set, &compute_budget); let result = { - #[expect(deprecated)] - let (blockhash, blockhash_lamports_per_signature) = sysvar_cache - .get_recent_blockhashes() - .ok() - .and_then(|x| (*x).last().cloned()) - .map(|x| (x.blockhash, x.fee_calculator.lamports_per_signature)) - .unwrap_or_default(); - - let program_runtime_environments = ProgramRuntimeEnvironments::new( - ProgramRuntimeEnvironment::clone(&program_runtime_environment), - program_runtime_environment, - ); + let (blockhash, blockhash_lamports_per_signature) = recent_blockhash(sysvar_cache); + let environment_config = EnvironmentConfig::new( blockhash, blockhash_lamports_per_signature, false, callback, &feature_set, - &program_runtime_environments, + &runtime_environments, sysvar_cache, ); @@ -207,17 +159,7 @@ pub fn execute_instr_proto(input: ProtoInstrContext) -> ProtoInstrEffects { let instr_context = InstrContext::from(input); // When testing with protobuf, we fill the sysvar cache from input accounts. - let sysvar_cache = { - let mut cache = SysvarCache::default(); - cache.fill_missing_entries(|pubkey, callbackback| { - if let Some(account) = instr_context.accounts.iter().find(|(key, _)| key == pubkey) - && account.1.lamports() > 0 - { - callbackback(account.1.data()); - } - }); - cache - }; + let sysvar_cache = sysvar_cache_from_accounts(&instr_context.accounts); // When testing with protobuf, we fill the program cache from input accounts. let mut program_cache = { @@ -225,19 +167,17 @@ pub fn execute_instr_proto(input: ProtoInstrContext) -> ProtoInstrEffects { let feature_set = &instr_context.feature_set; let simd_0268_active = feature_set.raise_cpi_nesting_limit_to_8; let compute_budget = ComputeBudget::new_with_defaults(simd_0268_active); + let environments = program_runtime_environments(feature_set, &compute_budget); - let environment = create_program_runtime_environment( - feature_set, - &compute_budget.to_budget(), - false, /* deployment */ - false, /* debugging_features */ + let mut cache = new_program_cache_with_builtins(slot); + fill_program_cache_from_accounts( + &mut cache, + environments.get_env_for_deployment(), + &instr_context.accounts, + slot, ) .unwrap(); - let mut cache = new_program_cache_with_builtins(slot); - fill_program_cache_from_accounts(&mut cache, &environment, &instr_context.accounts, slot) - .unwrap(); - cache }; diff --git a/svm/src/conformance/mod.rs b/svm/src/conformance/mod.rs index ff12bbcbcc4..74022a04263 100644 --- a/svm/src/conformance/mod.rs +++ b/svm/src/conformance/mod.rs @@ -2,9 +2,15 @@ #[cfg(feature = "conformance")] pub mod account_state; +pub mod callback; #[cfg(feature = "conformance")] pub mod elf_loader; #[cfg(feature = "conformance")] +pub mod fd_hash; +#[cfg(feature = "conformance")] pub mod feature_set; pub mod instr; pub mod programs; +#[cfg(feature = "conformance")] +pub mod serialization; +mod setup; diff --git a/svm/src/conformance/serialization.rs b/svm/src/conformance/serialization.rs new file mode 100644 index 00000000000..f24f9292102 --- /dev/null +++ b/svm/src/conformance/serialization.rs @@ -0,0 +1,272 @@ +//! VM serialization conformance harness. + +use { + crate::conformance::{ + callback::DefaultCallback, + fd_hash::fd_hash, + instr::context::InstrContext, + setup::{ + compile_transaction_context, program_runtime_environments, recent_blockhash, + sysvar_cache_from_accounts, + }, + }, + prost::Message, + protosol::protos::{ + InstrContext as ProtoInstrContext, VmInputMemoryRegion as ProtoVmInputMemoryRegion, + VmSerializationEffects as ProtoVmSerializationEffects, + VmSerializedAccountMetadata as ProtoVmSerializedAccountMetadata, + }, + solana_compute_budget::compute_budget::ComputeBudget, + solana_program_runtime::{ + invoke_context::{EnvironmentConfig, InvokeContext}, + loaded_programs::ProgramCacheForTxBatch, + memory_context::SerializedAccountMetadata, + serialization::serialize_parameters, + solana_sbpf::memory_region::MemoryRegion, + }, + solana_svm_log_collector::LogCollector, + std::ffi::c_int, +}; + +pub fn execute_vm_serialize(input: ProtoInstrContext) -> ProtoVmSerializationEffects { + let instr_context = InstrContext::from(input); + + let log_collector = LogCollector::new_ref(); + let feature_set = instr_context.feature_set; + let virtual_address_space_adjustments = feature_set.virtual_address_space_adjustments; + let direct_mapping = feature_set.account_data_direct_mapping; + let direct_account_pointers = feature_set.direct_account_pointers_in_program_input; + + let compute_budget = ComputeBudget::new_with_defaults(feature_set.raise_cpi_nesting_limit_to_8); + // No CU limit for this harness. + + let sysvar_cache = sysvar_cache_from_accounts(&instr_context.accounts); + let rent = sysvar_cache.get_rent().unwrap(); + let program_id = instr_context.instruction.program_id; + let loader_key = instr_context + .accounts + .iter() + .find(|(key, _)| *key == program_id) + .map(|(_, account)| account.owner) + .expect("program not found in accounts"); + + let (sanitized_message, mut transaction_context) = compile_transaction_context( + &instr_context.instruction, + &instr_context.accounts, + &program_id, + &loader_key, + &compute_budget, + (*rent).clone(), + ); + + // We're only testing the parameter serialization, so use an empty cache. + let mut program_cache = ProgramCacheForTxBatch::default(); + + let runtime_environments = program_runtime_environments(&feature_set, &compute_budget); + let (blockhash, lamports_per_signature) = recent_blockhash(&sysvar_cache); + let environment_config = EnvironmentConfig::new( + blockhash, + lamports_per_signature, + false, + &DefaultCallback, + &feature_set, + &runtime_environments, + &sysvar_cache, + ); + + let mut invoke_context = InvokeContext::new( + &mut transaction_context, + &mut program_cache, + environment_config, + Some(log_collector.clone()), + compute_budget.to_budget(), + compute_budget.to_cost(), + ); + + invoke_context + .prepare_top_level_instructions(&sanitized_message) + .unwrap(); + invoke_context.push().unwrap(); + + let instruction_context = invoke_context + .transaction_context + .get_current_instruction_context() + .unwrap(); + + match serialize_parameters( + &instruction_context, + virtual_address_space_adjustments, + direct_mapping, + direct_account_pointers, + ) { + Ok((aligned_memory, input_memory_regions, account_metadatas, _instruction_data_offset)) => { + let serialized_memory_hash = fd_hash(0, aligned_memory.as_slice()); + let vm_input_memory_regions = input_memory_regions + .iter() + .map(memory_region_to_proto) + .collect(); + let serialized_account_metadata = account_metadatas + .iter() + .map(serialized_acct_meta_to_proto) + .collect(); + ProtoVmSerializationEffects { + has_error: false, + serialized_memory_hash, + vm_input_memory_regions, + serialized_account_metadata, + } + } + Err(_) => ProtoVmSerializationEffects { + has_error: true, + ..Default::default() + }, + } +} + +fn memory_region_to_proto(region: &MemoryRegion) -> ProtoVmInputMemoryRegion { + ProtoVmInputMemoryRegion { + vm_address: region.vm_addr, + region_size: region.len, + is_writable: region.writable, + } +} + +fn serialized_acct_meta_to_proto( + meta: &SerializedAccountMetadata, +) -> ProtoVmSerializedAccountMetadata { + ProtoVmSerializedAccountMetadata { + original_data_len: meta.original_data_len as u64, + vm_data_addr: meta.vm_data_addr, + vm_key_addr: meta.vm_key_addr, + vm_lamports_addr: meta.vm_lamports_addr, + vm_owner_addr: meta.vm_owner_addr, + } +} + +/// # Safety +/// +/// `in_ptr` must point to `in_sz` initialized bytes. `out_ptr` must point +/// to a writable buffer of at least `*out_psz` bytes. On return, `*out_psz` +/// is updated to the number of bytes written. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn sol_compat_vm_serialize_execute_v1( + out_ptr: *mut u8, + out_psz: *mut u64, + in_ptr: *mut u8, + in_sz: u64, +) -> c_int { + let in_slice = unsafe { std::slice::from_raw_parts(in_ptr, in_sz as usize) }; + let Ok(instr_context) = ProtoInstrContext::decode(in_slice) else { + return 0; + }; + + let effects = execute_vm_serialize(instr_context); + let out_slice = unsafe { std::slice::from_raw_parts_mut(out_ptr, (*out_psz) as usize) }; + let out_vec = effects.encode_to_vec(); + if out_vec.len() > out_slice.len() { + return 0; + } + out_slice[..out_vec.len()].copy_from_slice(&out_vec); + unsafe { *out_psz = out_vec.len() as u64 }; + + 1 +} + +#[cfg(test)] +mod tests { + use { + super::*, + protosol::protos::{AcctState as ProtoAcctState, InstrAcct as ProtoInstrAcct}, + solana_pubkey::Pubkey, + solana_rent::Rent, + solana_sdk_ids::{bpf_loader_deprecated, bpf_loader_upgradeable, sysvar}, + }; + + const PROGRAM_ID: [u8; 32] = [7; 32]; + + fn account(address: u8, owner: Pubkey, data_len: usize) -> ProtoAcctState { + ProtoAcctState { + address: vec![address; 32], + lamports: 1, + data: vec![0; data_len], + executable: false, + owner: owner.to_bytes().to_vec(), + } + } + + fn instr_acct(index: u32, is_writable: bool, is_signer: bool) -> ProtoInstrAcct { + ProtoInstrAcct { + index, + is_writable, + is_signer, + } + } + + fn rent_sysvar() -> ProtoAcctState { + ProtoAcctState { + address: sysvar::rent::id().to_bytes().to_vec(), + lamports: 1, + data: bincode::serialize(&Rent::default()).unwrap(), + executable: false, + owner: sysvar::id().to_bytes().to_vec(), + } + } + + fn serialize( + mut accounts: Vec, + instr_accounts: Vec, + ) -> ProtoVmSerializationEffects { + accounts.push(rent_sysvar()); // <-- Loads Rent into SysvarCache + execute_vm_serialize(ProtoInstrContext { + program_id: PROGRAM_ID.to_vec(), + accounts, + instr_accounts, + ..Default::default() + }) + } + + #[test] + fn test_serialize_single_account() { + let program_id = Pubkey::new_from_array(PROGRAM_ID); + let effects = serialize( + vec![ + account(1, program_id, 8), + account(PROGRAM_ID[0], bpf_loader_upgradeable::id(), 0), + ], + vec![instr_acct(0, true, false)], + ); + assert!(!effects.has_error); + assert_eq!(effects.serialized_account_metadata.len(), 1); + } + + #[test] + fn test_serialize_multiple_accounts() { + let program_id = Pubkey::new_from_array(PROGRAM_ID); + let effects = serialize( + vec![ + account(1, program_id, 4), + account(2, program_id, 16), + account(PROGRAM_ID[0], bpf_loader_upgradeable::id(), 0), + ], + vec![instr_acct(0, true, true), instr_acct(1, false, false)], + ); + assert!(!effects.has_error); + assert_eq!(effects.serialized_account_metadata.len(), 2); + } + + #[test] + fn test_serialize_deprecated_loader_program() { + // A program owned by the deprecated loader exercises the unaligned + // serialization path. + let program_id = Pubkey::new_from_array(PROGRAM_ID); + let effects = serialize( + vec![ + account(1, program_id, 8), + account(PROGRAM_ID[0], bpf_loader_deprecated::id(), 0), + ], + vec![instr_acct(0, true, false)], + ); + assert!(!effects.has_error); + assert_eq!(effects.serialized_account_metadata.len(), 1); + } +} diff --git a/svm/src/conformance/setup.rs b/svm/src/conformance/setup.rs new file mode 100644 index 00000000000..697ca973930 --- /dev/null +++ b/svm/src/conformance/setup.rs @@ -0,0 +1,94 @@ +//! Shared setup helpers for the execution harnesses. +//! +//! Each helper builds one owned prerequisite for an `InvokeContext` (the +//! transaction context, the runtime environments, the blockhash). The harness +//! still assembles its own `EnvironmentConfig`/`InvokeContext`, since those +//! borrow these pieces — so rather than one big `create_invoke_context_fields` +//! returning a tuple of everything, this is a handful of small, composable +//! pieces the harnesses pick from. + +#[cfg(feature = "conformance")] +use solana_account::ReadableAccount; +use { + solana_account::Account, + solana_compute_budget::compute_budget::ComputeBudget, + solana_hash::Hash, + solana_instruction::Instruction, + solana_message::SanitizedMessage, + solana_program_runtime::{ + invoke_context::mock_compile_message, + loaded_programs::{ProgramRuntimeEnvironment, ProgramRuntimeEnvironments}, + sysvar_cache::SysvarCache, + }, + solana_pubkey::Pubkey, + solana_rent::Rent, + solana_svm_feature_set::SVMFeatureSet, + solana_svm_transaction::svm_message::SVMStaticMessage, + solana_syscalls::create_program_runtime_environment, + solana_transaction_context::transaction::TransactionContext, +}; + +/// Compile `instruction` into a sanitized message and a fresh transaction +/// context sized for a single top-level instruction. +pub(crate) fn compile_transaction_context( + instruction: &Instruction, + accounts: &[(Pubkey, Account)], + program_id: &Pubkey, + loader_key: &Pubkey, + compute_budget: &ComputeBudget, + rent: Rent, +) -> (SanitizedMessage, TransactionContext<'static>) { + let (sanitized_message, transaction_accounts) = + mock_compile_message(instruction, accounts, program_id, loader_key); + let transaction_context = TransactionContext::new( + transaction_accounts, + rent, + compute_budget.max_instruction_stack_depth, + compute_budget.max_instruction_trace_length, + sanitized_message.num_instructions(), + ); + (sanitized_message, transaction_context) +} + +/// The paired (execution + deployment) program runtime environments for a +/// harness invocation. Both halves share one environment. +pub(crate) fn program_runtime_environments( + feature_set: &SVMFeatureSet, + compute_budget: &ComputeBudget, +) -> ProgramRuntimeEnvironments { + let environment = create_program_runtime_environment( + feature_set, + &compute_budget.to_budget(), + false, /* deployment */ + false, /* debugging_features */ + ) + .unwrap(); + ProgramRuntimeEnvironments::new(ProgramRuntimeEnvironment::clone(&environment), environment) +} + +/// The most recent blockhash and its lamports-per-signature from the sysvar +/// cache, or defaults when unavailable. +pub(crate) fn recent_blockhash(sysvar_cache: &SysvarCache) -> (Hash, u64) { + #[expect(deprecated)] + sysvar_cache + .get_recent_blockhashes() + .ok() + .and_then(|entries| entries.last().cloned()) + .map(|entry| (entry.blockhash, entry.fee_calculator.lamports_per_signature)) + .unwrap_or_default() +} + +/// Build a sysvar cache populated from any sysvar accounts present in the +/// input account set. +#[cfg(feature = "conformance")] +pub(crate) fn sysvar_cache_from_accounts(accounts: &[(Pubkey, Account)]) -> SysvarCache { + let mut cache = SysvarCache::default(); + cache.fill_missing_entries(|pubkey, set_sysvar| { + if let Some(account) = accounts.iter().find(|(key, _)| key == pubkey) + && account.1.lamports() > 0 + { + set_sysvar(account.1.data()); + } + }); + cache +} From cabb6ab03cd00d1edce2eec936d65650b7ef25d2 Mon Sep 17 00:00:00 2001 From: Yihau Chen Date: Wed, 3 Jun 2026 09:44:24 +0800 Subject: [PATCH 177/576] ci: trigger PR pipeline steps more granularly (#12904) --- ci/xtask/src/commands/generate_pipeline.rs | 168 ++++++++++++++++++--- 1 file changed, 148 insertions(+), 20 deletions(-) diff --git a/ci/xtask/src/commands/generate_pipeline.rs b/ci/xtask/src/commands/generate_pipeline.rs index ea2af9a598e..50820d2c8ab 100644 --- a/ci/xtask/src/commands/generate_pipeline.rs +++ b/ci/xtask/src/commands/generate_pipeline.rs @@ -168,51 +168,179 @@ async fn get_changed_files(pr_number: u64) -> Result> { Ok(changed_files) } +struct PullRequestPipelineFlags { + shellcheck: bool, + checks: bool, + feature_check: bool, + miri: bool, + frozen_abi: bool, + stable: bool, + local_cluster: bool, + docs: bool, + localnet: bool, + stable_sbf: bool, + shuttle: bool, + coverage: bool, +} + +impl PullRequestPipelineFlags { + fn from_changed_files(changed_files: &[String]) -> Self { + let trigger_all = changed_files.iter().any(|file| { + file.starts_with("ci/xtask/") + || file.ends_with("ci/rust-version.sh") + || file.ends_with("rust-toolchain.toml") + || file.ends_with("ci/docker-run-default-image.sh") + || file.ends_with("ci/docker-run.sh") + || file.ends_with("ci/docker/Dockerfile") + || file.ends_with("ci/docker/env.sh") + }); + + let rust_changed = changed_files.iter().any(|file| { + file.ends_with("Cargo.toml") || file.ends_with("Cargo.lock") || file.ends_with(".rs") + }); + + Self { + shellcheck: changed_files.iter().any(|file| file.ends_with(".sh")), + checks: trigger_all + || rust_changed + || changed_files.iter().any(|file| { + file.ends_with("ci/test-checks.sh") + || file.ends_with("scripts/cargo-for-all-lock-files.sh") + || file.ends_with("scripts/check-dev-context-only-utils.sh") + || file.ends_with("scripts/agave-build-lists.sh") + || file.ends_with("ci/order-crates-for-publishing.py") + || file.ends_with("scripts/cargo-clippy.sh") + || file.ends_with("ci/do-audit.sh") + || file.ends_with("ci/check-install-all.sh") + || file.ends_with("scripts/spl-token-cli-version.sh") + || file.ends_with("scripts/cargo-build-sbf-version.sh") + }), + feature_check: trigger_all + || rust_changed + || changed_files + .iter() + .any(|file| file.starts_with("ci/feature-check/")), + miri: trigger_all + || rust_changed + || changed_files + .iter() + .any(|file| file.ends_with("ci/test-miri.sh")), + frozen_abi: trigger_all + || rust_changed + || changed_files + .iter() + .any(|file| file.ends_with("ci/test-frozen-abi.sh")), + stable: trigger_all + || rust_changed + || changed_files.iter().any(|file| { + file.ends_with("ci/stable/run-partition.sh") + || file.ends_with("ci/stable/common.sh") + || file.ends_with("ci/common/shared-functions.sh") + || file.ends_with("ci/common/limit-threads.sh") + }), + local_cluster: trigger_all + || rust_changed + || changed_files.iter().any(|file| { + file.ends_with("ci/stable/run-local-cluster-partially.sh") + || file.ends_with("ci/stable/common.sh") + || file.ends_with("ci/common/shared-functions.sh") + }), + docs: trigger_all + || rust_changed + || changed_files.iter().any(|file| { + file.ends_with("ci/test-docs.sh") + || file.ends_with("ci/test-stable.sh") + || file.ends_with("scripts/ulimit-n.sh") + || file.ends_with("ci/common/limit-threads.sh") + || file.ends_with("ci/common/shared-functions.sh") + }), + localnet: trigger_all + || rust_changed + || changed_files.iter().any(|file| { + file.ends_with("ci/stable/run-localnet.sh") + || file.ends_with("ci/localnet-sanity.sh") + || file.ends_with("ci/run-sanity.sh") + || file.ends_with("scripts/wallet-sanity.sh") + || file.ends_with("ci/upload-ci-artifact.sh") + || file.ends_with("scripts/configure-metrics.sh") + || file.ends_with("scripts/run.sh") + }), + stable_sbf: trigger_all + || rust_changed + || changed_files.iter().any(|file| { + file.ends_with("ci/test-stable-sbf.sh") + || file.ends_with("ci/test-stable.sh") + || file.ends_with("scripts/ulimit-n.sh") + || file.ends_with("ci/common/limit-threads.sh") + || file.ends_with("ci/common/shared-functions.sh") + || file.ends_with("programs/sbf/install.sh") + }), + shuttle: trigger_all + || rust_changed + || changed_files + .iter() + .any(|file| file.ends_with("ci/test-shuttle.sh")), + coverage: trigger_all + || rust_changed + || changed_files.iter().any(|file| { + file.ends_with("scripts/coverage.sh") + || file.ends_with("ci/test-coverage.sh") + || file.starts_with("ci/coverage/") + }), + } + } +} + pub async fn generate_pull_request_pipeline(pr_number: u64) -> Result { let changed_files = get_changed_files(pr_number).await?; + let flags = PullRequestPipelineFlags::from_changed_files(&changed_files); let mut pipeline = buildkite::Pipeline::new(); pipeline.add_step(default_sanity_step()); - if changed_files.iter().any(|file| file.ends_with(".sh")) { + if flags.shellcheck { pipeline.add_step(default_shellcheck_step()); } pipeline.add_step(buildkite::Step::Wait(buildkite::WaitStep {})); - let rust_changed = changed_files.iter().any(|file| { - file.ends_with("Cargo.toml") - || file.ends_with("Cargo.lock") - || file.ends_with(".rs") - || file.ends_with("rust-toolchain.toml") - || file.ends_with("ci/rust-version.sh") - || file.ends_with("ci/docker/Dockerfile") - }); - let coverage_scripts_changed = changed_files.iter().any(|file| { - file.ends_with("scripts/coverage.sh") - || file.ends_with("ci/test-coverage.sh") - || file.starts_with("ci/coverage/") - }); - - if rust_changed { + if flags.checks { pipeline.add_step(default_checks_step()); + } + if flags.feature_check { pipeline.add_step(default_feature_check_step(5)); + } + if flags.miri { pipeline.add_step(default_miri_step()); + } + if flags.frozen_abi { pipeline.add_step(default_frozen_abi_step()); + } - pipeline.add_step(buildkite::Step::Wait(buildkite::WaitStep {})); + pipeline.add_step(buildkite::Step::Wait(buildkite::WaitStep {})); + if flags.stable { pipeline.add_step(default_stable_step(3)); + } + if flags.local_cluster { pipeline.add_step(default_local_cluster_step(10)); + } + if flags.docs { pipeline.add_step(default_docs_check_step()); + } + if flags.localnet { pipeline.add_step(default_localnet_step()); + } - pipeline.add_step(buildkite::Step::Wait(buildkite::WaitStep {})); + pipeline.add_step(buildkite::Step::Wait(buildkite::WaitStep {})); + if flags.stable_sbf { pipeline.add_step(default_stable_sbf_step()); + } + if flags.shuttle { pipeline.add_step(default_shuttle_step()); - pipeline.add_step(default_coverage_step(3)); - } else if coverage_scripts_changed { + } + if flags.coverage { pipeline.add_step(default_coverage_step(3)); } From e2c332b6a9eacc8e50b71716a878c9e45be3fe30 Mon Sep 17 00:00:00 2001 From: Yihau Chen Date: Wed, 3 Jun 2026 09:45:00 +0800 Subject: [PATCH 178/576] ci: remove downstream project tests (#12901) * remove github action path * remove local run path --- .../scripts/downstream-project-spl-common.sh | 28 --- .../downstream-project-spl-install-deps.sh | 5 - .github/scripts/purge-ubuntu-runner.sh | 17 -- .../workflows/downstream-project-anchor.yml | 59 ------ .../downstream-project-spl-nightly.yml | 17 -- .github/workflows/downstream-project-spl.yml | 171 ------------------ .mergify.yml | 23 --- ci/downstream-projects/common.sh | 17 -- ci/downstream-projects/func-openbook-dex.sh | 34 ---- ci/downstream-projects/func-spl.sh | 47 ----- ci/downstream-projects/run-all.sh | 16 -- ci/downstream-projects/run-openbook-dex.sh | 12 -- ci/downstream-projects/run-spl.sh | 12 -- ci/run-local.sh | 1 - ci/test-downstream-builds.sh | 9 - scripts/build-downstream-anchor-projects.sh | 134 -------------- scripts/patch-spl-crates-for-anchor.sh | 78 -------- 17 files changed, 680 deletions(-) delete mode 100644 .github/scripts/downstream-project-spl-common.sh delete mode 100755 .github/scripts/downstream-project-spl-install-deps.sh delete mode 100755 .github/scripts/purge-ubuntu-runner.sh delete mode 100644 .github/workflows/downstream-project-anchor.yml delete mode 100644 .github/workflows/downstream-project-spl-nightly.yml delete mode 100644 .github/workflows/downstream-project-spl.yml delete mode 100755 ci/downstream-projects/common.sh delete mode 100755 ci/downstream-projects/func-openbook-dex.sh delete mode 100755 ci/downstream-projects/func-spl.sh delete mode 100755 ci/downstream-projects/run-all.sh delete mode 100755 ci/downstream-projects/run-openbook-dex.sh delete mode 100755 ci/downstream-projects/run-spl.sh delete mode 100755 ci/test-downstream-builds.sh delete mode 100755 scripts/build-downstream-anchor-projects.sh delete mode 100644 scripts/patch-spl-crates-for-anchor.sh diff --git a/.github/scripts/downstream-project-spl-common.sh b/.github/scripts/downstream-project-spl-common.sh deleted file mode 100644 index 6db72e70dd3..00000000000 --- a/.github/scripts/downstream-project-spl-common.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -set -e - -here="$(dirname "${BASH_SOURCE[0]}")" -program="$1" - -#shellcheck source=ci/downstream-projects/common.sh -source "$here"/../../ci/downstream-projects/common.sh - -set -x -rm -rf "${program}" -git clone https://github.com/solana-program/"${program}".git - -# copy toolchain file to use solana's rust version -cp "$SOLANA_DIR"/rust-toolchain.toml "${program}"/ -cd "${program}" || exit 1 -echo "HEAD: $(git rev-parse HEAD)" - -project_used_solana_version=$(sed -nE 's/solana = \"(.*)\"/\1/p' <"Cargo.toml") -echo "used solana version: $project_used_solana_version" -if semverGT "$project_used_solana_version" "$SOLANA_VER"; then - echo "skip" - export SKIP_SPL_DOWNSTREAM_PROJECT_TEST=1 - return -fi - -update_solana_dependencies . "$SOLANA_VER" -patch_crates_io_solana Cargo.toml "$SOLANA_DIR" diff --git a/.github/scripts/downstream-project-spl-install-deps.sh b/.github/scripts/downstream-project-spl-install-deps.sh deleted file mode 100755 index defb64fab23..00000000000 --- a/.github/scripts/downstream-project-spl-install-deps.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash -set -e - -sudo apt-get update -sudo apt-get install libudev-dev libclang-dev protobuf-compiler -y diff --git a/.github/scripts/purge-ubuntu-runner.sh b/.github/scripts/purge-ubuntu-runner.sh deleted file mode 100755 index e8008202867..00000000000 --- a/.github/scripts/purge-ubuntu-runner.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash - -sudo rm -rf /usr/share/dotnet -sudo rm -rf /usr/share/swift -sudo rm -rf /usr/share/mysql -sudo rm -rf /usr/share/az_* -sudo rm -rf /usr/share/postgresql-common - -sudo rm -rf /opt/ghc -sudo rm -rf /opt/az -sudo rm -rf /opt/pipx -sudo rm -rf /opt/microsoft -sudo rm -rf /opt/google -sudo rm -rf /opt/hostedtoolcache - -sudo rm -rf /usr/local/lib/android -sudo rm -rf /usr/local/lib/heroku diff --git a/.github/workflows/downstream-project-anchor.yml b/.github/workflows/downstream-project-anchor.yml deleted file mode 100644 index be76e10ea2f..00000000000 --- a/.github/workflows/downstream-project-anchor.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: Downstream Project - Anchor - -on: - push: - branches: - - master - - v[0-9]+.[0-9]+ - pull_request: - branches: - - master - - v[0-9]+.[0-9]+ - paths: - - "**.rs" - - "Cargo.toml" - - "Cargo.lock" - - "scripts/build-downstream-anchor-projects.sh" - - "scripts/patch-spl-crates-for-anchor.sh" - - ".github/scripts/purge-ubuntu-runner.sh" - - ".github/scripts/downstream-project-spl-install-deps.sh" - - ".github/workflows/downstream-project-anchor.yml" - workflow_call: - inputs: - branch: - required: false - type: string - default: "master" - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -env: - SHELL: /bin/bash - SCCACHE_GHA_ENABLED: "true" - RUSTC_WRAPPER: "sccache" - -jobs: - test: - #if: github.repository_owner == 'anza-xyz' - if: false # restore once all program-specific repos are ready - runs-on: ubuntu-24.04 - strategy: - matrix: - version: ["master"] - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - shell: bash - run: | - .github/scripts/purge-ubuntu-runner.sh - - - uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10 - with: - version: "v0.10.0" - - - shell: bash - run: | - source .github/scripts/downstream-project-spl-install-deps.sh - ./scripts/build-downstream-anchor-projects.sh ${{ matrix.version }} diff --git a/.github/workflows/downstream-project-spl-nightly.yml b/.github/workflows/downstream-project-spl-nightly.yml deleted file mode 100644 index 687ae0f2ab3..00000000000 --- a/.github/workflows/downstream-project-spl-nightly.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: Downstream Project - SPL (Nightly) - -on: - schedule: - - cron: "0 3 * * *" - -jobs: - main: - if: github.repository_owner == 'anza-xyz' - strategy: - fail-fast: false - matrix: - branch: - - master - uses: ./.github/workflows/downstream-project-spl.yml - with: - branch: ${{ matrix.branch }} diff --git a/.github/workflows/downstream-project-spl.yml b/.github/workflows/downstream-project-spl.yml deleted file mode 100644 index 508dc6d8921..00000000000 --- a/.github/workflows/downstream-project-spl.yml +++ /dev/null @@ -1,171 +0,0 @@ -name: Downstream Project - SPL - -on: - push: - branches: - - master - - v[0-9]+.[0-9]+ - pull_request: - branches: - - master - - v[0-9]+.[0-9]+ - paths: - - "**.rs" - - "rust-toolchain.toml" - - "Cargo.toml" - - "Cargo.lock" - - "ci/downstream-projects/run-spl.sh" - - ".github/workflows/downstream-project-spl.yml" - - ".github/scripts/downstream-project-spl-common.sh" - - ".github/scripts/downstream-project-spl-install-deps.sh" - workflow_call: - inputs: - branch: - required: false - type: string - default: "master" - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -env: - SHELL: /bin/bash - SCCACHE_GHA_ENABLED: "true" - RUSTC_WRAPPER: "sccache" - -jobs: - check: - #if: github.repository_owner == 'anza-xyz' - if: false - runs-on: ubuntu-24.04 - timeout-minutes: 60 - strategy: - matrix: - programs: - - associated-token-account - - feature-proposal - - instruction-padding - - memo - - record - - single-pool - - slashing - - stake-pool - - token-2022 - # re-enable with https://github.com/buffalojoec/mollusk/pull/74 - # - token - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - shell: bash - run: | - .github/scripts/purge-ubuntu-runner.sh - - - uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10 - with: - version: "v0.10.0" - - - shell: bash - run: | - source .github/scripts/downstream-project-spl-install-deps.sh - source .github/scripts/downstream-project-spl-common.sh "${{ matrix.programs }}" - if [ -n "$SKIP_SPL_DOWNSTREAM_PROJECT_TEST" ]; then - exit 0 - fi - cargo check - - test_cli: - #if: github.repository_owner == 'anza-xyz' - if: false - runs-on: ubuntu-24.04 - timeout-minutes: 60 - strategy: - matrix: - programs: - - single-pool - - token-2022 - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - shell: bash - run: | - .github/scripts/purge-ubuntu-runner.sh - - - uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10 - with: - version: "v0.10.0" - - - shell: bash - run: | - source .github/scripts/downstream-project-spl-install-deps.sh - source .github/scripts/downstream-project-spl-common.sh "${{ matrix.programs }}" - if [ -n "$SKIP_SPL_DOWNSTREAM_PROJECT_TEST" ]; then - exit 0 - fi - $CARGO_BUILD_SBF --manifest-path program/Cargo.toml - cargo test --manifest-path clients/cli/Cargo.toml - - cargo-test-sbf: - #if: github.repository_owner == 'anza-xyz' - if: false - runs-on: ubuntu-24.04 - timeout-minutes: 60 - strategy: - matrix: - programs: - - associated-token-account - - feature-proposal - - instruction-padding - - memo - - record - - single-pool - - slashing - - stake-pool - - token-2022 - # re-enable with https://github.com/buffalojoec/mollusk/pull/74 - # - token - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - shell: bash - run: | - .github/scripts/purge-ubuntu-runner.sh - - - uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10 - with: - version: "v0.10.0" - - - name: Install dependencies - shell: bash - run: | - source .github/scripts/downstream-project-spl-install-deps.sh - source .github/scripts/downstream-project-spl-common.sh "${{ matrix.programs }}" - if [ -n "$SKIP_SPL_DOWNSTREAM_PROJECT_TEST" ]; then - exit 0 - fi - - - name: Test SBPFv0 - shell: bash - run: | - source ci/downstream-projects/common.sh - cd "${{ matrix.programs }}" - $CARGO_BUILD_SBF --arch v0 --manifest-path program/Cargo.toml - RUSTFLAGS="-Awarnings" SBF_OUT_DIR="../target/deploy" cargo test --features test-sbf --manifest-path program/Cargo.toml - - - name: Test SBPFv1 - shell: bash - run: | - source ci/downstream-projects/common.sh - cd "${{ matrix.programs }}" - rm -rf target/deploy target/sbpf* - $CARGO_BUILD_SBF --arch v1 --manifest-path program/Cargo.toml - RUSTFLAGS="-Awarnings" SBF_OUT_DIR="../target/deploy" cargo test --features test-sbf --manifest-path program/Cargo.toml - - - name: Test SBPFv2 - shell: bash - run: | - source ci/downstream-projects/common.sh - cd "${{ matrix.programs }}" - rm -rf target/deploy target/sbpf* - $CARGO_BUILD_SBF --arch v2 --manifest-path program/Cargo.toml - RUSTFLAGS="-Awarnings" SBF_OUT_DIR="../target/deploy" cargo test --features test-sbf --manifest-path program/Cargo.toml diff --git a/.mergify.yml b/.mergify.yml index de40fdf55bd..fbf17d99a40 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -44,29 +44,6 @@ pull_request_rules: - or: - check-success=clippy-nightly (macos-latest) - check-success=clippy-nightly (macos-latest-large) - - or: - - -files~=(\.rs|Cargo\.toml|Cargo\.lock|cargo-build-sbf|cargo-test-sbf|ci/downstream-projects/run-spl\.sh|\.github/workflows/downstream-project-spl\.yml)$ - - and: - - status-success=check (associated-token-account) - - status-success=check (feature-proposal) - - status-success=check (instruction-padding) - - status-success=check (memo) - - status-success=check (record) - - status-success=check (single-pool) - - status-success=check (slashing) - - status-success=check (stake-pool) - - status-success=check (token-2022) - - status-success=test_cli (single-pool) - - status-success=test_cli (token-2022) - - status-success=cargo-test-sbf (associated-token-account) - - status-success=cargo-test-sbf (feature-proposal) - - status-success=cargo-test-sbf (instruction-padding) - - status-success=cargo-test-sbf (memo) - - status-success=cargo-test-sbf (record) - - status-success=cargo-test-sbf (single-pool) - - status-success=cargo-test-sbf (slashing) - - status-success=cargo-test-sbf (stake-pool) - - status-success=cargo-test-sbf (token-2022) - or: - -files~=(Cargo\.toml|.github/workflows/crate-check\.yml|ci/check-crates\.sh)$ - check-success=crate check diff --git a/ci/downstream-projects/common.sh b/ci/downstream-projects/common.sh deleted file mode 100755 index 54a7c8c5f0a..00000000000 --- a/ci/downstream-projects/common.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash -set -e - -source ci/_ -source ci/semver_bash/semver.sh -source scripts/patch-crates.sh -source scripts/read-cargo-variable.sh - -SOLANA_VER=$(readCargoVariable version Cargo.toml) -export SOLANA_VER -export SOLANA_DIR=$PWD -export CARGO="$SOLANA_DIR"/cargo -export CARGO_BUILD_SBF="$SOLANA_DIR"/cargo-build-sbf -export CARGO_TEST_SBF="$SOLANA_DIR"/cargo-test-sbf - -mkdir -p target/downstream-projects -cd target/downstream-projects diff --git a/ci/downstream-projects/func-openbook-dex.sh b/ci/downstream-projects/func-openbook-dex.sh deleted file mode 100755 index 71adddf2804..00000000000 --- a/ci/downstream-projects/func-openbook-dex.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env bash - -openbook_dex() { - ( - set -x - rm -rf openbook-dex - git clone https://github.com/openbook-dex/program.git openbook-dex - # copy toolchain file to use solana's rust version - cp "$SOLANA_DIR"/rust-toolchain.toml openbook-dex/ - cd openbook-dex || exit 1 - - update_solana_dependencies . "$SOLANA_VER" - patch_crates_io_solana Cargo.toml "$SOLANA_DIR" - cat >> Cargo.toml <> dex/Cargo.toml <=<~]*(.*)\"/\1/p' <"token/program/Cargo.toml") - echo "used solana version: $project_used_solana_version" - if semverGT "$project_used_solana_version" "$SOLANA_VER"; then - echo "skip" - return - fi - - ./patch.crates-io.sh "$SOLANA_DIR" - - for program in "${PROGRAMS[@]}"; do - $CARGO_TEST_SBF --manifest-path "$program"/Cargo.toml - done - - # TODO better: `build.rs` for spl-token-cli doesn't seem to properly build - # the required programs to run the tests, so instead we run the tests - # after we know programs have been built - cargo build - cargo test - ) -} diff --git a/ci/downstream-projects/run-all.sh b/ci/downstream-projects/run-all.sh deleted file mode 100755 index ca33b53084d..00000000000 --- a/ci/downstream-projects/run-all.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash -set -e - -here="$(dirname "$0")" - -#shellcheck source=ci/downstream-projects/func-spl.sh -source "$here"/func-spl.sh - -#shellcheck source=ci/downstream-projects/func-openbook-dex.sh -source "$here"/func-openbook-dex.sh - -#shellcheck source=ci/downstream-projects/common.sh -source "$here"/common.sh - -_ spl -_ openbook_dex diff --git a/ci/downstream-projects/run-openbook-dex.sh b/ci/downstream-projects/run-openbook-dex.sh deleted file mode 100755 index d79df546466..00000000000 --- a/ci/downstream-projects/run-openbook-dex.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash -set -e - -here="$(dirname "$0")" - -#shellcheck source=ci/downstream-projects/func-openbook-dex.sh -source "$here"/func-openbook-dex.sh - -#shellcheck source=ci/downstream-projects/common.sh -source "$here"/common.sh - -_ openbook_dex diff --git a/ci/downstream-projects/run-spl.sh b/ci/downstream-projects/run-spl.sh deleted file mode 100755 index 543dca928ea..00000000000 --- a/ci/downstream-projects/run-spl.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash -set -e - -here="$(dirname "$0")" - -#shellcheck source=ci/downstream-projects/func-spl.sh -source "$here"/func-spl.sh - -#shellcheck source=ci/downstream-projects/common.sh -source "$here"/common.sh - -_ spl diff --git a/ci/run-local.sh b/ci/run-local.sh index aa1bf1344fa..451d2969b0b 100755 --- a/ci/run-local.sh +++ b/ci/run-local.sh @@ -14,7 +14,6 @@ steps+=(test-coverage) steps+=(test-stable) steps+=(test-stable-sbf) steps+=(test-stable-perf) -steps+=(test-downstream-builds) steps+=(test-bench) steps+=(test-local-cluster) steps+=(test-local-cluster-flakey) diff --git a/ci/test-downstream-builds.sh b/ci/test-downstream-builds.sh deleted file mode 100755 index 9ddba8755cc..00000000000 --- a/ci/test-downstream-builds.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash - -cd "$(dirname "$0")/.." - -export CI_LOCAL_RUN=true - -set -ex - -ci/downstream-projects/run-all.sh diff --git a/scripts/build-downstream-anchor-projects.sh b/scripts/build-downstream-anchor-projects.sh deleted file mode 100755 index 4fd2fc34aa7..00000000000 --- a/scripts/build-downstream-anchor-projects.sh +++ /dev/null @@ -1,134 +0,0 @@ -#!/usr/bin/env bash -# -# Builds known downstream projects against local solana source -# - -set -e -cd "$(dirname "$0")"/.. -source ci/_ -source scripts/patch-crates.sh -source scripts/read-cargo-variable.sh -source scripts/patch-spl-crates-for-anchor.sh - -anchor_version=$1 -solana_ver=$(readCargoVariable version Cargo.toml) -solana_dir=$PWD -cargo="$solana_dir"/cargo -cargo_build_sbf="$solana_dir"/cargo-build-sbf -cargo_test_sbf="$solana_dir"/cargo-test-sbf - -mkdir -p target/downstream-projects-anchor -cd target/downstream-projects-anchor - -update_anchor_dependencies() { - declare project_root="$1" - declare anchor_ver="$2" - declare tomls=() - while IFS='' read -r line; do tomls+=("$line"); done < <(find "$project_root" -name Cargo.toml) - - sed -i -e "s#\(anchor-lang = \"\)[^\"]*\(\"\)#\1=$anchor_ver\2#g" "${tomls[@]}" || return $? - sed -i -e "s#\(anchor-spl = \"\)[^\"]*\(\"\)#\1=$anchor_ver\2#g" "${tomls[@]}" || return $? - sed -i -e "s#\(anchor-lang = { version = \"\)[^\"]*\(\"\)#\1=$anchor_ver\2#g" "${tomls[@]}" || return $? - sed -i -e "s#\(anchor-spl = { version = \"\)[^\"]*\(\"\)#\1=$anchor_ver\2#g" "${tomls[@]}" || return $? -} - -patch_crates_io_anchor() { - declare Cargo_toml="$1" - declare anchor_dir="$2" - cat >> "$Cargo_toml" <> "$Cargo_toml" < Date: Tue, 2 Jun 2026 23:21:08 -0400 Subject: [PATCH 179/576] docs: Add information about setting the BLS Public Key (#12908) * docs: Add information about setting the BLS Public Key * pr feedback --- docs/src/operations/guides/validator-start.md | 4 ++ docs/src/operations/guides/vote-accounts.md | 50 +++++++++++++++++++ docs/src/operations/setup-a-validator.md | 4 ++ 3 files changed, 58 insertions(+) diff --git a/docs/src/operations/guides/validator-start.md b/docs/src/operations/guides/validator-start.md index ea7e2e8fd1c..804da5fcbac 100644 --- a/docs/src/operations/guides/validator-start.md +++ b/docs/src/operations/guides/validator-start.md @@ -250,6 +250,10 @@ solana create-vote-account ~/vote-account-keypair.json ~/validator-keypair.json Remember to move your authorized withdrawer keypair into a very secure location after running the above command. +After SIMD-0387 has been activated on your cluster, set the BLS public key for +your vote account before starting the validator. Follow the +[BLS public key instructions](./vote-accounts.md#set-the-bls-public-key). + Read more about [creating and managing a vote account](./vote-accounts.md). ## Known validators diff --git a/docs/src/operations/guides/vote-accounts.md b/docs/src/operations/guides/vote-accounts.md index 636a3806bfa..75fdae01f9f 100644 --- a/docs/src/operations/guides/vote-accounts.md +++ b/docs/src/operations/guides/vote-accounts.md @@ -23,11 +23,61 @@ of the account. [vote-update-validator](../../cli/usage.md#solana-vote-update-validator). - To change the [vote authority](#vote-authority), use [vote-authorize-voter-checked](../../cli/usage.md#solana-vote-authorize-voter-checked). +- To set the BLS public key associated with the + [vote authority](#vote-authority), use + [vote-authorize-voter-checked](../../cli/usage.md#solana-vote-authorize-voter-checked) + after SIMD-0387 is active on the cluster. - To change the [authorized withdrawer](#authorized-withdrawer), use [vote-authorize-withdrawer-checked](../../cli/usage.md#solana-vote-authorize-withdrawer-checked). - To change the [commission](#commission), use [vote-update-commission](../../cli/usage.md#solana-vote-update-commission). +### Set the BLS Public Key + +Voting validators must set the BLS public key associated with their authorized voter +after SIMD-0387, BLS pubkey management, is active on the target cluster. The +Solana CLI will not set the BLS public key before the feature is active. + +> **IMPORTANT** As of June 2026, SIMD-0387 is active only on testnet. Setting +> the BLS public key is a prerequisite for SIMD-0357. If the BLS public key is +> not set, the vote account will behave as unstaked once SIMD-0357 is active. + +For almost all validators, the authorized voter is the validator identity. If +you are unsure which keypair is the current authorized voter, run: + +```bash +solana vote-account | grep "Vote Authority" +``` + +Then set the BLS public key by running the following command. + +```bash +solana vote-authorize-voter-checked +``` + +This does not change the authorized voter. It fills in the BLS public key +associated with the authorized voter. + +For a new vote account, follow the normal +[create vote account](#create-a-vote-account) instructions first, then run the +same `vote-authorize-voter-checked` command above to set the BLS public key. + +To check whether the BLS public key is set on chain, run: + +```bash +solana vote-account | grep "BLS Public Key" +``` + +If the command does not print a `BLS Public Key` line, the vote account does not +have a BLS public key set. + +To view the BLS public key derived from the authorized voter keypair locally, +run: + +```bash +solana-keygen bls_pubkey +``` + ## Vote Account Structure ### Vote Account Address diff --git a/docs/src/operations/setup-a-validator.md b/docs/src/operations/setup-a-validator.md index 21a49df5075..0e801d29fb5 100644 --- a/docs/src/operations/setup-a-validator.md +++ b/docs/src/operations/setup-a-validator.md @@ -158,6 +158,10 @@ solana create-vote-account -ut \ > cli properly above but they are useful to ensure you're using the intended > cluster and keypair. +After SIMD-0387 has been activated on your cluster, set the BLS public key for +your vote account before starting the validator. Follow the +[BLS public key instructions](./guides/vote-accounts.md#set-the-bls-public-key). + ## Save the Withdrawer Keypair Securely Make sure your `authorized-withdrawer-keypair.json` is stored in a safe place. From f1a33fc61a28237ca2700d2183a33009917bba12 Mon Sep 17 00:00:00 2001 From: Akhilesh Singhania Date: Wed, 3 Jun 2026 08:07:06 +0200 Subject: [PATCH 180/576] Update cli to support alpenglow votes and epoch credits (#12872) --- CHANGELOG.md | 4 + Cargo.lock | 3 + cli-output/Cargo.toml | 3 + cli-output/src/cli_output.rs | 253 +++++++++++++++--- cli/src/vote.rs | 43 +-- dev-bins/Cargo.lock | 3 + programs/sbf/Cargo.lock | 3 + .../block_component_processor/vote_reward.rs | 5 +- runtime/src/inflation_rewards/points.rs | 7 +- votor-messages/src/migration.rs | 3 + 10 files changed, 255 insertions(+), 72 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 666b200a255..3d072600f8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,10 @@ Release channels have their own copy of this changelog: deprecated in v4.0.0; mmap mode has now been removed entirely). The flag is still accepted for backward compatibility, but account storages are always accessed via file I/O. #### Changes +### CLI +#### Breaking +#### Changes +* `vote-account` supports Alpenglow and as such `vote-account --output json` breaks compatibility with older versions. ## 4.1.0 ### RPC diff --git a/Cargo.lock b/Cargo.lock index 0a157a844d9..0d9847c5987 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7484,6 +7484,7 @@ version = "4.2.0-alpha.0" dependencies = [ "Inflector", "agave-reserved-account-keys", + "agave-votor-messages", "base64 0.22.1", "bitvec", "chrono", @@ -7503,9 +7504,11 @@ dependencies = [ "solana-cli-config", "solana-clock", "solana-epoch-info", + "solana-epoch-schedule", "solana-hash 4.3.0", "solana-keypair", "solana-message", + "solana-native-token", "solana-packet 4.1.0", "solana-pubkey 4.2.0", "solana-rpc-client-api", diff --git a/cli-output/Cargo.toml b/cli-output/Cargo.toml index eddbe7522cf..0e7f2324222 100644 --- a/cli-output/Cargo.toml +++ b/cli-output/Cargo.toml @@ -18,6 +18,7 @@ agave-unstable-api = [] [dependencies] Inflector = { workspace = true } agave-reserved-account-keys = { workspace = true } +agave-votor-messages = { workspace = true } base64 = { workspace = true } bitvec = { workspace = true } chrono = { workspace = true, features = ["default", "serde"] } @@ -37,8 +38,10 @@ solana-clap-utils = { workspace = true } solana-cli-config = { workspace = true } solana-clock = { workspace = true } solana-epoch-info = { workspace = true, features = ["serde"] } +solana-epoch-schedule = { workspace = true } solana-hash = { workspace = true } solana-message = { workspace = true } +solana-native-token = { workspace = true } solana-packet = { workspace = true } solana-pubkey = { workspace = true } solana-rpc-client-api = { workspace = true } diff --git a/cli-output/src/cli_output.rs b/cli-output/src/cli_output.rs index 430eab193b1..abe279f5b5b 100644 --- a/cli-output/src/cli_output.rs +++ b/cli-output/src/cli_output.rs @@ -10,6 +10,7 @@ use { writeln_transaction, }, }, + agave_votor_messages::{certificate::Certificate, migration::AG_MIGRATION_EPOCH_CREDIT}, base64::{Engine, prelude::BASE64_STANDARD}, bitvec::vec::BitVec, chrono::{Local, TimeZone, Utc}, @@ -27,7 +28,9 @@ use { solana_clap_utils::keypair::SignOnly, solana_clock::{Epoch, Slot, UnixTimestamp}, solana_epoch_info::EpochInfo, + solana_epoch_schedule::EpochSchedule, solana_hash::Hash, + solana_native_token::LAMPORTS_PER_SOL, solana_pubkey::Pubkey, solana_rpc_client_api::response::{ RpcAccountBalance, RpcContactInfo, RpcInflationGovernor, RpcInflationRate, RpcKeyedAccount, @@ -46,9 +49,13 @@ use { solana_transaction_status_client_types::UiTransactionError, solana_vote_program::{ authorized_voters::AuthorizedVoters, - vote_state::{BlockTimestamp, LandedVote, MAX_EPOCH_CREDITS_HISTORY, MAX_LOCKOUT_HISTORY}, + vote_state::{ + BlockTimestamp, LandedVote, MAX_EPOCH_CREDITS_HISTORY, MAX_LOCKOUT_HISTORY, + VOTE_CREDITS_MAXIMUM_PER_SLOT, VoteStateV4, + }, }, std::{ + cmp::Ordering, collections::{BTreeMap, HashMap}, fmt, str::FromStr, @@ -1199,7 +1206,7 @@ impl fmt::Display for CliKeyedEpochRewards { } } -fn show_votes_and_credits( +fn show_tower_votes( f: &mut fmt::Formatter, votes: &[CliLandedVote], epoch_voting_history: &[CliEpochVotingHistory], @@ -1213,7 +1220,7 @@ fn show_votes_and_credits( writeln!( f, - "{} Votes (using {}/{} entries):", + "{} Tower votes (using {}/{} entries):", (if newest_history_entry.is_none() { "All" } else { @@ -1235,14 +1242,22 @@ fn show_votes_and_credits( writeln!(f, " (latency {})", vote.latency)?; } } - if let Some(newest) = newest_history_entry { + if let Some(newest) = newest_history_entry + && let CliEpochVotingHistory::Tower(newest) = newest + { writeln!( f, "- ... (truncated {} rooted votes, which have been credited)", newest.credits )?; } + Ok(()) +} +fn show_epoch_credits( + f: &mut fmt::Formatter, + epoch_voting_history: &[CliEpochVotingHistory], +) -> fmt::Result { if !epoch_voting_history.is_empty() { writeln!( f, @@ -1262,39 +1277,76 @@ fn show_votes_and_credits( } for entry in epoch_voting_history.iter().rev() { - writeln!( - f, // tame fmt so that this will be folded like following - "- epoch: {}", - entry.epoch - )?; - writeln!( - f, - " credits range: ({}..{}]", - entry.prev_credits, entry.credits - )?; + match entry { + CliEpochVotingHistory::Tower(tower) => { + writeln!( + f, // tame fmt so that this will be folded like following + "- Tower epoch: {}", + tower.epoch + )?; + writeln!( + f, + " credits range: ({}..{}]", + tower.prev_credits, tower.credits + )?; + writeln!( + f, + " credits/max credits: {}/{}", + tower.credits_earned, + tower.slots_in_epoch * u64::from(tower.max_credits_per_slot) + )?; + } + CliEpochVotingHistory::Alpenglow(ag) => { + writeln!(f, "- Alpenglow epoch: {}", ag.epoch)?; + writeln!( + f, + " lamports range: ({}..{}]", + ag.prev_lamports, ag.lamports + )?; + writeln!( + f, + " lamports earned: {} ({:.4} SOL)", + ag.lamports_earned, + ag.lamports_earned as f64 / LAMPORTS_PER_SOL as f64 + )?; + } + } + } + if let Some(oldest) = epoch_voting_history.iter().next() + && oldest.prev() > 0 + { + // Oldest entry doesn't start with 0. so history must be truncated... + + // count of this combined pseudo credits range: (0..=oldest.prev_credits] like the above + // (or this is just [1..=oldest.prev_credits] for human's simpler minds) + let count = oldest.prev(); + writeln!( f, - " credits/max credits: {}/{}", - entry.credits_earned, - entry.slots_in_epoch * u64::from(entry.max_credits_per_slot) + "- ... (omitting {count} past rooted votes, which have already been credited)" )?; } - if let Some(oldest) = epoch_voting_history.iter().next() { - if oldest.prev_credits > 0 { - // Oldest entry doesn't start with 0. so history must be truncated... - // count of this combined pseudo credits range: (0..=oldest.prev_credits] like the above - // (or this is just [1..=oldest.prev_credits] for human's simpler minds) - let count = oldest.prev_credits; + Ok(()) +} - writeln!( - f, - "- ... (omitting {count} past rooted votes, which have already been credited)" - )?; +fn show_votes_and_credits( + f: &mut fmt::Formatter, + votes: &VotesObserved, + epoch_voting_history: &[CliEpochVotingHistory], +) -> fmt::Result { + match votes { + VotesObserved::Tower(votes) => { + show_tower_votes(f, votes, epoch_voting_history)?; + show_epoch_credits(f, epoch_voting_history) + } + VotesObserved::Alpenglow(vote) => { + if let Some(vote) = vote { + writeln!(f, "Latest Alpenglow vote for slot: {}", vote.slot)?; + } + show_epoch_credits(f, epoch_voting_history) } } - - Ok(()) } enum Format { @@ -1749,6 +1801,36 @@ impl fmt::Display for CliValidatorInfo { } } +#[derive(Serialize, Deserialize)] +pub enum VotesObserved { + Alpenglow(Option), + Tower(Vec), +} + +impl VotesObserved { + pub fn new(vote_state: &VoteStateV4, ag_genesis_cert: &Option) -> Self { + match ag_genesis_cert { + None => Self::Tower(vote_state.votes.iter().map(CliLandedVote::from).collect()), + Some(cert) => match vote_state.votes.iter().last() { + None => Self::Alpenglow(None), + Some(vote) => { + if vote.lockout.slot() <= cert.cert_type.slot() { + Self::Tower(vote_state.votes.iter().map(CliLandedVote::from).collect()) + } else { + Self::Alpenglow(Some(CliLandedVote::from(vote))) + } + } + }, + } + } +} + +impl Default for VotesObserved { + fn default() -> Self { + Self::Tower(vec![]) + } +} + #[derive(Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CliVoteAccount { @@ -1761,7 +1843,7 @@ pub struct CliVoteAccount { pub commission: u8, pub root_slot: Option, pub recent_timestamp: BlockTimestamp, - pub votes: Vec, + pub votes_observed: VotesObserved, pub epoch_voting_history: Vec, #[serde(skip_serializing)] pub use_lamports_unit: bool, @@ -1835,7 +1917,7 @@ impl fmt::Display for CliVoteAccount { unix_timestamp_to_string(self.recent_timestamp.timestamp), self.recent_timestamp.slot )?; - show_votes_and_credits(f, &self.votes, &self.epoch_voting_history)?; + show_votes_and_credits(f, &self.votes_observed, &self.epoch_voting_history)?; show_epoch_rewards(f, &self.epoch_rewards, self.use_csv)?; Ok(()) } @@ -1884,9 +1966,25 @@ impl From<&AuthorizedVoters> for CliAuthorizedVoters { } } -#[derive(Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum CliEpochVotingHistory { + Alpenglow(AgEpochHistory), + Tower(TowerEpochHistory), +} + +impl CliEpochVotingHistory { + fn prev(&self) -> u64 { + match self { + Self::Tower(t) => t.prev_credits, + Self::Alpenglow(a) => a.prev_lamports, + } + } +} + +#[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct CliEpochVotingHistory { +pub struct TowerEpochHistory { pub epoch: Epoch, pub slots_in_epoch: u64, pub credits_earned: u64, @@ -1895,7 +1993,94 @@ pub struct CliEpochVotingHistory { pub max_credits_per_slot: u8, } -#[derive(Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AgEpochHistory { + pub epoch: Epoch, + pub slots_in_epoch: u64, + pub lamports_earned: u64, + pub lamports: u64, + pub prev_lamports: u64, +} + +pub fn get_epoch_history( + epoch_schedule: &EpochSchedule, + vote_state: &VoteStateV4, + ag_genesis_cert: &Option, + tvc_activation_epoch: Option, +) -> Vec { + let mut ret = vec![]; + let mut marker_seen = false; + for entry in vote_state.epoch_credits.iter().copied() { + if entry == AG_MIGRATION_EPOCH_CREDIT { + marker_seen = true; + continue; + } + let (epoch, credits, prev_credits) = entry; + let slots_in_epoch = epoch_schedule.get_slots_in_epoch(epoch); + + enum TowerOrAg { + Tower(Slot), + Ag(Slot), + } + + let tower_or_ag = match ag_genesis_cert { + None => TowerOrAg::Tower(slots_in_epoch), + Some(cert) => { + let migration_slot = cert.cert_type.slot(); + let migration_epoch = epoch_schedule.get_epoch(migration_slot); + match epoch.cmp(&migration_epoch) { + Ordering::Less => TowerOrAg::Tower(slots_in_epoch), + Ordering::Greater => TowerOrAg::Ag(slots_in_epoch), + Ordering::Equal => { + // +1 because migration slot is still tower + let migration_offset = + migration_slot - epoch_schedule.get_first_slot_in_epoch(epoch) + 1; + if marker_seen { + TowerOrAg::Ag(slots_in_epoch - migration_offset) + } else { + TowerOrAg::Tower(migration_offset) + } + } + } + } + }; + + let entry = match tower_or_ag { + TowerOrAg::Tower(slots_in_epoch) => { + let credits_earned = credits.saturating_sub(prev_credits); + let is_tvc_active = tvc_activation_epoch.map(|e| epoch >= e).unwrap_or_default(); + let max_credits_per_slot = if is_tvc_active { + VOTE_CREDITS_MAXIMUM_PER_SLOT + } else { + 1 + }; + CliEpochVotingHistory::Tower(TowerEpochHistory { + epoch, + slots_in_epoch, + credits_earned, + credits, + prev_credits, + max_credits_per_slot, + }) + } + TowerOrAg::Ag(slots_in_epoch) => { + let lamports_earned = credits.saturating_sub(prev_credits); + CliEpochVotingHistory::Alpenglow(AgEpochHistory { + epoch, + slots_in_epoch, + lamports_earned, + lamports: credits, + prev_lamports: prev_credits, + }) + } + }; + ret.push(entry); + } + ret +} + +#[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CliLandedVote { pub latency: u8, diff --git a/cli/src/vote.rs b/cli/src/vote.rs index b1d2b180fa7..40225ae0c9d 100644 --- a/cli/src/vote.rs +++ b/cli/src/vote.rs @@ -30,8 +30,8 @@ use { offline::*, }, solana_cli_output::{ - CliEpochVotingHistory, CliLandedVote, CliVoteAccount, ReturnSignersConfig, - display::build_balance_message, return_signers_with_config, + CliVoteAccount, ReturnSignersConfig, VotesObserved, display::build_balance_message, + get_epoch_history, return_signers_with_config, }, solana_commitment_config::CommitmentConfig, solana_feature_gate_interface::from_account, @@ -47,8 +47,8 @@ use { vote_error::VoteError, vote_instruction::{self, CreateVoteAccountConfig, withdraw}, vote_state::{ - VOTE_CREDITS_MAXIMUM_PER_SLOT, VoteAuthorize, VoteInit, VoteInitV2, VoteStateV4, - VoterWithBLSArgs, create_bls_proof_of_possession, + VoteAuthorize, VoteInit, VoteInitV2, VoteStateV4, VoterWithBLSArgs, + create_bls_proof_of_possession, }, }, std::rc::Rc, @@ -1616,31 +1616,14 @@ pub async fn process_show_vote_account( .and_then(|feature| feature.activated_at); let tvc_activation_epoch = tvc_activation_slot.map(|s| epoch_schedule.get_epoch(s)); - let mut votes: Vec = vec![]; - let mut epoch_voting_history: Vec = vec![]; - if !vote_state.votes.is_empty() { - for vote in &vote_state.votes { - votes.push(vote.into()); - } - for (epoch, credits, prev_credits) in vote_state.epoch_credits.iter().copied() { - let credits_earned = credits.saturating_sub(prev_credits); - let slots_in_epoch = epoch_schedule.get_slots_in_epoch(epoch); - let is_tvc_active = tvc_activation_epoch.map(|e| epoch >= e).unwrap_or_default(); - let max_credits_per_slot = if is_tvc_active { - VOTE_CREDITS_MAXIMUM_PER_SLOT - } else { - 1 - }; - epoch_voting_history.push(CliEpochVotingHistory { - epoch, - slots_in_epoch, - credits_earned, - credits, - prev_credits, - max_credits_per_slot, - }); - } - } + let ag_genesis_cert = rpc_client.get_ag_genesis_cert().await?; + let votes_observed = VotesObserved::new(&vote_state, &ag_genesis_cert); + let epoch_voting_history = get_epoch_history( + &epoch_schedule, + &vote_state, + &ag_genesis_cert, + tvc_activation_epoch, + ); let epoch_rewards = if let Some(num_epochs) = with_rewards { match crate::stake::fetch_epoch_rewards( @@ -1673,7 +1656,7 @@ pub async fn process_show_vote_account( .min(u8::MAX as u16) as u8, root_slot: vote_state.root_slot, recent_timestamp: vote_state.last_timestamp.clone(), - votes, + votes_observed, epoch_voting_history, use_lamports_unit, use_csv, diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index f841720a9a6..460b1b20951 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -6420,6 +6420,7 @@ version = "4.2.0-alpha.0" dependencies = [ "Inflector", "agave-reserved-account-keys", + "agave-votor-messages", "base64 0.22.1", "bitvec", "chrono", @@ -6439,8 +6440,10 @@ dependencies = [ "solana-cli-config", "solana-clock", "solana-epoch-info", + "solana-epoch-schedule", "solana-hash 4.3.0", "solana-message", + "solana-native-token", "solana-packet 4.1.0", "solana-pubkey 4.2.0", "solana-rpc-client-api", diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index 3d0fa981b3a..671ad010a4c 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -6267,6 +6267,7 @@ version = "4.2.0-alpha.0" dependencies = [ "Inflector", "agave-reserved-account-keys", + "agave-votor-messages", "base64 0.22.1", "bitvec", "chrono", @@ -6286,8 +6287,10 @@ dependencies = [ "solana-cli-config", "solana-clock", "solana-epoch-info", + "solana-epoch-schedule", "solana-hash 4.3.0", "solana-message", + "solana-native-token", "solana-packet 4.1.0", "solana-pubkey 4.2.0", "solana-rpc-client-api", diff --git a/runtime/src/block_component_processor/vote_reward.rs b/runtime/src/block_component_processor/vote_reward.rs index 0cd732c96ff..e6a555db7b1 100644 --- a/runtime/src/block_component_processor/vote_reward.rs +++ b/runtime/src/block_component_processor/vote_reward.rs @@ -1,5 +1,6 @@ use { crate::{bank::Bank, validated_reward_certificate::ValidatedRewardCert}, + agave_votor_messages::migration::AG_MIGRATION_EPOCH_CREDIT, epoch_inflation_account_state::{EpochInflationAccountState, EpochInflationState}, log::info, solana_account::{AccountSharedData, ReadableAccount, WritableAccount}, @@ -15,11 +16,9 @@ use { thiserror::Error, }; -pub mod epoch_inflation_account_state; +pub(crate) mod epoch_inflation_account_state; mod migration_test; -pub(crate) const AG_MIGRATION_EPOCH_CREDIT: (Epoch, u64, u64) = (Epoch::MAX, u64::MAX, u64::MAX); - /// Different types of errors that can happen when calculating and paying voting reward. /// /// These errors should cause the processing of the bank to fail. diff --git a/runtime/src/inflation_rewards/points.rs b/runtime/src/inflation_rewards/points.rs index 9fb39db56f9..b8359e4146e 100644 --- a/runtime/src/inflation_rewards/points.rs +++ b/runtime/src/inflation_rewards/points.rs @@ -1,11 +1,8 @@ //! Information about points calculation based on stake state. use { - crate::{ - alpenglow_epoch_type::AlpenglowEpochType, - block_component_processor::vote_reward::AG_MIGRATION_EPOCH_CREDIT, - epoch_stakes::VersionedEpochStakes, - }, + crate::{alpenglow_epoch_type::AlpenglowEpochType, epoch_stakes::VersionedEpochStakes}, + agave_votor_messages::migration::AG_MIGRATION_EPOCH_CREDIT, log::{error, trace}, solana_clock::Epoch, solana_instruction::error::InstructionError, diff --git a/votor-messages/src/migration.rs b/votor-messages/src/migration.rs index f0db953e503..7d916c55485 100644 --- a/votor-messages/src/migration.rs +++ b/votor-messages/src/migration.rs @@ -77,6 +77,9 @@ pub const MIGRATION_SLOT_OFFSET: Slot = 5000; #[cfg(feature = "dev-context-only-utils")] pub const MIGRATION_SLOT_OFFSET: Slot = 32; +/// A marker for vote accounts' epoch credit to indicate migration from tower to alpenwlow +pub const AG_MIGRATION_EPOCH_CREDIT: (Epoch, u64, u64) = (Epoch::MAX, u64::MAX, u64::MAX); + /// We match Alpenglow's 20 + 20 model, by allowing a maximum of 20% malicious stake during the migration. pub const MIGRATION_MALICIOUS_THRESHOLD: f64 = 20.0 / 100.0; From efab54576caab099f61f012f4ea91754a1fe9bdf Mon Sep 17 00:00:00 2001 From: Dmitry Adamushko Date: Wed, 3 Jun 2026 10:39:13 +0200 Subject: [PATCH 181/576] scheduler priority floor mechanism (load-shedding via published priority floor) (#12263) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit scheduler priority floor: load-shedding low-priority transactions in sigverify Adds a saturation-detection + priority-floor mechanism to banking-stage's central scheduler. When the scheduler is saturated, it publishes a priority floor — derived from the lowest-priority transaction in its queue — to a shared atomic. Sigverify reads the floor and drops incoming transactions whose priority is at or below it, ahead of signature verification. Saturation triggers (either): - the retained scheduler buffer crosses the saturation watermark - on-capacity drops in the just-completed receive_and_buffer call Desaturation requires both the buffer below the desaturation watermark and no on-capacity drops in this tick. SaturationState clears the shared floor on Drop so a stale value doesn't outlive the controller. Floor drop in sigverify happens in each non-vote worker thread, post-dedup and pre-signature-verify. Workers parse the transaction priority from raw packet bytes using the same priority formula the scheduler queue uses, so a packet drops on the same comparison value it would face at the queue's tail. Simple votes are detected at parse time and exempted from the floor — vote priority is governed by a separate policy in banking stage. The PacketFlags::SIMPLE_VOTE_TX flag can't be used for this exemption because it's set during sigverify, which runs after the floor check. --- banking-stage-ingress-types/src/lib.rs | 41 +++- core/benches/sigverify_stage.rs | 1 + core/src/banking_simulation.rs | 3 +- core/src/banking_stage.rs | 11 +- .../scheduler_controller.rs | 182 +++++++++++++++++- .../scheduler_metrics.rs | 2 +- core/src/sigverify.rs | 82 +++++++- core/src/sigverify_stage.rs | 36 +++- core/src/tpu.rs | 8 + core/src/transaction_priority.rs | 155 ++++++++++++++- 10 files changed, 503 insertions(+), 18 deletions(-) diff --git a/banking-stage-ingress-types/src/lib.rs b/banking-stage-ingress-types/src/lib.rs index 6432b509aa6..eb8c982670d 100644 --- a/banking-stage-ingress-types/src/lib.rs +++ b/banking-stage-ingress-types/src/lib.rs @@ -1,5 +1,44 @@ #![cfg(feature = "agave-unstable-api")] -use {crossbeam_channel::Receiver, solana_perf::packet::PacketBatch, std::sync::Arc}; +use { + crossbeam_channel::Receiver, + solana_perf::packet::PacketBatch, + std::sync::{ + Arc, + atomic::{AtomicU64, Ordering}, + }, +}; pub type BankingPacketBatch = Arc>; pub type BankingPacketReceiver = Receiver; + +/// Priority floor shared from the banking-stage scheduler to sigverify. +/// +/// When saturated, the scheduler publishes the queue-min transaction's +/// priority. Sigverify drops at-or-below-floor arrivals. +/// In practice, transactions always have non-zero priorities. +#[derive(Debug)] +pub struct SchedulerPriorityFloor(AtomicU64); + +impl SchedulerPriorityFloor { + pub fn new() -> Self { + Self(AtomicU64::new(0)) + } + + pub fn set(&self, floor: u64) { + self.0.store(floor, Ordering::Relaxed); + } + + pub fn clear(&self) { + self.set(0); + } + + pub fn get(&self) -> u64 { + self.0.load(Ordering::Relaxed) + } +} + +impl Default for SchedulerPriorityFloor { + fn default() -> Self { + Self::new() + } +} diff --git a/core/benches/sigverify_stage.rs b/core/benches/sigverify_stage.rs index 76e9e41d2f0..66134d0777f 100644 --- a/core/benches/sigverify_stage.rs +++ b/core/benches/sigverify_stage.rs @@ -88,6 +88,7 @@ fn bench_sigverify_stage(bencher: &mut Bencher, use_same_tx: bool) { NonZeroUsize::new(sigverify::threadpool_for_benches().current_num_threads()).unwrap(), false, sharable_banks, + None, ); let packet_s = packet_s; let packet_s_for_bench = packet_s.clone(); diff --git a/core/src/banking_simulation.rs b/core/src/banking_simulation.rs index d1321c0442e..b0c337ab569 100644 --- a/core/src/banking_simulation.rs +++ b/core/src/banking_simulation.rs @@ -12,7 +12,7 @@ use { }, validator::BlockProductionMethod, }, - agave_banking_stage_ingress_types::BankingPacketBatch, + agave_banking_stage_ingress_types::{BankingPacketBatch, SchedulerPriorityFloor}, agave_votor_messages::migration::MigrationStatus, assert_matches::assert_matches, bincode::deserialize_from, @@ -855,6 +855,7 @@ impl BankingSimulator { bank_forks.clone(), None, Arc::default(), + Arc::new(SchedulerPriorityFloor::default()), ); let (&_slot, &raw_base_event_time) = freeze_time_by_slot diff --git a/core/src/banking_stage.rs b/core/src/banking_stage.rs index 96373d3a12e..951553a896d 100644 --- a/core/src/banking_stage.rs +++ b/core/src/banking_stage.rs @@ -18,7 +18,7 @@ use { }, validator::BlockProductionMethod, }, - agave_banking_stage_ingress_types::BankingPacketReceiver, + agave_banking_stage_ingress_types::{BankingPacketReceiver, SchedulerPriorityFloor}, crossbeam_channel::{Receiver, Sender, unbounded}, futures::{StreamExt, stream::FuturesUnordered}, histogram::Histogram, @@ -336,6 +336,7 @@ pub struct BankingStage { committer: Committer, log_messages_bytes_limit: Option, filter_keys: Arc>, + priority_floor: Arc, threads: FuturesUnordered>>, } @@ -357,6 +358,7 @@ impl BankingStage { bank_forks: Arc>, prioritization_fee_cache: Option>, filter_keys: Arc>, + priority_floor: Arc, ) -> BankingStageHandle { let committer = Committer::new( transaction_status_sender, @@ -379,6 +381,7 @@ impl BankingStage { committer, log_messages_bytes_limit, filter_keys, + priority_floor, threads: FuturesUnordered::default(), }; @@ -556,6 +559,7 @@ impl BankingStage { finished_work_receiver, GreedySchedulerConfig::default(), ); + let priority_floor = self.priority_floor.clone(); let exit = exit.clone(); let shutdown_signal = self.banking_shutdown_signal.clone(); threads.push( @@ -570,6 +574,7 @@ impl BankingStage { sharable_banks, scheduler, worker_metrics, + priority_floor, ); match scheduler_controller.run() { @@ -911,6 +916,7 @@ mod tests { bank_forks, None, Arc::default(), + Arc::new(SchedulerPriorityFloor::new()), ); drop(non_vote_sender); drop(tpu_vote_sender); @@ -972,6 +978,7 @@ mod tests { bank_forks, // keep a local-copy of bank-forks so worker threads do not lose weak access to bank-forks None, Arc::default(), + Arc::new(SchedulerPriorityFloor::new()), ); // good tx, and no verify @@ -1127,6 +1134,7 @@ mod tests { bank_forks, None, Arc::default(), + Arc::new(SchedulerPriorityFloor::new()), ); // wait for banking_stage to eat the packets @@ -1281,6 +1289,7 @@ mod tests { bank_forks, None, Arc::default(), + Arc::new(SchedulerPriorityFloor::new()), ); let keypairs = (0..100).map(|_| Keypair::new()).collect_vec(); diff --git a/core/src/banking_stage/transaction_scheduler/scheduler_controller.rs b/core/src/banking_stage/transaction_scheduler/scheduler_controller.rs index ea69e82c44f..9013121a08b 100644 --- a/core/src/banking_stage/transaction_scheduler/scheduler_controller.rs +++ b/core/src/banking_stage/transaction_scheduler/scheduler_controller.rs @@ -20,6 +20,7 @@ use { }, validator::SchedulerPacing, }, + agave_banking_stage_ingress_types::SchedulerPriorityFloor, solana_clock::DEFAULT_MS_PER_SLOT, solana_cost_model::cost_tracker::SharedBlockCost, solana_measure::measure_us, @@ -37,6 +38,13 @@ use { const CHECK_CHUNK: usize = 128; +/// Publish the priority floor once the scheduler's retained transaction buffer is +/// this full. Capacity is enforced on the buffer, not just the priority queue: +/// scheduled transactions still occupy buffer space until workers finish them. +const SATURATION_BUFFER_PCT: u8 = 99; +/// Clear the priority floor once the retained buffer drains below this watermark. +const DESATURATION_BUFFER_PCT: u8 = 95; + #[derive(Clone)] pub struct SchedulerConfig { pub scheduler_pacing: SchedulerPacing, @@ -56,6 +64,56 @@ const DEFAULT_SCHEDULER_PACING_NON_FILL_TIME_MILLIS: u64 = 50; pub(crate) const DEFAULT_SCHEDULER_PACING_FILL_TIME_MILLIS: NonZeroU64 = NonZeroU64::new(DEFAULT_MS_PER_SLOT - DEFAULT_SCHEDULER_PACING_NON_FILL_TIME_MILLIS).unwrap(); +/// Detects saturation and publishes the priority floor for dropping low-priority transactions upstream. +struct SaturationState { + priority_floor: Arc, + saturated: bool, + saturation_watermark: usize, + desaturation_watermark: usize, +} + +impl SaturationState { + fn new(priority_floor: Arc, container_capacity: usize) -> Self { + let saturation_watermark = + container_capacity.saturating_mul(SATURATION_BUFFER_PCT as usize) / 100; + let desaturation_watermark = + container_capacity.saturating_mul(DESATURATION_BUFFER_PCT as usize) / 100; + Self { + priority_floor, + saturated: false, + saturation_watermark, + desaturation_watermark, + } + } + + /// Update the saturation state. + fn update(&mut self, buffer_size: usize, num_dropped_on_capacity: usize) -> bool { + if self.saturated { + if buffer_size < self.desaturation_watermark && num_dropped_on_capacity == 0 { + self.saturated = false; + } + } else if buffer_size >= self.saturation_watermark || num_dropped_on_capacity > 0 { + self.saturated = true; + } + + self.saturated + } + + /// Publish the priority floor. + fn publish_floor(&self, floor: u64) { + self.priority_floor.set(floor); + } +} + +impl Drop for SaturationState { + fn drop(&mut self) { + // The priority floor outlives individual scheduler controllers (it's + // shared with sigverify). Clear it on tear-down so a stale floor + // from this controller can't keep sigverify dropping packets. + self.priority_floor.clear(); + } +} + /// Controls packet and transaction flow into scheduler, and scheduling execution. pub(crate) struct SchedulerController where @@ -88,6 +146,8 @@ where recheck_cursor: Option, /// Recheck IDs scratch space. recheck_chunk: Vec, + /// Saturation detection and priority floor publication. + saturation_state: SaturationState, } impl SchedulerController @@ -103,14 +163,18 @@ where sharable_banks: SharableBanks, scheduler: S, worker_metrics: Vec>, + priority_floor: Arc, ) -> Self { + priority_floor.clear(); + let container_capacity = TOTAL_BUFFERED_PACKETS; + let saturation_state = SaturationState::new(priority_floor, container_capacity); Self { exit, config, decision_maker, receive_and_buffer, sharable_banks, - container: R::Container::with_capacity(TOTAL_BUFFERED_PACKETS), + container: R::Container::with_capacity(container_capacity), scheduler, count_metrics: SchedulerCountMetrics::default(), timing_metrics: SchedulerTimingMetrics::default(), @@ -118,6 +182,7 @@ where scheduling_details: SchedulingDetails::default(), recheck_cursor: None, recheck_chunk: Vec::with_capacity(CHECK_CHUNK), + saturation_state, } } @@ -195,7 +260,7 @@ where timing_metrics.clean_time_us += clean_time_us; }); } - self.receive_and_buffer_packets(&decision).map_err(|_| { + let receiving_stats = self.receive_and_buffer_packets(&decision).map_err(|_| { SchedulerError::DisconnectedRecvChannel("receive and buffer disconnected") })?; // Report metrics only if there is data. @@ -205,6 +270,7 @@ where self.count_metrics.update(|count_metrics| { count_metrics.update_priority_stats(priority_min_max); }); + self.update_scheduler_priority_floor(receiving_stats.num_dropped_on_capacity); self.count_metrics .maybe_report_and_reset_interval(should_report); self.timing_metrics @@ -265,6 +331,27 @@ where Ok(scheduled) } + /// Update the scheduler priority floor. + /// + /// Semantics: when the retained scheduler buffer is nearly full, drop + /// arrivals that are at-or-below the current queue-min priority, i.e. no + /// better than what the bounded scheduler candidate set would evict. + fn update_scheduler_priority_floor(&mut self, num_dropped_on_capacity: usize) { + let buffer_size = self.container.buffer_size(); + let saturated = self + .saturation_state + .update(buffer_size, num_dropped_on_capacity); + let priority_floor = if saturated { + self.container + .get_min_max_priority() + .map_or(0, |(min, _)| min) + } else { + 0 + }; + + self.saturation_state.publish_floor(priority_floor); + } + /// Clears the transaction state container. /// This only clears pending transactions, and does **not** clear in-flight transactions. fn clear_container(&mut self) { @@ -547,6 +634,7 @@ mod tests { bank_forks.read().unwrap().sharable_banks(), scheduler, vec![], // no actual workers with metrics to report, this can be empty + Arc::new(SchedulerPriorityFloor::default()), ); (test_frame, scheduler_controller) @@ -995,3 +1083,93 @@ mod tests { assert_eq!(message_hashes, vec![&tx1_hash]); } } + +#[cfg(test)] +mod saturation_state_tests { + use super::*; + + fn saturation_watermark() -> usize { + TOTAL_BUFFERED_PACKETS.saturating_mul(SATURATION_BUFFER_PCT as usize) / 100 + } + + fn desaturation_watermark() -> usize { + TOTAL_BUFFERED_PACKETS.saturating_mul(DESATURATION_BUFFER_PCT as usize) / 100 + } + + fn make_state() -> (SaturationState, Arc) { + let priority_floor = Arc::new(SchedulerPriorityFloor::new()); + let state = SaturationState::new(priority_floor.clone(), TOTAL_BUFFERED_PACKETS); + (state, priority_floor) + } + + #[test] + fn starts_unsaturated() { + let (mut state, floor) = make_state(); + assert!(!state.update(0, 0)); + assert_eq!(floor.get(), 0); + } + + #[test] + fn does_not_enter_when_buffer_below_saturation_watermark() { + let (mut state, floor) = make_state(); + assert!(!state.update(saturation_watermark() - 1, 0)); + assert_eq!(floor.get(), 0); + } + + #[test] + fn enters_when_buffer_reaches_saturation_watermark() { + let (mut state, _floor) = make_state(); + assert!(state.update(saturation_watermark(), 0)); + } + + #[test] + fn enters_on_capacity_drops_even_below_watermark() { + // Direct trigger: any on-capacity drop saturates regardless of + // buffer-watermark — invariant to future changes in the trim policy. + let (mut state, _floor) = make_state(); + assert!(state.update(0, 1)); + } + + #[test] + fn publish_floor_writes_value() { + let (state, floor) = make_state(); + state.publish_floor(42); + assert_eq!(floor.get(), 42); + } + + #[test] + fn stays_saturated_at_desaturation_watermark() { + let (mut state, _floor) = make_state(); + assert!(state.update(saturation_watermark(), 0)); + assert!(state.update(desaturation_watermark(), 0)); + } + + #[test] + fn exits_when_buffer_drops_below_desaturation_watermark() { + let (mut state, _floor) = make_state(); + assert!(state.update(saturation_watermark(), 0)); + assert!(!state.update(desaturation_watermark() - 1, 0)); + } + + #[test] + fn stays_saturated_on_drops_even_below_desaturation_watermark() { + // Direct trigger keeps us saturated even if the buffer has drained, + // because drops in the same tick mean we still need backpressure. + let (mut state, _floor) = make_state(); + assert!(state.update(saturation_watermark(), 0)); + assert!(state.update(desaturation_watermark() - 1, 1)); + } + + #[test] + fn drop_clears_floor() { + // The floor outlives the controller (it's shared with sigverify), so + // tear-down must clear any stale value. + let floor = Arc::new(SchedulerPriorityFloor::new()); + { + let state = SaturationState::new(floor.clone(), TOTAL_BUFFERED_PACKETS); + state.publish_floor(123); + assert_eq!(floor.get(), 123); + } + assert_eq!(floor.get(), 0); + } +} diff --git a/core/src/banking_stage/transaction_scheduler/scheduler_metrics.rs b/core/src/banking_stage/transaction_scheduler/scheduler_metrics.rs index 3883d6868cd..e4b9862f1d6 100644 --- a/core/src/banking_stage/transaction_scheduler/scheduler_metrics.rs +++ b/core/src/banking_stage/transaction_scheduler/scheduler_metrics.rs @@ -188,7 +188,7 @@ impl SchedulerCountMetricsInner { ), ("num_dropped_on_capacity", num_dropped_on_capacity, i64), ("min_priority", self.get_min_priority(), i64), - ("max_priority", self.get_max_priority(), i64) + ("max_priority", self.get_max_priority(), i64), ); if let Some(slot) = slot { datapoint.add_field_i64("slot", slot as i64); diff --git a/core/src/sigverify.rs b/core/src/sigverify.rs index ec9f691d14f..6b3f5ace8aa 100644 --- a/core/src/sigverify.rs +++ b/core/src/sigverify.rs @@ -3,8 +3,11 @@ //! cores. use { - crate::{banking_trace::BankingPacketSender, sigverify_stage::SigVerifyServiceError}, - agave_banking_stage_ingress_types::BankingPacketBatch, + crate::{ + banking_trace::BankingPacketSender, sigverify_stage::SigVerifyServiceError, + transaction_priority::calculate_priority_from_bytes, + }, + agave_banking_stage_ingress_types::{BankingPacketBatch, SchedulerPriorityFloor}, crossbeam_channel::{Receiver, Sender, TrySendError, bounded}, solana_measure::measure_us, solana_perf::{ @@ -12,7 +15,7 @@ use { packet::PacketBatch, sigverify::{self}, }, - solana_runtime::bank_forks::SharableBanks, + solana_runtime::{bank::Bank, bank_forks::SharableBanks}, solana_transaction::Transaction, std::{ num::NonZeroUsize, @@ -47,6 +50,8 @@ pub(crate) struct SigVerifyWorkerStats { pub(crate) max_pre_send_len: Arc, /// Count of sends where the EvictingSender had to drop a batch to make room. pub(crate) eviction_drops: Arc, + pub(crate) total_dropped_below_priority_floor: Arc, + pub(crate) total_priority_floor_time_us: Arc, } #[derive(Clone)] @@ -54,6 +59,12 @@ pub(crate) struct SigVerifyWorkerState { banking_stage_sender: BankingPacketSender, deduper: Arc>, stats: SigVerifyWorkerStats, + /// Scheduler-published priority floor: when saturated, the scheduler publishes + /// the queue-min transaction's priority and workers drop at-or-below-floor + /// arrivals here, ahead of signature verification. `None` disables the + /// check (e.g. for the vote worker, which is governed by a separate + /// priority policy in banking stage). + priority_floor: Option>, } impl SigVerifyWorkerState { @@ -61,11 +72,13 @@ impl SigVerifyWorkerState { banking_stage_sender: BankingPacketSender, deduper: Arc>, stats: SigVerifyWorkerStats, + priority_floor: Option>, ) -> Self { Self { banking_stage_sender, deduper, stats, + priority_floor, } } } @@ -279,7 +292,33 @@ impl SigVerifyWorkerPool { .total_dedup_time_us .fetch_add(dedup_time_us as usize, Ordering::Relaxed); - let enable_tx_v1 = sharable_banks.working().feature_set.snapshot().enable_tx_v1; + let working_bank = sharable_banks.working(); + + if let Some(floor) = state.priority_floor.as_ref() { + let floor = floor.get(); + if floor > 0 { + let ((dropped, all_below), priority_floor_time_us) = measure_us!( + apply_priority_floor_to_batch(&mut batch, floor, &working_bank) + ); + state + .stats + .total_priority_floor_time_us + .fetch_add(priority_floor_time_us as usize, Ordering::Relaxed); + if dropped > 0 { + state + .stats + .total_dropped_below_priority_floor + .fetch_add(dropped, Ordering::Relaxed); + } + if all_below { + // Entire batch went below-floor: nothing left to verify or + // forward. + return true; + } + } + } + + let enable_tx_v1 = working_bank.feature_set.snapshot().enable_tx_v1; let (_, verify_time_us) = measure_us!(sigverify::ed25519_verify_serial( &mut batch, reject_non_vote, @@ -355,3 +394,38 @@ impl SigVerifyWorkerPool { } } } + +/// Apply the scheduler-published priority floor to a single batch in place. +/// +/// Below-floor packets are marked `discard`. Returns `(dropped, all_below)`, +/// where `dropped` is the number of packets newly marked and `all_below` is +/// true iff no useful packets remain in the batch (so the caller can skip +/// downstream work for this batch entirely). +fn apply_priority_floor_to_batch( + batch: &mut PacketBatch, + floor: u64, + bank: &Bank, +) -> (usize, bool) { + let mut dropped: usize = 0; + let mut any_kept = false; + for mut packet in batch.iter_mut() { + if packet.meta().discard() { + continue; + } + let Some(data) = packet.data(..) else { + // Zero-length or otherwise unreadable: leave to downstream + // stages to reject. + any_kept = true; + continue; + }; + // Unparseable packets are kept and left for downstream rejection. + match calculate_priority_from_bytes(bank, data) { + Some(priority) if priority <= floor => { + packet.meta_mut().set_discard(true); + dropped = dropped.saturating_add(1); + } + _ => any_kept = true, + } + } + (dropped, !any_kept) +} diff --git a/core/src/sigverify_stage.rs b/core/src/sigverify_stage.rs index 16204d335b6..d078d739767 100644 --- a/core/src/sigverify_stage.rs +++ b/core/src/sigverify_stage.rs @@ -12,7 +12,7 @@ use { SigVerifyWorkerSenders, SigVerifyWorkerState, SigVerifyWorkerStats, }, }, - agave_banking_stage_ingress_types::BankingPacketBatch, + agave_banking_stage_ingress_types::{BankingPacketBatch, SchedulerPriorityFloor}, core::time::Duration, crossbeam_channel::{Receiver, Sender, unbounded}, solana_perf::{deduper::Deduper, packet::PacketBatch}, @@ -65,6 +65,8 @@ struct SigVerifierStats { max_pre_send_len: Arc, /// Count of sends in which the EvictingSender had to drop a batch. eviction_drops: Arc, + total_dropped_below_priority_floor: Arc, + total_priority_floor_time_us: Arc, } struct ServicerState { @@ -109,14 +111,25 @@ impl SigVerifierStats { self.total_dedup_time_us.swap(0, Ordering::Relaxed), i64 ), + ( + "total_dropped_below_priority_floor", + self.total_dropped_below_priority_floor + .swap(0, Ordering::Relaxed), + i64 + ), + ( + "total_priority_floor_time_us", + self.total_priority_floor_time_us.swap(0, Ordering::Relaxed), + i64 + ), ( "total_valid_packets", - self.total_valid_packets.swap(0, Ordering::Relaxed) as i64, + self.total_valid_packets.swap(0, Ordering::Relaxed), i64 ), ( "total_verify_time_us", - self.total_verify_time_us.swap(0, Ordering::Relaxed) as i64, + self.total_verify_time_us.swap(0, Ordering::Relaxed), i64 ), ( @@ -143,6 +156,7 @@ impl SigVerifyStage { num_workers: NonZeroUsize, forward_non_votes: bool, sharable_banks: SharableBanks, + scheduler_priority_floor: Option>, ) -> (Self, GossipSigVerifyHandle) { let (gossip_verified_vote_sender, verified_vote_receiver) = unbounded(); let non_vote_stats = SigVerifierStats::default(); @@ -173,7 +187,14 @@ impl SigVerifyStage { total_verify_time_us: non_vote_stats.total_verify_time_us.clone(), max_pre_send_len: non_vote_stats.max_pre_send_len.clone(), eviction_drops: non_vote_stats.eviction_drops.clone(), + total_dropped_below_priority_floor: non_vote_stats + .total_dropped_below_priority_floor + .clone(), + total_priority_floor_time_us: non_vote_stats + .total_priority_floor_time_us + .clone(), }, + scheduler_priority_floor, ), SigVerifyWorkerState::new( tpu_vote_sender, @@ -187,7 +208,14 @@ impl SigVerifyStage { total_verify_time_us: tpu_vote_stats.total_verify_time_us.clone(), max_pre_send_len: tpu_vote_stats.max_pre_send_len.clone(), eviction_drops: tpu_vote_stats.eviction_drops.clone(), + total_dropped_below_priority_floor: tpu_vote_stats + .total_dropped_below_priority_floor + .clone(), + total_priority_floor_time_us: tpu_vote_stats + .total_priority_floor_time_us + .clone(), }, + None, // votes are not dropped for priority-floor ), ); let servicer_thread_hdl = Self::servicer( @@ -396,6 +424,7 @@ mod tests { NonZeroUsize::new(4).unwrap(), false, sharable_banks, + None, ); let now = Instant::now(); @@ -470,6 +499,7 @@ mod tests { NonZeroUsize::new(1).unwrap(), false, sharable_banks, + None, ); let mut bytes_batch = BytesPacketBatch::with_capacity(1); diff --git a/core/src/tpu.rs b/core/src/tpu.rs index 9d4c908fbd9..2fc899c9bc6 100644 --- a/core/src/tpu.rs +++ b/core/src/tpu.rs @@ -23,6 +23,7 @@ use { tpu_entry_notifier::TpuEntryNotifier, validator::{BlockProductionMethod, GeneratorConfig}, }, + agave_banking_stage_ingress_types::SchedulerPriorityFloor, agave_votor::event::VotorEventSender, agave_xdp::transmitter::XdpSender, crossbeam_channel::{Receiver, bounded, unbounded}, @@ -268,6 +269,11 @@ impl Tpu { let (forward_stage_sender, forward_stage_receiver) = bounded(50_000); + // Shared between sigverify and scheduler. The scheduler publishes + // a priority floor under saturation; sigverify reads it and drops + // below-floor packets ahead of signature verification. + let scheduler_priority_floor = Arc::new(SchedulerPriorityFloor::new()); + let (sigverify_stage, gossip_sigverify_handle) = SigVerifyStage::new( packet_receiver, vote_packet_receiver, @@ -277,6 +283,7 @@ impl Tpu { tpu_sigverify_threads, enable_block_production_forwarding, bank_forks.read().unwrap().sharable_banks(), + Some(scheduler_priority_floor.clone()), ); let cluster_info_vote_listener = ClusterInfoVoteListener::new( @@ -311,6 +318,7 @@ impl Tpu { bank_forks.clone(), prioritization_fee_cache, filter_keys, + scheduler_priority_floor, ); #[cfg(unix)] diff --git a/core/src/transaction_priority.rs b/core/src/transaction_priority.rs index e3056c184d0..6950583608b 100644 --- a/core/src/transaction_priority.rs +++ b/core/src/transaction_priority.rs @@ -1,9 +1,14 @@ use { + agave_transaction_view::transaction_view::SanitizedTransactionView, solana_cost_model::cost_model::CostModel, - solana_runtime::bank::Bank, + solana_fee::FeeFeatures, + solana_runtime::bank::{Bank, CollectorFeeDetails}, solana_runtime_transaction::{ - transaction_meta::TransactionConfiguration, transaction_with_meta::TransactionWithMeta, + runtime_transaction::RuntimeTransaction, + transaction_meta::{TransactionConfiguration, TransactionMeta}, }, + solana_svm_transaction::svm_message::SVMStaticMessage, + solana_transaction::sanitized::MessageHash, }; /// Calculate priority and cost for a transaction: @@ -24,13 +29,21 @@ use { /// from user input. They should never be zero. /// Any difference in the prioritization is negligible for /// the current transaction costs. -pub(crate) fn calculate_priority_and_cost( +pub(crate) fn calculate_priority_and_cost( bank: &Bank, - transaction: &impl TransactionWithMeta, + transaction: &Tx, transaction_configuration: &TransactionConfiguration, ) -> (u64, u64) { let cost = CostModel::calculate_cost(transaction, &bank.feature_set).sum(); - let reward = bank.calculate_reward_for_transaction(transaction, transaction_configuration); + let fee_details = solana_fee::calculate_fee_details( + transaction, + bank.fee_structure().lamports_per_signature, + transaction_configuration.priority_fee_lamports, + FeeFeatures::from(bank.feature_set.as_ref()), + ); + let reward = bank + .calculate_reward_and_burn_fee_details(&CollectorFeeDetails::from(fee_details)) + .get_deposit(); // We need a multiplier here to avoid rounding down too aggressively. // For many transactions, the cost will be greater than the fees in terms of raw lamports. @@ -45,3 +58,135 @@ pub(crate) fn calculate_priority_and_cost( cost, ) } + +/// Evaluate raw packet bytes against the pf-floor, returning the computed +/// priority. +/// +/// Returns `None` if the bytes don't parse as a valid transaction, in which +/// case the caller should leave the packet to downstream stages to reject. +pub(crate) fn calculate_priority_from_bytes(bank: &Bank, data: &[u8]) -> Option { + let enable_instruction_accounts_limit = bank.feature_set.snapshot().limit_instruction_accounts; + let view = SanitizedTransactionView::try_new_sanitized(data, enable_instruction_accounts_limit) + .ok()?; + let runtime_tx = RuntimeTransaction::>::try_new( + view, + MessageHash::Compute, + None, + ) + .ok()?; + let transaction_configuration = runtime_tx + .transaction_configuration(&bank.feature_set) + .ok()?; + let (priority, _cost) = + calculate_priority_and_cost(bank, &runtime_tx, &transaction_configuration); + + Some(priority) +} + +#[cfg(test)] +mod tests { + use { + super::*, + solana_compute_budget_interface::ComputeBudgetInstruction, + solana_hash::Hash, + solana_keypair::Keypair, + solana_ledger::genesis_utils::{GenesisConfigInfo, create_genesis_config}, + solana_message::Message, + solana_pubkey::Pubkey, + solana_signer::Signer, + solana_system_interface::instruction as system_instruction, + solana_transaction::{Transaction, versioned::VersionedTransaction}, + std::sync::Arc, + }; + + fn test_bank_with_lamports_per_signature(lamports_per_signature: u64) -> (Arc, Keypair) { + let GenesisConfigInfo { + mut genesis_config, + mint_keypair, + .. + } = create_genesis_config(u64::MAX); + if lamports_per_signature > 0 { + genesis_config.fee_rate_governor = + solana_fee_calculator::FeeRateGovernor::new(lamports_per_signature, 0); + } + let (bank, _bank_forks) = Bank::new_with_bank_forks_for_tests(&genesis_config); + (bank, mint_keypair) + } + + fn test_bank() -> (Arc, Keypair) { + test_bank_with_lamports_per_signature(0) + } + + fn make_tx_bytes(mint: &Keypair, recent_blockhash: Hash, compute_unit_price: u64) -> Vec { + let to = Pubkey::new_unique(); + let transfer = system_instruction::transfer(&mint.pubkey(), &to, 1); + let prioritization = ComputeBudgetInstruction::set_compute_unit_price(compute_unit_price); + let message = Message::new(&[transfer, prioritization], Some(&mint.pubkey())); + let tx = Transaction::new(&[mint], message, recent_blockhash); + bincode::serialize(&VersionedTransaction::from(tx)).unwrap() + } + + fn priority_from(bank: &Bank, bytes: &[u8]) -> u64 { + calculate_priority_from_bytes(bank, bytes).unwrap() + } + + #[test] + fn priority_from_bytes_returns_none_for_garbage() { + let (bank, _) = test_bank(); + assert!(calculate_priority_from_bytes(&bank, &[]).is_none()); + assert!(calculate_priority_from_bytes(&bank, &[0u8; 32]).is_none()); + } + + #[test] + fn priority_is_zero_when_base_and_priority_fees_are_zero() { + // Test bank has lamports_per_signature = 0, so base fee is 0. + // With compute_unit_price = 0, priority fee is also 0 → reward 0 → priority 0. + let (bank, mint) = test_bank(); + assert_eq!(bank.fee_structure().lamports_per_signature, 0); + let bytes = make_tx_bytes(&mint, bank.last_blockhash(), 0); + assert_eq!(priority_from(&bank, &bytes), 0); + } + + #[test] + fn higher_compute_unit_price_yields_higher_priority() { + // Need non-zero base fee, otherwise the reward short-circuits to 0 + // and all priorities collapse regardless of compute_unit_price. + let (bank, mint) = test_bank_with_lamports_per_signature(5_000); + let low = priority_from(&bank, &make_tx_bytes(&mint, bank.last_blockhash(), 1)); + let high = priority_from( + &bank, + &make_tx_bytes(&mint, bank.last_blockhash(), 1_000_000), + ); + assert!(high > low, "expected high {high} > low {low}"); + } + + #[test] + fn floor_priority_from_bytes_matches_typed_path() { + // The bytes-path and the typed-path must agree on the same packet, + // since the scheduler-side queue priority is computed via the typed + // path and the sigverify-side floor check via the bytes path. + let (bank, mint) = test_bank(); + let bytes = make_tx_bytes(&mint, bank.last_blockhash(), 100); + + let from_bytes = priority_from(&bank, &bytes); + + let view = SanitizedTransactionView::try_new_sanitized( + &bytes[..], + bank.feature_set.snapshot().limit_instruction_accounts, + ) + .unwrap(); + let runtime_tx = RuntimeTransaction::>::try_new( + view, + MessageHash::Compute, + None, + ) + .unwrap(); + let transaction_configuration = runtime_tx + .transaction_configuration(&bank.feature_set) + .unwrap(); + let (from_typed, _cost) = + calculate_priority_and_cost(&bank, &runtime_tx, &transaction_configuration); + + assert_eq!(from_bytes, from_typed); + } +} From f2ff948db3197b7442fd931d9c2dfff03af34def Mon Sep 17 00:00:00 2001 From: Joe C Date: Wed, 3 Jun 2026 18:06:31 +0800 Subject: [PATCH 182/576] svm: conformance: add vote and zk-elgamal proof program builtins (#12906) The conformance program cache only registered the always-active builtins (system, the BPF loaders, compute budget), so vote and zk-elgamal-proof fixtures failed with UnsupportedProgramId. Register both under the `conformance` feature. The zk-elgamal-proof program is registered unconditionally: its entrypoint self-gates on the disable/reenable_zk_elgamal_proof_program features (returning InvalidInstructionData when disabled), so no feature check is needed here. solana-vote-program pulls solana-bls-signatures, which brings serde_json into the test graph under `conformance`; its PartialEq impls make four unannotated account_loader test assertions ambiguous (surfaced by CI's `clippy --all-targets --features dummy-for-ci-check`), so pin those conversions to u32::from. --- Cargo.lock | 2 ++ svm/Cargo.toml | 4 ++++ svm/src/account_loader.rs | 8 ++++---- svm/src/conformance/programs.rs | 12 ++++++++++++ 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0d9847c5987..605ef93fe84 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10619,6 +10619,8 @@ dependencies = [ "solana-transaction", "solana-transaction-context", "solana-transaction-error", + "solana-vote-program", + "solana-zk-elgamal-proof-program", "spl-generic-token", "spl-token-interface", "test-case", diff --git a/svm/Cargo.toml b/svm/Cargo.toml index 53889e64cfe..68ca5b74aa5 100644 --- a/svm/Cargo.toml +++ b/svm/Cargo.toml @@ -25,6 +25,8 @@ conformance = [ "dep:bincode", "dep:prost", "dep:protosol", + "dep:solana-vote-program", + "dep:solana-zk-elgamal-proof-program", "dep:xxhash-rust", "dev-context-only-utils", "solana-instruction-error/serde", @@ -110,6 +112,8 @@ solana-system-program = { workspace = true, optional = true } solana-sysvar-id = { workspace = true } solana-transaction-context = { workspace = true } solana-transaction-error = { workspace = true } +solana-vote-program = { workspace = true, optional = true } +solana-zk-elgamal-proof-program = { workspace = true, optional = true } spl-generic-token = { workspace = true } thiserror = { workspace = true } xxhash-rust = { workspace = true, optional = true, features = ["xxh64"] } diff --git a/svm/src/account_loader.rs b/svm/src/account_loader.rs index 65b576534fa..3bc26a8512c 100644 --- a/svm/src/account_loader.rs +++ b/svm/src/account_loader.rs @@ -1155,14 +1155,14 @@ mod tests { acc.increase_calculated_data_size(data_size, &mut error_metrics) .is_ok() ); - assert_eq!(data_size as u32, acc.clone().into()); + assert_eq!(data_size as u32, u32::from(acc.clone())); // OK - loaded data size meets limit assert!( acc.increase_calculated_data_size(1, &mut error_metrics) .is_ok() ); - assert_eq!(requested_data_size_limit, acc.clone().into()); + assert_eq!(requested_data_size_limit, u32::from(acc.clone())); // fail - loading more data would exceed limit // data size helper reports the limit only @@ -1170,7 +1170,7 @@ mod tests { acc.increase_calculated_data_size(1, &mut error_metrics), Err(TransactionError::MaxLoadedAccountsDataSizeExceeded) ); - assert_eq!(requested_data_size_limit, acc.into()); + assert_eq!(requested_data_size_limit, u32::from(acc)); let mut acc = LoadedTransactionDataSize::with_max_size(requested_data_size_limit); @@ -1180,7 +1180,7 @@ mod tests { acc.increase_calculated_data_size(u32::MAX as usize + 1, &mut error_metrics), Err(TransactionError::MaxLoadedAccountsDataSizeExceeded) ); - assert_eq!(requested_data_size_limit, acc.into()); + assert_eq!(requested_data_size_limit, u32::from(acc)); } struct ValidateFeePayerTestParameter { diff --git a/svm/src/conformance/programs.rs b/svm/src/conformance/programs.rs index 63c78bd2735..d32a50ca92c 100644 --- a/svm/src/conformance/programs.rs +++ b/svm/src/conformance/programs.rs @@ -55,6 +55,18 @@ static SVM_BUILTINS: &[SvmBuiltinPrototype] = &[ name: "compute_budget_program", register_fn: solana_compute_budget_program::Entrypoint::register, }, + #[cfg(feature = "conformance")] + SvmBuiltinPrototype { + program_id: solana_vote_program::id(), + name: "vote_program", + register_fn: solana_vote_program::vote_processor::Entrypoint::register, + }, + #[cfg(feature = "conformance")] + SvmBuiltinPrototype { + program_id: solana_sdk_ids::zk_elgamal_proof_program::id(), + name: "zk_elgamal_proof_program", + register_fn: solana_zk_elgamal_proof_program::Entrypoint::register, + }, ]; fn create_keyed_account_for_builtin_program(program_id: &Pubkey, name: &str) -> (Pubkey, Account) { From 64310872533a558cbe64e1a845ac5842ceb9cbb1 Mon Sep 17 00:00:00 2001 From: Alex Pyattaev Date: Wed, 3 Jun 2026 14:14:54 +0300 Subject: [PATCH 183/576] local-cluster: decouple RPC-only cluster_tests from ConnectionCache (#12910) Functions that only poll RPC were creating a full QuicTpuClient just to call .rpc_client() on it. Replace with a plain RpcClient constructed from ContactInfo::rpc(). No behavioral change. --- local-cluster/src/cluster_tests.rs | 61 ++++++++-------------------- local-cluster/src/local_cluster.rs | 23 ++--------- local-cluster/tests/local_cluster.rs | 9 +--- 3 files changed, 20 insertions(+), 73 deletions(-) diff --git a/local-cluster/src/cluster_tests.rs b/local-cluster/src/cluster_tests.rs index 286e5357408..855dc4d5dba 100644 --- a/local-cluster/src/cluster_tests.rs +++ b/local-cluster/src/cluster_tests.rs @@ -124,12 +124,10 @@ pub fn spend_and_verify_all_nodes( pub fn verify_balances( expected_balances: HashMap, node: &ContactInfo, - connection_cache: Arc, ) { - let client = new_tpu_quic_client(node, connection_cache.clone()).unwrap(); + let rpc_client = RpcClient::new(format!("http://{}", node.rpc().unwrap())); for (pk, b) in expected_balances { - let bal = client - .rpc_client() + let bal = rpc_client .poll_get_balance_with_commitment(&pk, CommitmentConfig::processed()) .expect("balance in source"); assert_eq!(bal, b); @@ -350,20 +348,14 @@ pub fn apply_votes_to_tower(node_keypair: &Keypair, votes: Vec<(Slot, Hash)>, to tower_storage.store(&saved_tower).unwrap(); } -pub fn check_min_slot_is_rooted( - min_slot: Slot, - contact_infos: &[ContactInfo], - connection_cache: &Arc, - test_name: &str, -) { +pub fn check_min_slot_is_rooted(min_slot: Slot, contact_infos: &[ContactInfo], test_name: &str) { let mut last_print = Instant::now(); let loop_start = Instant::now(); let loop_timeout = Duration::from_secs(180); for ingress_node in contact_infos.iter() { - let client = new_tpu_quic_client(ingress_node, connection_cache.clone()).unwrap(); + let rpc_client = RpcClient::new(format!("http://{}", ingress_node.rpc().unwrap())); loop { - let root_slot = client - .rpc_client() + let root_slot = rpc_client .get_slot_with_commitment(CommitmentConfig::finalized()) .unwrap_or(0); if root_slot >= min_slot || last_print.elapsed().as_secs() > 3 { @@ -388,7 +380,6 @@ pub fn check_min_slot_is_rooted( fn check_for_new_slots_with_commitment( num_new_slots: usize, contact_infos: &[ContactInfo], - connection_cache: &Arc, test_name: &str, commitment: CommitmentConfig, ) { @@ -402,11 +393,8 @@ fn check_for_new_slots_with_commitment( assert!(loop_start.elapsed() < loop_timeout); for (i, ingress_node) in contact_infos.iter().enumerate() { - let client = new_tpu_quic_client(ingress_node, connection_cache.clone()).unwrap(); - let slot = client - .rpc_client() - .get_slot_with_commitment(commitment) - .unwrap_or(0); + let rpc_client = RpcClient::new(format!("http://{}", ingress_node.rpc().unwrap())); + let slot = rpc_client.get_slot_with_commitment(commitment).unwrap_or(0); slots[i].insert(slot); num_slots_map.insert(*ingress_node.pubkey(), slots[i].len()); let num_slots = slots.iter().map(|r| r.len()).min().unwrap(); @@ -424,16 +412,10 @@ fn check_for_new_slots_with_commitment( } } -pub fn check_for_new_roots( - num_new_roots: usize, - contact_infos: &[ContactInfo], - connection_cache: &Arc, - test_name: &str, -) { +pub fn check_for_new_roots(num_new_roots: usize, contact_infos: &[ContactInfo], test_name: &str) { check_for_new_slots_with_commitment( num_new_roots, contact_infos, - connection_cache, test_name, CommitmentConfig::finalized(), ); @@ -442,13 +424,11 @@ pub fn check_for_new_roots( pub fn check_for_new_processed( num_new_processed: usize, contact_infos: &[ContactInfo], - connection_cache: &Arc, test_name: &str, ) { check_for_new_slots_with_commitment( num_new_processed, contact_infos, - connection_cache, test_name, CommitmentConfig::processed(), ); @@ -495,7 +475,6 @@ pub fn start_quic_streamer_to_listen_for_votes_and_certs( pub fn check_for_new_notarized_votes( num_new_votes: usize, contact_infos: &[ContactInfo], - connection_cache: &Arc, test_name: &str, vote_listener_socket: UdpSocket, validator_keys: &[Arc], @@ -507,10 +486,7 @@ pub fn check_for_new_notarized_votes( let Some(current_slot) = contact_infos .iter() .map(|ingress_node| { - let client = new_tpu_quic_client(ingress_node, connection_cache.clone()).unwrap(); - - client - .rpc_client() + RpcClient::new(format!("http://{}", ingress_node.rpc().unwrap())) .get_slot_with_commitment(CommitmentConfig::processed()) .unwrap_or(0) }) @@ -593,7 +569,6 @@ pub fn check_for_new_notarized_votes( pub fn check_no_new_roots( num_slots_to_wait: usize, contact_infos: &[&ContactInfo], - connection_cache: &Arc, test_name: &str, ) { assert!(!contact_infos.is_empty()); @@ -602,14 +577,12 @@ pub fn check_no_new_roots( .iter() .enumerate() .map(|(i, ingress_node)| { - let client = new_tpu_quic_client(ingress_node, connection_cache.clone()).unwrap(); - let initial_root = client - .rpc_client() + let rpc_client = RpcClient::new(format!("http://{}", ingress_node.rpc().unwrap())); + let initial_root = rpc_client .get_slot() .unwrap_or_else(|_| panic!("get_slot for {} failed", ingress_node.pubkey())); roots[i] = initial_root; - client - .rpc_client() + rpc_client .get_slot_with_commitment(CommitmentConfig::processed()) .unwrap_or_else(|_| panic!("get_slot for {} failed", ingress_node.pubkey())) }) @@ -622,9 +595,8 @@ pub fn check_no_new_roots( let mut reached_end_slot = false; loop { for contact_info in contact_infos { - let client = new_tpu_quic_client(contact_info, connection_cache.clone()).unwrap(); - current_slot = client - .rpc_client() + let rpc_client = RpcClient::new(format!("http://{}", contact_info.rpc().unwrap())); + current_slot = rpc_client .get_slot_with_commitment(CommitmentConfig::processed()) .unwrap_or_else(|_| panic!("get_slot for {} failed", contact_infos[0].pubkey())); if current_slot > end_slot { @@ -648,10 +620,9 @@ pub fn check_no_new_roots( } for (i, ingress_node) in contact_infos.iter().enumerate() { - let client = new_tpu_quic_client(ingress_node, connection_cache.clone()).unwrap(); + let rpc_client = RpcClient::new(format!("http://{}", ingress_node.rpc().unwrap())); assert_eq!( - client - .rpc_client() + rpc_client .get_slot() .unwrap_or_else(|_| panic!("get_slot for {} failed", ingress_node.pubkey())), roots[i] diff --git a/local-cluster/src/local_cluster.rs b/local-cluster/src/local_cluster.rs index 543e2ce99a9..e0bf90f1628 100644 --- a/local-cluster/src/local_cluster.rs +++ b/local-cluster/src/local_cluster.rs @@ -874,12 +874,7 @@ impl LocalCluster { ) { let alive_node_contact_infos = self.discover_nodes(socket_addr_space, test_name); info!("{test_name} looking minimum root {min_root} on all nodes"); - cluster_tests::check_min_slot_is_rooted( - min_root, - &alive_node_contact_infos, - &self.connection_cache, - test_name, - ); + cluster_tests::check_min_slot_is_rooted(min_root, &alive_node_contact_infos, test_name); info!("{test_name} done waiting for roots"); } @@ -891,12 +886,7 @@ impl LocalCluster { ) { let alive_node_contact_infos = self.discover_nodes(socket_addr_space, test_name); info!("{test_name} looking for new roots on all nodes"); - cluster_tests::check_for_new_roots( - num_new_roots, - &alive_node_contact_infos, - &self.connection_cache, - test_name, - ); + cluster_tests::check_for_new_roots(num_new_roots, &alive_node_contact_infos, test_name); info!("{test_name} done waiting for roots"); } @@ -911,7 +901,6 @@ impl LocalCluster { cluster_tests::check_for_new_processed( num_new_processed, &alive_node_contact_infos, - &self.connection_cache, test_name, ); info!("{test_name} done waiting for processed slots"); @@ -931,7 +920,6 @@ impl LocalCluster { cluster_tests::check_for_new_notarized_votes( num_new_notarized_votes, &alive_node_contact_infos, - &self.connection_cache, test_name, vote_listener_addr, validator_keys, @@ -962,12 +950,7 @@ impl LocalCluster { .unwrap(); info!("{} discovered {} nodes", test_name, cluster_nodes.len()); info!("{test_name} making sure no new roots on any nodes"); - cluster_tests::check_no_new_roots( - num_slots_to_wait, - &alive_node_contact_infos, - &self.connection_cache, - test_name, - ); + cluster_tests::check_no_new_roots(num_slots_to_wait, &alive_node_contact_infos, test_name); info!("{test_name} done waiting for roots"); } diff --git a/local-cluster/tests/local_cluster.rs b/local-cluster/tests/local_cluster.rs index f394e9a5c0a..2b82b8a0391 100644 --- a/local-cluster/tests/local_cluster.rs +++ b/local-cluster/tests/local_cluster.rs @@ -1467,11 +1467,7 @@ fn test_snapshots_restart_validity() { // Verify account balances on validator trace!("Verifying balances"); - cluster_tests::verify_balances( - expected_balances.clone(), - &cluster.entry_point_info, - cluster.connection_cache.clone(), - ); + cluster_tests::verify_balances(expected_balances.clone(), &cluster.entry_point_info); // Check that we can still push transactions trace!("Spending and verifying"); @@ -1758,7 +1754,6 @@ fn test_optimistic_confirmation_violation_detection() { cluster_tests::check_for_new_roots( 16, &[cluster.get_contact_info(&node_to_restart).unwrap().clone()], - &cluster.connection_cache, "test_optimistic_confirmation_violation", ); } @@ -4504,7 +4499,6 @@ fn test_slot_hash_expiry() { cluster_tests::check_for_new_roots( 16, &[cluster.get_contact_info(&a_pubkey).unwrap().clone()], - &cluster.connection_cache, "test_slot_hashes_expiry", ); } @@ -4775,7 +4769,6 @@ fn test_duplicate_with_pruned_ancestor() { cluster_tests::check_for_new_roots( 16, &[cluster.get_contact_info(&our_node_pubkey).unwrap().clone()], - &cluster.connection_cache, "test_duplicate_with_pruned_ancestor", ); } From 327409570b337dbc79aab6b11adf68af0b5f6938 Mon Sep 17 00:00:00 2001 From: Kamil Skalski Date: Wed, 3 Jun 2026 15:32:03 +0200 Subject: [PATCH 184/576] chore(deps): bump solana-frozen-abi-* to 3.5.0 (#12915) Bump deps --- Cargo.lock | 8 ++++---- Cargo.toml | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 605ef93fe84..1aaa7cc3533 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8287,9 +8287,9 @@ dependencies = [ [[package]] name = "solana-frozen-abi" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5b2792cc4cd4eaa419104dac4bed8e57b3189de001f9139b04f84b1a6f12d4a" +checksum = "e6fc5ab0701253517fdc7de93f965988cce05e52ac494f9c11e1f46742b2c346" dependencies = [ "bincode", "boxcar", @@ -8314,9 +8314,9 @@ dependencies = [ [[package]] name = "solana-frozen-abi-macro" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4efd54c3f198403955addf2160af0ccc30294a04cf34e155c775d69960e1f0f6" +checksum = "9385c5dc5555c010a96504d80f47be914ec4637a320d99b2e26d964ecd32691a" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index a1744628870..2fbf0c60c9f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -392,8 +392,8 @@ solana-fee = { path = "fee", version = "=4.2.0-alpha.0", features = ["agave-unst solana-fee-calculator = "3.2.0" solana-fee-structure = "3.0.0" solana-file-download = "3.1.4" -solana-frozen-abi = "3.4.0" -solana-frozen-abi-macro = "3.3.0" +solana-frozen-abi = "3.5.0" +solana-frozen-abi-macro = "3.5.0" solana-genesis = { path = "genesis", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-genesis-config = "4.0.0" solana-genesis-utils = { path = "genesis-utils", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } From 4cd535ce52ac46892de4a964f943af1e4bd12d51 Mon Sep 17 00:00:00 2001 From: Yihau Chen Date: Wed, 3 Jun 2026 21:50:37 +0800 Subject: [PATCH 185/576] test(xtask): add piepline flag tests (#12917) --- ci/xtask/src/commands/generate_pipeline.rs | 88 ++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/ci/xtask/src/commands/generate_pipeline.rs b/ci/xtask/src/commands/generate_pipeline.rs index 50820d2c8ab..f3a9853de7e 100644 --- a/ci/xtask/src/commands/generate_pipeline.rs +++ b/ci/xtask/src/commands/generate_pipeline.rs @@ -762,4 +762,92 @@ mod tests { assert!(changed_files.contains(&String::from("rbpf/tests/elfs/syscall_static.so"))); assert!(changed_files.contains(&String::from("rbpf/tests/elfs/syscalls.rs"))); } + + fn flags(files: &[&str]) -> PullRequestPipelineFlags { + let owned: Vec = files.iter().map(|s| s.to_string()).collect(); + PullRequestPipelineFlags::from_changed_files(&owned) + } + + #[test] + fn test_readme_triggers_nothing() { + let f = flags(&["README.md"]); + assert!(!f.shellcheck); + assert!(!f.checks); + assert!(!f.feature_check); + assert!(!f.miri); + assert!(!f.frozen_abi); + assert!(!f.stable); + assert!(!f.local_cluster); + assert!(!f.docs); + assert!(!f.localnet); + assert!(!f.stable_sbf); + assert!(!f.shuttle); + assert!(!f.coverage); + } + + #[test] + fn test_docker_change_triggers_all() { + let f = flags(&["ci/docker/Dockerfile"]); + assert!(f.checks); + assert!(f.feature_check); + assert!(f.miri); + assert!(f.frozen_abi); + assert!(f.stable); + assert!(f.local_cluster); + assert!(f.docs); + assert!(f.localnet); + assert!(f.stable_sbf); + assert!(f.shuttle); + assert!(f.coverage); + } + + #[test] + fn test_rust_change_triggers_all() { + let f = flags(&["core/src/lib.rs"]); + assert!(f.checks); + assert!(f.feature_check); + assert!(f.miri); + assert!(f.frozen_abi); + assert!(f.stable); + assert!(f.local_cluster); + assert!(f.docs); + assert!(f.localnet); + assert!(f.stable_sbf); + assert!(f.shuttle); + assert!(f.coverage); + } + + #[test] + fn test_unimportant_shell_triggers_shellcheck_only() { + let f = flags(&["some/random/script.sh"]); + assert!(f.shellcheck); + assert!(!f.checks); + assert!(!f.feature_check); + assert!(!f.miri); + assert!(!f.frozen_abi); + assert!(!f.stable); + assert!(!f.local_cluster); + assert!(!f.docs); + assert!(!f.localnet); + assert!(!f.stable_sbf); + assert!(!f.shuttle); + assert!(!f.coverage); + } + + #[test] + fn test_test_docs_sh_triggers_docs_only() { + let f = flags(&["ci/test-docs.sh"]); + assert!(f.shellcheck); + assert!(f.docs); + assert!(!f.checks); + assert!(!f.feature_check); + assert!(!f.miri); + assert!(!f.frozen_abi); + assert!(!f.stable); + assert!(!f.local_cluster); + assert!(!f.localnet); + assert!(!f.stable_sbf); + assert!(!f.shuttle); + assert!(!f.coverage); + } } From 14567ff9caf5e8a361cc4531cc27046e283f47ee Mon Sep 17 00:00:00 2001 From: Akhilesh Singhania Date: Wed, 3 Jun 2026 17:54:33 +0200 Subject: [PATCH 186/576] renames entry types to distinguish from certs (#12903) --- core/src/block_creation_loop.rs | 48 ++++---- core/src/replay_stage/tests.rs | 14 +-- entry/src/block_component.rs | 108 +++++++++--------- ledger/src/blockstore/tests.rs | 8 +- ledger/src/blockstore_processor.rs | 6 +- poh/src/poh_recorder.rs | 8 +- runtime/src/block_component_processor.rs | 66 ++++++----- runtime/src/validated_block_finalization.rs | 44 +++---- .../src/broadcast_stage/broadcast_utils.rs | 6 +- .../broadcast_stage/standard_broadcast_run.rs | 2 +- 10 files changed, 159 insertions(+), 151 deletions(-) diff --git a/core/src/block_creation_loop.rs b/core/src/block_creation_loop.rs index 60bfb623983..49ca052b762 100644 --- a/core/src/block_creation_loop.rs +++ b/core/src/block_creation_loop.rs @@ -22,7 +22,7 @@ use { crossbeam_channel::{Receiver, Sender, select_biased}, solana_clock::Slot, solana_entry::block_component::{ - BlockFooterV1, GenesisCertificate, UpdateParentV1, VersionedBlockMarker, + BlockFooterV1, GenesisCertBlockMarker, UpdateParentV1, VersionedBlockMarker, }, solana_gossip::cluster_info::ClusterInfo, solana_hash::Hash, @@ -167,7 +167,7 @@ struct LeaderContext { slot_metrics: SlotMetrics, // Migration information - genesis_cert: GenesisCertificate, + genesis_cert_block_marker: GenesisCertBlockMarker, } #[derive(Default)] @@ -266,7 +266,7 @@ fn start_loop(config: BlockCreationLoopConfig, reward_certs_requestor: CertsRequ .migration_status() .genesis_certificate() .expect("Migration complete, genesis certificate must exist"); - let genesis_cert = GenesisCertificate::try_from((*genesis_cert).clone()) + let genesis_cert_block_marker = GenesisCertBlockMarker::try_from((*genesis_cert).clone()) .expect("Genesis certificate must be valid"); info!("{my_pubkey}: PohService has shutdown, BlockCreationLoop is enabled"); @@ -292,7 +292,7 @@ fn start_loop(config: BlockCreationLoopConfig, reward_certs_requestor: CertsRequ metrics: LoopMetrics::default(), slot_metrics: SlotMetrics::default(), highest_finalized, - genesis_cert, + genesis_cert_block_marker, }; // Setup poh @@ -533,13 +533,14 @@ fn produce_block_footer( } // Convert finalization certs into block marker - let final_cert = highest_finalized.map(ValidatedBlockFinalizationCert::to_final_certificate); + let block_final_cert = + highest_finalized.map(ValidatedBlockFinalizationCert::to_block_final_cert); BlockFooterV1 { bank_hash: Hash::default(), block_producer_time_nanos: block_producer_time_nanos as u64, block_user_agent: format!("agave/{}", version!()).into_bytes(), - final_cert, + block_final_cert, skip_reward_cert, notar_reward_cert, } @@ -874,7 +875,7 @@ fn send_update_parent( new_parent_slot, new_parent_block_id, }; - let marker = VersionedBlockMarker::new_update_parent(update_parent); + let marker = VersionedBlockMarker::from_update_parent(update_parent); poh_recorder.write().unwrap().send_marker(marker)?; Ok(()) } @@ -1268,7 +1269,7 @@ fn create_and_insert_leader_bank( // If this is the first alpenglow block, set PoH to low power mode. // This is persisted in the recorder when we set bank. // Replayers will update hashes_per_tick when they process the genesis certificate block marker. - if should_include_genesis_certificate(parent_slot, &ctx.genesis_cert) { + if should_include_genesis_certificate(parent_slot, &ctx.genesis_cert_block_marker) { tpu_bank.set_hashes_per_tick(None); } @@ -1305,22 +1306,23 @@ fn maybe_include_genesis_certificate( parent_slot: Slot, ctx: &LeaderContext, ) -> Result<(), PohRecorderError> { - if !should_include_genesis_certificate(parent_slot, &ctx.genesis_cert) { + if !should_include_genesis_certificate(parent_slot, &ctx.genesis_cert_block_marker) { return Ok(()); } // Send the genesis certificate - let genesis_marker = VersionedBlockMarker::new_genesis_certificate(ctx.genesis_cert.clone()); + let block_marker = + VersionedBlockMarker::from_genesis_cert_block_marker(ctx.genesis_cert_block_marker.clone()); let mut poh_recorder = ctx.poh_recorder.write().unwrap(); - poh_recorder.send_marker(genesis_marker)?; + poh_recorder.send_marker(block_marker)?; // Process the genesis certificate let bank = poh_recorder.bank().expect("Bank cannot have been cleared"); let processor = bank.block_component_processor.read().unwrap(); processor - .on_genesis_certificate( + .on_genesis_cert_block_marker( bank.clone(), - ctx.genesis_cert.clone(), + ctx.genesis_cert_block_marker.clone(), &ctx.bank_forks.read().unwrap().migration_status(), ) .expect("Recording genesis certificate should not fail"); @@ -1329,9 +1331,9 @@ fn maybe_include_genesis_certificate( fn should_include_genesis_certificate( parent_slot: Slot, - genesis_cert: &GenesisCertificate, + genesis_cert_block_marker: &GenesisCertBlockMarker, ) -> bool { - parent_slot == genesis_cert.slot && parent_slot != 0 + parent_slot == genesis_cert_block_marker.slot && parent_slot != 0 } #[cfg(test)] @@ -1369,8 +1371,8 @@ mod tests { )) } - fn test_genesis_certificate() -> GenesisCertificate { - GenesisCertificate { + fn test_genesis_cert_block_marker() -> GenesisCertBlockMarker { + GenesisCertBlockMarker { slot: Slot::MAX, block_id: Hash::default(), bls_signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), @@ -1557,7 +1559,7 @@ mod tests { banking_stage_sender, metrics: LoopMetrics::default(), slot_metrics: SlotMetrics::default(), - genesis_cert: test_genesis_certificate(), + genesis_cert_block_marker: test_genesis_cert_block_marker(), }; create_and_insert_leader_bank(1, root_bank.clone(), &mut ctx).unwrap(); @@ -1643,8 +1645,8 @@ mod tests { let (_record_sender, record_receiver) = record_channels(false); let (_leader_window_info_sender, leader_window_info_receiver) = unbounded(); let (banking_stage_sender, _banking_stage_receiver) = BankingTracer::channel_for_test(); - let mut genesis_cert = test_genesis_certificate(); - genesis_cert.slot = parent_bank.slot(); + let mut genesis_cert_block_marker = test_genesis_cert_block_marker(); + genesis_cert_block_marker.slot = parent_bank.slot(); let bank_forks_controller = test_bank_forks_controller(bank_forks.clone()); let (reward_certs_requestor, _receiver) = CertsRequestor::new(); @@ -1669,7 +1671,7 @@ mod tests { banking_stage_sender, metrics: LoopMetrics::default(), slot_metrics: SlotMetrics::default(), - genesis_cert, + genesis_cert_block_marker, }; let err = create_and_insert_leader_bank(2, parent_bank, &mut ctx).unwrap_err(); @@ -1738,7 +1740,7 @@ mod tests { banking_stage_sender, metrics: LoopMetrics::default(), slot_metrics: SlotMetrics::default(), - genesis_cert: test_genesis_certificate(), + genesis_cert_block_marker: test_genesis_cert_block_marker(), }; create_and_insert_leader_bank(1, root_bank, &mut ctx).unwrap(); @@ -1841,7 +1843,7 @@ mod tests { banking_stage_sender, metrics: LoopMetrics::default(), slot_metrics: SlotMetrics::default(), - genesis_cert: test_genesis_certificate(), + genesis_cert_block_marker: test_genesis_cert_block_marker(), }; let leader_slot = 4; diff --git a/core/src/replay_stage/tests.rs b/core/src/replay_stage/tests.rs index 1acb582e5a9..c3d89517c43 100644 --- a/core/src/replay_stage/tests.rs +++ b/core/src/replay_stage/tests.rs @@ -185,11 +185,11 @@ fn insert_update_parent_slot( update_parent_block_id: Hash, replay_fec_set_index: u32, ) { - let header = VersionedBlockMarker::new_block_header(BlockHeaderV1 { + let header = VersionedBlockMarker::from_block_header(BlockHeaderV1 { parent_slot: block_header_parent_slot, parent_block_id: Hash::new_unique(), }); - let update_parent = VersionedBlockMarker::new_update_parent(UpdateParentV1 { + let update_parent = VersionedBlockMarker::from_update_parent(UpdateParentV1 { new_parent_slot: update_parent_slot, new_parent_block_id: update_parent_block_id, }); @@ -2895,16 +2895,16 @@ fn test_headerless_update_parent() { let migration_status = post_migration_status_for_tests(); let footer_marker = || { - VersionedBlockMarker::new_block_footer(BlockFooterV1 { + VersionedBlockMarker::from_block_footer(BlockFooterV1 { bank_hash: Hash::new_unique(), block_producer_time_nanos: 0, block_user_agent: vec![], - final_cert: None, + block_final_cert: None, skip_reward_cert: None, notar_reward_cert: None, }) }; - let update_parent = VersionedBlockMarker::new_update_parent(UpdateParentV1 { + let update_parent = VersionedBlockMarker::from_update_parent(UpdateParentV1 { new_parent_slot: 0, new_parent_block_id: Hash::default(), }); @@ -3220,11 +3220,11 @@ fn test_before_update_soft_dead() { let bank0 = bank_forks.read().unwrap().get(0).unwrap(); let bank = Bank::new_from_parent(bank0, SlotLeader::default(), slot); let bank = bank_forks.write().unwrap().insert(bank); - let header = VersionedBlockMarker::new_block_header(BlockHeaderV1 { + let header = VersionedBlockMarker::from_block_header(BlockHeaderV1 { parent_slot: 0, parent_block_id: Hash::default(), }); - let update_parent = VersionedBlockMarker::new_update_parent(UpdateParentV1 { + let update_parent = VersionedBlockMarker::from_update_parent(UpdateParentV1 { new_parent_slot: 0, new_parent_block_id: Hash::default(), }); diff --git a/entry/src/block_component.rs b/entry/src/block_component.rs index 7e489bfb13f..bca55a920c4 100644 --- a/entry/src/block_component.rs +++ b/entry/src/block_component.rs @@ -66,30 +66,30 @@ /// /// ### BlockFooterV1 Layout /// ```text -/// ┌─────────────────────────────────────────┐ -/// │ Bank Hash (32 bytes) │ -/// ├─────────────────────────────────────────┤ -/// │ Producer Time Nanos (8 bytes) │ -/// ├─────────────────────────────────────────┤ -/// │ User Agent Length (1 byte) │ -/// ├─────────────────────────────────────────┤ -/// │ User Agent Bytes (0-255 bytes) │ -/// ├─────────────────────────────────────────┤ -/// │ Final Cert Present (1 byte) │ -/// ├─────────────────────────────────────────┤ -/// │ FinalCertificate (if present, variable) │ -/// ├─────────────────────────────────────────┤ -/// │ Skip reward cert Present (1 byte) │ -/// ├─────────────────────────────────────────┤ -/// │ SkipRewardCert (if present, variable) │ -/// ├─────────────────────────────────────────┤ -/// │ Notar reward cert Present (1 byte) │ -/// ├─────────────────────────────────────────┤ -/// │ NotarRewardCert (if present, variable) │ -/// └─────────────────────────────────────────┘ +/// ┌──────────────────────────────────────────────┐ +/// │ Bank Hash (32 bytes) │ +/// ├──────────────────────────────────────────────┤ +/// │ Producer Time Nanos (8 bytes) │ +/// ├──────────────────────────────────────────────┤ +/// │ User Agent Length (1 byte) │ +/// ├──────────────────────────────────────────────┤ +/// │ User Agent Bytes (0-255 bytes) │ +/// ├──────────────────────────────────────────────┤ +/// │ Block Final Cert Present (1 byte) │ +/// ├──────────────────────────────────────────────┤ +/// │ BlockFinalizationCert (if present, variable) │ +/// ├──────────────────────────────────────────────┤ +/// │ Skip reward cert Present (1 byte) │ +/// ├──────────────────────────────────────────────┤ +/// │ SkipRewardCert (if present, variable) │ +/// ├──────────────────────────────────────────────┤ +/// │ Notar reward cert Present (1 byte) │ +/// ├──────────────────────────────────────────────┤ +/// │ NotarRewardCert (if present, variable) │ +/// └──────────────────────────────────────────────┘ /// ``` /// -/// ### FinalCertificate Layout +/// ### BlockFinalizationCert Layout /// ```text /// ┌─────────────────────────────────────────┐ /// │ Slot (8 bytes) │ @@ -210,7 +210,7 @@ pub struct BlockFooterV1 { pub block_producer_time_nanos: u64, #[wincode(with = "WincodeVec>")] pub block_user_agent: Vec, - pub final_cert: Option, + pub block_final_cert: Option, pub skip_reward_cert: Option, pub notar_reward_cert: Option, } @@ -229,7 +229,7 @@ pub struct UpdateParentV1 { /// Attests to genesis block finalization with a BLS aggregate signature. #[derive(Clone, PartialEq, Eq, Debug, SchemaWrite, SchemaRead)] -pub struct GenesisCertificate { +pub struct GenesisCertBlockMarker { pub slot: Slot, pub block_id: Hash, #[wincode(with = "PodBLSSignature")] @@ -238,12 +238,12 @@ pub struct GenesisCertificate { pub bitmap: Vec, } -impl GenesisCertificate { +impl GenesisCertBlockMarker { /// Max bitmap size in bytes (supports up to 4096 validators). pub const MAX_BITMAP_SIZE: usize = 512; } -impl TryFrom for GenesisCertificate { +impl TryFrom for GenesisCertBlockMarker { type Error = String; fn try_from(cert: Certificate) -> Result { @@ -266,8 +266,8 @@ impl TryFrom for GenesisCertificate { } } -impl From for Certificate { - fn from(cert: GenesisCertificate) -> Self { +impl From for Certificate { + fn from(cert: GenesisCertBlockMarker) -> Self { Self { cert_type: CertificateType::Genesis(cert.slot, cert.block_id), signature: cert.bls_signature, @@ -277,17 +277,17 @@ impl From for Certificate { } #[derive(Clone, PartialEq, Eq, Debug, SchemaWrite, SchemaRead)] -pub struct FinalCertificate { +pub struct BlockFinalizationCert { pub slot: Slot, pub block_id: Hash, pub final_aggregate: VotesAggregate, pub notar_aggregate: Option, } -impl FinalCertificate { +impl BlockFinalizationCert { #[cfg(feature = "dev-context-only-utils")] - pub fn new_for_tests() -> FinalCertificate { - FinalCertificate { + pub fn new_for_tests() -> BlockFinalizationCert { + BlockFinalizationCert { slot: 1234567890, block_id: Hash::new_from_array([1u8; 32]), final_aggregate: VotesAggregate { @@ -363,7 +363,7 @@ pub enum BlockMarkerV1 { BlockFooter(LengthPrefixed), BlockHeader(LengthPrefixed), UpdateParent(LengthPrefixed), - GenesisCertificate(LengthPrefixed), + GenesisCertificate(LengthPrefixed), } impl BlockMarkerV1 { @@ -379,7 +379,7 @@ impl BlockMarkerV1 { Self::UpdateParent(LengthPrefixed::new(u)) } - pub fn new_genesis_certificate(c: GenesisCertificate) -> Self { + pub fn new_genesis_certificate(c: GenesisCertBlockMarker) -> Self { Self::GenesisCertificate(LengthPrefixed::new(c)) } @@ -404,7 +404,7 @@ impl BlockMarkerV1 { } } - pub fn as_genesis_certificate(&self) -> Option<&GenesisCertificate> { + pub fn as_genesis_certificate(&self) -> Option<&GenesisCertBlockMarker> { match self { Self::GenesisCertificate(lp) => Some(lp.inner()), _ => None, @@ -424,40 +424,40 @@ impl VersionedBlockMarker { Self::V1(marker) } - pub fn new_block_footer(f: BlockFooterV1) -> Self { + pub fn from_block_footer(f: BlockFooterV1) -> Self { let f = VersionedBlockFooter::V1(f); let f = BlockMarkerV1::BlockFooter(LengthPrefixed::new(f)); - VersionedBlockMarker::V1(f) + Self::new(f) } - pub fn new_block_header(h: BlockHeaderV1) -> Self { + pub fn from_block_header(h: BlockHeaderV1) -> Self { let h = VersionedBlockHeader::V1(h); let h = BlockMarkerV1::BlockHeader(LengthPrefixed::new(h)); - VersionedBlockMarker::V1(h) + Self::new(h) } - pub fn new_update_parent(u: UpdateParentV1) -> Self { + pub fn from_update_parent(u: UpdateParentV1) -> Self { let u = VersionedUpdateParent::V1(u); let u = BlockMarkerV1::UpdateParent(LengthPrefixed::new(u)); - VersionedBlockMarker::V1(u) + Self::new(u) } - pub fn new_genesis_certificate(g: GenesisCertificate) -> Self { + pub fn from_genesis_cert_block_marker(g: GenesisCertBlockMarker) -> Self { let g = BlockMarkerV1::GenesisCertificate(LengthPrefixed::new(g)); - VersionedBlockMarker::V1(g) + Self::new(g) } pub fn is_update_parent(&self) -> bool { match self { - VersionedBlockMarker::V1(BlockMarkerV1::UpdateParent(_)) => true, - VersionedBlockMarker::V1(_) => false, + Self::V1(BlockMarkerV1::UpdateParent(_)) => true, + Self::V1(_) => false, } } pub fn is_footer(&self) -> bool { match self { - VersionedBlockMarker::V1(BlockMarkerV1::BlockFooter(_)) => true, - VersionedBlockMarker::V1(_) => false, + Self::V1(BlockMarkerV1::BlockFooter(_)) => true, + Self::V1(_) => false, } } } @@ -497,7 +497,7 @@ impl BlockComponent { parent_slot, parent_block_id, }; - Self::new_block_marker(VersionedBlockMarker::new_block_header(header)) + Self::new_block_marker(VersionedBlockMarker::from_block_header(header)) } pub const fn as_marker(&self) -> Option<&VersionedBlockMarker> { @@ -584,7 +584,7 @@ mod tests { bank_hash: Hash::new_unique(), block_producer_time_nanos: 1234567890, block_user_agent: b"test-agent".to_vec(), - final_cert: Some(FinalCertificate::new_for_tests()), + block_final_cert: Some(BlockFinalizationCert::new_for_tests()), skip_reward_cert: None, notar_reward_cert: None, } @@ -609,19 +609,19 @@ mod tests { wincode::deserialize::(&bytes).unwrap() ); - let cert = GenesisCertificate { + let marker = GenesisCertBlockMarker { slot: 999, block_id: Hash::new_unique(), bls_signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), bitmap: vec![1, 2, 3], }; - let bytes = wincode::serialize(&cert).unwrap(); + let bytes = wincode::serialize(&marker).unwrap(); assert_eq!( - cert, - wincode::deserialize::(&bytes).unwrap() + marker, + wincode::deserialize::(&bytes).unwrap() ); - let marker = VersionedBlockMarker::new_block_footer(footer.clone()); + let marker = VersionedBlockMarker::from_block_footer(footer.clone()); let bytes = wincode::serialize(&marker).unwrap(); assert_eq!( marker, diff --git a/ledger/src/blockstore/tests.rs b/ledger/src/blockstore/tests.rs index 73074e9f55e..9fabc0d40a7 100644 --- a/ledger/src/blockstore/tests.rs +++ b/ledger/src/blockstore/tests.rs @@ -77,7 +77,7 @@ fn create_update_parent_shreds_with_shred_parent( is_last_in_slot: bool, ) -> Vec { use solana_entry::block_component::UpdateParentV1; - let component = VersionedBlockMarker::new_update_parent(UpdateParentV1 { + let component = VersionedBlockMarker::from_update_parent(UpdateParentV1 { new_parent_slot: parent_slot, new_parent_block_id: parent_block_id, }); @@ -109,7 +109,7 @@ fn create_block_header_shreds_with_shred_parent( parent_block_id: Hash, ) -> Vec { use solana_entry::block_component::BlockHeaderV1; - let component = VersionedBlockMarker::new_block_header(BlockHeaderV1 { + let component = VersionedBlockMarker::from_block_header(BlockHeaderV1 { parent_slot, parent_block_id, }); @@ -166,11 +166,11 @@ fn create_block_footer_shreds_with_last( bank_hash: Hash::new_unique(), block_producer_time_nanos: 0, block_user_agent: vec![], - final_cert: None, + block_final_cert: None, skip_reward_cert: None, notar_reward_cert: None, }; - let component = VersionedBlockMarker::new_block_footer(footer); + let component = VersionedBlockMarker::from_block_footer(footer); let component = BlockComponent::new_block_marker(component); Shredder::new(slot, parent_slot, 0, 0) diff --git a/ledger/src/blockstore_processor.rs b/ledger/src/blockstore_processor.rs index ced44cd56bf..890f9c064dd 100644 --- a/ledger/src/blockstore_processor.rs +++ b/ledger/src/blockstore_processor.rs @@ -6096,17 +6096,17 @@ pub mod tests { let keypair = Arc::new(Keypair::new()); let reed_solomon_cache = ReedSolomonCache::default(); - let header = VersionedBlockMarker::new_block_header(BlockHeaderV1 { + let header = VersionedBlockMarker::from_block_header(BlockHeaderV1 { parent_slot: 0, parent_block_id: Hash::default(), }); let header_component = BlockComponent::new_block_marker(header); - let footer = VersionedBlockMarker::new_block_footer(BlockFooterV1 { + let footer = VersionedBlockMarker::from_block_footer(BlockFooterV1 { bank_hash: Hash::new_unique(), block_producer_time_nanos: 1_000_000_000, block_user_agent: b"test".to_vec(), - final_cert: None, + block_final_cert: None, skip_reward_cert: None, notar_reward_cert: None, }); diff --git a/poh/src/poh_recorder.rs b/poh/src/poh_recorder.rs index 58b897df2ac..70a654052a1 100644 --- a/poh/src/poh_recorder.rs +++ b/poh/src/poh_recorder.rs @@ -627,7 +627,7 @@ impl PohRecorder { // Send out the block footer - we now have the bank hash footer.bank_hash = working_bank.bank.hash(); - let footer = VersionedBlockMarker::new_block_footer(footer); + let footer = VersionedBlockMarker::from_block_footer(footer); let footer_entry_marker = ( EntryOrMarker::Marker(footer), working_bank.max_tick_height - 1, @@ -1389,11 +1389,11 @@ mod tests { poh_recorder.set_bank_for_test(bank); drop(entry_receiver); - let marker = VersionedBlockMarker::new_block_footer(BlockFooterV1 { + let marker = VersionedBlockMarker::from_block_footer(BlockFooterV1 { bank_hash: Hash::new_unique(), block_producer_time_nanos: 0, block_user_agent: vec![], - final_cert: None, + block_final_cert: None, skip_reward_cert: None, notar_reward_cert: None, }); @@ -1406,7 +1406,7 @@ mod tests { bank_hash: Hash::default(), block_producer_time_nanos: 0, block_user_agent: vec![], - final_cert: None, + block_final_cert: None, skip_reward_cert: None, notar_reward_cert: None, } diff --git a/runtime/src/block_component_processor.rs b/runtime/src/block_component_processor.rs index 1a14d98f598..badb27d55a6 100644 --- a/runtime/src/block_component_processor.rs +++ b/runtime/src/block_component_processor.rs @@ -20,7 +20,7 @@ use { log::*, solana_clock::Slot, solana_entry::block_component::{ - BlockFooterV1, BlockMarkerV1, GenesisCertificate, VersionedBlockFooter, + BlockFooterV1, BlockMarkerV1, GenesisCertBlockMarker, VersionedBlockFooter, VersionedBlockHeader, VersionedBlockMarker, VersionedUpdateParent, }, solana_hash::Hash, @@ -197,10 +197,14 @@ impl BlockComponentProcessor { BlockMarkerV1::BlockHeader(header) if markers_fully_enabled || in_migration => { self.on_header(header.inner(), bank.parent_slot()) } - BlockMarkerV1::GenesisCertificate(genesis_cert) + BlockMarkerV1::GenesisCertificate(genesis_cert_block_marker) if markers_fully_enabled || in_migration => { - self.on_genesis_certificate(bank, genesis_cert.into_inner(), migration_status) + self.on_genesis_cert_block_marker( + bank, + genesis_cert_block_marker.into_inner(), + migration_status, + ) } // Everything else is only valid once migration is complete @@ -220,10 +224,10 @@ impl BlockComponentProcessor { } } - pub fn on_genesis_certificate( + pub fn on_genesis_cert_block_marker( &self, bank: Arc, - genesis_cert: GenesisCertificate, + genesis_block_marker: GenesisCertBlockMarker, migration_status: &MigrationStatus, ) -> Result<(), BlockComponentProcessorError> { // Genesis Certificate is only allowed for direct child of genesis @@ -234,7 +238,9 @@ impl BlockComponentProcessor { let parent_block_id = bank .parent_block_id() .expect("Block id is populated for all slots > 0"); - if (bank.parent_slot(), parent_block_id) != (genesis_cert.slot, genesis_cert.block_id) { + if (bank.parent_slot(), parent_block_id) + != (genesis_block_marker.slot, genesis_block_marker.block_id) + { return Err(BlockComponentProcessorError::GenesisCertificateOnNonChild); } @@ -242,7 +248,7 @@ impl BlockComponentProcessor { return Err(BlockComponentProcessorError::GenesisCertificateAlreadyPopulated); } - let genesis_cert = Certificate::from(genesis_cert); + let genesis_cert = Certificate::from(genesis_block_marker); Self::verify_genesis_certificate(&bank, &genesis_cert)?; bank.set_alpenglow_genesis_certificate(&genesis_cert); bank.set_hashes_per_tick(None); @@ -326,7 +332,7 @@ impl BlockComponentProcessor { bank_hash, block_producer_time_nanos, block_user_agent: _, - final_cert, + block_final_cert, skip_reward_cert, notar_reward_cert, } = footer; @@ -335,7 +341,7 @@ impl BlockComponentProcessor { ValidatedRewardCert::try_new(&bank, &skip_reward_cert, ¬ar_reward_cert)?; let block_producer_time_nanos = Self::block_producer_time_nanos_as_i64(block_producer_time_nanos)?; - let final_cert = final_cert + let final_cert = block_final_cert .map(|final_cert| { ValidatedBlockFinalizationCert::try_from_footer(final_cert, &bank) .map_err(BlockComponentProcessorError::InvalidFinalizationCertificate) @@ -637,7 +643,7 @@ mod tests { bank_hash: Hash::new_unique(), block_producer_time_nanos: footer_time_nanos as u64, block_user_agent: vec![], - final_cert: None, + block_final_cert: None, skip_reward_cert: None, notar_reward_cert: None, }); @@ -676,7 +682,7 @@ mod tests { bank_hash: Hash::new_unique(), block_producer_time_nanos: footer_time_nanos as u64, block_user_agent: vec![], - final_cert: None, + block_final_cert: None, skip_reward_cert: None, notar_reward_cert: None, }); @@ -724,7 +730,7 @@ mod tests { fn test_on_marker_processes_header() { let migration_status = MigrationStatus::post_migration_status(); let mut processor = BlockComponentProcessor::default(); - let marker = VersionedBlockMarker::new_block_header(BlockHeaderV1 { + let marker = VersionedBlockMarker::from_block_header(BlockHeaderV1 { parent_slot: 0, parent_block_id: Hash::default(), }); @@ -742,7 +748,7 @@ mod tests { fn test_on_marker_rejects_header_parent_slot_mismatch() { let migration_status = MigrationStatus::post_migration_status(); let mut processor = BlockComponentProcessor::default(); - let marker = VersionedBlockMarker::new_block_header(BlockHeaderV1 { + let marker = VersionedBlockMarker::from_block_header(BlockHeaderV1 { parent_slot: 7, // mismatches bank.parent_slot() below (0) parent_block_id: Hash::default(), }); @@ -775,11 +781,11 @@ mod tests { let footer_time_nanos = parent_time_nanos + 300_000_000; // parent + 300ms let expected_time_secs = footer_time_nanos / 1_000_000_000; - let marker = VersionedBlockMarker::new_block_footer(BlockFooterV1 { + let marker = VersionedBlockMarker::from_block_footer(BlockFooterV1 { bank_hash: Hash::new_unique(), block_producer_time_nanos: footer_time_nanos as u64, block_user_agent: vec![], - final_cert: None, + block_final_cert: None, skip_reward_cert: None, notar_reward_cert: None, }); @@ -820,7 +826,7 @@ mod tests { bank_hash: Hash::new_unique(), block_producer_time_nanos: footer_time_nanos as u64, block_user_agent: vec![], - final_cert: None, + block_final_cert: None, skip_reward_cert: None, notar_reward_cert: None, }); @@ -844,7 +850,7 @@ mod tests { let bank = create_child_bank(&bank_forks, &parent, 1); // Try to process a block header marker pre-migration - should fail - let marker = VersionedBlockMarker::new_block_header(BlockHeaderV1 { + let marker = VersionedBlockMarker::from_block_header(BlockHeaderV1 { parent_slot: 0, parent_block_id: Hash::default(), }); @@ -863,11 +869,11 @@ mod tests { let bank = create_child_bank(&bank_forks, &parent, 1); let parent_time_nanos = parent.clock().unix_timestamp.saturating_mul(1_000_000_000); - let footer_marker = VersionedBlockMarker::new_block_footer(BlockFooterV1 { + let footer_marker = VersionedBlockMarker::from_block_footer(BlockFooterV1 { bank_hash: Hash::new_unique(), block_producer_time_nanos: (parent_time_nanos + 500_000_000) as u64, block_user_agent: vec![], - final_cert: None, + block_final_cert: None, skip_reward_cert: None, notar_reward_cert: None, }); @@ -885,7 +891,7 @@ mod tests { Err(BlockComponentProcessorError::BlockComponentPreMigration) )); - let update_parent_marker = VersionedBlockMarker::new_update_parent(UpdateParentV1 { + let update_parent_marker = VersionedBlockMarker::from_update_parent(UpdateParentV1 { new_parent_slot: 0, new_parent_block_id: Hash::default(), }); @@ -926,7 +932,7 @@ mod tests { let bank = create_child_bank(&bank_forks, &parent, 1); // Process header marker - let header_marker = VersionedBlockMarker::new_block_header(BlockHeaderV1 { + let header_marker = VersionedBlockMarker::from_block_header(BlockHeaderV1 { parent_slot: 0, parent_block_id: Hash::default(), }); @@ -950,11 +956,11 @@ mod tests { let expected_time_secs = footer_time_nanos / 1_000_000_000; // Process footer marker - let footer_marker = VersionedBlockMarker::new_block_footer(BlockFooterV1 { + let footer_marker = VersionedBlockMarker::from_block_footer(BlockFooterV1 { bank_hash: Hash::new_unique(), block_producer_time_nanos: footer_time_nanos as u64, block_user_agent: vec![], - final_cert: None, + block_final_cert: None, skip_reward_cert: None, notar_reward_cert: None, }); @@ -987,7 +993,7 @@ mod tests { bank_hash: Hash::new_unique(), block_producer_time_nanos: 1_000_000_000, block_user_agent: vec![], - final_cert: None, + block_final_cert: None, skip_reward_cert: None, notar_reward_cert: None, }); @@ -1016,11 +1022,11 @@ mod tests { let expected_time_secs = footer_time_nanos / 1_000_000_000; // Process footer marker - let footer_marker = VersionedBlockMarker::new_block_footer(BlockFooterV1 { + let footer_marker = VersionedBlockMarker::from_block_footer(BlockFooterV1 { bank_hash: Hash::new_unique(), block_producer_time_nanos: footer_time_nanos as u64, block_user_agent: vec![], - final_cert: None, + block_final_cert: None, skip_reward_cert: None, notar_reward_cert: None, }); @@ -1101,7 +1107,7 @@ mod tests { bank_hash: Hash::new_unique(), block_producer_time_nanos: footer_time_nanos as u64, block_user_agent: vec![], - final_cert: None, + block_final_cert: None, skip_reward_cert: None, notar_reward_cert: None, }); @@ -1148,7 +1154,7 @@ mod tests { bank_hash: Hash::new_unique(), block_producer_time_nanos: footer_time_nanos as u64, block_user_agent: vec![], - final_cert: None, + block_final_cert: None, skip_reward_cert: None, notar_reward_cert: None, }); @@ -1219,7 +1225,7 @@ mod tests { bank_hash: Hash::new_unique(), block_producer_time_nanos: u64::MAX, block_user_agent: vec![], - final_cert: None, + block_final_cert: None, skip_reward_cert: None, notar_reward_cert: None, }); @@ -1421,7 +1427,7 @@ mod tests { bank_hash: Hash::new_unique(), block_producer_time_nanos: (parent_time_nanos + 100_000_000) as u64, block_user_agent: vec![], - final_cert: None, + block_final_cert: None, skip_reward_cert: None, notar_reward_cert: None, }); diff --git a/runtime/src/validated_block_finalization.rs b/runtime/src/validated_block_finalization.rs index 93e16b2b547..d4a14b47bbc 100644 --- a/runtime/src/validated_block_finalization.rs +++ b/runtime/src/validated_block_finalization.rs @@ -13,7 +13,7 @@ use { log::warn, solana_bls_signatures::BlsError, solana_clock::Slot, - solana_entry::block_component::{FinalCertificate, VotesAggregate}, + solana_entry::block_component::{BlockFinalizationCert, VotesAggregate}, solana_hash::Hash, solana_pubkey::Pubkey, solana_signer_store::{DecodeError, Decoded, decode}, @@ -92,12 +92,12 @@ impl ValidatedBlockFinalizationCert { /// - Certificate signature verification fails /// - The certificates don't meet the required stake thresholds pub fn try_from_footer( - final_cert: FinalCertificate, + block_final_cert: BlockFinalizationCert, bank: &Bank, ) -> Result { - let (slot, block_id) = (final_cert.slot, final_cert.block_id); + let (slot, block_id) = (block_final_cert.slot, block_final_cert.block_id); - if let Some(notar_aggregate) = final_cert.notar_aggregate { + if let Some(notar_aggregate) = block_final_cert.notar_aggregate { // Slow finalization let notarize_cert_type = CertificateType::Notarize(slot, block_id); let finalize_cert_type = CertificateType::Finalize(slot); @@ -111,11 +111,11 @@ impl ValidatedBlockFinalizationCert { }; let finalize_cert = Certificate { cert_type: finalize_cert_type, - signature: final_cert + signature: block_final_cert .final_aggregate .uncompress_signature() .map_err(|e| BlockFinalizationCertError::BlsError(finalize_cert_type, e))?, - bitmap: final_cert.final_aggregate.into_bitmap(), + bitmap: block_final_cert.final_aggregate.into_bitmap(), }; // Verify both certificates @@ -171,13 +171,13 @@ impl ValidatedBlockFinalizationCert { let fast_finalize_cert = Certificate { cert_type: fast_finalize_cert_type, - signature: final_cert + signature: block_final_cert .final_aggregate .uncompress_signature() .map_err(|e| { BlockFinalizationCertError::BlsError(fast_finalize_cert_type, e) })?, - bitmap: final_cert.final_aggregate.into_bitmap(), + bitmap: block_final_cert.final_aggregate.into_bitmap(), }; let (finalize_stake, total_stake) = @@ -321,8 +321,8 @@ impl ValidatedBlockFinalizationCert { (signers, final_cert, notar_cert) } - /// Converts this validated certificate into a [`FinalCertificate`] for inclusion in a block footer. - pub fn to_final_certificate(&self) -> FinalCertificate { + /// Converts this validated certificate into a [`BlockFinalizationCert`] for inclusion in a block footer. + pub fn to_block_final_cert(&self) -> BlockFinalizationCert { match &self.kind { ValidatedBlockFinalizationCertKind::Finalize { finalize_cert, @@ -334,7 +334,7 @@ impl ValidatedBlockFinalizationCert { .to_block() .expect("notarize certificates correspond to blocks") .1; - FinalCertificate { + BlockFinalizationCert { slot, block_id, final_aggregate: VotesAggregate::from_certificate(finalize_cert), @@ -346,7 +346,7 @@ impl ValidatedBlockFinalizationCert { .cert_type .to_block() .expect("fast finalizations correspond to blocks"); - FinalCertificate { + BlockFinalizationCert { slot, block_id, final_aggregate: VotesAggregate::from_certificate(cert), @@ -487,14 +487,14 @@ mod tests { let fast_finalize_cert = build_certificate_manual(cert_type, vote, &signing_ranks, &validator_keypairs); - let final_cert = FinalCertificate { + let block_final_cert = BlockFinalizationCert { slot, block_id, final_aggregate: VotesAggregate::from_certificate(&fast_finalize_cert), notar_aggregate: None, }; - let result = ValidatedBlockFinalizationCert::try_from_footer(final_cert, &bank); + let result = ValidatedBlockFinalizationCert::try_from_footer(block_final_cert, &bank); assert!( result.is_ok(), "Valid fast finalize certificate should pass verification: {result:?}" @@ -524,14 +524,14 @@ mod tests { &validator_keypairs, ); - let final_cert = FinalCertificate { + let block_final_cert = BlockFinalizationCert { slot, block_id, final_aggregate: VotesAggregate::from_certificate(&finalize_cert), notar_aggregate: Some(VotesAggregate::from_certificate(¬arize_cert)), }; - let result = ValidatedBlockFinalizationCert::try_from_footer(final_cert, &bank); + let result = ValidatedBlockFinalizationCert::try_from_footer(block_final_cert, &bank); assert!( result.is_ok(), "Valid slow finalize certificate should pass verification: {result:?}" @@ -561,14 +561,14 @@ mod tests { let fast_finalize_cert = build_certificate_manual(cert_type, vote, &signing_ranks, &validator_keypairs); - let final_cert = FinalCertificate { + let block_final_cert = BlockFinalizationCert { slot, block_id, final_aggregate: VotesAggregate::from_certificate(&fast_finalize_cert), notar_aggregate: None, }; - let result = ValidatedBlockFinalizationCert::try_from_footer(final_cert, &bank); + let result = ValidatedBlockFinalizationCert::try_from_footer(block_final_cert, &bank); assert!( matches!( result, @@ -602,14 +602,14 @@ mod tests { &validator_keypairs, ); - let final_cert = FinalCertificate { + let block_final_cert = BlockFinalizationCert { slot, block_id, final_aggregate: VotesAggregate::from_certificate(&finalize_cert), notar_aggregate: Some(VotesAggregate::from_certificate(¬arize_cert)), }; - let result = ValidatedBlockFinalizationCert::try_from_footer(final_cert, &bank); + let result = ValidatedBlockFinalizationCert::try_from_footer(block_final_cert, &bank); assert!( matches!( result, @@ -645,14 +645,14 @@ mod tests { &validator_keypairs, ); - let final_cert = FinalCertificate { + let block_final_cert = BlockFinalizationCert { slot, block_id, final_aggregate: VotesAggregate::from_certificate(&finalize_cert), notar_aggregate: Some(VotesAggregate::from_certificate(¬arize_cert)), }; - let result = ValidatedBlockFinalizationCert::try_from_footer(final_cert, &bank); + let result = ValidatedBlockFinalizationCert::try_from_footer(block_final_cert, &bank); assert!( matches!( result, diff --git a/turbine/src/broadcast_stage/broadcast_utils.rs b/turbine/src/broadcast_stage/broadcast_utils.rs index 1ea9a50a982..c0bf2007724 100644 --- a/turbine/src/broadcast_stage/broadcast_utils.rs +++ b/turbine/src/broadcast_stage/broadcast_utils.rs @@ -444,7 +444,7 @@ mod tests { .unwrap(); } - let marker = solana_entry::block_component::VersionedBlockMarker::new_block_header( + let marker = solana_entry::block_component::VersionedBlockMarker::from_block_header( solana_entry::block_component::BlockHeaderV1 { parent_slot: 0, parent_block_id: Hash::default(), @@ -480,7 +480,7 @@ mod tests { s.send((bank1.clone(), (EntryOrMarker::Entry(entry1.clone()), 1))) .unwrap(); - let marker = solana_entry::block_component::VersionedBlockMarker::new_block_header( + let marker = solana_entry::block_component::VersionedBlockMarker::from_block_header( solana_entry::block_component::BlockHeaderV1 { parent_slot: 0, parent_block_id: Hash::default(), @@ -546,7 +546,7 @@ mod tests { // Bank changes to bank2, but the next item is a marker with tick_height=3 — this should // leave entries empty after the clear. The stale last_tick_height (5, from bank1) must not // leak into subsequent results. - let marker = solana_entry::block_component::VersionedBlockMarker::new_block_header( + let marker = solana_entry::block_component::VersionedBlockMarker::from_block_header( solana_entry::block_component::BlockHeaderV1 { parent_slot: 1, parent_block_id: Hash::default(), diff --git a/turbine/src/broadcast_stage/standard_broadcast_run.rs b/turbine/src/broadcast_stage/standard_broadcast_run.rs index 4a6e78af7a5..ef1bb8525a5 100644 --- a/turbine/src/broadcast_stage/standard_broadcast_run.rs +++ b/turbine/src/broadcast_stage/standard_broadcast_run.rs @@ -1264,7 +1264,7 @@ mod test { let new_parent_slot = 7; let new_parent_block_id = Hash::new_unique(); - let component = BlockComponent::new_block_marker(VersionedBlockMarker::new_update_parent( + let component = BlockComponent::new_block_marker(VersionedBlockMarker::from_update_parent( solana_entry::block_component::UpdateParentV1 { new_parent_slot, new_parent_block_id, From c1a976f7d9b968e0abd468dbc908adf5b4cf2303 Mon Sep 17 00:00:00 2001 From: Kamil Skalski Date: Wed, 3 Jun 2026 20:00:24 +0200 Subject: [PATCH 187/576] feat(snapshots): add StableAbi for obsolete accounts format (#12919) --- runtime/src/serde_snapshot/obsolete_accounts.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/runtime/src/serde_snapshot/obsolete_accounts.rs b/runtime/src/serde_snapshot/obsolete_accounts.rs index 25fe6aeff98..4a6e34855f7 100644 --- a/runtime/src/serde_snapshot/obsolete_accounts.rs +++ b/runtime/src/serde_snapshot/obsolete_accounts.rs @@ -12,7 +12,7 @@ use { }; #[repr(C)] -#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] +#[cfg_attr(feature = "frozen-abi", derive(AbiExample, StableAbi, StableAbiSample))] #[derive(Debug, SchemaRead, SchemaWrite, Serialize)] pub struct SerdeObsoleteAccountItem { /// Offset of the account in the account storage entry @@ -23,7 +23,7 @@ pub struct SerdeObsoleteAccountItem { pub slot: Slot, } -#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] +#[cfg_attr(feature = "frozen-abi", derive(AbiExample, StableAbi, StableAbiSample))] #[derive(Debug, Default, Serialize, SchemaRead, SchemaWrite)] pub(crate) struct SerdeObsoleteAccounts { /// The ID of the associated account file. Used for verification to ensure the restored @@ -89,8 +89,12 @@ impl SerdeObsoleteAccounts { /// to capture and restore obsolete accounts information for account storages. #[cfg_attr( feature = "frozen-abi", - derive(AbiExample), - frozen_abi(digest = "FbAP5pg2FLgSkMWiv3eMwEh433qMoNRbaJij9aLtG2NH") + derive(AbiExample, StableAbi, StableAbiSample), + frozen_abi( + api_digest = "FbAP5pg2FLgSkMWiv3eMwEh433qMoNRbaJij9aLtG2NH", + abi_digest = "5AMjJ751HYUj4rCtZgAsjVi9q5NgdYYZ11YEp9W7cB86", + abi_serializer = "wincode" + ) )] #[derive(Serialize, Debug, SchemaRead, SchemaWrite)] pub(crate) struct SerdeObsoleteAccountsMap { From 205b6e29464930ab27183be4240ed32856c60526 Mon Sep 17 00:00:00 2001 From: Rory Harris Date: Wed, 3 Jun 2026 11:49:56 -0700 Subject: [PATCH 188/576] Add Cached Account updates to capitalization (#12909) * Add Cached Account updates to capitalization * Moved lamports from storage into lambda --- accounts-db/src/accounts_db.rs | 70 +++++++++++++++++++++++++--------- 1 file changed, 51 insertions(+), 19 deletions(-) diff --git a/accounts-db/src/accounts_db.rs b/accounts-db/src/accounts_db.rs index 546c5a5b759..555c74930f0 100644 --- a/accounts-db/src/accounts_db.rs +++ b/accounts-db/src/accounts_db.rs @@ -5007,34 +5007,66 @@ impl AccountsDb { /// /// Only intended to be called at startup by ledger-tool or tests. pub fn calculate_capitalization_at_startup_from_index(&self, ancestors: &Ancestors) -> u64 { - self.accounts_index + let stored_lamports = |pubkey: &Pubkey| { + self.accounts_index + .get_with_and_then(pubkey, ancestors, false, |(slot, account_info)| { + (!account_info.is_zero_lamport()).then(|| { + self.get_account_accessor(slot, pubkey, &account_info.storage_location()) + .get_loaded_account(|loaded_account| loaded_account.lamports()) + // SAFETY: The index said this pubkey exists, so + // there must be an account to load. + .unwrap() + }) + }) + .flatten() + .unwrap_or(0) + }; + + let storage_capitialization = self + .accounts_index .account_maps .par_iter() .map(|accounts_index_bin| { accounts_index_bin .keys() .into_iter() - .map(|pubkey| { - self.accounts_index - .get_with_and_then(&pubkey, ancestors, false, |(slot, account_info)| { - (!account_info.is_zero_lamport()).then(|| { - self.get_account_accessor( - slot, - &pubkey, - &account_info.storage_location(), - ) - .get_loaded_account(|loaded_account| loaded_account.lamports()) - // SAFETY: The index said this pubkey exists, so - // there must be an account to load. - .unwrap() - }) - }) - .flatten() - .unwrap_or(0) - }) + .map(|pubkey| stored_lamports(&pubkey)) .try_fold(0, u64::checked_add) }) .try_reduce(|| 0, u64::checked_add) + .expect("capitalization cannot overflow"); + + // Sum as i128 because there is potential (although unlikely) for the cache updates to + // overflow i64::MAX. For example, if the cache has multiple transactions that transfer a + // large amount of lamports from one account to another, it could sum all of the transfers + // from accounts first, overflow i128. Wrapping logic could also handle this properly (ie. + // come to the correct answer), but then detection of overflow would be broken. + let cached_update = self + .accounts_cache + .cached_pubkeys() + .iter() + .map(|pubkey| { + // subtract out whatever older version the index walk produced (if any) + let stored_lamports = stored_lamports(pubkey); + + // add in the cached amount of lamports + let cached_lamports = self + .load( + ancestors, + pubkey, + LoadHint::FixedMaxRoot, + PopulateReadCache::False, + ) + .map(|(account, _slot)| account.lamports()) + .unwrap_or(0); + + cached_lamports as i128 - stored_lamports as i128 + }) + .sum::(); + + i128::from(storage_capitialization) + .checked_add(cached_update) + .and_then(|result| u64::try_from(result).ok()) .expect("capitalization cannot overflow") } From e227d3df29493216f799b42c52dd7446623cc1b3 Mon Sep 17 00:00:00 2001 From: Akhilesh Singhania Date: Wed, 3 Jun 2026 21:17:45 +0200 Subject: [PATCH 189/576] Updates vote_reward module to set timestamp on vote accounts (#12898) --- programs/vote/src/vote_state/handler.rs | 4 +- runtime/src/block_component_processor.rs | 7 +- .../block_component_processor/vote_reward.rs | 157 ++++++++++++------ 3 files changed, 115 insertions(+), 53 deletions(-) diff --git a/programs/vote/src/vote_state/handler.rs b/programs/vote/src/vote_state/handler.rs index 7f7ccc474d6..aa859207e2b 100644 --- a/programs/vote/src/vote_state/handler.rs +++ b/programs/vote/src/vote_state/handler.rs @@ -283,13 +283,13 @@ impl VoteStateHandler { } } - pub(crate) fn last_timestamp(&self) -> &BlockTimestamp { + pub fn last_timestamp(&self) -> &BlockTimestamp { match &self.target_state { TargetVoteState::V4(v4) => &v4.last_timestamp, } } - pub(crate) fn set_last_timestamp(&mut self, timestamp: BlockTimestamp) { + pub fn set_last_timestamp(&mut self, timestamp: BlockTimestamp) { match &mut self.target_state { TargetVoteState::V4(v4) => v4.last_timestamp = timestamp, } diff --git a/runtime/src/block_component_processor.rs b/runtime/src/block_component_processor.rs index badb27d55a6..5d7234e45ab 100644 --- a/runtime/src/block_component_processor.rs +++ b/runtime/src/block_component_processor.rs @@ -524,7 +524,12 @@ impl BlockComponentProcessor { final_cert_input: Option<(&HashSet, Slot)>, ) -> Result<(), BankFooterError> { bank.update_clock_from_footer(block_producer_time_nanos); - calc_vote_rewards_update_vote_states(bank, reward_cert, final_cert_input)?; + calc_vote_rewards_update_vote_states( + bank, + reward_cert, + final_cert_input, + block_producer_time_nanos, + )?; if let Some(hash) = bank_hash { // Record expected bank hash from footer for later verification when the bank is frozen. diff --git a/runtime/src/block_component_processor/vote_reward.rs b/runtime/src/block_component_processor/vote_reward.rs index e6a555db7b1..bb2de616898 100644 --- a/runtime/src/block_component_processor/vote_reward.rs +++ b/runtime/src/block_component_processor/vote_reward.rs @@ -7,7 +7,9 @@ use { solana_clock::{Epoch, Slot}, solana_pubkey::Pubkey, solana_vote::vote_account::VoteAccount, - solana_vote_interface::state::{LandedVote, Lockout, MAX_EPOCH_CREDITS_HISTORY}, + solana_vote_interface::state::{ + BlockTimestamp, LandedVote, Lockout, MAX_EPOCH_CREDITS_HISTORY, + }, solana_vote_program::vote_state::handler::VoteStateHandler, std::{ collections::{HashMap, HashSet, VecDeque, hash_map::Entry}, @@ -35,16 +37,10 @@ pub enum CalcVoteRewardUpdateVoteStatesError { /// These errors should cause the processing of the bank to fail. #[derive(Debug, Error)] pub enum AllocateAccountsError { - #[error("did not find rank map for final_slot={final_slot} in current_slot={current_slot}")] - FinalCert { - current_slot: Slot, - final_slot: Slot, - }, - #[error("did not find rank map for reward_slot={reward_slot} in current_slot={current_slot}")] - RewardCert { - current_slot: Slot, - reward_slot: Slot, - }, + #[error("did not find rank map for final_slot={final_slot} in bank_slot={bank_slot}")] + FinalCert { bank_slot: Slot, final_slot: Slot }, + #[error("did not find rank map for reward_slot={reward_slot} in bank_slot={bank_slot}")] + RewardCert { bank_slot: Slot, reward_slot: Slot }, } /// Different types of error that happen when looking up state to process the reward cert. @@ -52,34 +48,25 @@ pub enum AllocateAccountsError { /// These errors should cause the processing of the bank to fail. #[derive(Debug, Error)] pub enum RewardStateError { - #[error("missing epoch stakes for reward_slot {reward_slot} in current_slot {current_slot}")] - MissingEpochStakes { - reward_slot: Slot, - current_slot: Slot, - }, - #[error("missing EpochInflationAccountState for current slot {current_slot}")] - MissingEpochInflationAccountState { current_slot: Slot }, + #[error("missing epoch stakes for reward_slot {reward_slot} in bank_slot {bank_slot}")] + MissingEpochStakes { reward_slot: Slot, bank_slot: Slot }, + #[error("missing EpochInflationAccountState for bank_slot {bank_slot}")] + MissingEpochInflationAccountState { bank_slot: Slot }, #[error( - "missing validator stake info for reward epoch {reward_epoch} in current_slot \ - {current_slot}" + "missing validator stake info for reward epoch {reward_epoch} in bank_slot {bank_slot}" )] NoEpochValidatorStake { reward_epoch: Epoch, - current_slot: Slot, + bank_slot: Slot, }, - #[error( - "validator {pubkey} missing in current slot {current_slot} for reward slot {reward_slot}" - )] + #[error("validator {pubkey} missing in bank_slot {bank_slot} for reward slot {reward_slot}")] MissingRewardSlotValidator { pubkey: Pubkey, reward_slot: Slot, - current_slot: Slot, - }, - #[error("genesis cert not found. reward_slot={reward_slot}; current_slot={current_slot}")] - GenesisCertNotFound { - reward_slot: Slot, - current_slot: Slot, + bank_slot: Slot, }, + #[error("genesis cert not found. reward_slot={reward_slot}; bank_slot={bank_slot}")] + GenesisCertNotFound { reward_slot: Slot, bank_slot: Slot }, } /// Data needed to operate on `VoteStateHandler`. @@ -146,9 +133,8 @@ impl VoteState { } } - /// Sets the slot in `votes` in the vote state to the max of `slot`, vote_state.root_slot, and - /// vote_state.last_voted_slot. - fn maybe_update_votes(&mut self, slot: Slot) { + /// Updates `votes` and `last_timestamp` in the vote state. + fn maybe_update_votes(&mut self, slot: Slot, slot_timestamp_ns: i64) { let latest_voted_slot = slot .max(self.handler.last_voted_slot().unwrap_or(0)) .max(self.handler.root_slot().unwrap_or(0)); @@ -156,6 +142,12 @@ impl VoteState { lockout: Lockout::new(latest_voted_slot), latency: 0, }])); + + let timestamp = slot_timestamp_ns / 1_000_000_000; + if timestamp > self.handler.last_timestamp().timestamp { + self.handler + .set_last_timestamp(BlockTimestamp { slot, timestamp }); + } } /// If `slot` is bigger than vote_state.root_slot, then updates vote_state.root_slot. @@ -168,6 +160,7 @@ impl VoteState { /// Common state required to pay rewards. #[derive(Debug)] struct RewardState<'a> { + reward_slot_timestamp_ns: i64, /// The epoch in which the reward was paid into the vote account. current_epoch: Epoch, /// The slot in which the reward was earned. @@ -175,7 +168,7 @@ struct RewardState<'a> { /// Validators present in the reward cert. reward_validators: &'a HashSet, /// The slot in which the reward is being paid into the vote account. - current_slot: Slot, + bank_slot: Slot, /// The pubkey of the validator that will receive the leader rewards. leader_vote_pubkey: Pubkey, /// Vote accounts at reward slot. @@ -192,12 +185,13 @@ impl<'a> RewardState<'a> { bank: &'a Bank, reward_slot: Slot, reward_validators: &'a HashSet, + block_producer_time_nanos: i64, ) -> Result { - let current_slot = bank.slot(); + let bank_slot = bank.slot(); let epoch_stakes = bank.epoch_stakes_from_slot(reward_slot).ok_or( RewardStateError::MissingEpochStakes { reward_slot, - current_slot, + bank_slot, }, )?; let accounts = epoch_stakes.stakes().vote_accounts().as_ref(); @@ -211,23 +205,26 @@ impl<'a> RewardState<'a> { // that activated Alpenglow should have created the account. debug_assert!(epoch_inflation_account_state.is_some()); epoch_inflation_account_state - .ok_or(RewardStateError::MissingEpochInflationAccountState { current_slot })? + .ok_or(RewardStateError::MissingEpochInflationAccountState { bank_slot })? .get_epoch_state(reward_epoch) .ok_or(RewardStateError::NoEpochValidatorStake { reward_epoch, - current_slot, + bank_slot, })? }; let migration_epoch = get_migration_epoch(bank).ok_or(RewardStateError::GenesisCertNotFound { reward_slot, - current_slot, + bank_slot, })?; + let reward_slot_timestamp_ns = + calc_slot_timestamp(bank, reward_slot, block_producer_time_nanos); Ok(Self { + reward_slot_timestamp_ns, current_epoch: bank.epoch(), reward_slot, reward_validators, - current_slot, + bank_slot, leader_vote_pubkey: bank.leader().vote_address, accounts, total_stake, @@ -250,7 +247,7 @@ impl<'a> RewardState<'a> { .ok_or(RewardStateError::MissingRewardSlotValidator { pubkey: validator, reward_slot: self.reward_slot, - current_slot: self.current_slot, + bank_slot: self.bank_slot, })?; let (validator_reward, leader_reward) = calculate_reward( &self.epoch_inflation_state, @@ -263,7 +260,7 @@ impl<'a> RewardState<'a> { fn update_votes(&self, vote_state: &mut VoteState) { debug_assert!(self.reward_validators.contains(&vote_state.vote_pubkey)); - vote_state.maybe_update_votes(self.reward_slot); + vote_state.maybe_update_votes(self.reward_slot, self.reward_slot_timestamp_ns); } fn update_account( @@ -307,9 +304,25 @@ impl<'a> RewardState<'a> { struct FinalCertState<'a> { signers: &'a HashSet, final_slot: Slot, + final_slot_timestamp_ns: i64, } impl<'a> FinalCertState<'a> { + fn new( + bank: &Bank, + signers: &'a HashSet, + final_slot: Slot, + block_producer_time_nanos: i64, + ) -> Self { + let final_slot_timestamp_ns = + calc_slot_timestamp(bank, final_slot, block_producer_time_nanos); + Self { + signers, + final_slot, + final_slot_timestamp_ns, + } + } + /// Updates the `root_slot` and the `votes` fields in the `VoteStateHandler`. #[must_use] fn update_account(&self, vote_state: &mut VoteState) -> bool { @@ -317,7 +330,7 @@ impl<'a> FinalCertState<'a> { vote_state.maybe_update_root(self.final_slot); // If a validator is included in the finalization cert, it must have voted for it. // So even if the reward cert is absent, we can still update votes. - vote_state.maybe_update_votes(self.final_slot); + vote_state.maybe_update_votes(self.final_slot, self.final_slot_timestamp_ns); true } else { false @@ -344,14 +357,14 @@ fn allocate_updated_accounts( let final_cert_slot_max_validators = bank .get_rank_map(*slot) .ok_or(AllocateAccountsError::FinalCert { - current_slot: bank.slot(), + bank_slot: bank.slot(), final_slot: *slot, })? .len(); let reward_cert_slot_max_validators = bank .get_rank_map(reward_cert.slot()) .ok_or(AllocateAccountsError::RewardCert { - current_slot: bank.slot(), + bank_slot: bank.slot(), reward_slot: reward_cert.slot(), })? .len(); @@ -420,18 +433,23 @@ pub(super) fn calc_vote_rewards_update_vote_states( bank: &Bank, reward_cert: Option, final_cert_input: Option<(&HashSet, Slot)>, + block_producer_time_nanos: i64, ) -> Result<(), CalcVoteRewardUpdateVoteStatesError> { let Some(updated_accounts) = allocate_updated_accounts(bank, &reward_cert, &final_cert_input)? else { return Ok(()); }; let reward_state = match &reward_cert { - Some(c) => Some(RewardState::try_new(bank, c.slot(), c.validators())?), + Some(c) => Some(RewardState::try_new( + bank, + c.slot(), + c.validators(), + block_producer_time_nanos, + )?), None => None, }; - let final_cert_state = final_cert_input.map(|(signers, final_slot)| FinalCertState { - signers, - final_slot, + let final_cert_state = final_cert_input.map(|(signers, final_slot)| { + FinalCertState::new(bank, signers, final_slot, block_producer_time_nanos) }); let vote_accounts = bank.vote_accounts(); @@ -492,6 +510,12 @@ fn calculate_reward( (validator_reward_lamports, leader_reward_lamports) } +fn calc_slot_timestamp(bank: &Bank, slot: Slot, block_producer_time_nanos: i64) -> i64 { + block_producer_time_nanos.saturating_sub( + i64::try_from(bank.slot_range_duration_nanos(slot + 1, bank.slot())).unwrap(), + ) +} + fn get_migration_epoch(bank: &Bank) -> Option { let migration_slot = bank.get_alpenglow_migration_slot()?; let migration_epoch = bank.epoch_schedule.get_epoch(migration_slot); @@ -614,6 +638,7 @@ mod tests { std::{ collections::HashMap, sync::{Arc, RwLock}, + time::{SystemTime, UNIX_EPOCH}, }, test_case::test_matrix, }; @@ -756,6 +781,10 @@ mod tests { let bank = new_bank_from_parent(prev_bank.clone(), current_slot); let reward_slot = current_slot - NUM_SLOTS_FOR_REWARD; + let block_producer_time_nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() as i64; calc_vote_rewards_update_vote_states( &bank, Some(ValidatedRewardCert::new_for_tests( @@ -763,8 +792,14 @@ mod tests { validator_pubkeys_to_reward.clone(), )), None, + block_producer_time_nanos, ) .unwrap(); + let slot_timestamp = BlockTimestamp { + slot: reward_slot, + timestamp: calc_slot_timestamp(&bank, reward_slot, block_producer_time_nanos) + / 1_000_000_000, + }; let vote_accounts = bank.vote_accounts(); let rewards = validator_pubkeys_to_reward @@ -779,6 +814,7 @@ mod tests { .unwrap() .total_stake(); let reward_epoch = bank.epoch_schedule.get_epoch(reward_slot); + assert_eq!(vote_state.last_timestamp(), &slot_timestamp); let (expected_validator_reward, expected_leader_reward_per_validator) = calc_reward_for_test( &bank, @@ -861,6 +897,10 @@ mod tests { vec![target_vote_pubkey], )), final_cert_input, + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() as i64, ) .unwrap(); @@ -929,6 +969,10 @@ mod tests { vec![target_vote_pubkey], )), final_cert_input, + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() as i64, ) .unwrap(); @@ -988,6 +1032,10 @@ mod tests { vec![vote_pubkey], )), None, + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() as i64, ) .unwrap(); let vote_accounts = bank.vote_accounts(); @@ -1338,7 +1386,16 @@ mod tests { looping_bank.slot() - 100, validators_to_reward.clone(), ); - calc_vote_rewards_update_vote_states(&looping_bank, Some(reward_cert), None).unwrap(); + calc_vote_rewards_update_vote_states( + &looping_bank, + Some(reward_cert), + None, + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() as i64, + ) + .unwrap(); let slot = looping_bank.slot() + 1; looping_bank = new_bank_from_parent(looping_bank, slot); From dd3bfb3ab43fa2856e49322be8cb45aa1779be79 Mon Sep 17 00:00:00 2001 From: Brooks Date: Wed, 3 Jun 2026 16:21:21 -0400 Subject: [PATCH 190/576] Marks zero lamport single ref accounts in newly shrunk storages (#12912) --- accounts-db/src/account_storage_entry.rs | 4 + accounts-db/src/accounts_db.rs | 50 ++++++++- accounts-db/src/accounts_db/stats.rs | 14 +++ accounts-db/src/accounts_db/tests.rs | 128 +++++++++++++++++++++++ 4 files changed, 194 insertions(+), 2 deletions(-) diff --git a/accounts-db/src/account_storage_entry.rs b/accounts-db/src/account_storage_entry.rs index 2c5ec696c65..e4efdda4dec 100644 --- a/accounts-db/src/account_storage_entry.rs +++ b/accounts-db/src/account_storage_entry.rs @@ -258,4 +258,8 @@ impl AccountStorageEntry { pub(crate) fn obsolete_accounts(&self) -> &RwLock { &self.obsolete_accounts } + + pub(crate) fn zero_lamport_single_ref_offsets(&self) -> &RwLock> { + &self.zero_lamport_single_ref_offsets + } } diff --git a/accounts-db/src/accounts_db.rs b/accounts-db/src/accounts_db.rs index 555c74930f0..0dd3043456c 100644 --- a/accounts-db/src/accounts_db.rs +++ b/accounts-db/src/accounts_db.rs @@ -2684,6 +2684,7 @@ impl AccountsDb { /// These accounts were found during shrink of `slot` to be slot_list=[slot] and ref_count == 1 and lamports = 0. /// This means this slot contained the only account data for this pubkey and it is zero lamport. + /// And also `slot` is <= the latest full snapshot slot, and can purge zero lamport accounts. /// Thus, we did NOT treat this as an alive account, so we did NOT copy the zero lamport account to the new /// storage. So, the account will no longer be alive or exist at `slot`. /// So, first, remove the ref count since this newly shrunk storage will no longer access it. @@ -5268,7 +5269,7 @@ impl AccountsDb { /// not touched, since shrink only changes `(store_id, offset)` and they index by pubkey. fn update_index_for_shrink<'a>( &self, - infos: Vec, + infos: &[AccountInfo], accounts: &impl StorableAccounts<'a>, update_index_thread_selection: UpdateIndexThreadSelection, thread_pool: &ThreadPool, @@ -5729,9 +5730,14 @@ impl AccountsDb { let infos = self.write_accounts_to_storage(slot, storage, &accounts); let write_accounts_us = write_accounts_time.end_as_us(); + let mark_zero_lamport_single_ref_time = Measure::start("mark_zero_lamport_single_ref"); + let num_zero_lamport_single_ref_accounts_marked = + self.mark_zero_lamport_single_ref_accounts_for_shrink(&infos, &accounts, storage); + let mark_zero_lamport_single_ref_us = mark_zero_lamport_single_ref_time.end_as_us(); + let update_index_time = Measure::start("update_index"); self.update_index_for_shrink( - infos, + &infos, &accounts, update_index_thread_selection, &self.thread_pool_background, @@ -5744,12 +5750,19 @@ impl AccountsDb { stats .write_to_storage_us .fetch_add(write_accounts_us, Ordering::Relaxed); + stats + .mark_zero_lamport_single_ref_accounts_us + .fetch_add(mark_zero_lamport_single_ref_us, Ordering::Relaxed); stats .update_index_us .fetch_add(update_index_us, Ordering::Relaxed); stats .num_accounts_stored .fetch_add(num_accounts_stored as u64, Ordering::Relaxed); + stats.num_zero_lamport_single_ref_accounts_marked.fetch_add( + num_zero_lamport_single_ref_accounts_marked, + Ordering::Relaxed, + ); stats.report(); StoreAccountsTiming { @@ -6029,6 +6042,39 @@ impl AccountsDb { num_marked } + /// Marks zero lamport single reference accounts in the storage during store_accounts_for_shrink + /// + /// Returns the number of accounts marked. + fn mark_zero_lamport_single_ref_accounts_for_shrink<'a>( + &self, + account_infos: &[AccountInfo], + accounts: &impl StorableAccounts<'a>, + storage: &AccountStorageEntry, + ) -> u64 { + debug_assert_eq!(account_infos.len(), accounts.len()); + let mut num_marked = 0; + for (i, account_info) in account_infos.iter().enumerate() { + if account_info.is_zero_lamport() { + let pubkey = accounts.pubkey(i); + let is_single_ref = self.accounts_index.get_and_then(pubkey, |entry| { + let is_single_ref = entry.is_some_and(|entry| { + entry.ref_count() == 1 && entry.slot_list_lock_read_len() == 1 + }); + (false, is_single_ref) + }); + if is_single_ref { + debug_assert!(!account_info.is_cached()); + debug_assert_eq!(account_info.store_id(), storage.id()); + if storage.insert_zero_lamport_single_ref_account_offset(account_info.offset()) + { + num_marked += 1; + } + } + } + } + num_marked + } + fn report_store_timings(&self) { if self.stats.last_store_report.should_update(1000) { let read_cache_stats = self.read_only_accounts_cache.get_and_reset_stats(); diff --git a/accounts-db/src/accounts_db/stats.rs b/accounts-db/src/accounts_db/stats.rs index 5873e43a49f..f9dd010abd3 100644 --- a/accounts-db/src/accounts_db/stats.rs +++ b/accounts-db/src/accounts_db/stats.rs @@ -107,8 +107,10 @@ pub struct StoreAccountsForShrinkStats { pub last_report: AtomicInterval, pub flush_read_cache_us: AtomicU64, pub write_to_storage_us: AtomicU64, + pub mark_zero_lamport_single_ref_accounts_us: AtomicU64, pub update_index_us: AtomicU64, pub num_accounts_stored: AtomicU64, + pub num_zero_lamport_single_ref_accounts_marked: AtomicU64, } impl StoreAccountsForShrinkStats { @@ -132,6 +134,12 @@ impl StoreAccountsForShrinkStats { self.write_to_storage_us.swap(0, Ordering::Relaxed), i64 ), + ( + "mark_zero_lamport_single_ref_accounts_us", + self.mark_zero_lamport_single_ref_accounts_us + .swap(0, Ordering::Relaxed), + i64 + ), ( "update_index_us", self.update_index_us.swap(0, Ordering::Relaxed), @@ -142,6 +150,12 @@ impl StoreAccountsForShrinkStats { self.num_accounts_stored.swap(0, Ordering::Relaxed), i64 ), + ( + "num_zero_lamport_single_ref_accounts_marked", + self.num_zero_lamport_single_ref_accounts_marked + .swap(0, Ordering::Relaxed), + i64 + ), ); } } diff --git a/accounts-db/src/accounts_db/tests.rs b/accounts-db/src/accounts_db/tests.rs index edbb836fd7b..bfb239a3c06 100644 --- a/accounts-db/src/accounts_db/tests.rs +++ b/accounts-db/src/accounts_db/tests.rs @@ -1165,6 +1165,134 @@ fn test_shrink_zero_lamport_single_ref_account() { } } +/// Ensure that `shrink` marks zero lamport single ref accounts in the new storage. +#[test] +fn test_shrink_marks_zero_lamport_single_ref_account_in_new_storage() { + let accounts_db = AccountsDb::new_single_for_tests(); + let slot0 = 0; + let slot1 = slot0 + 1; + // latest full snapshot must be older than the slot(s) we plan to shrink, + // otherwise zero lamport single ref accounts will be purged + accounts_db.set_latest_full_snapshot_slot(slot0); + + let obsolete_pubkey = Pubkey::new_unique(); + let obsolete_zero_lamport_pubkey = Pubkey::new_unique(); + let zero_lamport_single_ref_pubkey = Pubkey::new_unique(); + let zero_lamport_multi_ref_pubkey = Pubkey::new_unique(); + let alive_pubkey = Pubkey::new_unique(); + let closed_account = AccountSharedData::new(0, 0, &Pubkey::default()); + let open_account = AccountSharedData::new(1, 0, &Pubkey::default()); + + let (_temp_dirs, paths) = get_temp_accounts_paths(1).unwrap(); + let storage1 = Arc::new(AccountStorageEntry::new( + &paths[0], + slot1, + 10, + DEFAULT_FILE_SIZE, + AccountsFileProvider::AppendVec, + )); + // store an obsolete account; it should not be marked ZLSR + append_single_account_with_default_hash( + &storage1, + &obsolete_pubkey, + &open_account, + true, + Some(&accounts_db.accounts_index), + ); + // store an obsolete zero lamport account; it should not be marked ZLSR + append_single_account_with_default_hash( + &storage1, + &obsolete_zero_lamport_pubkey, + &closed_account, + true, + Some(&accounts_db.accounts_index), + ); + // store a zero lamport single ref account; it *should* be marked ZLSR + append_single_account_with_default_hash( + &storage1, + &zero_lamport_single_ref_pubkey, + &closed_account, + true, + Some(&accounts_db.accounts_index), + ); + // store a zero lamport multi ref account; it should not be marked ZLSR + append_single_account_with_default_hash( + &storage1, + &zero_lamport_multi_ref_pubkey, + &closed_account, + true, + Some(&accounts_db.accounts_index), + ); + // store an alive account; it should not be marked ZLSR + append_single_account_with_default_hash( + &storage1, + &alive_pubkey, + &open_account, + true, + Some(&accounts_db.accounts_index), + ); + insert_store(&accounts_db, Arc::clone(&storage1)); + accounts_db.add_root(slot1); + + // we manually created the storage, so nothing got marked + assert_eq!(storage1.num_zero_lamport_single_ref_accounts(), 0); + + // store the multi ref account again, in slot 2, so it becomes multi ref + let slot2 = slot1 + 1; + accounts_db.store_for_tests(( + slot2, + [(&zero_lamport_multi_ref_pubkey, &closed_account)].as_slice(), + )); + accounts_db.add_root(slot2); + // flush without clean so the ZLMR account isn't marked obsolete in slot 1 + accounts_db.flush_rooted_accounts_cache_without_clean(); + + // mark the obsolete accounts as obsolete + let ancestors = Ancestors::from(vec![slot2]); + for pubkey in [obsolete_pubkey, obsolete_zero_lamport_pubkey] { + let account_info = accounts_db + .accounts_index + .get_with_and_then(&pubkey, &ancestors, false, |account_info| account_info) + .unwrap(); + accounts_db.remove_dead_accounts( + [account_info].iter(), + None, + MarkAccountsObsolete::Yes(slot1), + ); + } + + accounts_db.shrink_slot_forced(slot1); + + let new_storage1 = accounts_db.get_and_assert_single_storage(slot1); + let new_zero_lamport_single_ref_info = accounts_db + .accounts_index + .get_with_and_then( + &zero_lamport_single_ref_pubkey, + &ancestors, + false, + |(_slot, account_info)| account_info, + ) + .unwrap(); + + // ensure ids are different, to indicate shrink ran + assert_ne!(new_storage1.id(), storage1.id()); + // ensure there are three accounts in the storage now, removing the two obsolete ones + assert_eq!(new_storage1.count(), 3); + // ensure the zlsr account info has the correct id for the new shrunk storage + assert_eq!( + new_zero_lamport_single_ref_info.store_id(), + new_storage1.id(), + ); + // ensure the new shrunk storage does have a marked zlsr account + assert_eq!(new_storage1.num_zero_lamport_single_ref_accounts(), 1); + let new_storage_zlsr_offsets = new_storage1 + .zero_lamport_single_ref_offsets() + .read() + .unwrap(); + // ensure the storage's zlsr offset list contains the zlsr account info's + assert!(new_storage_zlsr_offsets.contains(&new_zero_lamport_single_ref_info.offset())); +} + /// unit test for `alive_bytes_after_shrink()` /// /// Check all the permutations of latest full snapshot slot w.r.t. if/how From 7a542eac95f0be75f8f229e94e32ad1eb255e276 Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Wed, 3 Jun 2026 16:48:06 -0400 Subject: [PATCH 191/576] poh: require low power mode when alpenglow is active (#12930) --- ledger/src/blockstore_processor.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/ledger/src/blockstore_processor.rs b/ledger/src/blockstore_processor.rs index 890f9c064dd..55e87d944d7 100644 --- a/ledger/src/blockstore_processor.rs +++ b/ledger/src/blockstore_processor.rs @@ -1089,6 +1089,15 @@ fn verify_ticks( } if migration_status.should_have_alpenglow_ticks(bank.slot()) { + // When alpenglow is active, PoH MUST be in low power mode. + // We require that each block only has 1 tick at the very end + if entries.iter().any(|entry| entry.num_hashes != 1) { + warn!( + "Alpenglow entry with invalid num_hashes found in slot: {}", + bank.slot() + ); + return Err(BlockError::InvalidTickHashCount); + } return Ok(()); } From 90f279a50d73c2cf4a269d75c4ccbf6732d655fa Mon Sep 17 00:00:00 2001 From: Brennan Date: Wed, 3 Jun 2026 14:32:22 -0700 Subject: [PATCH 192/576] SIMD-0525: Slot Time Reductions (#12264) halve slot times --- accounts-db/src/partitioned_rewards.rs | 25 +- feature-set/src/lib.rs | 32 ++ ledger/src/shred/filter.rs | 55 +++- runtime/src/bank.rs | 131 ++++++-- .../src/bank/partitioned_epoch_rewards/mod.rs | 5 + runtime/src/bank/tests.rs | 286 +++++++++++++++++- .../epoch_inflation_account_state.rs | 101 ++++--- runtime/src/slot_params.rs | 196 ++++++++++-- .../broadcast_stage/standard_broadcast_run.rs | 128 +++++--- 9 files changed, 813 insertions(+), 146 deletions(-) diff --git a/accounts-db/src/partitioned_rewards.rs b/accounts-db/src/partitioned_rewards.rs index 85758a4df84..e6565c2c2c9 100644 --- a/accounts-db/src/partitioned_rewards.rs +++ b/accounts-db/src/partitioned_rewards.rs @@ -1,16 +1,22 @@ //! Code related to partitioned rewards distribution -/// # stake accounts to store in one block during partitioned reward interval -/// Target to store 64 rewards per entry/tick in a block. A block has a minimum of 64 -/// entries/tick. This gives 4096 total rewards to store in one block. -/// This constant affects consensus. -const MAX_PARTITIONED_REWARDS_PER_BLOCK: u64 = 4096; +/// Baseline number of stake accounts to store in one 400ms block during the +/// partitioned reward interval. +/// +/// The target is 64 rewards per entry/tick. A block has a minimum of 64 +/// entries/ticks, giving 4096 total rewards to store in one 400ms block. This +/// constant affects consensus; shorter slot-time targets scale this value down +/// in `Bank` state. +pub const MAX_PARTITIONED_REWARDS_PER_BLOCK: u64 = 4096; -#[derive(Debug, Clone, Copy)] /// Configuration options for partitioned epoch rewards. +#[derive(Debug, Clone, Copy)] pub struct PartitionedEpochRewardsConfig { - /// number of stake accounts to store in one block during partitioned reward interval - /// normally, this is a number tuned for reasonable performance, such as 4096 accounts/block + /// Baseline number of stake accounts to store in one block during the + /// partitioned reward interval. + /// + /// This value is stored as the 400ms-slot baseline. Runtime `Bank` state + /// derives the effective per-bank value from the active slot-time target. pub stake_account_stores_per_block: u64, } @@ -30,7 +36,8 @@ impl Default for PartitionedEpochRewardsConfig { } impl PartitionedEpochRewardsConfig { - /// Only for tests and benchmarks + /// Constructs a config with an explicit 400ms-slot baseline for tests and + /// benchmarks. pub fn new_for_test(stake_account_stores_per_block: u64) -> Self { Self { stake_account_stores_per_block, diff --git a/feature-set/src/lib.rs b/feature-set/src/lib.rs index 611e4480ad9..f4e72cd3d59 100644 --- a/feature-set/src/lib.rs +++ b/feature-set/src/lib.rs @@ -1525,6 +1525,22 @@ pub mod set_lamports_per_byte_to_6960 { pub const LAMPORTS_PER_BYTE: u64 = 6960; } +pub mod reduce_slot_time_to_350ms { + solana_pubkey::declare_id!("iBRL2iJvhLssJveF1utbmmQGmjonmNYZALcJFEHTbUF"); +} + +pub mod reduce_slot_time_to_300ms { + solana_pubkey::declare_id!("iBRLA3zvd6x9445cK1vS7xt8n6Y7DS3otfRcDdW8JRW"); +} + +pub mod reduce_slot_time_to_250ms { + solana_pubkey::declare_id!("iBRLR6nG3fDi8YD4mpPTUVTgo5NaiYfZgrzokCfADP2"); +} + +pub mod reduce_slot_time_to_200ms { + solana_pubkey::declare_id!("iBRLypKvvj9VEvwoTeRpbLhbW55NFR4T3GE9BUR8A16"); +} + pub static FEATURE_NAMES: LazyLock> = LazyLock::new(|| { [ (secp256k1_program_enabled::id(), "secp256k1 program"), @@ -2415,6 +2431,22 @@ pub static FEATURE_NAMES: LazyLock> = LazyLock::n alpenglow::id(), "SIMD-0326: Alpenglow: new consensus algorithm", ), + ( + reduce_slot_time_to_350ms::id(), + "SIMD-0525: Reduce slot time to 350ms", + ), + ( + reduce_slot_time_to_300ms::id(), + "SIMD-0525: Reduce slot time to 300ms", + ), + ( + reduce_slot_time_to_250ms::id(), + "SIMD-0525: Reduce slot time to 250ms", + ), + ( + reduce_slot_time_to_200ms::id(), + "SIMD-0525: Reduce slot time to 200ms", + ), ( disable_zk_elgamal_proof_program::id(), "Disables zk-elgamal-proof program", diff --git a/ledger/src/shred/filter.rs b/ledger/src/shred/filter.rs index a78b3ad4dc3..65df4c45821 100644 --- a/ledger/src/shred/filter.rs +++ b/ledger/src/shred/filter.rs @@ -595,7 +595,10 @@ mod tests { itertools::Itertools, solana_leader_schedule::SlotLeader, solana_perf::packet::{Packet, PacketFlags}, - solana_runtime::bank::Bank, + solana_runtime::{ + bank::Bank, + slot_params::{slot_time_feature_gates, slot_time_feature_ids}, + }, std::{ io::{Cursor, Seek, SeekFrom, Write}, sync::Arc, @@ -620,6 +623,26 @@ mod tests { } } + fn deactivate_slot_time_features(bank: &mut Bank) { + for feature_id in slot_time_feature_ids() { + bank.deactivate_feature(&feature_id); + } + } + + fn shred_filter_for_tests( + feature_ids: impl IntoIterator, + ) -> (ShredFilterContext, Slot) { + let genesis_config = create_genesis_config(1).genesis_config; + let mut root_bank = Bank::new_for_tests(&genesis_config); + deactivate_slot_time_features(&mut root_bank); + for feature_id in feature_ids { + root_bank.activate_feature(&feature_id); + } + let first_epoch_slot = root_bank.epoch_schedule().get_first_slot_in_epoch(1); + let (root_bank, _) = root_bank.wrap_with_bank_forks_for_tests(); + (ShredFilterContext::new(root_bank, 0), first_epoch_slot) + } + #[test_case(true ; "last_in_slot")] #[test_case(false ; "not_last_in_slot")] fn test_should_discard_shred(is_last_in_slot: bool) { @@ -1046,6 +1069,36 @@ mod tests { } } + #[test] + fn test_shred_limit_for_slot_times() { + for (features, shred_limits) in std::iter::once((vec![], ShredLimits::DEFAULT)).chain( + slot_time_feature_gates().map(|(feature_id, params)| { + ( + vec![feature_id], + ShredLimits::new( + params.max_data_shreds_per_slot(), + params.max_code_shreds_per_slot(), + ), + ) + }), + ) { + let (shred_filter, effective_slot) = shred_filter_for_tests(features); + assert_eq!( + shred_filter.shred_limits(effective_slot.saturating_sub(1)), + ShredLimits::DEFAULT + ); + assert_eq!(shred_filter.shred_limits(effective_slot), shred_limits); + } + + let [reduce_to_350ms, _, _, reduce_to_200ms] = slot_time_feature_ids(); + let (shred_filter, effective_slot) = + shred_filter_for_tests([reduce_to_350ms, reduce_to_200ms]); + assert_eq!( + shred_filter.shred_limits(effective_slot), + ShredLimits::new(16_384, 16_384) + ); + } + #[test] fn test_data_complete_shred_index_validation() { agave_logger::setup(); diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index f6a2bb30881..8e60cfa6472 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -112,7 +112,7 @@ use { solana_cluster_type::ClusterType, solana_compute_budget::compute_budget::ComputeBudget, solana_compute_budget_instruction::instructions_processor::process_compute_budget_instructions, - solana_cost_model::{block_cost_limits::simd_0286_block_limit, cost_tracker::CostTracker}, + solana_cost_model::cost_tracker::CostTracker, solana_epoch_info::EpochInfo, solana_epoch_schedule::EpochSchedule, solana_feature_gate_interface as feature, @@ -179,7 +179,6 @@ use { solana_system_transaction as system_transaction, solana_sysvar::{self as sysvar, SysvarSerialize, last_restart_slot::LastRestartSlot}, solana_sysvar_id::SysvarId, - solana_time_utils::years_as_slots, solana_transaction::{ Transaction, TransactionVerificationMode, sanitized::{MAX_TX_ACCOUNT_LOCKS, MessageHash, SanitizedTransaction}, @@ -2180,20 +2179,7 @@ impl Bank { snapshot and genesis.bin might pertain to different clusters" ); assert_eq!(bank.ticks_per_slot, genesis_config.ticks_per_slot); - assert_eq!( - bank.ns_per_slot, - genesis_config.poh_config.target_tick_duration.as_nanos() - * genesis_config.ticks_per_slot as u128 - ); assert_eq!(bank.max_tick_height, (bank.slot + 1) * bank.ticks_per_slot); - assert_eq!( - bank.slots_per_year, - years_as_slots( - 1.0, - &genesis_config.poh_config.target_tick_duration, - bank.ticks_per_slot, - ) - ); assert_eq!(bank.epoch_schedule, genesis_config.epoch_schedule); bank.refresh_slot_params_with_baseline(Self::genesis_config_slot_params( @@ -2710,12 +2696,18 @@ impl Bank { }); } + /// Rebuilds slot-param state from the current feature set. + fn refresh_slot_params(&mut self) { + self.refresh_slot_params_with_baseline(self.slot_params.baseline_params()); + } + /// Rebuilds cached slot params while preserving the supplied slot-0 baseline. /// /// The cache is not serialized into snapshots; it is reconstructed from /// existing Bank fields during genesis and snapshot restore. fn refresh_slot_params_with_baseline(&mut self, baseline_params: SlotParams) { - self.slot_params = SlotParamsArchive::new(&self.epoch_schedule, baseline_params); + self.slot_params = + SlotParamsArchive::new(&self.feature_set, &self.epoch_schedule, baseline_params); } /// Builds slot-0 params from the genesis config. @@ -2736,6 +2728,11 @@ impl Bank { self.slot_params.params_at_slot(slot) } + /// Returns the slot params that should be effective for this bank's slot. + fn current_slot_params(&self) -> SlotParams { + self.slot_params_at_slot(self.slot) + } + /// Returns the effective slot duration for `slot`. pub fn ns_per_slot_at_slot(&self, slot: Slot) -> u128 { self.slot_params_at_slot(slot).ns_per_slot() @@ -4676,24 +4673,83 @@ impl Bank { self.rc.accounts.clone() } + /// Recomputes cost tracker limits from active feature state. fn apply_cost_tracker_limits_for_active_features(&mut self) { + let params = self.current_slot_params(); + let (account_cost_limit, block_cost_limit, vote_cost_limit, data_size_limit) = + params.cost_limits(self.feature_set.snapshot().raise_block_limits_to_100m); + let mut cost_tracker = self.write_cost_tracker().unwrap(); - let block_cost_limit = if self.feature_set.snapshot().raise_block_limits_to_100m { - simd_0286_block_limit() - } else { - cost_tracker.get_block_limit() - }; - let account_cost_limit = block_cost_limit.saturating_mul(40).saturating_div(100); - let vote_cost_limit = cost_tracker.get_vote_limit(); - let allocated_data_size_limit = cost_tracker.get_allocated_data_size_limit(); cost_tracker.set_limits( account_cost_limit, block_cost_limit, vote_cost_limit, - allocated_data_size_limit, + data_size_limit, + ); + } + + /// Recomputes this bank's effective partitioned-reward write budget. + fn apply_partitioned_epoch_rewards_config_for_active_features(&mut self) { + self.partitioned_rewards_stake_account_stores_per_block = self + .current_slot_params() + .partitioned_epoch_rewards_stake_account_stores_per_block(); + } + + /// Applies slot-time changes for fields serialized into snapshots. + fn apply_slot_time_persistent_changes(&mut self) { + let params = self.current_slot_params(); + self.ns_per_slot = params.ns_per_slot(); + self.slots_per_year = params.slots_per_year(); + self.rent_collector.slots_per_year = params.slots_per_year(); + if !self.feature_set.is_active(&feature_set::alpenglow::id()) + && self.hashes_per_tick().is_some() + { + self.set_hashes_per_tick(params.hashes_per_tick()); + } + } + + /// Verifies bank fields are consistent with current slot params. + fn assert_bank_matches_slot_params(&self) { + let params = self.current_slot_params(); + assert_eq!( + self.ns_per_slot, + params.ns_per_slot(), + "snapshot slot-time ns_per_slot mismatch" + ); + assert_eq!( + self.slots_per_year.to_bits(), + params.slots_per_year().to_bits(), + "snapshot slot-time slots_per_year mismatch" + ); + assert_eq!( + self.rent_collector.slots_per_year.to_bits(), + params.slots_per_year().to_bits(), + "snapshot slot-time rent_collector.slots_per_year mismatch" + ); + let hashes_per_tick = self.hashes_per_tick(); + if !self.feature_set.is_active(&feature_set::alpenglow::id()) && hashes_per_tick.is_some() { + assert_eq!( + hashes_per_tick, + params.hashes_per_tick(), + "snapshot slot-time hashes_per_tick mismatch" + ); + } + assert_eq!( + self.entry_bytes_budget().slot_limit(), + params.max_entry_bytes_per_slot(), + "snapshot slot-time entry byte budget mismatch" ); } + /// Applies slot-time changes for runtime-only fields. This function is + /// expected to be idempotent. + fn apply_slot_time_runtime_changes(&mut self) { + self.entry_bytes_consumed = + EntryBytesBudget::new(self.current_slot_params().max_entry_bytes_per_slot()); + self.apply_cost_tracker_limits_for_active_features(); + self.apply_partitioned_epoch_rewards_config_for_active_features(); + } + fn apply_simd_0339_invoke_cost_changes(&mut self) { let simd_0268_active = self.feature_set.snapshot().raise_cpi_nesting_limit_to_8; let compute_budget = self @@ -4715,11 +4771,10 @@ impl Bank { Arc::new(reserved_keys) }; - // Cost-Tracker is not serialized in snapshot or any configs. - // We must apply previously activated features related to limits here - // so that the initial bank state is consistent with the feature set. - // Cost-tracker limits are propagated through children banks. - self.apply_cost_tracker_limits_for_active_features(); + // Many fields are not serialized in snapshot or any configs. Rebuild + // them from the feature set so the initial bank state is consistent. + self.refresh_slot_params(); + self.apply_slot_time_runtime_changes(); self.apply_simd_0339_invoke_cost_changes(); let program_runtime_environment = @@ -5814,12 +5869,14 @@ impl Bank { let mut feature_set = Arc::make_mut(&mut self.feature_set).clone(); feature_set.deactivate(id); self.feature_set = Arc::new(feature_set); + self.refresh_slot_params(); } pub fn activate_feature(&mut self, id: &Pubkey) { let mut feature_set = Arc::make_mut(&mut self.feature_set).clone(); feature_set.activate(id, 0); self.feature_set = Arc::new(feature_set); + self.refresh_slot_params(); } pub fn fill_bank_with_ticks_for_tests(&self) { @@ -5895,6 +5952,7 @@ impl Bank { self.feature_set = Arc::new(feature_set); self.apply_activated_features(); + self.assert_bank_matches_slot_params(); } /// This is called from each epoch boundary @@ -5903,6 +5961,7 @@ impl Bank { let (feature_set, new_feature_activations) = self.compute_active_feature_set(include_pending); self.feature_set = Arc::new(feature_set); + self.refresh_slot_params(); // Update activation slot of features in `new_feature_activations` for feature_id in new_feature_activations.iter() { @@ -5985,10 +6044,11 @@ impl Bank { self.fee_rate_governor.burn_percent = solana_fee_calculator::DEFAULT_BURN_PERCENT; } + // Apply unconditionally: this is relatively cheap and idempotent. + self.apply_slot_time_persistent_changes(); + self.apply_slot_time_runtime_changes(); + self.apply_new_builtin_program_feature_transitions(&new_feature_activations); - if new_feature_activations.contains(&feature_set::raise_block_limits_to_100m::id()) { - self.apply_cost_tracker_limits_for_active_features(); - } if new_feature_activations.contains(&feature_set::replace_spl_token_with_p_token::id()) { if let Err(e) = self.upgrade_loader_v2_program_with_loader_v3_program( @@ -6943,6 +7003,11 @@ impl Bank { minimized_account_set.insert(*pubkey); }); } + + /// Returns true when this bank is using slot params beyond its genesis baseline. + pub fn slot_time_reduction_active(&self) -> bool { + self.ns_per_slot != self.slot_params.baseline_params().ns_per_slot() + } } /// Returns a thread pool intended to be used for reward calculation. This diff --git a/runtime/src/bank/partitioned_epoch_rewards/mod.rs b/runtime/src/bank/partitioned_epoch_rewards/mod.rs index 9218ceac4e7..e7492dbc4a6 100644 --- a/runtime/src/bank/partitioned_epoch_rewards/mod.rs +++ b/runtime/src/bank/partitioned_epoch_rewards/mod.rs @@ -417,6 +417,7 @@ mod tests { bank_forks::BankForks, genesis_utils::{ GenesisConfigInfo, ValidatorVoteKeypairs, create_genesis_config_with_vote_accounts, + deactivate_features, }, runtime_config::RuntimeConfig, stake_utils, @@ -574,6 +575,9 @@ mod tests { stake_account_stores_per_block: u64, advance_num_slots: u64, ) -> (RewardBank, Arc>) { + // Disable slot time reduction features as they will override the custom + // stores per block provided in this test helper. + let features_to_deactivate = crate::slot_params::slot_time_feature_ids().to_vec(); let validator_keypairs = (0..stakes.len()) .map(|_| ValidatorVoteKeypairs::new_rand()) .collect::>(); @@ -582,6 +586,7 @@ mod tests { mut genesis_config, .. } = create_genesis_config_with_vote_accounts(1_000_000_000, &validator_keypairs, stakes); genesis_config.epoch_schedule = EpochSchedule::new(SLOTS_PER_EPOCH); + deactivate_features(&mut genesis_config, &features_to_deactivate); let mut accounts_db_config: AccountsDbConfig = ACCOUNTS_DB_CONFIG_FOR_TESTING; accounts_db_config.partitioned_epoch_rewards_config = diff --git a/runtime/src/bank/tests.rs b/runtime/src/bank/tests.rs index 0352704a421..b8763acd85d 100644 --- a/runtime/src/bank/tests.rs +++ b/runtime/src/bank/tests.rs @@ -19,7 +19,11 @@ use { }, runtime_config::RuntimeConfig, serde_snapshot::fields_from_stream, - slot_params::{DEFAULT_MAX_ENTRY_BYTES_PER_SLOT, LEGACY_SLOT_PARAMS}, + slot_params::{ + DEFAULT_MAX_ENTRY_BYTES_PER_SLOT, LEGACY_HASHES_PER_TICK, LEGACY_SLOT_PARAMS, + SLOT_PARAMS_200MS, SLOT_PARAMS_250MS, SLOT_PARAMS_300MS, SLOT_PARAMS_350MS, SlotParams, + slot_time_feature_gates, slot_time_feature_ids, + }, stake_history::StakeHistory, stake_utils, stakes::{DeserializableStakes, InvalidCacheEntryReason, SerdeStakesToStakeFormat, Stakes}, @@ -180,6 +184,12 @@ pub(in crate::bank) fn create_genesis_config(lamports: u64) -> (GenesisConfig, K solana_genesis_config::create_genesis_config(lamports) } +fn create_genesis_config_with_legacy_hashes(lamports: u64) -> (GenesisConfig, Keypair) { + let (mut genesis_config, mint_keypair) = create_genesis_config(lamports); + genesis_config.poh_config.hashes_per_tick = Some(LEGACY_HASHES_PER_TICK); + (genesis_config, mint_keypair) +} + pub(in crate::bank) fn new_sanitized_message(message: Message) -> SanitizedMessage { SanitizedMessage::try_from_legacy_message(message, &ReservedAccountKeys::empty_key_set()) .unwrap() @@ -5257,9 +5267,9 @@ fn test_bank_hash_consistency(deprecate_rent_exemption_threshold: bool) { assert_eq!( bank.hash().to_string(), if deprecate_rent_exemption_threshold { - "F4ns41hFn3ignVHqaVehaTZ8LMUxPaELAgvN8iTcowDD" + "9ycftRwjpQ17PrhnwhGPbVzDKd3Q9BybmLYU8UD1Pg1T" } else { - "FyxeAqHjieaKMA6mLK3yfSD46Xcw2U6hzKfyeu1qVuCD" + "4XzjZMjhP9s8iBeFQsnHFwaQ991dpiBGHEkzj1ifAcpS" }, ); } @@ -5268,9 +5278,9 @@ fn test_bank_hash_consistency(deprecate_rent_exemption_threshold: bool) { assert_eq!( bank.hash().to_string(), if deprecate_rent_exemption_threshold { - "sF1tuUv3r5JCr9smYyhm9Qv27f5jcoJ75LnoLGV5Scd" + "7sbqfkN4W3PkBREyduDc3R2eiKu68JKYRXZt6gCWNt4N" } else { - "DrpEs59czmKpLQvR9kBjSmWJUYMNKDdQ4Zsyu4WjfoPb" + "ArB3XNVLJsyV7AtrG3C3Bj4akb8s7AzpS5rHJnqGW5sw" }, ); break; @@ -6296,6 +6306,272 @@ fn test_block_limits() { ); } +fn assert_slot_time_bank_state(bank: &Bank, params: SlotParams) { + assert_eq!(bank.ns_per_slot, params.ns_per_slot()); + assert_eq!(bank.hashes_per_tick(), params.hashes_per_tick()); + assert_eq!( + bank.slots_per_year.to_bits(), + params.slots_per_year().to_bits() + ); + assert_eq!( + bank.rent_collector.slots_per_year.to_bits(), + bank.slots_per_year.to_bits() + ); + assert_eq!( + bank.partitioned_rewards_stake_account_stores_per_block, + params.partitioned_epoch_rewards_stake_account_stores_per_block() + ); + assert_eq!( + bank.entry_bytes_budget().slot_limit(), + params.max_entry_bytes_per_slot() + ); + let cost_tracker = bank.read_cost_tracker().unwrap(); + let (account_limit, block_limit, vote_limit, data_size_limit) = params.cost_limits( + bank.feature_set + .is_active(&feature_set::raise_block_limits_to_100m::id()), + ); + assert_eq!(cost_tracker.get_account_limit(), account_limit); + assert_eq!(cost_tracker.get_block_limit(), block_limit); + assert_eq!(cost_tracker.get_vote_limit(), vote_limit); + assert_eq!( + cost_tracker.get_allocated_data_size_limit(), + data_size_limit + ); +} + +#[test] +fn test_reduce_slot_time_features() { + let (genesis_config, _) = create_genesis_config_with_legacy_hashes(1_000_000); + let feature_account_balance = genesis_config.rent.minimum_balance(Feature::size_of()); + + let (mut bank, bank_forks) = Bank::new_with_bank_forks_for_tests(&genesis_config); + assert!(!bank.slot_time_reduction_active()); + assert_eq!(bank.hashes_per_tick(), Some(LEGACY_HASHES_PER_TICK)); + + for (feature_id, params) in slot_time_feature_gates() { + let previous_params = bank.current_slot_params(); + bank.store_account( + &feature_id, + &feature::create_account(&Feature::default(), feature_account_balance), + ); + + let activation_slot = bank + .epoch_schedule() + .get_first_slot_in_epoch(bank.epoch().saturating_add(1)); + let activation_bank = Bank::new_from_parent_with_bank_forks( + &bank_forks, + bank, + SlotLeader::default(), + activation_slot, + ); + + assert!(activation_bank.feature_set.is_active(&feature_id)); + assert_eq!(activation_bank.current_slot_params(), previous_params); + assert_slot_time_bank_state(&activation_bank, previous_params); + + let effective_slot = activation_bank + .epoch_schedule() + .get_first_slot_in_epoch(activation_bank.epoch().saturating_add(1)); + bank = Bank::new_from_parent_with_bank_forks( + &bank_forks, + activation_bank, + SlotLeader::default(), + effective_slot, + ); + assert!(bank.slot_time_reduction_active()); + assert_slot_time_bank_state(&bank, params); + } +} + +#[test] +fn test_reduce_slot_time_features_active_at_genesis() { + let (mut genesis_config, _) = create_genesis_config_with_legacy_hashes(1_000_000); + for feature_id in slot_time_feature_ids() { + activate_feature(&mut genesis_config, feature_id); + } + activate_feature( + &mut genesis_config, + feature_set::raise_block_limits_to_100m::id(), + ); + + let (bank, bank_forks) = Bank::new_with_bank_forks_for_tests(&genesis_config); + assert!(!bank.slot_time_reduction_active()); + assert_slot_time_bank_state(&bank, LEGACY_SLOT_PARAMS); + + let effective_slot = bank.epoch_schedule().get_first_slot_in_epoch(1); + let bank = Bank::new_from_parent_with_bank_forks( + &bank_forks, + bank, + SlotLeader::default(), + effective_slot, + ); + assert_slot_time_bank_state(&bank, SLOT_PARAMS_200MS); +} + +#[test] +fn test_reduce_slot_time_hashes_per_tick() { + let assert_reduced_slot_time_hashes_per_tick = + |genesis_config, expected_initial_hashes_per_tick, expected_effective_hashes_per_tick| { + let (bank, bank_forks) = Bank::new_with_bank_forks_for_tests(&genesis_config); + assert_eq!(bank.ns_per_slot, LEGACY_SLOT_PARAMS.ns_per_slot()); + assert_eq!(bank.hashes_per_tick(), expected_initial_hashes_per_tick); + + let effective_slot = bank.epoch_schedule().get_first_slot_in_epoch(1); + let bank = Bank::new_from_parent_with_bank_forks( + &bank_forks, + bank, + SlotLeader::default(), + effective_slot, + ); + assert_eq!(bank.ns_per_slot, SLOT_PARAMS_200MS.ns_per_slot()); + assert_eq!(bank.hashes_per_tick(), expected_effective_hashes_per_tick); + }; + + let (mut genesis_config, _) = create_genesis_config_with_legacy_hashes(1_000_000); + activate_feature( + &mut genesis_config, + feature_set::reduce_slot_time_to_200ms::id(), + ); + assert_reduced_slot_time_hashes_per_tick( + genesis_config, + Some(LEGACY_HASHES_PER_TICK), + SLOT_PARAMS_200MS.hashes_per_tick(), + ); + + let (mut genesis_config, _) = create_genesis_config_with_legacy_hashes(1_000_000); + genesis_utils::activate_all_features_alpenglow(&mut genesis_config); + assert_eq!(genesis_config.poh_config.hashes_per_tick, None); + assert_reduced_slot_time_hashes_per_tick(genesis_config, None, None); +} + +#[test] +fn test_reduce_slot_time_range_duration() { + const SLOTS_PER_EPOCH: Slot = 32; + let bank_for_activations = |activations: &[(Pubkey, Slot)]| { + let (mut genesis_config, _) = create_genesis_config_with_legacy_hashes(1_000_000); + genesis_config.epoch_schedule = + EpochSchedule::custom(SLOTS_PER_EPOCH, SLOTS_PER_EPOCH, false); + let mut bank = Bank::new_for_tests(&genesis_config); + let mut feature_set = FeatureSet::default(); + for (feature_id, activation_slot) in activations { + feature_set.activate(feature_id, *activation_slot); + } + bank.feature_set = Arc::new(feature_set); + bank.refresh_slot_params(); + bank + }; + + let ordered_activations = slot_time_feature_gates() + .into_iter() + .zip([1, 33, 65, 97]) + .map(|((feature_id, _), activation_slot)| (feature_id, activation_slot)) + .collect::>(); + let bank = bank_for_activations(&ordered_activations); + + let expected_duration = [ + (SLOTS_PER_EPOCH, LEGACY_SLOT_PARAMS), + (SLOTS_PER_EPOCH, SLOT_PARAMS_350MS), + (SLOTS_PER_EPOCH, SLOT_PARAMS_300MS), + (SLOTS_PER_EPOCH, SLOT_PARAMS_250MS), + (SLOTS_PER_EPOCH, SLOT_PARAMS_200MS), + ] + .into_iter() + .map(|(slots, params)| slots as f64 / params.slots_per_year()) + .sum::(); + + assert_eq!( + bank.slot_range_duration_in_years(0, SLOTS_PER_EPOCH * 5) + .to_bits(), + expected_duration.to_bits() + ); + assert_eq!( + bank.slot_range_duration_nanos(0, SLOTS_PER_EPOCH * 5 - 1), + [ + (SLOTS_PER_EPOCH, LEGACY_SLOT_PARAMS), + (SLOTS_PER_EPOCH, SLOT_PARAMS_350MS), + (SLOTS_PER_EPOCH, SLOT_PARAMS_300MS), + (SLOTS_PER_EPOCH, SLOT_PARAMS_250MS), + (SLOTS_PER_EPOCH, SLOT_PARAMS_200MS), + ] + .into_iter() + .map(|(slots, params)| u128::from(slots) * params.ns_per_slot()) + .sum::() + ); + + let [reduce_to_350ms, _, _, reduce_to_200ms] = slot_time_feature_ids(); + let bank = + bank_for_activations(&[(reduce_to_200ms, 1), (reduce_to_350ms, SLOTS_PER_EPOCH + 1)]); + assert_eq!( + bank.slot_params_at_slot(SLOTS_PER_EPOCH - 1), + LEGACY_SLOT_PARAMS + ); + assert_eq!(bank.slot_params_at_slot(SLOTS_PER_EPOCH), SLOT_PARAMS_200MS); + assert_eq!( + bank.slot_params_at_slot(SLOTS_PER_EPOCH * 2), + SLOT_PARAMS_200MS + ); + assert_eq!( + bank.slot_range_duration_in_years(0, SLOTS_PER_EPOCH * 3) + .to_bits(), + (SLOTS_PER_EPOCH as f64 / LEGACY_SLOT_PARAMS.slots_per_year() + + (SLOTS_PER_EPOCH * 2) as f64 / SLOT_PARAMS_200MS.slots_per_year()) + .to_bits() + ); + assert_eq!( + bank.slot_range_duration_nanos(0, SLOTS_PER_EPOCH * 3 - 1), + u128::from(SLOTS_PER_EPOCH) * LEGACY_SLOT_PARAMS.ns_per_slot() + + u128::from(SLOTS_PER_EPOCH * 2) * SLOT_PARAMS_200MS.ns_per_slot() + ); +} + +#[test] +fn test_update_clock_slot_range_duration() { + const SLOTS_PER_EPOCH: Slot = 32; + + let leader_pubkey = solana_pubkey::new_rand(); + let GenesisConfigInfo { + mut genesis_config, + voting_keypair, + .. + } = create_genesis_config_with_leader(5, &leader_pubkey, 3); + genesis_config.epoch_schedule = EpochSchedule::custom(SLOTS_PER_EPOCH, SLOTS_PER_EPOCH, false); + activate_feature( + &mut genesis_config, + feature_set::reduce_slot_time_to_200ms::id(), + ); + + let (bank, bank_forks) = Bank::new_with_bank_forks_for_tests(&genesis_config); + let vote_timestamp_slot = 4; + let vote_timestamp = bank.unix_timestamp_from_genesis(); + update_vote_account_timestamp( + BlockTimestamp { + slot: vote_timestamp_slot, + timestamp: vote_timestamp, + }, + &bank, + &voting_keypair.pubkey(), + ); + + let current_slot = SLOTS_PER_EPOCH + 3; + let bank = Bank::new_from_parent_with_bank_forks( + &bank_forks, + bank, + SlotLeader::default(), + current_slot, + ); + let expected_elapsed = Duration::from_nanos_u128( + bank.slot_range_duration_nanos(vote_timestamp_slot + 1, current_slot), + ); + let scalar_elapsed = + Duration::from_nanos(bank.ns_per_slot as u64) * (current_slot - vote_timestamp_slot) as u32; + + assert_ne!(expected_elapsed.as_secs(), scalar_elapsed.as_secs()); + assert_eq!( + bank.clock().unix_timestamp, + vote_timestamp + expected_elapsed.as_secs() as i64 + ); +} + #[test] fn test_rent_feature_gates_epoch_transition() { let (mut genesis_config, _mint_keypair) = create_genesis_config(1_000_000); diff --git a/runtime/src/block_component_processor/vote_reward/epoch_inflation_account_state.rs b/runtime/src/block_component_processor/vote_reward/epoch_inflation_account_state.rs index 1c6c95d4de5..c68be91a25b 100644 --- a/runtime/src/block_component_processor/vote_reward/epoch_inflation_account_state.rs +++ b/runtime/src/block_component_processor/vote_reward/epoch_inflation_account_state.rs @@ -155,9 +155,11 @@ mod tests { bank_forks::BankForks, genesis_utils::{ GenesisConfigInfo, ValidatorVoteKeypairs, create_genesis_config, - create_genesis_config_with_alpenglow_vote_accounts, + create_genesis_config_with_alpenglow_vote_accounts, deactivate_features, }, + slot_params::slot_time_feature_ids, }, + agave_feature_set as feature_set, rand::Rng, solana_epoch_schedule::EpochSchedule, solana_genesis_config::GenesisConfig, @@ -231,7 +233,7 @@ mod tests { } #[test] - fn new_epoch_update_account_use_current_reward_budget() { + fn new_epoch_update_account_uses_current_epoch_rewards() { let GenesisConfigInfo { mut genesis_config, .. } = create_genesis_config(1_000_000_000); @@ -239,46 +241,71 @@ mod tests { // Warmup is necessary here to give epoch 0 and epoch 1 different reward // budgets. genesis_config.epoch_schedule = EpochSchedule::custom(64, 64, true); + deactivate_features(&mut genesis_config, &slot_time_feature_ids().to_vec()); - let bank_forks = BankForks::new_rw_arc(Bank::new_for_tests(&genesis_config)); - let bank_epoch_0 = bank_forks.read().unwrap().root_bank(); - let epoch_1_first_slot = bank_epoch_0.epoch_schedule().get_first_slot_in_epoch(1); - let bank_epoch_1 = Bank::new_from_parent_with_bank_forks( - bank_forks.as_ref(), - bank_epoch_0.clone(), - SlotLeader::new_unique(), - epoch_1_first_slot, - ); + for (case, genesis_config, activate_slot_time_feature) in [ + ("current reward budget", genesis_config, false), + ( + "current epoch slot params", + GenesisConfig { + inflation: Inflation::full(), + ..GenesisConfig::default() + }, + true, + ), + ] { + let mut bank_epoch_0 = Bank::new_for_tests(&genesis_config); + if activate_slot_time_feature { + bank_epoch_0.activate_feature(&feature_set::reduce_slot_time_to_350ms::id()); + } + let bank_forks = BankForks::new_rw_arc(bank_epoch_0); + let bank_epoch_0 = bank_forks.read().unwrap().root_bank(); + let epoch_1_first_slot = bank_epoch_0.epoch_schedule().get_first_slot_in_epoch(1); + let bank_epoch_1 = Bank::new_from_parent_with_bank_forks( + bank_forks.as_ref(), + bank_epoch_0.clone(), + SlotLeader::new_unique(), + epoch_1_first_slot, + ); - assert_eq!(bank_epoch_0.epoch(), 0); - assert_eq!(bank_epoch_1.epoch(), 1); - assert_ne!( - bank_epoch_1.epoch_schedule().get_slots_in_epoch(0), - bank_epoch_1.epoch_schedule().get_slots_in_epoch(1) - ); + assert_eq!(bank_epoch_0.epoch(), 0, "{case}"); + assert_eq!(bank_epoch_1.epoch(), 1, "{case}"); + if activate_slot_time_feature { + assert_ne!(bank_epoch_0.ns_per_slot, bank_epoch_1.ns_per_slot, "{case}"); + } else { + assert_ne!( + bank_epoch_1.epoch_schedule().get_slots_in_epoch(0), + bank_epoch_1.epoch_schedule().get_slots_in_epoch(1), + "{case}", + ); + } - let epoch_start_capitalization = bank_epoch_0.capitalization(); - let expected_current_epoch_rewards = bank_epoch_1 - .calculate_epoch_inflation_rewards(epoch_start_capitalization, bank_epoch_1.epoch()); - assert_ne!( - expected_current_epoch_rewards, - bank_epoch_1.calculate_epoch_inflation_rewards( + let epoch_start_capitalization = bank_epoch_0.capitalization(); + let expected_current_epoch_rewards = bank_epoch_1.calculate_epoch_inflation_rewards( epoch_start_capitalization, - bank_epoch_0.epoch() - ) - ); + bank_epoch_1.epoch(), + ); + assert_ne!( + expected_current_epoch_rewards, + bank_epoch_1.calculate_epoch_inflation_rewards( + epoch_start_capitalization, + bank_epoch_0.epoch() + ), + "{case}", + ); - EpochInflationAccountState::new_epoch_update_account( - &bank_epoch_1, - epoch_start_capitalization, - 0, - ); - let state = EpochInflationAccountState::new_from_bank(&bank_epoch_1).unwrap(); - assert_eq!(state.current.epoch, bank_epoch_1.epoch()); - assert_eq!( - state.current.max_possible_validator_reward, - expected_current_epoch_rewards - ); + EpochInflationAccountState::new_epoch_update_account( + &bank_epoch_1, + epoch_start_capitalization, + 0, + ); + let state = EpochInflationAccountState::new_from_bank(&bank_epoch_1).unwrap(); + assert_eq!(state.current.epoch, bank_epoch_1.epoch(), "{case}"); + assert_eq!( + state.current.max_possible_validator_reward, expected_current_epoch_rewards, + "{case}", + ); + } } #[test] diff --git a/runtime/src/slot_params.rs b/runtime/src/slot_params.rs index 0e25bb31ec9..a5615b073c6 100644 --- a/runtime/src/slot_params.rs +++ b/runtime/src/slot_params.rs @@ -1,6 +1,8 @@ use { + agave_feature_set::{self as feature_set, FeatureSet}, solana_clock::Slot, solana_epoch_schedule::EpochSchedule, + solana_pubkey::Pubkey, std::{ collections::BTreeMap, ops::Bound::{Excluded, Included}, @@ -11,10 +13,8 @@ pub const DEFAULT_MAX_ENTRY_BYTES_PER_SLOT: u64 = 20 * 1024 * 1024; // 20 MiB /// Runtime parameters tied to a slot-time regime. /// -/// This intentionally groups values that need to move together when slot time -/// changes. For now, only the legacy baseline exists, so routing through this -/// type preserves current behavior while giving future feature-gated changes a -/// single table-shaped home. +/// Slot-time reduction features select explicit table values instead of +/// deriving ratios at runtime, which avoids rounding drift in consensus paths. #[derive(Clone, Copy, Debug, PartialEq)] pub struct SlotParams { pub(crate) ns_per_slot: u128, @@ -87,20 +87,35 @@ impl SlotParams { } /// Returns the per-bank cost limits for these params. - pub const fn cost_limits(self) -> (u64, u64, u64, u64) { + pub(crate) const fn cost_limits( + self, + raise_block_limits_to_100m: bool, + ) -> (u64, u64, u64, u64) { + let (max_writable_account_units, max_block_units) = if raise_block_limits_to_100m { + ( + self.max_writable_account_units + .saturating_mul(100) + .saturating_div(60), + self.max_block_units.saturating_mul(100).saturating_div(60), + ) + } else { + (self.max_writable_account_units, self.max_block_units) + }; + ( - self.max_writable_account_units, - self.max_block_units, + max_writable_account_units, + max_block_units, self.max_vote_units, self.max_block_accounts_data_size_delta, ) } } +pub const LEGACY_HASHES_PER_TICK: u64 = 62_500; pub(crate) const LEGACY_SLOT_PARAMS: SlotParams = SlotParams { ns_per_slot: 400_000_000, slots_per_year: 78_892_314.984, - hashes_per_tick: Some(62_500), + hashes_per_tick: Some(LEGACY_HASHES_PER_TICK), max_block_units: 60_000_000, max_writable_account_units: 24_000_000, max_vote_units: 36_000_000, @@ -111,6 +126,92 @@ pub(crate) const LEGACY_SLOT_PARAMS: SlotParams = SlotParams { partitioned_epoch_rewards_stake_account_stores_per_block: 4096, }; +pub(crate) const SLOT_PARAMS_350MS: SlotParams = SlotParams { + ns_per_slot: 350_000_000, + slots_per_year: 90_162_645.696, + hashes_per_tick: Some(54_687), + max_block_units: 52_500_000, + max_writable_account_units: 21_000_000, + max_vote_units: 31_500_000, + max_block_accounts_data_size_delta: 87_500_000, + max_data_shreds_per_slot: 28_672, + max_code_shreds_per_slot: 28_672, + max_entry_bytes_per_slot: 18_350_080, + partitioned_epoch_rewards_stake_account_stores_per_block: 3_584, +}; + +pub(crate) const SLOT_PARAMS_300MS: SlotParams = SlotParams { + ns_per_slot: 300_000_000, + slots_per_year: 105_189_753.312, + hashes_per_tick: Some(46_875), + max_block_units: 45_000_000, + max_writable_account_units: 18_000_000, + max_vote_units: 27_000_000, + max_block_accounts_data_size_delta: 75_000_000, + max_data_shreds_per_slot: 24_576, + max_code_shreds_per_slot: 24_576, + max_entry_bytes_per_slot: 15_728_640, + partitioned_epoch_rewards_stake_account_stores_per_block: 3_072, +}; + +pub(crate) const SLOT_PARAMS_250MS: SlotParams = SlotParams { + ns_per_slot: 250_000_000, + slots_per_year: 126_227_703.974, + hashes_per_tick: Some(39_062), + max_block_units: 37_500_000, + max_writable_account_units: 15_000_000, + max_vote_units: 22_500_000, + max_block_accounts_data_size_delta: 62_500_000, + max_data_shreds_per_slot: 20_480, + max_code_shreds_per_slot: 20_480, + max_entry_bytes_per_slot: 13_107_200, + partitioned_epoch_rewards_stake_account_stores_per_block: 2_560, +}; + +pub(crate) const SLOT_PARAMS_200MS: SlotParams = SlotParams { + ns_per_slot: 200_000_000, + slots_per_year: 157_784_629.968, + hashes_per_tick: Some(31_250), + max_block_units: 30_000_000, + max_writable_account_units: 12_000_000, + max_vote_units: 18_000_000, + max_block_accounts_data_size_delta: 50_000_000, + max_data_shreds_per_slot: 16_384, + max_code_shreds_per_slot: 16_384, + max_entry_bytes_per_slot: 10_485_760, + partitioned_epoch_rewards_stake_account_stores_per_block: 2_048, +}; + +/// Slot-time reduction gates in the intended activation order. +const SLOT_TIME_REDUCTION_PARAMS: [(Pubkey, SlotParams); 4] = [ + ( + feature_set::reduce_slot_time_to_350ms::ID, + SLOT_PARAMS_350MS, + ), + ( + feature_set::reduce_slot_time_to_300ms::ID, + SLOT_PARAMS_300MS, + ), + ( + feature_set::reduce_slot_time_to_250ms::ID, + SLOT_PARAMS_250MS, + ), + ( + feature_set::reduce_slot_time_to_200ms::ID, + SLOT_PARAMS_200MS, + ), +]; + +/// Returns slot-time feature gates mapped to runtime slot parameters. +pub fn slot_time_feature_gates() -> [(Pubkey, SlotParams); 4] { + SLOT_TIME_REDUCTION_PARAMS +} + +/// Returns all slot-time reduction feature IDs in activation order. +pub fn slot_time_feature_ids() -> [Pubkey; 4] { + SLOT_TIME_REDUCTION_PARAMS.map(|(feature_id, _)| feature_id) +} + /// Bank-local archive of slot-parameter transitions. /// /// This is rebuilt from feature accounts on startup instead of being serialized @@ -124,25 +225,49 @@ pub(crate) struct SlotParamsArchive { /// This is used when a bank, usually the root bank, must answer "which /// parameters apply to some other slot?" Examples include shred filtering /// for incoming shreds and inflation calculations that span historical slot - /// ranges. + /// ranges. The transitions are normalized so slot duration never increases + /// as slots advance, even if slot-time features activate out of order. param_transitions: BTreeMap, } impl Default for SlotParamsArchive { fn default() -> Self { - Self::new(&EpochSchedule::default(), LEGACY_SLOT_PARAMS) + Self::new( + &FeatureSet::default(), + &EpochSchedule::default(), + LEGACY_SLOT_PARAMS, + ) } } impl SlotParamsArchive { - /// Rebuilds slot-parameter transitions from the supplied baseline. - /// - /// `_epoch_schedule` is accepted now so the call sites already have the - /// right shape for delayed, epoch-boundary-effective feature transitions. - pub(crate) fn new(_epoch_schedule: &EpochSchedule, baseline_params: SlotParams) -> Self { - Self { - param_transitions: BTreeMap::from([(0, baseline_params)]), + /// Rebuilds slot-parameter transitions from the active feature set. + pub(crate) fn new( + feature_set: &FeatureSet, + epoch_schedule: &EpochSchedule, + baseline_params: SlotParams, + ) -> Self { + let mut param_transitions = BTreeMap::from([(0, baseline_params)]); + let mut earliest_same_or_shorter_slot = Slot::MAX; + + // The feature table is ordered longest-to-shortest. Walk it in reverse + // so once a same-or-shorter target is effective at slot S, any longer + // target effective at S or later is known to be redundant. + for (feature_id, params) in slot_time_feature_gates().into_iter().rev() { + if params.ns_per_slot > baseline_params.ns_per_slot { + continue; + } + let Some(activation_slot) = feature_set.activated_slot(&feature_id) else { + continue; + }; + let effective_slot = Self::feature_effective_slot(epoch_schedule, activation_slot); + if effective_slot < earliest_same_or_shorter_slot { + param_transitions.insert(effective_slot, params); + earliest_same_or_shorter_slot = effective_slot; + } } + + Self { param_transitions } } /// Returns the baseline params supplied at genesis or snapshot restore. @@ -195,4 +320,41 @@ impl SlotParamsArchive { let remaining_slots = u128::from(end_slot.saturating_sub(cursor)) + 1; duration.saturating_add(remaining_slots.saturating_mul(params.ns_per_slot())) } + + /// Returns the first slot where a slot-time feature may affect bank state. + /// + /// A gate that activates in epoch E is effective starting at the first slot + /// of epoch E + 1, giving shred filters a full epoch of advance notice + /// before enforcing lower shred limits. + fn feature_effective_slot(epoch_schedule: &EpochSchedule, activation_slot: Slot) -> Slot { + let activation_epoch = epoch_schedule.get_epoch(activation_slot); + epoch_schedule.get_first_slot_in_epoch(activation_epoch.saturating_add(1)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_cost_limits_scaling_matches_simd_0525() { + for (params, expected_account_limit, expected_block_limit) in [ + (LEGACY_SLOT_PARAMS, 40_000_000, 100_000_000), + (SLOT_PARAMS_350MS, 35_000_000, 87_500_000), + (SLOT_PARAMS_300MS, 30_000_000, 75_000_000), + (SLOT_PARAMS_250MS, 25_000_000, 62_500_000), + (SLOT_PARAMS_200MS, 20_000_000, 50_000_000), + ] { + let (_, _, vote_limit, data_size_limit) = params.cost_limits(false); + assert_eq!( + params.cost_limits(true), + ( + expected_account_limit, + expected_block_limit, + vote_limit, + data_size_limit + ) + ); + } + } } diff --git a/turbine/src/broadcast_stage/standard_broadcast_run.rs b/turbine/src/broadcast_stage/standard_broadcast_run.rs index ef1bb8525a5..2e103859f56 100644 --- a/turbine/src/broadcast_stage/standard_broadcast_run.rs +++ b/turbine/src/broadcast_stage/standard_broadcast_run.rs @@ -676,7 +676,12 @@ mod test { shred::{DATA_SHREDS_PER_FEC_BLOCK, max_ticks_per_n_shreds}, }, solana_net_utils::{SocketAddrSpace, sockets::bind_to_localhost_unique}, - solana_runtime::bank::Bank, + solana_pubkey::Pubkey, + solana_runtime::{ + bank::{Bank, SlotLeader}, + genesis_utils::{activate_feature, deactivate_features}, + slot_params::{slot_time_feature_gates, slot_time_feature_ids}, + }, solana_signer::Signer, std::{ops::Deref, sync::Arc, time::Duration}, test_case::test_case, @@ -740,6 +745,65 @@ mod test { Arc::new(LeaderScheduleCache::new_from_bank(bank)) } + fn broadcast_shred_limits_for_slot_time_features( + feature_ids: impl IntoIterator, + ) -> (u32, u32) { + let ledger_path = get_tmp_ledger_path!(); + let blockstore = Arc::new( + Blockstore::open(&ledger_path).expect("Expected to be able to open database ledger"), + ); + let mut genesis_config = create_genesis_config(10_000).genesis_config; + let slot_time_features = slot_time_feature_ids().to_vec(); + deactivate_features(&mut genesis_config, &slot_time_features); + for feature_id in feature_ids { + activate_feature(&mut genesis_config, feature_id); + } + + let (parent_bank, bank_forks) = + Bank::new_for_tests(&genesis_config).wrap_with_bank_forks_for_tests(); + parent_bank.set_tick_height(parent_bank.max_tick_height()); + Bank::calculate_and_set_block_id_for_dcou(&parent_bank); + + let effective_slot = parent_bank.epoch_schedule().get_first_slot_in_epoch(1); + let bank = Bank::new_from_parent_with_bank_forks( + &bank_forks, + parent_bank, + SlotLeader::default(), + effective_slot, + ); + let ticks = create_ticks(1, 0, genesis_config.hash()); + let receive_results = ReceiveResults { + component: BlockComponent::EntryBatch(ticks.clone()), + bank: bank.clone(), + last_tick_height: bank.tick_height() + ticks.len() as u64, + }; + let (socket_sender, _socket_receiver) = unbounded(); + let (blockstore_sender, _blockstore_receiver) = unbounded(); + let (votor_event_sender, _votor_event_receiver) = unbounded(); + let mut standard_broadcast_run = StandardBroadcastRun::new( + 0, + Arc::new(MigrationStatus::default()), + votor_event_sender, + test_leader_schedule_cache(&bank), + ); + + standard_broadcast_run + .process_receive_results( + &Keypair::new(), + &blockstore, + &socket_sender, + &blockstore_sender, + receive_results, + &mut ProcessShredsStats::default(), + ) + .unwrap(); + + ( + standard_broadcast_run.max_data_shreds_per_slot, + standard_broadcast_run.max_code_shreds_per_slot, + ) + } + #[test] fn test_interrupted_slot_last_shred() { let keypair = Arc::new(Keypair::new()); @@ -1158,52 +1222,28 @@ mod test { #[test] fn test_update_shred_limits_from_bank() { - agave_logger::setup(); - let num_shreds_per_slot = 2; - let ( - blockstore, - genesis_config, - cluster_info, - parent_bank, - leader_keypair, - socket, - bank_forks, - ) = setup(num_shreds_per_slot); - let bank = new_child_bank(&parent_bank, 1); - let ticks = create_ticks(1, 0, genesis_config.hash()); - let receive_results = ReceiveResults { - component: BlockComponent::EntryBatch(ticks), - bank: bank.clone(), - last_tick_height: bank.tick_height() + 1, - }; - - let (votor_event_sender, _votor_event_receiver) = unbounded(); - let mut standard_broadcast_run = StandardBroadcastRun::new( - 0, - Arc::new(MigrationStatus::default()), - votor_event_sender, - test_leader_schedule_cache(&bank), - ); - standard_broadcast_run.max_data_shreds_per_slot = 1; - standard_broadcast_run.max_code_shreds_per_slot = 1; - standard_broadcast_run - .test_process_receive_results( - &leader_keypair, - &cluster_info, - &socket, - &blockstore, - receive_results, - &bank_forks, - ) - .unwrap(); - assert_eq!( - standard_broadcast_run.max_data_shreds_per_slot, - bank.max_data_shreds_per_slot() + broadcast_shred_limits_for_slot_time_features(std::iter::empty()), + ( + DEFAULT_MAX_DATA_SHREDS_PER_SLOT, + DEFAULT_MAX_CODE_SHREDS_PER_SLOT + ) ); + + for ((feature_id, _), expected_shreds_per_slot) in slot_time_feature_gates() + .into_iter() + .zip([28_672, 24_576, 20_480, 16_384]) + { + assert_eq!( + broadcast_shred_limits_for_slot_time_features([feature_id]), + (expected_shreds_per_slot, expected_shreds_per_slot) + ); + } + + let [reduce_to_350ms, _, _, reduce_to_200ms] = slot_time_feature_ids(); assert_eq!( - standard_broadcast_run.max_code_shreds_per_slot, - bank.max_code_shreds_per_slot() + broadcast_shred_limits_for_slot_time_features([reduce_to_350ms, reduce_to_200ms]), + (16_384, 16_384) ); } From 8cff9b26b2e1f231465b75ab986c563bdb3029aa Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Wed, 3 Jun 2026 18:50:37 -0400 Subject: [PATCH 193/576] replay: reword alpenglow bank hash mismatch log message (#12934) --- core/src/replay_stage.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/src/replay_stage.rs b/core/src/replay_stage.rs index 458b6c68773..4693732e2f3 100644 --- a/core/src/replay_stage.rs +++ b/core/src/replay_stage.rs @@ -3913,12 +3913,12 @@ impl ReplayStage { ); if let Err((expected_hash, computed_hash)) = verify_result { - error!( - "Bank hash mismatch for slot {bank_slot} expected: {expected_hash} \ - computed: {computed_hash}", + warn!( + "For slot {bank_slot} the leader said the bank hash should be: \ + {expected_hash} however we computed: {computed_hash}", ); - datapoint_error!( + datapoint_warn!( "bank_hash_mismatch", ("slot", bank_slot, i64), ("expected", expected_hash.to_string(), String), From cd59cb8fc74fc3ec74d6ec9a14be174e714c42a0 Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Wed, 3 Jun 2026 20:23:18 -0400 Subject: [PATCH 194/576] replay: do not write bank_hash_details when clearing banks for ag (#12931) --- core/src/replay_stage.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/replay_stage.rs b/core/src/replay_stage.rs index 4693732e2f3..49082ddf2b9 100644 --- a/core/src/replay_stage.rs +++ b/core/src/replay_stage.rs @@ -2549,7 +2549,7 @@ impl ReplayStage { let root_bank = w_bank_forks.root_bank(); let (slots_to_purge, removed_banks) = - w_bank_forks.dump_slots(slots_to_clear.iter(), true); + w_bank_forks.dump_slots(slots_to_clear.iter(), false); (root_bank, slots_to_purge, removed_banks) }; From 1738c3e4b008a96e4ad4f53a645697fd91ec2da6 Mon Sep 17 00:00:00 2001 From: Brooks Date: Wed, 3 Jun 2026 20:52:00 -0400 Subject: [PATCH 195/576] Renames fn to mark_zero_lamport_single_ref_accounts_for_flush() (#12936) --- accounts-db/src/accounts_db.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/accounts-db/src/accounts_db.rs b/accounts-db/src/accounts_db.rs index 0dd3043456c..ae8e70c3a75 100644 --- a/accounts-db/src/accounts_db.rs +++ b/accounts-db/src/accounts_db.rs @@ -5797,7 +5797,7 @@ impl AccountsDb { let mark_zero_lamport_time = Measure::start("mark_zero_lamport"); let num_zero_lamport_single_ref_accounts_marked = - self.mark_zero_lamport_single_ref_accounts(&infos, storage, reclaim_handling); + self.mark_zero_lamport_single_ref_accounts_for_flush(&infos, storage, reclaim_handling); let mark_zero_lamport_us = mark_zero_lamport_time.end_as_us(); let update_index_time = Measure::start("update_index"); @@ -6007,7 +6007,7 @@ impl AccountsDb { /// Marks zero lamport single reference accounts in the storage during store_accounts_for_flush /// /// Returns the number of accounts marked. - fn mark_zero_lamport_single_ref_accounts( + fn mark_zero_lamport_single_ref_accounts_for_flush( &self, account_infos: &[AccountInfo], storage: &AccountStorageEntry, From 93e4f4e41cd2de4b8559246679aaba690f783a4f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2026 10:25:24 +0800 Subject: [PATCH 196/576] chore(deps): bump solana-transaction from 4.1.1 to 4.1.2 (#12938) * chore(deps): bump solana-transaction from 4.1.1 to 4.1.2 Bumps [solana-transaction](https://github.com/anza-xyz/solana-sdk) from 4.1.1 to 4.1.2. - [Release notes](https://github.com/anza-xyz/solana-sdk/releases) - [Commits](https://github.com/anza-xyz/solana-sdk/compare/message@v4.1.1...transaction@v4.1.2) --- updated-dependencies: - dependency-name: solana-transaction dependency-version: 4.1.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Update all workspaces --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- dev-bins/Cargo.lock | 4 ++-- install/Cargo.toml | 2 +- programs/sbf/Cargo.lock | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1aaa7cc3533..931e69fa214 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11080,9 +11080,9 @@ dependencies = [ [[package]] name = "solana-transaction" -version = "4.1.1" +version = "4.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a98253cefd62dea714b67926d3d918c3b9f7d66993f1f2322ac7549ea991dc3e" +checksum = "45eb9e481a70f582be316d0bf2f8f8704079664e636a4e5d0c0d077f1c9de8f3" dependencies = [ "serde", "serde_derive", diff --git a/Cargo.toml b/Cargo.toml index 2fbf0c60c9f..6d21e43eb94 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -502,7 +502,7 @@ solana-time-utils = "3.0.0" solana-tls-utils = { path = "tls-utils", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-tpu-client = { path = "tpu-client", version = "=4.2.0-alpha.0", default-features = false, features = ["agave-unstable-api"] } solana-tpu-client-next = { path = "tpu-client-next", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-transaction = "4.1.1" +solana-transaction = "4.1.2" solana-transaction-context = { path = "transaction-context", version = "=4.2.0-alpha.0", features = ["agave-unstable-api", "bincode"] } solana-transaction-error = "3.2.0" solana-transaction-status = { path = "transaction-status", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 460b1b20951..daa68e55417 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -9264,9 +9264,9 @@ dependencies = [ [[package]] name = "solana-transaction" -version = "4.1.1" +version = "4.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a98253cefd62dea714b67926d3d918c3b9f7d66993f1f2322ac7549ea991dc3e" +checksum = "45eb9e481a70f582be316d0bf2f8f8704079664e636a4e5d0c0d077f1c9de8f3" dependencies = [ "serde", "serde_derive", diff --git a/install/Cargo.toml b/install/Cargo.toml index f7ea930d771..3bc59ff0689 100644 --- a/install/Cargo.toml +++ b/install/Cargo.toml @@ -49,7 +49,7 @@ solana-rpc-client = { workspace = true } solana-sha256-hasher = { workspace = true } solana-signature = { version = "=3.4.0", default-features = false } solana-signer = "=3.0.0" -solana-transaction = { version = "=4.1.1", features = ["wincode"] } +solana-transaction = { version = "=4.1.2", features = ["wincode"] } solana-version = { workspace = true } tar = { workspace = true } tempfile = { workspace = true } diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index 671ad010a4c..7c325a99906 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -9757,9 +9757,9 @@ dependencies = [ [[package]] name = "solana-transaction" -version = "4.1.1" +version = "4.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a98253cefd62dea714b67926d3d918c3b9f7d66993f1f2322ac7549ea991dc3e" +checksum = "45eb9e481a70f582be316d0bf2f8f8704079664e636a4e5d0c0d077f1c9de8f3" dependencies = [ "serde", "serde_derive", From dead7e0c71bf6ce445baca2f82b1a45812e920cc Mon Sep 17 00:00:00 2001 From: steviez Date: Wed, 3 Jun 2026 22:25:15 -0500 Subject: [PATCH 197/576] Revert "SIMD-0525: Slot Time Reductions (#12264)" (#12940) This reverts commit 90f279a50d73c2cf4a269d75c4ccbf6732d655fa. --- accounts-db/src/partitioned_rewards.rs | 25 +- feature-set/src/lib.rs | 32 -- ledger/src/shred/filter.rs | 55 +--- runtime/src/bank.rs | 131 ++------ .../src/bank/partitioned_epoch_rewards/mod.rs | 5 - runtime/src/bank/tests.rs | 286 +----------------- .../epoch_inflation_account_state.rs | 101 +++---- runtime/src/slot_params.rs | 196 ++---------- .../broadcast_stage/standard_broadcast_run.rs | 128 +++----- 9 files changed, 146 insertions(+), 813 deletions(-) diff --git a/accounts-db/src/partitioned_rewards.rs b/accounts-db/src/partitioned_rewards.rs index e6565c2c2c9..85758a4df84 100644 --- a/accounts-db/src/partitioned_rewards.rs +++ b/accounts-db/src/partitioned_rewards.rs @@ -1,22 +1,16 @@ //! Code related to partitioned rewards distribution -/// Baseline number of stake accounts to store in one 400ms block during the -/// partitioned reward interval. -/// -/// The target is 64 rewards per entry/tick. A block has a minimum of 64 -/// entries/ticks, giving 4096 total rewards to store in one 400ms block. This -/// constant affects consensus; shorter slot-time targets scale this value down -/// in `Bank` state. -pub const MAX_PARTITIONED_REWARDS_PER_BLOCK: u64 = 4096; +/// # stake accounts to store in one block during partitioned reward interval +/// Target to store 64 rewards per entry/tick in a block. A block has a minimum of 64 +/// entries/tick. This gives 4096 total rewards to store in one block. +/// This constant affects consensus. +const MAX_PARTITIONED_REWARDS_PER_BLOCK: u64 = 4096; -/// Configuration options for partitioned epoch rewards. #[derive(Debug, Clone, Copy)] +/// Configuration options for partitioned epoch rewards. pub struct PartitionedEpochRewardsConfig { - /// Baseline number of stake accounts to store in one block during the - /// partitioned reward interval. - /// - /// This value is stored as the 400ms-slot baseline. Runtime `Bank` state - /// derives the effective per-bank value from the active slot-time target. + /// number of stake accounts to store in one block during partitioned reward interval + /// normally, this is a number tuned for reasonable performance, such as 4096 accounts/block pub stake_account_stores_per_block: u64, } @@ -36,8 +30,7 @@ impl Default for PartitionedEpochRewardsConfig { } impl PartitionedEpochRewardsConfig { - /// Constructs a config with an explicit 400ms-slot baseline for tests and - /// benchmarks. + /// Only for tests and benchmarks pub fn new_for_test(stake_account_stores_per_block: u64) -> Self { Self { stake_account_stores_per_block, diff --git a/feature-set/src/lib.rs b/feature-set/src/lib.rs index f4e72cd3d59..611e4480ad9 100644 --- a/feature-set/src/lib.rs +++ b/feature-set/src/lib.rs @@ -1525,22 +1525,6 @@ pub mod set_lamports_per_byte_to_6960 { pub const LAMPORTS_PER_BYTE: u64 = 6960; } -pub mod reduce_slot_time_to_350ms { - solana_pubkey::declare_id!("iBRL2iJvhLssJveF1utbmmQGmjonmNYZALcJFEHTbUF"); -} - -pub mod reduce_slot_time_to_300ms { - solana_pubkey::declare_id!("iBRLA3zvd6x9445cK1vS7xt8n6Y7DS3otfRcDdW8JRW"); -} - -pub mod reduce_slot_time_to_250ms { - solana_pubkey::declare_id!("iBRLR6nG3fDi8YD4mpPTUVTgo5NaiYfZgrzokCfADP2"); -} - -pub mod reduce_slot_time_to_200ms { - solana_pubkey::declare_id!("iBRLypKvvj9VEvwoTeRpbLhbW55NFR4T3GE9BUR8A16"); -} - pub static FEATURE_NAMES: LazyLock> = LazyLock::new(|| { [ (secp256k1_program_enabled::id(), "secp256k1 program"), @@ -2431,22 +2415,6 @@ pub static FEATURE_NAMES: LazyLock> = LazyLock::n alpenglow::id(), "SIMD-0326: Alpenglow: new consensus algorithm", ), - ( - reduce_slot_time_to_350ms::id(), - "SIMD-0525: Reduce slot time to 350ms", - ), - ( - reduce_slot_time_to_300ms::id(), - "SIMD-0525: Reduce slot time to 300ms", - ), - ( - reduce_slot_time_to_250ms::id(), - "SIMD-0525: Reduce slot time to 250ms", - ), - ( - reduce_slot_time_to_200ms::id(), - "SIMD-0525: Reduce slot time to 200ms", - ), ( disable_zk_elgamal_proof_program::id(), "Disables zk-elgamal-proof program", diff --git a/ledger/src/shred/filter.rs b/ledger/src/shred/filter.rs index 65df4c45821..a78b3ad4dc3 100644 --- a/ledger/src/shred/filter.rs +++ b/ledger/src/shred/filter.rs @@ -595,10 +595,7 @@ mod tests { itertools::Itertools, solana_leader_schedule::SlotLeader, solana_perf::packet::{Packet, PacketFlags}, - solana_runtime::{ - bank::Bank, - slot_params::{slot_time_feature_gates, slot_time_feature_ids}, - }, + solana_runtime::bank::Bank, std::{ io::{Cursor, Seek, SeekFrom, Write}, sync::Arc, @@ -623,26 +620,6 @@ mod tests { } } - fn deactivate_slot_time_features(bank: &mut Bank) { - for feature_id in slot_time_feature_ids() { - bank.deactivate_feature(&feature_id); - } - } - - fn shred_filter_for_tests( - feature_ids: impl IntoIterator, - ) -> (ShredFilterContext, Slot) { - let genesis_config = create_genesis_config(1).genesis_config; - let mut root_bank = Bank::new_for_tests(&genesis_config); - deactivate_slot_time_features(&mut root_bank); - for feature_id in feature_ids { - root_bank.activate_feature(&feature_id); - } - let first_epoch_slot = root_bank.epoch_schedule().get_first_slot_in_epoch(1); - let (root_bank, _) = root_bank.wrap_with_bank_forks_for_tests(); - (ShredFilterContext::new(root_bank, 0), first_epoch_slot) - } - #[test_case(true ; "last_in_slot")] #[test_case(false ; "not_last_in_slot")] fn test_should_discard_shred(is_last_in_slot: bool) { @@ -1069,36 +1046,6 @@ mod tests { } } - #[test] - fn test_shred_limit_for_slot_times() { - for (features, shred_limits) in std::iter::once((vec![], ShredLimits::DEFAULT)).chain( - slot_time_feature_gates().map(|(feature_id, params)| { - ( - vec![feature_id], - ShredLimits::new( - params.max_data_shreds_per_slot(), - params.max_code_shreds_per_slot(), - ), - ) - }), - ) { - let (shred_filter, effective_slot) = shred_filter_for_tests(features); - assert_eq!( - shred_filter.shred_limits(effective_slot.saturating_sub(1)), - ShredLimits::DEFAULT - ); - assert_eq!(shred_filter.shred_limits(effective_slot), shred_limits); - } - - let [reduce_to_350ms, _, _, reduce_to_200ms] = slot_time_feature_ids(); - let (shred_filter, effective_slot) = - shred_filter_for_tests([reduce_to_350ms, reduce_to_200ms]); - assert_eq!( - shred_filter.shred_limits(effective_slot), - ShredLimits::new(16_384, 16_384) - ); - } - #[test] fn test_data_complete_shred_index_validation() { agave_logger::setup(); diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 8e60cfa6472..f6a2bb30881 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -112,7 +112,7 @@ use { solana_cluster_type::ClusterType, solana_compute_budget::compute_budget::ComputeBudget, solana_compute_budget_instruction::instructions_processor::process_compute_budget_instructions, - solana_cost_model::cost_tracker::CostTracker, + solana_cost_model::{block_cost_limits::simd_0286_block_limit, cost_tracker::CostTracker}, solana_epoch_info::EpochInfo, solana_epoch_schedule::EpochSchedule, solana_feature_gate_interface as feature, @@ -179,6 +179,7 @@ use { solana_system_transaction as system_transaction, solana_sysvar::{self as sysvar, SysvarSerialize, last_restart_slot::LastRestartSlot}, solana_sysvar_id::SysvarId, + solana_time_utils::years_as_slots, solana_transaction::{ Transaction, TransactionVerificationMode, sanitized::{MAX_TX_ACCOUNT_LOCKS, MessageHash, SanitizedTransaction}, @@ -2179,7 +2180,20 @@ impl Bank { snapshot and genesis.bin might pertain to different clusters" ); assert_eq!(bank.ticks_per_slot, genesis_config.ticks_per_slot); + assert_eq!( + bank.ns_per_slot, + genesis_config.poh_config.target_tick_duration.as_nanos() + * genesis_config.ticks_per_slot as u128 + ); assert_eq!(bank.max_tick_height, (bank.slot + 1) * bank.ticks_per_slot); + assert_eq!( + bank.slots_per_year, + years_as_slots( + 1.0, + &genesis_config.poh_config.target_tick_duration, + bank.ticks_per_slot, + ) + ); assert_eq!(bank.epoch_schedule, genesis_config.epoch_schedule); bank.refresh_slot_params_with_baseline(Self::genesis_config_slot_params( @@ -2696,18 +2710,12 @@ impl Bank { }); } - /// Rebuilds slot-param state from the current feature set. - fn refresh_slot_params(&mut self) { - self.refresh_slot_params_with_baseline(self.slot_params.baseline_params()); - } - /// Rebuilds cached slot params while preserving the supplied slot-0 baseline. /// /// The cache is not serialized into snapshots; it is reconstructed from /// existing Bank fields during genesis and snapshot restore. fn refresh_slot_params_with_baseline(&mut self, baseline_params: SlotParams) { - self.slot_params = - SlotParamsArchive::new(&self.feature_set, &self.epoch_schedule, baseline_params); + self.slot_params = SlotParamsArchive::new(&self.epoch_schedule, baseline_params); } /// Builds slot-0 params from the genesis config. @@ -2728,11 +2736,6 @@ impl Bank { self.slot_params.params_at_slot(slot) } - /// Returns the slot params that should be effective for this bank's slot. - fn current_slot_params(&self) -> SlotParams { - self.slot_params_at_slot(self.slot) - } - /// Returns the effective slot duration for `slot`. pub fn ns_per_slot_at_slot(&self, slot: Slot) -> u128 { self.slot_params_at_slot(slot).ns_per_slot() @@ -4673,83 +4676,24 @@ impl Bank { self.rc.accounts.clone() } - /// Recomputes cost tracker limits from active feature state. fn apply_cost_tracker_limits_for_active_features(&mut self) { - let params = self.current_slot_params(); - let (account_cost_limit, block_cost_limit, vote_cost_limit, data_size_limit) = - params.cost_limits(self.feature_set.snapshot().raise_block_limits_to_100m); - let mut cost_tracker = self.write_cost_tracker().unwrap(); + let block_cost_limit = if self.feature_set.snapshot().raise_block_limits_to_100m { + simd_0286_block_limit() + } else { + cost_tracker.get_block_limit() + }; + let account_cost_limit = block_cost_limit.saturating_mul(40).saturating_div(100); + let vote_cost_limit = cost_tracker.get_vote_limit(); + let allocated_data_size_limit = cost_tracker.get_allocated_data_size_limit(); cost_tracker.set_limits( account_cost_limit, block_cost_limit, vote_cost_limit, - data_size_limit, - ); - } - - /// Recomputes this bank's effective partitioned-reward write budget. - fn apply_partitioned_epoch_rewards_config_for_active_features(&mut self) { - self.partitioned_rewards_stake_account_stores_per_block = self - .current_slot_params() - .partitioned_epoch_rewards_stake_account_stores_per_block(); - } - - /// Applies slot-time changes for fields serialized into snapshots. - fn apply_slot_time_persistent_changes(&mut self) { - let params = self.current_slot_params(); - self.ns_per_slot = params.ns_per_slot(); - self.slots_per_year = params.slots_per_year(); - self.rent_collector.slots_per_year = params.slots_per_year(); - if !self.feature_set.is_active(&feature_set::alpenglow::id()) - && self.hashes_per_tick().is_some() - { - self.set_hashes_per_tick(params.hashes_per_tick()); - } - } - - /// Verifies bank fields are consistent with current slot params. - fn assert_bank_matches_slot_params(&self) { - let params = self.current_slot_params(); - assert_eq!( - self.ns_per_slot, - params.ns_per_slot(), - "snapshot slot-time ns_per_slot mismatch" - ); - assert_eq!( - self.slots_per_year.to_bits(), - params.slots_per_year().to_bits(), - "snapshot slot-time slots_per_year mismatch" - ); - assert_eq!( - self.rent_collector.slots_per_year.to_bits(), - params.slots_per_year().to_bits(), - "snapshot slot-time rent_collector.slots_per_year mismatch" - ); - let hashes_per_tick = self.hashes_per_tick(); - if !self.feature_set.is_active(&feature_set::alpenglow::id()) && hashes_per_tick.is_some() { - assert_eq!( - hashes_per_tick, - params.hashes_per_tick(), - "snapshot slot-time hashes_per_tick mismatch" - ); - } - assert_eq!( - self.entry_bytes_budget().slot_limit(), - params.max_entry_bytes_per_slot(), - "snapshot slot-time entry byte budget mismatch" + allocated_data_size_limit, ); } - /// Applies slot-time changes for runtime-only fields. This function is - /// expected to be idempotent. - fn apply_slot_time_runtime_changes(&mut self) { - self.entry_bytes_consumed = - EntryBytesBudget::new(self.current_slot_params().max_entry_bytes_per_slot()); - self.apply_cost_tracker_limits_for_active_features(); - self.apply_partitioned_epoch_rewards_config_for_active_features(); - } - fn apply_simd_0339_invoke_cost_changes(&mut self) { let simd_0268_active = self.feature_set.snapshot().raise_cpi_nesting_limit_to_8; let compute_budget = self @@ -4771,10 +4715,11 @@ impl Bank { Arc::new(reserved_keys) }; - // Many fields are not serialized in snapshot or any configs. Rebuild - // them from the feature set so the initial bank state is consistent. - self.refresh_slot_params(); - self.apply_slot_time_runtime_changes(); + // Cost-Tracker is not serialized in snapshot or any configs. + // We must apply previously activated features related to limits here + // so that the initial bank state is consistent with the feature set. + // Cost-tracker limits are propagated through children banks. + self.apply_cost_tracker_limits_for_active_features(); self.apply_simd_0339_invoke_cost_changes(); let program_runtime_environment = @@ -5869,14 +5814,12 @@ impl Bank { let mut feature_set = Arc::make_mut(&mut self.feature_set).clone(); feature_set.deactivate(id); self.feature_set = Arc::new(feature_set); - self.refresh_slot_params(); } pub fn activate_feature(&mut self, id: &Pubkey) { let mut feature_set = Arc::make_mut(&mut self.feature_set).clone(); feature_set.activate(id, 0); self.feature_set = Arc::new(feature_set); - self.refresh_slot_params(); } pub fn fill_bank_with_ticks_for_tests(&self) { @@ -5952,7 +5895,6 @@ impl Bank { self.feature_set = Arc::new(feature_set); self.apply_activated_features(); - self.assert_bank_matches_slot_params(); } /// This is called from each epoch boundary @@ -5961,7 +5903,6 @@ impl Bank { let (feature_set, new_feature_activations) = self.compute_active_feature_set(include_pending); self.feature_set = Arc::new(feature_set); - self.refresh_slot_params(); // Update activation slot of features in `new_feature_activations` for feature_id in new_feature_activations.iter() { @@ -6044,11 +5985,10 @@ impl Bank { self.fee_rate_governor.burn_percent = solana_fee_calculator::DEFAULT_BURN_PERCENT; } - // Apply unconditionally: this is relatively cheap and idempotent. - self.apply_slot_time_persistent_changes(); - self.apply_slot_time_runtime_changes(); - self.apply_new_builtin_program_feature_transitions(&new_feature_activations); + if new_feature_activations.contains(&feature_set::raise_block_limits_to_100m::id()) { + self.apply_cost_tracker_limits_for_active_features(); + } if new_feature_activations.contains(&feature_set::replace_spl_token_with_p_token::id()) { if let Err(e) = self.upgrade_loader_v2_program_with_loader_v3_program( @@ -7003,11 +6943,6 @@ impl Bank { minimized_account_set.insert(*pubkey); }); } - - /// Returns true when this bank is using slot params beyond its genesis baseline. - pub fn slot_time_reduction_active(&self) -> bool { - self.ns_per_slot != self.slot_params.baseline_params().ns_per_slot() - } } /// Returns a thread pool intended to be used for reward calculation. This diff --git a/runtime/src/bank/partitioned_epoch_rewards/mod.rs b/runtime/src/bank/partitioned_epoch_rewards/mod.rs index e7492dbc4a6..9218ceac4e7 100644 --- a/runtime/src/bank/partitioned_epoch_rewards/mod.rs +++ b/runtime/src/bank/partitioned_epoch_rewards/mod.rs @@ -417,7 +417,6 @@ mod tests { bank_forks::BankForks, genesis_utils::{ GenesisConfigInfo, ValidatorVoteKeypairs, create_genesis_config_with_vote_accounts, - deactivate_features, }, runtime_config::RuntimeConfig, stake_utils, @@ -575,9 +574,6 @@ mod tests { stake_account_stores_per_block: u64, advance_num_slots: u64, ) -> (RewardBank, Arc>) { - // Disable slot time reduction features as they will override the custom - // stores per block provided in this test helper. - let features_to_deactivate = crate::slot_params::slot_time_feature_ids().to_vec(); let validator_keypairs = (0..stakes.len()) .map(|_| ValidatorVoteKeypairs::new_rand()) .collect::>(); @@ -586,7 +582,6 @@ mod tests { mut genesis_config, .. } = create_genesis_config_with_vote_accounts(1_000_000_000, &validator_keypairs, stakes); genesis_config.epoch_schedule = EpochSchedule::new(SLOTS_PER_EPOCH); - deactivate_features(&mut genesis_config, &features_to_deactivate); let mut accounts_db_config: AccountsDbConfig = ACCOUNTS_DB_CONFIG_FOR_TESTING; accounts_db_config.partitioned_epoch_rewards_config = diff --git a/runtime/src/bank/tests.rs b/runtime/src/bank/tests.rs index b8763acd85d..0352704a421 100644 --- a/runtime/src/bank/tests.rs +++ b/runtime/src/bank/tests.rs @@ -19,11 +19,7 @@ use { }, runtime_config::RuntimeConfig, serde_snapshot::fields_from_stream, - slot_params::{ - DEFAULT_MAX_ENTRY_BYTES_PER_SLOT, LEGACY_HASHES_PER_TICK, LEGACY_SLOT_PARAMS, - SLOT_PARAMS_200MS, SLOT_PARAMS_250MS, SLOT_PARAMS_300MS, SLOT_PARAMS_350MS, SlotParams, - slot_time_feature_gates, slot_time_feature_ids, - }, + slot_params::{DEFAULT_MAX_ENTRY_BYTES_PER_SLOT, LEGACY_SLOT_PARAMS}, stake_history::StakeHistory, stake_utils, stakes::{DeserializableStakes, InvalidCacheEntryReason, SerdeStakesToStakeFormat, Stakes}, @@ -184,12 +180,6 @@ pub(in crate::bank) fn create_genesis_config(lamports: u64) -> (GenesisConfig, K solana_genesis_config::create_genesis_config(lamports) } -fn create_genesis_config_with_legacy_hashes(lamports: u64) -> (GenesisConfig, Keypair) { - let (mut genesis_config, mint_keypair) = create_genesis_config(lamports); - genesis_config.poh_config.hashes_per_tick = Some(LEGACY_HASHES_PER_TICK); - (genesis_config, mint_keypair) -} - pub(in crate::bank) fn new_sanitized_message(message: Message) -> SanitizedMessage { SanitizedMessage::try_from_legacy_message(message, &ReservedAccountKeys::empty_key_set()) .unwrap() @@ -5267,9 +5257,9 @@ fn test_bank_hash_consistency(deprecate_rent_exemption_threshold: bool) { assert_eq!( bank.hash().to_string(), if deprecate_rent_exemption_threshold { - "9ycftRwjpQ17PrhnwhGPbVzDKd3Q9BybmLYU8UD1Pg1T" + "F4ns41hFn3ignVHqaVehaTZ8LMUxPaELAgvN8iTcowDD" } else { - "4XzjZMjhP9s8iBeFQsnHFwaQ991dpiBGHEkzj1ifAcpS" + "FyxeAqHjieaKMA6mLK3yfSD46Xcw2U6hzKfyeu1qVuCD" }, ); } @@ -5278,9 +5268,9 @@ fn test_bank_hash_consistency(deprecate_rent_exemption_threshold: bool) { assert_eq!( bank.hash().to_string(), if deprecate_rent_exemption_threshold { - "7sbqfkN4W3PkBREyduDc3R2eiKu68JKYRXZt6gCWNt4N" + "sF1tuUv3r5JCr9smYyhm9Qv27f5jcoJ75LnoLGV5Scd" } else { - "ArB3XNVLJsyV7AtrG3C3Bj4akb8s7AzpS5rHJnqGW5sw" + "DrpEs59czmKpLQvR9kBjSmWJUYMNKDdQ4Zsyu4WjfoPb" }, ); break; @@ -6306,272 +6296,6 @@ fn test_block_limits() { ); } -fn assert_slot_time_bank_state(bank: &Bank, params: SlotParams) { - assert_eq!(bank.ns_per_slot, params.ns_per_slot()); - assert_eq!(bank.hashes_per_tick(), params.hashes_per_tick()); - assert_eq!( - bank.slots_per_year.to_bits(), - params.slots_per_year().to_bits() - ); - assert_eq!( - bank.rent_collector.slots_per_year.to_bits(), - bank.slots_per_year.to_bits() - ); - assert_eq!( - bank.partitioned_rewards_stake_account_stores_per_block, - params.partitioned_epoch_rewards_stake_account_stores_per_block() - ); - assert_eq!( - bank.entry_bytes_budget().slot_limit(), - params.max_entry_bytes_per_slot() - ); - let cost_tracker = bank.read_cost_tracker().unwrap(); - let (account_limit, block_limit, vote_limit, data_size_limit) = params.cost_limits( - bank.feature_set - .is_active(&feature_set::raise_block_limits_to_100m::id()), - ); - assert_eq!(cost_tracker.get_account_limit(), account_limit); - assert_eq!(cost_tracker.get_block_limit(), block_limit); - assert_eq!(cost_tracker.get_vote_limit(), vote_limit); - assert_eq!( - cost_tracker.get_allocated_data_size_limit(), - data_size_limit - ); -} - -#[test] -fn test_reduce_slot_time_features() { - let (genesis_config, _) = create_genesis_config_with_legacy_hashes(1_000_000); - let feature_account_balance = genesis_config.rent.minimum_balance(Feature::size_of()); - - let (mut bank, bank_forks) = Bank::new_with_bank_forks_for_tests(&genesis_config); - assert!(!bank.slot_time_reduction_active()); - assert_eq!(bank.hashes_per_tick(), Some(LEGACY_HASHES_PER_TICK)); - - for (feature_id, params) in slot_time_feature_gates() { - let previous_params = bank.current_slot_params(); - bank.store_account( - &feature_id, - &feature::create_account(&Feature::default(), feature_account_balance), - ); - - let activation_slot = bank - .epoch_schedule() - .get_first_slot_in_epoch(bank.epoch().saturating_add(1)); - let activation_bank = Bank::new_from_parent_with_bank_forks( - &bank_forks, - bank, - SlotLeader::default(), - activation_slot, - ); - - assert!(activation_bank.feature_set.is_active(&feature_id)); - assert_eq!(activation_bank.current_slot_params(), previous_params); - assert_slot_time_bank_state(&activation_bank, previous_params); - - let effective_slot = activation_bank - .epoch_schedule() - .get_first_slot_in_epoch(activation_bank.epoch().saturating_add(1)); - bank = Bank::new_from_parent_with_bank_forks( - &bank_forks, - activation_bank, - SlotLeader::default(), - effective_slot, - ); - assert!(bank.slot_time_reduction_active()); - assert_slot_time_bank_state(&bank, params); - } -} - -#[test] -fn test_reduce_slot_time_features_active_at_genesis() { - let (mut genesis_config, _) = create_genesis_config_with_legacy_hashes(1_000_000); - for feature_id in slot_time_feature_ids() { - activate_feature(&mut genesis_config, feature_id); - } - activate_feature( - &mut genesis_config, - feature_set::raise_block_limits_to_100m::id(), - ); - - let (bank, bank_forks) = Bank::new_with_bank_forks_for_tests(&genesis_config); - assert!(!bank.slot_time_reduction_active()); - assert_slot_time_bank_state(&bank, LEGACY_SLOT_PARAMS); - - let effective_slot = bank.epoch_schedule().get_first_slot_in_epoch(1); - let bank = Bank::new_from_parent_with_bank_forks( - &bank_forks, - bank, - SlotLeader::default(), - effective_slot, - ); - assert_slot_time_bank_state(&bank, SLOT_PARAMS_200MS); -} - -#[test] -fn test_reduce_slot_time_hashes_per_tick() { - let assert_reduced_slot_time_hashes_per_tick = - |genesis_config, expected_initial_hashes_per_tick, expected_effective_hashes_per_tick| { - let (bank, bank_forks) = Bank::new_with_bank_forks_for_tests(&genesis_config); - assert_eq!(bank.ns_per_slot, LEGACY_SLOT_PARAMS.ns_per_slot()); - assert_eq!(bank.hashes_per_tick(), expected_initial_hashes_per_tick); - - let effective_slot = bank.epoch_schedule().get_first_slot_in_epoch(1); - let bank = Bank::new_from_parent_with_bank_forks( - &bank_forks, - bank, - SlotLeader::default(), - effective_slot, - ); - assert_eq!(bank.ns_per_slot, SLOT_PARAMS_200MS.ns_per_slot()); - assert_eq!(bank.hashes_per_tick(), expected_effective_hashes_per_tick); - }; - - let (mut genesis_config, _) = create_genesis_config_with_legacy_hashes(1_000_000); - activate_feature( - &mut genesis_config, - feature_set::reduce_slot_time_to_200ms::id(), - ); - assert_reduced_slot_time_hashes_per_tick( - genesis_config, - Some(LEGACY_HASHES_PER_TICK), - SLOT_PARAMS_200MS.hashes_per_tick(), - ); - - let (mut genesis_config, _) = create_genesis_config_with_legacy_hashes(1_000_000); - genesis_utils::activate_all_features_alpenglow(&mut genesis_config); - assert_eq!(genesis_config.poh_config.hashes_per_tick, None); - assert_reduced_slot_time_hashes_per_tick(genesis_config, None, None); -} - -#[test] -fn test_reduce_slot_time_range_duration() { - const SLOTS_PER_EPOCH: Slot = 32; - let bank_for_activations = |activations: &[(Pubkey, Slot)]| { - let (mut genesis_config, _) = create_genesis_config_with_legacy_hashes(1_000_000); - genesis_config.epoch_schedule = - EpochSchedule::custom(SLOTS_PER_EPOCH, SLOTS_PER_EPOCH, false); - let mut bank = Bank::new_for_tests(&genesis_config); - let mut feature_set = FeatureSet::default(); - for (feature_id, activation_slot) in activations { - feature_set.activate(feature_id, *activation_slot); - } - bank.feature_set = Arc::new(feature_set); - bank.refresh_slot_params(); - bank - }; - - let ordered_activations = slot_time_feature_gates() - .into_iter() - .zip([1, 33, 65, 97]) - .map(|((feature_id, _), activation_slot)| (feature_id, activation_slot)) - .collect::>(); - let bank = bank_for_activations(&ordered_activations); - - let expected_duration = [ - (SLOTS_PER_EPOCH, LEGACY_SLOT_PARAMS), - (SLOTS_PER_EPOCH, SLOT_PARAMS_350MS), - (SLOTS_PER_EPOCH, SLOT_PARAMS_300MS), - (SLOTS_PER_EPOCH, SLOT_PARAMS_250MS), - (SLOTS_PER_EPOCH, SLOT_PARAMS_200MS), - ] - .into_iter() - .map(|(slots, params)| slots as f64 / params.slots_per_year()) - .sum::(); - - assert_eq!( - bank.slot_range_duration_in_years(0, SLOTS_PER_EPOCH * 5) - .to_bits(), - expected_duration.to_bits() - ); - assert_eq!( - bank.slot_range_duration_nanos(0, SLOTS_PER_EPOCH * 5 - 1), - [ - (SLOTS_PER_EPOCH, LEGACY_SLOT_PARAMS), - (SLOTS_PER_EPOCH, SLOT_PARAMS_350MS), - (SLOTS_PER_EPOCH, SLOT_PARAMS_300MS), - (SLOTS_PER_EPOCH, SLOT_PARAMS_250MS), - (SLOTS_PER_EPOCH, SLOT_PARAMS_200MS), - ] - .into_iter() - .map(|(slots, params)| u128::from(slots) * params.ns_per_slot()) - .sum::() - ); - - let [reduce_to_350ms, _, _, reduce_to_200ms] = slot_time_feature_ids(); - let bank = - bank_for_activations(&[(reduce_to_200ms, 1), (reduce_to_350ms, SLOTS_PER_EPOCH + 1)]); - assert_eq!( - bank.slot_params_at_slot(SLOTS_PER_EPOCH - 1), - LEGACY_SLOT_PARAMS - ); - assert_eq!(bank.slot_params_at_slot(SLOTS_PER_EPOCH), SLOT_PARAMS_200MS); - assert_eq!( - bank.slot_params_at_slot(SLOTS_PER_EPOCH * 2), - SLOT_PARAMS_200MS - ); - assert_eq!( - bank.slot_range_duration_in_years(0, SLOTS_PER_EPOCH * 3) - .to_bits(), - (SLOTS_PER_EPOCH as f64 / LEGACY_SLOT_PARAMS.slots_per_year() - + (SLOTS_PER_EPOCH * 2) as f64 / SLOT_PARAMS_200MS.slots_per_year()) - .to_bits() - ); - assert_eq!( - bank.slot_range_duration_nanos(0, SLOTS_PER_EPOCH * 3 - 1), - u128::from(SLOTS_PER_EPOCH) * LEGACY_SLOT_PARAMS.ns_per_slot() - + u128::from(SLOTS_PER_EPOCH * 2) * SLOT_PARAMS_200MS.ns_per_slot() - ); -} - -#[test] -fn test_update_clock_slot_range_duration() { - const SLOTS_PER_EPOCH: Slot = 32; - - let leader_pubkey = solana_pubkey::new_rand(); - let GenesisConfigInfo { - mut genesis_config, - voting_keypair, - .. - } = create_genesis_config_with_leader(5, &leader_pubkey, 3); - genesis_config.epoch_schedule = EpochSchedule::custom(SLOTS_PER_EPOCH, SLOTS_PER_EPOCH, false); - activate_feature( - &mut genesis_config, - feature_set::reduce_slot_time_to_200ms::id(), - ); - - let (bank, bank_forks) = Bank::new_with_bank_forks_for_tests(&genesis_config); - let vote_timestamp_slot = 4; - let vote_timestamp = bank.unix_timestamp_from_genesis(); - update_vote_account_timestamp( - BlockTimestamp { - slot: vote_timestamp_slot, - timestamp: vote_timestamp, - }, - &bank, - &voting_keypair.pubkey(), - ); - - let current_slot = SLOTS_PER_EPOCH + 3; - let bank = Bank::new_from_parent_with_bank_forks( - &bank_forks, - bank, - SlotLeader::default(), - current_slot, - ); - let expected_elapsed = Duration::from_nanos_u128( - bank.slot_range_duration_nanos(vote_timestamp_slot + 1, current_slot), - ); - let scalar_elapsed = - Duration::from_nanos(bank.ns_per_slot as u64) * (current_slot - vote_timestamp_slot) as u32; - - assert_ne!(expected_elapsed.as_secs(), scalar_elapsed.as_secs()); - assert_eq!( - bank.clock().unix_timestamp, - vote_timestamp + expected_elapsed.as_secs() as i64 - ); -} - #[test] fn test_rent_feature_gates_epoch_transition() { let (mut genesis_config, _mint_keypair) = create_genesis_config(1_000_000); diff --git a/runtime/src/block_component_processor/vote_reward/epoch_inflation_account_state.rs b/runtime/src/block_component_processor/vote_reward/epoch_inflation_account_state.rs index c68be91a25b..1c6c95d4de5 100644 --- a/runtime/src/block_component_processor/vote_reward/epoch_inflation_account_state.rs +++ b/runtime/src/block_component_processor/vote_reward/epoch_inflation_account_state.rs @@ -155,11 +155,9 @@ mod tests { bank_forks::BankForks, genesis_utils::{ GenesisConfigInfo, ValidatorVoteKeypairs, create_genesis_config, - create_genesis_config_with_alpenglow_vote_accounts, deactivate_features, + create_genesis_config_with_alpenglow_vote_accounts, }, - slot_params::slot_time_feature_ids, }, - agave_feature_set as feature_set, rand::Rng, solana_epoch_schedule::EpochSchedule, solana_genesis_config::GenesisConfig, @@ -233,7 +231,7 @@ mod tests { } #[test] - fn new_epoch_update_account_uses_current_epoch_rewards() { + fn new_epoch_update_account_use_current_reward_budget() { let GenesisConfigInfo { mut genesis_config, .. } = create_genesis_config(1_000_000_000); @@ -241,71 +239,46 @@ mod tests { // Warmup is necessary here to give epoch 0 and epoch 1 different reward // budgets. genesis_config.epoch_schedule = EpochSchedule::custom(64, 64, true); - deactivate_features(&mut genesis_config, &slot_time_feature_ids().to_vec()); - for (case, genesis_config, activate_slot_time_feature) in [ - ("current reward budget", genesis_config, false), - ( - "current epoch slot params", - GenesisConfig { - inflation: Inflation::full(), - ..GenesisConfig::default() - }, - true, - ), - ] { - let mut bank_epoch_0 = Bank::new_for_tests(&genesis_config); - if activate_slot_time_feature { - bank_epoch_0.activate_feature(&feature_set::reduce_slot_time_to_350ms::id()); - } - let bank_forks = BankForks::new_rw_arc(bank_epoch_0); - let bank_epoch_0 = bank_forks.read().unwrap().root_bank(); - let epoch_1_first_slot = bank_epoch_0.epoch_schedule().get_first_slot_in_epoch(1); - let bank_epoch_1 = Bank::new_from_parent_with_bank_forks( - bank_forks.as_ref(), - bank_epoch_0.clone(), - SlotLeader::new_unique(), - epoch_1_first_slot, - ); + let bank_forks = BankForks::new_rw_arc(Bank::new_for_tests(&genesis_config)); + let bank_epoch_0 = bank_forks.read().unwrap().root_bank(); + let epoch_1_first_slot = bank_epoch_0.epoch_schedule().get_first_slot_in_epoch(1); + let bank_epoch_1 = Bank::new_from_parent_with_bank_forks( + bank_forks.as_ref(), + bank_epoch_0.clone(), + SlotLeader::new_unique(), + epoch_1_first_slot, + ); - assert_eq!(bank_epoch_0.epoch(), 0, "{case}"); - assert_eq!(bank_epoch_1.epoch(), 1, "{case}"); - if activate_slot_time_feature { - assert_ne!(bank_epoch_0.ns_per_slot, bank_epoch_1.ns_per_slot, "{case}"); - } else { - assert_ne!( - bank_epoch_1.epoch_schedule().get_slots_in_epoch(0), - bank_epoch_1.epoch_schedule().get_slots_in_epoch(1), - "{case}", - ); - } + assert_eq!(bank_epoch_0.epoch(), 0); + assert_eq!(bank_epoch_1.epoch(), 1); + assert_ne!( + bank_epoch_1.epoch_schedule().get_slots_in_epoch(0), + bank_epoch_1.epoch_schedule().get_slots_in_epoch(1) + ); - let epoch_start_capitalization = bank_epoch_0.capitalization(); - let expected_current_epoch_rewards = bank_epoch_1.calculate_epoch_inflation_rewards( + let epoch_start_capitalization = bank_epoch_0.capitalization(); + let expected_current_epoch_rewards = bank_epoch_1 + .calculate_epoch_inflation_rewards(epoch_start_capitalization, bank_epoch_1.epoch()); + assert_ne!( + expected_current_epoch_rewards, + bank_epoch_1.calculate_epoch_inflation_rewards( epoch_start_capitalization, - bank_epoch_1.epoch(), - ); - assert_ne!( - expected_current_epoch_rewards, - bank_epoch_1.calculate_epoch_inflation_rewards( - epoch_start_capitalization, - bank_epoch_0.epoch() - ), - "{case}", - ); + bank_epoch_0.epoch() + ) + ); - EpochInflationAccountState::new_epoch_update_account( - &bank_epoch_1, - epoch_start_capitalization, - 0, - ); - let state = EpochInflationAccountState::new_from_bank(&bank_epoch_1).unwrap(); - assert_eq!(state.current.epoch, bank_epoch_1.epoch(), "{case}"); - assert_eq!( - state.current.max_possible_validator_reward, expected_current_epoch_rewards, - "{case}", - ); - } + EpochInflationAccountState::new_epoch_update_account( + &bank_epoch_1, + epoch_start_capitalization, + 0, + ); + let state = EpochInflationAccountState::new_from_bank(&bank_epoch_1).unwrap(); + assert_eq!(state.current.epoch, bank_epoch_1.epoch()); + assert_eq!( + state.current.max_possible_validator_reward, + expected_current_epoch_rewards + ); } #[test] diff --git a/runtime/src/slot_params.rs b/runtime/src/slot_params.rs index a5615b073c6..0e25bb31ec9 100644 --- a/runtime/src/slot_params.rs +++ b/runtime/src/slot_params.rs @@ -1,8 +1,6 @@ use { - agave_feature_set::{self as feature_set, FeatureSet}, solana_clock::Slot, solana_epoch_schedule::EpochSchedule, - solana_pubkey::Pubkey, std::{ collections::BTreeMap, ops::Bound::{Excluded, Included}, @@ -13,8 +11,10 @@ pub const DEFAULT_MAX_ENTRY_BYTES_PER_SLOT: u64 = 20 * 1024 * 1024; // 20 MiB /// Runtime parameters tied to a slot-time regime. /// -/// Slot-time reduction features select explicit table values instead of -/// deriving ratios at runtime, which avoids rounding drift in consensus paths. +/// This intentionally groups values that need to move together when slot time +/// changes. For now, only the legacy baseline exists, so routing through this +/// type preserves current behavior while giving future feature-gated changes a +/// single table-shaped home. #[derive(Clone, Copy, Debug, PartialEq)] pub struct SlotParams { pub(crate) ns_per_slot: u128, @@ -87,35 +87,20 @@ impl SlotParams { } /// Returns the per-bank cost limits for these params. - pub(crate) const fn cost_limits( - self, - raise_block_limits_to_100m: bool, - ) -> (u64, u64, u64, u64) { - let (max_writable_account_units, max_block_units) = if raise_block_limits_to_100m { - ( - self.max_writable_account_units - .saturating_mul(100) - .saturating_div(60), - self.max_block_units.saturating_mul(100).saturating_div(60), - ) - } else { - (self.max_writable_account_units, self.max_block_units) - }; - + pub const fn cost_limits(self) -> (u64, u64, u64, u64) { ( - max_writable_account_units, - max_block_units, + self.max_writable_account_units, + self.max_block_units, self.max_vote_units, self.max_block_accounts_data_size_delta, ) } } -pub const LEGACY_HASHES_PER_TICK: u64 = 62_500; pub(crate) const LEGACY_SLOT_PARAMS: SlotParams = SlotParams { ns_per_slot: 400_000_000, slots_per_year: 78_892_314.984, - hashes_per_tick: Some(LEGACY_HASHES_PER_TICK), + hashes_per_tick: Some(62_500), max_block_units: 60_000_000, max_writable_account_units: 24_000_000, max_vote_units: 36_000_000, @@ -126,92 +111,6 @@ pub(crate) const LEGACY_SLOT_PARAMS: SlotParams = SlotParams { partitioned_epoch_rewards_stake_account_stores_per_block: 4096, }; -pub(crate) const SLOT_PARAMS_350MS: SlotParams = SlotParams { - ns_per_slot: 350_000_000, - slots_per_year: 90_162_645.696, - hashes_per_tick: Some(54_687), - max_block_units: 52_500_000, - max_writable_account_units: 21_000_000, - max_vote_units: 31_500_000, - max_block_accounts_data_size_delta: 87_500_000, - max_data_shreds_per_slot: 28_672, - max_code_shreds_per_slot: 28_672, - max_entry_bytes_per_slot: 18_350_080, - partitioned_epoch_rewards_stake_account_stores_per_block: 3_584, -}; - -pub(crate) const SLOT_PARAMS_300MS: SlotParams = SlotParams { - ns_per_slot: 300_000_000, - slots_per_year: 105_189_753.312, - hashes_per_tick: Some(46_875), - max_block_units: 45_000_000, - max_writable_account_units: 18_000_000, - max_vote_units: 27_000_000, - max_block_accounts_data_size_delta: 75_000_000, - max_data_shreds_per_slot: 24_576, - max_code_shreds_per_slot: 24_576, - max_entry_bytes_per_slot: 15_728_640, - partitioned_epoch_rewards_stake_account_stores_per_block: 3_072, -}; - -pub(crate) const SLOT_PARAMS_250MS: SlotParams = SlotParams { - ns_per_slot: 250_000_000, - slots_per_year: 126_227_703.974, - hashes_per_tick: Some(39_062), - max_block_units: 37_500_000, - max_writable_account_units: 15_000_000, - max_vote_units: 22_500_000, - max_block_accounts_data_size_delta: 62_500_000, - max_data_shreds_per_slot: 20_480, - max_code_shreds_per_slot: 20_480, - max_entry_bytes_per_slot: 13_107_200, - partitioned_epoch_rewards_stake_account_stores_per_block: 2_560, -}; - -pub(crate) const SLOT_PARAMS_200MS: SlotParams = SlotParams { - ns_per_slot: 200_000_000, - slots_per_year: 157_784_629.968, - hashes_per_tick: Some(31_250), - max_block_units: 30_000_000, - max_writable_account_units: 12_000_000, - max_vote_units: 18_000_000, - max_block_accounts_data_size_delta: 50_000_000, - max_data_shreds_per_slot: 16_384, - max_code_shreds_per_slot: 16_384, - max_entry_bytes_per_slot: 10_485_760, - partitioned_epoch_rewards_stake_account_stores_per_block: 2_048, -}; - -/// Slot-time reduction gates in the intended activation order. -const SLOT_TIME_REDUCTION_PARAMS: [(Pubkey, SlotParams); 4] = [ - ( - feature_set::reduce_slot_time_to_350ms::ID, - SLOT_PARAMS_350MS, - ), - ( - feature_set::reduce_slot_time_to_300ms::ID, - SLOT_PARAMS_300MS, - ), - ( - feature_set::reduce_slot_time_to_250ms::ID, - SLOT_PARAMS_250MS, - ), - ( - feature_set::reduce_slot_time_to_200ms::ID, - SLOT_PARAMS_200MS, - ), -]; - -/// Returns slot-time feature gates mapped to runtime slot parameters. -pub fn slot_time_feature_gates() -> [(Pubkey, SlotParams); 4] { - SLOT_TIME_REDUCTION_PARAMS -} - -/// Returns all slot-time reduction feature IDs in activation order. -pub fn slot_time_feature_ids() -> [Pubkey; 4] { - SLOT_TIME_REDUCTION_PARAMS.map(|(feature_id, _)| feature_id) -} - /// Bank-local archive of slot-parameter transitions. /// /// This is rebuilt from feature accounts on startup instead of being serialized @@ -225,49 +124,25 @@ pub(crate) struct SlotParamsArchive { /// This is used when a bank, usually the root bank, must answer "which /// parameters apply to some other slot?" Examples include shred filtering /// for incoming shreds and inflation calculations that span historical slot - /// ranges. The transitions are normalized so slot duration never increases - /// as slots advance, even if slot-time features activate out of order. + /// ranges. param_transitions: BTreeMap, } impl Default for SlotParamsArchive { fn default() -> Self { - Self::new( - &FeatureSet::default(), - &EpochSchedule::default(), - LEGACY_SLOT_PARAMS, - ) + Self::new(&EpochSchedule::default(), LEGACY_SLOT_PARAMS) } } impl SlotParamsArchive { - /// Rebuilds slot-parameter transitions from the active feature set. - pub(crate) fn new( - feature_set: &FeatureSet, - epoch_schedule: &EpochSchedule, - baseline_params: SlotParams, - ) -> Self { - let mut param_transitions = BTreeMap::from([(0, baseline_params)]); - let mut earliest_same_or_shorter_slot = Slot::MAX; - - // The feature table is ordered longest-to-shortest. Walk it in reverse - // so once a same-or-shorter target is effective at slot S, any longer - // target effective at S or later is known to be redundant. - for (feature_id, params) in slot_time_feature_gates().into_iter().rev() { - if params.ns_per_slot > baseline_params.ns_per_slot { - continue; - } - let Some(activation_slot) = feature_set.activated_slot(&feature_id) else { - continue; - }; - let effective_slot = Self::feature_effective_slot(epoch_schedule, activation_slot); - if effective_slot < earliest_same_or_shorter_slot { - param_transitions.insert(effective_slot, params); - earliest_same_or_shorter_slot = effective_slot; - } + /// Rebuilds slot-parameter transitions from the supplied baseline. + /// + /// `_epoch_schedule` is accepted now so the call sites already have the + /// right shape for delayed, epoch-boundary-effective feature transitions. + pub(crate) fn new(_epoch_schedule: &EpochSchedule, baseline_params: SlotParams) -> Self { + Self { + param_transitions: BTreeMap::from([(0, baseline_params)]), } - - Self { param_transitions } } /// Returns the baseline params supplied at genesis or snapshot restore. @@ -320,41 +195,4 @@ impl SlotParamsArchive { let remaining_slots = u128::from(end_slot.saturating_sub(cursor)) + 1; duration.saturating_add(remaining_slots.saturating_mul(params.ns_per_slot())) } - - /// Returns the first slot where a slot-time feature may affect bank state. - /// - /// A gate that activates in epoch E is effective starting at the first slot - /// of epoch E + 1, giving shred filters a full epoch of advance notice - /// before enforcing lower shred limits. - fn feature_effective_slot(epoch_schedule: &EpochSchedule, activation_slot: Slot) -> Slot { - let activation_epoch = epoch_schedule.get_epoch(activation_slot); - epoch_schedule.get_first_slot_in_epoch(activation_epoch.saturating_add(1)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_cost_limits_scaling_matches_simd_0525() { - for (params, expected_account_limit, expected_block_limit) in [ - (LEGACY_SLOT_PARAMS, 40_000_000, 100_000_000), - (SLOT_PARAMS_350MS, 35_000_000, 87_500_000), - (SLOT_PARAMS_300MS, 30_000_000, 75_000_000), - (SLOT_PARAMS_250MS, 25_000_000, 62_500_000), - (SLOT_PARAMS_200MS, 20_000_000, 50_000_000), - ] { - let (_, _, vote_limit, data_size_limit) = params.cost_limits(false); - assert_eq!( - params.cost_limits(true), - ( - expected_account_limit, - expected_block_limit, - vote_limit, - data_size_limit - ) - ); - } - } } diff --git a/turbine/src/broadcast_stage/standard_broadcast_run.rs b/turbine/src/broadcast_stage/standard_broadcast_run.rs index 2e103859f56..ef1bb8525a5 100644 --- a/turbine/src/broadcast_stage/standard_broadcast_run.rs +++ b/turbine/src/broadcast_stage/standard_broadcast_run.rs @@ -676,12 +676,7 @@ mod test { shred::{DATA_SHREDS_PER_FEC_BLOCK, max_ticks_per_n_shreds}, }, solana_net_utils::{SocketAddrSpace, sockets::bind_to_localhost_unique}, - solana_pubkey::Pubkey, - solana_runtime::{ - bank::{Bank, SlotLeader}, - genesis_utils::{activate_feature, deactivate_features}, - slot_params::{slot_time_feature_gates, slot_time_feature_ids}, - }, + solana_runtime::bank::Bank, solana_signer::Signer, std::{ops::Deref, sync::Arc, time::Duration}, test_case::test_case, @@ -745,65 +740,6 @@ mod test { Arc::new(LeaderScheduleCache::new_from_bank(bank)) } - fn broadcast_shred_limits_for_slot_time_features( - feature_ids: impl IntoIterator, - ) -> (u32, u32) { - let ledger_path = get_tmp_ledger_path!(); - let blockstore = Arc::new( - Blockstore::open(&ledger_path).expect("Expected to be able to open database ledger"), - ); - let mut genesis_config = create_genesis_config(10_000).genesis_config; - let slot_time_features = slot_time_feature_ids().to_vec(); - deactivate_features(&mut genesis_config, &slot_time_features); - for feature_id in feature_ids { - activate_feature(&mut genesis_config, feature_id); - } - - let (parent_bank, bank_forks) = - Bank::new_for_tests(&genesis_config).wrap_with_bank_forks_for_tests(); - parent_bank.set_tick_height(parent_bank.max_tick_height()); - Bank::calculate_and_set_block_id_for_dcou(&parent_bank); - - let effective_slot = parent_bank.epoch_schedule().get_first_slot_in_epoch(1); - let bank = Bank::new_from_parent_with_bank_forks( - &bank_forks, - parent_bank, - SlotLeader::default(), - effective_slot, - ); - let ticks = create_ticks(1, 0, genesis_config.hash()); - let receive_results = ReceiveResults { - component: BlockComponent::EntryBatch(ticks.clone()), - bank: bank.clone(), - last_tick_height: bank.tick_height() + ticks.len() as u64, - }; - let (socket_sender, _socket_receiver) = unbounded(); - let (blockstore_sender, _blockstore_receiver) = unbounded(); - let (votor_event_sender, _votor_event_receiver) = unbounded(); - let mut standard_broadcast_run = StandardBroadcastRun::new( - 0, - Arc::new(MigrationStatus::default()), - votor_event_sender, - test_leader_schedule_cache(&bank), - ); - - standard_broadcast_run - .process_receive_results( - &Keypair::new(), - &blockstore, - &socket_sender, - &blockstore_sender, - receive_results, - &mut ProcessShredsStats::default(), - ) - .unwrap(); - - ( - standard_broadcast_run.max_data_shreds_per_slot, - standard_broadcast_run.max_code_shreds_per_slot, - ) - } - #[test] fn test_interrupted_slot_last_shred() { let keypair = Arc::new(Keypair::new()); @@ -1222,28 +1158,52 @@ mod test { #[test] fn test_update_shred_limits_from_bank() { - assert_eq!( - broadcast_shred_limits_for_slot_time_features(std::iter::empty()), - ( - DEFAULT_MAX_DATA_SHREDS_PER_SLOT, - DEFAULT_MAX_CODE_SHREDS_PER_SLOT - ) - ); + agave_logger::setup(); + let num_shreds_per_slot = 2; + let ( + blockstore, + genesis_config, + cluster_info, + parent_bank, + leader_keypair, + socket, + bank_forks, + ) = setup(num_shreds_per_slot); + let bank = new_child_bank(&parent_bank, 1); + let ticks = create_ticks(1, 0, genesis_config.hash()); + let receive_results = ReceiveResults { + component: BlockComponent::EntryBatch(ticks), + bank: bank.clone(), + last_tick_height: bank.tick_height() + 1, + }; - for ((feature_id, _), expected_shreds_per_slot) in slot_time_feature_gates() - .into_iter() - .zip([28_672, 24_576, 20_480, 16_384]) - { - assert_eq!( - broadcast_shred_limits_for_slot_time_features([feature_id]), - (expected_shreds_per_slot, expected_shreds_per_slot) - ); - } + let (votor_event_sender, _votor_event_receiver) = unbounded(); + let mut standard_broadcast_run = StandardBroadcastRun::new( + 0, + Arc::new(MigrationStatus::default()), + votor_event_sender, + test_leader_schedule_cache(&bank), + ); + standard_broadcast_run.max_data_shreds_per_slot = 1; + standard_broadcast_run.max_code_shreds_per_slot = 1; + standard_broadcast_run + .test_process_receive_results( + &leader_keypair, + &cluster_info, + &socket, + &blockstore, + receive_results, + &bank_forks, + ) + .unwrap(); - let [reduce_to_350ms, _, _, reduce_to_200ms] = slot_time_feature_ids(); assert_eq!( - broadcast_shred_limits_for_slot_time_features([reduce_to_350ms, reduce_to_200ms]), - (16_384, 16_384) + standard_broadcast_run.max_data_shreds_per_slot, + bank.max_data_shreds_per_slot() + ); + assert_eq!( + standard_broadcast_run.max_code_shreds_per_slot, + bank.max_code_shreds_per_slot() ); } From 70f2be18ac4e9820064232ae412f9299432f9bf8 Mon Sep 17 00:00:00 2001 From: steviez Date: Wed, 3 Jun 2026 23:19:26 -0500 Subject: [PATCH 198/576] blockstore: Adjust cleanup service edge condition (#12937) Adjust BlockstoreCleanupService to retain at least the latest root when purging; old logic allowed purging of the latest root. Practically speaking, this case will not get hit on a real cluster given that the default blockstore retention is pretty large and other things will fail first if there is not a root first --- ledger/src/blockstore/blockstore_purge.rs | 4 ++-- ledger/src/blockstore/error.rs | 4 ++-- ledger/src/blockstore_cleanup_service.rs | 23 +++++++++++++---------- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/ledger/src/blockstore/blockstore_purge.rs b/ledger/src/blockstore/blockstore_purge.rs index e4e7584b61a..cfe49c1052a 100644 --- a/ledger/src/blockstore/blockstore_purge.rs +++ b/ledger/src/blockstore/blockstore_purge.rs @@ -445,9 +445,9 @@ impl Blockstore { // Deleting data newer than the latest root is likely to interfere // with replay so save any callers from themself let max_root = self.max_root(); - if max_slot_to_delete > max_root { + if max_slot_to_delete >= max_root { return Err(BlockstoreError::ManualPurge( - BlockstoreManualPurgeError::SlotNewerThanRoot { + BlockstoreManualPurgeError::SlotGreaterThanOrEqualToRoot { request_slot: max_slot_to_delete, max_root, }, diff --git a/ledger/src/blockstore/error.rs b/ledger/src/blockstore/error.rs index e97bbdbee8a..2f4408ff7f8 100644 --- a/ledger/src/blockstore/error.rs +++ b/ledger/src/blockstore/error.rs @@ -121,8 +121,8 @@ pub enum BlockstoreManualPurgeError { #[error("purge request sender is unavailable")] SenderUnavailable, - #[error("purge request for slot {request_slot} is newer than the latest root {max_root}")] - SlotNewerThanRoot { request_slot: Slot, max_root: Slot }, + #[error("purge request for slot {request_slot} must be less than the latest root {max_root}")] + SlotGreaterThanOrEqualToRoot { request_slot: Slot, max_root: Slot }, #[error("purge request try send error")] TrySend, diff --git a/ledger/src/blockstore_cleanup_service.rs b/ledger/src/blockstore_cleanup_service.rs index 1323671b7ff..fcb08ce4481 100644 --- a/ledger/src/blockstore_cleanup_service.rs +++ b/ledger/src/blockstore_cleanup_service.rs @@ -194,8 +194,10 @@ impl BlockstoreCleanupService { return; }; - // Ensure we don't cleanup anything past the last root we saw - let lowest_cleanup_slot = std::cmp::min(lowest_slot + num_slots_to_clean - 1, root); + // Use min() to ensure we do not purge the latest root or anything newer + // Purge is inclusive so subtract one from min() result + let lowest_cleanup_slot = + std::cmp::min(lowest_slot + num_slots_to_clean, root).saturating_sub(1); match cleanup_request_sender.try_send(lowest_cleanup_slot) { Ok(()) => {} @@ -313,7 +315,7 @@ mod tests { // Start with 1 as the latest root let mut latest_root = 1; blockstore.set_roots(std::iter::once(&latest_root)).unwrap(); - // Auto clean will select slot 1 (latest_root) as min clean slot + // Auto clean will select slot 0 (latest_root - 1) as min clean slot let max_ledger_shreds = Some(1); BlockstoreCleanupService::maybe_generate_automatic_cleanup_request( &blockstore, @@ -322,7 +324,7 @@ mod tests { &mut last_purge_slot, purge_interval, ); - assert_eq!(receiver.try_recv().unwrap(), latest_root); + assert_eq!(receiver.try_recv().unwrap(), latest_root - 1); // Reset last_purge_slot assert_eq!(last_purge_slot, 1); @@ -334,7 +336,7 @@ mod tests { &mut last_purge_slot, purge_interval, ); - assert_eq!(receiver.try_recv().unwrap(), latest_root); + assert_eq!(receiver.try_recv().unwrap(), latest_root - 1); // Reset last_purge_slot assert_eq!(last_purge_slot, 1); last_purge_slot = 0; @@ -385,13 +387,14 @@ mod tests { &mut last_purge_slot, purge_interval, ); - assert_eq!(receiver.try_recv().unwrap(), latest_root); + assert_eq!(receiver.try_recv().unwrap(), latest_root - 1); // Reset last_purge_slot assert_eq!(last_purge_slot, 1); last_purge_slot = 0; for slot in 1..=num_slots { - // Set last_root to make slots <= slot eligible for cleaning + // Update latest_root so that any slots < latest_root will become + // eligible to be cleaned latest_root = slot; blockstore.set_roots(std::iter::once(&latest_root)).unwrap(); // Set max_ledger_shreds to 0 so that all eligible slots are cleaned @@ -403,7 +406,7 @@ mod tests { &mut last_purge_slot, purge_interval, ); - assert_eq!(receiver.try_recv().unwrap(), latest_root); + assert_eq!(receiver.try_recv().unwrap(), latest_root - 1); } } @@ -439,10 +442,10 @@ mod tests { // A request will be generated and consumed so channel should be empty assert!(receiver.is_empty()); - // Ensure that slots 0-40 are not present + // Ensure that slots 0-39 are not present; the root (40) is retained blockstore .slot_meta_iterator(0) .unwrap() - .for_each(|(slot, _)| assert!(slot > 40)); + .for_each(|(slot, _)| assert!(slot >= 40)); } } From 6dd90fc034b3098a543c959a47156f1084ccc1df Mon Sep 17 00:00:00 2001 From: Akhilesh Singhania Date: Thu, 4 Jun 2026 15:48:26 +0200 Subject: [PATCH 199/576] CertBuilder: unify handling of skip and notarfallback certs (#12945) CertBuilder: unify handling of certs that need two votes --- .../src/consensus_pool/certificate_builder.rs | 83 +++---------------- 1 file changed, 12 insertions(+), 71 deletions(-) diff --git a/votor/src/consensus_pool/certificate_builder.rs b/votor/src/consensus_pool/certificate_builder.rs index e688ec62293..2d8c13afbd6 100644 --- a/votor/src/consensus_pool/certificate_builder.rs +++ b/votor/src/consensus_pool/certificate_builder.rs @@ -98,20 +98,9 @@ enum BuilderType { signature: SignatureProjective, bitmap: BitVec, }, - /// A [`Certificate`] of type Skip will be produced. - /// - /// It can require two types of [`VoteMessage`]s. - /// In order to be able to produce certificates for reward purposes, signature aggregates for the two types are tracked separately. - Skip { - signature0: SignatureProjective, - bitmap0: BitVec, - sig_and_bitmap1: Option<(SignatureProjective, BitVec)>, - }, - /// A [`Certificate`] of type NotarFallback will be produced. - /// - /// It can require two types of [`VoteMessage`]s. - /// This certificate is not used for rewards so its signature can be aggregated in a single container. - NotarFallback { + + /// The produced [`Certificate`] will require two types of [`VoteMessage`]s. + DoubleVote { signature: SignatureProjective, bitmap0: BitVec, bitmap1: Option>, @@ -122,16 +111,13 @@ impl BuilderType { /// Creates a new instance of [`BuilderType`]. fn new(cert_type: &CertificateType) -> Self { match cert_type { - CertificateType::Skip(_) => Self::Skip { - signature0: SignatureProjective::identity(), - bitmap0: default_bitvec(), - sig_and_bitmap1: None, - }, - CertificateType::NotarizeFallback(_, _) => Self::NotarFallback { - signature: SignatureProjective::identity(), - bitmap0: default_bitvec(), - bitmap1: None, - }, + CertificateType::Skip(_) | CertificateType::NotarizeFallback(_, _) => { + Self::DoubleVote { + signature: SignatureProjective::identity(), + bitmap0: default_bitvec(), + bitmap1: None, + } + } _ => Self::SingleVote { signature: SignatureProjective::identity(), bitmap: default_bitvec(), @@ -147,39 +133,7 @@ impl BuilderType { ) -> Result<(), AggregateError> { let vote_types = cert_type.limits_and_vote_types().1; match self { - Self::Skip { - signature0, - bitmap0, - sig_and_bitmap1, - } => { - assert_eq!(vote_types.len(), 2); - for msg in msgs { - let vote_type = msg.vote.get_type(); - if vote_type == vote_types[0] { - try_set_bitmap(bitmap0, msg.rank)?; - } else { - assert_eq!(vote_type, vote_types[1]); - let (_, bitmap) = sig_and_bitmap1.get_or_insert_with(|| { - (SignatureProjective::identity(), default_bitvec()) - }); - try_set_bitmap(bitmap, msg.rank)?; - } - } - signature0.aggregate_with(msgs.iter().filter_map(|msg| { - (msg.vote.get_type() == vote_types[0]).then_some(&msg.signature) - }))?; - sig_and_bitmap1 - .as_mut() - .map(|(signature, _)| { - signature.aggregate_with(msgs.iter().filter_map(|msg| { - (msg.vote.get_type() == vote_types[1]).then_some(&msg.signature) - })) - }) - .unwrap_or(Ok(()))?; - Ok(()) - } - - Self::NotarFallback { + Self::DoubleVote { signature, bitmap0, bitmap1, @@ -215,20 +169,7 @@ impl BuilderType { Self::SingleVote { signature, bitmap } => { build_cert_from_bitmap(cert_type, signature, bitmap).map_err(BuildError::Encode) } - Self::Skip { - mut signature0, - bitmap0, - sig_and_bitmap1, - } => match sig_and_bitmap1 { - None => build_cert_from_bitmap(cert_type, signature0, bitmap0) - .map_err(BuildError::Encode), - Some((signature1, bitmap1)) => { - signature0.aggregate_with([signature1].iter())?; - build_cert_from_bitmaps(cert_type, signature0, bitmap0, bitmap1) - .map_err(BuildError::Encode) - } - }, - Self::NotarFallback { + Self::DoubleVote { signature, bitmap0, bitmap1, From 16e70aab03f4c8ce22d472cc938cf1e4ef9e608f Mon Sep 17 00:00:00 2001 From: Akhilesh Singhania Date: Thu, 4 Jun 2026 16:00:14 +0200 Subject: [PATCH 200/576] consensus_pool: Use `NonZero` to represent total stake (#12946) consensus_pool: Use NonZero to represent total stake --- votor/src/consensus_pool.rs | 31 +++----- .../src/consensus_pool/slot_stake_counters.rs | 42 ++++++----- votor/src/consensus_pool/vote_pool.rs | 73 ++++++++++++++----- 3 files changed, 91 insertions(+), 55 deletions(-) diff --git a/votor/src/consensus_pool.rs b/votor/src/consensus_pool.rs index 6c9bbb2823d..366036b8954 100644 --- a/votor/src/consensus_pool.rs +++ b/votor/src/consensus_pool.rs @@ -29,7 +29,7 @@ use { solana_hash::Hash, solana_pubkey::Pubkey, solana_runtime::{bank::Bank, validated_block_finalization::ValidatedBlockFinalizationCert}, - std::{cmp::Ordering, collections::BTreeMap, num::NonZeroU64, sync::Arc}, + std::{cmp::Ordering, collections::BTreeMap, num::NonZero, sync::Arc}, thiserror::Error, }; @@ -76,7 +76,7 @@ fn get_key_and_stakes( root_bank: &Bank, slot: Slot, rank: u16, -) -> Result<(Pubkey, Stake, Stake), AddVoteError> { +) -> Result<(Pubkey, NonZero, NonZero), AddVoteError> { let epoch_stakes = root_bank.epoch_stakes_from_slot(slot).ok_or_else(|| { AddVoteError::EpochStakesNotFound(root_bank.epoch_schedule().get_epoch(slot)) })?; @@ -86,17 +86,15 @@ fn get_key_and_stakes( else { return Err(AddVoteError::InvalidRank(rank)); }; - if entry.stake == 0 { - // Since we have a valid rank, this should never happen, there is no rank for zero stake. - panic!( - "Validator stake is zero for pubkey: {}", - entry.vote_account_pubkey - ); - } Ok(( entry.vote_account_pubkey, - entry.stake, - epoch_stakes.total_stake(), + NonZero::new(entry.stake).unwrap_or_else(|| { + panic!( + "Validator stake is zero for pubkey: {}", + entry.vote_account_pubkey, + ) + }), + NonZero::new(epoch_stakes.total_stake()).expect("expect total stakes to not be 0"), )) } @@ -176,7 +174,7 @@ impl ConsensusPool { &mut self, vote: VoteMessage, validator_vote_key: Pubkey, - validator_stake: Stake, + validator_stake: NonZero, ) -> Option { let vote_type = vote.vote.get_type(); let pool = self @@ -206,7 +204,7 @@ impl ConsensusPool { vote: &Vote, block_id: Option, events: &mut Vec, - total_stake: Stake, + total_stake: NonZero, ) -> Result>, AddVoteError> { let slot = vote.slot(); let mut new_certificates_to_send = Vec::new(); @@ -233,7 +231,6 @@ impl ConsensusPool { }) }) .sum::(); - let total_stake = NonZeroU64::new(total_stake).unwrap(); if Fraction::new(accumulated_stake, total_stake) < limit { continue; } @@ -414,12 +411,6 @@ impl ConsensusPool { let (validator_vote_key, validator_stake, total_stake) = get_key_and_stakes(root_bank, vote_slot, rank)?; - // Since we have a valid rank, this should never happen, there is no rank for zero stake. - assert_ne!( - validator_stake, 0, - "Validator stake is zero for pubkey: {validator_vote_key}" - ); - self.stats.incoming_votes = self.stats.incoming_votes.saturating_add(1); if vote_slot < root_bank.slot() { self.stats.out_of_range_votes = self.stats.out_of_range_votes.saturating_add(1); diff --git a/votor/src/consensus_pool/slot_stake_counters.rs b/votor/src/consensus_pool/slot_stake_counters.rs index 874a7a462f6..e47a60a22dd 100644 --- a/votor/src/consensus_pool/slot_stake_counters.rs +++ b/votor/src/consensus_pool/slot_stake_counters.rs @@ -11,13 +11,13 @@ use { solana_clock::Slot, solana_hash::Hash, solana_leader_schedule::NUM_CONSECUTIVE_LEADER_SLOTS, - std::{collections::BTreeMap, num::NonZeroU64}, + std::{collections::BTreeMap, num::NonZero}, }; -#[derive(Debug, Default)] +#[derive(Debug)] pub(crate) struct SlotStakeCounters { my_first_vote: Option, - total_stake: Stake, + total_stake: NonZero, skip_total: Stake, notarize_total: Stake, notarize_entry_total: BTreeMap, @@ -27,10 +27,16 @@ pub(crate) struct SlotStakeCounters { } impl SlotStakeCounters { - pub fn new(total_stake: Stake) -> Self { + pub fn new(total_stake: NonZero) -> Self { Self { + my_first_vote: None, total_stake, - ..Default::default() + skip_total: 0, + notarize_total: 0, + notarize_entry_total: BTreeMap::new(), + top_notarized_stake: 0, + safe_to_notar_sent: vec![], + safe_to_skip_sent: false, } } @@ -110,14 +116,15 @@ impl SlotStakeCounters { } trace!( "safe_to_notar {block_id:?} skip_ratio={} notarized_ratio={}", - self.skip_total as f64 / self.total_stake as f64, - *stake as f64 / self.total_stake as f64 + self.skip_total as f64 / self.total_stake.get() as f64, + *stake as f64 / self.total_stake.get() as f64 ); // Check if the block fits condition (i) 40% of stake holders voted notarize - let total_stake = NonZeroU64::new(self.total_stake).unwrap(); - let notarized_ratio = Fraction::new(*stake, total_stake); - let notarized_plus_skip_ratio = - Fraction::new(self.skip_total.checked_add(*stake).unwrap(), total_stake); + let notarized_ratio = Fraction::new(*stake, self.total_stake); + let notarized_plus_skip_ratio = Fraction::new( + self.skip_total.checked_add(*stake).unwrap(), + self.total_stake, + ); notarized_ratio >= SAFE_TO_NOTAR_MIN_NOTARIZE_ONLY // Check if the block fits condition (ii) 20% notarized, and 60% notarized or skip || (notarized_ratio >= SAFE_TO_NOTAR_MIN_NOTARIZE_FOR_NOTARIZE_OR_SKIP @@ -141,9 +148,8 @@ impl SlotStakeCounters { let num_stake = self .skip_total .saturating_add(self.notarize_total.saturating_sub(self.top_notarized_stake)); - let total_stake = NonZeroU64::new(self.total_stake).unwrap(); - Fraction::new(num_stake, total_stake) >= SAFE_TO_SKIP_THRESHOLD + Fraction::new(num_stake, self.total_stake) >= SAFE_TO_SKIP_THRESHOLD } else { false } @@ -156,7 +162,7 @@ mod tests { #[test] fn test_safe_to_notar_first_in_leader_window() { - let mut counters = SlotStakeCounters::new(100); + let mut counters = SlotStakeCounters::new(NonZero::new(100).unwrap()); let mut events = vec![]; let mut pending_safe_to_notar = vec![]; @@ -208,7 +214,7 @@ mod tests { assert_eq!(stats.event_safe_to_notarize, 1); // Reset counters with slot 4 (first in next leader window) - counters = SlotStakeCounters::new(100); + counters = SlotStakeCounters::new(NonZero::new(100).unwrap()); events.clear(); pending_safe_to_notar.clear(); stats = ConsensusPoolStats::default(); @@ -261,7 +267,7 @@ mod tests { #[test] fn test_safe_to_notar_intrawindow_block() { - let mut counters = SlotStakeCounters::new(100); + let mut counters = SlotStakeCounters::new(NonZero::new(100).unwrap()); let mut events = vec![]; let mut pending_safe_to_notar = vec![]; @@ -300,7 +306,7 @@ mod tests { #[test] fn test_safe_to_skip() { - let mut counters = SlotStakeCounters::new(100); + let mut counters = SlotStakeCounters::new(NonZero::new(100).unwrap()); let mut events = vec![]; let mut pending_safe_to_notar = vec![]; @@ -345,7 +351,7 @@ mod tests { assert_eq!(stats.event_safe_to_skip, 1); // Reset counters - counters = SlotStakeCounters::new(100); + counters = SlotStakeCounters::new(NonZero::new(100).unwrap()); events.clear(); pending_safe_to_notar.clear(); stats = ConsensusPoolStats::default(); diff --git a/votor/src/consensus_pool/vote_pool.rs b/votor/src/consensus_pool/vote_pool.rs index 1a870f62a75..1eb93d089ec 100644 --- a/votor/src/consensus_pool/vote_pool.rs +++ b/votor/src/consensus_pool/vote_pool.rs @@ -3,7 +3,10 @@ use { agave_votor_messages::consensus_message::VoteMessage, solana_hash::Hash, solana_pubkey::Pubkey, - std::collections::{BTreeMap, BTreeSet}, + std::{ + collections::{BTreeMap, BTreeSet}, + num::NonZero, + }, }; /// There are two types of vote pools: @@ -27,14 +30,14 @@ impl SimpleVotePool { pub(super) fn add_vote( &mut self, validator_vote_key: Pubkey, - validator_stake: Stake, + validator_stake: NonZero, vote: VoteMessage, ) -> Option { if !self.prev_voted_validators.insert(validator_vote_key) { return None; } self.votes.push(vote); - self.total_stake = self.total_stake.saturating_add(validator_stake); + self.total_stake = self.total_stake.saturating_add(validator_stake.get()); Some(self.total_stake) } @@ -75,7 +78,7 @@ impl DuplicateBlockVotePool { pub(super) fn add_vote( &mut self, validator_vote_key: Pubkey, - validator_stake: Stake, + validator_stake: NonZero, vote: VoteMessage, ) -> Option { let block_id = *vote.vote.block_id().unwrap(); @@ -96,7 +99,7 @@ impl DuplicateBlockVotePool { vote_entry.votes.push(vote); vote_entry.total_stake_by_key = vote_entry .total_stake_by_key - .saturating_add(validator_stake); + .saturating_add(validator_stake.get()); Some(vote_entry.total_stake_by_key) } @@ -146,16 +149,25 @@ mod test { }; let my_pubkey = Pubkey::new_unique(); - assert_eq!(vote_pool.add_vote(my_pubkey, 10, vote_message), Some(10)); + assert_eq!( + vote_pool.add_vote(my_pubkey, NonZero::new(10).unwrap(), vote_message), + Some(10) + ); assert_eq!(vote_pool.total_stake(), 10); // Adding the same key again should fail - assert_eq!(vote_pool.add_vote(my_pubkey, 10, vote_message), None); + assert_eq!( + vote_pool.add_vote(my_pubkey, NonZero::new(10).unwrap(), vote_message), + None + ); assert_eq!(vote_pool.total_stake(), 10); // Adding a different key should succeed let new_pubkey = Pubkey::new_unique(); - assert_eq!(vote_pool.add_vote(new_pubkey, 60, vote_message), Some(70)); + assert_eq!( + vote_pool.add_vote(new_pubkey, NonZero::new(60).unwrap(), vote_message), + Some(70) + ); assert_eq!(vote_pool.total_stake(), 70); } @@ -170,18 +182,30 @@ mod test { signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), rank: 1, }; - assert_eq!(vote_pool.add_vote(my_pubkey, 10, vote), Some(10)); + assert_eq!( + vote_pool.add_vote(my_pubkey, NonZero::new(10).unwrap(), vote), + Some(10) + ); assert_eq!(vote_pool.total_stake_by_block_id(&block_id), 10); // Adding the same key again should fail - assert_eq!(vote_pool.add_vote(my_pubkey, 10, vote), None); + assert_eq!( + vote_pool.add_vote(my_pubkey, NonZero::new(10).unwrap(), vote), + None + ); // Adding a different bankhash should fail - assert_eq!(vote_pool.add_vote(my_pubkey, 10, vote), None); + assert_eq!( + vote_pool.add_vote(my_pubkey, NonZero::new(10).unwrap(), vote), + None + ); // Adding a different key should succeed let new_pubkey = Pubkey::new_unique(); - assert_eq!(vote_pool.add_vote(new_pubkey, 60, vote), Some(70)); + assert_eq!( + vote_pool.add_vote(new_pubkey, NonZero::new(60).unwrap(), vote), + Some(70) + ); assert_eq!(vote_pool.total_stake_by_block_id(&block_id), 70); } @@ -204,14 +228,20 @@ mod test { // Adding the first 3 votes should succeed, but total_stake should remain at 10 for vote in votes.iter().take(3).cloned() { - assert_eq!(vote_pool.add_vote(my_pubkey, 10, vote), Some(10)); + assert_eq!( + vote_pool.add_vote(my_pubkey, NonZero::new(10).unwrap(), vote), + Some(10) + ); assert_eq!( vote_pool.total_stake_by_block_id(vote.vote.block_id().unwrap()), 10 ); } // Adding the 4th vote should fail - assert_eq!(vote_pool.add_vote(my_pubkey, 10, votes[3]), None); + assert_eq!( + vote_pool.add_vote(my_pubkey, NonZero::new(10).unwrap(), votes[3]), + None + ); assert_eq!( vote_pool.total_stake_by_block_id(votes[3].vote.block_id().unwrap()), 0 @@ -220,7 +250,10 @@ mod test { // Adding a different key should succeed let new_pubkey = Pubkey::new_unique(); for vote in votes.iter().skip(1).take(2).cloned() { - assert_eq!(vote_pool.add_vote(new_pubkey, 60, vote), Some(70)); + assert_eq!( + vote_pool.add_vote(new_pubkey, NonZero::new(60).unwrap(), vote), + Some(70) + ); assert_eq!( vote_pool.total_stake_by_block_id(vote.vote.block_id().unwrap()), 70 @@ -228,13 +261,19 @@ mod test { } // The new key only added 2 votes, so adding block_ids[3] should succeed - assert_eq!(vote_pool.add_vote(new_pubkey, 60, votes[3]), Some(60)); + assert_eq!( + vote_pool.add_vote(new_pubkey, NonZero::new(60).unwrap(), votes[3]), + Some(60) + ); assert_eq!( vote_pool.total_stake_by_block_id(votes[3].vote.block_id().unwrap()), 60 ); // Now if adding the same key again, it should fail - assert_eq!(vote_pool.add_vote(new_pubkey, 60, votes[0]), None); + assert_eq!( + vote_pool.add_vote(new_pubkey, NonZero::new(60).unwrap(), votes[0]), + None + ); } } From a49541ffcb3303db0e14d6f3705208835d5e51bc Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Thu, 4 Jun 2026 10:41:43 -0400 Subject: [PATCH 201/576] blockstore: add secondary feature flag for SIMD-0340 (#12854) --- core/src/window_service.rs | 16 ++- feature-set/src/lib.rs | 10 ++ ledger/src/blockstore.rs | 190 +++++++++++++++++------------ ledger/src/blockstore/tests.rs | 73 ++++++++--- ledger/src/blockstore_processor.rs | 25 +++- ledger/src/shred.rs | 12 ++ 6 files changed, 225 insertions(+), 101 deletions(-) diff --git a/core/src/window_service.rs b/core/src/window_service.rs index 7d1ff101c71..68e745d20e4 100644 --- a/core/src/window_service.rs +++ b/core/src/window_service.rs @@ -131,18 +131,30 @@ fn run_check_duplicate( shred_slot, &root_bank, ); + let validate_chained_block_id_2 = shred::filter::check_feature_activation_from_bank( + &feature_set::validate_chained_block_id_2::id(), + shred_slot, + &root_bank, + ); + let (shred1, shred2) = match shred { PossibleDuplicateShred::LastIndexConflict(shred, conflict) | PossibleDuplicateShred::ErasureConflict(shred, conflict) | PossibleDuplicateShred::MerkleRootConflict(shred, conflict) => (shred, conflict), - PossibleDuplicateShred::ChainedMerkleRootConflict(_shred, _conflict) => { - if validate_chained_block_id { + PossibleDuplicateShred::ChainedMerkleRootConflict(_slot) => { + if validate_chained_block_id || validate_chained_block_id_2 { // Although chained merkle roots are not necessary for agave duplicate resolution protocols, // We still need to mark the block as dead for other client teams. blockstore.set_dead_slot(shred_slot)?; } return Ok(()); } + PossibleDuplicateShred::FixedFECChainedMerkleRootConflict(_slot) => { + if validate_chained_block_id_2 { + blockstore.set_dead_slot(shred_slot)?; + } + return Ok(()); + } PossibleDuplicateShred::Exists(shred) => { // Unlike the other cases we have to wait until here to decide to handle the duplicate and store // in blockstore. This is because the duplicate could have been part of the same insert batch, diff --git a/feature-set/src/lib.rs b/feature-set/src/lib.rs index 611e4480ad9..3a59e237d56 100644 --- a/feature-set/src/lib.rs +++ b/feature-set/src/lib.rs @@ -87,6 +87,7 @@ pub struct FeatureSnapshot { pub relax_post_exec_min_balance_check: bool, pub enable_tx_v1: bool, pub define_ltds_fee_only_semantics: bool, + pub validate_chained_block_id_2: bool, } impl From<&AHashMap> for FeatureSnapshot { @@ -200,6 +201,7 @@ impl From<&AHashMap> for FeatureSnapshot { relax_post_exec_min_balance_check: is_active(&relax_post_exec_min_balance_check::ID), enable_tx_v1: is_active(&enable_tx_v1::ID), define_ltds_fee_only_semantics: is_active(&define_ltds_fee_only_semantics::ID), + validate_chained_block_id_2: is_active(&validate_chained_block_id_2::ID), } } } @@ -1483,6 +1485,10 @@ pub mod validate_chained_block_id { solana_pubkey::declare_id!("vcmrbYbiMVKaq1snKP6eCacNDcr6qZvpCNUjmk6gxvZ"); } +pub mod validate_chained_block_id_2 { + solana_pubkey::declare_id!("vcmrw431aNM8ngQ46derkZXipoTGQdbHkEygBDh12dA"); +} + pub mod validator_admission_ticket { solana_pubkey::declare_id!("VAT9huvhPjRN9cyrPytq9rwvEJ3J4ADtjdncgZRyANJ"); } @@ -2583,6 +2589,10 @@ pub static FEATURE_NAMES: LazyLock> = LazyLock::n set_lamports_per_byte_to_6960::id(), "SIMD-0438: Reset lamports per byte to legacy value of 6960", ), + ( + validate_chained_block_id_2::id(), + "SIMD-340: Encompassing check for validate chained block ID", + ), /*************** ADD NEW FEATURES HERE ***************/ /***** ADD NEW FEATURE BOOL TO `FeatureSnapshot` *****/ ] diff --git a/ledger/src/blockstore.rs b/ledger/src/blockstore.rs index 65bb30cdf90..3da26b4e298 100644 --- a/ledger/src/blockstore.rs +++ b/ledger/src/blockstore.rs @@ -197,10 +197,11 @@ pub enum PossibleDuplicateShred { shred::Payload, // conflict ), // Merkle root chaining conflict with previous fec set - ChainedMerkleRootConflict( - Shred, // original - shred::Payload, // conflict - ), + // As part of SIMD-0340 + ChainedMerkleRootConflict(Slot), + // Secondary chained merkle root conflict with previous fec set + // As part of SIMD-0340 + FixedFECChainedMerkleRootConflict(Slot), } impl PossibleDuplicateShred { @@ -210,7 +211,8 @@ impl PossibleDuplicateShred { Self::LastIndexConflict(shred, _) => shred.slot(), Self::ErasureConflict(shred, _) => shred.slot(), Self::MerkleRootConflict(shred, _) => shred.slot(), - Self::ChainedMerkleRootConflict(shred, _) => shred.slot(), + Self::ChainedMerkleRootConflict(slot) + | Self::FixedFECChainedMerkleRootConflict(slot) => *slot, } } } @@ -870,6 +872,33 @@ impl Blockstore { ) } + /// Attempts to find the previous consecutive erasure set for `erasure_set` + /// using the fact that FEC sets are fixed in size at DATA_SHREDS_PER_FEC_BLOCK + /// data shreds. + /// + /// Only for use in the `Original` column + fn previous_fec_set_shred_id( + &self, + erasure_set: &ErasureSetId, + merkle_root_metas: &HashMap<(BlockLocation, ErasureSetId), WorkingEntry>, + ) -> Option { + let prev_erasure_set = erasure_set.previous_fec_set()?; + let prev_merkle_root_meta = merkle_root_metas + .get(&(BlockLocation::Original, prev_erasure_set)) + .map(WorkingEntry::as_ref) + .map(Cow::Borrowed) + .or_else(|| { + self.merkle_root_meta(prev_erasure_set) + .unwrap() + .map(Cow::Owned) + })?; + Some(ShredId::new( + erasure_set.slot(), + prev_merkle_root_meta.first_received_shred_index(), + prev_merkle_root_meta.first_received_shred_type(), + )) + } + /// Attempts to find the previous consecutive erasure set for `erasure_set`. /// /// Checks the map `erasure_metas`, if not present scans blockstore. Returns None @@ -1727,9 +1756,6 @@ impl Blockstore { continue; } let (slot, _) = erasure_set.store_key(); - if self.has_duplicate_shreds_in_slot(slot) { - continue; - } // First coding shred from this erasure batch, check the forward merkle root chaining // Note: This check is not performed on Alternate columns, as we cannot repair coding shreds @@ -1747,13 +1773,20 @@ impl Blockstore { .get(&(BlockLocation::Original, shred_id)) .expect("Erasure meta was just created, initial shred must exist"); - self.check_forward_chained_merkle_root_consistency( - shred, - erasure_meta, - &shred_insertion_tracker.just_inserted_shreds, - &shred_insertion_tracker.merkle_root_metas, - &mut shred_insertion_tracker.duplicate_shreds, - ); + // Forward check for SIMD-0340 + if let Some(next_fec_set_index) = erasure_meta.next_fec_set_index() { + let next_erasure_set = ErasureSetId::new(slot, next_fec_set_index); + if !self.check_forward_chained_merkle_root_consistency( + shred, + next_erasure_set, + &shred_insertion_tracker.just_inserted_shreds, + &shred_insertion_tracker.merkle_root_metas, + ) { + shred_insertion_tracker + .duplicate_shreds + .push(PossibleDuplicateShred::ChainedMerkleRootConflict(slot)); + } + } } for ((location, erasure_set), working_merkle_root_meta) in @@ -1764,9 +1797,6 @@ impl Blockstore { continue; } let (slot, _) = erasure_set.store_key(); - if self.has_duplicate_shreds_in_slot(slot) { - continue; - } // First shred from this erasure batch, check the backwards merkle root chaining // Note: this check is not performed on Alternate columns as they are already validated @@ -1786,12 +1816,60 @@ impl Blockstore { .get(&(*location, shred_id)) .expect("Merkle root meta was just created, initial shred must exist"); - self.check_backwards_chained_merkle_root_consistency( - shred, - &shred_insertion_tracker.just_inserted_shreds, - &shred_insertion_tracker.erasure_metas, - &mut shred_insertion_tracker.duplicate_shreds, - ); + // Backward check for SIMD-0340 + if let Some((_prev_erasure_set, prev_erasure_meta)) = self + .previous_erasure_set(*erasure_set, &shred_insertion_tracker.erasure_metas) + .expect("Expect database operations to succeed") + { + let prev_shred_id = ShredId::new( + slot, + prev_erasure_meta + .first_received_coding_shred_index() + .expect("First received coding index must fit in u32"), + ShredType::Code, + ); + if !self.check_backwards_chained_merkle_root_consistency( + shred, + prev_shred_id, + &shred_insertion_tracker.just_inserted_shreds, + ) { + shred_insertion_tracker + .duplicate_shreds + .push(PossibleDuplicateShred::ChainedMerkleRootConflict(slot)); + continue; + } + } + + // Encompassing forward check for SIMD-0340 + if let Some(next_erasure_set) = erasure_set.next_fec_set() { + if !self.check_forward_chained_merkle_root_consistency( + shred, + next_erasure_set, + &shred_insertion_tracker.just_inserted_shreds, + &shred_insertion_tracker.merkle_root_metas, + ) { + shred_insertion_tracker.duplicate_shreds.push( + PossibleDuplicateShred::FixedFECChainedMerkleRootConflict(slot), + ); + continue; + } + } + + // Encompassing backward check for SIMD-0340 + if let Some(prev_shred_id) = self + .previous_fec_set_shred_id(erasure_set, &shred_insertion_tracker.merkle_root_metas) + { + if !self.check_backwards_chained_merkle_root_consistency( + shred, + prev_shred_id, + &shred_insertion_tracker.just_inserted_shreds, + ) { + shred_insertion_tracker.duplicate_shreds.push( + PossibleDuplicateShred::FixedFECChainedMerkleRootConflict(slot), + ); + continue; + } + } } } @@ -3032,21 +3110,14 @@ impl Blockstore { fn check_forward_chained_merkle_root_consistency( &self, shred: &Shred, - erasure_meta: &ErasureMeta, + next_erasure_set: ErasureSetId, just_inserted_shreds: &HashMap<(BlockLocation, ShredId), Cow<'_, Shred>>, merkle_root_metas: &HashMap<(BlockLocation, ErasureSetId), WorkingEntry>, - duplicate_shreds: &mut Vec, ) -> bool { - debug_assert!(erasure_meta.check_coding_shred(shred)); let slot = shred.slot(); let erasure_set = shred.erasure_set(); // If a shred from the next fec set has already been inserted, check the chaining - let Some(next_fec_set_index) = erasure_meta.next_fec_set_index() else { - error!("Invalid erasure meta, unable to compute next fec set index {erasure_meta:?}"); - return false; - }; - let next_erasure_set = ErasureSetId::new(slot, next_fec_set_index); let Some(next_merkle_root_meta) = merkle_root_metas .get(&(BlockLocation::Original, next_erasure_set)) .map(WorkingEntry::as_ref) @@ -3073,10 +3144,9 @@ impl Blockstore { ) .map(Cow::into_owned) else { error!( - "Shred {next_shred_id:?} indicated by merkle root meta {next_merkle_root_meta:?} \ - is missing from blockstore. This should only happen in extreme cases where \ - blockstore cleanup has caught up to the root. Skipping the forward chained \ - merkle root consistency check" + "Shred {next_shred_id:?} is missing from blockstore. This should only happen in \ + extreme cases where blockstore cleanup has caught up to the root. Skipping the \ + forward chained merkle root consistency check" ); return true; }; @@ -3093,12 +3163,6 @@ impl Blockstore { next_merkle_root_meta.first_received_shred_type(), ); - if !self.has_duplicate_shreds_in_slot(shred.slot()) { - duplicate_shreds.push(PossibleDuplicateShred::ChainedMerkleRootConflict( - shred.clone(), - next_shred, - )); - } return false; } @@ -3120,12 +3184,10 @@ impl Blockstore { fn check_backwards_chained_merkle_root_consistency( &self, shred: &Shred, + prev_shred_id: ShredId, just_inserted_shreds: &HashMap<(BlockLocation, ShredId), Cow<'_, Shred>>, - erasure_metas: &BTreeMap>, - duplicate_shreds: &mut Vec, ) -> bool { let slot = shred.slot(); - let erasure_set = shred.erasure_set(); let fec_set_index = shred.fec_set_index(); if fec_set_index == 0 { @@ -3136,26 +3198,6 @@ impl Blockstore { return true; } - // If a shred from the previous fec set has already been inserted, check the chaining. - // Since we cannot compute the previous fec set index, we check the in memory map, otherwise - // check the previous key from blockstore to see if it is consecutive with our current set. - let Some((prev_erasure_set, prev_erasure_meta)) = self - .previous_erasure_set(erasure_set, erasure_metas) - .expect("Expect database operations to succeed") - else { - // No shreds from the previous erasure batch have been received, - // so nothing to check. Once the previous erasure batch is received, - // we will verify this chain through the forward check above. - return true; - }; - - let prev_shred_id = ShredId::new( - slot, - prev_erasure_meta - .first_received_coding_shred_index() - .expect("First received coding index must fit in u32"), - ShredType::Code, - ); let Some(prev_shred) = Self::get_shred_from_just_inserted_or_db( self, just_inserted_shreds, @@ -3164,10 +3206,9 @@ impl Blockstore { ) .map(Cow::into_owned) else { warn!( - "Shred {prev_shred_id:?} indicated by the erasure meta {prev_erasure_meta:?} is \ - missing from blockstore. This can happen if you have recently upgraded from a \ - version < v1.18.13, or if blockstore cleanup has caught up to the root. Skipping \ - the backwards chained merkle root consistency check" + "Shred {prev_shred_id:?} is missing from blockstore. This can happen if \ + blockstore cleanup has caught up to the root. Skipping the backwards chained \ + merkle root consistency check" ); return true; }; @@ -3177,19 +3218,12 @@ impl Blockstore { if !self.check_chaining(merkle_root, chained_merkle_root) { warn!( "Received conflicting chained merkle roots for slot: {slot}, shred {:?} type {:?} \ - chains to merkle root {chained_merkle_root:?}, however previous fec set coding \ - shred {prev_erasure_set:?} has merkle root {merkle_root:?}. Reporting as \ - duplicate", + chains to merkle root {chained_merkle_root:?}, however previous fec set shred \ + {prev_shred_id:?} has merkle root {merkle_root:?}. Reporting as duplicate", shred.erasure_set(), shred.shred_type(), ); - if !self.has_duplicate_shreds_in_slot(shred.slot()) { - duplicate_shreds.push(PossibleDuplicateShred::ChainedMerkleRootConflict( - shred.clone(), - prev_shred, - )); - } return false; } diff --git a/ledger/src/blockstore/tests.rs b/ledger/src/blockstore/tests.rs index 9fabc0d40a7..0504f9228e0 100644 --- a/ledger/src/blockstore/tests.rs +++ b/ledger/src/blockstore/tests.rs @@ -6160,10 +6160,7 @@ fn test_chained_merkle_root_inconsistency_backwards_insert_code() { assert_eq!(duplicate_shreds.len(), 1); assert_eq!( duplicate_shreds[0], - PossibleDuplicateShred::ChainedMerkleRootConflict( - coding_shred, - coding_shred_previous.into_payload() - ) + PossibleDuplicateShred::ChainedMerkleRootConflict(coding_shred.slot()) ); // Should not check again, even though this shred conflicts as well @@ -6213,10 +6210,7 @@ fn test_chained_merkle_root_inconsistency_backwards_insert_data() { assert_eq!(duplicate_shreds.len(), 1); assert_eq!( duplicate_shreds[0], - PossibleDuplicateShred::ChainedMerkleRootConflict( - data_shred, - coding_shred_previous.into_payload(), - ) + PossibleDuplicateShred::ChainedMerkleRootConflict(data_shred.slot()) ); // Should not check again, even though this shred conflicts as well assert!( @@ -6263,13 +6257,58 @@ fn test_chained_merkle_root_inconsistency_forwards() { let duplicate_shreds = blockstore.insert_shred_return_duplicate(coding_shred.clone(), &leader_schedule); + assert_eq!(duplicate_shreds.len(), 2); + assert!( + duplicate_shreds.contains(&PossibleDuplicateShred::ChainedMerkleRootConflict( + coding_shred.slot(), + )) + ); + assert!( + duplicate_shreds.contains(&PossibleDuplicateShred::FixedFECChainedMerkleRootConflict( + coding_shred.slot(), + )) + ); +} + +#[test] +fn test_chained_merkle_root_inconsistency_data_shreds_only() { + // Insert data shreds from consecutive FEC sets without any coding shreds. + // The ErasureMeta-based SIMD-0340 check cannot run, so the fixed-FEC + // MerkleRootMeta check must catch the inconsistent chain. + let ledger_path = get_tmp_ledger_path_auto_delete!(); + let blockstore = Blockstore::open(ledger_path.path()).unwrap(); + + let parent_slot = 0; + let slot = 1; + let fec_set_index = 0; + let (data_shreds, _, leader_schedule) = + setup_erasure_shreds_with_index(slot, parent_slot, 10, fec_set_index); + let data_shred_previous = data_shreds[0].clone(); + let next_fec_set_index = fec_set_index + data_shreds.len() as u32; + + assert!( + blockstore + .insert_shred_return_duplicate(data_shred_previous.clone(), &leader_schedule,) + .is_empty() + ); + + let merkle_root = Hash::new_unique(); + assert!(merkle_root != data_shred_previous.merkle_root().unwrap()); + let (data_shreds, _, leader_schedule) = setup_erasure_shreds_with_index_and_chained_merkle( + slot, + parent_slot, + 10, + next_fec_set_index, + merkle_root, + ); + let data_shred = data_shreds[0].clone(); + + let duplicate_shreds = + blockstore.insert_shred_return_duplicate(data_shred.clone(), &leader_schedule); assert_eq!(duplicate_shreds.len(), 1); assert_eq!( duplicate_shreds[0], - PossibleDuplicateShred::ChainedMerkleRootConflict( - coding_shred, - next_data_shred.into_payload(), - ) + PossibleDuplicateShred::FixedFECChainedMerkleRootConflict(data_shred.slot()) ); } @@ -6337,10 +6376,7 @@ fn test_chained_merkle_root_inconsistency_both() { assert_eq!(duplicate_shreds.len(), 1); assert_eq!( duplicate_shreds[0], - PossibleDuplicateShred::ChainedMerkleRootConflict( - data_shred, - prev_coding_shred.into_payload(), - ) + PossibleDuplicateShred::ChainedMerkleRootConflict(data_shred.slot()) ); // Insert coding shred @@ -6351,10 +6387,7 @@ fn test_chained_merkle_root_inconsistency_both() { assert_eq!(duplicate_shreds.len(), 1); assert_eq!( duplicate_shreds[0], - PossibleDuplicateShred::ChainedMerkleRootConflict( - coding_shred, - next_data_shred.into_payload(), - ) + PossibleDuplicateShred::ChainedMerkleRootConflict(coding_shred.slot()) ); } diff --git a/ledger/src/blockstore_processor.rs b/ledger/src/blockstore_processor.rs index 55e87d944d7..f00b6093b60 100644 --- a/ledger/src/blockstore_processor.rs +++ b/ledger/src/blockstore_processor.rs @@ -2713,7 +2713,9 @@ pub fn check_chained_block_id( bank: &Bank, migration_status: &MigrationStatus, ) -> ChainedBlockIdCheck { - if !bank.feature_set.snapshot().validate_chained_block_id { + let feature_snapshot = bank.feature_set.snapshot(); + if !(feature_snapshot.validate_chained_block_id || feature_snapshot.validate_chained_block_id_2) + { return ChainedBlockIdCheck::Inactive; } @@ -6373,6 +6375,27 @@ pub mod tests { ChainedBlockIdCheck::Mismatch )); + // Case 3a: With only the replacement feature active, replay should + // still run the existing inter-slot SIMD-0340 check. + insert_shreds_with_chained_merkle_root(14, 0, Hash::new_unique()); + let mut child_bank = Bank::new_from_parent(parent_bank.clone(), SlotLeader::default(), 14); + child_bank.deactivate_feature(&agave_feature_set::validate_chained_block_id::id()); + child_bank.activate_feature(&agave_feature_set::validate_chained_block_id_2::id()); + assert!(matches!( + check_chained_block_id(&blockstore, &child_bank, &MigrationStatus::default()), + ChainedBlockIdCheck::Mismatch + )); + + // Case 3b: With both feature gates inactive, replay should not run the + // chained block ID check. + let mut child_bank = Bank::new_from_parent(parent_bank.clone(), SlotLeader::default(), 14); + child_bank.deactivate_feature(&agave_feature_set::validate_chained_block_id::id()); + child_bank.deactivate_feature(&agave_feature_set::validate_chained_block_id_2::id()); + assert!(matches!( + check_chained_block_id(&blockstore, &child_bank, &MigrationStatus::default()), + ChainedBlockIdCheck::Inactive + )); + // Case 4: UpdateParent metadata does not bypass Tower validation. insert_shreds_with_chained_merkle_root(16, 0, Hash::new_unique()); let mut meta = blockstore.meta(16).unwrap().unwrap(); diff --git a/ledger/src/shred.rs b/ledger/src/shred.rs index 1b30cf25c67..3f3b0b29f72 100644 --- a/ledger/src/shred.rs +++ b/ledger/src/shred.rs @@ -352,6 +352,18 @@ impl ErasureSetId { self.1 } + pub(crate) fn previous_fec_set(&self) -> Option { + self.1 + .checked_sub(DATA_SHREDS_PER_FEC_BLOCK as u32) + .map(|fec_set_index| Self::new(self.0, fec_set_index)) + } + + pub(crate) fn next_fec_set(&self) -> Option { + self.1 + .checked_add(DATA_SHREDS_PER_FEC_BLOCK as u32) + .map(|fec_set_index| Self::new(self.0, fec_set_index)) + } + // Storage key for ErasureMeta and MerkleRootMeta in blockstore db. // Note: ErasureMeta column uses u64 so this will need to be typecast pub(crate) fn store_key(&self) -> (Slot, /*fec_set_index:*/ u32) { From c1f0a5a069587c4e8fd81ffccfd52ed4518ecba1 Mon Sep 17 00:00:00 2001 From: Greg Cusack Date: Thu, 4 Jun 2026 08:24:51 -0700 Subject: [PATCH 202/576] XDP: add os error logging in xdp socket logic (#10563) * add XdpError error logging in xdp socket logic * change XdpError to Error --- xdp/src/socket.rs | 85 ++++++++++++++++++++++++++++++++++++++++------ xdp/src/tx_loop.rs | 10 +++--- 2 files changed, 78 insertions(+), 17 deletions(-) diff --git a/xdp/src/socket.rs b/xdp/src/socket.rs index e5f99ad3038..a5beb95ae50 100644 --- a/xdp/src/socket.rs +++ b/xdp/src/socket.rs @@ -44,7 +44,11 @@ impl Socket { unsafe { let fd = socket(AF_XDP, SOCK_RAW, 0); if fd < 0 { - return Err(io::Error::last_os_error()); + return Err(Error::syscall( + "socket(AF_XDP, SOCK_RAW) failed", + io::Error::last_os_error(), + ) + .into()); } let fd = OwnedFd::from_raw_fd(fd); @@ -65,7 +69,11 @@ impl Socket { mem::size_of::() as libc::socklen_t, ) < 0 { - return Err(io::Error::last_os_error()); + return Err(Error::syscall( + "setsockopt(XDP_UMEM_REG) failed", + io::Error::last_os_error(), + ) + .into()); } for (ring, size) in [ @@ -87,7 +95,11 @@ impl Socket { mem::size_of::() as socklen_t, ) < 0 { - return Err(io::Error::last_os_error()); + return Err(Error::syscall( + format!("setsockopt(SOL_XDP, ring={ring}, size={size}) failed",), + io::Error::last_os_error(), + ) + .into()); } } @@ -101,7 +113,11 @@ impl Socket { &mut optlen, ) < 0 { - return Err(io::Error::last_os_error()); + return Err(Error::syscall( + "getsockopt(XDP_MMAP_OFFSETS) failed", + io::Error::last_os_error(), + ) + .into()); } let tx_completion_ring = TxCompletionRing::new( @@ -110,7 +126,8 @@ impl Socket { tx_completion_ring_size.saturating_mul(mem::size_of::()), &offsets.cr, XDP_UMEM_PGOFF_COMPLETION_RING, - )?, + ) + .map_err(|source| Error::syscall("mmap completion ring failed", source))?, tx_completion_ring_size as u32, ); @@ -120,7 +137,8 @@ impl Socket { rx_fill_ring_size.saturating_mul(mem::size_of::()), &offsets.fr, XDP_UMEM_PGOFF_FILL_RING, - )?, + ) + .map_err(|source| Error::syscall("mmap fill ring failed", source))?, rx_fill_ring_size as u32, fd.as_raw_fd(), ); @@ -130,9 +148,15 @@ impl Socket { // pre-populated before calling bind() for _ in 0..rx_fill_ring_size { let Some(frame) = umem.reserve() else { - return Err(io::Error::other("Failed to reserve frame for RX fill ring")); + return Err(Error::InsufficientUmemFrames { + required: rx_fill_ring_size, + available: umem.available(), + } + .into()); }; - rx_fill_ring.write(frame)?; + rx_fill_ring + .write(frame) + .map_err(|source| Error::syscall("RX fill ring write failed", source))?; } rx_fill_ring.commit(); } @@ -143,7 +167,8 @@ impl Socket { tx_ring_size.saturating_mul(mem::size_of::()), &offsets.tx, XDP_PGOFF_TX_RING as u64, - )?, + ) + .map_err(|source| Error::syscall("mmap tx ring failed", source))?, tx_ring_size as u32, fd.as_raw_fd(), )); @@ -155,7 +180,8 @@ impl Socket { rx_ring_size.saturating_mul(mem::size_of::()), &offsets.rx, XDP_PGOFF_RX_RING as u64, - )?, + ) + .map_err(|source| Error::syscall("mmap rx ring failed", source))?, rx_ring_size as u32, fd.as_raw_fd(), )) @@ -178,7 +204,14 @@ impl Socket { mem::size_of::() as socklen_t, ) < 0 { - return Err(io::Error::last_os_error()); + return Err(Error::syscall( + format!( + "bind(AF_XDP, ifindex={}, queue={}, flags=0x{:x}) failed", + sxdp.sxdp_ifindex, sxdp.sxdp_queue_id, sxdp.sxdp_flags + ), + io::Error::last_os_error(), + ) + .into()); } let tx = Tx { @@ -371,3 +404,33 @@ impl RxRing { self.consumer.sync(commit); } } + +#[derive(Debug, thiserror::Error)] +enum Error { + #[error("{message}: {source}")] + Syscall { + message: String, + #[source] + source: io::Error, + }, + #[error( + "insufficient UMEM frames for RX fill ring prefill: required={required}, \ + available={available}" + )] + InsufficientUmemFrames { required: usize, available: usize }, +} + +impl Error { + fn syscall(message: impl Into, source: io::Error) -> Self { + Self::Syscall { + message: message.into(), + source, + } + } +} + +impl From for io::Error { + fn from(error: Error) -> io::Error { + io::Error::other(error) + } +} diff --git a/xdp/src/tx_loop.rs b/xdp/src/tx_loop.rs index 2576b1ae613..00630cdd7d6 100644 --- a/xdp/src/tx_loop.rs +++ b/xdp/src/tx_loop.rs @@ -157,16 +157,14 @@ impl TxLoopBuilder> { } = self; let queue_id = queue.id(); - let (socket, tx) = match Socket::tx(queue, umem, zero_copy, tx_size * 2, tx_size) { - Ok(socket_tx) => socket_tx, - Err(err) => { + let (socket, tx) = + Socket::tx(queue, umem, zero_copy, tx_size * 2, tx_size).map_err(|err| { log::error!( "failed to create AF_XDP TX socket for queue {queue_id:?} on CPU {cpu_id}: \ {err}" ); - return Err(err); - } - }; + err + })?; let Tx { // this is where we'll queue frames From 2b077efd68cc1fc0389e01531103a111383fa00f Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Thu, 4 Jun 2026 11:59:13 -0400 Subject: [PATCH 203/576] replay: if leader aborts a block in alpenglow, do not emit Error log (#12933) --- core/src/replay_stage/dead_slots.rs | 6 +++++- entry/src/block_component.rs | 8 ++++++++ ledger/src/blockstore.rs | 10 +++++++--- ledger/src/blockstore/error.rs | 2 ++ 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/core/src/replay_stage/dead_slots.rs b/core/src/replay_stage/dead_slots.rs index f79f7ed39e8..a33f98ec875 100644 --- a/core/src/replay_stage/dead_slots.rs +++ b/core/src/replay_stage/dead_slots.rs @@ -15,7 +15,8 @@ use { agave_votor_messages::migration::MigrationStatus, solana_clock::Slot, solana_ledger::{ - block_error::BlockError, blockstore::Blockstore, + block_error::BlockError, + blockstore::{Blockstore, BlockstoreError}, blockstore_processor::BlockstoreProcessorError, }, solana_rpc::{rpc_subscriptions::RpcSubscriptions, slot_status_notifier::SlotStatusNotifier}, @@ -44,6 +45,9 @@ impl DeadSlotLogLevel { pub(super) fn for_replay_error(err: &BlockstoreProcessorError) -> Self { match err { BlockstoreProcessorError::InvalidBlock(BlockError::TooFewTicks) => Self::Info, + BlockstoreProcessorError::FailedToLoadEntries(BlockstoreError::BlockAborted(_)) => { + Self::Info + } _ => Self::Error, } } diff --git a/entry/src/block_component.rs b/entry/src/block_component.rs index bca55a920c4..a890cda8abb 100644 --- a/entry/src/block_component.rs +++ b/entry/src/block_component.rs @@ -472,6 +472,7 @@ pub enum BlockComponent { impl BlockComponent { const MAX_ENTRIES: usize = u32::MAX as usize; const ENTRY_COUNT_SIZE: usize = 8; + const EMPTY_ENTRY_BATCH: [u8; Self::ENTRY_COUNT_SIZE] = 0u64.to_le_bytes(); pub fn new_entry_batch(entries: Vec) -> Result { if entries.is_empty() { @@ -517,6 +518,13 @@ impl BlockComponent { pub fn infer_is_block_marker(data: &[u8]) -> Option { Self::infer_is_entry_batch(data).map(|is_entry_batch| !is_entry_batch) } + + /// In Alpenglow an empty entry batch will fail to deserialize + /// Leader's should only use an empty entry batch to indicate that they + /// are aborting the block + pub fn infer_is_empty_entry_batch(data: &[u8]) -> bool { + *data == Self::EMPTY_ENTRY_BATCH + } } unsafe impl SchemaWrite for BlockComponent { diff --git a/ledger/src/blockstore.rs b/ledger/src/blockstore.rs index 3da26b4e298..cdb82191f71 100644 --- a/ledger/src/blockstore.rs +++ b/ledger/src/blockstore.rs @@ -5037,9 +5037,13 @@ impl Blockstore { wincode::deserialize(&payload) .map(|component| vec![component]) .map_err(|e| { - BlockstoreError::InvalidShredData(format!( - "could not reconstruct block component: {e}" - )) + if BlockComponent::infer_is_empty_entry_batch(&payload) { + BlockstoreError::BlockAborted(slot) + } else { + BlockstoreError::InvalidShredData(format!( + "could not reconstruct block component: {e}" + )) + } }) }) } diff --git a/ledger/src/blockstore/error.rs b/ledger/src/blockstore/error.rs index 2f4408ff7f8..5b53df023d1 100644 --- a/ledger/src/blockstore/error.rs +++ b/ledger/src/blockstore/error.rs @@ -113,6 +113,8 @@ pub enum BlockstoreError { block_header_parent_slot: Slot, shred_parent_slot: Slot, }, + #[error("Block in slot {0} was aborted as leader sent an empty entry batch")] + BlockAborted(Slot), } pub type Result = std::result::Result; From 7d1db96d840d2def0e6adec1be3cd8c88141711e Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Thu, 4 Jun 2026 12:08:49 -0400 Subject: [PATCH 204/576] Revert "fix(votor): only add vote to vote_history if voting (#12713)" (#12953) This reverts commit 0aeb9c2e51e452f53409a7367c0cda9307b5d835. --- votor/src/voting_utils.rs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/votor/src/voting_utils.rs b/votor/src/voting_utils.rs index 660fcbbbc37..4bbdd85532c 100644 --- a/votor/src/voting_utils.rs +++ b/votor/src/voting_utils.rs @@ -238,6 +238,11 @@ fn insert_vote_and_create_bls_message( is_refresh: bool, context: &mut VotingContext, ) -> Result { + // Update and save the vote history + if !is_refresh { + context.vote_history.add_vote(vote); + } + let bank = context.sharable_banks.root(); let message = match generate_vote_tx( vote, @@ -257,15 +262,6 @@ fn insert_vote_and_create_bls_message( } } }; - - // Only record after generate_vote_tx has produced a signed message. - // Recording before would leave vote_history claiming we voted for the - // slot when HotSpare/NonVoting/NoRankFound short-circuited the broadcast, - // permanently blocking us from voting for that slot. - if !is_refresh { - context.vote_history.add_vote(vote); - } - context .own_vote_sender .send(vec![message.clone()]) From ff8c6da13cf7ec98a1b0b393f77433d77d1cf36f Mon Sep 17 00:00:00 2001 From: Tao Zhu <82401714+tao-stones@users.noreply.github.com> Date: Thu, 4 Jun 2026 13:24:39 -0500 Subject: [PATCH 205/576] refactor - remove simple vote static cost after feature activated (#12902) * removes SimpleVote and vote-limit code path; simplify TRansactionCost API * remove MAX_VOTE_UNITS from block_cost_limits * remove remove MAX_VOTE_UNITS from block_cost_limits from feature snapshot --- core/src/banking_simulation.rs | 14 +- core/src/banking_stage/consumer.rs | 9 +- core/src/banking_stage/qos_service.rs | 20 ++- cost-model/benches/cost_tracker.rs | 2 +- cost-model/src/block_cost_limits.rs | 4 - cost-model/src/cost_model.rs | 77 +++++------ cost-model/src/cost_tracker.rs | 174 +++++++----------------- cost-model/src/transaction_cost.rs | 184 +++++++------------------- feature-set/src/lib.rs | 2 - ledger/src/blockstore_processor.rs | 7 +- runtime/src/bank.rs | 2 - 11 files changed, 150 insertions(+), 345 deletions(-) diff --git a/core/src/banking_simulation.rs b/core/src/banking_simulation.rs index b0c337ab569..5be8cb35ffb 100644 --- a/core/src/banking_simulation.rs +++ b/core/src/banking_simulation.rs @@ -264,18 +264,16 @@ struct SimulatorLoopLogger { } impl SimulatorLoopLogger { - fn bank_costs(bank: &Bank) -> (u64, u64) { - bank.read_cost_tracker() - .map(|t| (t.block_cost(), t.vote_cost())) - .unwrap() + fn bank_cost(bank: &Bank) -> u64 { + bank.read_cost_tracker().map(|t| t.block_cost()).unwrap() } fn log_frozen_bank_cost(&self, bank: &Bank, bank_elapsed: Duration) { info!( - "simulated bank slot+delta: {}+{}ms costs: {:?} fees: {:?} txs: {} (frozen)", + "simulated bank slot+delta: {}+{}ms cost: {} fees: {:?} txs: {} (frozen)", bank.slot(), bank_elapsed.as_millis(), - Self::bank_costs(bank), + Self::bank_cost(bank), bank.get_collector_fee_details(), bank.executed_transaction_count(), ); @@ -283,10 +281,10 @@ impl SimulatorLoopLogger { fn log_ongoing_bank_cost(&self, bank: &Bank, bank_elapsed: Duration) { info!( - "simulated bank slot+delta: {}+{}ms costs: {:?} fees: {:?} txs: {} (ongoing)", + "simulated bank slot+delta: {}+{}ms cost: {} fees: {:?} txs: {} (ongoing)", bank.slot(), bank_elapsed.as_millis(), - Self::bank_costs(bank), + Self::bank_cost(bank), bank.get_collector_fee_details(), bank.executed_transaction_count(), ); diff --git a/core/src/banking_stage/consumer.rs b/core/src/banking_stage/consumer.rs index fdb814f9734..897052f5876 100644 --- a/core/src/banking_stage/consumer.rs +++ b/core/src/banking_stage/consumer.rs @@ -514,7 +514,7 @@ mod tests { self as address_lookup_table, state::{AddressLookupTable, LookupTableMeta}, }, - solana_cost_model::{cost_model::CostModel, transaction_cost::TransactionCost}, + solana_cost_model::cost_model::CostModel, solana_fee_calculator::FeeCalculator, solana_hash::Hash, solana_instruction::error::InstructionError, @@ -969,10 +969,9 @@ mod tests { }; let mut cost = CostModel::calculate_cost(&transactions[0], &bank.feature_set); - if let TransactionCost::Transaction(ref mut usage_cost) = cost { - usage_cost.programs_execution_cost = actual_programs_execution_cost; - usage_cost.loaded_accounts_data_size_cost = actual_loaded_accounts_data_size_cost; - } + let usage_cost = cost.usage_cost_details_mut(); + usage_cost.programs_execution_cost = actual_programs_execution_cost; + usage_cost.loaded_accounts_data_size_cost = actual_loaded_accounts_data_size_cost; block_cost + cost.sum() }; diff --git a/core/src/banking_stage/qos_service.rs b/core/src/banking_stage/qos_service.rs index e4920e7baa7..69781da313c 100644 --- a/core/src/banking_stage/qos_service.rs +++ b/core/src/banking_stage/qos_service.rs @@ -64,14 +64,12 @@ impl QosService { pre_result.map(|()| { let mut reserving_cost = CostModel::calculate_cost(tx, feature_set); - if let TransactionCost::Transaction(ref mut usage_cost_details) = reserving_cost - { - // To maintain cost tracking consistency, reserve at least one page for - // loading the fee payer account in fee-only fallback scenarios. - usage_cost_details.loaded_accounts_data_size_cost = usage_cost_details - .loaded_accounts_data_size_cost - .max(CostModel::calculate_pages_cost(1)); - } + let usage_cost_details = reserving_cost.usage_cost_details_mut(); + // To maintain cost tracking consistency, reserve at least one page for + // loading the fee payer account in fee-only fallback scenarios. + usage_cost_details.loaded_accounts_data_size_cost = usage_cost_details + .loaded_accounts_data_size_cost + .max(CostModel::calculate_pages_cost(1)); reserving_cost }) @@ -302,7 +300,7 @@ mod tests { let cost_limit = transfer_tx_cost + vote_tx_cost; bank.write_cost_tracker() .unwrap() - .set_limits(cost_limit, cost_limit, cost_limit, 0); + .set_limits(cost_limit, cost_limit, 0); let (results, num_selected) = QosService::select_transactions_per_cost(txs.iter(), txs_costs.into_iter(), &bank); assert_eq!(num_selected, 2); @@ -561,9 +559,7 @@ mod tests { .expect("one tx cost") .expect("tx cost should be computed"); - let TransactionCost::Transaction(usage_cost_details) = tx_cost else { - panic!("expected TransactionCost::Transaction"); - }; + let usage_cost_details = tx_cost.usage_cost_details(); assert_eq!( usage_cost_details.loaded_accounts_data_size_cost, diff --git a/cost-model/benches/cost_tracker.rs b/cost-model/benches/cost_tracker.rs index 9e3893c338e..d1bd53132b2 100644 --- a/cost-model/benches/cost_tracker.rs +++ b/cost-model/benches/cost_tracker.rs @@ -49,7 +49,7 @@ fn get_costs( transactions .iter() .map(|transaction| { - TransactionCost::Transaction(UsageCostDetails { + TransactionCost::new(UsageCostDetails { transaction, signature_cost: 0, write_lock_cost: 0, diff --git a/cost-model/src/block_cost_limits.rs b/cost-model/src/block_cost_limits.rs index fd8ffd26784..caa18b6594f 100644 --- a/cost-model/src/block_cost_limits.rs +++ b/cost-model/src/block_cost_limits.rs @@ -32,10 +32,6 @@ pub const MAX_BLOCK_UNITS_SIMD_0286: u64 = 100_000_000; /// reduce block's parallelism. pub const MAX_WRITABLE_ACCOUNT_UNITS: u64 = 24_000_000; -/// Number of compute units that a block can have for vote transactions, -/// set to less than MAX_BLOCK_UNITS to leave room for non-vote transactions -pub const MAX_VOTE_UNITS: u64 = 36_000_000; - /// The maximum allowed size, in bytes, that accounts data can grow, per block. /// This can also be thought of as the maximum size of new allocations per block. pub const MAX_BLOCK_ACCOUNTS_DATA_SIZE_DELTA: u64 = 100_000_000; diff --git a/cost-model/src/cost_model.rs b/cost-model/src/cost_model.rs index 51de53ebc5e..28e72ad4c56 100644 --- a/cost-model/src/cost_model.rs +++ b/cost-model/src/cost_model.rs @@ -37,24 +37,18 @@ impl CostModel { transaction: &'a Tx, feature_set: &FeatureSet, ) -> TransactionCost<'a, Tx> { - let remove_simple_vote_from_cost_model = - feature_set.snapshot().remove_simple_vote_from_cost_model; - if transaction.is_simple_vote_transaction() && !remove_simple_vote_from_cost_model { - TransactionCost::SimpleVote { transaction } - } else { - let (programs_execution_cost, loaded_accounts_data_size_cost) = - Self::get_estimated_execution_cost(transaction, feature_set); - let data_bytes_cost = Self::get_instructions_data_cost(transaction); - Self::calculate_non_vote_transaction_cost( - transaction, - transaction.program_instructions_iter(), - transaction.num_write_locks(), - programs_execution_cost, - loaded_accounts_data_size_cost, - data_bytes_cost, - feature_set, - ) - } + let (programs_execution_cost, loaded_accounts_data_size_cost) = + Self::get_estimated_execution_cost(transaction, feature_set); + let data_bytes_cost = Self::get_instructions_data_cost(transaction); + Self::calculate_transaction_cost( + transaction, + transaction.program_instructions_iter(), + transaction.num_write_locks(), + programs_execution_cost, + loaded_accounts_data_size_cost, + data_bytes_cost, + feature_set, + ) } // Calculate executed transaction CU cost, with actual execution and loaded accounts size @@ -65,27 +59,21 @@ impl CostModel { actual_loaded_accounts_data_size_bytes: u32, feature_set: &FeatureSet, ) -> TransactionCost<'a, Tx> { - let remove_simple_vote_from_cost_model = - feature_set.snapshot().remove_simple_vote_from_cost_model; - if transaction.is_simple_vote_transaction() && !remove_simple_vote_from_cost_model { - TransactionCost::SimpleVote { transaction } - } else { - let loaded_accounts_data_size_cost = Self::calculate_loaded_accounts_data_size_cost( - actual_loaded_accounts_data_size_bytes, - feature_set, - ); - let instructions_data_cost = Self::get_instructions_data_cost(transaction); - - Self::calculate_non_vote_transaction_cost( - transaction, - transaction.program_instructions_iter(), - transaction.num_write_locks(), - actual_programs_execution_cost, - loaded_accounts_data_size_cost, - instructions_data_cost, - feature_set, - ) - } + let loaded_accounts_data_size_cost = Self::calculate_loaded_accounts_data_size_cost( + actual_loaded_accounts_data_size_bytes, + feature_set, + ); + let instructions_data_cost = Self::get_instructions_data_cost(transaction); + + Self::calculate_transaction_cost( + transaction, + transaction.program_instructions_iter(), + transaction.num_write_locks(), + actual_programs_execution_cost, + loaded_accounts_data_size_cost, + instructions_data_cost, + feature_set, + ) } /// Return an estimated total cost for a transaction given its': @@ -98,15 +86,10 @@ impl CostModel { num_write_locks: u64, feature_set: &FeatureSet, ) -> TransactionCost<'a, Tx> { - let remove_simple_vote_from_cost_model = - feature_set.snapshot().remove_simple_vote_from_cost_model; - if transaction.is_simple_vote_transaction() && !remove_simple_vote_from_cost_model { - return TransactionCost::SimpleVote { transaction }; - } let (programs_execution_cost, loaded_accounts_data_size_cost) = Self::get_estimated_execution_cost(transaction, feature_set); let data_bytes_cost = Self::get_instructions_data_cost(transaction); - Self::calculate_non_vote_transaction_cost( + Self::calculate_transaction_cost( transaction, instructions, num_write_locks, @@ -117,7 +100,7 @@ impl CostModel { ) } - fn calculate_non_vote_transaction_cost<'a, Tx: TransactionMeta>( + fn calculate_transaction_cost<'a, Tx: TransactionMeta>( transaction: &'a Tx, instructions: impl Iterator)>, num_write_locks: u64, @@ -142,7 +125,7 @@ impl CostModel { allocated_accounts_data_size, }; - TransactionCost::Transaction(usage_cost_details) + TransactionCost::new(usage_cost_details) } /// Returns signature details and the total signature cost diff --git a/cost-model/src/cost_tracker.rs b/cost-model/src/cost_tracker.rs index 23bd69816a6..708cf6e849a 100644 --- a/cost-model/src/cost_tracker.rs +++ b/cost-model/src/cost_tracker.rs @@ -29,9 +29,6 @@ pub enum CostTrackerError { /// would exceed block max limit WouldExceedBlockMaxLimit, - /// would exceed vote max limit - WouldExceedVoteMaxLimit, - /// would exceed account max limit WouldExceedAccountMaxLimit, @@ -46,7 +43,6 @@ impl From for TransactionError { fn from(err: CostTrackerError) -> Self { match err { CostTrackerError::WouldExceedBlockMaxLimit => Self::WouldExceedMaxBlockCostLimit, - CostTrackerError::WouldExceedVoteMaxLimit => Self::WouldExceedMaxVoteCostLimit, CostTrackerError::WouldExceedAccountMaxLimit => Self::WouldExceedMaxAccountCostLimit, CostTrackerError::WouldExceedAccountDataBlockLimit => { Self::WouldExceedAccountDataBlockLimit @@ -72,12 +68,10 @@ pub struct UpdatedCosts { pub struct CostTracker { account_cost_limit: u64, block_cost_limit: u64, - vote_cost_limit: u64, // Maximum new account allocation data per block in bytes. allocated_data_size_limit: u64, cost_by_writable_accounts: HashMap, block_cost: SharedBlockCost, - vote_cost: u64, transaction_count: Saturating, allocated_accounts_data_size: Saturating, transaction_signature_count: Saturating, @@ -93,19 +87,16 @@ pub struct CostTracker { impl Default for CostTracker { fn default() -> Self { const _: () = assert!(MAX_WRITABLE_ACCOUNT_UNITS <= MAX_BLOCK_UNITS); - const _: () = assert!(MAX_VOTE_UNITS <= MAX_BLOCK_UNITS); Self { account_cost_limit: MAX_WRITABLE_ACCOUNT_UNITS, block_cost_limit: MAX_BLOCK_UNITS, - vote_cost_limit: MAX_VOTE_UNITS, allocated_data_size_limit: MAX_BLOCK_ACCOUNTS_DATA_SIZE_DELTA, cost_by_writable_accounts: HashMap::with_capacity_and_hasher( WRITABLE_ACCOUNTS_PER_BLOCK, ahash::RandomState::new(), ), block_cost: SharedBlockCost::new(0), - vote_cost: 0, transaction_count: Saturating(0), allocated_accounts_data_size: Saturating(0), transaction_signature_count: Saturating(0), @@ -123,7 +114,6 @@ impl CostTracker { new.set_limits( self.account_cost_limit, self.block_cost_limit, - self.vote_cost_limit, self.allocated_data_size_limit, ); new @@ -139,11 +129,6 @@ impl CostTracker { self.block_cost_limit } - /// Get the overall vote limit. - pub fn get_vote_limit(&self) -> u64 { - self.vote_cost_limit - } - /// Get the overall allocated account data size limit. pub fn get_allocated_data_size_limit(&self) -> u64 { self.allocated_data_size_limit @@ -154,17 +139,15 @@ impl CostTracker { &mut self, account_cost_limit: u64, block_cost_limit: u64, - vote_cost_limit: u64, allocated_data_size_limit: u64, ) { self.account_cost_limit = account_cost_limit; self.block_cost_limit = block_cost_limit; - self.vote_cost_limit = vote_cost_limit; self.allocated_data_size_limit = allocated_data_size_limit; } pub fn set_limits_max(&mut self) { - self.set_limits(u64::MAX, u64::MAX, u64::MAX, u64::MAX); + self.set_limits(u64::MAX, u64::MAX, u64::MAX); } pub fn in_flight_transaction_count(&self) -> usize { @@ -231,10 +214,6 @@ impl CostTracker { self.block_cost.clone() } - pub fn vote_cost(&self) -> u64 { - self.vote_cost - } - pub fn transaction_count(&self) -> u64 { self.transaction_count.0 } @@ -259,7 +238,6 @@ impl CostTracker { "is_leader" => is_leader.to_string(), ("bank_slot", bank_slot, i64), ("block_cost", self.block_cost(), i64), - ("vote_cost", self.vote_cost, i64), ("transaction_count", self.transaction_count.0, i64), ("number_of_accounts", self.number_of_accounts(), i64), ("costliest_account", costliest_account.to_string(), String), @@ -327,13 +305,6 @@ impl CostTracker { ) -> Result<(), CostTrackerError> { let cost: u64 = tx_cost.sum(); - if tx_cost.should_track_as_simple_vote() { - // if vote transaction, check if it exceeds vote_transaction_limit - if self.vote_cost.saturating_add(cost) > self.vote_cost_limit { - return Err(CostTrackerError::WouldExceedVoteMaxLimit); - } - } - if self.block_cost().saturating_add(cost) > self.block_cost_limit { // check against the total package cost return Err(CostTrackerError::WouldExceedBlockMaxLimit); @@ -411,9 +382,6 @@ impl CostTracker { costliest_account_cost = costliest_account_cost.max(*account_cost); } self.block_cost.fetch_add(adjustment); - if tx_cost.should_track_as_simple_vote() { - self.vote_cost = self.vote_cost.saturating_add(adjustment); - } costliest_account_cost } @@ -432,9 +400,6 @@ impl CostTracker { *account_cost = account_cost.saturating_sub(adjustment); } self.block_cost.fetch_sub(adjustment); - if tx_cost.should_track_as_simple_vote() { - self.vote_cost = self.vote_cost.saturating_sub(adjustment); - } } /// count number of none-zero CU accounts @@ -486,17 +451,14 @@ mod tests { solana_keypair::Keypair, solana_signer::Signer, std::cmp, - test_case::test_case, }; impl CostTracker { - fn new(account_cost_limit: u64, block_cost_limit: u64, vote_cost_limit: u64) -> Self { + fn new(account_cost_limit: u64, block_cost_limit: u64) -> Self { assert!(account_cost_limit <= block_cost_limit); - assert!(vote_cost_limit <= block_cost_limit); Self { account_cost_limit, block_cost_limit, - vote_cost_limit, ..Self::default() } } @@ -537,7 +499,7 @@ mod tests { transaction: &WritableKeysTransaction, programs_execution_cost: u64, ) -> TransactionCost<'_, WritableKeysTransaction> { - TransactionCost::Transaction(simple_usage_cost_details( + TransactionCost::new(simple_usage_cost_details( transaction, programs_execution_cost, )) @@ -545,29 +507,23 @@ mod tests { fn simple_vote_transaction_cost( transaction: &WritableKeysTransaction, - remove_simple_vote_from_cost_model: bool, ) -> TransactionCost<'_, WritableKeysTransaction> { - if !remove_simple_vote_from_cost_model { - TransactionCost::SimpleVote { transaction } - } else { - TransactionCost::Transaction(UsageCostDetails { - transaction, - signature_cost: 1, - write_lock_cost: 2, - data_bytes_cost: 0, - programs_execution_cost: solana_vote_program::vote_processor::DEFAULT_COMPUTE_UNITS, - loaded_accounts_data_size_cost: 8, - allocated_accounts_data_size: 0, - }) - } + TransactionCost::new(UsageCostDetails { + transaction, + signature_cost: 1, + write_lock_cost: 2, + data_bytes_cost: 0, + programs_execution_cost: solana_vote_program::vote_processor::DEFAULT_COMPUTE_UNITS, + loaded_accounts_data_size_cost: 8, + allocated_accounts_data_size: 0, + }) } #[test] fn test_cost_tracker_initialization() { - let testee = CostTracker::new(10, 11, 8); + let testee = CostTracker::new(10, 11); assert_eq!(10, testee.account_cost_limit); assert_eq!(11, testee.block_cost_limit); - assert_eq!(8, testee.vote_cost_limit); assert_eq!(0, testee.cost_by_writable_accounts.len()); assert_eq!(0, testee.block_cost()); } @@ -580,34 +536,26 @@ mod tests { let cost = tx_cost.sum(); // build testee to have capacity for one simple transaction - let mut testee = CostTracker::new(cost, cost, cost); + let mut testee = CostTracker::new(cost, cost); assert!(testee.would_fit(&tx_cost).is_ok()); testee.add_transaction_cost(&tx_cost); assert_eq!(cost, testee.block_cost()); - assert_eq!(0, testee.vote_cost); let (_costliest_account, costliest_account_cost) = testee.find_costliest_account(); assert_eq!(cost, costliest_account_cost); } - #[test_case(false; "remove_simple_vote_from_cost_model not activated")] - #[test_case(true; "remove_simple_vote_from_cost_model activated")] - fn test_cost_tracker_ok_add_one_vote(remove_simple_vote_from_cost_model: bool) { + #[test] + fn test_cost_tracker_ok_add_one_vote() { let mint_keypair = test_setup(); let tx = build_simple_vote_transaction(&mint_keypair); - let tx_cost = simple_vote_transaction_cost(&tx, remove_simple_vote_from_cost_model); + let tx_cost = simple_vote_transaction_cost(&tx); let cost = tx_cost.sum(); // build testee to have capacity for one simple transaction - let mut testee = CostTracker::new(cost, cost, cost); + let mut testee = CostTracker::new(cost, cost); assert!(testee.would_fit(&tx_cost).is_ok()); testee.add_transaction_cost(&tx_cost); assert_eq!(cost, testee.block_cost()); - if !remove_simple_vote_from_cost_model { - assert_eq!(cost, testee.vote_cost); - } else { - // not trackijng vote cu after feature activation - assert_eq!(0, testee.vote_cost); - } let (_costliest_account, costliest_account_cost) = testee.find_costliest_account(); assert_eq!(cost, costliest_account_cost); } @@ -617,15 +565,13 @@ mod tests { let mint_keypair = test_setup(); let tx = build_simple_transaction(&mint_keypair); let mut tx_cost = simple_transaction_cost(&tx, 5); - if let TransactionCost::Transaction(ref mut usage_cost) = tx_cost { - usage_cost.allocated_accounts_data_size = 1; - } else { - unreachable!(); - } + tx_cost + .usage_cost_details_mut() + .allocated_accounts_data_size = 1; let cost = tx_cost.sum(); // build testee to have capacity for one simple transaction - let mut testee = CostTracker::new(cost, cost, cost); + let mut testee = CostTracker::new(cost, cost); assert!(testee.would_fit(&tx_cost).is_ok()); let old = testee.allocated_accounts_data_size; testee.add_transaction_cost(&tx_cost); @@ -644,7 +590,7 @@ mod tests { let cost2 = tx_cost2.sum(); // build testee to have capacity for two simple transactions, with same accounts - let mut testee = CostTracker::new(cost1 + cost2, cost1 + cost2, cost1 + cost2); + let mut testee = CostTracker::new(cost1 + cost2, cost1 + cost2); { assert!(testee.would_fit(&tx_cost1).is_ok()); testee.add_transaction_cost(&tx_cost1); @@ -673,7 +619,7 @@ mod tests { let cost2 = tx_cost2.sum(); // build testee to have capacity for two simple transactions, with same accounts - let mut testee = CostTracker::new(cmp::max(cost1, cost2), cost1 + cost2, cost1 + cost2); + let mut testee = CostTracker::new(cmp::max(cost1, cost2), cost1 + cost2); { assert!(testee.would_fit(&tx_cost1).is_ok()); testee.add_transaction_cost(&tx_cost1); @@ -700,7 +646,7 @@ mod tests { let cost2 = tx_cost2.sum(); // build testee to have capacity for two simple transactions, but not for same accounts - let mut testee = CostTracker::new(cmp::min(cost1, cost2), cost1 + cost2, cost1 + cost2); + let mut testee = CostTracker::new(cmp::min(cost1, cost2), cost1 + cost2); // should have room for first transaction { assert!(testee.would_fit(&tx_cost1).is_ok()); @@ -725,8 +671,7 @@ mod tests { let cost2 = tx_cost2.sum(); // build testee to have capacity for each chain, but not enough room for both transactions - let mut testee = - CostTracker::new(cmp::max(cost1, cost2), cost1 + cost2 - 1, cost1 + cost2 - 1); + let mut testee = CostTracker::new(cmp::max(cost1, cost2), cost1 + cost2 - 1); // should have room for first transaction { assert!(testee.would_fit(&tx_cost1).is_ok()); @@ -738,40 +683,26 @@ mod tests { } } - #[test_case(false; "remove_simple_vote_from_cost_model not activated")] - #[test_case(true; "remove_simple_vote_from_cost_model activated")] - fn test_cost_tracker_reach_vote_limit(remove_simple_vote_from_cost_model: bool) { + #[test] + fn test_cost_tracker_vote_transactions_use_regular_limits() { let mint_keypair = test_setup(); // build two mocking vote transactions with diff accounts let second_account = Keypair::new(); let tx1 = build_simple_vote_transaction(&mint_keypair); - let tx_cost1 = simple_vote_transaction_cost(&tx1, remove_simple_vote_from_cost_model); + let tx_cost1 = simple_vote_transaction_cost(&tx1); let cost1 = tx_cost1.sum(); let tx2 = build_simple_vote_transaction(&second_account); - let tx_cost2 = simple_vote_transaction_cost(&tx2, remove_simple_vote_from_cost_model); + let tx_cost2 = simple_vote_transaction_cost(&tx2); let cost2 = tx_cost2.sum(); - // build testee to have capacity for each chain, but not enough room for both votes - let mut testee = CostTracker::new(cmp::max(cost1, cost2), cost1 + cost2, cost1 + cost2 - 1); + // build testee to have capacity for both vote transactions + let mut testee = CostTracker::new(cmp::max(cost1, cost2), cost1 + cost2); // should have room for first vote { assert!(testee.would_fit(&tx_cost1).is_ok()); testee.add_transaction_cost(&tx_cost1); } - if !remove_simple_vote_from_cost_model { - // but no more room for package as whole - assert!(testee.would_fit(&tx_cost2).is_err()); - } else { - // no more vote cu limit if feature activated - assert!(testee.would_fit(&tx_cost2).is_ok()); - } - // however there is room for none-vote tx3 - { - let third_account = Keypair::new(); - let tx3 = build_simple_transaction(&third_account); - let tx_cost3 = simple_transaction_cost(&tx3, 5); - assert!(testee.would_fit(&tx_cost3).is_ok()); - } + assert!(testee.would_fit(&tx_cost2).is_ok()); } #[test] @@ -783,21 +714,17 @@ mod tests { let mut tx_cost1 = simple_transaction_cost(&tx1, 5); let tx2 = build_simple_transaction(&second_account); let mut tx_cost2 = simple_transaction_cost(&tx2, 5); - if let TransactionCost::Transaction(ref mut usage_cost) = tx_cost1 { - usage_cost.allocated_accounts_data_size = MAX_BLOCK_ACCOUNTS_DATA_SIZE_DELTA; - } else { - unreachable!(); - } - if let TransactionCost::Transaction(ref mut usage_cost) = tx_cost2 { - usage_cost.allocated_accounts_data_size = MAX_BLOCK_ACCOUNTS_DATA_SIZE_DELTA + 1; - } else { - unreachable!(); - } + tx_cost1 + .usage_cost_details_mut() + .allocated_accounts_data_size = MAX_BLOCK_ACCOUNTS_DATA_SIZE_DELTA; + tx_cost2 + .usage_cost_details_mut() + .allocated_accounts_data_size = MAX_BLOCK_ACCOUNTS_DATA_SIZE_DELTA + 1; let cost1 = tx_cost1.sum(); let cost2 = tx_cost2.sum(); // build testee that passes - let testee = CostTracker::new(cmp::max(cost1, cost2), cost1 + cost2 - 1, cost1 + cost2 - 1); + let testee = CostTracker::new(cmp::max(cost1, cost2), cost1 + cost2 - 1); assert!(testee.would_fit(&tx_cost1).is_ok()); // data is too big assert_eq!( @@ -812,14 +739,12 @@ mod tests { let mint_keypair = test_setup(); let tx = build_simple_transaction(&mint_keypair); let mut tx_cost = simple_transaction_cost(&tx, 5); - if let TransactionCost::Transaction(ref mut usage_cost) = tx_cost { - usage_cost.allocated_accounts_data_size = 2; - } else { - unreachable!(); - } + tx_cost + .usage_cost_details_mut() + .allocated_accounts_data_size = 2; // Transaction fits with default limit. - let mut testee = CostTracker::new(u64::MAX, u64::MAX, u64::MAX); + let mut testee = CostTracker::new(u64::MAX, u64::MAX); assert_eq!(testee.would_fit(&tx_cost), Ok(()),); // Transaction does not fit with 1B limit. @@ -843,7 +768,7 @@ mod tests { let cost2 = tx_cost2.sum(); // build testee - let mut testee = CostTracker::new(cost1 + cost2, cost1 + cost2, cost1 + cost2); + let mut testee = CostTracker::new(cost1 + cost2, cost1 + cost2); assert!(testee.try_add(&tx_cost1).is_ok()); assert!(testee.try_add(&tx_cost2).is_ok()); @@ -870,7 +795,7 @@ mod tests { let account_max = cost * 2; let block_max = account_max * 3; // for three accts - let mut testee = CostTracker::new(account_max, block_max, block_max); + let mut testee = CostTracker::new(account_max, block_max); // case 1: a tx writes to 3 accounts, should success, we will have: // | acct1 | $cost | @@ -930,7 +855,7 @@ mod tests { let account_max = cost * 2; let block_max = account_max * 3; // for three accts - let mut testee = CostTracker::new(account_max, block_max, block_max); + let mut testee = CostTracker::new(account_max, block_max); let transaction = WritableKeysTransaction::new(vec![acct1, acct2, acct3]); let tx_cost = simple_transaction_cost(&transaction, cost); let mut expected_block_cost = tx_cost.sum(); @@ -990,7 +915,7 @@ mod tests { let mut usage_cost = simple_usage_cost_details(&transaction, estimated_programs_execution_cost); usage_cost.loaded_accounts_data_size_cost = estimated_loaded_accounts_data_size_cost; - let tx_cost = TransactionCost::Transaction(usage_cost); + let tx_cost = TransactionCost::new(usage_cost); // confirm tx_cost is only made up by programs_execution_cost and // loaded_accounts_data_size_cost let estimated_tx_cost = tx_cost.sum(); @@ -1021,7 +946,6 @@ mod tests { ); assert_eq!(expected_cost, cost_tracker.block_cost()); - assert_eq!(0, cost_tracker.vote_cost); assert_eq!( number_writeble_accounts, cost_tracker.cost_by_writable_accounts.len() @@ -1055,7 +979,6 @@ mod tests { assert_eq!(1, cost_tracker.transaction_count.0); assert_eq!(1, cost_tracker.number_of_accounts()); assert_eq!(cost, cost_tracker.block_cost()); - assert_eq!(0, cost_tracker.vote_cost); assert_eq!(0, cost_tracker.allocated_accounts_data_size.0); cost_tracker.remove_transaction_cost(&tx_cost); @@ -1063,7 +986,6 @@ mod tests { assert_eq!(0, cost_tracker.transaction_count.0); assert_eq!(0, cost_tracker.number_of_accounts()); assert_eq!(0, cost_tracker.block_cost()); - assert_eq!(0, cost_tracker.vote_cost); assert_eq!(0, cost_tracker.allocated_accounts_data_size.0); } diff --git a/cost-model/src/transaction_cost.rs b/cost-model/src/transaction_cost.rs index 2b97dde84d4..2faede7e14f 100644 --- a/cost-model/src/transaction_cost.rs +++ b/cost-model/src/transaction_cost.rs @@ -1,105 +1,58 @@ use { - crate::block_cost_limits, solana_pubkey::Pubkey, - solana_runtime_transaction::transaction_meta::TransactionMeta, + solana_pubkey::Pubkey, solana_runtime_transaction::transaction_meta::TransactionMeta, solana_svm_transaction::svm_message::SVMMessage, }; -/// `TransactionCost`` is used to represent resources required to process a -/// transaction, denominated in Compute Units (CUs). Resources required to -/// process a regular transaction often include an array of variables, such as -/// execution cost, loaded bytes, write lock and read lock etc. -/// -/// SimpleVote has a simpler and pre-determined format. It has: -/// - 1 or 2 signatures -/// - 2 write locks -/// - 1 vote instruction -/// - less than 32k (page size) accounts to load -/// -/// Its cost therefore can be static #33269. -const SIMPLE_VOTE_USAGE_COST: u64 = 3428; - #[derive(Debug)] -pub enum TransactionCost<'a, Tx> { - SimpleVote { transaction: &'a Tx }, - Transaction(UsageCostDetails<'a, Tx>), +pub struct TransactionCost<'a, Tx> { + usage_cost: UsageCostDetails<'a, Tx>, } -impl TransactionCost<'_, Tx> { - pub fn sum(&self) -> u64 { - #![allow(clippy::assertions_on_constants)] - match self { - Self::SimpleVote { .. } => { - const _: () = assert!( - SIMPLE_VOTE_USAGE_COST - == solana_vote_program::vote_processor::DEFAULT_COMPUTE_UNITS - + block_cost_limits::SIGNATURE_COST - + 2 * block_cost_limits::WRITE_LOCK_UNITS - + 8 - ); +impl<'a, Tx> TransactionCost<'a, Tx> { + pub fn new(usage_cost: UsageCostDetails<'a, Tx>) -> Self { + Self { usage_cost } + } - SIMPLE_VOTE_USAGE_COST - } - Self::Transaction(usage_cost) => usage_cost.sum(), - } + pub fn usage_cost_details(&self) -> &UsageCostDetails<'a, Tx> { + &self.usage_cost } - pub fn programs_execution_cost(&self) -> u64 { - match self { - Self::SimpleVote { .. } => solana_vote_program::vote_processor::DEFAULT_COMPUTE_UNITS, - Self::Transaction(usage_cost) => usage_cost.programs_execution_cost, - } + pub fn usage_cost_details_mut(&mut self) -> &mut UsageCostDetails<'a, Tx> { + &mut self.usage_cost } - pub fn should_track_as_simple_vote(&self) -> bool { - match self { - Self::SimpleVote { .. } => true, - Self::Transaction(_) => false, - } + pub fn sum(&self) -> u64 { + self.usage_cost.sum() + } + + pub fn programs_execution_cost(&self) -> u64 { + self.usage_cost.programs_execution_cost } pub fn data_bytes_cost(&self) -> u16 { - match self { - Self::SimpleVote { .. } => 0, - Self::Transaction(usage_cost) => usage_cost.data_bytes_cost, - } + self.usage_cost.data_bytes_cost } pub fn allocated_accounts_data_size(&self) -> u64 { - match self { - Self::SimpleVote { .. } => 0, - Self::Transaction(usage_cost) => usage_cost.allocated_accounts_data_size, - } + self.usage_cost.allocated_accounts_data_size } pub fn loaded_accounts_data_size_cost(&self) -> u64 { - match self { - Self::SimpleVote { .. } => 8, // simple-vote loads less than 32K account data, - // the cost round up to be one page (32K) cost: 8CU - Self::Transaction(usage_cost) => usage_cost.loaded_accounts_data_size_cost, - } + self.usage_cost.loaded_accounts_data_size_cost } pub fn signature_cost(&self) -> u64 { - match self { - Self::SimpleVote { .. } => block_cost_limits::SIGNATURE_COST, - Self::Transaction(usage_cost) => usage_cost.signature_cost, - } + self.usage_cost.signature_cost } pub fn write_lock_cost(&self) -> u64 { - match self { - Self::SimpleVote { .. } => block_cost_limits::WRITE_LOCK_UNITS.saturating_mul(2), - Self::Transaction(usage_cost) => usage_cost.write_lock_cost, - } + self.usage_cost.write_lock_cost } } impl TransactionCost<'_, Tx> { pub fn writable_accounts(&self) -> impl Iterator { - let transaction = match self { - Self::SimpleVote { transaction } => transaction, - Self::Transaction(usage_cost) => usage_cost.transaction, - }; + let transaction = self.usage_cost.transaction; transaction .account_keys() .iter() @@ -110,43 +63,31 @@ impl TransactionCost<'_, Tx> { impl TransactionCost<'_, Tx> { pub fn num_transaction_signatures(&self) -> u64 { - match self { - Self::SimpleVote { .. } => 1, - Self::Transaction(usage_cost) => usage_cost - .transaction - .signature_details() - .num_transaction_signatures(), - } + self.usage_cost + .transaction + .signature_details() + .num_transaction_signatures() } pub fn num_secp256k1_instruction_signatures(&self) -> u64 { - match self { - Self::SimpleVote { .. } => 0, - Self::Transaction(usage_cost) => usage_cost - .transaction - .signature_details() - .num_secp256k1_instruction_signatures(), - } + self.usage_cost + .transaction + .signature_details() + .num_secp256k1_instruction_signatures() } pub fn num_ed25519_instruction_signatures(&self) -> u64 { - match self { - Self::SimpleVote { .. } => 0, - Self::Transaction(usage_cost) => usage_cost - .transaction - .signature_details() - .num_ed25519_instruction_signatures(), - } + self.usage_cost + .transaction + .signature_details() + .num_ed25519_instruction_signatures() } pub fn num_secp256r1_instruction_signatures(&self) -> u64 { - match self { - Self::SimpleVote { .. } => 0, - Self::Transaction(usage_cost) => usage_cost - .transaction - .signature_details() - .num_secp256r1_instruction_signatures(), - } + self.usage_cost + .transaction + .signature_details() + .num_secp256r1_instruction_signatures() } } @@ -336,7 +277,7 @@ impl solana_runtime_transaction::transaction_with_meta::TransactionWithMeta mod tests { use { super::*, - crate::cost_model::CostModel, + crate::{block_cost_limits, cost_model::CostModel}, agave_feature_set::{FeatureSet, bls_pubkey_management_in_vote_account}, agave_reserved_account_keys::ReservedAccountKeys, solana_hash::Hash, @@ -346,7 +287,6 @@ mod tests { solana_transaction::{sanitized::MessageHash, versioned::VersionedTransaction}, solana_vote::vote_transaction, solana_vote_program::vote_state::TowerSync, - test_case::test_matrix, }; fn get_example_transaction() -> VersionedTransaction { @@ -365,19 +305,8 @@ mod tests { VersionedTransaction::from(transaction) } - #[test_matrix( - [false, true], - [false, true] - )] - fn test_vote_transaction_cost( - remove_simple_vote_from_cost_model: bool, - simd_0387_enabled: bool, - ) { - // SIMD-0387 requires `remove_simple_vote_from_cost_model`. - if simd_0387_enabled && !remove_simple_vote_from_cost_model { - return; - } - + #[test] + fn test_vote_transaction_cost() { agave_logger::setup(); use { @@ -399,24 +328,13 @@ mod tests { ) .unwrap(); - let mut feature_set = FeatureSet::all_enabled(); - if !remove_simple_vote_from_cost_model { - feature_set.deactivate(&agave_feature_set::remove_simple_vote_from_cost_model::id()); - } - if !simd_0387_enabled { - feature_set.deactivate(&bls_pubkey_management_in_vote_account::id()); - } + for simd_0387_enabled in [false, true] { + let mut feature_set = FeatureSet::all_enabled(); + if !simd_0387_enabled { + feature_set.deactivate(&bls_pubkey_management_in_vote_account::id()); + } - // Verify actual cost matches expected. - let expected_cost = if !remove_simple_vote_from_cost_model { - SIMPLE_VOTE_USAGE_COST - } else { - // when feature `stop-use-static-simple-vote-tx-cost` is enabled, vote transaction - // cost is calculated based on its UsageCostDetails too: - // - // sample transaction has 2 signatures let signature_cost = 2 * block_cost_limits::SIGNATURE_COST; - // sample transaction has 2 write lock let write_lock_cost = 2 * block_cost_limits::WRITE_LOCK_UNITS; let data_bytes_cost = vote_transaction.instruction_data_len() / (INSTRUCTION_DATA_BYTES_COST as u16); @@ -443,11 +361,11 @@ mod tests { loaded_accounts_data_size_cost, allocated_accounts_data_size: 0, }; - vote_program_usage_details.sum() - }; + let expected_cost = vote_program_usage_details.sum(); - let vote_cost = CostModel::calculate_cost(&vote_transaction, &feature_set); - assert_eq!(expected_cost, vote_cost.sum()); + let vote_cost = CostModel::calculate_cost(&vote_transaction, &feature_set); + assert_eq!(expected_cost, vote_cost.sum()); + } } #[test] diff --git a/feature-set/src/lib.rs b/feature-set/src/lib.rs index 3a59e237d56..77711b1c83b 100644 --- a/feature-set/src/lib.rs +++ b/feature-set/src/lib.rs @@ -74,7 +74,6 @@ pub struct FeatureSnapshot { pub set_lamports_per_byte_to_5080: bool, pub set_lamports_per_byte_to_2575: bool, pub set_lamports_per_byte_to_1322: bool, - pub remove_simple_vote_from_cost_model: bool, pub limit_instruction_accounts: bool, pub block_revenue_sharing: bool, pub vote_account_initialize_v2: bool, @@ -184,7 +183,6 @@ impl From<&AHashMap> for FeatureSnapshot { set_lamports_per_byte_to_5080: is_active(&set_lamports_per_byte_to_5080::ID), set_lamports_per_byte_to_2575: is_active(&set_lamports_per_byte_to_2575::ID), set_lamports_per_byte_to_1322: is_active(&set_lamports_per_byte_to_1322::ID), - remove_simple_vote_from_cost_model: is_active(&remove_simple_vote_from_cost_model::ID), limit_instruction_accounts: is_active(&limit_instruction_accounts::ID), block_revenue_sharing: is_active(&block_revenue_sharing::ID), vote_account_initialize_v2: is_active(&vote_account_initialize_v2::ID), diff --git a/ledger/src/blockstore_processor.rs b/ledger/src/blockstore_processor.rs index f00b6093b60..e80bb0a6c01 100644 --- a/ledger/src/blockstore_processor.rs +++ b/ledger/src/blockstore_processor.rs @@ -2973,7 +2973,6 @@ pub mod tests { rayon::ThreadPoolBuilder, solana_account::{AccountSharedData, WritableAccount}, solana_bls_signatures::{BLS_SIGNATURE_AFFINE_SIZE, Signature as BLSSignature}, - solana_cost_model::transaction_cost::TransactionCost, solana_entry::{ block_component::{BlockComponent, BlockFooterV1, BlockHeaderV1, VersionedBlockMarker}, entry::{create_ticks, next_entry, next_entry_mut}, @@ -6278,9 +6277,7 @@ pub mod tests { let mut tx_cost = CostModel::calculate_cost(&tx, &bank.feature_set); let actual_execution_cu = 1; let actual_loaded_accounts_data_size = 64 * 1024; - let TransactionCost::Transaction(ref mut usage_cost_details) = tx_cost else { - unreachable!("test tx is non-vote tx"); - }; + let usage_cost_details = tx_cost.usage_cost_details_mut(); usage_cost_details.programs_execution_cost = actual_execution_cu; usage_cost_details.loaded_accounts_data_size_cost = CostModel::calculate_loaded_accounts_data_size_cost( @@ -6291,7 +6288,7 @@ pub mod tests { let block_limit = tx_cost.sum(); bank.write_cost_tracker() .unwrap() - .set_limits(u64::MAX, block_limit, u64::MAX, u64::MAX); + .set_limits(u64::MAX, block_limit, u64::MAX); let tx_costs = vec![None, Some(tx_cost), None]; // The transaction will fit when added the first time diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index f6a2bb30881..3903b0a0898 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -4684,12 +4684,10 @@ impl Bank { cost_tracker.get_block_limit() }; let account_cost_limit = block_cost_limit.saturating_mul(40).saturating_div(100); - let vote_cost_limit = cost_tracker.get_vote_limit(); let allocated_data_size_limit = cost_tracker.get_allocated_data_size_limit(); cost_tracker.set_limits( account_cost_limit, block_cost_limit, - vote_cost_limit, allocated_data_size_limit, ); } From b3fa9cbbc2cd4cecf2ed4390890675da1cda52db Mon Sep 17 00:00:00 2001 From: Alex Pyattaev Date: Thu, 4 Jun 2026 21:55:27 +0300 Subject: [PATCH 206/576] multinode-demo: add ability to spawn staked validators (#12395) --- multinode-demo/validator.sh | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/multinode-demo/validator.sh b/multinode-demo/validator.sh index 5ac5443a633..0d76fab0284 100755 --- a/multinode-demo/validator.sh +++ b/multinode-demo/validator.sh @@ -13,9 +13,11 @@ args=( ) airdrops_enabled=1 node_sol=500 # 500 SOL: number of SOL to airdrop the node for transaction fees and vote account rent exemption (ignored if airdrops_enabled=0) +stake_sol= # If set, create a stake account funded with this many SOL and delegate it to the validator's vote account label= identity= vote_account= +stake_account= no_restart=0 gossip_entrypoint= ledger_dir= @@ -29,7 +31,7 @@ usage() { usage: $0 [OPTIONS] [cluster entry point hostname] -Start a validator with no stake +Start a validator (optionally staked via --stake-sol) OPTIONS: --ledger PATH - store ledger under this PATH @@ -37,6 +39,8 @@ OPTIONS: --label LABEL - Append the given label to the configuration files, useful when running multiple validators in the same workspace --node-sol SOL - Number of SOL this node has been funded from the genesis config (default: $node_sol) + --stake-sol SOL - If set, create a stake account funded with this many SOL and delegate it to the validator's vote account + --stake-account PATH - Path to stake-account keypair file (default: /stake-account.json) --no-voting - start node without vote signer --rpc-port port - custom RPC port for this node --no-restart - do not restart the node if it exits @@ -61,6 +65,12 @@ while [[ -n $1 ]]; do elif [[ $1 = --node-sol ]]; then node_sol="$2" shift 2 + elif [[ $1 = --stake-sol ]]; then + stake_sol="$2" + shift 2 + elif [[ $1 = --stake-account ]]; then + stake_account=$2 + shift 2 elif [[ $1 = --no-airdrop ]]; then airdrops_enabled=0 shift @@ -242,6 +252,7 @@ faucet_address="${gossip_entrypoint%:*}":9900 : "${identity:=$ledger_dir/identity.json}" : "${vote_account:=$ledger_dir/vote-account.json}" +: "${stake_account:=$ledger_dir/stake-account.json}" : "${authorized_withdrawer:=$ledger_dir/authorized-withdrawer.json}" default_arg --entrypoint "$gossip_entrypoint" @@ -314,6 +325,24 @@ setup_validator_accounts() { echo "Creating validator vote account" wallet create-vote-account "$vote_account" "$identity" "$authorized_withdrawer" || return $? + + if [[ -n $stake_sol ]] && (( stake_sol > 0 )); then + if ((airdrops_enabled)); then + echo "Transferring $stake_sol SOL to fund stake account:" + ( + set -x + $solana_cli \ + --keypair "$SOLANA_CONFIG_DIR/faucet.json" --url "$rpc_url" \ + transfer --allow-unfunded-recipient "$identity" "$stake_sol" + ) || return $? + fi + echo "Creating stake account" + wallet create-stake-account "$stake_account" "$stake_sol" || return $? + echo "Delegating stake" + declare vote_pubkey + vote_pubkey=$($solana_keygen pubkey "$vote_account") || return $? + wallet delegate-stake "$stake_account" "$vote_pubkey" || return $? + fi fi echo "Validator vote account configured" @@ -329,6 +358,9 @@ rpc_url=$($solana_gossip --allow-private-addr rpc-url --timeout 180 --entrypoint [[ -r "$identity" ]] || $solana_keygen new --no-passphrase -so "$identity" [[ -r "$vote_account" ]] || $solana_keygen new --no-passphrase -so "$vote_account" [[ -r "$authorized_withdrawer" ]] || $solana_keygen new --no-passphrase -so "$authorized_withdrawer" +if [[ -n $stake_sol ]] && (( stake_sol > 0 )); then + [[ -r "$stake_account" ]] || $solana_keygen new --no-passphrase -so "$stake_account" +fi setup_validator_accounts "$node_sol" From fc4ccdfdd4bfb926ca21ca818373f8f9215f62cb Mon Sep 17 00:00:00 2001 From: Akhilesh Singhania Date: Thu, 4 Jun 2026 21:12:31 +0200 Subject: [PATCH 207/576] Converts the `Block` type alias to a `Block` type for better readability and type safety (#12929) Converts the Block type alias to a Block type for better readability and type safety --- bls-cert-verify/benches/cert_verify.rs | 22 +- bls-cert-verify/src/cert_verify.rs | 52 +- cli/src/cluster_query.rs | 2 +- core/benches/bls_vote_sigverify.rs | 10 +- core/src/block_creation_loop.rs | 89 ++-- .../rewards/certs_builder/entry.rs | 13 +- .../certs_builder/entry/notar_entry.rs | 20 +- core/src/bls_sigverify/bls_sigverifier.rs | 97 +++- core/src/repair/block_id_repair_service.rs | 90 ++-- core/src/repair/serve_repair.rs | 4 +- core/src/replay_stage.rs | 78 +-- core/src/replay_stage/tests.rs | 42 +- core/src/tvu.rs | 13 +- core/src/validator.rs | 10 +- entry/src/block_component.rs | 12 +- ledger/src/blockstore_processor.rs | 29 +- rpc-client/src/mock_sender.rs | 7 +- rpc/src/rpc_subscriptions.rs | 4 +- runtime/src/bank_forks.rs | 6 +- .../block_component_processor/vote_reward.rs | 9 +- .../vote_reward/migration_test.rs | 10 +- runtime/src/genesis_utils.rs | 7 +- runtime/src/validated_block_finalization.rs | 105 ++-- runtime/src/validated_reward_certificate.rs | 15 +- .../broadcast_stage/standard_broadcast_run.rs | 32 +- votor-messages/src/certificate.rs | 63 ++- votor-messages/src/consensus_message.rs | 34 +- votor-messages/src/migration.rs | 47 +- votor-messages/src/vote.rs | 51 +- votor/src/common.rs | 10 +- votor/src/consensus_pool.rs | 277 +++++++---- .../src/consensus_pool/certificate_builder.rs | 60 ++- .../consensus_pool/parent_ready_tracker.rs | 89 ++-- .../src/consensus_pool/slot_stake_counters.rs | 78 ++- votor/src/consensus_pool/stats.rs | 8 +- votor/src/consensus_pool/vote_pool.rs | 12 +- votor/src/consensus_pool_service.rs | 104 ++-- votor/src/event.rs | 32 +- votor/src/event_handler.rs | 458 +++++++++++++----- votor/src/event_handler/stats.rs | 4 +- votor/src/root_utils.rs | 5 +- votor/src/vote_history.rs | 104 ++-- votor/src/voting_utils.rs | 31 +- votor/src/votor.rs | 7 +- 44 files changed, 1511 insertions(+), 741 deletions(-) diff --git a/bls-cert-verify/benches/cert_verify.rs b/bls-cert-verify/benches/cert_verify.rs index a0824700aa1..5c58d3a9355 100644 --- a/bls-cert-verify/benches/cert_verify.rs +++ b/bls-cert-verify/benches/cert_verify.rs @@ -3,7 +3,7 @@ use { agave_votor::consensus_pool::certificate_builder::CertificateBuilder, agave_votor_messages::{ certificate::{Certificate, CertificateType}, - consensus_message::VoteMessage, + consensus_message::{Block, VoteMessage}, vote::Vote, }, bitvec::vec::BitVec, @@ -36,7 +36,10 @@ fn create_signed_vote_message(bls_keypair: &BlsKeypair, vote: Vote, rank: usize) fn create_base2_cert(keypairs: &[BlsKeypair], num_signers: usize) -> Certificate { let slot = 100; let hash = Hash::new_unique(); - let cert_type = CertificateType::Notarize(slot, hash); + let cert_type = CertificateType::Notarize(Block { + slot, + block_id: hash, + }); let vote = cert_type.to_source_vote(); let vote_messages: Vec = (0..num_signers) @@ -57,10 +60,19 @@ fn create_base3_cert( ) -> Certificate { let slot = 100; let hash = Hash::new_unique(); - let cert_type = CertificateType::NotarizeFallback(slot, hash); + let cert_type = CertificateType::NotarizeFallback(Block { + slot, + block_id: hash, + }); - let vote_notarize = Vote::new_notarization_vote(slot, hash); - let vote_fallback = Vote::new_notarization_fallback_vote(slot, hash); + let vote_notarize = Vote::new_notarization_vote(Block { + slot, + block_id: hash, + }); + let vote_fallback = Vote::new_notarization_fallback_vote(Block { + slot, + block_id: hash, + }); let mut vote_messages = Vec::new(); diff --git a/bls-cert-verify/src/cert_verify.rs b/bls-cert-verify/src/cert_verify.rs index 9505b58979f..ede586b2c04 100644 --- a/bls-cert-verify/src/cert_verify.rs +++ b/bls-cert-verify/src/cert_verify.rs @@ -108,14 +108,14 @@ pub fn verify_certificate( fn get_vote_payloads(cert_type: CertificateType) -> (Vote, Option) { match cert_type { - CertificateType::Notarize(slot, hash) | CertificateType::FinalizeFast(slot, hash) => { - (Vote::new_notarization_vote(slot, hash), None) + CertificateType::Notarize(block) | CertificateType::FinalizeFast(block) => { + (Vote::new_notarization_vote(block), None) } CertificateType::Finalize(slot) => (Vote::new_finalization_vote(slot), None), - CertificateType::Genesis(slot, hash) => (Vote::new_genesis_vote(slot, hash), None), - CertificateType::NotarizeFallback(slot, hash) => ( - Vote::new_notarization_vote(slot, hash), - Some(Vote::new_notarization_fallback_vote(slot, hash)), + CertificateType::Genesis(block) => (Vote::new_genesis_vote(block), None), + CertificateType::NotarizeFallback(block) => ( + Vote::new_notarization_vote(block), + Some(Vote::new_notarization_fallback_vote(block)), ), CertificateType::Skip(slot) => ( Vote::new_skip_vote(slot), @@ -272,7 +272,10 @@ mod test { use { super::*, agave_votor::consensus_pool::certificate_builder::CertificateBuilder, - agave_votor_messages::{consensus_message::VoteMessage, vote::Vote}, + agave_votor_messages::{ + consensus_message::{Block, VoteMessage}, + vote::Vote, + }, solana_bls_signatures::{ keypair::Keypair as BLSKeypair, signature::Signature as BLSSignature, }, @@ -323,7 +326,10 @@ mod test { #[test] fn test_verify_certificate_base2_valid() { let bls_keypairs = create_bls_keypairs(10); - let cert_type = CertificateType::Notarize(10, Hash::new_unique()); + let cert_type = CertificateType::Notarize(Block { + slot: 10, + block_id: Hash::new_unique(), + }); let cert = create_signed_certificate_message( &bls_keypairs, cert_type, @@ -341,10 +347,12 @@ mod test { #[test] fn test_verify_certificate_base3_valid() { let bls_keypairs = create_bls_keypairs(10); - let slot = 20; - let block_hash = Hash::new_unique(); - let notarize_vote = Vote::new_notarization_vote(slot, block_hash); - let notarize_fallback_vote = Vote::new_notarization_fallback_vote(slot, block_hash); + let block = Block { + slot: 20, + block_id: Hash::new_unique(), + }; + let notarize_vote = Vote::new_notarization_vote(block); + let notarize_fallback_vote = Vote::new_notarization_fallback_vote(block); let mut all_vote_messages = Vec::new(); (0..4).for_each(|i| { all_vote_messages.push(create_signed_vote_message(&bls_keypairs, notarize_vote, i)) @@ -356,7 +364,7 @@ mod test { i, )) }); - let cert_type = CertificateType::NotarizeFallback(slot, block_hash); + let cert_type = CertificateType::NotarizeFallback(block); let mut builder = CertificateBuilder::new(cert_type); builder .aggregate(&all_vote_messages) @@ -376,9 +384,11 @@ mod test { let bls_keypairs = create_bls_keypairs(10); let num_signers = 7; - let slot = 10; - let block_hash = Hash::new_unique(); - let cert_type = CertificateType::Notarize(slot, block_hash); + let block = Block { + slot: 10, + block_id: Hash::new_unique(), + }; + let cert_type = CertificateType::Notarize(block); let mut bitmap = BitVec::new(); bitmap.resize(num_signers, false); for i in 0..num_signers { @@ -404,11 +414,13 @@ mod test { fn base3_cert_with_no_primary_verifies() { let max_validators = 10; let bls_keypairs = create_bls_keypairs(max_validators); - let slot = 20; - let block_hash = Hash::new_unique(); - let cert_type = CertificateType::NotarizeFallback(slot, block_hash); + let block = Block { + slot: 20, + block_id: Hash::new_unique(), + }; + let cert_type = CertificateType::NotarizeFallback(block); let mut builder = CertificateBuilder::new(cert_type); - let vote = Vote::new_notarization_fallback_vote(slot, block_hash); + let vote = Vote::new_notarization_fallback_vote(block); let vote_msgs = (0..max_validators) .map(|i| create_signed_vote_message(&bls_keypairs, vote, i)) .collect::>(); diff --git a/cli/src/cluster_query.rs b/cli/src/cluster_query.rs index bd46fa2209f..048b60194bc 100644 --- a/cli/src/cluster_query.rs +++ b/cli/src/cluster_query.rs @@ -1109,7 +1109,7 @@ pub async fn process_get_ag_genesis_info( CliAgGenesisInfo::Ag(CliAgGenesisInfoPayload { epoch, slot: cert_type.slot(), - block_id: cert_type.to_block().unwrap().1, + block_id: cert_type.to_block().unwrap().block_id, bitvec, signature, }) diff --git a/core/benches/bls_vote_sigverify.rs b/core/benches/bls_vote_sigverify.rs index 4c9dc565a6a..b2e90eaf9c7 100644 --- a/core/benches/bls_vote_sigverify.rs +++ b/core/benches/bls_vote_sigverify.rs @@ -4,7 +4,10 @@ */ use { - agave_votor_messages::{consensus_message::VoteMessage, vote::Vote}, + agave_votor_messages::{ + consensus_message::{Block, VoteMessage}, + vote::Vote, + }, criterion::{BatchSize, Criterion, criterion_group, criterion_main}, rayon::{ThreadPool, ThreadPoolBuilder}, solana_bls_signatures::{Keypair as BLSKeypair, PreparedHashedMessage, VerifySignature}, @@ -54,7 +57,10 @@ fn generate_test_data(num_distinct_messages: usize, batch_size: usize) -> Vec>> = (0..num_distinct_messages) .map(|i| { let slot = (i as u64).saturating_add(100); - let vote = Vote::new_notarization_vote(slot, Hash::new_unique()); + let vote = Vote::new_notarization_vote(Block { + slot, + block_id: Hash::new_unique(), + }); Arc::new(bincode::serialize(&vote).unwrap()) }) .collect(); diff --git a/core/src/block_creation_loop.rs b/core/src/block_creation_loop.rs index 49ca052b762..078bcd287f9 100644 --- a/core/src/block_creation_loop.rs +++ b/core/src/block_creation_loop.rs @@ -125,7 +125,7 @@ pub struct BlockCreationLoopConfig { // Receivers / notifications from banking stage / replay / votor pub leader_window_info_receiver: Receiver, - pub highest_parent_ready: Arc>, + pub highest_parent_ready: Arc>, pub replay_highest_frozen: Arc, pub highest_finalized: Arc>>, @@ -145,7 +145,7 @@ struct LeaderContext { /// ParentReady for a future window observed while producing the current one. pending_parent_ready: Option, /// Highest ParentReady event observed by Votor, used to abandon stale windows. - highest_parent_ready: Arc>, + highest_parent_ready: Arc>, highest_finalized: Arc>>, blockstore: Arc, @@ -365,12 +365,12 @@ fn start_loop(config: BlockCreationLoopConfig, reward_certs_requestor: CertsRequ let LeaderWindowInfo { start_slot, end_slot, - parent_block: (parent_slot, parent_hash), + parent_block, block_timer, } = info; trace!( - "{my_pubkey}: window {start_slot}-{end_slot} parent {parent_slot} \ + "{my_pubkey}: window {start_slot}-{end_slot} parent {parent_block:?} \ flh={fast_leader_handover}" ); @@ -383,8 +383,7 @@ fn start_loop(config: BlockCreationLoopConfig, reward_certs_requestor: CertsRequ fast_leader_handover, start_slot, end_slot, - parent_slot, - parent_hash, + parent_block, block_timer, &mut ctx, ) { @@ -552,16 +551,15 @@ fn produce_window( fast_leader_handover: bool, start_slot: Slot, end_slot: Slot, - parent_slot: Slot, - parent_hash: Hash, + parent_block: Block, mut block_timer: Instant, ctx: &mut LeaderContext, ) -> Result<(), StartLeaderError> { // Insert the first bank let mut working_bank = start_leader_wait_for_parent_replay( start_slot, - parent_slot, - Some(parent_hash), + parent_block.slot, + Some(parent_block.block_id), block_timer, ctx, )?; @@ -582,7 +580,7 @@ fn produce_window( let mut bank_completion_measure = Measure::start("bank_completion"); let optimistic_parent = - (fast_leader_handover && slot == start_slot).then_some((parent_slot, parent_hash)); + (fast_leader_handover && slot == start_slot).then_some(parent_block); if let Err(e) = record_and_complete_block(ctx, slot, optimistic_parent, &mut block_timer, timeout) { @@ -631,7 +629,7 @@ fn produce_window( fn record_and_complete_block( ctx: &mut LeaderContext, bank_slot: Slot, - mut optimistic_parent: Option<(Slot, Hash)>, + mut optimistic_parent: Option, block_timer: &mut Instant, block_timeout: Duration, ) -> Result<(), PohRecorderError> { @@ -801,7 +799,7 @@ fn process_parent_ready( ctx: &mut LeaderContext, info: LeaderWindowInfo, bank_slot: Slot, - optimistic_parent: &mut Option<(Slot, Hash)>, + optimistic_parent: &mut Option, accumulated_txs: &mut Vec, block_timer: &mut Instant, records_shutdown: &mut bool, @@ -870,10 +868,9 @@ fn send_update_parent( poh_recorder: &RwLock, new_parent_block: Block, ) -> Result<(), PohRecorderError> { - let (new_parent_slot, new_parent_block_id) = new_parent_block; let update_parent = UpdateParentV1 { - new_parent_slot, - new_parent_block_id, + new_parent_slot: new_parent_block.slot, + new_parent_block_id: new_parent_block.block_id, }; let marker = VersionedBlockMarker::from_update_parent(update_parent); poh_recorder.write().unwrap().send_marker(marker)?; @@ -894,7 +891,7 @@ fn send_update_parent( fn handle_parent_ready( ctx: &mut LeaderContext, leader_window_info: LeaderWindowInfo, - optimistic_parent_block: (Slot, Hash), + optimistic_parent_block: Block, mut accumulated_txs: Vec, block_timer: &mut Instant, ) -> Result>, PohRecorderError> { @@ -925,8 +922,11 @@ fn handle_parent_ready( send_update_parent(&ctx.poh_recorder, leader_window_info.parent_block)?; let slot = leader_window_info.start_slot; - let (old_parent_slot, _) = optimistic_parent_block; - let (new_parent_slot, new_parent_hash) = leader_window_info.parent_block; + let old_parent_slot = optimistic_parent_block.slot; + let Block { + slot: new_parent_slot, + block_id: new_parent_hash, + } = leader_window_info.parent_block; let bank = ctx .poh_recorder @@ -1435,7 +1435,10 @@ mod tests { LeaderWindowInfo { start_slot, end_slot: last_of_consecutive_leader_slots(start_slot), - parent_block: (parent_slot, Hash::new_unique()), + parent_block: Block { + slot: parent_slot, + block_id: Hash::new_unique(), + }, block_timer: Instant::now(), } } @@ -1453,7 +1456,7 @@ mod tests { .unwrap(); assert_eq!(selected.start_slot, 16); - assert_eq!(selected.parent_block.0, 15); + assert_eq!(selected.parent_block.slot, 15); } #[test] @@ -1463,7 +1466,7 @@ mod tests { .unwrap(); assert_eq!(selected.start_slot, 8); - assert_eq!(selected.parent_block.0, 6); + assert_eq!(selected.parent_block.slot, 6); } fn recv_update_parent_marker( @@ -1543,7 +1546,13 @@ mod tests { my_pubkey, leader_window_info_receiver, pending_parent_ready: None, - highest_parent_ready: Arc::new(RwLock::new((0, (0, Hash::default())))), + highest_parent_ready: Arc::new(RwLock::new(( + 0, + Block { + slot: 0, + block_id: Hash::default(), + }, + ))), highest_finalized: Arc::new(RwLock::new(None)), blockstore, record_receiver, @@ -1655,7 +1664,13 @@ mod tests { my_pubkey, leader_window_info_receiver, pending_parent_ready: None, - highest_parent_ready: Arc::new(RwLock::new((0, (0, Hash::default())))), + highest_parent_ready: Arc::new(RwLock::new(( + 0, + Block { + slot: 0, + block_id: Hash::default(), + }, + ))), highest_finalized: Arc::new(RwLock::new(None)), blockstore, record_receiver, @@ -1724,7 +1739,13 @@ mod tests { my_pubkey, leader_window_info_receiver, pending_parent_ready: None, - highest_parent_ready: Arc::new(RwLock::new((2, (0, Hash::default())))), + highest_parent_ready: Arc::new(RwLock::new(( + 2, + Block { + slot: 0, + block_id: Hash::default(), + }, + ))), highest_finalized: Arc::new(RwLock::new(None)), blockstore, record_receiver, @@ -1827,7 +1848,13 @@ mod tests { my_pubkey, leader_window_info_receiver, pending_parent_ready: None, - highest_parent_ready: Arc::new(RwLock::new((4, (new_parent_slot, new_parent_hash)))), + highest_parent_ready: Arc::new(RwLock::new(( + 4, + Block { + slot: new_parent_slot, + block_id: new_parent_hash, + }, + ))), highest_finalized: Arc::new(RwLock::new(None)), blockstore, record_receiver, @@ -1863,13 +1890,19 @@ mod tests { let parent_ready = LeaderWindowInfo { start_slot: leader_slot, end_slot: 7, - parent_block: (new_parent_slot, new_parent_hash), + parent_block: Block { + slot: new_parent_slot, + block_id: new_parent_hash, + }, block_timer: Instant::now(), }; let new_bank = handle_parent_ready( &mut ctx, parent_ready, - (optimistic_parent_slot, optimistic_parent_hash), + Block { + slot: optimistic_parent_slot, + block_id: optimistic_parent_hash, + }, vec![accumulated_tx.clone()], &mut Instant::now(), ) diff --git a/core/src/block_creation_loop/rewards/certs_builder/entry.rs b/core/src/block_creation_loop/rewards/certs_builder/entry.rs index b9ffc8e1172..f21233d867e 100644 --- a/core/src/block_creation_loop/rewards/certs_builder/entry.rs +++ b/core/src/block_creation_loop/rewards/certs_builder/entry.rs @@ -70,7 +70,7 @@ impl Entry { rank_map, vote.rank, &vote.signature, - notar.block_id, + notar.block.block_id, self.max_validators, ), Vote::Skip(_) => self.skip.add_vote(rank_map, vote.rank, &vote.signature), @@ -120,6 +120,7 @@ impl Entry { mod tests { use { super::*, + agave_votor_messages::consensus_message::Block, solana_bls_signatures::{Keypair as BlsKeypair, PubkeyCompressed as BlsPubkeyCompressed}, solana_epoch_schedule::EpochSchedule, solana_hash::Hash, @@ -230,12 +231,18 @@ mod tests { let blockid1 = Hash::new_unique(); for rank in 0..2 { - let notar = Vote::new_notarization_vote(slot, blockid0); + let notar = Vote::new_notarization_vote(Block { + slot, + block_id: blockid0, + }); let vote = new_vote(notar, rank, &keypairs); entry.add_vote(&rank_map, &vote).unwrap(); } for rank in 2..5 { - let notar = Vote::new_notarization_vote(slot, blockid1); + let notar = Vote::new_notarization_vote(Block { + slot, + block_id: blockid1, + }); let vote = new_vote(notar, rank, &keypairs); entry.add_vote(&rank_map, &vote).unwrap(); } diff --git a/core/src/block_creation_loop/rewards/certs_builder/entry/notar_entry.rs b/core/src/block_creation_loop/rewards/certs_builder/entry/notar_entry.rs index be9d0c91538..67fb117fc2a 100644 --- a/core/src/block_creation_loop/rewards/certs_builder/entry/notar_entry.rs +++ b/core/src/block_creation_loop/rewards/certs_builder/entry/notar_entry.rs @@ -97,7 +97,10 @@ mod tests { crate::block_creation_loop::rewards::certs_builder::entry::tests::{ get_rank_map_keypairs, new_vote, validate_bitmap, }, - agave_votor_messages::{consensus_message::VoteMessage, vote::Vote}, + agave_votor_messages::{ + consensus_message::{Block, VoteMessage}, + vote::Vote, + }, solana_bls_signatures::signature::BLS_SIGNATURE_AFFINE_SIZE, solana_hash::Hash, }; @@ -111,7 +114,10 @@ mod tests { let mut entry = NotarEntry::new(max_validators); let blockid0 = Hash::new_unique(); - let notar = Vote::new_notarization_vote(slot, blockid0); + let notar = Vote::new_notarization_vote(Block { + slot, + block_id: blockid0, + }); let invalid_vote = VoteMessage { vote: notar, signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), @@ -162,7 +168,10 @@ mod tests { let blockid1 = Hash::new_unique(); for rank in 0..2 { - let notar = Vote::new_notarization_vote(slot, blockid0); + let notar = Vote::new_notarization_vote(Block { + slot, + block_id: blockid0, + }); let vote = new_vote(notar, rank, &keypairs); entry .add_vote( @@ -175,7 +184,10 @@ mod tests { .unwrap(); } for rank in 2..5 { - let notar = Vote::new_notarization_vote(slot, blockid1); + let notar = Vote::new_notarization_vote(Block { + slot, + block_id: blockid1, + }); let vote = new_vote(notar, rank, &keypairs); entry .add_vote( diff --git a/core/src/bls_sigverify/bls_sigverifier.rs b/core/src/bls_sigverify/bls_sigverifier.rs index 129c8dd8980..a49acfb9439 100644 --- a/core/src/bls_sigverify/bls_sigverifier.rs +++ b/core/src/bls_sigverify/bls_sigverifier.rs @@ -339,7 +339,7 @@ mod tests { }, agave_votor_messages::{ certificate::{Certificate, CertificateType}, - consensus_message::{ConsensusMessage, VoteMessage}, + consensus_message::{Block, ConsensusMessage, VoteMessage}, vote::Vote, }, bitvec::prelude::{BitVec, Lsb0}, @@ -543,7 +543,10 @@ mod tests { let vote_rank2 = 3; let vote_message2 = create_signed_vote_message( &ctx.validator_keypairs, - Vote::new_notarization_vote(6, Hash::new_unique()), + Vote::new_notarization_vote(Block { + slot: 6, + block_id: Hash::new_unique(), + }), vote_rank2, ); let messages2 = vec![ConsensusMessage::Vote(vote_message2)]; @@ -567,7 +570,10 @@ mod tests { let vote_rank3 = 9; let vote_message3 = create_signed_vote_message( &ctx.validator_keypairs, - Vote::new_notarization_fallback_vote(7, Hash::new_unique()), + Vote::new_notarization_fallback_vote(Block { + slot: 7, + block_id: Hash::new_unique(), + }), vote_rank3, ); let messages3 = vec![ConsensusMessage::Vote(vote_message3)]; @@ -649,7 +655,10 @@ mod tests { )); let msg2 = ConsensusMessage::Vote(create_signed_vote_message( &ctx.validator_keypairs, - Vote::new_notarization_fallback_vote(6, Hash::new_unique()), + Vote::new_notarization_fallback_vote(Block { + slot: 6, + block_id: Hash::new_unique(), + }), 2, )); ctx.verifier @@ -769,7 +778,10 @@ mod tests { let vote1 = Vote::new_skip_vote(42); let _vote1_payload = wincode::serialize(&vote1).expect("Failed to serialize vote"); - let vote2 = Vote::new_notarization_vote(43, Hash::new_unique()); + let vote2 = Vote::new_notarization_vote(Block { + slot: 43, + block_id: Hash::new_unique(), + }); let _vote2_payload = wincode::serialize(&vote2).expect("Failed to serialize vote"); // Group 1 votes @@ -948,7 +960,10 @@ mod tests { // 2/3 of validators sign the cert. let num_signers = (ctx.validator_keypairs.len() * 2).div_ceil(3); - let cert_type = CertificateType::Notarize(10, Hash::new_unique()); + let cert_type = CertificateType::Notarize(Block { + slot: 10, + block_id: Hash::new_unique(), + }); let cert = create_signed_certificate_message( &ctx.validator_keypairs, cert_type, @@ -973,7 +988,10 @@ mod tests { // 60% of validators sign the cert. let num_signers = (ctx.validator_keypairs.len() * 6).div_ceil(10); - let cert_type = CertificateType::Notarize(10, Hash::new_unique()); + let cert_type = CertificateType::Notarize(Block { + slot: 10, + block_id: Hash::new_unique(), + }); let cert = create_signed_certificate_message( &ctx.validator_keypairs, cert_type, @@ -999,7 +1017,10 @@ mod tests { // < 60% of validators sign the cert assert!(ctx.validator_keypairs.len() >= 2); let num_signers = (ctx.validator_keypairs.len() * 6) / 10 - 1; - let cert_type = CertificateType::Notarize(10, Hash::new_unique()); + let cert_type = CertificateType::Notarize(Block { + slot: 10, + block_id: Hash::new_unique(), + }); let cert = create_signed_certificate_message( &ctx.validator_keypairs, cert_type, @@ -1026,8 +1047,12 @@ mod tests { let slot = 20; let block_hash = Hash::new_unique(); - let notarize_vote = Vote::new_notarization_vote(slot, block_hash); - let notarize_fallback_vote = Vote::new_notarization_fallback_vote(slot, block_hash); + let block = Block { + slot, + block_id: block_hash, + }; + let notarize_vote = Vote::new_notarization_vote(block); + let notarize_fallback_vote = Vote::new_notarization_fallback_vote(block); let mut all_vote_messages = Vec::new(); (0..4).for_each(|i| { all_vote_messages.push(create_signed_vote_message( @@ -1043,7 +1068,7 @@ mod tests { i, )) }); - let cert_type = CertificateType::NotarizeFallback(slot, block_hash); + let cert_type = CertificateType::NotarizeFallback(block); let mut builder = CertificateBuilder::new(cert_type); builder .aggregate(&all_vote_messages) @@ -1068,8 +1093,12 @@ mod tests { let slot = 20; let block_hash = Hash::new_unique(); - let notarize_vote = Vote::new_notarization_vote(slot, block_hash); - let notarize_fallback_vote = Vote::new_notarization_fallback_vote(slot, block_hash); + let block = Block { + slot, + block_id: block_hash, + }; + let notarize_vote = Vote::new_notarization_vote(block); + let notarize_fallback_vote = Vote::new_notarization_fallback_vote(block); let mut all_vote_messages = Vec::new(); (0..4).for_each(|i| { all_vote_messages.push(create_signed_vote_message( @@ -1085,7 +1114,7 @@ mod tests { i, )) }); - let cert_type = CertificateType::NotarizeFallback(slot, block_hash); + let cert_type = CertificateType::NotarizeFallback(block); let mut builder = CertificateBuilder::new(cert_type); builder .aggregate(&all_vote_messages) @@ -1110,8 +1139,12 @@ mod tests { let slot = 20; let block_hash = Hash::new_unique(); - let notarize_vote = Vote::new_notarization_vote(slot, block_hash); - let notarize_fallback_vote = Vote::new_notarization_fallback_vote(slot, block_hash); + let block = Block { + slot, + block_id: block_hash, + }; + let notarize_vote = Vote::new_notarization_vote(block); + let notarize_fallback_vote = Vote::new_notarization_fallback_vote(block); let mut all_vote_messages = Vec::new(); (0..4).for_each(|i| { all_vote_messages.push(create_signed_vote_message( @@ -1127,7 +1160,7 @@ mod tests { i, )) }); - let cert_type = CertificateType::NotarizeFallback(slot, block_hash); + let cert_type = CertificateType::NotarizeFallback(block); let mut builder = CertificateBuilder::new(cert_type); builder .aggregate(&all_vote_messages) @@ -1155,7 +1188,10 @@ mod tests { let num_signers = (ctx.validator_keypairs.len() * 7).div_ceil(10); let slot = 10; let block_hash = Hash::new_unique(); - let cert_type = CertificateType::Notarize(slot, block_hash); + let cert_type = CertificateType::Notarize(Block { + slot, + block_id: block_hash, + }); let mut bitmap = BitVec::::new(); bitmap.resize(num_signers, false); for i in 0..num_signers { @@ -1204,8 +1240,11 @@ mod tests { // 70% of validators sign. let num_signers = (ctx.validator_keypairs.len() * 7).div_ceil(10); - let cert_type = CertificateType::Notarize(10, Hash::new_unique()); - let cert_original_vote = Vote::new_notarization_vote(10, cert_type.to_block().unwrap().1); + let cert_type = CertificateType::Notarize(Block { + slot: 10, + block_id: Hash::new_unique(), + }); + let cert_original_vote = Vote::new_notarization_vote(cert_type.to_block().unwrap()); let cert_payload = wincode::serialize(&cert_original_vote).unwrap(); let cert_vote_messages: Vec = (0..num_signers) @@ -1360,8 +1399,12 @@ mod tests { let num_signers = (ctx.validator_keypairs.len() * 8).div_ceil(10); let slot = 10; let block_hash = Hash::new_unique(); - let cert_type = CertificateType::Notarize(slot, block_hash); - let original_vote = Vote::new_notarization_vote(slot, block_hash); + let block = Block { + slot, + block_id: block_hash, + }; + let cert_type = CertificateType::Notarize(block); + let original_vote = Vote::new_notarization_vote(block); let signed_payload = wincode::serialize(&original_vote).unwrap(); let mut vote_messages: Vec = (0..num_signers) .map(|i| { @@ -1419,7 +1462,10 @@ mod tests { )); let cert_message = ConsensusMessage::Certificate(create_signed_certificate_message( &ctx.validator_keypairs, - CertificateType::Notarize(43, Hash::new_unique()), + CertificateType::Notarize(Block { + slot: 43, + block_id: Hash::new_unique(), + }), &(0..7).collect::>(), )); let vote_sender = Pubkey::new_unique(); @@ -1493,7 +1539,10 @@ mod tests { let messages: Vec<_> = (0..5) .map(|i| { let slot = 10 + i as u64; - let cert_type = CertificateType::Notarize(slot, Hash::new_unique()); + let cert_type = CertificateType::Notarize(Block { + slot, + block_id: Hash::new_unique(), + }); let mut cert = create_signed_certificate_message( &ctx.validator_keypairs, cert_type, diff --git a/core/src/repair/block_id_repair_service.rs b/core/src/repair/block_id_repair_service.rs index 81433086489..5b523a26594 100644 --- a/core/src/repair/block_id_repair_service.rs +++ b/core/src/repair/block_id_repair_service.rs @@ -189,7 +189,7 @@ struct RepairState { sent_requests: HashMap, /// Blocks we've previously requested. Used to avoid re-initiating repair for an in progress block. - requested_blocks: HashSet, + requested_blocks: HashSet<(Slot, Hash)>, // Stats response_stats: BlockIdRepairResponsesStats, @@ -611,8 +611,10 @@ impl BlockIdRepairService { } => { // Queue a request to repair the parent (filtered out later if we already have the parent) state.push_pending_repair_event(RepairEvent::FetchBlock { - slot: p_slot, - block_id: p_block_id, + block: Block { + slot: p_slot, + block_id: p_block_id, + }, }); // Queue FecSetRoot requests @@ -718,14 +720,16 @@ impl BlockIdRepairService { event: RepairEvent, root: Slot, blockstore: &Blockstore, - requested_blocks: &HashSet, + requested_blocks: &HashSet<(Slot, Hash)>, ) -> Result { if event.slot() <= root { return Ok(PendingRepairDecision::Drop); } match event { - RepairEvent::FetchBlock { slot, block_id } => { + RepairEvent::FetchBlock { + block: Block { slot, block_id }, + } => { if requested_blocks.contains(&(slot, block_id)) { return Ok(PendingRepairDecision::Drop); } @@ -860,8 +864,10 @@ impl BlockIdRepairService { .expect("SlotMeta must be populated for full slots"); state.push_pending_repair_event(RepairEvent::FetchBlock { - slot: meta.parent_slot.expect("Parent must exist for full slots"), - block_id: meta.parent_block_id, + block: Block { + slot: meta.parent_slot.expect("Parent must exist for full slots"), + block_id: meta.parent_block_id, + }, }); Ok(()) @@ -1183,19 +1189,25 @@ mod tests { for slot in base_slot..base_slot + MAX_PENDING_REPAIR_EVENTS as Slot { state.push_pending_repair_event(RepairEvent::FetchBlock { - slot, - block_id: Hash::new_unique(), + block: Block { + slot, + block_id: Hash::new_unique(), + }, }); assert!(state.pending_repair_events.len() <= MAX_PENDING_REPAIR_EVENTS); } state.push_pending_repair_event(RepairEvent::FetchBlock { - slot: 1, - block_id: Hash::new_unique(), + block: Block { + slot: 1, + block_id: Hash::new_unique(), + }, }); state.push_pending_repair_event(RepairEvent::FetchBlock { - slot: base_slot + MAX_PENDING_REPAIR_EVENTS as Slot, - block_id: Hash::new_unique(), + block: Block { + slot: base_slot + MAX_PENDING_REPAIR_EVENTS as Slot, + block_id: Hash::new_unique(), + }, }); assert_eq!(state.pending_repair_events.len(), MAX_PENDING_REPAIR_EVENTS); @@ -1446,12 +1458,9 @@ mod tests { // Verify: FetchBlock event for parent was added to pending_repair_events assert_eq!(state.pending_repair_events.len(), 1); - let RepairEvent::FetchBlock { - slot: s, - block_id: b, - } = state.pending_repair_events.first().unwrap(); - assert_eq!(*s, parent_slot); - assert_eq!(*b, parent_block_id); + let RepairEvent::FetchBlock { block } = state.pending_repair_events.first().unwrap(); + assert_eq!(block.slot, parent_slot); + assert_eq!(block.block_id, parent_block_id); // Verify: FecSetRoot requests were added to pending assert_eq!(state.pending_repair_requests.len(), fec_set_count_usize); @@ -1714,7 +1723,9 @@ mod tests { // Mark the slot as dead (Turbine failed) blockstore.set_dead_slot(slot).unwrap(); - let event = RepairEvent::FetchBlock { slot, block_id }; + let event = RepairEvent::FetchBlock { + block: Block { slot, block_id }, + }; process_repair_event_for_test(Pubkey::new_unique(), event, 0, &blockstore, &mut state) .unwrap(); @@ -1748,7 +1759,9 @@ mod tests { let slot = 100u64; let block_id = Hash::new_unique(); - let event = RepairEvent::FetchBlock { slot, block_id }; + let event = RepairEvent::FetchBlock { + block: Block { slot, block_id }, + }; process_repair_event_for_test(Pubkey::new_unique(), event, 0, &blockstore, &mut state) .unwrap(); @@ -1758,12 +1771,9 @@ mod tests { // Verify: Event was deferred assert_eq!(state.pending_repair_events.len(), 1); - let RepairEvent::FetchBlock { - slot: s, - block_id: b, - } = state.pending_repair_events.first().unwrap(); - assert_eq!(*s, slot); - assert_eq!(*b, block_id); + let RepairEvent::FetchBlock { block } = state.pending_repair_events.first().unwrap(); + assert_eq!(block.slot, slot); + assert_eq!(block.block_id, block_id); // Verify: block was NOT added to requested_blocks (so it can be re-added when reprocessed) assert!(!state.requested_blocks.contains(&(slot, block_id))); @@ -1786,8 +1796,10 @@ mod tests { .unwrap(); let event = RepairEvent::FetchBlock { - slot, - block_id: requested_block_id, + block: Block { + slot, + block_id: requested_block_id, + }, }; process_repair_event_for_test(Pubkey::new_unique(), event, 0, &blockstore, &mut state) @@ -1825,7 +1837,9 @@ mod tests { // Pre-add block to requested_blocks state.requested_blocks.insert((slot, block_id)); - let event = RepairEvent::FetchBlock { slot, block_id }; + let event = RepairEvent::FetchBlock { + block: Block { slot, block_id }, + }; process_repair_event_for_test(Pubkey::new_unique(), event, 0, &blockstore, &mut state) .unwrap(); @@ -1843,7 +1857,9 @@ mod tests { // Use slot 0 which is at root let slot = 0u64; let block_id = Hash::new_unique(); - let event = RepairEvent::FetchBlock { slot, block_id }; + let event = RepairEvent::FetchBlock { + block: Block { slot, block_id }, + }; process_repair_event_for_test(Pubkey::new_unique(), event, 0, &blockstore, &mut state) .unwrap(); @@ -1868,8 +1884,10 @@ mod tests { let new_block_id = Hash::new_unique(); let event = RepairEvent::FetchBlock { - slot, - block_id: new_block_id, + block: Block { + slot, + block_id: new_block_id, + }, }; process_repair_event_for_test(Pubkey::new_unique(), event, 0, &blockstore, &mut state) @@ -1898,8 +1916,10 @@ mod tests { .iter() .map(|block_id| { let event = RepairEvent::FetchBlock { - slot, - block_id: *block_id, + block: Block { + slot, + block_id: *block_id, + }, }; match BlockIdRepairService::decide_pending_repair_event( &my_pubkey, diff --git a/core/src/repair/serve_repair.rs b/core/src/repair/serve_repair.rs index 3d6e00c789d..dcf3900c9a0 100644 --- a/core/src/repair/serve_repair.rs +++ b/core/src/repair/serve_repair.rs @@ -16,7 +16,7 @@ use { result::{Error, RepairVerifyError, Result}, }, }, - agave_votor_messages::{consensus_message::Block, migration::MigrationStatus}, + agave_votor_messages::migration::MigrationStatus, bincode::{Options, serialize}, crossbeam_channel::{Receiver, RecvTimeoutError}, lazy_lru::LruCache, @@ -223,7 +223,7 @@ pub enum BlockIdRepairType { } impl BlockIdRepairType { - pub(crate) fn block(&self) -> Block { + pub(crate) fn block(&self) -> (Slot, Hash) { match self { BlockIdRepairType::ParentAndFecSetCount { slot, block_id } => (*slot, *block_id), BlockIdRepairType::FecSetRoot { slot, block_id, .. } => (*slot, *block_id), diff --git a/core/src/replay_stage.rs b/core/src/replay_stage.rs index 49082ddf2b9..b5891da9f44 100644 --- a/core/src/replay_stage.rs +++ b/core/src/replay_stage.rs @@ -42,7 +42,7 @@ use { voting_utils::{self, GenerateVoteTxResult}, }, agave_votor_messages::{ - consensus_message::ConsensusMessage, + consensus_message::{Block, ConsensusMessage}, migration::{GENESIS_VOTE_REFRESH, MigrationStatus}, vote::Vote, }, @@ -1641,7 +1641,7 @@ impl ReplayStage { ) { let root_bank = bank_forks.read().unwrap().root_bank(); - let genesis_block @ (genesis_slot, block_id) = migration_status + let genesis_block = migration_status .genesis_block() .expect("Must be ready to enable"); info!( @@ -1649,16 +1649,16 @@ impl ReplayStage { alpenglow. Genesis block {genesis_block:?}" ); - let genesis_bank = bank_forks.read().unwrap().get(genesis_slot).expect( + let genesis_bank = bank_forks.read().unwrap().get(genesis_block.slot).expect( "{my_pubkey}: Attempting to enable alpenglow before receiving the genesis block", ); assert!(genesis_bank.is_frozen()); - if genesis_bank.block_id() != Some(block_id) { + if genesis_bank.block_id() != Some(genesis_block.block_id) { panic!( "{my_pubkey}: Attempting to enable alpenglow but we have the wrong version of the \ - genesis block our version: ({genesis_slot}, {:?}), certified version \ - ({genesis_slot}, {block_id})", + genesis block our version: ({}, {:?}), certified version ({genesis_block:?})", + genesis_block.slot, genesis_bank.block_id() ); } @@ -1684,7 +1684,7 @@ impl ReplayStage { .unwrap() .banks() .keys() - .filter_map(|slot| (*slot > genesis_slot).then_some(*slot)) + .filter_map(|slot| (*slot > genesis_block.slot).then_some(*slot)) .collect(); for slot in slots_to_purge.into_iter() { info!("{my_pubkey} Alpenglow migration: Purging poh block in slot {slot}"); @@ -1700,7 +1700,7 @@ impl ReplayStage { } // Purge any partial slots greater than the genesis slot - let start_slot = genesis_slot + 1; + let start_slot = genesis_block.slot + 1; let end_slot = blockstore .highest_slot() .unwrap() @@ -1718,7 +1718,7 @@ impl ReplayStage { assert!(migration_status.is_alpenglow_enabled()); datapoint_info!( "migration-complete", - ("genesis_slot", genesis_slot as i64, i64), + ("genesis_slot", genesis_block.slot as i64, i64), ); } @@ -1733,12 +1733,12 @@ impl ReplayStage { own_vote_sender: &Sender>, bls_sender: &Sender, ) -> bool { - let Some((slot, block_id)) = migration_status.eligible_genesis_block() else { + let Some(block) = migration_status.eligible_genesis_block() else { // We have not yet discovered the genesis block return false; }; - let vote = Vote::new_genesis_vote(slot, block_id); + let vote = Vote::new_genesis_vote(block); match voting_utils::generate_vote_tx( vote, bank_forks.read().unwrap().root_bank().as_ref(), @@ -1751,14 +1751,14 @@ impl ReplayStage { GenerateVoteTxResult::ConsensusMessage(message) => { // Send vote to ConsensusPool and rest of cluster warn!( - "{} Alpenglow migration: Casting genesis vote for ({slot}, {block_id})", + "{} Alpenglow migration: Casting genesis vote for ({block:?})", identity_keypair.pubkey() ); // If sending fails that means the channel is disconnected and we are shutting down let _ = own_vote_sender.send(vec![message.clone()]); let _ = bls_sender.send(BLSOp::PushVote { message: Arc::new(message), - slot, + slot: block.slot, saved_vote_history: agave_votor::vote_history_storage::SavedVoteHistoryVersions::Current( SavedVoteHistory::default(), @@ -1767,8 +1767,7 @@ impl ReplayStage { } e => { warn!( - "{} Alpenglow migration: Unable to send genesis vote for {slot} {block_id}: \ - {e:?}", + "{} Alpenglow migration: Unable to send genesis vote for {block:?}: {e:?}", identity_keypair.pubkey() ); } @@ -2381,7 +2380,7 @@ impl ReplayStage { fn process_switch_bank_events( my_pubkey: &Pubkey, latest_switch_request: &LatestSwitchRequest, - pending_switch: &mut Option<(Slot, Hash)>, + pending_switch: &mut Option, blockstore: &Blockstore, bank_forks: &RwLock, progress: &mut ProgressMap, @@ -2389,29 +2388,33 @@ impl ReplayStage { ) -> Result<(), BlockstoreError> { let root = bank_forks.read().unwrap().root(); - if let Some((slot, block_id)) = latest_switch_request + if let Some(block) = latest_switch_request .take() .map(|ev| ev.block()) - .filter(|(slot, _)| *slot > root) + .filter(|block| block.slot > root) { - // Overwrite the pending switch, later switches take precedence - if Some(slot) >= pending_switch.map(|(slot, _)| slot) { - if let Some(prev_switch_request) = pending_switch.replace((slot, block_id)) { - trace!( - "{my_pubkey}: Overwriting previous switch request {prev_switch_request:?} \ - with ({slot}, {block_id})" - ); - } else { - trace!("{my_pubkey}: Received switch request in {slot} to {block_id}"); + match pending_switch { + None => { + trace!("{my_pubkey}: Setting empty pending_switch to ({block:?})"); + *pending_switch = Some(block); + } + Some(pending_switch_block) => { + if block.slot >= pending_switch_block.slot { + trace!( + "{my_pubkey}: Overwriting previous switch request \ + {pending_switch_block:?} with ({block:?})" + ); + *pending_switch_block = block; + } } } }; - let Some((slot, block_id)) = *pending_switch else { + let Some(block) = *pending_switch else { return Ok(()); }; - if bank_forks.read().unwrap().block_id(slot) == Some(block_id) { + if bank_forks.read().unwrap().block_id(block.slot) == Some(block.block_id) { // Nothing to switch *pending_switch = None; return Ok(()); @@ -2419,8 +2422,8 @@ impl ReplayStage { // Check if we have received the block and all of its ancestors and collect the ones we // need to switch out - let mut ancestor_slot = slot; - let mut ancestor_block_id = block_id; + let mut ancestor_slot = block.slot; + let mut ancestor_block_id = block.block_id; let mut blocks_to_switch = vec![]; loop { if ancestor_slot <= root { @@ -4240,7 +4243,10 @@ impl ReplayStage { let leader_window_info = LeaderWindowInfo { start_slot: next_slot, end_slot, - parent_block: (bank.slot(), block_id), + parent_block: Block { + slot: bank.slot(), + block_id, + }, block_timer: Instant::now(), }; @@ -4414,13 +4420,13 @@ impl ReplayStage { .get(genesis_slot) .expect("Genesis bank must exist, no rooting past migration slot"); - let genesis_block = ( - genesis_slot, - genesis_bank.block_id().expect( + let genesis_block = Block { + slot: genesis_slot, + block_id: genesis_bank.block_id().expect( "It is impossible for block id to not be known at this point, as \ a descendant of this block has reached super oc status", ), - ); + }; migration_status.set_genesis_block(genesis_block); } diff --git a/core/src/replay_stage/tests.rs b/core/src/replay_stage/tests.rs index c3d89517c43..f3081a06f90 100644 --- a/core/src/replay_stage/tests.rs +++ b/core/src/replay_stage/tests.rs @@ -11,7 +11,10 @@ use { replay_stage::ReplayStage, vote_simulator::{self, VoteSimulator}, }, - agave_votor_messages::certificate::{Certificate, CertificateType}, + agave_votor_messages::{ + certificate::{Certificate, CertificateType}, + consensus_message::Block, + }, blockstore_processor::{ ConfirmationProgress, ProcessOptions, confirm_full_slot, fill_blockstore_slot_with_ticks, process_bank_0, @@ -127,9 +130,12 @@ impl ProcessActiveBanksContext { fn post_migration_status_for_tests() -> MigrationStatus { let migration_status = MigrationStatus::default(); migration_status.record_feature_activation(0); - let genesis_block = (0, Hash::default()); + let genesis_block = Block { + slot: 0, + block_id: Hash::default(), + }; let genesis_certificate = Arc::new(Certificate { - cert_type: CertificateType::Genesis(genesis_block.0, genesis_block.1), + cert_type: CertificateType::Genesis(genesis_block), signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), bitmap: vec![], }); @@ -3364,7 +3370,10 @@ fn test_latest_parent_coalesces() { .try_send(LeaderWindowInfo { start_slot: 8, end_slot: 11, - parent_block: (7, Hash::new_unique()), + parent_block: Block { + slot: 7, + block_id: Hash::new_unique(), + }, block_timer: Instant::now(), }) .unwrap(); @@ -3375,7 +3384,10 @@ fn test_latest_parent_coalesces() { LeaderWindowInfo { start_slot: 12, end_slot: 15, - parent_block: (11, Hash::new_unique()), + parent_block: Block { + slot: 11, + block_id: Hash::new_unique(), + }, block_timer: Instant::now(), }, ); @@ -3389,7 +3401,10 @@ fn test_latest_parent_coalesces() { .try_send(LeaderWindowInfo { start_slot: 20, end_slot: 22, - parent_block: (19, Hash::new_unique()), + parent_block: Block { + slot: 19, + block_id: Hash::new_unique(), + }, block_timer: Instant::now(), }) .unwrap(); @@ -3400,7 +3415,10 @@ fn test_latest_parent_coalesces() { LeaderWindowInfo { start_slot: 20, end_slot: 23, - parent_block: (19, Hash::new_unique()), + parent_block: Block { + slot: 19, + block_id: Hash::new_unique(), + }, block_timer: Instant::now(), }, ); @@ -3414,7 +3432,10 @@ fn test_latest_parent_coalesces() { .try_send(LeaderWindowInfo { start_slot: 20, end_slot: 23, - parent_block: (19, Hash::new_unique()), + parent_block: Block { + slot: 19, + block_id: Hash::new_unique(), + }, block_timer: Instant::now(), }) .unwrap(); @@ -3425,7 +3446,10 @@ fn test_latest_parent_coalesces() { LeaderWindowInfo { start_slot: 16, end_slot: 19, - parent_block: (15, Hash::new_unique()), + parent_block: Block { + slot: 15, + block_id: Hash::new_unique(), + }, block_timer: Instant::now(), }, ); diff --git a/core/src/tvu.rs b/core/src/tvu.rs index dd32fcd3bf0..c119efc2aa4 100644 --- a/core/src/tvu.rs +++ b/core/src/tvu.rs @@ -37,6 +37,7 @@ use { voting_service::{VotingService as BLSVotingService, VotingServiceOverride}, votor::{Votor, VotorConfig}, }, + agave_votor_messages::consensus_message::Block, crossbeam_channel::{Receiver, Sender, bounded, unbounded}, solana_client::connection_cache::ConnectionCache, solana_clock::Slot, @@ -45,7 +46,6 @@ use { cluster_info::ClusterInfo, duplicate_shred_handler::DuplicateShredHandler, duplicate_shred_listener::DuplicateShredListener, }, - solana_hash::Hash, solana_keypair::Keypair, solana_ledger::{ blockstore::{Blockstore, MAX_COMPLETED_SLOTS_IN_CHANNEL, UpdateParentReceiver}, @@ -170,7 +170,7 @@ pub struct AlpenglowInitializationState { pub optimistic_parent_sender: Sender, pub optimistic_parent_receiver: Receiver, pub replay_highest_frozen: Arc, - pub highest_parent_ready: Arc>, + pub highest_parent_ready: Arc>, pub highest_finalized: Arc>>, pub bank_forks_controller: Arc, pub bank_forks_controller_receiver: BankForksCommandReceiver, @@ -717,6 +717,7 @@ pub mod tests { }, serial_test::serial, solana_gossip::{cluster_info::ClusterInfo, node::Node}, + solana_hash::Hash, solana_keypair::Keypair, solana_ledger::{ blockstore::BlockstoreSignals, @@ -805,7 +806,13 @@ pub mod tests { let replay_highest_frozen = Arc::new(ReplayHighestFrozen::default()); let (leader_window_info_sender, _leader_window_info_receiver) = unbounded(); let (optimistic_parent_sender, optimistic_parent_receiver) = unbounded(); - let highest_parent_ready = Arc::new(RwLock::new((0, (0, Hash::default())))); + let highest_parent_ready = Arc::new(RwLock::new(( + 0, + Block { + slot: 0, + block_id: Hash::default(), + }, + ))); let (votor_event_sender, votor_event_receiver): (VotorEventSender, VotorEventReceiver) = unbounded(); let staked_nodes = Arc::new(RwLock::new(StakedNodes::default())); diff --git a/core/src/validator.rs b/core/src/validator.rs index 0a79970b8f4..95d2466f09e 100644 --- a/core/src/validator.rs +++ b/core/src/validator.rs @@ -3113,7 +3113,10 @@ mod tests { #[test] fn test_should_require_vote_history_file() { use { - agave_votor_messages::certificate::{Certificate, CertificateType}, + agave_votor_messages::{ + certificate::{Certificate, CertificateType}, + consensus_message::Block, + }, solana_account::{AccountSharedData, state_traits::StateMut}, solana_bls_signatures::{BLS_SIGNATURE_AFFINE_SIZE, Signature as BLSSignature}, }; @@ -3172,7 +3175,10 @@ mod tests { )); bank.set_alpenglow_genesis_certificate(&Certificate { - cert_type: CertificateType::Genesis(40, Hash::new_unique()), + cert_type: CertificateType::Genesis(Block { + slot: 40, + block_id: Hash::new_unique(), + }), signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), bitmap: vec![], }); diff --git a/entry/src/block_component.rs b/entry/src/block_component.rs index a890cda8abb..dba44656f96 100644 --- a/entry/src/block_component.rs +++ b/entry/src/block_component.rs @@ -133,6 +133,7 @@ use { crate::entry::{Entry, MaxDataShredsLen}, agave_votor_messages::{ certificate::{Certificate, CertificateType}, + consensus_message::Block, reward_certificate::{NotarRewardCertificate, SkipRewardCertificate}, }, solana_bls_signatures::{ @@ -247,7 +248,7 @@ impl TryFrom for GenesisCertBlockMarker { type Error = String; fn try_from(cert: Certificate) -> Result { - let CertificateType::Genesis(slot, block_id) = cert.cert_type else { + let CertificateType::Genesis(block) = cert.cert_type else { return Err("expected genesis certificate".into()); }; if cert.bitmap.len() > Self::MAX_BITMAP_SIZE { @@ -258,8 +259,8 @@ impl TryFrom for GenesisCertBlockMarker { )); } Ok(Self { - slot, - block_id, + slot: block.slot, + block_id: block.block_id, bls_signature: cert.signature, bitmap: cert.bitmap, }) @@ -269,7 +270,10 @@ impl TryFrom for GenesisCertBlockMarker { impl From for Certificate { fn from(cert: GenesisCertBlockMarker) -> Self { Self { - cert_type: CertificateType::Genesis(cert.slot, cert.block_id), + cert_type: CertificateType::Genesis(Block { + slot: cert.slot, + block_id: cert.block_id, + }), signature: cert.bls_signature, bitmap: cert.bitmap, } diff --git a/ledger/src/blockstore_processor.rs b/ledger/src/blockstore_processor.rs index e80bb0a6c01..85cfd6db909 100644 --- a/ledger/src/blockstore_processor.rs +++ b/ledger/src/blockstore_processor.rs @@ -2967,7 +2967,10 @@ pub mod tests { }, shred::{ProcessShredsStats, ReedSolomonCache, Shred, Shredder}, }, - agave_votor_messages::certificate::{Certificate, CertificateType}, + agave_votor_messages::{ + certificate::{Certificate, CertificateType}, + consensus_message::Block, + }, assert_matches::assert_matches, rand::{Rng, rng}, rayon::ThreadPoolBuilder, @@ -3019,30 +3022,32 @@ pub mod tests { }; /// Generate a dummy alpenglow genesis certificate - fn genesis_certificate(slot: Slot, block_id: Hash) -> Arc { + fn genesis_certificate(genesis_block: Block) -> Arc { Arc::new(Certificate { - cert_type: CertificateType::Genesis(slot, block_id), + cert_type: CertificateType::Genesis(genesis_block), signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), bitmap: vec![], }) } /// Generate a dummy `MigrationStatus` in the `ReadyToEnable` phase - fn ready_to_enable_migration_status(genesis_slot: Slot, block_id: Hash) -> MigrationStatus { + fn ready_to_enable_migration_status(genesis_block: Block) -> MigrationStatus { let migration_status = MigrationStatus::default(); let migration_slot = migration_status.record_feature_activation(0); - assert!(genesis_slot < migration_slot); - migration_status.set_genesis_block((genesis_slot, block_id)); - migration_status.set_genesis_certificate(genesis_certificate(genesis_slot, block_id)); + assert!(genesis_block.slot < migration_slot); + migration_status.set_genesis_block(genesis_block); + migration_status.set_genesis_certificate(genesis_certificate(genesis_block)); assert!(migration_status.is_ready_to_enable()); migration_status } #[test] fn test_startup_replay_enable_waits_for_poh_service_when_started() { - let genesis_slot = 1; - let block_id = Hash::new_from_array([7; solana_hash::HASH_BYTES]); - let migration_status = Arc::new(ready_to_enable_migration_status(genesis_slot, block_id)); + let genesis_block = Block { + slot: 1, + block_id: Hash::new_from_array([7; solana_hash::HASH_BYTES]), + }; + let migration_status = Arc::new(ready_to_enable_migration_status(genesis_block)); let poh_service = { let migration_status = Arc::clone(&migration_status); migration_status.set_poh_service_started(); @@ -3056,14 +3061,14 @@ pub mod tests { assert_eq!( migration_status.enable_alpenglow_during_startup(), - genesis_slot + genesis_block.slot ); poh_service.join().unwrap(); assert!(migration_status.is_alpenglow_enabled()); assert_eq!( migration_status.wait_for_migration_or_exit(&AtomicBool::new(false)), - Some((genesis_slot, block_id)) + Some(genesis_block) ); } diff --git a/rpc-client/src/mock_sender.rs b/rpc-client/src/mock_sender.rs index ba5ebe246fe..218404cb851 100644 --- a/rpc-client/src/mock_sender.rs +++ b/rpc-client/src/mock_sender.rs @@ -2,7 +2,10 @@ use { crate::rpc_sender::*, - agave_votor_messages::certificate::{Certificate, CertificateType}, + agave_votor_messages::{ + certificate::{Certificate, CertificateType}, + consensus_message::Block, + }, async_trait::async_trait, base64::{Engine, prelude::BASE64_STANDARD}, serde_json::{Number, Value, json}, @@ -176,7 +179,7 @@ impl RpcSender for MockSender { })?, "getAgGenesisCert" => { let cert = Certificate { - cert_type: CertificateType::Genesis(0, Hash::default()), + cert_type: CertificateType::Genesis(Block { slot: 0, block_id: Hash::default() }), signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), bitmap: Vec::default(), }; diff --git a/rpc/src/rpc_subscriptions.rs b/rpc/src/rpc_subscriptions.rs index 93919f8cd55..a8f41bce01a 100644 --- a/rpc/src/rpc_subscriptions.rs +++ b/rpc/src/rpc_subscriptions.rs @@ -2720,7 +2720,9 @@ pub(crate) mod tests { let bank2 = Bank::new_from_parent(bank0, SlotLeader::default(), 2); bank_forks.write().unwrap().insert(bank2); - let alice = Keypair::from_base58_string("sfLnS4rZ5a8gXke3aGxCgM6usFAVPxLUaBSRdssGY9uS5eoiEWQ41CqDcpXbcekpKsie8Lyy3LNFdhEvjUE1wd9"); + let alice = Keypair::from_base58_string( + "sfLnS4rZ5a8gXke3aGxCgM6usFAVPxLUaBSRdssGY9uS5eoiEWQ41CqDcpXbcekpKsie8Lyy3LNFdhEvjUE1wd9", + ); let optimistically_confirmed_bank = OptimisticallyConfirmedBank::locked_from_bank_forks_root(&bank_forks); diff --git a/runtime/src/bank_forks.rs b/runtime/src/bank_forks.rs index 40328c97a49..f17e9ee3727 100644 --- a/runtime/src/bank_forks.rs +++ b/runtime/src/bank_forks.rs @@ -788,6 +788,7 @@ mod tests { agave_feature_set::FeatureSet, agave_votor_messages::{ certificate::{Certificate, CertificateType}, + consensus_message::Block, migration::{GENESIS_CERTIFICATE_ACCOUNT, MIGRATION_SLOT_OFFSET}, }, assert_matches::assert_matches, @@ -1029,7 +1030,10 @@ mod tests { fn test_initialize_migration_status() { let ff_activation_slot = 5; let genesis_cert = Certificate { - cert_type: CertificateType::Genesis(1, Hash::default()), + cert_type: CertificateType::Genesis(Block { + slot: 1, + block_id: Hash::default(), + }), signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), bitmap: vec![], }; diff --git a/runtime/src/block_component_processor/vote_reward.rs b/runtime/src/block_component_processor/vote_reward.rs index bb2de616898..1de3939db1e 100644 --- a/runtime/src/block_component_processor/vote_reward.rs +++ b/runtime/src/block_component_processor/vote_reward.rs @@ -616,6 +616,7 @@ mod tests { agave_feature_set::FeatureSet, agave_votor_messages::{ certificate::{Certificate, CertificateType}, + consensus_message::Block, reward_certificate::NUM_SLOTS_FOR_REWARD, }, bitvec::prelude::*, @@ -672,9 +673,11 @@ mod tests { bank: &Bank, signing_ranks: &[usize], ) -> ValidatedBlockFinalizationCert { - let slot = bank.slot(); - let block_id = Hash::new_unique(); - let cert_type = CertificateType::FinalizeFast(slot, block_id); + let block = Block { + slot: bank.slot(), + block_id: Hash::new_unique(), + }; + let cert_type = CertificateType::FinalizeFast(block); let max_rank = signing_ranks.iter().copied().max().unwrap_or(0); let mut bitvec = BitVec::::repeat(false, max_rank.saturating_add(1)); for &rank in signing_ranks { diff --git a/runtime/src/block_component_processor/vote_reward/migration_test.rs b/runtime/src/block_component_processor/vote_reward/migration_test.rs index c7f81acd176..33fd50a1b60 100644 --- a/runtime/src/block_component_processor/vote_reward/migration_test.rs +++ b/runtime/src/block_component_processor/vote_reward/migration_test.rs @@ -16,7 +16,10 @@ mod tests { stake_utils, }, agave_feature_set::FeatureSet, - agave_votor_messages::certificate::{Certificate, CertificateType}, + agave_votor_messages::{ + certificate::{Certificate, CertificateType}, + consensus_message::Block, + }, solana_account::{Account, ReadableAccount}, solana_bls_signatures::{BLS_SIGNATURE_AFFINE_SIZE, Signature as BLSSignature}, solana_cluster_type::ClusterType, @@ -441,7 +444,10 @@ mod tests { let bank_with_tower_rewards = state.add_tower_rewards(bank_at_migration0); let genesis_cert = Certificate { - cert_type: CertificateType::Genesis(bank_with_tower_rewards.slot(), Hash::default()), + cert_type: CertificateType::Genesis(Block { + slot: bank_with_tower_rewards.slot(), + block_id: Hash::default(), + }), signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), bitmap: vec![], }; diff --git a/runtime/src/genesis_utils.rs b/runtime/src/genesis_utils.rs index 92b7bad9b11..ccd0f635443 100644 --- a/runtime/src/genesis_utils.rs +++ b/runtime/src/genesis_utils.rs @@ -10,7 +10,7 @@ use { agave_votor_messages::{ self, certificate::{Certificate, CertificateType}, - consensus_message::BLS_KEYPAIR_DERIVE_SEED, + consensus_message::{BLS_KEYPAIR_DERIVE_SEED, Block}, migration::GENESIS_CERTIFICATE_ACCOUNT, }, bincode::serialize, @@ -328,7 +328,10 @@ pub fn activate_all_features_alpenglow(genesis_config: &mut GenesisConfig) { // This is a dev cluster with alpenglow enabled at genesis. We don't want to test the migration pathway // so we add a fake genesis certificate. let cert = Certificate { - cert_type: CertificateType::Genesis(0, Hash::default()), + cert_type: CertificateType::Genesis(Block { + slot: 0, + block_id: Hash::default(), + }), signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), bitmap: encode_base2(&BitVec::new()).unwrap(), }; diff --git a/runtime/src/validated_block_finalization.rs b/runtime/src/validated_block_finalization.rs index d4a14b47bbc..8ad47918032 100644 --- a/runtime/src/validated_block_finalization.rs +++ b/runtime/src/validated_block_finalization.rs @@ -8,13 +8,13 @@ use { crate::bank::Bank, agave_votor_messages::{ certificate::{Certificate, CertificateType}, + consensus_message::Block, fraction::Fraction, }, log::warn, solana_bls_signatures::BlsError, solana_clock::Slot, solana_entry::block_component::{BlockFinalizationCert, VotesAggregate}, - solana_hash::Hash, solana_pubkey::Pubkey, solana_signer_store::{DecodeError, Decoded, decode}, std::{collections::HashSet, num::NonZeroU64}, @@ -95,12 +95,15 @@ impl ValidatedBlockFinalizationCert { block_final_cert: BlockFinalizationCert, bank: &Bank, ) -> Result { - let (slot, block_id) = (block_final_cert.slot, block_final_cert.block_id); + let block = Block { + slot: block_final_cert.slot, + block_id: block_final_cert.block_id, + }; if let Some(notar_aggregate) = block_final_cert.notar_aggregate { // Slow finalization - let notarize_cert_type = CertificateType::Notarize(slot, block_id); - let finalize_cert_type = CertificateType::Finalize(slot); + let notarize_cert_type = CertificateType::Notarize(block); + let finalize_cert_type = CertificateType::Finalize(block.slot); let notarize_cert = Certificate { cert_type: notarize_cert_type, @@ -131,9 +134,9 @@ impl ValidatedBlockFinalizationCert { if notarize_percent < notarize_threshold { warn!( - "Received a slow finalization in the footer for slot {slot} block_id \ - {block_id:?} in bank slot {} with notarize {notarize_percent} stake \ - expecting at least {notarize_threshold}", + "Received a slow finalization in the footer for block {block:?} in bank slot \ + {} with notarize {notarize_percent} stake expecting at least \ + {notarize_threshold}", bank.slot() ); return Err(BlockFinalizationCertError::InsufficientStake { @@ -145,9 +148,9 @@ impl ValidatedBlockFinalizationCert { if finalize_percent < finalize_threshold { warn!( - "Received a slow finalization in the footer for slot {slot} block_id \ - {block_id:?} in bank slot {} with finalize {finalize_percent} stake \ - expecting at least {finalize_threshold}", + "Received a slow finalization in the footer for block {block:?} in bank slot \ + {} with finalize {finalize_percent} stake expecting at least \ + {finalize_threshold}", bank.slot() ); return Err(BlockFinalizationCertError::InsufficientStake { @@ -167,7 +170,7 @@ impl ValidatedBlockFinalizationCert { }) } else { // Fast finalization - let fast_finalize_cert_type = CertificateType::FinalizeFast(slot, block_id); + let fast_finalize_cert_type = CertificateType::FinalizeFast(block); let fast_finalize_cert = Certificate { cert_type: fast_finalize_cert_type, @@ -189,9 +192,8 @@ impl ValidatedBlockFinalizationCert { if finalize_percent < finalize_threshold { warn!( - "Received a fast finalization in the footer for slot {slot} block_id \ - {block_id:?} in bank slot {} with {finalize_percent} stake expecting at \ - least {finalize_threshold}", + "Received a fast finalization in the footer for block {block:?} in bank slot \ + {} with {finalize_percent} stake expecting at least {finalize_threshold}", bank.slot() ); return Err(BlockFinalizationCertError::InsufficientStake { @@ -279,7 +281,7 @@ impl ValidatedBlockFinalizationCert { } /// Returns the block that is finalized (slot, block_id). - pub fn block(&self) -> (Slot, Hash) { + pub fn block(&self) -> Block { match &self.kind { ValidatedBlockFinalizationCertKind::Finalize { notarize_cert, .. } => notarize_cert .cert_type @@ -333,7 +335,7 @@ impl ValidatedBlockFinalizationCert { .cert_type .to_block() .expect("notarize certificates correspond to blocks") - .1; + .block_id; BlockFinalizationCert { slot, block_id, @@ -342,13 +344,13 @@ impl ValidatedBlockFinalizationCert { } } ValidatedBlockFinalizationCertKind::FastFinalize(cert) => { - let (slot, block_id) = cert + let block = cert .cert_type .to_block() .expect("fast finalizations correspond to blocks"); BlockFinalizationCert { - slot, - block_id, + slot: block.slot, + block_id: block.block_id, final_aggregate: VotesAggregate::from_certificate(cert), notar_aggregate: None, } @@ -408,6 +410,7 @@ mod tests { bitvec::prelude::*, solana_bls_signatures::SignatureProjective, solana_entry::block_component::VotesAggregate, + solana_hash::Hash, solana_signer_store::encode_base2, std::sync::Arc, }; @@ -475,21 +478,23 @@ mod tests { .collect(); let (bank, validator_keypairs) = create_bank_with_bls_validators(num_validators, stakes); - let slot = bank.slot(); - let block_id = Hash::new_unique(); + let block = Block { + slot: bank.slot(), + block_id: Hash::new_unique(), + }; // Test 1: Fast finalize (requires 80% stake = 4400) // Top 6 validators = 1000+900+800+700+600+500 = 4500 (>= 80%) { - let cert_type = CertificateType::FinalizeFast(slot, block_id); - let vote = Vote::new_notarization_vote(slot, block_id); + let cert_type = CertificateType::FinalizeFast(block); + let vote = Vote::new_notarization_vote(block); let signing_ranks: Vec = (0..6).collect(); let fast_finalize_cert = build_certificate_manual(cert_type, vote, &signing_ranks, &validator_keypairs); let block_final_cert = BlockFinalizationCert { - slot, - block_id, + slot: block.slot, + block_id: block.block_id, final_aggregate: VotesAggregate::from_certificate(&fast_finalize_cert), notar_aggregate: None, }; @@ -504,8 +509,8 @@ mod tests { // Test 2: Slow finalize (requires 60% stake = 3300 for both certs) // Top 4 validators = 1000+900+800+700 = 3400 (>= 60%) { - let notarize_cert_type = CertificateType::Notarize(slot, block_id); - let notarize_vote = Vote::new_notarization_vote(slot, block_id); + let notarize_cert_type = CertificateType::Notarize(block); + let notarize_vote = Vote::new_notarization_vote(block); let notarize_signing_ranks: Vec = (0..4).collect(); let notarize_cert = build_certificate_manual( notarize_cert_type, @@ -514,8 +519,8 @@ mod tests { &validator_keypairs, ); - let finalize_cert_type = CertificateType::Finalize(slot); - let finalize_vote = Vote::new_finalization_vote(slot); + let finalize_cert_type = CertificateType::Finalize(block.slot); + let finalize_vote = Vote::new_finalization_vote(block.slot); let finalize_signing_ranks: Vec = (0..4).collect(); let finalize_cert = build_certificate_manual( finalize_cert_type, @@ -525,8 +530,8 @@ mod tests { ); let block_final_cert = BlockFinalizationCert { - slot, - block_id, + slot: block.slot, + block_id: block.block_id, final_aggregate: VotesAggregate::from_certificate(&finalize_cert), notar_aggregate: Some(VotesAggregate::from_certificate(¬arize_cert)), }; @@ -549,21 +554,23 @@ mod tests { .collect(); let (bank, validator_keypairs) = create_bank_with_bls_validators(num_validators, stakes); - let slot = bank.slot(); - let block_id = Hash::new_unique(); + let block = Block { + slot: bank.slot(), + block_id: Hash::new_unique(), + }; // Fast finalize with insufficient stake (requires 80% = 4400) // Top 5 validators = 1000+900+800+700+600 = 4000 (< 80%) { - let cert_type = CertificateType::FinalizeFast(slot, block_id); - let vote = Vote::new_notarization_vote(slot, block_id); + let cert_type = CertificateType::FinalizeFast(block); + let vote = Vote::new_notarization_vote(block); let signing_ranks: Vec = (0..5).collect(); let fast_finalize_cert = build_certificate_manual(cert_type, vote, &signing_ranks, &validator_keypairs); let block_final_cert = BlockFinalizationCert { - slot, - block_id, + slot: block.slot, + block_id: block.block_id, final_aggregate: VotesAggregate::from_certificate(&fast_finalize_cert), notar_aggregate: None, }; @@ -581,8 +588,8 @@ mod tests { // Slow finalize with insufficient notarize stake (requires 60% = 3300) // Top 3 validators = 1000+900+800 = 2700 (< 60%) { - let notarize_cert_type = CertificateType::Notarize(slot, block_id); - let notarize_vote = Vote::new_notarization_vote(slot, block_id); + let notarize_cert_type = CertificateType::Notarize(block); + let notarize_vote = Vote::new_notarization_vote(block); let notarize_signing_ranks: Vec = (0..3).collect(); let notarize_cert = build_certificate_manual( notarize_cert_type, @@ -592,8 +599,8 @@ mod tests { ); // Finalize cert has enough stake - let finalize_cert_type = CertificateType::Finalize(slot); - let finalize_vote = Vote::new_finalization_vote(slot); + let finalize_cert_type = CertificateType::Finalize(block.slot); + let finalize_vote = Vote::new_finalization_vote(block.slot); let finalize_signing_ranks: Vec = (0..4).collect(); let finalize_cert = build_certificate_manual( finalize_cert_type, @@ -603,8 +610,8 @@ mod tests { ); let block_final_cert = BlockFinalizationCert { - slot, - block_id, + slot: block.slot, + block_id: block.block_id, final_aggregate: VotesAggregate::from_certificate(&finalize_cert), notar_aggregate: Some(VotesAggregate::from_certificate(¬arize_cert)), }; @@ -623,8 +630,8 @@ mod tests { // Notarize has enough stake, but finalize doesn't { // Notarize cert has enough stake - let notarize_cert_type = CertificateType::Notarize(slot, block_id); - let notarize_vote = Vote::new_notarization_vote(slot, block_id); + let notarize_cert_type = CertificateType::Notarize(block); + let notarize_vote = Vote::new_notarization_vote(block); let notarize_signing_ranks: Vec = (0..4).collect(); let notarize_cert = build_certificate_manual( notarize_cert_type, @@ -635,8 +642,8 @@ mod tests { // Finalize cert has insufficient stake // Top 3 validators = 1000+900+800 = 2700 (< 60%) - let finalize_cert_type = CertificateType::Finalize(slot); - let finalize_vote = Vote::new_finalization_vote(slot); + let finalize_cert_type = CertificateType::Finalize(block.slot); + let finalize_vote = Vote::new_finalization_vote(block.slot); let finalize_signing_ranks: Vec = (0..3).collect(); let finalize_cert = build_certificate_manual( finalize_cert_type, @@ -646,8 +653,8 @@ mod tests { ); let block_final_cert = BlockFinalizationCert { - slot, - block_id, + slot: block.slot, + block_id: block.block_id, final_aggregate: VotesAggregate::from_certificate(&finalize_cert), notar_aggregate: Some(VotesAggregate::from_certificate(¬arize_cert)), }; diff --git a/runtime/src/validated_reward_certificate.rs b/runtime/src/validated_reward_certificate.rs index c6b454a2f98..9ad01928c7b 100644 --- a/runtime/src/validated_reward_certificate.rs +++ b/runtime/src/validated_reward_certificate.rs @@ -2,6 +2,7 @@ use { crate::bank::Bank, agave_bls_cert_verify::cert_verify::{Error as BlsCertVerifyError, verify_base2}, agave_votor_messages::{ + consensus_message::Block, reward_certificate::{NUM_SLOTS_FOR_REWARD, NotarRewardCertificate, SkipRewardCertificate}, vote::Vote, }, @@ -112,7 +113,10 @@ impl ValidatedRewardCert { )? } if let Some(notar) = notar { - let vote = Vote::new_notarization_vote(notar.slot, notar.block_id); + let vote = Vote::new_notarization_vote(Block { + slot: notar.slot, + block_id: notar.block_id, + }); // unwrap should be safe as we contructed the vote ourselves. let payload = bincode::serialize(&vote).unwrap(); verify_base2( @@ -238,14 +242,17 @@ mod tests { }) .collect::>(); - let blockid = Hash::new_unique(); - let notar_vote = Vote::new_notarization_vote(reward_slot, blockid); + let block_id = Hash::new_unique(); + let notar_vote = Vote::new_notarization_vote(Block { + slot: reward_slot, + block_id, + }); let notar_votes = (0..num_notar_validators) .map(|rank| new_vote(notar_vote, rank, signing_keys[rank])) .collect::>(); let (signature, bitmap) = build_sig_bitmap(¬ar_votes); let notar_reward_cert = - NotarRewardCertificate::try_new(reward_slot, blockid, signature, bitmap).unwrap(); + NotarRewardCertificate::try_new(reward_slot, block_id, signature, bitmap).unwrap(); let skip_vote = Vote::new_skip_vote(reward_slot); let skip_votes = (num_notar_validators..num_validators) diff --git a/turbine/src/broadcast_stage/standard_broadcast_run.rs b/turbine/src/broadcast_stage/standard_broadcast_run.rs index ef1bb8525a5..09fffbfc27e 100644 --- a/turbine/src/broadcast_stage/standard_broadcast_run.rs +++ b/turbine/src/broadcast_stage/standard_broadcast_run.rs @@ -87,7 +87,10 @@ impl StandardBroadcastRun { slot: Slot::MAX, parent: Slot::MAX, parent_block_id: Hash::default(), - parent_for_double_merkle: (Slot::MAX, Hash::default()), + parent_for_double_merkle: Block { + slot: Slot::MAX, + block_id: Hash::default(), + }, chained_merkle_root: Hash::default(), double_merkle_leaves: vec![], carryover_entry: None, @@ -162,7 +165,10 @@ impl StandardBroadcastRun { self.slot = bank.slot(); self.parent = bank.parent_slot(); self.parent_block_id = parent_block_id; - self.parent_for_double_merkle = (bank.parent_slot(), parent_block_id); + self.parent_for_double_merkle = Block { + slot: bank.parent_slot(), + block_id: parent_block_id, + }; self.chained_merkle_root = chained_merkle_root; self.double_merkle_leaves.clear(); self.next_shred_index = 0u32; @@ -279,10 +285,10 @@ impl StandardBroadcastRun { return; }; let parent_for_double_merkle = match update_parent { - VersionedUpdateParent::V1(update_parent) => ( - update_parent.new_parent_slot, - update_parent.new_parent_block_id, - ), + VersionedUpdateParent::V1(update_parent) => Block { + slot: update_parent.new_parent_slot, + block_id: update_parent.new_parent_block_id, + }, }; self.parent_for_double_merkle = parent_for_double_merkle; } @@ -290,8 +296,8 @@ impl StandardBroadcastRun { /// Leaf that binds the current replay parent into the double-merkle block id. fn parent_info_leaf(&self, fec_set_count: u32) -> Hash { hashv(&[ - &self.parent_for_double_merkle.0.to_le_bytes(), - self.parent_for_double_merkle.1.as_bytes(), + &self.parent_for_double_merkle.slot.to_le_bytes(), + self.parent_for_double_merkle.block_id.as_bytes(), &fec_set_count.to_le_bytes(), ]) } @@ -1260,7 +1266,10 @@ mod test { bs.parent = 10; let original_parent_block_id = Hash::new_unique(); bs.parent_block_id = original_parent_block_id; - bs.parent_for_double_merkle = (bs.parent, bs.parent_block_id); + bs.parent_for_double_merkle = Block { + slot: bs.parent, + block_id: bs.parent_block_id, + }; let new_parent_slot = 7; let new_parent_block_id = Hash::new_unique(); @@ -1286,7 +1295,10 @@ mod test { assert_eq!(bs.parent_block_id, original_parent_block_id); assert_eq!( bs.parent_for_double_merkle, - (new_parent_slot, new_parent_block_id) + Block { + slot: new_parent_slot, + block_id: new_parent_block_id + } ); let fec_set_count = 3u32; assert_eq!( diff --git a/votor-messages/src/certificate.rs b/votor-messages/src/certificate.rs index 73476453252..0648a676107 100644 --- a/votor-messages/src/certificate.rs +++ b/votor-messages/src/certificate.rs @@ -10,7 +10,6 @@ use { serde::{Deserialize, Serialize}, solana_bls_signatures::Signature as BLSSignature, solana_clock::Slot, - solana_hash::Hash, wincode::{SchemaRead, SchemaWrite, pod_wrapper}, }; @@ -25,7 +24,7 @@ pod_wrapper! { #[cfg_attr( feature = "frozen-abi", derive(AbiExample), - frozen_abi(digest = "Aijp38PLH2Y9rwxXeAdRXvCisL9Fnx6spwnKGY55WcuU") + frozen_abi(digest = "5WqvPnvSnVXQFrAs9o29szFGDiCk45Pgk8K1evTZSrwo") )] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, SchemaWrite, SchemaRead)] pub struct Certificate { @@ -43,7 +42,7 @@ pub struct Certificate { #[cfg_attr( feature = "frozen-abi", derive(AbiExample, AbiEnumVisitor), - frozen_abi(digest = "CazjewshYYizgQuCgBBRv6gzasJpUvFVKoSeEirWRKgA") + frozen_abi(digest = "Fi1rPdeeVstWxxnnPiS7bYtXMEyX6sDGV4o3R2aDMnjt") )] #[derive( Debug, @@ -63,15 +62,15 @@ pub enum CertificateType { /// Finalize certificate Finalize(Slot), /// Fast finalize certificate - FinalizeFast(Slot, Hash), + FinalizeFast(Block), /// Notarize certificate - Notarize(Slot, Hash), + Notarize(Block), /// Notarize fallback certificate - NotarizeFallback(Slot, Hash), + NotarizeFallback(Block), /// Skip certificate Skip(Slot), /// Genesis certificate - Genesis(Slot, Hash), + Genesis(Block), } impl CertificateType { @@ -79,22 +78,22 @@ impl CertificateType { pub fn slot(&self) -> Slot { match self { CertificateType::Finalize(slot) - | CertificateType::FinalizeFast(slot, _) - | CertificateType::Notarize(slot, _) - | CertificateType::NotarizeFallback(slot, _) - | CertificateType::Genesis(slot, _) + | CertificateType::FinalizeFast(Block { slot, block_id: _ }) + | CertificateType::NotarizeFallback(Block { slot, block_id: _ }) + | CertificateType::Notarize(Block { slot, block_id: _ }) + | CertificateType::Genesis(Block { slot, block_id: _ }) | CertificateType::Skip(slot) => *slot, } } /// Is this a fast finalize certificate? pub fn is_fast_finalization(&self) -> bool { - matches!(self, Self::FinalizeFast(_, _)) + matches!(self, Self::FinalizeFast(_)) } /// Is this a finalize / fast finalize certificate? pub fn is_finalization(&self) -> bool { - matches!(self, Self::Finalize(_) | Self::FinalizeFast(_, _)) + matches!(self, Self::Finalize(_) | Self::FinalizeFast(_)) } /// Is this a slow finalize certificate? @@ -104,12 +103,12 @@ impl CertificateType { /// Is this a notarize certificate? pub fn is_notarize(&self) -> bool { - matches!(self, Self::Notarize(_, _)) + matches!(self, Self::Notarize(_)) } /// Is this a notarize fallback certificate? pub fn is_notarize_fallback(&self) -> bool { - matches!(self, Self::NotarizeFallback(_, _)) + matches!(self, Self::NotarizeFallback(_)) } /// Is this a skip certificate? @@ -119,17 +118,17 @@ impl CertificateType { /// Is this a genesis certificate? pub fn is_genesis(&self) -> bool { - matches!(self, Self::Genesis(_, _)) + matches!(self, Self::Genesis(_)) } /// Gets the block associated with this certificate, if present pub fn to_block(self) -> Option { match self { CertificateType::Finalize(_) | CertificateType::Skip(_) => None, - CertificateType::Notarize(slot, block_id) - | CertificateType::NotarizeFallback(slot, block_id) - | CertificateType::Genesis(slot, block_id) - | CertificateType::FinalizeFast(slot, block_id) => Some((slot, block_id)), + CertificateType::Notarize(block) + | CertificateType::NotarizeFallback(block) + | CertificateType::Genesis(block) + | CertificateType::FinalizeFast(block) => Some(block), } } @@ -147,12 +146,12 @@ impl CertificateType { /// function. pub fn to_source_vote(self) -> Vote { match self { - Self::Notarize(slot, block_id) - | Self::FinalizeFast(slot, block_id) - | Self::NotarizeFallback(slot, block_id) => Vote::new_notarization_vote(slot, block_id), + Self::Notarize(block) | Self::FinalizeFast(block) | Self::NotarizeFallback(block) => { + Vote::new_notarization_vote(block) + } Self::Finalize(slot) => Vote::new_finalization_vote(slot), Self::Skip(slot) => Vote::new_skip_vote(slot), - Self::Genesis(slot, block_id) => Vote::new_genesis_vote(slot, block_id), + Self::Genesis(block) => Vote::new_genesis_vote(block), } } @@ -166,9 +165,9 @@ impl CertificateType { /// the verifier uses to check the single aggregate signature. pub fn to_source_votes(self) -> Option<(Vote, Vote)> { match self { - Self::NotarizeFallback(slot, block_id) => { - let vote1 = Vote::new_notarization_vote(slot, block_id); - let vote2 = Vote::new_notarization_fallback_vote(slot, block_id); + Self::NotarizeFallback(block) => { + let vote1 = Vote::new_notarization_vote(block); + let vote2 = Vote::new_notarization_fallback_vote(block); Some((vote1, vote2)) } Self::Skip(slot) => { @@ -187,14 +186,12 @@ impl CertificateType { /// Must be in sync with `Vote::to_cert_types` pub const fn limits_and_vote_types(&self) -> (Fraction, &'static [VoteType]) { match self { - CertificateType::Notarize(_, _) => { - (Fraction::from_percentage(60), &[VoteType::Notarize]) - } - CertificateType::NotarizeFallback(_, _) => ( + CertificateType::Notarize(_) => (Fraction::from_percentage(60), &[VoteType::Notarize]), + CertificateType::NotarizeFallback(_) => ( Fraction::from_percentage(60), &[VoteType::Notarize, VoteType::NotarizeFallback], ), - CertificateType::FinalizeFast(_, _) => { + CertificateType::FinalizeFast(_) => { (Fraction::from_percentage(80), &[VoteType::Notarize]) } CertificateType::Finalize(_) => (Fraction::from_percentage(60), &[VoteType::Finalize]), @@ -202,7 +199,7 @@ impl CertificateType { Fraction::from_percentage(60), &[VoteType::Skip, VoteType::SkipFallback], ), - CertificateType::Genesis(_, _) => (GENESIS_VOTE_THRESHOLD, &[VoteType::Genesis]), + CertificateType::Genesis(_) => (GENESIS_VOTE_THRESHOLD, &[VoteType::Genesis]), } } } diff --git a/votor-messages/src/consensus_message.rs b/votor-messages/src/consensus_message.rs index c29aed7659a..369902f940b 100644 --- a/votor-messages/src/consensus_message.rs +++ b/votor-messages/src/consensus_message.rs @@ -20,13 +20,39 @@ pod_wrapper! { /// The seed used to derive the BLS keypair pub const BLS_KEYPAIR_DERIVE_SEED: &[u8; 9] = b"alpenglow"; -/// Block, a (slot, hash) tuple -pub type Block = (Slot, Hash); +/// An alpenglow block +#[cfg_attr( + feature = "frozen-abi", + derive(AbiExample), + frozen_abi(digest = "xCqtGMfgy9TMmCDZP9o4BidVTPKfMWrLmqxpRDLYwtR") +)] +#[derive( + Clone, + Copy, + Debug, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + Default, + Serialize, + Deserialize, + SchemaWrite, + SchemaRead, +)] +pub struct Block { + /// The slot in the block. + pub slot: Slot, + /// The block_id of the block. + pub block_id: Hash, +} + /// A consensus vote. #[cfg_attr( feature = "frozen-abi", derive(AbiExample), - frozen_abi(digest = "5eorzdc18a1sNEUDLAKPgrHCqHmA8ssuTwKSGsZLwBqR") + frozen_abi(digest = "CTiXEk2aQbpf6TS6PNKcaTsGkLruDvAYsTLFhHKW2vsm") )] #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, SchemaWrite, SchemaRead)] pub struct VoteMessage { @@ -43,7 +69,7 @@ pub struct VoteMessage { #[cfg_attr( feature = "frozen-abi", derive(AbiExample, AbiEnumVisitor), - frozen_abi(digest = "FJKdeeziPmtNpqrV7Wk3tVeHS6CzWWLvkX3MdgYXoKwk") + frozen_abi(digest = "CbPatwRWz8NyUAj3HeAxAAWAWTxJnHGAfekLspUQpMHN") )] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, SchemaWrite, SchemaRead)] #[allow(clippy::large_enum_variant)] diff --git a/votor-messages/src/migration.rs b/votor-messages/src/migration.rs index 7d916c55485..94272a567f4 100644 --- a/votor-messages/src/migration.rs +++ b/votor-messages/src/migration.rs @@ -350,7 +350,10 @@ impl MigrationStatus { #[cfg(feature = "dev-context-only-utils")] pub fn post_migration_status() -> Self { let genesis_certificate = Certificate { - cert_type: CertificateType::Genesis(0, Hash::default()), + cert_type: CertificateType::Genesis(Block { + slot: 0, + block_id: Hash::default(), + }), signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), bitmap: vec![], }; @@ -362,15 +365,18 @@ impl MigrationStatus { /// Enable alpenglow for testing code #[cfg(feature = "dev-context-only-utils")] pub fn enable_alpenglow_for_tests(&self) { - let genesis_block = (0, Hash::new_unique()); + let genesis_block = Block { + slot: 0, + block_id: Hash::new_unique(), + }; self.record_feature_activation(0); self.set_genesis_block(genesis_block); self.set_genesis_certificate(Arc::new(Certificate { - cert_type: CertificateType::Genesis(genesis_block.0, genesis_block.1), + cert_type: CertificateType::Genesis(genesis_block), signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), bitmap: vec![], })); - assert_eq!(self.enable_alpenglow_during_startup(), genesis_block.0); + assert_eq!(self.enable_alpenglow_during_startup(), genesis_block.slot); } /// Initialize migration status based on feature flag activation and genesis certificate @@ -509,7 +515,7 @@ impl MigrationStatus { /// /// Should only be used during `Migration`, and transitions to `ReadyToEnable` if we have already /// received a genesis certificate and it matches. - pub fn set_genesis_block(&self, discovered_genesis_block @ (slot, _): Block) { + pub fn set_genesis_block(&self, discovered_genesis_block: Block) { let mut phase = self.phase.write().unwrap(); if phase.is_pre_feature_activation() { unreachable!( @@ -536,7 +542,7 @@ impl MigrationStatus { } assert!( - slot < *migration_slot, + discovered_genesis_block.slot < *migration_slot, "Attempting to set a genesis block that is past the migration start" ); warn!( @@ -548,18 +554,14 @@ impl MigrationStatus { let Some(genesis_cert) = genesis_cert else { return; }; - let CertificateType::Genesis(slot, block_id) = genesis_cert.cert_type else { + let CertificateType::Genesis(block) = genesis_cert.cert_type else { unreachable!("Programmer error invalid genesis certificate"); }; - if genesis_block - .as_ref() - .map(|b| *b != (slot, block_id)) - .unwrap_or(true) - { + if genesis_block.as_ref().map(|b| *b != block).unwrap_or(true) { panic!( "{}: We wish to cast a genesis vote on {discovered_genesis_block:?}, however we \ - have received a genesis certificate for ({slot}, {block_id}). This means there \ - is significant malicious activity causing two distinct forks to reach the \ + have received a genesis certificate for ({block:?}). This means there is \ + significant malicious activity causing two distinct forks to reach the \ {GENESIS_VOTE_THRESHOLD}. We cannot recover without operator intervention.", self.my_pubkey() ); @@ -594,29 +596,26 @@ impl MigrationStatus { return; }; - let CertificateType::Genesis(slot, block_id) = cert.cert_type else { + let CertificateType::Genesis(block) = cert.cert_type else { unreachable!("Programmer error adding invalid genesis certificate"); }; assert!( - slot < *migration_slot, + block.slot < *migration_slot, "Attempting to set a genesis certificate past the migration start" ); - warn!( - "{} Setting genesis cert for ({slot},{block_id:?})", - self.my_pubkey() - ); + warn!("{} Setting genesis cert for ({block:?})", self.my_pubkey()); *genesis_cert = Some(cert.clone()); let Some(genesis_block) = genesis_block else { return; }; - if *genesis_block != (slot, block_id) { + if *genesis_block != block { panic!( "{}: We cast a genesis vote on {genesis_block:?}, however we have received a \ - genesis certificate for ({slot}, {block_id}). This means there is significant \ - malicious activity causing two distinct forks to reach the \ - {GENESIS_VOTE_THRESHOLD}. We cannot recover without operator intervention.", + genesis certificate for ({block:?}). This means there is significant malicious \ + activity causing two distinct forks to reach the {GENESIS_VOTE_THRESHOLD}. We \ + cannot recover without operator intervention.", self.my_pubkey() ); } diff --git a/votor-messages/src/vote.rs b/votor-messages/src/vote.rs index ec3b2dbd017..9610de3b40b 100644 --- a/votor-messages/src/vote.rs +++ b/votor-messages/src/vote.rs @@ -1,5 +1,6 @@ //! Vote data types for use by clients use { + crate::consensus_message::Block, serde::{Deserialize, Serialize}, solana_clock::Slot, solana_hash::Hash, @@ -11,7 +12,7 @@ use { #[cfg_attr( feature = "frozen-abi", derive(AbiExample, AbiEnumVisitor), - frozen_abi(digest = "AgKoR2cpjUSVCW7Cpihob5nDiPcFt1PXmoPKWJg3zuSB") + frozen_abi(digest = "Fd13KXQMkc1mCJEoHwyXWkcewqBCdRcAiMhS7Aqe4sm1") )] #[derive( Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, SchemaWrite, SchemaRead, @@ -50,8 +51,8 @@ pub enum VoteType { impl Vote { /// Create a new notarization vote - pub fn new_notarization_vote(slot: Slot, block_id: Hash) -> Self { - Self::from(NotarizationVote { slot, block_id }) + pub fn new_notarization_vote(block: Block) -> Self { + Self::from(NotarizationVote { block }) } /// Create a new finalization vote @@ -65,8 +66,8 @@ impl Vote { } /// Create a new notarization fallback vote - pub fn new_notarization_fallback_vote(slot: Slot, block_id: Hash) -> Self { - Self::from(NotarizationFallbackVote { slot, block_id }) + pub fn new_notarization_fallback_vote(block: Block) -> Self { + Self::from(NotarizationFallbackVote { block }) } /// Create a new skip fallback vote @@ -75,28 +76,28 @@ impl Vote { } /// Create a new genesis vote - pub fn new_genesis_vote(slot: Slot, block_id: Hash) -> Self { - Self::from(GenesisVote { slot, block_id }) + pub fn new_genesis_vote(block: Block) -> Self { + Self::from(GenesisVote { block }) } /// The slot which was voted for pub fn slot(&self) -> Slot { match self { - Self::Notarize(vote) => vote.slot, + Self::Notarize(vote) => vote.block.slot, Self::Finalize(vote) => vote.slot, Self::Skip(vote) => vote.slot, - Self::NotarizeFallback(vote) => vote.slot, + Self::NotarizeFallback(vote) => vote.block.slot, Self::SkipFallback(vote) => vote.slot, - Self::Genesis(vote) => vote.slot, + Self::Genesis(vote) => vote.block.slot, } } /// The block id associated with the block which was voted for pub fn block_id(&self) -> Option<&Hash> { match self { - Self::Notarize(vote) => Some(&vote.block_id), - Self::NotarizeFallback(vote) => Some(&vote.block_id), - Self::Genesis(vote) => Some(&vote.block_id), + Self::Notarize(vote) => Some(&vote.block.block_id), + Self::NotarizeFallback(vote) => Some(&vote.block.block_id), + Self::Genesis(vote) => Some(&vote.block.block_id), Self::Finalize(_) | Self::Skip(_) | Self::SkipFallback(_) => None, } } @@ -184,7 +185,7 @@ impl From for Vote { #[cfg_attr( feature = "frozen-abi", derive(AbiExample), - frozen_abi(digest = "5AdwChAjsj5QUXLdpDnGGK2L2nA8y8EajVXi6jsmTv1m") + frozen_abi(digest = "F9veHPmSwMyrYNSVuBLcvLGYSLgc7voTD3kUhxUHUTRU") )] #[derive( Clone, @@ -200,10 +201,8 @@ impl From for Vote { SchemaRead, )] pub struct NotarizationVote { - /// The slot this vote is cast for. - pub slot: Slot, - /// The block id this vote is for. - pub block_id: Hash, + /// The block this vote is cast for + pub block: Block, } /// A finalization vote @@ -260,7 +259,7 @@ pub struct SkipVote { #[cfg_attr( feature = "frozen-abi", derive(AbiExample), - frozen_abi(digest = "7j5ZPwwyz1FaG3fpyQv5PVnQXicdSmqSk8NvqzkG1Eqz") + frozen_abi(digest = "6UW4zutbRvyri4z8WAyKx8aUZkJrZX4XoiqC4XMUnUZk") )] #[derive( Clone, @@ -276,10 +275,8 @@ pub struct SkipVote { SchemaRead, )] pub struct NotarizationFallbackVote { - /// The slot this vote is cast for. - pub slot: Slot, - /// The block id this vote is for. - pub block_id: Hash, + /// The block this vote is cast for + pub block: Block, } /// A skip fallback vote @@ -310,7 +307,7 @@ pub struct SkipFallbackVote { #[cfg_attr( feature = "frozen-abi", derive(AbiExample), - frozen_abi(digest = "2JAiHmnnKHCzhkyCY3Bej6rAaVkMHsXgRcz1TPCNqAJ9") + frozen_abi(digest = "8ty2gETfpyVGPNMYrEFS1YXeDRprfZaisSAmJwoAYusb") )] #[derive( Clone, @@ -326,8 +323,6 @@ pub struct SkipFallbackVote { SchemaRead, )] pub struct GenesisVote { - /// The slot this vote is cast for. - pub slot: Slot, - /// The block id this vote is for. - pub block_id: Hash, + /// The block this vote is cast for + pub block: Block, } diff --git a/votor/src/common.rs b/votor/src/common.rs index cecb01253ca..c5620b23b31 100644 --- a/votor/src/common.rs +++ b/votor/src/common.rs @@ -41,17 +41,17 @@ pub const fn conflicting_types(vote_type: VoteType) -> &'static [VoteType] { pub fn vote_to_cert_types(vote: &Vote) -> Vec { match vote { Vote::Notarize(vote) => vec![ - CertificateType::Notarize(vote.slot, vote.block_id), - CertificateType::NotarizeFallback(vote.slot, vote.block_id), - CertificateType::FinalizeFast(vote.slot, vote.block_id), + CertificateType::Notarize(vote.block), + CertificateType::NotarizeFallback(vote.block), + CertificateType::FinalizeFast(vote.block), ], Vote::NotarizeFallback(vote) => { - vec![CertificateType::NotarizeFallback(vote.slot, vote.block_id)] + vec![CertificateType::NotarizeFallback(vote.block)] } Vote::Finalize(vote) => vec![CertificateType::Finalize(vote.slot)], Vote::Skip(vote) => vec![CertificateType::Skip(vote.slot)], Vote::SkipFallback(vote) => vec![CertificateType::Skip(vote.slot)], - Vote::Genesis(vote) => vec![CertificateType::Genesis(vote.slot, vote.block_id)], + Vote::Genesis(vote) => vec![CertificateType::Genesis(vote.block)], } } diff --git a/votor/src/consensus_pool.rs b/votor/src/consensus_pool.rs index 366036b8954..3464b480cc5 100644 --- a/votor/src/consensus_pool.rs +++ b/votor/src/consensus_pool.rs @@ -306,22 +306,22 @@ impl ConsensusPool { ); self.completed_certificates.insert(cert_type, cert.clone()); match cert_type { - CertificateType::NotarizeFallback(slot, block_id) => { - events.push(VotorEvent::BlockNotarFallback((slot, block_id))); + CertificateType::NotarizeFallback(block) => { + events.push(VotorEvent::BlockNotarFallback(block)); self.parent_ready_tracker - .add_new_notar_fallback_or_stronger((slot, block_id), events); + .add_new_notar_fallback_or_stronger(block, events); } CertificateType::Skip(slot) => self.parent_ready_tracker.add_new_skip(slot, events), - CertificateType::Notarize(slot, block_id) => { - events.push(VotorEvent::BlockNotarized((slot, block_id))); + CertificateType::Notarize(block) => { + events.push(VotorEvent::BlockNotarized(block)); self.parent_ready_tracker - .add_new_notar_fallback_or_stronger((slot, block_id), events); + .add_new_notar_fallback_or_stronger(block, events); - if let Some(finalize_cert) = self.get_finalize_cert(slot) { + if let Some(finalize_cert) = self.get_finalize_cert(block.slot) { // It's fine to set FastFinalization to false here, because // we will report correctly as long as we have FastFinalization cert. - events.push(VotorEvent::Finalized((slot, block_id), false)); - if self.highest_finalized_slot().is_none_or(|s| s < slot) { + events.push(VotorEvent::Finalized(block, false)); + if self.highest_finalized_slot().is_none_or(|s| s < block.slot) { self.highest_finalized_slot_cert = Some(ValidatedBlockFinalizationCert::from_validated_slow( Arc::unwrap_or_clone(finalize_cert.clone()), @@ -333,8 +333,8 @@ impl ConsensusPool { } CertificateType::Finalize(slot) => { if let Some(notarize_cert) = self.get_notarize_cert(slot) { - let (_s, block_id) = notarize_cert.cert_type.to_block().unwrap(); - events.push(VotorEvent::Finalized((slot, block_id), false)); + let block = notarize_cert.cert_type.to_block().unwrap(); + events.push(VotorEvent::Finalized(block, false)); if self.highest_finalized_slot().is_none_or(|s| s < slot) { self.highest_finalized_slot_cert = Some(ValidatedBlockFinalizationCert::from_validated_slow( @@ -345,12 +345,15 @@ impl ConsensusPool { } } } - CertificateType::FinalizeFast(slot, block_id) => { - events.push(VotorEvent::Finalized((slot, block_id), true)); + CertificateType::FinalizeFast(block) => { + events.push(VotorEvent::Finalized(block, true)); self.parent_ready_tracker - .add_new_notar_fallback_or_stronger((slot, block_id), events); + .add_new_notar_fallback_or_stronger(block, events); // Use <= for FastFinalize since it supersedes standard finalization at the same slot - if self.highest_finalized_slot().is_none_or(|s| s <= slot) { + if self + .highest_finalized_slot() + .is_none_or(|s| s <= block.slot) + { self.highest_finalized_slot_cert = Some(ValidatedBlockFinalizationCert::from_validated_fast( Arc::unwrap_or_clone(cert), @@ -358,11 +361,11 @@ impl ConsensusPool { )); } } - CertificateType::Genesis(slot, block_id) => { + CertificateType::Genesis(block) => { self.migration_status.set_genesis_certificate(cert); // The genesis block is automatically certified self.parent_ready_tracker - .add_new_notar_fallback_or_stronger((slot, block_id), events); + .add_new_notar_fallback_or_stronger(block, events); } } } @@ -491,7 +494,7 @@ impl ConsensusPool { self.completed_certificates .iter() .find_map(|(cert_type, cert)| match cert_type { - CertificateType::Notarize(s, _) if slot == *s => Some(cert.clone()), + CertificateType::Notarize(block) if slot == block.slot => Some(cert.clone()), _ => None, }) } @@ -513,12 +516,11 @@ impl ConsensusPool { self.completed_certificates .keys() .filter_map(|cert_type| match cert_type { - CertificateType::Notarize(s, _) => Some(s), - CertificateType::NotarizeFallback(s, _) => Some(s), + CertificateType::Notarize(block) => Some(block.slot), + CertificateType::NotarizeFallback(block) => Some(block.slot), _ => None, }) .max() - .copied() .unwrap_or(0) } @@ -535,12 +537,16 @@ impl ConsensusPool { .unwrap_or(0) } - /// Checks if any block in the slot `s` is finalized + /// Checks if any block in the `slot` is finalized #[cfg(test)] fn is_finalized(&self, slot: Slot) -> bool { - self.completed_certificates.keys().any(|cert_type| { - matches!(cert_type, CertificateType::Finalize(s) | CertificateType::FinalizeFast(s, _) if *s == slot) - }) + self.completed_certificates + .keys() + .any(|cert_type| match cert_type { + CertificateType::Finalize(s) => s == &slot, + CertificateType::FinalizeFast(block) => block.slot == slot, + _ => false, + }) } /// Checks if the any block in slot `slot` has received a `NotarizeFallback` certificate, if so return @@ -548,7 +554,7 @@ impl ConsensusPool { #[cfg(test)] fn slot_has_notarized_fallback(&self, slot: Slot) -> bool { self.completed_certificates.iter().any( - |(cert_type, _)| matches!(cert_type, CertificateType::NotarizeFallback(s,_) if *s == slot), + |(cert_type, _)| matches!(cert_type, CertificateType::NotarizeFallback(block) if block.slot == slot), ) } @@ -629,7 +635,7 @@ impl ConsensusPool { self.slot_stake_counters_map = self.slot_stake_counters_map.split_off(&root_slot); self.parent_ready_tracker.set_root(root_slot); self.pending_safe_to_notar - .retain(|(slot, _)| *slot >= root_slot); + .retain(|block| block.slot >= root_slot); self.last_pruned_slot = root_slot; } @@ -654,13 +660,13 @@ impl ConsensusPool { (Ordering::Greater, _, _) | ( Ordering::Equal, - CertificateType::Finalize(_) | CertificateType::Notarize(_, _), + CertificateType::Finalize(_) | CertificateType::Notarize(_), false, ) - | (Ordering::Equal, CertificateType::FinalizeFast(_, _), true) => { + | (Ordering::Equal, CertificateType::FinalizeFast(_), true) => { Some(cert.clone()) } - (Ordering::Equal, CertificateType::FinalizeFast(_, _), false) => { + (Ordering::Equal, CertificateType::FinalizeFast(_), false) => { panic!("Should not happen while certificate pool is single threaded") } _ => None, @@ -733,7 +739,10 @@ mod tests { let root_bank = bank_forks.read().unwrap().root_bank(); let generated_cert_types = Arc::new(GeneratedCertTypes::default()); let cluster_info = get_cluster_info(Keypair::new()); - let root_block = (root_bank.slot(), root_bank.block_id().unwrap_or_default()); + let root_block = Block { + slot: root_bank.slot(), + block_id: root_bank.block_id().unwrap_or_default(), + }; let initial_parent_ready = (root_bank.slot().checked_add(1).unwrap(), root_block); Self { validators: validator_keypairs, @@ -764,9 +773,11 @@ mod tests { self.add_message(dummy_vote_message(&self.validators, &vote, rank)); } match vote { - Vote::Notarize(vote) => assert_eq!(self.pool.highest_notarized_slot(), vote.slot), + Vote::Notarize(vote) => { + assert_eq!(self.pool.highest_notarized_slot(), vote.block.slot) + } Vote::NotarizeFallback(vote) => { - assert_eq!(self.pool.highest_notarized_slot(), vote.slot) + assert_eq!(self.pool.highest_notarized_slot(), vote.block.slot) } Vote::Skip(vote) => assert_eq!(self.pool.highest_skip_slot(), vote.slot), Vote::SkipFallback(vote) => assert_eq!(self.pool.highest_skip_slot(), vote.slot), @@ -978,7 +989,10 @@ mod tests { ctx.bank_forks.write().unwrap().insert(bank); // Notarize slot 5 - ctx.add_certificate(Vote::new_notarization_vote(5, Hash::default())); + ctx.add_certificate(Vote::new_notarization_vote(Block { + slot: 5, + block_id: Hash::default(), + })); assert_eq!(ctx.pool.highest_notarized_slot(), 5); // No skip certificate for 6-10 @@ -999,7 +1013,10 @@ mod tests { let mut ctx = TestContext::new(); // Notarize slot 5 - ctx.add_certificate(Vote::new_notarization_vote(5, Hash::default())); + ctx.add_certificate(Vote::new_notarization_vote(Block { + slot: 5, + block_id: Hash::default(), + })); assert_eq!(ctx.pool.highest_notarized_slot(), 5); // Leader slot is just +1 from notarized slot (no skip needed) @@ -1018,7 +1035,10 @@ mod tests { let mut ctx = TestContext::new(); // Notarize slot 5 - ctx.add_certificate(Vote::new_notarization_vote(5, Hash::default())); + ctx.add_certificate(Vote::new_notarization_vote(Block { + slot: 5, + block_id: Hash::default(), + })); assert_eq!(ctx.pool.highest_notarized_slot(), 5); // Valid skip certificate for 6-9 exists @@ -1041,7 +1061,10 @@ mod tests { let mut ctx = TestContext::new(); // Notarize slot 5 - ctx.add_certificate(Vote::new_notarization_vote(5, Hash::default())); + ctx.add_certificate(Vote::new_notarization_vote(Block { + slot: 5, + block_id: Hash::default(), + })); assert_eq!(ctx.pool.highest_notarized_slot(), 5); // Valid skip certificate for 4-9 exists @@ -1062,8 +1085,8 @@ mod tests { } #[test_case(Vote::new_finalization_vote(5), vec![CertificateType::Finalize(5)])] - #[test_case(Vote::new_notarization_vote(6, Hash::default()), vec![CertificateType::Notarize(6, Hash::default()), CertificateType::NotarizeFallback(6, Hash::default())])] - #[test_case(Vote::new_notarization_fallback_vote(7, Hash::default()), vec![CertificateType::NotarizeFallback(7, Hash::default())])] + #[test_case(Vote::new_notarization_vote(Block { slot: 6, block_id: Hash::default() }), vec![CertificateType::Notarize(Block { slot: 6, block_id: Hash::default() }), CertificateType::NotarizeFallback(Block { slot: 6, block_id: Hash::default() })])] + #[test_case(Vote::new_notarization_fallback_vote(Block { slot: 7, block_id: Hash::default() }), vec![CertificateType::NotarizeFallback(Block { slot: 7, block_id: Hash::default() })])] #[test_case(Vote::new_skip_vote(8), vec![CertificateType::Skip(8)])] #[test_case(Vote::new_skip_fallback_vote(9), vec![CertificateType::Skip(9)])] fn test_add_vote_and_create_new_certificate_with_types( @@ -1077,7 +1100,10 @@ mod tests { // because finalization requires both Finalize and Notarize certificates if vote.is_finalize() { let notarize_cert = Certificate { - cert_type: CertificateType::Notarize(vote.slot(), Hash::default()), + cert_type: CertificateType::Notarize(Block { + slot: vote.slot(), + block_id: Hash::default(), + }), signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), bitmap: dummy_bitmap(), }; @@ -1130,16 +1156,16 @@ mod tests { #[test_case(CertificateType::Finalize(5), Vote::new_finalization_vote(5))] #[test_case( - CertificateType::FinalizeFast(6, Hash::default()), - Vote::new_notarization_vote(6, Hash::default()) + CertificateType::FinalizeFast(Block { slot: 6, block_id: Hash::default() }), + Vote::new_notarization_vote(Block { slot: 6, block_id: Hash::default() }) )] #[test_case( - CertificateType::Notarize(6, Hash::default()), - Vote::new_notarization_vote(6, Hash::default()) + CertificateType::Notarize(Block { slot: 6, block_id: Hash::default() }), + Vote::new_notarization_vote(Block { slot: 6, block_id: Hash::default() }) )] #[test_case( - CertificateType::NotarizeFallback(7, Hash::default()), - Vote::new_notarization_fallback_vote(7, Hash::default()) + CertificateType::NotarizeFallback(Block { slot: 7, block_id: Hash::default() }), + Vote::new_notarization_fallback_vote(Block { slot: 7, block_id: Hash::default() }) )] #[test_case(CertificateType::Skip(8), Vote::new_skip_vote(8))] fn test_add_certificate_with_types(cert_type: CertificateType, vote: Vote) { @@ -1155,7 +1181,10 @@ mod tests { // because finalization requires both certificates to be present if matches!(cert_type, CertificateType::Finalize(slot) if slot == 5) { let notarize_cert = Certificate { - cert_type: CertificateType::Notarize(5, Hash::default()), + cert_type: CertificateType::Notarize(Block { + slot: 5, + block_id: Hash::default(), + }), signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), bitmap: dummy_bitmap(), }; @@ -1176,7 +1205,7 @@ mod tests { .unwrap(); // Because this is the first certificate of this type, it should be sent out. if matches!(cert_type, CertificateType::Finalize(_)) - || matches!(cert_type, CertificateType::FinalizeFast(_, _)) + || matches!(cert_type, CertificateType::FinalizeFast(_)) { assert_eq!(new_finalized_slot, Some(cert_type.slot())); } else { @@ -1531,7 +1560,7 @@ mod tests { // 40% notarized, should succeed for rank in 1..5 { - let vote = Vote::new_notarization_vote(slot, block_id); + let vote = Vote::new_notarization_vote(Block { slot, block_id }); ctx.pool .add_message( &bank, @@ -1542,9 +1571,8 @@ mod tests { .unwrap(); } assert_eq!(new_events.len(), 1); - if let VotorEvent::SafeToNotar((event_slot, event_block_id)) = new_events[0] { - assert_eq!(block_id, event_block_id); - assert_eq!(slot, event_slot); + if let VotorEvent::SafeToNotar(block) = &new_events[0] { + assert_eq!(*block, Block { slot, block_id }); } else { panic!("Expected SafeToNotar event"); } @@ -1556,7 +1584,7 @@ mod tests { // Add 20% notarize, but no vote from myself, should fail for rank in 1..3 { - let vote = Vote::new_notarization_vote(slot, block_id); + let vote = Vote::new_notarization_vote(Block { slot, block_id }); ctx.pool .add_message( &bank, @@ -1569,7 +1597,10 @@ mod tests { assert!(new_events.is_empty()); // Add a notarize from myself for some other block, but still not enough notar or skip, should fail. - let vote = Vote::new_notarization_vote(slot, Hash::new_unique()); + let vote = Vote::new_notarization_vote(Block { + slot, + block_id: Hash::new_unique(), + }); ctx.pool .add_message( &bank, @@ -1599,9 +1630,8 @@ mod tests { } else { panic!("Expected SafeToSkip event"); } - if let VotorEvent::SafeToNotar((event_slot, event_block_id)) = new_events[1] { - assert_eq!(block_id, event_block_id); - assert_eq!(slot, event_slot); + if let VotorEvent::SafeToNotar(block) = &new_events[1] { + assert_eq!(*block, Block { slot, block_id }); } else { panic!("Expected SafeToNotar event"); } @@ -1611,7 +1641,10 @@ mod tests { // but not on the same block_id because we already sent the event let duplicate_block_id = Hash::new_unique(); for rank in 7..9 { - let vote = Vote::new_notarization_vote(slot, duplicate_block_id); + let vote = Vote::new_notarization_vote(Block { + slot, + block_id: duplicate_block_id, + }); ctx.pool .add_message( &bank, @@ -1623,9 +1656,14 @@ mod tests { } assert_eq!(new_events.len(), 1); - if let VotorEvent::SafeToNotar((event_slot, event_block_id)) = new_events[0] { - assert_eq!(duplicate_block_id, event_block_id); - assert_eq!(slot, event_slot); + if let VotorEvent::SafeToNotar(block) = &new_events[0] { + assert_eq!( + *block, + Block { + slot, + block_id: duplicate_block_id + } + ); } else { panic!("Expected SafeToNotar event"); } @@ -1641,7 +1679,7 @@ mod tests { // Add a notarize from myself. let block_id = Hash::new_unique(); - let vote = Vote::new_notarization_vote(2, block_id); + let vote = Vote::new_notarization_vote(Block { slot: 2, block_id }); ctx.pool .add_message( &bank, @@ -1672,7 +1710,7 @@ mod tests { } new_events.clear(); // Add 10% more notarize, will not send new SafeToSkip because the event was already sent - let vote = Vote::new_notarization_vote(2, block_id); + let vote = Vote::new_notarization_vote(Block { slot: 2, block_id }); ctx.pool .add_message( &bank, @@ -1686,14 +1724,21 @@ mod tests { fn create_new_vote(vote_type: VoteType, slot: Slot) -> Vote { match vote_type { - VoteType::Notarize => Vote::new_notarization_vote(slot, Hash::default()), - VoteType::NotarizeFallback => { - Vote::new_notarization_fallback_vote(slot, Hash::default()) - } + VoteType::Notarize => Vote::new_notarization_vote(Block { + slot, + block_id: Hash::default(), + }), + VoteType::NotarizeFallback => Vote::new_notarization_fallback_vote(Block { + slot, + block_id: Hash::default(), + }), VoteType::Skip => Vote::new_skip_vote(slot), VoteType::SkipFallback => Vote::new_skip_fallback_vote(slot), VoteType::Finalize => Vote::new_finalization_vote(slot), - VoteType::Genesis => Vote::new_genesis_vote(slot, Hash::default()), + VoteType::Genesis => Vote::new_genesis_vote(Block { + slot, + block_id: Hash::default(), + }), } } @@ -1769,7 +1814,10 @@ mod tests { ) .unwrap(); let cert_2 = Certificate { - cert_type: CertificateType::FinalizeFast(2, Hash::new_unique()), + cert_type: CertificateType::FinalizeFast(Block { + slot: 2, + block_id: Hash::new_unique(), + }), signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), bitmap: dummy_bitmap(), }; @@ -1808,7 +1856,10 @@ mod tests { ); // Send a cert on slot 2, it should be rejected - let cert_type = CertificateType::Notarize(2, Hash::new_unique()); + let cert_type = CertificateType::Notarize(Block { + slot: 2, + block_id: Hash::new_unique(), + }); let cert = ConsensusMessage::Certificate(Certificate { cert_type, signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), @@ -1830,7 +1881,10 @@ mod tests { // Add notar-fallback cert on 3 and finalize cert on 4 let cert_3 = Certificate { - cert_type: CertificateType::NotarizeFallback(3, Hash::new_unique()), + cert_type: CertificateType::NotarizeFallback(Block { + slot: 3, + block_id: Hash::new_unique(), + }), signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), bitmap: dummy_bitmap(), }; @@ -1845,13 +1899,16 @@ mod tests { let certs = ctx.pool.get_certs_for_standstill(); assert_eq!(certs.len(), 2); assert!(certs.iter().any(|cert| cert.cert_type.slot() == 3 - && matches!(cert.cert_type, CertificateType::NotarizeFallback(_, _)))); + && matches!(cert.cert_type, CertificateType::NotarizeFallback(_)))); assert!(certs.iter().any(|cert| cert.cert_type.slot() == 4 && matches!(cert.cert_type, CertificateType::Finalize(_)))); // Add Notarize cert on 5 let cert_5 = Certificate { - cert_type: CertificateType::Notarize(5, Hash::new_unique()), + cert_type: CertificateType::Notarize(Block { + slot: 5, + block_id: Hash::new_unique(), + }), signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), bitmap: dummy_bitmap(), }; @@ -1867,7 +1924,10 @@ mod tests { // Add FinalizeFast cert on 5 let cert_5 = Certificate { - cert_type: CertificateType::FinalizeFast(5, Hash::new_unique()), + cert_type: CertificateType::FinalizeFast(Block { + slot: 5, + block_id: Hash::new_unique(), + }), signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), bitmap: dummy_bitmap(), }; @@ -1877,12 +1937,15 @@ mod tests { assert_eq!(certs.len(), 1); assert!( certs[0].cert_type.slot() == 5 - && matches!(certs[0].cert_type, CertificateType::FinalizeFast(_, _)) + && matches!(certs[0].cert_type, CertificateType::FinalizeFast(_)) ); // Now add Notarize cert on 6 let cert_6 = Certificate { - cert_type: CertificateType::Notarize(6, Hash::new_unique()), + cert_type: CertificateType::Notarize(Block { + slot: 6, + block_id: Hash::new_unique(), + }), signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), bitmap: dummy_bitmap(), }; @@ -1891,9 +1954,9 @@ mod tests { let certs = ctx.pool.get_certs_for_standstill(); assert_eq!(certs.len(), 2); assert!(certs.iter().any(|cert| cert.cert_type.slot() == 5 - && matches!(cert.cert_type, CertificateType::FinalizeFast(_, _)))); + && matches!(cert.cert_type, CertificateType::FinalizeFast(_)))); assert!(certs.iter().any(|cert| cert.cert_type.slot() == 6 - && matches!(cert.cert_type, CertificateType::Notarize(_, _)))); + && matches!(cert.cert_type, CertificateType::Notarize(_)))); // Add another Finalize cert on 6 let cert_6_finalize = Certificate { @@ -1904,7 +1967,10 @@ mod tests { ctx.add_message(ConsensusMessage::Certificate(cert_6_finalize)); // Add a NotarizeFallback cert on 6 let cert_6_notarize_fallback = Certificate { - cert_type: CertificateType::NotarizeFallback(6, Hash::new_unique()), + cert_type: CertificateType::NotarizeFallback(Block { + slot: 6, + block_id: Hash::new_unique(), + }), signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), bitmap: dummy_bitmap(), }; @@ -1916,7 +1982,7 @@ mod tests { assert!(certs.iter().any(|cert| cert.cert_type.slot() == 6 && matches!(cert.cert_type, CertificateType::Finalize(_)))); assert!(certs.iter().any(|cert| cert.cert_type.slot() == 6 - && matches!(cert.cert_type, CertificateType::Notarize(_, _)))); + && matches!(cert.cert_type, CertificateType::Notarize(_)))); // Add another skip on 7 let cert_7 = Certificate { @@ -1931,7 +1997,7 @@ mod tests { assert!(certs.iter().any(|cert| cert.cert_type.slot() == 6 && matches!(cert.cert_type, CertificateType::Finalize(_)))); assert!(certs.iter().any(|cert| cert.cert_type.slot() == 6 - && matches!(cert.cert_type, CertificateType::Notarize(_, _)))); + && matches!(cert.cert_type, CertificateType::Notarize(_)))); assert!( certs.iter().any(|cert| cert.cert_type.slot() == 7 && matches!(cert.cert_type, CertificateType::Skip(_))) @@ -1945,7 +2011,10 @@ mod tests { }; ctx.add_message(ConsensusMessage::Certificate(cert_8_finalize)); let cert_8_notarize = Certificate { - cert_type: CertificateType::Notarize(8, Hash::new_unique()), + cert_type: CertificateType::Notarize(Block { + slot: 8, + block_id: Hash::new_unique(), + }), signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), bitmap: dummy_bitmap(), }; @@ -1957,7 +2026,7 @@ mod tests { assert!(certs.iter().any(|cert| cert.cert_type.slot() == 8 && matches!(cert.cert_type, CertificateType::Finalize(_)))); assert!(certs.iter().any(|cert| cert.cert_type.slot() == 8 - && matches!(cert.cert_type, CertificateType::Notarize(_, _)))); + && matches!(cert.cert_type, CertificateType::Notarize(_)))); } #[test] @@ -1970,7 +2039,10 @@ mod tests { let hash = Hash::new_unique(); for slot in 1..=3 { let cert = Certificate { - cert_type: CertificateType::Notarize(slot, hash), + cert_type: CertificateType::Notarize(Block { + slot, + block_id: hash, + }), signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), bitmap: dummy_bitmap(), }; @@ -1990,15 +2062,18 @@ mod tests { .iter() .any(|event| matches!(event, VotorEvent::ParentReady { slot: 4, - parent_block: (3, h) - } if h == &hash)) + parent_block + } if parent_block == &Block { slot: 3, block_id: hash })) ); events.clear(); // Also works if we add FinalizeFast for slot 4 to 7 for slot in 4..=7 { let cert = Certificate { - cert_type: CertificateType::FinalizeFast(slot, hash), + cert_type: CertificateType::FinalizeFast(Block { + slot, + block_id: hash, + }), signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), bitmap: dummy_bitmap(), }; @@ -2018,22 +2093,28 @@ mod tests { .iter() .any(|event| matches!(event, VotorEvent::ParentReady { slot: 8, - parent_block: (7, h) - } if h == &hash)) + parent_block + } if parent_block == &Block { slot: 7, block_id: hash })) ); events.clear(); // NotarizeFallback on slot 8 to 10 and FinalizeFast on slot 11 for slot in 8..=10 { let cert = Certificate { - cert_type: CertificateType::NotarizeFallback(slot, hash), + cert_type: CertificateType::NotarizeFallback(Block { + slot, + block_id: hash, + }), signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), bitmap: dummy_bitmap(), }; ctx.add_message(ConsensusMessage::Certificate(cert)); } let cert = Certificate { - cert_type: CertificateType::FinalizeFast(11, hash), + cert_type: CertificateType::FinalizeFast(Block { + slot: 11, + block_id: hash, + }), signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), bitmap: dummy_bitmap(), }; @@ -2053,8 +2134,8 @@ mod tests { .iter() .any(|event| matches!(event, VotorEvent::ParentReady { slot: 12, - parent_block: (11, h) - } if h == &hash)) + parent_block + } if parent_block == &Block { slot: 11, block_id: hash })) ); } @@ -2062,7 +2143,10 @@ mod tests { fn test_vote_message_signature_verification() { let ctx = TestContext::new(); let rank_to_test = 3; - let vote = Vote::new_notarization_vote(42, Hash::new_unique()); + let vote = Vote::new_notarization_vote(Block { + slot: 42, + block_id: Hash::new_unique(), + }); let consensus_message = dummy_vote_message(&ctx.validators, &vote, rank_to_test); let ConsensusMessage::Vote(vote_message) = consensus_message else { @@ -2086,7 +2170,10 @@ mod tests { let mut ctx = TestContext::new(); let slot = ctx.bank_forks.read().unwrap().root_bank().slot() + 1; let hash = Hash::default(); - let cert_type = CertificateType::NotarizeFallback(slot, hash); + let cert_type = CertificateType::NotarizeFallback(Block { + slot, + block_id: hash, + }); let cert = Certificate { cert_type, signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), diff --git a/votor/src/consensus_pool/certificate_builder.rs b/votor/src/consensus_pool/certificate_builder.rs index 2d8c13afbd6..b2721d600ec 100644 --- a/votor/src/consensus_pool/certificate_builder.rs +++ b/votor/src/consensus_pool/certificate_builder.rs @@ -111,13 +111,11 @@ impl BuilderType { /// Creates a new instance of [`BuilderType`]. fn new(cert_type: &CertificateType) -> Self { match cert_type { - CertificateType::Skip(_) | CertificateType::NotarizeFallback(_, _) => { - Self::DoubleVote { - signature: SignatureProjective::identity(), - bitmap0: default_bitvec(), - bitmap1: None, - } - } + CertificateType::Skip(_) | CertificateType::NotarizeFallback(_) => Self::DoubleVote { + signature: SignatureProjective::identity(), + bitmap0: default_bitvec(), + bitmap1: None, + }, _ => Self::SingleVote { signature: SignatureProjective::identity(), bitmap: default_bitvec(), @@ -215,7 +213,9 @@ mod tests { use { super::*, agave_votor_messages::{ - certificate::CertificateType, consensus_message::VoteMessage, vote::Vote, + certificate::CertificateType, + consensus_message::{Block, VoteMessage}, + vote::Vote, }, solana_bls_signatures::{ BLS_SIGNATURE_AFFINE_SIZE, Keypair as BLSKeypair, PreparedHashedMessage, @@ -228,12 +228,15 @@ mod tests { #[test] fn test_normal_build() { - let hash = Hash::new_unique(); - let cert_type = CertificateType::NotarizeFallback(1, hash); + let block = Block { + slot: 1, + block_id: Hash::new_unique(), + }; + let cert_type = CertificateType::NotarizeFallback(block); let mut builder = CertificateBuilder::new(cert_type); // Test building the certificate from Notarize and NotarizeFallback votes // Create Notarize on validator 1, 4, 6 - let vote = Vote::new_notarization_vote(1, hash); + let vote = Vote::new_notarization_vote(block); let rank_1 = [1, 4, 6]; let messages_1 = rank_1 .iter() @@ -251,7 +254,7 @@ mod tests { .aggregate(&messages_1) .expect("Failed to aggregate notarization votes"); // Create NotarizeFallback on validator 2, 3, 5, 7 - let vote = Vote::new_notarization_fallback_vote(1, hash); + let vote = Vote::new_notarization_fallback_vote(block); let rank_2 = [2, 3, 5, 7]; let messages_2 = rank_2 .iter() @@ -334,13 +337,16 @@ mod tests { #[test] fn test_builder_with_errors() { - let hash = Hash::new_unique(); - let cert_type = CertificateType::NotarizeFallback(1, hash); + let block = Block { + slot: 1, + block_id: Hash::new_unique(), + }; + let cert_type = CertificateType::NotarizeFallback(block); let mut builder = CertificateBuilder::new(cert_type); // Test with a rank that exceeds the maximum allowed - let vote = Vote::new_notarization_vote(1, hash); - let vote2 = Vote::new_notarization_fallback_vote(1, hash); + let vote = Vote::new_notarization_vote(block); + let vote2 = Vote::new_notarization_fallback_vote(block); let rank_out_of_bounds = MAXIMUM_VALIDATORS.saturating_add(1); // Exceeds MAXIMUM_VALIDATORS let keypair = BLSKeypair::new(); let signature = keypair.sign(b"fake_vote_message"); @@ -393,16 +399,18 @@ mod tests { #[test] fn test_certificate_verification_base2_encoding() { - let slot = 10; - let hash = Hash::new_unique(); - let cert_type = CertificateType::Notarize(slot, hash); + let block = Block { + slot: 10, + block_id: Hash::new_unique(), + }; + let cert_type = CertificateType::Notarize(block); // 1. Setup: Create keypairs and a single vote object. // All validators will sign the same message, resulting in a single bitmap. let num_validators = 5; let mut keypairs = Vec::new(); let mut vote_messages = Vec::new(); - let vote = Vote::new_notarization_vote(slot, hash); + let vote = Vote::new_notarization_vote(block); let serialized_vote = wincode::serialize(&vote).unwrap(); for i in 0..num_validators { @@ -434,17 +442,19 @@ mod tests { #[test] fn test_certificate_verification_base3_encoding() { - let slot = 20; - let hash = Hash::new_unique(); + let block = Block { + slot: 20, + block_id: Hash::new_unique(), + }; // A NotarizeFallback certificate can be composed of both Notarize and NotarizeFallback // votes. - let cert_type = CertificateType::NotarizeFallback(slot, hash); + let cert_type = CertificateType::NotarizeFallback(block); // 1. Setup: Create two groups of validators signing two different vote types. let mut all_vote_messages = Vec::new(); let mut all_pubkeys = Vec::new(); // Group 1: Signs a Notarize vote. - let notarize_vote = Vote::new_notarization_vote(slot, hash); + let notarize_vote = Vote::new_notarization_vote(block); let serialized_notarize_vote = wincode::serialize(¬arize_vote).unwrap(); for i in 0..3 { let keypair = BLSKeypair::new(); @@ -458,7 +468,7 @@ mod tests { } // Group 2: Signs a NotarizeFallback vote. - let notarize_fallback_vote = Vote::new_notarization_fallback_vote(slot, hash); + let notarize_fallback_vote = Vote::new_notarization_fallback_vote(block); let serialized_fallback_vote = wincode::serialize(¬arize_fallback_vote).unwrap(); for i in 3..6 { let keypair = BLSKeypair::new(); diff --git a/votor/src/consensus_pool/parent_ready_tracker.rs b/votor/src/consensus_pool/parent_ready_tracker.rs index 93c6a45b200..d272131bd72 100644 --- a/votor/src/consensus_pool/parent_ready_tracker.rs +++ b/votor/src/consensus_pool/parent_ready_tracker.rs @@ -69,13 +69,13 @@ impl ParentReadyTracker { pub(super) fn new( cluster_info: Arc, root: Slot, - (slot, parent_block @ (parent_slot, _)): ParentReady, + (slot, parent_block): ParentReady, ) -> Self { - debug_assert!(parent_slot < slot); + debug_assert!(parent_block.slot < slot); let mut slot_statuses = HashMap::new(); // Parent block is notarize fallback slot_statuses.insert( - parent_slot, + parent_block.slot, ParentReadyStatus { skip: false, notar_fallbacks: vec![parent_block], @@ -83,7 +83,7 @@ impl ParentReadyTracker { }, ); // Intermediate blocks have skips - for s in (parent_slot.checked_add(1).unwrap())..slot { + for s in (parent_block.slot.checked_add(1).unwrap())..slot { slot_statuses.insert( s, ParentReadyStatus { @@ -106,7 +106,7 @@ impl ParentReadyTracker { Self { cluster_info: DebugIgnore(cluster_info), slot_statuses, - root: parent_slot.min(root), + root: parent_block.slot.min(root), highest_with_parent_ready: slot.max(root), } } @@ -114,14 +114,14 @@ impl ParentReadyTracker { /// Adds a new notarize fallback certificate, we can use Notarize/NotarizeFallback/FastFinalize pub(super) fn add_new_notar_fallback_or_stronger( &mut self, - block @ (slot, _): Block, + block: Block, events: &mut Vec, ) { - if slot <= self.root { + if block.slot <= self.root { return; } - let status = self.slot_statuses.entry(slot).or_default(); + let status = self.slot_statuses.entry(block.slot).or_default(); if status.notar_fallbacks.contains(&block) { return; } @@ -133,7 +133,7 @@ impl ParentReadyTracker { assert!(status.notar_fallbacks.len() <= MAX_NOTAR_FALLBACK_BLOCKS); // Add this block as valid parent to skip connected future blocks - for s in slot.saturating_add(1).. { + for s in block.slot.saturating_add(1).. { trace!( "{}: Adding new parent ready for {s} parent {block:?}", self.cluster_info.0.id() @@ -255,9 +255,9 @@ impl ParentReadyTracker { /// Returns whether the block is notarized or notarized-fallback /// /// This includes any special cases like: genesis, snapshot root, migration Genesis cert - pub(super) fn has_notar_fallback_or_stronger(&self, block @ (slot, _): Block) -> bool { + pub(super) fn has_notar_fallback_or_stronger(&self, block: Block) -> bool { self.slot_statuses - .get(&slot) + .get(&block.slot) .is_some_and(|ss| ss.notar_fallbacks.contains(&block)) } @@ -278,15 +278,12 @@ mod tests { solana_keypair::Keypair, }; - fn root_parent_ready(root_block @ (root_slot, _): Block) -> ParentReady { - (root_slot.saturating_add(1), root_block) + fn root_parent_ready(root_block: Block) -> ParentReady { + (root_block.slot.saturating_add(1), root_block) } - fn new_tracker( - cluster_info: Arc, - root_block @ (root_slot, _): Block, - ) -> ParentReadyTracker { - ParentReadyTracker::new(cluster_info, root_slot, root_parent_ready(root_block)) + fn new_tracker(cluster_info: Arc, root_block: Block) -> ParentReadyTracker { + ParentReadyTracker::new(cluster_info, root_block.slot, root_parent_ready(root_block)) } #[test] @@ -297,7 +294,10 @@ mod tests { let mut events = vec![]; for i in 1..2 * NUM_CONSECUTIVE_LEADER_SLOTS.get() as Slot { - let block = (i, Hash::new_unique()); + let block = Block { + slot: i, + block_id: Hash::new_unique(), + }; tracker.add_new_notar_fallback_or_stronger(block, &mut events); assert_eq!(tracker.highest_parent_ready(), i + 1); assert!(tracker.parent_ready(i + 1, block)); @@ -310,7 +310,10 @@ mod tests { let genesis = Block::default(); let mut tracker = new_tracker(cluster_info, genesis); let mut events = vec![]; - let block = (1, Hash::new_unique()); + let block = Block { + slot: 1, + block_id: Hash::new_unique(), + }; tracker.add_new_notar_fallback_or_stronger(block, &mut events); tracker.add_new_skip(1, &mut events); @@ -328,7 +331,10 @@ mod tests { let genesis = Block::default(); let mut tracker = new_tracker(cluster_info, genesis); let mut events = vec![]; - let block = (1, Hash::new_unique()); + let block = Block { + slot: 1, + block_id: Hash::new_unique(), + }; tracker.add_new_skip(3, &mut events); tracker.add_new_skip(2, &mut events); @@ -346,7 +352,10 @@ mod tests { fn snapshot_wfsm() { let cluster_info = get_cluster_info(Keypair::new()); let root_slot = 2147; - let root_block = (root_slot, Hash::new_unique()); + let root_block = Block { + slot: root_slot, + block_id: Hash::new_unique(), + }; let mut tracker = new_tracker(cluster_info, root_block); let mut events = vec![]; @@ -364,7 +373,10 @@ mod tests { assert!(tracker.parent_ready(root_slot + 3, root_block)); assert_eq!(tracker.highest_parent_ready(), root_slot + 3); - let block = (root_slot + 4, Hash::new_unique()); + let block = Block { + slot: root_slot + 4, + block_id: Hash::new_unique(), + }; tracker.add_new_notar_fallback_or_stronger(block, &mut events); assert!(tracker.parent_ready(root_slot + 3, root_block)); assert!(tracker.parent_ready(root_slot + 5, block)); @@ -375,7 +387,10 @@ mod tests { fn restored_parent_ready() { let cluster_info = get_cluster_info(Keypair::new()); let root_slot = 4; - let restored = (13, Hash::new_unique()); + let restored = Block { + slot: 13, + block_id: Hash::new_unique(), + }; let tracker = ParentReadyTracker::new(cluster_info, root_slot, (16, restored)); assert!(tracker.parent_ready(16, restored)); @@ -421,7 +436,13 @@ mod tests { BlockProductionParent::ParentNotReady ); - tracker.add_new_notar_fallback_or_stronger((4, Hash::new_unique()), &mut events); + tracker.add_new_notar_fallback_or_stronger( + Block { + slot: 4, + block_id: Hash::new_unique(), + }, + &mut events, + ); assert_eq!(tracker.highest_parent_ready(), 5); assert_eq!( tracker.block_production_parent(4), @@ -432,7 +453,13 @@ mod tests { tracker.block_production_parent(8), BlockProductionParent::ParentNotReady ); - tracker.add_new_notar_fallback_or_stronger((64, Hash::new_unique()), &mut events); + tracker.add_new_notar_fallback_or_stronger( + Block { + slot: 64, + block_id: Hash::new_unique(), + }, + &mut events, + ); assert_eq!(tracker.highest_parent_ready(), 65); assert_eq!( tracker.block_production_parent(8), @@ -449,7 +476,13 @@ mod tests { for i in 1..=10 { tracker.add_new_skip(i, &mut vec![]); - tracker.add_new_notar_fallback_or_stronger((i, Hash::new_unique()), &mut vec![]); + tracker.add_new_notar_fallback_or_stronger( + Block { + slot: i, + block_id: Hash::new_unique(), + }, + &mut vec![], + ); } tracker.add_new_skip(11, &mut events); @@ -460,7 +493,7 @@ mod tests { .map(|event| match event { VotorEvent::ParentReady { slot, parent_block } => { assert!(slot == 12); - parent_block.0 + parent_block.slot } _ => panic!("Invalid event"), }) diff --git a/votor/src/consensus_pool/slot_stake_counters.rs b/votor/src/consensus_pool/slot_stake_counters.rs index e47a60a22dd..9685a5756cd 100644 --- a/votor/src/consensus_pool/slot_stake_counters.rs +++ b/votor/src/consensus_pool/slot_stake_counters.rs @@ -60,7 +60,7 @@ impl SlotStakeCounters { Vote::Notarize(vote) => { let old_entry_stake = self .notarize_entry_total - .insert(vote.block_id, entry_stake) + .insert(vote.block.block_id, entry_stake) .unwrap_or(0); self.notarize_total = self .notarize_total @@ -89,11 +89,17 @@ impl SlotStakeCounters { if is_first_in_leader_window { // First block in leader window - emit event immediately - events.push(VotorEvent::SafeToNotar((slot, *block_id))); + events.push(VotorEvent::SafeToNotar(Block { + slot, + block_id: *block_id, + })); stats.event_safe_to_notarize = stats.event_safe_to_notarize.saturating_add(1); } else { // Intrawindow block - add to pending for later processing - pending_safe_to_notar.push((slot, *block_id)); + pending_safe_to_notar.push(Block { + slot, + block_id: *block_id, + }); } } } @@ -110,7 +116,7 @@ impl SlotStakeCounters { // but not to notarize b. Moreover: // notar(b) >= 40% or (skip(s) + notar(b) >= 60% and notar(b) >= 20%) if let Some(Vote::Notarize(my_vote)) = self.my_first_vote.as_ref() { - if &my_vote.block_id == block_id { + if &my_vote.block.block_id == block_id { return false; // I voted for the same block, no need to send NotarizeFallback } } @@ -184,7 +190,10 @@ mod tests { // 40% of stake holders voted notarize counters.add_vote( - &Vote::new_notarization_vote(slot, Hash::default()), + &Vote::new_notarization_vote(Block { + slot, + block_id: Hash::default(), + }), 40, false, &mut events, @@ -193,16 +202,23 @@ mod tests { ); // First in leader window goes to events directly assert_eq!(events.len(), 1); - assert!( - matches!(events[0], VotorEvent::SafeToNotar((s, block_id)) if s == slot && block_id == Hash::default()) - ); + match &events[0] { + VotorEvent::SafeToNotar(block) => { + assert_eq!(block.slot, slot); + assert_eq!(block.block_id, Hash::default()); + } + rest => panic!("unexpected: {rest:?}"), + } assert!(pending_safe_to_notar.is_empty()); assert_eq!(stats.event_safe_to_notarize, 1); events.clear(); // Adding more notarizations does not trigger more events counters.add_vote( - &Vote::new_notarization_vote(slot, Hash::default()), + &Vote::new_notarization_vote(Block { + slot, + block_id: Hash::default(), + }), 20, false, &mut events, @@ -223,7 +239,10 @@ mod tests { // I voted for notarize b let hash_1 = Hash::new_unique(); counters.add_vote( - &Vote::new_notarization_vote(slot, hash_1), + &Vote::new_notarization_vote(Block { + slot, + block_id: hash_1, + }), 1, true, &mut events, @@ -237,7 +256,10 @@ mod tests { // 25% of stake holders voted notarize b' let hash_2 = Hash::new_unique(); counters.add_vote( - &Vote::new_notarization_vote(slot, hash_2), + &Vote::new_notarization_vote(Block { + slot, + block_id: hash_2, + }), 25, false, &mut events, @@ -258,9 +280,13 @@ mod tests { &mut stats, ); assert_eq!(events.len(), 1); - assert!( - matches!(events[0], VotorEvent::SafeToNotar((s, block_id)) if s == slot && block_id == hash_2) - ); + match &events[0] { + VotorEvent::SafeToNotar(block) => { + assert_eq!(block.slot, slot); + assert_eq!(block.block_id, hash_2); + } + rest => panic!("unexpected: {rest:?}"), + } assert!(pending_safe_to_notar.is_empty()); assert_eq!(stats.event_safe_to_notarize, 1); } @@ -289,7 +315,7 @@ mod tests { // 40% of stake holders voted notarize let block_id = Hash::new_unique(); counters.add_vote( - &Vote::new_notarization_vote(slot, block_id), + &Vote::new_notarization_vote(Block { slot, block_id }), 40, false, &mut events, @@ -299,7 +325,7 @@ mod tests { // Intrawindow block goes to pending instead of events assert!(events.is_empty()); assert_eq!(pending_safe_to_notar.len(), 1); - assert_eq!(pending_safe_to_notar[0], (slot, block_id)); + assert_eq!(pending_safe_to_notar[0], Block { slot, block_id }); // Stats are not updated for pending assert_eq!(stats.event_safe_to_notarize, 0); } @@ -314,7 +340,10 @@ mod tests { let slot = 2; // I voted for notarize b counters.add_vote( - &Vote::new_notarization_vote(slot, Hash::default()), + &Vote::new_notarization_vote(Block { + slot, + block_id: Hash::default(), + }), 10, true, &mut events, @@ -359,7 +388,10 @@ mod tests { // I voted for notarize b, 10% of stake holders voted with me let hash_1 = Hash::new_unique(); counters.add_vote( - &Vote::new_notarization_vote(slot, hash_1), + &Vote::new_notarization_vote(Block { + slot, + block_id: hash_1, + }), 10, true, &mut events, @@ -369,7 +401,10 @@ mod tests { // 20% of stake holders voted a different notarization b' let hash_2 = Hash::new_unique(); counters.add_vote( - &Vote::new_notarization_vote(slot, hash_2), + &Vote::new_notarization_vote(Block { + slot, + block_id: hash_2, + }), 20, false, &mut events, @@ -392,7 +427,10 @@ mod tests { // Adding more notarization on b does not trigger more events counters.add_vote( - &Vote::new_notarization_vote(slot, hash_1), + &Vote::new_notarization_vote(Block { + slot, + block_id: hash_1, + }), 10, false, &mut events, diff --git a/votor/src/consensus_pool/stats.rs b/votor/src/consensus_pool/stats.rs index c704af2a783..cacd8d6eb45 100644 --- a/votor/src/consensus_pool/stats.rs +++ b/votor/src/consensus_pool/stats.rs @@ -21,15 +21,15 @@ impl CertificateStats { fn increment(&mut self, cert_type: &CertificateType) { match cert_type { CertificateType::Finalize(_) => self.finalize = self.finalize.saturating_add(1), - CertificateType::FinalizeFast(_, _) => { + CertificateType::FinalizeFast(_) => { self.finalize_fast = self.finalize_fast.saturating_add(1) } - CertificateType::Notarize(_, _) => self.notarize = self.notarize.saturating_add(1), - CertificateType::NotarizeFallback(_, _) => { + CertificateType::Notarize(_) => self.notarize = self.notarize.saturating_add(1), + CertificateType::NotarizeFallback(_) => { self.notarize_fallback = self.notarize_fallback.saturating_add(1) } CertificateType::Skip(_) => self.skip = self.skip.saturating_add(1), - CertificateType::Genesis(_, _) => self.genesis = self.genesis.saturating_add(1), + CertificateType::Genesis(_) => self.genesis = self.genesis.saturating_add(1), } } diff --git a/votor/src/consensus_pool/vote_pool.rs b/votor/src/consensus_pool/vote_pool.rs index 1eb93d089ec..d4afa8638fa 100644 --- a/votor/src/consensus_pool/vote_pool.rs +++ b/votor/src/consensus_pool/vote_pool.rs @@ -134,7 +134,10 @@ impl DuplicateBlockVotePool { mod test { use { super::*, - agave_votor_messages::{consensus_message::VoteMessage, vote::Vote}, + agave_votor_messages::{ + consensus_message::{Block, VoteMessage}, + vote::Vote, + }, solana_bls_signatures::{BLS_SIGNATURE_AFFINE_SIZE, Signature as BLSSignature}, }; @@ -176,7 +179,7 @@ mod test { let mut vote_pool = DuplicateBlockVotePool::new(1); let my_pubkey = Pubkey::new_unique(); let block_id = Hash::new_unique(); - let vote = Vote::new_notarization_vote(3, block_id); + let vote = Vote::new_notarization_vote(Block { slot: 3, block_id }); let vote = VoteMessage { vote, signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), @@ -217,7 +220,10 @@ mod test { let votes = (0..4) .map(|_| { - let vote = Vote::new_notarization_fallback_vote(7, Hash::new_unique()); + let vote = Vote::new_notarization_fallback_vote(Block { + slot: 7, + block_id: Hash::new_unique(), + }); VoteMessage { vote, signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), diff --git a/votor/src/consensus_pool_service.rs b/votor/src/consensus_pool_service.rs index c4c3a0a6aac..c662e7252c0 100644 --- a/votor/src/consensus_pool_service.rs +++ b/votor/src/consensus_pool_service.rs @@ -230,16 +230,16 @@ impl ConsensusPoolService { /// The max of genesis block, root block, or the restored parent ready from vote history fn initial_parent_ready( genesis_block: Option, - root_block @ (root_slot, _): Block, + root_block: Block, vote_history_highest_parent_ready: Option<(Slot, Block)>, ) -> ParentReady { let Some(genesis_block) = genesis_block else { // Alpenglow is not yet enabled, start with just the root - return (root_slot.checked_add(1).unwrap(), root_block); + return (root_block.slot.checked_add(1).unwrap(), root_block); }; - let initial_block @ (initial_slot, _) = genesis_block.max(root_block); - let initial_parent_ready = (initial_slot.checked_add(1).unwrap(), initial_block); + let initial_block = genesis_block.max(root_block); + let initial_parent_ready = (initial_block.slot.checked_add(1).unwrap(), initial_block); if let Some(restored @ (restored_slot, _)) = vote_history_highest_parent_ready && restored_slot > initial_parent_ready.0 @@ -251,13 +251,13 @@ impl ConsensusPoolService { } fn root_block(root_bank: &Bank) -> Block { - ( - root_bank.slot(), - root_bank + Block { + slot: root_bank.slot(), + block_id: root_bank .block_id() // Once SIMD-0333 is active we can hard unwrap here .unwrap_or_default(), - ) + } } // Main loop for the certificate pool service, it only exits when any channel is disconnected @@ -539,13 +539,13 @@ impl ConsensusPoolService { stats: &mut ConsensusPoolServiceStats, ) -> Result<(), ()> { // First, collect new pending blocks from the consensus pool and send them for repair - for block @ (slot, block_id) in consensus_pool.take_pending_safe_to_notar() { + for block in consensus_pool.take_pending_safe_to_notar() { if pending_safe_to_notar.contains(&block) { continue; } match ctx .repair_event_sender - .try_send(RepairEvent::FetchBlock { slot, block_id }) + .try_send(RepairEvent::FetchBlock { block }) { Ok(()) => { stats.pending_safe_to_notar_repair_sent += 1; @@ -564,16 +564,16 @@ impl ConsensusPoolService { let highest_finalized = consensus_pool.highest_finalized_slot().unwrap_or(0); - pending_safe_to_notar.retain(|&(slot, block_id)| { + pending_safe_to_notar.retain(|&block| { // Discard if slot is at or below highest finalized - if slot <= highest_finalized { + if block.slot <= highest_finalized { return false; } // Check if we've received the full block in blockstore let Some(location) = ctx .blockstore - .get_block_location(slot, block_id) + .get_block_location(block.slot, block.block_id) .expect("Blockstore operations must succeed") else { // Block not yet received, keep waiting @@ -583,21 +583,21 @@ impl ConsensusPoolService { // Block has been received, get the parent meta let slot_meta = ctx .blockstore - .meta_from_location(slot, location) + .meta_from_location(block.slot, location) .expect("Blockstore operations must succeed") .expect("SlotMeta must exist if block location is present"); - let parent_block = ( - slot_meta + let parent_block = Block { + slot: slot_meta .parent_slot .expect("parent slot must exist for full blocks"), - slot_meta.parent_block_id, - ); + block_id: slot_meta.parent_block_id, + }; // Check if the parent has a NotarizeFallback certificate (or stronger) if consensus_pool.block_has_notar_fallback_or_stronger(parent_block) { // All conditions met - emit SafeToNotar event - events.push(VotorEvent::SafeToNotar((slot, block_id))); + events.push(VotorEvent::SafeToNotar(block)); stats.pending_safe_to_notar_resolved += 1; return false; } @@ -691,7 +691,10 @@ mod tests { let root_bank = sharable_banks.root(); let cluster_info = get_cluster_info(my_keypair.insecure_clone()); - let root_block = (root_bank.slot(), root_bank.block_id().unwrap_or_default()); + let root_block = Block { + slot: root_bank.slot(), + block_id: root_bank.block_id().unwrap_or_default(), + }; let initial_parent_ready = (root_bank.slot().checked_add(1).unwrap(), root_block); let consensus_pool = ConsensusPool::new( cluster_info.clone(), @@ -736,7 +739,10 @@ mod tests { // validator 0 to 7 send Notarize on slot 2 let block_id = Hash::new_unique(); let target_slot = 2; - let notarize_vote = Vote::new_notarization_vote(target_slot, block_id); + let notarize_vote = Vote::new_notarization_vote(Block { + slot: target_slot, + block_id, + }); let mut events = vec![]; let root_bank = ctx.sharable_banks.root(); @@ -792,12 +798,9 @@ mod tests { if let BLSOp::PushCertificate { certificate } = event { assert_eq!(certificate.cert_type.slot(), target_slot); assert!( - matches!(certificate.cert_type, CertificateType::Notarize(_, _)) - || matches!(certificate.cert_type, CertificateType::FinalizeFast(_, _)) - || matches!( - certificate.cert_type, - CertificateType::NotarizeFallback(_, _) - ) + matches!(certificate.cert_type, CertificateType::Notarize(_)) + || matches!(certificate.cert_type, CertificateType::FinalizeFast(_)) + || matches!(certificate.cert_type, CertificateType::NotarizeFallback(_,)) ); found_certificate = true; } @@ -805,12 +808,11 @@ mod tests { assert!(found_certificate, "Should have received a certificate"); // Verify that we received a finalized slot event - let finalized_event = events.iter().find(|event| { - matches!( - event, - VotorEvent::Finalized((slot, received_block_id), is_fast_finalized) - if *slot == target_slot && *received_block_id == block_id && *is_fast_finalized - ) + let finalized_event = events.iter().find(|event| match event { + VotorEvent::Finalized(block, is_fast_finalized) => { + block.slot == target_slot && block.block_id == block_id && *is_fast_finalized + } + _ => false, }); assert!( finalized_event.is_some(), @@ -938,7 +940,10 @@ mod tests { // Add a ParentReady event for the slot before our leader slot events.push(VotorEvent::ParentReady { slot: next_leader_slot.0, - parent_block: (next_leader_slot.0 - 1, Hash::new_unique()), + parent_block: Block { + slot: next_leader_slot.0 - 1, + block_id: Hash::new_unique(), + }, }); ConsensusPoolService::add_produce_block_event( @@ -978,7 +983,10 @@ mod tests { .0; let restored_parent_ready = ( next_leader_slot, - (next_leader_slot.checked_sub(1).unwrap(), Hash::new_unique()), + Block { + slot: next_leader_slot.checked_sub(1).unwrap(), + block_id: Hash::new_unique(), + }, ); let exit = ctx.exit.clone(); @@ -1039,20 +1047,38 @@ mod tests { #[test] fn test_kick_off_parent_ready_uses_restored_vote_history() { - let genesis_block = Some((10, Hash::new_unique())); - let root_block = (12, Hash::new_unique()); + let genesis_block = Some(Block { + slot: 10, + block_id: Hash::new_unique(), + }); + let root_block = Block { + slot: 12, + block_id: Hash::new_unique(), + }; assert_eq!( ConsensusPoolService::initial_parent_ready(genesis_block, root_block, None), (13, root_block) ); - let restored = (16, (15, Hash::new_unique())); + let restored = ( + 16, + Block { + slot: 15, + block_id: Hash::new_unique(), + }, + ); assert_eq!( ConsensusPoolService::initial_parent_ready(genesis_block, root_block, Some(restored)), restored ); - let stale = (12, (11, Hash::new_unique())); + let stale = ( + 12, + Block { + slot: 11, + block_id: Hash::new_unique(), + }, + ); assert_eq!( ConsensusPoolService::initial_parent_ready(genesis_block, root_block, Some(stale)), (13, root_block) diff --git a/votor/src/event.rs b/votor/src/event.rs index 27fc0435e85..ee4f631b7a5 100644 --- a/votor/src/event.rs +++ b/votor/src/event.rs @@ -2,7 +2,6 @@ use { agave_votor_messages::consensus_message::Block, crossbeam_channel::{Receiver, Sender}, solana_clock::Slot, - solana_hash::Hash, solana_runtime::bank::Bank, std::{ sync::{Arc, Mutex}, @@ -86,10 +85,25 @@ impl VotorEvent { | VotorEvent::SafeToSkip(s) | VotorEvent::TimeoutCrashedLeader(s) | VotorEvent::FirstShred(s) - | VotorEvent::SafeToNotar((s, _)) - | VotorEvent::Finalized((s, _), _) - | VotorEvent::BlockNotarized((s, _)) - | VotorEvent::BlockNotarFallback((s, _)) + | VotorEvent::SafeToNotar(Block { + slot: s, + block_id: _, + }) + | VotorEvent::Finalized( + Block { + slot: s, + block_id: _, + }, + _, + ) + | VotorEvent::BlockNotarized(Block { + slot: s, + block_id: _, + }) + | VotorEvent::BlockNotarFallback(Block { + slot: s, + block_id: _, + }) | VotorEvent::ParentReady { slot: s, parent_block: _, @@ -111,13 +125,13 @@ pub enum RepairEvent { /// - The block has received a NotarizeFallback certificate or stronger /// - An intrawindow block has reached the SafeToNotar threshold, however we need /// to check that the parent has reached notarize-fallback requiring us to fetch this block - FetchBlock { slot: Slot, block_id: Hash }, + FetchBlock { block: Block }, } impl RepairEvent { pub fn slot(&self) -> Slot { match self { - RepairEvent::FetchBlock { slot, .. } => *slot, + RepairEvent::FetchBlock { block } => block.slot, } } } @@ -126,13 +140,13 @@ impl RepairEvent { #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] pub enum SwitchBankEvent { /// We need to switch any existing banks to this bank including ancestors. - Switch { slot: Slot, block_id: Hash }, + Switch { block: Block }, } impl SwitchBankEvent { pub fn block(&self) -> Block { match self { - SwitchBankEvent::Switch { slot, block_id } => (*slot, *block_id), + SwitchBankEvent::Switch { block } => *block, } } } diff --git a/votor/src/event_handler.rs b/votor/src/event_handler.rs index 229b2a1f8de..7c7277690a0 100644 --- a/votor/src/event_handler.rs +++ b/votor/src/event_handler.rs @@ -206,7 +206,7 @@ impl EventHandler { fn handle_parent_ready_event( slot: Slot, - parent_block @ (parent_slot, _): Block, + parent_block: Block, vctx: &mut VotingContext, ctx: &SharedContext, local_context: &mut LocalContext, @@ -217,7 +217,7 @@ impl EventHandler { info!("{my_pubkey}: Parent ready {slot} {parent_block:?}"); // We need to ensure that we've replayed the parent bank. - if parent_slot > vctx.sharable_banks.root().slot() { + if parent_block.slot > vctx.sharable_banks.root().slot() { request_switch(&ctx.latest_switch_request, *my_pubkey, parent_block); } @@ -388,20 +388,20 @@ impl EventHandler { // We have observed the safe to notar condition, and can send a notar fallback vote // TODO: update cert pool to check parent block id for intra window slots - VotorEvent::SafeToNotar(block @ (slot, block_id)) => { + VotorEvent::SafeToNotar(block) => { info!("{my_pubkey}: SafeToNotar {block:?}"); - Self::try_skip_window(my_pubkey, slot, vctx, &mut votes)?; - if vctx.vote_history.its_over(slot) - || vctx.vote_history.voted_notar_fallback(slot, block_id) + Self::try_skip_window(my_pubkey, block.slot, vctx, &mut votes)?; + if vctx.vote_history.its_over(block.slot) + || vctx + .vote_history + .voted_notar_fallback(block.slot, block.block_id) { return Ok(votes); } - info!("{my_pubkey}: Voting notarize-fallback for {slot} {block_id}"); - if let Some(bls_op) = generate_vote_message( - Vote::new_notarization_fallback_vote(slot, block_id), - false, - vctx, - )? { + info!("{my_pubkey}: Voting notarize-fallback for {block:?}"); + if let Some(bls_op) = + generate_vote_message(Vote::new_notarization_fallback_vote(block), false, vctx)? + { votes.push(bls_op); } } @@ -445,12 +445,12 @@ impl EventHandler { ); if let Some(slot) = *standstill_slot { - if block.0 > slot { + if block.slot > slot { *standstill_slot = None; info!( "{my_pubkey}: Standstill initially detected at slot={slot} has ended \ at slot={}. Ending timeout extension", - block.0 + block.slot ); } } @@ -459,7 +459,7 @@ impl EventHandler { Self::add_missing_parent_ready(block, ctx, vctx, local_context) { Self::handle_parent_ready_event( - block.0, + block.slot, parent_block, vctx, ctx, @@ -471,7 +471,7 @@ impl EventHandler { vctx.consensus_metrics_sender .send(( Instant::now(), - vec![ConsensusMetricsEvent::SlotFinalized { slot: block.0 }], + vec![ConsensusMetricsEvent::SlotFinalized { slot: block.slot }], )) .map_err(|_| SendError(()))?; } @@ -548,7 +548,7 @@ impl EventHandler { vctx: &mut VotingContext, local_context: &mut LocalContext, ) -> Option { - let (slot, block_id) = finalized_block; + let Block { slot, block_id } = finalized_block; let first_slot_of_window = first_of_consecutive_leader_slots(slot); if first_slot_of_window == slot || first_slot_of_window == 0 { // No need to trigger parent ready for the first slot of the window @@ -584,7 +584,10 @@ impl EventHandler { {parent_block_id}", local_context.my_pubkey ); - Some((parent_slot, parent_block_id)) + Some(Block { + slot: parent_slot, + block_id: parent_block_id, + }) } fn handle_set_identity( @@ -610,10 +613,10 @@ impl EventHandler { fn get_block_parent_block(bank: &Bank) -> (Block, Block) { let slot = bank.slot(); - let block = ( + let block = Block { slot, - bank.block_id().expect("Block id must be set upstream"), - ); + block_id: bank.block_id().expect("Block id must be set upstream"), + }; let parent_slot = bank.parent_slot(); let parent_block_id = bank.parent_block_id().unwrap_or_else(|| { // To account for child of genesis and snapshots we insert a @@ -623,7 +626,10 @@ impl EventHandler { trace!("Using default block id for {slot} parent {parent_slot}"); Hash::default() }); - let parent_block = (parent_slot, parent_block_id); + let parent_block = Block { + slot: parent_slot, + block_id: parent_block_id, + }; (block, parent_block) } @@ -636,8 +642,8 @@ impl EventHandler { /// An error returned will cause the voting process to be aborted. fn try_notar( my_pubkey: &Pubkey, - (slot, block_id): Block, - parent_block @ (parent_slot, parent_block_id): Block, + Block { slot, block_id }: Block, + parent_block: Block, pending_blocks: &mut PendingBlocks, voting_context: &mut VotingContext, votes: &mut Vec, @@ -655,11 +661,13 @@ impl EventHandler { return Ok(false); } } else { - if parent_slot.saturating_add(1) != slot { + if parent_block.slot.saturating_add(1) != slot { // Non consecutive return Ok(false); } - if voting_context.vote_history.voted_notar(parent_slot) != Some(parent_block_id) { + if voting_context.vote_history.voted_notar(parent_block.slot) + != Some(parent_block.block_id) + { // Voted skip, or notarize on a different version of the parent return Ok(false); } @@ -667,7 +675,7 @@ impl EventHandler { info!("{my_pubkey}: Voting notarize for {slot} {block_id}"); if let Some(bls_op) = generate_vote_message( - Vote::new_notarization_vote(slot, block_id), + Vote::new_notarization_vote(Block { slot, block_id }), false, voting_context, )? { @@ -680,7 +688,7 @@ impl EventHandler { )?; pending_blocks.remove(&slot); - Self::try_final(my_pubkey, (slot, block_id), voting_context, votes)?; + Self::try_final(my_pubkey, Block { slot, block_id }, voting_context, votes)?; Ok(true) } @@ -722,29 +730,31 @@ impl EventHandler { /// An error returned will cause the voting process to be aborted. fn try_final( my_pubkey: &Pubkey, - block @ (slot, block_id): Block, + block: Block, voting_context: &mut VotingContext, votes: &mut Vec, ) -> Result { if !voting_context.vote_history.is_block_notarized(&block) - || voting_context.vote_history.its_over(slot) - || voting_context.vote_history.bad_window(slot) + || voting_context.vote_history.its_over(block.slot) + || voting_context.vote_history.bad_window(block.slot) { return Ok(false); } if voting_context .vote_history - .voted_notar(slot) - .is_none_or(|bid| bid != block_id) + .voted_notar(block.slot) + .is_none_or(|bid| bid != block.block_id) { return Ok(false); } - info!("{my_pubkey}: Voting finalize for {slot}"); - if let Some(bls_op) = - generate_vote_message(Vote::new_finalization_vote(slot), false, voting_context)? - { + info!("{my_pubkey}: Voting finalize for {}", block.slot); + if let Some(bls_op) = generate_vote_message( + Vote::new_finalization_vote(block.slot), + false, + voting_context, + )? { votes.push(bls_op); } Ok(true) @@ -826,31 +836,31 @@ impl EventHandler { let old_root = bank_forks_r.root().max(vctx.vote_history.root()); let Some(new_root) = finalized_blocks .iter() - .filter_map(|&(slot, block_id)| { - let bank = bank_forks_r.get(slot)?; + .filter_map(|&block| { + let bank = bank_forks_r.get(block.slot)?; // Check for bank hash mismatch if bank.is_frozen() - && bank.block_id() == Some(block_id) + && bank.block_id() == Some(block.block_id) && let Some(expected_hash) = bank.expected_bank_hash() && expected_hash != bank.hash() { panic!( - "{my_pubkey}: Slot {slot} block_id {block_id} has been finalized, however \ - we have a bank hash mismatch. The cluster bank hash is {expected_hash} \ - however we computed {}. At this point we will be unable to recover. \ - Please save a copy of your ledger to share on discord and restart from a \ - snapshot > {slot}.", + "{my_pubkey}: Block {block:?} has been finalized, however we have a bank \ + hash mismatch. The cluster bank hash is {expected_hash} however we \ + computed {}. At this point we will be unable to recover. Please save a \ + copy of your ledger to share on discord and restart from a snapshot > {}.", bank.hash(), + block.slot ); } // Check if this block is rootable - (slot > old_root - && vctx.vote_history.voted(slot) + (block.slot > old_root + && vctx.vote_history.voted(block.slot) && bank.is_frozen() - && bank.block_id().is_some_and(|bid| bid == block_id)) - .then_some(slot) + && bank.block_id().is_some_and(|bid| bid == block.block_id)) + .then_some(block.slot) }) .max() else { @@ -882,14 +892,14 @@ fn request_repair( my_pubkey: Pubkey, block: Block, ) -> Result<(), EventLoopError> { - let (slot, block_id) = block; - let event = RepairEvent::FetchBlock { slot, block_id }; + let event = RepairEvent::FetchBlock { block }; match sender.try_send(event) { Ok(()) => Ok(()), Err(TrySendError::Full(event)) => { error!( "{my_pubkey}: Repair event channel is full, this should not happen. Blocking to \ - send event for slot {slot}" + send event for slot {}", + block.slot ); sender .send(event) @@ -903,12 +913,9 @@ fn request_repair( /// Updates the latest switch-bank request for replay to consume. fn request_switch(latest: &LatestSwitchRequest, my_pubkey: Pubkey, block: Block) { - let (slot, block_id) = block; - let event = SwitchBankEvent::Switch { slot, block_id }; + let event = SwitchBankEvent::Switch { block }; if let Some(prev) = latest.try_advance(event) { - trace!( - "{my_pubkey}: Overwriting previous switch request {prev:?} with ({slot}, {block_id})" - ); + trace!("{my_pubkey}: Overwriting previous switch request {prev:?} with ({block:?})"); } } @@ -968,7 +975,7 @@ mod tests { my_bls_keypair: BLSKeypair, timer_manager: Arc>, leader_window_info_receiver: Receiver, - highest_parent_ready: Arc>, + highest_parent_ready: Arc>, drop_bank_receiver: Receiver>, cluster_info: Arc, consensus_metrics_receiver: ConsensusMetricsEventReceiver, @@ -1445,8 +1452,20 @@ mod tests { // If there is a parent ready for block 1 Notarization is sent out. let slot = 1; let parent_slot = 0; - test_context.send_parent_ready_event(slot, (parent_slot, Hash::default())); - test_context.check_parent_ready_slot((slot, (parent_slot, Hash::default()))); + test_context.send_parent_ready_event( + slot, + Block { + slot: parent_slot, + block_id: Hash::default(), + }, + ); + test_context.check_parent_ready_slot(( + slot, + Block { + slot: parent_slot, + block_id: Hash::default(), + }, + )); let root_bank = test_context .bank_forks .read() @@ -1459,7 +1478,10 @@ mod tests { test_context.check_for_metrics_event(ConsensusMetricsEvent::StartOfSlot { slot }); // We should receive Notarize Vote for block 1 - test_context.check_for_vote(&Vote::new_notarization_vote(slot, block_id_1)); + test_context.check_for_vote(&Vote::new_notarization_vote(Block { + slot, + block_id: block_id_1, + })); test_context.check_for_commitment(CommitmentType::Notarize, slot); // Add block event for 1 again will not trigger another Notarize or commitment test_context.send_block_event(1, bank1.clone()); @@ -1470,7 +1492,10 @@ mod tests { let block_id_2 = bank2.block_id().unwrap(); // Because 2 is middle of window, we should see Notarize vote for block 2 even without parentready - test_context.check_for_vote(&Vote::new_notarization_vote(slot, block_id_2)); + test_context.check_for_vote(&Vote::new_notarization_vote(Block { + slot, + block_id: block_id_2, + })); test_context.check_for_commitment(CommitmentType::Notarize, slot); // Slot 3 somehow links to block 1, should not trigger Notarize vote because it has a wrong parent (not 2) let _ = test_context.create_block_and_send_block_event(3, bank1); @@ -1482,9 +1507,24 @@ mod tests { let block_id_4 = bank4.block_id().unwrap(); // Send parent ready for slot 4 should trigger Notarize vote for slot 4 - test_context.send_parent_ready_event(slot, (2, block_id_2)); - test_context.check_parent_ready_slot((slot, (2, block_id_2))); - test_context.check_for_vote(&Vote::new_notarization_vote(slot, block_id_4)); + test_context.send_parent_ready_event( + slot, + Block { + slot: 2, + block_id: block_id_2, + }, + ); + test_context.check_parent_ready_slot(( + slot, + Block { + slot: 2, + block_id: block_id_2, + }, + )); + test_context.check_for_vote(&Vote::new_notarization_vote(Block { + slot, + block_id: block_id_4, + })); test_context.check_for_commitment(CommitmentType::Notarize, slot); } @@ -1492,7 +1532,10 @@ mod tests { fn test_restored_parent_ready_sets_timeout() { let mut test_context = setup(); let slot = 4; - let parent_block = (3, Hash::new_unique()); + let parent_block = Block { + slot: 3, + block_id: Hash::new_unique(), + }; assert!( test_context @@ -1523,20 +1566,44 @@ mod tests { let block_id_1 = bank1.block_id().unwrap(); // Add parent ready for 0 to trigger notar vote for 1 - test_context.send_parent_ready_event(1, (0, Hash::default())); - test_context.check_parent_ready_slot((1, (0, Hash::default()))); - test_context.check_for_vote(&Vote::new_notarization_vote(1, block_id_1)); + test_context.send_parent_ready_event( + 1, + Block { + slot: 0, + block_id: Hash::default(), + }, + ); + test_context.check_parent_ready_slot(( + 1, + Block { + slot: 0, + block_id: Hash::default(), + }, + )); + test_context.check_for_vote(&Vote::new_notarization_vote(Block { + slot: 1, + block_id: block_id_1, + })); test_context.check_for_commitment(CommitmentType::Notarize, 1); // Send block notarized event should trigger Finalize vote - test_context.send_block_notarized_event((1, block_id_1)); + test_context.send_block_notarized_event(Block { + slot: 1, + block_id: block_id_1, + }); test_context.check_for_vote(&Vote::new_finalization_vote(1)); let bank2 = test_context.create_block_and_send_block_event(2, bank1); let block_id_2 = bank2.block_id().unwrap(); // Both Notarize and Finalize votes should trigger for 2 - test_context.check_for_vote(&Vote::new_notarization_vote(2, block_id_2)); + test_context.check_for_vote(&Vote::new_notarization_vote(Block { + slot: 2, + block_id: block_id_2, + })); test_context.check_for_commitment(CommitmentType::Notarize, 2); - test_context.send_block_notarized_event((2, block_id_2)); + test_context.send_block_notarized_event(Block { + slot: 2, + block_id: block_id_2, + }); test_context.check_for_vote(&Vote::new_finalization_vote(2)); // Create bank3 but do not Notarize, so Finalize vote should not trigger @@ -1546,14 +1613,20 @@ mod tests { // Check no notarization vote for 3 test_context.check_no_vote_or_commitment(); - test_context.send_block_notarized_event((slot, block_id_3)); + test_context.send_block_notarized_event(Block { + slot, + block_id: block_id_3, + }); // Check no Finalize vote for 3 test_context.check_no_vote_or_commitment(); // Now send Block event simulating replay completed for 3 test_context.send_block_event(slot, bank3.clone()); // There should be a notarization vote for 3 - test_context.check_for_vote(&Vote::new_notarization_vote(slot, block_id_3)); + test_context.check_for_vote(&Vote::new_notarization_vote(Block { + slot, + block_id: block_id_3, + })); test_context.check_for_commitment(CommitmentType::Notarize, slot); // Check there is a Finalize vote for 3 test_context.check_for_vote(&Vote::new_finalization_vote(slot)); @@ -1616,33 +1689,63 @@ mod tests { .root(); let bank_1 = test_context.create_block_and_send_block_event(1, root_bank); let block_id_1_old = bank_1.block_id().unwrap(); - test_context.send_parent_ready_event(1, (0, Hash::default())); + test_context.send_parent_ready_event( + 1, + Block { + slot: 0, + block_id: Hash::default(), + }, + ); - test_context.check_parent_ready_slot((1, (0, Hash::default()))); - test_context.check_for_vote(&Vote::new_notarization_vote(1, block_id_1_old)); + test_context.check_parent_ready_slot(( + 1, + Block { + slot: 0, + block_id: Hash::default(), + }, + )); + test_context.check_for_vote(&Vote::new_notarization_vote(Block { + slot: 1, + block_id: block_id_1_old, + })); test_context.check_for_commitment(CommitmentType::Notarize, 1); // Now we got safe_to_notar event for slot 1 and a different block id let block_id_1_1 = Hash::new_unique(); - test_context.send_safe_to_notar_event((1, block_id_1_1)); + test_context.send_safe_to_notar_event(Block { + slot: 1, + block_id: block_id_1_1, + }); // We should see rest of the window skipped test_context.check_for_vote(&Vote::new_skip_vote(2)); test_context.check_for_vote(&Vote::new_skip_vote(3)); // We should also see notarize fallback for the new block id - test_context.check_for_vote(&Vote::new_notarization_fallback_vote(1, block_id_1_1)); + test_context.check_for_vote(&Vote::new_notarization_fallback_vote(Block { + slot: 1, + block_id: block_id_1_1, + })); // We can trigger safe_to_notar event again for a different block id // In this test you can trigger this any number of times, but the white paper // proved we can only get up to 3 different block ids on a slot, and our // certificate pool implementation checks that. let block_id_1_2 = Hash::new_unique(); - test_context.send_safe_to_notar_event((1, block_id_1_2)); + test_context.send_safe_to_notar_event(Block { + slot: 1, + block_id: block_id_1_2, + }); // No skips this time because we already skipped the rest of the window // We should also see notarize fallback for the new block id - test_context.check_for_vote(&Vote::new_notarization_fallback_vote(1, block_id_1_2)); + test_context.check_for_vote(&Vote::new_notarization_fallback_vote(Block { + slot: 1, + block_id: block_id_1_2, + })); // But getting safe_to_notar for a block id we voted before should be no-op - test_context.send_safe_to_notar_event((1, block_id_1_1)); + test_context.send_safe_to_notar_event(Block { + slot: 1, + block_id: block_id_1_1, + }); test_context.check_no_vote_or_commitment(); } @@ -1659,10 +1762,25 @@ mod tests { .root(); let bank_1 = test_context.create_block_and_send_block_event(1, root_bank); let block_id_1 = bank_1.block_id().unwrap(); - test_context.send_parent_ready_event(1, (0, Hash::default())); + test_context.send_parent_ready_event( + 1, + Block { + slot: 0, + block_id: Hash::default(), + }, + ); - test_context.check_parent_ready_slot((1, (0, Hash::default()))); - test_context.check_for_vote(&Vote::new_notarization_vote(1, block_id_1)); + test_context.check_parent_ready_slot(( + 1, + Block { + slot: 0, + block_id: Hash::default(), + }, + )); + test_context.check_for_vote(&Vote::new_notarization_vote(Block { + slot: 1, + block_id: block_id_1, + })); test_context.check_for_commitment(CommitmentType::Notarize, 1); // Now we got safe_to_skip event for slot 1 test_context.send_safe_to_skip_event(1); @@ -1683,7 +1801,14 @@ mod tests { // Produce a full window of blocks // Assume the leader for 1-3 is us, send produce window event - test_context.send_produce_window_event(1, 3, (0, Hash::default())); + test_context.send_produce_window_event( + 1, + 3, + Block { + slot: 0, + block_id: Hash::default(), + }, + ); // Check that leader_window_info is sent via channel let received_leader_window_info = @@ -1692,17 +1817,33 @@ mod tests { assert_eq!(received_leader_window_info.end_slot, 3); assert_eq!( received_leader_window_info.parent_block, - (0, Hash::default()) + Block { + slot: 0, + block_id: Hash::default() + } ); // Suddenly I found out I produced block 1 already, send new produce window event let block_id_1 = Hash::new_unique(); - test_context.send_produce_window_event(2, 3, (1, block_id_1)); + test_context.send_produce_window_event( + 2, + 3, + Block { + slot: 1, + block_id: block_id_1, + }, + ); let received_leader_window_info = test_context.leader_window_info_receiver.try_recv().unwrap(); assert_eq!(received_leader_window_info.start_slot, 2); assert_eq!(received_leader_window_info.end_slot, 3); - assert_eq!(received_leader_window_info.parent_block, (1, block_id_1)); + assert_eq!( + received_leader_window_info.parent_block, + Block { + slot: 1, + block_id: block_id_1 + } + ); } #[test] @@ -1718,13 +1859,34 @@ mod tests { let bank1 = test_context.create_block_and_send_block_event(1, root_bank); let block_id_1 = bank1.block_id().unwrap(); - test_context.send_parent_ready_event(1, (0, Hash::default())); + test_context.send_parent_ready_event( + 1, + Block { + slot: 0, + block_id: Hash::default(), + }, + ); - test_context.check_parent_ready_slot((1, (0, Hash::default()))); - test_context.check_for_vote(&Vote::new_notarization_vote(1, block_id_1)); + test_context.check_parent_ready_slot(( + 1, + Block { + slot: 0, + block_id: Hash::default(), + }, + )); + test_context.check_for_vote(&Vote::new_notarization_vote(Block { + slot: 1, + block_id: block_id_1, + })); test_context.check_for_commitment(CommitmentType::Notarize, 1); // Now we got finalized event for slot 1 - test_context.send_finalized_event((1, block_id_1), true); + test_context.send_finalized_event( + Block { + slot: 1, + block_id: block_id_1, + }, + true, + ); // Listen on drop bank receiver, it should get bank 0 let dropped_banks = test_context.drop_bank_receiver.try_recv().unwrap(); assert_eq!(dropped_banks.len(), 1); @@ -1748,7 +1910,7 @@ mod tests { let expected_hash = Hash::new_unique(); bank.set_expected_bank_hash(expected_hash); - test_context.send_finalized_event((1, block_id), true); + test_context.send_finalized_event(Block { slot: 1, block_id }, true); } #[test] @@ -1765,7 +1927,13 @@ mod tests { bank.set_expected_bank_hash(expected_hash); let finalized_block_id = Hash::new_unique(); - test_context.send_finalized_event((1, finalized_block_id), true); + test_context.send_finalized_event( + Block { + slot: 1, + block_id: finalized_block_id, + }, + true, + ); } #[test] @@ -1785,21 +1953,45 @@ mod tests { let bank5 = test_context.create_block_and_send_block_event(5, bank4); let block_id_5 = bank5.block_id().unwrap(); - test_context.send_finalized_event((5, block_id_5), true); + test_context.send_finalized_event( + Block { + slot: 5, + block_id: block_id_5, + }, + true, + ); // We should now have parent ready for slot 5 - test_context.check_parent_ready_slot((5, (4, block_id_4))); + test_context.check_parent_ready_slot(( + 5, + Block { + slot: 4, + block_id: block_id_4, + }, + )); // We are partitioned off from rest of the network, and suddenly received finalize for // slot 9 a little before we finished replay slot 9 let bank9 = test_context.create_block_only(9, bank5); let block_id_9 = bank9.block_id().unwrap(); - test_context.send_finalized_event((9, block_id_9), true); + test_context.send_finalized_event( + Block { + slot: 9, + block_id: block_id_9, + }, + true, + ); test_context.send_block_event(9, bank9); // We should now have parent ready for slot 9 - test_context.check_parent_ready_slot((9, (5, block_id_5))); + test_context.check_parent_ready_slot(( + 9, + Block { + slot: 5, + block_id: block_id_5, + }, + )); } #[test] @@ -1815,9 +2007,18 @@ mod tests { .root(); let bank1 = test_context.create_block_and_send_block_event(1, root_bank); let block_id_1 = bank1.block_id().unwrap(); - test_context.send_parent_ready_event(1, (0, Hash::default())); + test_context.send_parent_ready_event( + 1, + Block { + slot: 0, + block_id: Hash::default(), + }, + ); - test_context.check_for_vote(&Vote::new_notarization_vote(1, block_id_1)); + test_context.check_for_vote(&Vote::new_notarization_vote(Block { + slot: 1, + block_id: block_id_1, + })); test_context.send_timeout_event(2); test_context.check_for_vote(&Vote::new_skip_vote(2)); @@ -1827,13 +2028,22 @@ mod tests { test_context.send_standstill_event(0); test_context.check_for_votes(&[ - Vote::new_notarization_vote(1, block_id_1), + Vote::new_notarization_vote(Block { + slot: 1, + block_id: block_id_1, + }), Vote::new_skip_vote(2), Vote::new_skip_vote(3), ]); // Finalize block 1, should deactivate standstill - test_context.send_finalized_event((1, block_id_1), true); + test_context.send_finalized_event( + Block { + slot: 1, + block_id: block_id_1, + }, + true, + ); assert!(test_context.local_context.standstill_slot.is_none()); // Send another standstill event with highest finalized at 1, we should refresh votes for 2 and 3 only @@ -1861,7 +2071,13 @@ mod tests { .sharable_banks() .root(); let _ = test_context.create_block_and_send_block_event(1, root_bank.clone()); - test_context.send_parent_ready_event(1, (0, Hash::default())); + test_context.send_parent_ready_event( + 1, + Block { + slot: 0, + block_id: Hash::default(), + }, + ); // There should be no votes but we should see commitments for hot spares assert_eq!( @@ -1878,8 +2094,17 @@ mod tests { let slot = 4; let bank4 = test_context.create_block_and_send_block_event(slot, root_bank); let block_id_4 = bank4.block_id().unwrap(); - test_context.send_parent_ready_event(slot, (0, Hash::default())); - test_context.check_for_vote(&Vote::new_notarization_vote(slot, block_id_4)); + test_context.send_parent_ready_event( + slot, + Block { + slot: 0, + block_id: Hash::default(), + }, + ); + test_context.check_for_vote(&Vote::new_notarization_vote(Block { + slot, + block_id: block_id_4, + })); test_context.check_for_commitment(CommitmentType::Notarize, slot); for file in files_to_remove { @@ -1903,8 +2128,17 @@ mod tests { .root(); let bank1 = test_context.create_block_and_send_block_event(1, root_bank); let block_id_1 = bank1.block_id().unwrap(); - test_context.send_parent_ready_event(1, (0, Hash::default())); - test_context.check_for_vote(&Vote::new_notarization_vote(1, block_id_1)); + test_context.send_parent_ready_event( + 1, + Block { + slot: 0, + block_id: Hash::default(), + }, + ); + test_context.check_for_vote(&Vote::new_notarization_vote(Block { + slot: 1, + block_id: block_id_1, + })); // Send standstill event - should record the standstill slot test_context.send_standstill_event(0); @@ -1920,7 +2154,13 @@ mod tests { assert_eq!(test_context.local_context.standstill_slot, Some(0)); // Send a finalized event - should reset standstill_slot - test_context.send_finalized_event((1, block_id_1), false); + test_context.send_finalized_event( + Block { + slot: 1, + block_id: block_id_1, + }, + false, + ); assert!(test_context.local_context.standstill_slot.is_none()); } } diff --git a/votor/src/event_handler/stats.rs b/votor/src/event_handler/stats.rs index 028aa738692..c1228e0f0cb 100644 --- a/votor/src/event_handler/stats.rs +++ b/votor/src/event_handler/stats.rs @@ -154,8 +154,8 @@ impl EventHandlerStats { let entry = self.slot_tracking_map.entry(*slot).or_default(); entry.parent_ready = Some(Instant::now()); } - VotorEvent::Finalized((slot, _), is_fast_finalization) => { - let entry = self.slot_tracking_map.entry(*slot).or_default(); + VotorEvent::Finalized(block, is_fast_finalization) => { + let entry = self.slot_tracking_map.entry(block.slot).or_default(); if entry.finalized.is_none() { entry.finalized = Some((Instant::now(), *is_fast_finalization)); } else if *is_fast_finalization { diff --git a/votor/src/root_utils.rs b/votor/src/root_utils.rs index fc18c8dcffd..eec62f26a9e 100644 --- a/votor/src/root_utils.rs +++ b/votor/src/root_utils.rs @@ -43,7 +43,10 @@ pub(crate) fn set_root( info!("{my_pubkey}: setting root {new_root}"); vctx.vote_history.set_root(new_root); *pending_blocks = pending_blocks.split_off(&new_root); - *finalized_blocks = finalized_blocks.split_off(&(new_root, Hash::default())); + *finalized_blocks = finalized_blocks.split_off(&Block { + slot: new_root, + block_id: Hash::default(), + }); *received_shred = received_shred.split_off(&new_root); rctx.bank_forks_controller diff --git a/votor/src/vote_history.rs b/votor/src/vote_history.rs index fe3f1d9a3a2..31fa9d278e5 100644 --- a/votor/src/vote_history.rs +++ b/votor/src/vote_history.rs @@ -32,7 +32,7 @@ impl VoteHistoryVersions { #[cfg_attr( feature = "frozen-abi", derive(AbiExample), - frozen_abi(digest = "9dp4rEVqAsT7mfiL5oEgWrxgWCUiEe4Fk8xJoTWwSN1X") + frozen_abi(digest = "BdNXvHhpPUTEba3DfDCLqirLoLXAmja2jyhroicM63bT") )] #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Default, SchemaWrite, SchemaRead)] pub struct VoteHistory { @@ -167,8 +167,12 @@ impl VoteHistory { // Asserts here guard against vote equivocation. match vote { Vote::Notarize(vote) => { - assert!(self.voted.insert(vote.slot)); - assert!(self.voted_notar.insert(vote.slot, vote.block_id).is_none()); + assert!(self.voted.insert(vote.block.slot)); + assert!( + self.voted_notar + .insert(vote.block.slot, vote.block.block_id) + .is_none() + ); } Vote::Finalize(vote) => { assert!(!self.skipped(vote.slot)); @@ -179,13 +183,13 @@ impl VoteHistory { self.skipped.insert(vote.slot); } Vote::NotarizeFallback(vote) => { - assert!(self.voted(vote.slot)); - assert!(!self.its_over(vote.slot)); - self.skipped.insert(vote.slot); + assert!(self.voted(vote.block.slot)); + assert!(!self.its_over(vote.block.slot)); + self.skipped.insert(vote.block.slot); self.voted_notar_fallback - .entry(vote.slot) + .entry(vote.block.slot) .or_default() - .insert(vote.block_id); + .insert(vote.block.block_id); } Vote::SkipFallback(vote) => { assert!(self.voted(vote.slot)); @@ -203,8 +207,8 @@ impl VoteHistory { } /// Add a new notarized block - pub fn add_block_notarized(&mut self, block @ (slot, _): Block) { - if slot < self.root { + pub fn add_block_notarized(&mut self, block: Block) { + if block.slot < self.root { return; } self.notarized_blocks.insert(block); @@ -254,7 +258,7 @@ impl VoteHistory { self.skipped.retain(|s| *s >= root); self.its_over.retain(|s| *s >= root); self.votes_cast.retain(|s, _| *s >= root); - self.notarized_blocks.retain(|(s, _)| *s >= root); + self.notarized_blocks.retain(|block| block.slot >= root); self.parent_ready_slots.retain(|s, _| *s >= root); } @@ -335,7 +339,10 @@ mod test { // Vote Notarize on slot 1 let block_id_1 = Hash::new_unique(); - let vote_notarize_1 = Vote::new_notarization_vote(1, block_id_1); + let vote_notarize_1 = Vote::new_notarization_vote(Block { + slot: 1, + block_id: block_id_1, + }); vote_history.add_vote(vote_notarize_1); assert!(vote_history.voted(1)); assert!(!vote_history.its_over(1)); @@ -369,7 +376,10 @@ mod test { // Now vote NotarizeFallback on slot 2 let block_id_2 = Hash::new_unique(); - let vote_notarize_fallback_2 = Vote::new_notarization_fallback_vote(2, block_id_2); + let vote_notarize_fallback_2 = Vote::new_notarization_fallback_vote(Block { + slot: 2, + block_id: block_id_2, + }); vote_history.add_vote(vote_notarize_fallback_2); assert!(vote_history.voted(2)); assert!(vote_history.skipped(2)); @@ -390,7 +400,10 @@ mod test { // Vote Notarize on slot 3 let block_id_3 = Hash::new_unique(); - let vote_notarize_3 = Vote::new_notarization_vote(3, block_id_3); + let vote_notarize_3 = Vote::new_notarization_vote(Block { + slot: 3, + block_id: block_id_3, + }); vote_history.add_vote(vote_notarize_3); assert!(vote_history.voted(3)); assert!(!vote_history.skipped(3)); @@ -453,12 +466,18 @@ mod test { #[test] fn test_add_notarized_blocks() { let mut vote_history = VoteHistory::new(Pubkey::new_unique(), 0); - let block_1 = (1, Hash::new_unique()); + let block_1 = Block { + slot: 1, + block_id: Hash::new_unique(), + }; assert!(!vote_history.is_block_notarized(&block_1)); vote_history.add_block_notarized(block_1); assert!(vote_history.is_block_notarized(&block_1)); - let block_2 = (2, Hash::new_unique()); + let block_2 = Block { + slot: 2, + block_id: Hash::new_unique(), + }; assert!(!vote_history.is_block_notarized(&block_2)); vote_history.add_block_notarized(block_2); assert!(vote_history.is_block_notarized(&block_2)); @@ -477,46 +496,52 @@ mod test { fn test_add_parent_ready() { let mut vote_history = VoteHistory::new(Pubkey::new_unique(), 0); assert_eq!(vote_history.highest_parent_ready_slot(), None); - let block_id_0 = (0, Hash::new_unique()); - vote_history.add_parent_ready(1, block_id_0); - assert!(vote_history.is_parent_ready(1, &block_id_0)); + let block_0 = Block { + slot: 0, + block_id: Hash::new_unique(), + }; + vote_history.add_parent_ready(1, block_0); + assert!(vote_history.is_parent_ready(1, &block_0)); assert_eq!(vote_history.highest_parent_ready_slot(), Some(1)); vote_history.set_root(1); assert_eq!(vote_history.root(), 1); - assert!(vote_history.is_parent_ready(1, &block_id_0)); + assert!(vote_history.is_parent_ready(1, &block_0)); assert_eq!(vote_history.highest_parent_ready_slot(), Some(1)); // Add parent ready for slot 2 - let block_id_2_0 = (1, Hash::new_unique()); - let block_id_2_1 = (1, Hash::new_unique()); - assert!(vote_history.add_parent_ready(2, block_id_2_0)); - assert!(vote_history.is_parent_ready(2, &block_id_2_0)); + let block_2_0 = Block { + slot: 1, + block_id: Hash::new_unique(), + }; + let block_2_1 = Block { + slot: 1, + block_id: Hash::new_unique(), + }; + assert!(vote_history.add_parent_ready(2, block_2_0)); + assert!(vote_history.is_parent_ready(2, &block_2_0)); assert_eq!(vote_history.highest_parent_ready_slot(), Some(2)); - assert!(!vote_history.add_parent_ready(2, block_id_2_1)); - assert!(vote_history.is_parent_ready(2, &block_id_2_1)); - assert!(!vote_history.add_parent_ready(2, block_id_0)); - assert!(vote_history.is_parent_ready(2, &block_id_0)); + assert!(!vote_history.add_parent_ready(2, block_2_1)); + assert!(vote_history.is_parent_ready(2, &block_2_1)); + assert!(!vote_history.add_parent_ready(2, block_0)); + assert!(vote_history.is_parent_ready(2, &block_0)); // Set root to 2 vote_history.set_root(2); assert_eq!(vote_history.root(), 2); - assert!(!vote_history.is_parent_ready(1, &block_id_0)); - assert!(vote_history.is_parent_ready(2, &block_id_2_0)); - assert!(vote_history.is_parent_ready(2, &block_id_2_1)); - assert!(vote_history.is_parent_ready(2, &block_id_0)); + assert!(!vote_history.is_parent_ready(1, &block_0)); + assert!(vote_history.is_parent_ready(2, &block_2_0)); + assert!(vote_history.is_parent_ready(2, &block_2_1)); + assert!(vote_history.is_parent_ready(2, &block_0)); assert_eq!(vote_history.highest_parent_ready_slot(), Some(2)); - let expected_parent = [block_id_2_0, block_id_2_1, block_id_0] - .into_iter() - .min() - .unwrap(); + let expected_parent = [block_2_0, block_2_1, block_0].into_iter().min().unwrap(); assert_eq!( vote_history.highest_parent_ready(), Some((2, expected_parent)) ); // Adding a parent ready for slot before root silently returns false - assert!(!vote_history.add_parent_ready(1, block_id_0)); + assert!(!vote_history.add_parent_ready(1, block_0)); } #[test] @@ -527,7 +552,10 @@ mod test { let vote_history_storage = FileVoteHistoryStorage::new(tmp_dir.path().to_path_buf()); // Add Notarize on 1 and Skip on 2 - let vote_1 = Vote::new_notarization_vote(1, Hash::new_unique()); + let vote_1 = Vote::new_notarization_vote(Block { + slot: 1, + block_id: Hash::new_unique(), + }); let vote_2 = Vote::new_skip_vote(2); vote_history.add_vote(vote_1); vote_history.add_vote(vote_2); diff --git a/votor/src/voting_utils.rs b/votor/src/voting_utils.rs index 4bbdd85532c..b6190adf116 100644 --- a/votor/src/voting_utils.rs +++ b/votor/src/voting_utils.rs @@ -306,6 +306,7 @@ pub fn generate_vote_message( mod tests { use { super::*, + agave_votor_messages::consensus_message::Block, crossbeam_channel::unbounded, solana_hash::Hash, solana_runtime::{ @@ -402,7 +403,10 @@ mod tests { // Generate a normal notarization vote and check it's sent out correctly. let block_id = Hash::new_unique(); let vote_slot = 2; - let vote = Vote::new_notarization_vote(vote_slot, block_id); + let vote = Vote::new_notarization_vote(Block { + slot: vote_slot, + block_id, + }); let result = generate_vote_message(vote, false, &mut voting_context) .ok() .unwrap() @@ -515,7 +519,10 @@ mod tests { // Wrong identity keypair should return HotSpare based on rank_map.node_pubkey. let wrong_identity_keypair = Arc::new(Keypair::new()); - let vote = Vote::new_notarization_vote(6, Hash::new_unique()); + let vote = Vote::new_notarization_vote(Block { + slot: 6, + block_id: Hash::new_unique(), + }); assert!(matches!( generate_vote_tx( vote, @@ -543,7 +550,10 @@ mod tests { // Wrong vote account pubkey voting_context.vote_account_pubkey = Pubkey::new_unique(); - let vote = Vote::new_notarization_vote(7, Hash::new_unique()); + let vote = Vote::new_notarization_vote(Block { + slot: 7, + block_id: Hash::new_unique(), + }); assert!( generate_vote_message(vote, true, &mut voting_context) .unwrap() @@ -573,7 +583,10 @@ mod tests { setup_voting_context_and_bank_forks(own_vote_sender, &validator_keypairs, my_index); // If we try to vote for a slot in the future, we should panic - let vote = Vote::new_notarization_vote(1_000_000_000, Hash::new_unique()); + let vote = Vote::new_notarization_vote(Block { + slot: 1_000_000_000, + block_id: Hash::new_unique(), + }); let _ = generate_vote_message(vote, false, &mut voting_context); } @@ -628,7 +641,10 @@ mod tests { .root() .epoch_schedule() .get_first_slot_in_epoch(1); - let vote = Vote::new_notarization_vote(first_slot_in_epoch_1, Hash::new_unique()); + let vote = Vote::new_notarization_vote(Block { + slot: first_slot_in_epoch_1, + block_id: Hash::new_unique(), + }); assert!( generate_vote_message(vote, false, &mut voting_context) .unwrap() @@ -641,7 +657,10 @@ mod tests { .root() .epoch_schedule() .get_first_slot_in_epoch(2); - let vote = Vote::new_notarization_vote(first_slot_in_epoch_2, Hash::new_unique()); + let vote = Vote::new_notarization_vote(Block { + slot: first_slot_in_epoch_2, + block_id: Hash::new_unique(), + }); assert!( generate_vote_message(vote, false, &mut voting_context) .unwrap() diff --git a/votor/src/votor.rs b/votor/src/votor.rs index 42ae859199e..f943f6961b8 100644 --- a/votor/src/votor.rs +++ b/votor/src/votor.rs @@ -63,12 +63,11 @@ use { voting_service::BLSOp, voting_utils::VotingContext, }, - agave_votor_messages::consensus_message::ConsensusMessage, + agave_votor_messages::consensus_message::{Block, ConsensusMessage}, crossbeam_channel::{Receiver, Sender}, parking_lot::RwLock as PlRwLock, solana_clock::Slot, solana_gossip::cluster_info::ClusterInfo, - solana_hash::Hash, solana_keypair::Keypair, solana_ledger::{blockstore::Blockstore, leader_schedule_cache::LeaderScheduleCache}, solana_pubkey::Pubkey, @@ -110,7 +109,7 @@ pub struct VotorConfig { pub commitment_sender: Sender, pub bank_notification_sender: Option, pub leader_window_info_sender: Sender, - pub highest_parent_ready: Arc>, + pub highest_parent_ready: Arc>, pub event_sender: VotorEventSender, pub own_vote_sender: Sender>, pub repair_event_sender: RepairEventSender, @@ -128,7 +127,7 @@ pub(crate) struct SharedContext { pub(crate) bank_forks: Arc>, pub(crate) cluster_info: Arc, pub(crate) leader_window_info_sender: Sender, - pub(crate) highest_parent_ready: Arc>, + pub(crate) highest_parent_ready: Arc>, pub(crate) vote_history_storage: Arc, pub(crate) repair_event_sender: RepairEventSender, pub(crate) latest_switch_request: LatestSwitchRequest, From 570a49bb4e00f0c5b7bfb154d07acbd4e6bcf0b9 Mon Sep 17 00:00:00 2001 From: Greg Cusack Date: Thu, 4 Jun 2026 14:28:24 -0700 Subject: [PATCH 208/576] docs: add xdp setup details to docs (#12818) add xdp setup details to docs --- .../operations/best-practices/_category_.json | 2 +- docs/src/operations/guides/_category_.json | 2 +- docs/src/operations/running-with-af-xdp.md | 84 +++++++++++++++++++ 3 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 docs/src/operations/running-with-af-xdp.md diff --git a/docs/src/operations/best-practices/_category_.json b/docs/src/operations/best-practices/_category_.json index caf50117dc3..c26affc4d1b 100644 --- a/docs/src/operations/best-practices/_category_.json +++ b/docs/src/operations/best-practices/_category_.json @@ -1,5 +1,5 @@ { - "position": 7, + "position": 8, "label": "Best Practices", "collapsible": true, "collapsed": true, diff --git a/docs/src/operations/guides/_category_.json b/docs/src/operations/guides/_category_.json index 133867d4088..c399ad35754 100644 --- a/docs/src/operations/guides/_category_.json +++ b/docs/src/operations/guides/_category_.json @@ -1,5 +1,5 @@ { - "position": 8, + "position": 9, "label": "Validator Guides", "collapsible": true, "collapsed": true, diff --git a/docs/src/operations/running-with-af-xdp.md b/docs/src/operations/running-with-af-xdp.md new file mode 100644 index 00000000000..94b1f57b4a2 --- /dev/null +++ b/docs/src/operations/running-with-af-xdp.md @@ -0,0 +1,84 @@ +--- +title: Running with AF_XDP +sidebar_label: Running with AF_XDP +sidebar_position: 7 +--- + +XDP: eXpress Data Path is a Linux kernel technology that allows developers to write high-performance networking code which bypasses the kernel’s usual packet handling path. This means fewer data copies and fewer context switches between user and kernel space. By handling packets directly with the network interface card in user space, XDP greatly reduces the overhead per packet. + +Agave version 3.0.9 and later supports XDP for Turbine packet handling. XDP works most efficiently with a NIC that has XDP driver support. Early testing indicates that 100M-CU blocks are achievable if operators adopt XDP in validator operations, providing the headroom needed to scale block propagation. + +## Configuration + +Before rolling out XDP on a production validator, you should test it on your setup and verify a few things: +* **Driver Compatibility:** No unexpected NIC driver or hardware issues when XDP is enabled on your system. +* **Performance Gain:** Confirm that performance is improved with the new configuration (e.g. lower CPU usage or higher throughput in Turbine’s retransmit stage). +* **Metric Visibility:** Verify that you can observe the retransmit-stage metrics, which show time spent sending shreds, to gauge the impact of XDP on network transmission. + +To enable XDP in Agave, add the following command-line flags to your validator startup command (using Agave v3.0.9+): + +```bash +--experimental-retransmit-xdp-cpu-cores 1 +--experimental-retransmit-xdp-zero-copy # Do NOT pass this flag when using the bnxt_en driver. +--experimental-poh-pinned-cpu-core 10 +``` + +Note that --experimental-retransmit-xdp-zero-copy will avoid using socket buffers for data, but this is only possible when talking directly to the Network Interface Card (NIC). As a result, zero copy cannot be used with the bonded interface itself. When using a bonded network interface, specify the underlying member interface to which the XDP program should be attached: + +```bash +--experimental-retransmit-xdp-interface +``` + + Also note that XDP and PoH *must* be assigned to separate (physical) cores. The +--experimental-poh-pinned-cpu-core N flag can be used to move the PoH thread. + +Next, your validator binary will need to have access to a few higher level permissions. The validator process requires the CAP_NET_RAW, CAP_NET_ADMIN, CAP_BPF, and CAP_PERFMON capabilities. These can be configured in the systemd service file by setting CapabilityBoundingSet=CAP_NET_RAW CAP_NET_ADMIN CAP_BPF CAP_PERFMON under the [Service] section or directly on the binary with the command: + +```bash +sudo setcap cap_net_raw,cap_net_admin,cap_bpf,cap_perfmon=p +#this command must be run each time the binary is replaced +``` + +The setcap stores the updated privileges on the binary file, so this command will need to be rerun any time the binary is upgraded. + +## Conclusion + +Enabling XDP in Agave allows a validator to send data out more efficiently (fewer copies and syscalls), which translates to faster block propagation and more headroom for future growth. For now, we encourage a subset validator operators, especially nodes with XDP-capable NICs, to try it out and report any issues to #validator-support in the Solana Tech Discord. Thank you for contributing to Solana and helping the cluster prepare for 100M CUs. + +## Troubleshooting + +Some driver versions seem to return non-power of 2 queue sizes, which can cause issues. Setting these explicitly resolves the problem. For example, if the queues return a size of 511, forcing to 512 resolves things. + +```bash +sudo ethtool -G enp196s0f0np0 rx 512 tx 512 +``` + +The igb driver supports zero copy starting from kernel version 6.14. For all other drivers, kernel version 6.8 or newer is recommended. + +## Debug Data Collection + +When encountering issues, understanding the kernel, NIC, and driver information will be crucial to being able to debug issues + +```bash +uname -a +sudo lshw -c network | grep 'logical name' +sudo ethtool -i +sudo ethtool -g +lspci | grep Ethernet +modinfo bnxt_en +``` + + +## XDP / Zero-Copy Driver Support Matrix + +| Driver / NIC family | AF_XDP w/o ZC | AF_XDP w/ ZC | Status | +| --- | --- | --- | --- | +| `mlx5` / Mellanox ConnectX | ✅ Works | ✅ Works | Operator reports: `mlx5` works with XDP + ZC on kernel 6.8; ConnectX-6 Lx worked after kernel 6.17 upgrade. Highest-confidence family in the discussion. | +| `i40e` / Intel 700 series | ✅ Works | ✅ Works | Operator report: `i40e` works with XDP + ZC on kernel 6.8. | +| `igb` / Intel I210 | ✅ Works | ✅ Works w/ caveat | caveat: `igb` requires kernel `>= 6.14` for ZC. Field report: I210 on 6.17 enabled ZC but had severe network degradation/high skips, so fall back to non-ZC if unstable. | +| `ixgbe` / Intel X540, X550 | ✅ Works | ⚠️ Mixed / unstable | Alessandro guidance for freeze/link-flap cases: start without ZC while `ixgbe` is debugged. Stay tuned! | +| `ice` / Intel E800 | ✅ Works | ✅ Works | `ice` supports native XDP and AF_XDP zero-copy. Caveats: XDP is blocked for frame sizes larger than 3KB | +| `bnxt_en` / Broadcom | ✅ Works | ❌ Does not work | `bnxt_en` works with XDP, but do not pass the zero-copy flag. Broadcom non-ZC can still be reasonably fast. But please get a non-broadcom NIC | +| `tg3` / Broadcom | ❌ No native/driver XDP; generic XDP only at best | ❌ Does not work | Broadcom BCM5720 uses the `tg3` driver. Treat as unsupported for Agave/AF_XDP performance work: no native XDP and no AF_XDP zero-copy. | +| `r8169` / Realtek | ❌ No native/driver XDP; generic XDP only at best | ❌ Does not work | Realtek NICs using `r8169` should be treated as unsupported for Agave/AF_XDP performance work: no native XDP and no AF_XDP zero-copy.| +| `mlx4_en` / Mellanox ConnectX-3 | ❌ Do not use | ❌ Does not work | Driver is no longer supported. Zero-copy does not work. Do not use. | From 08b88945c6d6531b3734c4fc80e78cb5b3fbf3ee Mon Sep 17 00:00:00 2001 From: febo Date: Fri, 5 Jun 2026 00:28:14 +0100 Subject: [PATCH 209/576] program-binaries: Use latest p-token tag (#12948) Use latest p-token tag --- fetch-spl.sh | 2 +- program-binaries/src/lib.rs | 2 +- ...l_p_token-1.0.0-rc.1.so => spl_p_token-1.0.0.so} | Bin 3 files changed, 2 insertions(+), 2 deletions(-) rename program-binaries/src/programs/{spl_p_token-1.0.0-rc.1.so => spl_p_token-1.0.0.so} (100%) diff --git a/fetch-spl.sh b/fetch-spl.sh index d87508a0ea8..722bbc0f756 100755 --- a/fetch-spl.sh +++ b/fetch-spl.sh @@ -39,7 +39,7 @@ add_spl_program_to_fetch() { programs+=("$name $version $address $loader $download_url") } -add_spl_program_to_fetch token 1.0.0-rc.1 TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA BPFLoaderUpgradeab1e11111111111111111111111 p-token +add_spl_program_to_fetch token 1.0.0 TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA BPFLoaderUpgradeab1e11111111111111111111111 p-token add_spl_program_to_fetch token-2022 10.0.0 TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb BPFLoaderUpgradeab1e11111111111111111111111 add_spl_program_to_fetch memo 1.0.0 Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo BPFLoader1111111111111111111111111111111111 add_spl_program_to_fetch memo 3.0.0 MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr BPFLoader2111111111111111111111111111111111 diff --git a/program-binaries/src/lib.rs b/program-binaries/src/lib.rs index d427caf242e..24f163aaddc 100644 --- a/program-binaries/src/lib.rs +++ b/program-binaries/src/lib.rs @@ -23,7 +23,7 @@ static SPL_PROGRAMS: &[(Pubkey, Pubkey, &[u8])] = &[ ( spl_generic_token::token::ID, solana_sdk_ids::bpf_loader_upgradeable::ID, - include_bytes!("programs/spl_p_token-1.0.0-rc.1.so"), + include_bytes!("programs/spl_p_token-1.0.0.so"), ), ( spl_generic_token::token_2022::ID, diff --git a/program-binaries/src/programs/spl_p_token-1.0.0-rc.1.so b/program-binaries/src/programs/spl_p_token-1.0.0.so similarity index 100% rename from program-binaries/src/programs/spl_p_token-1.0.0-rc.1.so rename to program-binaries/src/programs/spl_p_token-1.0.0.so From 066c1d7d595a29fff65e88fa78ffeab4276bf342 Mon Sep 17 00:00:00 2001 From: Yihau Chen Date: Fri, 5 Jun 2026 10:17:16 +0800 Subject: [PATCH 210/576] ci: get repo info from BUILDKITE_REPO in xtask pipeline generator (#12914) --- ci/xtask/src/commands/generate_pipeline.rs | 95 +++++++++++++++++++--- 1 file changed, 85 insertions(+), 10 deletions(-) diff --git a/ci/xtask/src/commands/generate_pipeline.rs b/ci/xtask/src/commands/generate_pipeline.rs index f3a9853de7e..415902165a0 100644 --- a/ci/xtask/src/commands/generate_pipeline.rs +++ b/ci/xtask/src/commands/generate_pipeline.rs @@ -24,6 +24,54 @@ pub enum Pipeline { Private, } +struct Repo { + owner: String, + name: String, +} + +impl Repo { + /// Resolve the GitHub repo from Buildkite's built-in `BUILDKITE_REPO`, + /// falling back to the agave repo when it is unset or unparseable. + fn from_env() -> Self { + match env::var("BUILDKITE_REPO") + .ok() + .and_then(|url| Self::parse_github_url(&url)) + { + Some(repo) => { + info!( + "Resolved repo {}/{} from `BUILDKITE_REPO`", + repo.owner, repo.name + ); + repo + } + None => { + info!("Falling back to default repo anza-xyz/agave"); + Repo { + owner: String::from("anza-xyz"), + name: String::from("agave"), + } + } + } + } + + /// Extract `owner` and `name` from a GitHub remote URL, handling both the + /// HTTPS (`https://github.com/owner/name.git`) and scp-like SSH + /// (`git@github.com:owner/name.git`) forms. + fn parse_github_url(url: &str) -> Option { + let trimmed = url.trim().trim_end_matches('/'); + let trimmed = trimmed.strip_suffix(".git").unwrap_or(trimmed); + // Both `/` and `:` separate the path, so splitting on either yields the + // trailing `.../owner/name` regardless of URL flavor. + let mut parts = trimmed.rsplit(['/', ':']); + let name = parts.next().filter(|s| !s.is_empty())?; + let owner = parts.next().filter(|s| !s.is_empty())?; + Some(Repo { + owner: owner.to_string(), + name: name.to_string(), + }) + } +} + pub async fn run(args: CommandArgs) -> Result<()> { let pipeline = match args.pipeline { Pipeline::Agave => generate_agave_pipeline().await?, @@ -55,8 +103,9 @@ async fn generate_agave_pipeline() -> Result { .parse::() .map_err(|e| anyhow::anyhow!("failed to parse PR number: {e}"))?; - annotate_pull_request(pr_number)?; - return generate_pull_request_pipeline(pr_number).await; + let repo = Repo::from_env(); + annotate_pull_request(&repo, pr_number)?; + return generate_pull_request_pipeline(&repo, pr_number).await; } info!("failed to get PR number from branch: {branch}, running full pipeline."); @@ -102,7 +151,7 @@ fn generate_private_pipeline() -> Result { Ok(pipeline) } -fn annotate_pull_request(pr_number: u64) -> Result<()> { +fn annotate_pull_request(repo: &Repo, pr_number: u64) -> Result<()> { let is_ci = env::var("CI") .map(|value| matches!(value.to_ascii_lowercase().as_str(), "1" | "true")) .unwrap_or(false); @@ -110,8 +159,10 @@ fn annotate_pull_request(pr_number: u64) -> Result<()> { return Ok(()); } - let annotation = - format!("Github Pull Request: https://github.com/anza-xyz/agave/pull/{pr_number}"); + let annotation = format!( + "Github Pull Request: https://github.com/{}/{}/pull/{pr_number}", + repo.owner, repo.name + ); let status = Command::new("buildkite-agent") .args([ @@ -140,7 +191,7 @@ fn generate_merge_queue_pipeline() -> Result { Ok(pipeline) } -async fn get_changed_files(pr_number: u64) -> Result> { +async fn get_changed_files(repo: &Repo, pr_number: u64) -> Result> { let mut changed_files = vec![]; let github_client_builder = match env::var("GH_TOKEN") { Ok(token) if !token.trim().is_empty() => { @@ -157,7 +208,7 @@ async fn get_changed_files(pr_number: u64) -> Result> { }; let github_client = github_client_builder.build()?; let stream = github_client - .pulls("anza-xyz", "agave") + .pulls(&repo.owner, &repo.name) .list_files(pr_number) .await? .into_stream(&github_client); @@ -291,8 +342,11 @@ impl PullRequestPipelineFlags { } } -pub async fn generate_pull_request_pipeline(pr_number: u64) -> Result { - let changed_files = get_changed_files(pr_number).await?; +async fn generate_pull_request_pipeline( + repo: &Repo, + pr_number: u64, +) -> Result { + let changed_files = get_changed_files(repo, pr_number).await?; let flags = PullRequestPipelineFlags::from_changed_files(&changed_files); let mut pipeline = buildkite::Pipeline::new(); @@ -679,11 +733,32 @@ fn default_trigger_secondary_step() -> buildkite::Step { mod tests { use {super::*, pretty_assertions::assert_eq}; + #[test] + fn test_parse_github_url() { + for url in [ + "https://github.com/anza-xyz/agave.git", + "https://github.com/anza-xyz/agave", + "git@github.com:anza-xyz/agave.git", + "git@github.com:anza-xyz/agave", + "ssh://git@github.com/anza-xyz/agave.git", + " https://github.com/anza-xyz/agave.git/ ", + ] { + let repo = + Repo::parse_github_url(url).unwrap_or_else(|| panic!("failed to parse url: {url}")); + assert_eq!(repo.owner, "anza-xyz", "url: {url}"); + assert_eq!(repo.name, "agave", "url: {url}"); + } + } + // PR 1850 is a good large PR for testing #[cfg_attr(not(feature = "integration-tests"), ignore = "requires github api")] #[tokio::test] async fn test_get_changed_files_for_pr_1850() { - let changed_files = get_changed_files(1850).await.unwrap(); + let repo = Repo { + owner: String::from("anza-xyz"), + name: String::from("agave"), + }; + let changed_files = get_changed_files(&repo, 1850).await.unwrap(); assert_eq!(changed_files.len(), 68); assert!(changed_files.contains(&String::from("Cargo.lock"))); assert!(changed_files.contains(&String::from("Cargo.toml"))); From 702ae7159bfbaa342d520fce231271d84225dbdf Mon Sep 17 00:00:00 2001 From: Yihau Chen Date: Fri, 5 Jun 2026 18:29:34 +0800 Subject: [PATCH 211/576] ci: improve crate check instructions (#12970) update crate check instructions --- ci/check-crates.sh | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/ci/check-crates.sh b/ci/check-crates.sh index fa62985d7f9..e58bb24fe1a 100755 --- a/ci/check-crates.sh +++ b/ci/check-crates.sh @@ -106,14 +106,20 @@ for file in "${files[@]}"; do or -2. make a dummy publication. +2. make a dummy publication to reserve the name (requires your own crates.io token). - example: - scripts/reserve-cratesio-package-name.sh \ - --token \ - lib solana-new-lib-crate + a) publish the placeholder crate: - see also: scripts/reserve-cratesio-package-name.sh --help + scripts/reserve-cratesio-package-name.sh \ + --token \ + lib solana-new-lib-crate + + see also: scripts/reserve-cratesio-package-name.sh --help + + b) send a crate owner invitation to anza-team + + c) once your PR is ready to merge, ping the devops team to accept the + invitation. " else ((error_count++)) From 40cc9ece974d19075ad31dec064bc7ce0fe96031 Mon Sep 17 00:00:00 2001 From: Yihau Chen Date: Fri, 5 Jun 2026 18:29:36 +0800 Subject: [PATCH 212/576] ci: rename crate-check to Crate Check (#12971) --- .github/workflows/crate-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/crate-check.yml b/.github/workflows/crate-check.yml index 419f19b0298..e7db9c251cf 100644 --- a/.github/workflows/crate-check.yml +++ b/.github/workflows/crate-check.yml @@ -1,4 +1,4 @@ -name: crate-check +name: Crate Check on: push: From 4da4a28081a70ee92e9b01f2332fa310ff375269 Mon Sep 17 00:00:00 2001 From: Yihau Chen Date: Fri, 5 Jun 2026 18:38:38 +0800 Subject: [PATCH 213/576] ci: use github app token in dependabot pr (#12942) --- .github/workflows/dependabot-pr.yml | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/.github/workflows/dependabot-pr.yml b/.github/workflows/dependabot-pr.yml index 7280cbe302e..d634c0109ad 100644 --- a/.github/workflows/dependabot-pr.yml +++ b/.github/workflows/dependabot-pr.yml @@ -8,14 +8,33 @@ on: jobs: dependabot: timeout-minutes: 10 + environment: dependency-update runs-on: ubuntu-24.04 if: github.event.pull_request.user.login == 'dependabot[bot]' steps: + - name: Create github app token + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 + id: app-token + with: + client-id: ${{ vars.CLIENT_ID }} + private-key: ${{ secrets.PRIVATE_KEY }} + + - name: Set git config + run: | + git config --global user.email "${APP_ID}+${APP_SLUG}[bot]@users.noreply.github.com" + git config --global user.name "${APP_SLUG}[bot]" + git config --global url."https://x-access-token:${GITHUB_TOKEN}@github.com/".insteadOf https://github.com/ + env: + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} + APP_ID: ${{ vars.APP_ID }} + APP_SLUG: ${{ steps.app-token.outputs.app-slug }} + - name: checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.event.pull_request.head.ref }} - token: ${{ secrets.PAT }} + token: ${{ steps.app-token.outputs.token }} + persist-credentials: false - name: update code env: @@ -45,8 +64,6 @@ jobs: if [ -z "$(git status --porcelain)" ]; then echo "✅ Nothing to update" else - git config user.email "49699333+dependabot[bot]@users.noreply.github.com" - git config user.name "dependabot[bot]" git add **/Cargo.toml git add **/Cargo.lock git commit -am "Update all workspaces" From 9d35c66c18e4a94e465068443fe5711b849283ac Mon Sep 17 00:00:00 2001 From: Brennan Date: Fri, 5 Jun 2026 05:05:25 -0700 Subject: [PATCH 214/576] SIMD-0525: Slot Time Reductions (#12941) Reapply "SIMD-0525: Slot Time Reductions (#12264)" (#12940) This reverts commit dead7e0c71bf6ce445baca2f82b1a45812e920cc. fix: hpt divergence --- accounts-db/src/partitioned_rewards.rs | 25 +- feature-set/src/lib.rs | 32 ++ ledger/src/shred/filter.rs | 55 ++- runtime/src/bank.rs | 201 ++++++++--- .../src/bank/partitioned_epoch_rewards/mod.rs | 5 + runtime/src/bank/tests.rs | 336 +++++++++++++++++- .../epoch_inflation_account_state.rs | 101 ++++-- runtime/src/slot_params.rs | 210 +++++++++-- .../broadcast_stage/standard_broadcast_run.rs | 128 ++++--- 9 files changed, 937 insertions(+), 156 deletions(-) diff --git a/accounts-db/src/partitioned_rewards.rs b/accounts-db/src/partitioned_rewards.rs index 85758a4df84..e6565c2c2c9 100644 --- a/accounts-db/src/partitioned_rewards.rs +++ b/accounts-db/src/partitioned_rewards.rs @@ -1,16 +1,22 @@ //! Code related to partitioned rewards distribution -/// # stake accounts to store in one block during partitioned reward interval -/// Target to store 64 rewards per entry/tick in a block. A block has a minimum of 64 -/// entries/tick. This gives 4096 total rewards to store in one block. -/// This constant affects consensus. -const MAX_PARTITIONED_REWARDS_PER_BLOCK: u64 = 4096; +/// Baseline number of stake accounts to store in one 400ms block during the +/// partitioned reward interval. +/// +/// The target is 64 rewards per entry/tick. A block has a minimum of 64 +/// entries/ticks, giving 4096 total rewards to store in one 400ms block. This +/// constant affects consensus; shorter slot-time targets scale this value down +/// in `Bank` state. +pub const MAX_PARTITIONED_REWARDS_PER_BLOCK: u64 = 4096; -#[derive(Debug, Clone, Copy)] /// Configuration options for partitioned epoch rewards. +#[derive(Debug, Clone, Copy)] pub struct PartitionedEpochRewardsConfig { - /// number of stake accounts to store in one block during partitioned reward interval - /// normally, this is a number tuned for reasonable performance, such as 4096 accounts/block + /// Baseline number of stake accounts to store in one block during the + /// partitioned reward interval. + /// + /// This value is stored as the 400ms-slot baseline. Runtime `Bank` state + /// derives the effective per-bank value from the active slot-time target. pub stake_account_stores_per_block: u64, } @@ -30,7 +36,8 @@ impl Default for PartitionedEpochRewardsConfig { } impl PartitionedEpochRewardsConfig { - /// Only for tests and benchmarks + /// Constructs a config with an explicit 400ms-slot baseline for tests and + /// benchmarks. pub fn new_for_test(stake_account_stores_per_block: u64) -> Self { Self { stake_account_stores_per_block, diff --git a/feature-set/src/lib.rs b/feature-set/src/lib.rs index 77711b1c83b..1a3ae6bb64f 100644 --- a/feature-set/src/lib.rs +++ b/feature-set/src/lib.rs @@ -1529,6 +1529,22 @@ pub mod set_lamports_per_byte_to_6960 { pub const LAMPORTS_PER_BYTE: u64 = 6960; } +pub mod reduce_slot_time_to_350ms { + solana_pubkey::declare_id!("iBRL2iJvhLssJveF1utbmmQGmjonmNYZALcJFEHTbUF"); +} + +pub mod reduce_slot_time_to_300ms { + solana_pubkey::declare_id!("iBRLA3zvd6x9445cK1vS7xt8n6Y7DS3otfRcDdW8JRW"); +} + +pub mod reduce_slot_time_to_250ms { + solana_pubkey::declare_id!("iBRLR6nG3fDi8YD4mpPTUVTgo5NaiYfZgrzokCfADP2"); +} + +pub mod reduce_slot_time_to_200ms { + solana_pubkey::declare_id!("iBRLypKvvj9VEvwoTeRpbLhbW55NFR4T3GE9BUR8A16"); +} + pub static FEATURE_NAMES: LazyLock> = LazyLock::new(|| { [ (secp256k1_program_enabled::id(), "secp256k1 program"), @@ -2419,6 +2435,22 @@ pub static FEATURE_NAMES: LazyLock> = LazyLock::n alpenglow::id(), "SIMD-0326: Alpenglow: new consensus algorithm", ), + ( + reduce_slot_time_to_350ms::id(), + "SIMD-0525: Reduce slot time to 350ms", + ), + ( + reduce_slot_time_to_300ms::id(), + "SIMD-0525: Reduce slot time to 300ms", + ), + ( + reduce_slot_time_to_250ms::id(), + "SIMD-0525: Reduce slot time to 250ms", + ), + ( + reduce_slot_time_to_200ms::id(), + "SIMD-0525: Reduce slot time to 200ms", + ), ( disable_zk_elgamal_proof_program::id(), "Disables zk-elgamal-proof program", diff --git a/ledger/src/shred/filter.rs b/ledger/src/shred/filter.rs index a78b3ad4dc3..65df4c45821 100644 --- a/ledger/src/shred/filter.rs +++ b/ledger/src/shred/filter.rs @@ -595,7 +595,10 @@ mod tests { itertools::Itertools, solana_leader_schedule::SlotLeader, solana_perf::packet::{Packet, PacketFlags}, - solana_runtime::bank::Bank, + solana_runtime::{ + bank::Bank, + slot_params::{slot_time_feature_gates, slot_time_feature_ids}, + }, std::{ io::{Cursor, Seek, SeekFrom, Write}, sync::Arc, @@ -620,6 +623,26 @@ mod tests { } } + fn deactivate_slot_time_features(bank: &mut Bank) { + for feature_id in slot_time_feature_ids() { + bank.deactivate_feature(&feature_id); + } + } + + fn shred_filter_for_tests( + feature_ids: impl IntoIterator, + ) -> (ShredFilterContext, Slot) { + let genesis_config = create_genesis_config(1).genesis_config; + let mut root_bank = Bank::new_for_tests(&genesis_config); + deactivate_slot_time_features(&mut root_bank); + for feature_id in feature_ids { + root_bank.activate_feature(&feature_id); + } + let first_epoch_slot = root_bank.epoch_schedule().get_first_slot_in_epoch(1); + let (root_bank, _) = root_bank.wrap_with_bank_forks_for_tests(); + (ShredFilterContext::new(root_bank, 0), first_epoch_slot) + } + #[test_case(true ; "last_in_slot")] #[test_case(false ; "not_last_in_slot")] fn test_should_discard_shred(is_last_in_slot: bool) { @@ -1046,6 +1069,36 @@ mod tests { } } + #[test] + fn test_shred_limit_for_slot_times() { + for (features, shred_limits) in std::iter::once((vec![], ShredLimits::DEFAULT)).chain( + slot_time_feature_gates().map(|(feature_id, params)| { + ( + vec![feature_id], + ShredLimits::new( + params.max_data_shreds_per_slot(), + params.max_code_shreds_per_slot(), + ), + ) + }), + ) { + let (shred_filter, effective_slot) = shred_filter_for_tests(features); + assert_eq!( + shred_filter.shred_limits(effective_slot.saturating_sub(1)), + ShredLimits::DEFAULT + ); + assert_eq!(shred_filter.shred_limits(effective_slot), shred_limits); + } + + let [reduce_to_350ms, _, _, reduce_to_200ms] = slot_time_feature_ids(); + let (shred_filter, effective_slot) = + shred_filter_for_tests([reduce_to_350ms, reduce_to_200ms]); + assert_eq!( + shred_filter.shred_limits(effective_slot), + ShredLimits::new(16_384, 16_384) + ); + } + #[test] fn test_data_complete_shred_index_validation() { agave_logger::setup(); diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 3903b0a0898..5b7aec6608d 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -112,7 +112,7 @@ use { solana_cluster_type::ClusterType, solana_compute_budget::compute_budget::ComputeBudget, solana_compute_budget_instruction::instructions_processor::process_compute_budget_instructions, - solana_cost_model::{block_cost_limits::simd_0286_block_limit, cost_tracker::CostTracker}, + solana_cost_model::cost_tracker::CostTracker, solana_epoch_info::EpochInfo, solana_epoch_schedule::EpochSchedule, solana_feature_gate_interface as feature, @@ -179,7 +179,6 @@ use { solana_system_transaction as system_transaction, solana_sysvar::{self as sysvar, SysvarSerialize, last_restart_slot::LastRestartSlot}, solana_sysvar_id::SysvarId, - solana_time_utils::years_as_slots, solana_transaction::{ Transaction, TransactionVerificationMode, sanitized::{MAX_TX_ACCOUNT_LOCKS, MessageHash, SanitizedTransaction}, @@ -2180,27 +2179,10 @@ impl Bank { snapshot and genesis.bin might pertain to different clusters" ); assert_eq!(bank.ticks_per_slot, genesis_config.ticks_per_slot); - assert_eq!( - bank.ns_per_slot, - genesis_config.poh_config.target_tick_duration.as_nanos() - * genesis_config.ticks_per_slot as u128 - ); assert_eq!(bank.max_tick_height, (bank.slot + 1) * bank.ticks_per_slot); - assert_eq!( - bank.slots_per_year, - years_as_slots( - 1.0, - &genesis_config.poh_config.target_tick_duration, - bank.ticks_per_slot, - ) - ); assert_eq!(bank.epoch_schedule, genesis_config.epoch_schedule); - bank.refresh_slot_params_with_baseline(Self::genesis_config_slot_params( - genesis_config, - bank.partitioned_rewards_stake_account_stores_per_block, - )); - + bank.refresh_slot_params_from_snapshot(genesis_config); bank.initialize_after_snapshot_restore(|| rewards_calculation_thread_pool); datapoint_info!( @@ -2710,12 +2692,25 @@ impl Bank { }); } + /// Rebuilds slot-param state from the current feature set. + fn refresh_slot_params(&mut self) { + self.refresh_slot_params_with_baseline(self.slot_params.baseline_params()); + } + + fn refresh_slot_params_from_snapshot(&mut self, genesis_config: &GenesisConfig) { + let (feature_set, _) = self.compute_active_feature_set(false); + self.refresh_slot_params_with_baseline( + self.snapshot_restore_slot_params_baseline(genesis_config, &feature_set), + ); + } + /// Rebuilds cached slot params while preserving the supplied slot-0 baseline. /// /// The cache is not serialized into snapshots; it is reconstructed from /// existing Bank fields during genesis and snapshot restore. fn refresh_slot_params_with_baseline(&mut self, baseline_params: SlotParams) { - self.slot_params = SlotParamsArchive::new(&self.epoch_schedule, baseline_params); + self.slot_params = + SlotParamsArchive::new(&self.feature_set, &self.epoch_schedule, baseline_params); } /// Builds slot-0 params from the genesis config. @@ -2731,11 +2726,72 @@ impl Bank { ) } + /// Builds the slot-param baseline from the restored bank fields. + /// + /// Snapshot fields represent the cluster's current reality. This can + /// differ from genesis for values, such as `hashes_per_tick`, that were + /// changed by older feature gates before slot-time reductions existed. + fn restored_bank_slot_params(&self) -> SlotParams { + SlotParams::genesis_baseline( + self.ns_per_slot, + self.slots_per_year, + self.hashes_per_tick(), + self.partitioned_rewards_stake_account_stores_per_block, + ) + } + + /// Returns true if any slot-time reduction has taken effect by this bank. + /// + /// Feature activation happens in one epoch, but slot params become effective + /// at the start of the following epoch. + fn any_slot_time_reduction_effective( + &self, + feature_set: &FeatureSet, + ns_per_slot: u128, + ) -> bool { + SlotParamsArchive::any_slot_time_reduction_effective( + &self.epoch_schedule, + self.slot, + feature_set, + ns_per_slot, + ) + } + + /// Selects the slot-param baseline to use when reconstructing from snapshot. + /// + /// Before any slot-time reduction is effective, the baseline should match + /// restored bank fields because historical non-slot-time feature gates may + /// have already changed some values away from genesis. Once a slot-time + /// reduction is effective, keep the genesis baseline so historical lookups + /// for pre-reduction slots remain correct. + fn snapshot_restore_slot_params_baseline( + &self, + genesis_config: &GenesisConfig, + feature_set: &FeatureSet, + ) -> SlotParams { + if self.any_slot_time_reduction_effective(feature_set, genesis_config.ns_per_slot()) { + Self::genesis_config_slot_params( + genesis_config, + self.partitioned_rewards_stake_account_stores_per_block, + ) + } else { + // Default to whatever is in the bank if we've never enabled any + // slot time reductions. This prevents resetting any slot params + // that may have been changed previously back to genesis. + self.restored_bank_slot_params() + } + } + /// Returns the slot params effective at `slot`. fn slot_params_at_slot(&self, slot: Slot) -> SlotParams { self.slot_params.params_at_slot(slot) } + /// Returns the slot params that should be effective for this bank's slot. + fn current_slot_params(&self) -> SlotParams { + self.slot_params_at_slot(self.slot) + } + /// Returns the effective slot duration for `slot`. pub fn ns_per_slot_at_slot(&self, slot: Slot) -> u128 { self.slot_params_at_slot(slot).ns_per_slot() @@ -4676,22 +4732,78 @@ impl Bank { self.rc.accounts.clone() } + /// Recomputes cost tracker limits from active feature state. fn apply_cost_tracker_limits_for_active_features(&mut self) { + let params = self.current_slot_params(); + let (account_cost_limit, block_cost_limit, data_size_limit) = + params.cost_limits(self.feature_set.snapshot().raise_block_limits_to_100m); + let mut cost_tracker = self.write_cost_tracker().unwrap(); - let block_cost_limit = if self.feature_set.snapshot().raise_block_limits_to_100m { - simd_0286_block_limit() - } else { - cost_tracker.get_block_limit() - }; - let account_cost_limit = block_cost_limit.saturating_mul(40).saturating_div(100); - let allocated_data_size_limit = cost_tracker.get_allocated_data_size_limit(); - cost_tracker.set_limits( - account_cost_limit, - block_cost_limit, - allocated_data_size_limit, + cost_tracker.set_limits(account_cost_limit, block_cost_limit, data_size_limit); + } + + /// Recomputes this bank's effective partitioned-reward write budget. + fn apply_partitioned_epoch_rewards_config_for_active_features(&mut self) { + self.partitioned_rewards_stake_account_stores_per_block = self + .current_slot_params() + .partitioned_epoch_rewards_stake_account_stores_per_block(); + } + + /// Applies slot-time changes for fields serialized into snapshots. + fn apply_slot_time_persistent_changes(&mut self) { + let params = self.current_slot_params(); + self.ns_per_slot = params.ns_per_slot(); + self.slots_per_year = params.slots_per_year(); + self.rent_collector.slots_per_year = params.slots_per_year(); + if !self.feature_set.is_active(&feature_set::alpenglow::id()) + && self.hashes_per_tick().is_some() + { + self.set_hashes_per_tick(params.hashes_per_tick()); + } + } + + /// Verifies bank fields are consistent with current slot params. + fn assert_bank_matches_slot_params(&self) { + let params = self.current_slot_params(); + assert_eq!( + self.ns_per_slot, + params.ns_per_slot(), + "snapshot slot-time ns_per_slot mismatch" + ); + assert_eq!( + self.slots_per_year.to_bits(), + params.slots_per_year().to_bits(), + "snapshot slot-time slots_per_year mismatch" + ); + assert_eq!( + self.rent_collector.slots_per_year.to_bits(), + params.slots_per_year().to_bits(), + "snapshot slot-time rent_collector.slots_per_year mismatch" + ); + let hashes_per_tick = self.hashes_per_tick(); + if !self.feature_set.is_active(&feature_set::alpenglow::id()) && hashes_per_tick.is_some() { + assert_eq!( + hashes_per_tick, + params.hashes_per_tick(), + "snapshot slot-time hashes_per_tick mismatch" + ); + } + assert_eq!( + self.entry_bytes_budget().slot_limit(), + params.max_entry_bytes_per_slot(), + "snapshot slot-time entry byte budget mismatch" ); } + /// Applies slot-time changes for runtime-only fields. This function is + /// expected to be idempotent. + fn apply_slot_time_runtime_changes(&mut self) { + self.entry_bytes_consumed = + EntryBytesBudget::new(self.current_slot_params().max_entry_bytes_per_slot()); + self.apply_cost_tracker_limits_for_active_features(); + self.apply_partitioned_epoch_rewards_config_for_active_features(); + } + fn apply_simd_0339_invoke_cost_changes(&mut self) { let simd_0268_active = self.feature_set.snapshot().raise_cpi_nesting_limit_to_8; let compute_budget = self @@ -4713,11 +4825,10 @@ impl Bank { Arc::new(reserved_keys) }; - // Cost-Tracker is not serialized in snapshot or any configs. - // We must apply previously activated features related to limits here - // so that the initial bank state is consistent with the feature set. - // Cost-tracker limits are propagated through children banks. - self.apply_cost_tracker_limits_for_active_features(); + // Many fields are not serialized in snapshot or any configs. Rebuild + // them from the feature set so the initial bank state is consistent. + self.refresh_slot_params(); + self.apply_slot_time_runtime_changes(); self.apply_simd_0339_invoke_cost_changes(); let program_runtime_environment = @@ -5812,12 +5923,14 @@ impl Bank { let mut feature_set = Arc::make_mut(&mut self.feature_set).clone(); feature_set.deactivate(id); self.feature_set = Arc::new(feature_set); + self.refresh_slot_params(); } pub fn activate_feature(&mut self, id: &Pubkey) { let mut feature_set = Arc::make_mut(&mut self.feature_set).clone(); feature_set.activate(id, 0); self.feature_set = Arc::new(feature_set); + self.refresh_slot_params(); } pub fn fill_bank_with_ticks_for_tests(&self) { @@ -5893,6 +6006,7 @@ impl Bank { self.feature_set = Arc::new(feature_set); self.apply_activated_features(); + self.assert_bank_matches_slot_params(); } /// This is called from each epoch boundary @@ -5901,6 +6015,7 @@ impl Bank { let (feature_set, new_feature_activations) = self.compute_active_feature_set(include_pending); self.feature_set = Arc::new(feature_set); + self.refresh_slot_params(); // Update activation slot of features in `new_feature_activations` for feature_id in new_feature_activations.iter() { @@ -5983,10 +6098,11 @@ impl Bank { self.fee_rate_governor.burn_percent = solana_fee_calculator::DEFAULT_BURN_PERCENT; } + // Apply unconditionally: this is relatively cheap and idempotent. + self.apply_slot_time_persistent_changes(); + self.apply_slot_time_runtime_changes(); + self.apply_new_builtin_program_feature_transitions(&new_feature_activations); - if new_feature_activations.contains(&feature_set::raise_block_limits_to_100m::id()) { - self.apply_cost_tracker_limits_for_active_features(); - } if new_feature_activations.contains(&feature_set::replace_spl_token_with_p_token::id()) { if let Err(e) = self.upgrade_loader_v2_program_with_loader_v3_program( @@ -6941,6 +7057,11 @@ impl Bank { minimized_account_set.insert(*pubkey); }); } + + /// Returns true when this bank is using slot params beyond its genesis baseline. + pub fn slot_time_reduction_active(&self) -> bool { + self.ns_per_slot != self.slot_params.baseline_params().ns_per_slot() + } } /// Returns a thread pool intended to be used for reward calculation. This diff --git a/runtime/src/bank/partitioned_epoch_rewards/mod.rs b/runtime/src/bank/partitioned_epoch_rewards/mod.rs index 9218ceac4e7..e7492dbc4a6 100644 --- a/runtime/src/bank/partitioned_epoch_rewards/mod.rs +++ b/runtime/src/bank/partitioned_epoch_rewards/mod.rs @@ -417,6 +417,7 @@ mod tests { bank_forks::BankForks, genesis_utils::{ GenesisConfigInfo, ValidatorVoteKeypairs, create_genesis_config_with_vote_accounts, + deactivate_features, }, runtime_config::RuntimeConfig, stake_utils, @@ -574,6 +575,9 @@ mod tests { stake_account_stores_per_block: u64, advance_num_slots: u64, ) -> (RewardBank, Arc>) { + // Disable slot time reduction features as they will override the custom + // stores per block provided in this test helper. + let features_to_deactivate = crate::slot_params::slot_time_feature_ids().to_vec(); let validator_keypairs = (0..stakes.len()) .map(|_| ValidatorVoteKeypairs::new_rand()) .collect::>(); @@ -582,6 +586,7 @@ mod tests { mut genesis_config, .. } = create_genesis_config_with_vote_accounts(1_000_000_000, &validator_keypairs, stakes); genesis_config.epoch_schedule = EpochSchedule::new(SLOTS_PER_EPOCH); + deactivate_features(&mut genesis_config, &features_to_deactivate); let mut accounts_db_config: AccountsDbConfig = ACCOUNTS_DB_CONFIG_FOR_TESTING; accounts_db_config.partitioned_epoch_rewards_config = diff --git a/runtime/src/bank/tests.rs b/runtime/src/bank/tests.rs index 0352704a421..12f7d48811a 100644 --- a/runtime/src/bank/tests.rs +++ b/runtime/src/bank/tests.rs @@ -19,7 +19,11 @@ use { }, runtime_config::RuntimeConfig, serde_snapshot::fields_from_stream, - slot_params::{DEFAULT_MAX_ENTRY_BYTES_PER_SLOT, LEGACY_SLOT_PARAMS}, + slot_params::{ + DEFAULT_MAX_ENTRY_BYTES_PER_SLOT, LEGACY_HASHES_PER_TICK, LEGACY_SLOT_PARAMS, + SLOT_PARAMS_200MS, SLOT_PARAMS_250MS, SLOT_PARAMS_300MS, SLOT_PARAMS_350MS, SlotParams, + slot_time_feature_gates, slot_time_feature_ids, + }, stake_history::StakeHistory, stake_utils, stakes::{DeserializableStakes, InvalidCacheEntryReason, SerdeStakesToStakeFormat, Stakes}, @@ -180,6 +184,12 @@ pub(in crate::bank) fn create_genesis_config(lamports: u64) -> (GenesisConfig, K solana_genesis_config::create_genesis_config(lamports) } +fn create_genesis_config_with_legacy_hashes(lamports: u64) -> (GenesisConfig, Keypair) { + let (mut genesis_config, mint_keypair) = create_genesis_config(lamports); + genesis_config.poh_config.hashes_per_tick = Some(LEGACY_HASHES_PER_TICK); + (genesis_config, mint_keypair) +} + pub(in crate::bank) fn new_sanitized_message(message: Message) -> SanitizedMessage { SanitizedMessage::try_from_legacy_message(message, &ReservedAccountKeys::empty_key_set()) .unwrap() @@ -5257,9 +5267,9 @@ fn test_bank_hash_consistency(deprecate_rent_exemption_threshold: bool) { assert_eq!( bank.hash().to_string(), if deprecate_rent_exemption_threshold { - "F4ns41hFn3ignVHqaVehaTZ8LMUxPaELAgvN8iTcowDD" + "9ycftRwjpQ17PrhnwhGPbVzDKd3Q9BybmLYU8UD1Pg1T" } else { - "FyxeAqHjieaKMA6mLK3yfSD46Xcw2U6hzKfyeu1qVuCD" + "4XzjZMjhP9s8iBeFQsnHFwaQ991dpiBGHEkzj1ifAcpS" }, ); } @@ -5268,9 +5278,9 @@ fn test_bank_hash_consistency(deprecate_rent_exemption_threshold: bool) { assert_eq!( bank.hash().to_string(), if deprecate_rent_exemption_threshold { - "sF1tuUv3r5JCr9smYyhm9Qv27f5jcoJ75LnoLGV5Scd" + "7sbqfkN4W3PkBREyduDc3R2eiKu68JKYRXZt6gCWNt4N" } else { - "DrpEs59czmKpLQvR9kBjSmWJUYMNKDdQ4Zsyu4WjfoPb" + "ArB3XNVLJsyV7AtrG3C3Bj4akb8s7AzpS5rHJnqGW5sw" }, ); break; @@ -6296,6 +6306,271 @@ fn test_block_limits() { ); } +fn assert_slot_time_bank_state(bank: &Bank, params: SlotParams) { + assert_eq!(bank.ns_per_slot, params.ns_per_slot()); + assert_eq!(bank.hashes_per_tick(), params.hashes_per_tick()); + assert_eq!( + bank.slots_per_year.to_bits(), + params.slots_per_year().to_bits() + ); + assert_eq!( + bank.rent_collector.slots_per_year.to_bits(), + bank.slots_per_year.to_bits() + ); + assert_eq!( + bank.partitioned_rewards_stake_account_stores_per_block, + params.partitioned_epoch_rewards_stake_account_stores_per_block() + ); + assert_eq!( + bank.entry_bytes_budget().slot_limit(), + params.max_entry_bytes_per_slot() + ); + let cost_tracker = bank.read_cost_tracker().unwrap(); + let (account_limit, block_limit, data_size_limit) = params.cost_limits( + bank.feature_set + .is_active(&feature_set::raise_block_limits_to_100m::id()), + ); + assert_eq!(cost_tracker.get_account_limit(), account_limit); + assert_eq!(cost_tracker.get_block_limit(), block_limit); + assert_eq!( + cost_tracker.get_allocated_data_size_limit(), + data_size_limit + ); +} + +#[test] +fn test_reduce_slot_time_features() { + let (genesis_config, _) = create_genesis_config_with_legacy_hashes(1_000_000); + let feature_account_balance = genesis_config.rent.minimum_balance(Feature::size_of()); + + let (mut bank, bank_forks) = Bank::new_with_bank_forks_for_tests(&genesis_config); + assert!(!bank.slot_time_reduction_active()); + assert_eq!(bank.hashes_per_tick(), Some(LEGACY_HASHES_PER_TICK)); + + for (feature_id, params) in slot_time_feature_gates() { + let previous_params = bank.current_slot_params(); + bank.store_account( + &feature_id, + &feature::create_account(&Feature::default(), feature_account_balance), + ); + + let activation_slot = bank + .epoch_schedule() + .get_first_slot_in_epoch(bank.epoch().saturating_add(1)); + let activation_bank = Bank::new_from_parent_with_bank_forks( + &bank_forks, + bank, + SlotLeader::default(), + activation_slot, + ); + + assert!(activation_bank.feature_set.is_active(&feature_id)); + assert_eq!(activation_bank.current_slot_params(), previous_params); + assert_slot_time_bank_state(&activation_bank, previous_params); + + let effective_slot = activation_bank + .epoch_schedule() + .get_first_slot_in_epoch(activation_bank.epoch().saturating_add(1)); + bank = Bank::new_from_parent_with_bank_forks( + &bank_forks, + activation_bank, + SlotLeader::default(), + effective_slot, + ); + assert!(bank.slot_time_reduction_active()); + assert_slot_time_bank_state(&bank, params); + } +} + +#[test] +fn test_reduce_slot_time_features_active_at_genesis() { + let (mut genesis_config, _) = create_genesis_config_with_legacy_hashes(1_000_000); + for feature_id in slot_time_feature_ids() { + activate_feature(&mut genesis_config, feature_id); + } + activate_feature( + &mut genesis_config, + feature_set::raise_block_limits_to_100m::id(), + ); + + let (bank, bank_forks) = Bank::new_with_bank_forks_for_tests(&genesis_config); + assert!(!bank.slot_time_reduction_active()); + assert_slot_time_bank_state(&bank, LEGACY_SLOT_PARAMS); + + let effective_slot = bank.epoch_schedule().get_first_slot_in_epoch(1); + let bank = Bank::new_from_parent_with_bank_forks( + &bank_forks, + bank, + SlotLeader::default(), + effective_slot, + ); + assert_slot_time_bank_state(&bank, SLOT_PARAMS_200MS); +} + +#[test] +fn test_reduce_slot_time_hashes_per_tick() { + let assert_reduced_slot_time_hashes_per_tick = + |genesis_config, expected_initial_hashes_per_tick, expected_effective_hashes_per_tick| { + let (bank, bank_forks) = Bank::new_with_bank_forks_for_tests(&genesis_config); + assert_eq!(bank.ns_per_slot, LEGACY_SLOT_PARAMS.ns_per_slot()); + assert_eq!(bank.hashes_per_tick(), expected_initial_hashes_per_tick); + + let effective_slot = bank.epoch_schedule().get_first_slot_in_epoch(1); + let bank = Bank::new_from_parent_with_bank_forks( + &bank_forks, + bank, + SlotLeader::default(), + effective_slot, + ); + assert_eq!(bank.ns_per_slot, SLOT_PARAMS_200MS.ns_per_slot()); + assert_eq!(bank.hashes_per_tick(), expected_effective_hashes_per_tick); + }; + + let (mut genesis_config, _) = create_genesis_config_with_legacy_hashes(1_000_000); + activate_feature( + &mut genesis_config, + feature_set::reduce_slot_time_to_200ms::id(), + ); + assert_reduced_slot_time_hashes_per_tick( + genesis_config, + Some(LEGACY_HASHES_PER_TICK), + SLOT_PARAMS_200MS.hashes_per_tick(), + ); + + let (mut genesis_config, _) = create_genesis_config_with_legacy_hashes(1_000_000); + genesis_utils::activate_all_features_alpenglow(&mut genesis_config); + assert_eq!(genesis_config.poh_config.hashes_per_tick, None); + assert_reduced_slot_time_hashes_per_tick(genesis_config, None, None); +} + +#[test] +fn test_reduce_slot_time_range_duration() { + const SLOTS_PER_EPOCH: Slot = 32; + let bank_for_activations = |activations: &[(Pubkey, Slot)]| { + let (mut genesis_config, _) = create_genesis_config_with_legacy_hashes(1_000_000); + genesis_config.epoch_schedule = + EpochSchedule::custom(SLOTS_PER_EPOCH, SLOTS_PER_EPOCH, false); + let mut bank = Bank::new_for_tests(&genesis_config); + let mut feature_set = FeatureSet::default(); + for (feature_id, activation_slot) in activations { + feature_set.activate(feature_id, *activation_slot); + } + bank.feature_set = Arc::new(feature_set); + bank.refresh_slot_params(); + bank + }; + + let ordered_activations = slot_time_feature_gates() + .into_iter() + .zip([1, 33, 65, 97]) + .map(|((feature_id, _), activation_slot)| (feature_id, activation_slot)) + .collect::>(); + let bank = bank_for_activations(&ordered_activations); + + let expected_duration = [ + (SLOTS_PER_EPOCH, LEGACY_SLOT_PARAMS), + (SLOTS_PER_EPOCH, SLOT_PARAMS_350MS), + (SLOTS_PER_EPOCH, SLOT_PARAMS_300MS), + (SLOTS_PER_EPOCH, SLOT_PARAMS_250MS), + (SLOTS_PER_EPOCH, SLOT_PARAMS_200MS), + ] + .into_iter() + .map(|(slots, params)| slots as f64 / params.slots_per_year()) + .sum::(); + + assert_eq!( + bank.slot_range_duration_in_years(0, SLOTS_PER_EPOCH * 5) + .to_bits(), + expected_duration.to_bits() + ); + assert_eq!( + bank.slot_range_duration_nanos(0, SLOTS_PER_EPOCH * 5 - 1), + [ + (SLOTS_PER_EPOCH, LEGACY_SLOT_PARAMS), + (SLOTS_PER_EPOCH, SLOT_PARAMS_350MS), + (SLOTS_PER_EPOCH, SLOT_PARAMS_300MS), + (SLOTS_PER_EPOCH, SLOT_PARAMS_250MS), + (SLOTS_PER_EPOCH, SLOT_PARAMS_200MS), + ] + .into_iter() + .map(|(slots, params)| u128::from(slots) * params.ns_per_slot()) + .sum::() + ); + + let [reduce_to_350ms, _, _, reduce_to_200ms] = slot_time_feature_ids(); + let bank = + bank_for_activations(&[(reduce_to_200ms, 1), (reduce_to_350ms, SLOTS_PER_EPOCH + 1)]); + assert_eq!( + bank.slot_params_at_slot(SLOTS_PER_EPOCH - 1), + LEGACY_SLOT_PARAMS + ); + assert_eq!(bank.slot_params_at_slot(SLOTS_PER_EPOCH), SLOT_PARAMS_200MS); + assert_eq!( + bank.slot_params_at_slot(SLOTS_PER_EPOCH * 2), + SLOT_PARAMS_200MS + ); + assert_eq!( + bank.slot_range_duration_in_years(0, SLOTS_PER_EPOCH * 3) + .to_bits(), + (SLOTS_PER_EPOCH as f64 / LEGACY_SLOT_PARAMS.slots_per_year() + + (SLOTS_PER_EPOCH * 2) as f64 / SLOT_PARAMS_200MS.slots_per_year()) + .to_bits() + ); + assert_eq!( + bank.slot_range_duration_nanos(0, SLOTS_PER_EPOCH * 3 - 1), + u128::from(SLOTS_PER_EPOCH) * LEGACY_SLOT_PARAMS.ns_per_slot() + + u128::from(SLOTS_PER_EPOCH * 2) * SLOT_PARAMS_200MS.ns_per_slot() + ); +} + +#[test] +fn test_update_clock_slot_range_duration() { + const SLOTS_PER_EPOCH: Slot = 32; + + let leader_pubkey = solana_pubkey::new_rand(); + let GenesisConfigInfo { + mut genesis_config, + voting_keypair, + .. + } = create_genesis_config_with_leader(5, &leader_pubkey, 3); + genesis_config.epoch_schedule = EpochSchedule::custom(SLOTS_PER_EPOCH, SLOTS_PER_EPOCH, false); + activate_feature( + &mut genesis_config, + feature_set::reduce_slot_time_to_200ms::id(), + ); + + let (bank, bank_forks) = Bank::new_with_bank_forks_for_tests(&genesis_config); + let vote_timestamp_slot = 4; + let vote_timestamp = bank.unix_timestamp_from_genesis(); + update_vote_account_timestamp( + BlockTimestamp { + slot: vote_timestamp_slot, + timestamp: vote_timestamp, + }, + &bank, + &voting_keypair.pubkey(), + ); + + let current_slot = SLOTS_PER_EPOCH + 3; + let bank = Bank::new_from_parent_with_bank_forks( + &bank_forks, + bank, + SlotLeader::default(), + current_slot, + ); + let expected_elapsed = Duration::from_nanos_u128( + bank.slot_range_duration_nanos(vote_timestamp_slot + 1, current_slot), + ); + let scalar_elapsed = + Duration::from_nanos(bank.ns_per_slot as u64) * (current_slot - vote_timestamp_slot) as u32; + + assert_ne!(expected_elapsed.as_secs(), scalar_elapsed.as_secs()); + assert_eq!( + bank.clock().unix_timestamp, + vote_timestamp + expected_elapsed.as_secs() as i64 + ); +} + #[test] fn test_rent_feature_gates_epoch_transition() { let (mut genesis_config, _mint_keypair) = create_genesis_config(1_000_000); @@ -11915,6 +12190,57 @@ fn test_new_from_snapshot_uses_rent_from_sysvar() { assert_ne!(new_bank.rent_collector.rent, wrong_rent); } +#[test] +fn test_new_from_snapshot_hashes_per_tick_changed() { + let GenesisConfigInfo { + mut genesis_config, .. + } = create_genesis_config_with_leader(100_000, &Pubkey::new_unique(), 3); + genesis_config.poh_config.hashes_per_tick = Some(12_500); + + let bank = Bank::new_for_tests(&genesis_config); + bank.set_hashes_per_tick(Some(LEGACY_HASHES_PER_TICK)); + bank.set_block_id(Some(Hash::default())); + + let snapshot_storages = bank.get_snapshot_storages(None); + let mut buf = vec![]; + crate::serde_snapshot::bank_to_stream( + &mut std::io::BufWriter::new(Cursor::new(&mut buf)), + &bank, + &snapshot_storages, + ) + .unwrap(); + + let mut reader = std::io::BufReader::new(Cursor::new(&buf)); + let (mut fields, _accounts_db_fields) = fields_from_stream(&mut reader).unwrap(); + let epoch_stakes = std::mem::take(&mut fields.versioned_epoch_stakes) + .into_iter() + .map(|(epoch, stakes)| (epoch, stakes.into())) + .collect(); + + let new_bank = Bank::new_from_snapshot( + BankRc { + accounts: Arc::clone(&bank.rc.accounts), + parent: RwLock::new(None), + bank_id_generator: Arc::clone(&bank.rc.bank_id_generator), + }, + &genesis_config, + Arc::new(RuntimeConfig::default()), + fields, + None, + None, + bank.load_accounts_data_size(), + epoch_stakes, + ); + + assert_eq!(genesis_config.hashes_per_tick(), Some(12_500)); + assert_eq!(new_bank.hashes_per_tick(), Some(LEGACY_HASHES_PER_TICK)); + assert_eq!( + new_bank.slot_params.baseline_params().hashes_per_tick(), + Some(LEGACY_HASHES_PER_TICK) + ); + new_bank.assert_bank_matches_slot_params(); +} + #[test] fn test_calculate_and_set_block_id_for_dcou() { // scenario 1: block id already set diff --git a/runtime/src/block_component_processor/vote_reward/epoch_inflation_account_state.rs b/runtime/src/block_component_processor/vote_reward/epoch_inflation_account_state.rs index 1c6c95d4de5..c68be91a25b 100644 --- a/runtime/src/block_component_processor/vote_reward/epoch_inflation_account_state.rs +++ b/runtime/src/block_component_processor/vote_reward/epoch_inflation_account_state.rs @@ -155,9 +155,11 @@ mod tests { bank_forks::BankForks, genesis_utils::{ GenesisConfigInfo, ValidatorVoteKeypairs, create_genesis_config, - create_genesis_config_with_alpenglow_vote_accounts, + create_genesis_config_with_alpenglow_vote_accounts, deactivate_features, }, + slot_params::slot_time_feature_ids, }, + agave_feature_set as feature_set, rand::Rng, solana_epoch_schedule::EpochSchedule, solana_genesis_config::GenesisConfig, @@ -231,7 +233,7 @@ mod tests { } #[test] - fn new_epoch_update_account_use_current_reward_budget() { + fn new_epoch_update_account_uses_current_epoch_rewards() { let GenesisConfigInfo { mut genesis_config, .. } = create_genesis_config(1_000_000_000); @@ -239,46 +241,71 @@ mod tests { // Warmup is necessary here to give epoch 0 and epoch 1 different reward // budgets. genesis_config.epoch_schedule = EpochSchedule::custom(64, 64, true); + deactivate_features(&mut genesis_config, &slot_time_feature_ids().to_vec()); - let bank_forks = BankForks::new_rw_arc(Bank::new_for_tests(&genesis_config)); - let bank_epoch_0 = bank_forks.read().unwrap().root_bank(); - let epoch_1_first_slot = bank_epoch_0.epoch_schedule().get_first_slot_in_epoch(1); - let bank_epoch_1 = Bank::new_from_parent_with_bank_forks( - bank_forks.as_ref(), - bank_epoch_0.clone(), - SlotLeader::new_unique(), - epoch_1_first_slot, - ); + for (case, genesis_config, activate_slot_time_feature) in [ + ("current reward budget", genesis_config, false), + ( + "current epoch slot params", + GenesisConfig { + inflation: Inflation::full(), + ..GenesisConfig::default() + }, + true, + ), + ] { + let mut bank_epoch_0 = Bank::new_for_tests(&genesis_config); + if activate_slot_time_feature { + bank_epoch_0.activate_feature(&feature_set::reduce_slot_time_to_350ms::id()); + } + let bank_forks = BankForks::new_rw_arc(bank_epoch_0); + let bank_epoch_0 = bank_forks.read().unwrap().root_bank(); + let epoch_1_first_slot = bank_epoch_0.epoch_schedule().get_first_slot_in_epoch(1); + let bank_epoch_1 = Bank::new_from_parent_with_bank_forks( + bank_forks.as_ref(), + bank_epoch_0.clone(), + SlotLeader::new_unique(), + epoch_1_first_slot, + ); - assert_eq!(bank_epoch_0.epoch(), 0); - assert_eq!(bank_epoch_1.epoch(), 1); - assert_ne!( - bank_epoch_1.epoch_schedule().get_slots_in_epoch(0), - bank_epoch_1.epoch_schedule().get_slots_in_epoch(1) - ); + assert_eq!(bank_epoch_0.epoch(), 0, "{case}"); + assert_eq!(bank_epoch_1.epoch(), 1, "{case}"); + if activate_slot_time_feature { + assert_ne!(bank_epoch_0.ns_per_slot, bank_epoch_1.ns_per_slot, "{case}"); + } else { + assert_ne!( + bank_epoch_1.epoch_schedule().get_slots_in_epoch(0), + bank_epoch_1.epoch_schedule().get_slots_in_epoch(1), + "{case}", + ); + } - let epoch_start_capitalization = bank_epoch_0.capitalization(); - let expected_current_epoch_rewards = bank_epoch_1 - .calculate_epoch_inflation_rewards(epoch_start_capitalization, bank_epoch_1.epoch()); - assert_ne!( - expected_current_epoch_rewards, - bank_epoch_1.calculate_epoch_inflation_rewards( + let epoch_start_capitalization = bank_epoch_0.capitalization(); + let expected_current_epoch_rewards = bank_epoch_1.calculate_epoch_inflation_rewards( epoch_start_capitalization, - bank_epoch_0.epoch() - ) - ); + bank_epoch_1.epoch(), + ); + assert_ne!( + expected_current_epoch_rewards, + bank_epoch_1.calculate_epoch_inflation_rewards( + epoch_start_capitalization, + bank_epoch_0.epoch() + ), + "{case}", + ); - EpochInflationAccountState::new_epoch_update_account( - &bank_epoch_1, - epoch_start_capitalization, - 0, - ); - let state = EpochInflationAccountState::new_from_bank(&bank_epoch_1).unwrap(); - assert_eq!(state.current.epoch, bank_epoch_1.epoch()); - assert_eq!( - state.current.max_possible_validator_reward, - expected_current_epoch_rewards - ); + EpochInflationAccountState::new_epoch_update_account( + &bank_epoch_1, + epoch_start_capitalization, + 0, + ); + let state = EpochInflationAccountState::new_from_bank(&bank_epoch_1).unwrap(); + assert_eq!(state.current.epoch, bank_epoch_1.epoch(), "{case}"); + assert_eq!( + state.current.max_possible_validator_reward, expected_current_epoch_rewards, + "{case}", + ); + } } #[test] diff --git a/runtime/src/slot_params.rs b/runtime/src/slot_params.rs index 0e25bb31ec9..84e91d9c8b2 100644 --- a/runtime/src/slot_params.rs +++ b/runtime/src/slot_params.rs @@ -1,6 +1,8 @@ use { + agave_feature_set::{self as feature_set, FeatureSet}, solana_clock::Slot, solana_epoch_schedule::EpochSchedule, + solana_pubkey::Pubkey, std::{ collections::BTreeMap, ops::Bound::{Excluded, Included}, @@ -11,10 +13,8 @@ pub const DEFAULT_MAX_ENTRY_BYTES_PER_SLOT: u64 = 20 * 1024 * 1024; // 20 MiB /// Runtime parameters tied to a slot-time regime. /// -/// This intentionally groups values that need to move together when slot time -/// changes. For now, only the legacy baseline exists, so routing through this -/// type preserves current behavior while giving future feature-gated changes a -/// single table-shaped home. +/// Slot-time reduction features select explicit table values instead of +/// deriving ratios at runtime, which avoids rounding drift in consensus paths. #[derive(Clone, Copy, Debug, PartialEq)] pub struct SlotParams { pub(crate) ns_per_slot: u128, @@ -22,7 +22,6 @@ pub struct SlotParams { pub(crate) hashes_per_tick: Option, pub(crate) max_block_units: u64, pub(crate) max_writable_account_units: u64, - pub(crate) max_vote_units: u64, pub(crate) max_block_accounts_data_size_delta: u64, pub(crate) max_data_shreds_per_slot: u32, pub(crate) max_code_shreds_per_slot: u32, @@ -87,23 +86,33 @@ impl SlotParams { } /// Returns the per-bank cost limits for these params. - pub const fn cost_limits(self) -> (u64, u64, u64, u64) { + pub(crate) const fn cost_limits(self, raise_block_limits_to_100m: bool) -> (u64, u64, u64) { + let (max_writable_account_units, max_block_units) = if raise_block_limits_to_100m { + ( + self.max_writable_account_units + .saturating_mul(100) + .saturating_div(60), + self.max_block_units.saturating_mul(100).saturating_div(60), + ) + } else { + (self.max_writable_account_units, self.max_block_units) + }; + ( - self.max_writable_account_units, - self.max_block_units, - self.max_vote_units, + max_writable_account_units, + max_block_units, self.max_block_accounts_data_size_delta, ) } } +pub const LEGACY_HASHES_PER_TICK: u64 = 62_500; pub(crate) const LEGACY_SLOT_PARAMS: SlotParams = SlotParams { ns_per_slot: 400_000_000, slots_per_year: 78_892_314.984, - hashes_per_tick: Some(62_500), + hashes_per_tick: Some(LEGACY_HASHES_PER_TICK), max_block_units: 60_000_000, max_writable_account_units: 24_000_000, - max_vote_units: 36_000_000, max_block_accounts_data_size_delta: 100_000_000, max_data_shreds_per_slot: 32_768, max_code_shreds_per_slot: 32_768, @@ -111,6 +120,88 @@ pub(crate) const LEGACY_SLOT_PARAMS: SlotParams = SlotParams { partitioned_epoch_rewards_stake_account_stores_per_block: 4096, }; +pub(crate) const SLOT_PARAMS_350MS: SlotParams = SlotParams { + ns_per_slot: 350_000_000, + slots_per_year: 90_162_645.696, + hashes_per_tick: Some(54_687), + max_block_units: 52_500_000, + max_writable_account_units: 21_000_000, + max_block_accounts_data_size_delta: 87_500_000, + max_data_shreds_per_slot: 28_672, + max_code_shreds_per_slot: 28_672, + max_entry_bytes_per_slot: 18_350_080, + partitioned_epoch_rewards_stake_account_stores_per_block: 3_584, +}; + +pub(crate) const SLOT_PARAMS_300MS: SlotParams = SlotParams { + ns_per_slot: 300_000_000, + slots_per_year: 105_189_753.312, + hashes_per_tick: Some(46_875), + max_block_units: 45_000_000, + max_writable_account_units: 18_000_000, + max_block_accounts_data_size_delta: 75_000_000, + max_data_shreds_per_slot: 24_576, + max_code_shreds_per_slot: 24_576, + max_entry_bytes_per_slot: 15_728_640, + partitioned_epoch_rewards_stake_account_stores_per_block: 3_072, +}; + +pub(crate) const SLOT_PARAMS_250MS: SlotParams = SlotParams { + ns_per_slot: 250_000_000, + slots_per_year: 126_227_703.974, + hashes_per_tick: Some(39_062), + max_block_units: 37_500_000, + max_writable_account_units: 15_000_000, + max_block_accounts_data_size_delta: 62_500_000, + max_data_shreds_per_slot: 20_480, + max_code_shreds_per_slot: 20_480, + max_entry_bytes_per_slot: 13_107_200, + partitioned_epoch_rewards_stake_account_stores_per_block: 2_560, +}; + +pub(crate) const SLOT_PARAMS_200MS: SlotParams = SlotParams { + ns_per_slot: 200_000_000, + slots_per_year: 157_784_629.968, + hashes_per_tick: Some(31_250), + max_block_units: 30_000_000, + max_writable_account_units: 12_000_000, + max_block_accounts_data_size_delta: 50_000_000, + max_data_shreds_per_slot: 16_384, + max_code_shreds_per_slot: 16_384, + max_entry_bytes_per_slot: 10_485_760, + partitioned_epoch_rewards_stake_account_stores_per_block: 2_048, +}; + +/// Slot-time reduction gates in the intended activation order. +const SLOT_TIME_REDUCTION_PARAMS: [(Pubkey, SlotParams); 4] = [ + ( + feature_set::reduce_slot_time_to_350ms::ID, + SLOT_PARAMS_350MS, + ), + ( + feature_set::reduce_slot_time_to_300ms::ID, + SLOT_PARAMS_300MS, + ), + ( + feature_set::reduce_slot_time_to_250ms::ID, + SLOT_PARAMS_250MS, + ), + ( + feature_set::reduce_slot_time_to_200ms::ID, + SLOT_PARAMS_200MS, + ), +]; + +/// Returns slot-time feature gates mapped to runtime slot parameters. +pub fn slot_time_feature_gates() -> [(Pubkey, SlotParams); 4] { + SLOT_TIME_REDUCTION_PARAMS +} + +/// Returns all slot-time reduction feature IDs in activation order. +pub fn slot_time_feature_ids() -> [Pubkey; 4] { + SLOT_TIME_REDUCTION_PARAMS.map(|(feature_id, _)| feature_id) +} + /// Bank-local archive of slot-parameter transitions. /// /// This is rebuilt from feature accounts on startup instead of being serialized @@ -124,25 +215,49 @@ pub(crate) struct SlotParamsArchive { /// This is used when a bank, usually the root bank, must answer "which /// parameters apply to some other slot?" Examples include shred filtering /// for incoming shreds and inflation calculations that span historical slot - /// ranges. + /// ranges. The transitions are normalized so slot duration never increases + /// as slots advance, even if slot-time features activate out of order. param_transitions: BTreeMap, } impl Default for SlotParamsArchive { fn default() -> Self { - Self::new(&EpochSchedule::default(), LEGACY_SLOT_PARAMS) + Self::new( + &FeatureSet::default(), + &EpochSchedule::default(), + LEGACY_SLOT_PARAMS, + ) } } impl SlotParamsArchive { - /// Rebuilds slot-parameter transitions from the supplied baseline. - /// - /// `_epoch_schedule` is accepted now so the call sites already have the - /// right shape for delayed, epoch-boundary-effective feature transitions. - pub(crate) fn new(_epoch_schedule: &EpochSchedule, baseline_params: SlotParams) -> Self { - Self { - param_transitions: BTreeMap::from([(0, baseline_params)]), + /// Rebuilds slot-parameter transitions from the active feature set. + pub(crate) fn new( + feature_set: &FeatureSet, + epoch_schedule: &EpochSchedule, + baseline_params: SlotParams, + ) -> Self { + let mut param_transitions = BTreeMap::from([(0, baseline_params)]); + let mut earliest_same_or_shorter_slot = Slot::MAX; + + // The feature table is ordered longest-to-shortest. Walk it in reverse + // so once a same-or-shorter target is effective at slot S, any longer + // target effective at S or later is known to be redundant. + for (feature_id, params) in slot_time_feature_gates().into_iter().rev() { + if params.ns_per_slot > baseline_params.ns_per_slot { + continue; + } + let Some(activation_slot) = feature_set.activated_slot(&feature_id) else { + continue; + }; + let effective_slot = Self::feature_effective_slot(epoch_schedule, activation_slot); + if effective_slot < earliest_same_or_shorter_slot { + param_transitions.insert(effective_slot, params); + earliest_same_or_shorter_slot = effective_slot; + } } + + Self { param_transitions } } /// Returns the baseline params supplied at genesis or snapshot restore. @@ -195,4 +310,59 @@ impl SlotParamsArchive { let remaining_slots = u128::from(end_slot.saturating_sub(cursor)) + 1; duration.saturating_add(remaining_slots.saturating_mul(params.ns_per_slot())) } + + /// Returns the first slot where a slot-time feature may affect bank state. + /// + /// A gate that activates in epoch E is effective starting at the first slot + /// of epoch E + 1, giving shred filters a full epoch of advance notice + /// before enforcing lower shred limits. + fn feature_effective_slot(epoch_schedule: &EpochSchedule, activation_slot: Slot) -> Slot { + let activation_epoch = epoch_schedule.get_epoch(activation_slot); + epoch_schedule.get_first_slot_in_epoch(activation_epoch.saturating_add(1)) + } + + /// Returns true if any slot-time reduction has taken effect by this bank. + /// + /// Feature activation happens in one epoch, but slot params become effective + /// at the start of the following epoch. + pub(crate) fn any_slot_time_reduction_effective( + epoch_schedule: &EpochSchedule, + slot: Slot, + feature_set: &FeatureSet, + ns_per_slot: u128, + ) -> bool { + slot_time_feature_gates() + .into_iter() + .filter(|(_, params)| params.ns_per_slot() <= ns_per_slot) + .filter_map(|(feature_id, _)| feature_set.activated_slot(&feature_id)) + .any(|activation_slot| { + Self::feature_effective_slot(epoch_schedule, activation_slot) <= slot + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_cost_limits_scaling_matches_simd_0525() { + for (params, expected_account_limit, expected_block_limit) in [ + (LEGACY_SLOT_PARAMS, 40_000_000, 100_000_000), + (SLOT_PARAMS_350MS, 35_000_000, 87_500_000), + (SLOT_PARAMS_300MS, 30_000_000, 75_000_000), + (SLOT_PARAMS_250MS, 25_000_000, 62_500_000), + (SLOT_PARAMS_200MS, 20_000_000, 50_000_000), + ] { + let (_, _, data_size_limit) = params.cost_limits(false); + assert_eq!( + params.cost_limits(true), + ( + expected_account_limit, + expected_block_limit, + data_size_limit + ) + ); + } + } } diff --git a/turbine/src/broadcast_stage/standard_broadcast_run.rs b/turbine/src/broadcast_stage/standard_broadcast_run.rs index 09fffbfc27e..44e59e43753 100644 --- a/turbine/src/broadcast_stage/standard_broadcast_run.rs +++ b/turbine/src/broadcast_stage/standard_broadcast_run.rs @@ -682,7 +682,12 @@ mod test { shred::{DATA_SHREDS_PER_FEC_BLOCK, max_ticks_per_n_shreds}, }, solana_net_utils::{SocketAddrSpace, sockets::bind_to_localhost_unique}, - solana_runtime::bank::Bank, + solana_pubkey::Pubkey, + solana_runtime::{ + bank::{Bank, SlotLeader}, + genesis_utils::{activate_feature, deactivate_features}, + slot_params::{slot_time_feature_gates, slot_time_feature_ids}, + }, solana_signer::Signer, std::{ops::Deref, sync::Arc, time::Duration}, test_case::test_case, @@ -746,6 +751,65 @@ mod test { Arc::new(LeaderScheduleCache::new_from_bank(bank)) } + fn broadcast_shred_limits_for_slot_time_features( + feature_ids: impl IntoIterator, + ) -> (u32, u32) { + let ledger_path = get_tmp_ledger_path!(); + let blockstore = Arc::new( + Blockstore::open(&ledger_path).expect("Expected to be able to open database ledger"), + ); + let mut genesis_config = create_genesis_config(10_000).genesis_config; + let slot_time_features = slot_time_feature_ids().to_vec(); + deactivate_features(&mut genesis_config, &slot_time_features); + for feature_id in feature_ids { + activate_feature(&mut genesis_config, feature_id); + } + + let (parent_bank, bank_forks) = + Bank::new_for_tests(&genesis_config).wrap_with_bank_forks_for_tests(); + parent_bank.set_tick_height(parent_bank.max_tick_height()); + Bank::calculate_and_set_block_id_for_dcou(&parent_bank); + + let effective_slot = parent_bank.epoch_schedule().get_first_slot_in_epoch(1); + let bank = Bank::new_from_parent_with_bank_forks( + &bank_forks, + parent_bank, + SlotLeader::default(), + effective_slot, + ); + let ticks = create_ticks(1, 0, genesis_config.hash()); + let receive_results = ReceiveResults { + component: BlockComponent::EntryBatch(ticks.clone()), + bank: bank.clone(), + last_tick_height: bank.tick_height() + ticks.len() as u64, + }; + let (socket_sender, _socket_receiver) = unbounded(); + let (blockstore_sender, _blockstore_receiver) = unbounded(); + let (votor_event_sender, _votor_event_receiver) = unbounded(); + let mut standard_broadcast_run = StandardBroadcastRun::new( + 0, + Arc::new(MigrationStatus::default()), + votor_event_sender, + test_leader_schedule_cache(&bank), + ); + + standard_broadcast_run + .process_receive_results( + &Keypair::new(), + &blockstore, + &socket_sender, + &blockstore_sender, + receive_results, + &mut ProcessShredsStats::default(), + ) + .unwrap(); + + ( + standard_broadcast_run.max_data_shreds_per_slot, + standard_broadcast_run.max_code_shreds_per_slot, + ) + } + #[test] fn test_interrupted_slot_last_shred() { let keypair = Arc::new(Keypair::new()); @@ -1164,52 +1228,28 @@ mod test { #[test] fn test_update_shred_limits_from_bank() { - agave_logger::setup(); - let num_shreds_per_slot = 2; - let ( - blockstore, - genesis_config, - cluster_info, - parent_bank, - leader_keypair, - socket, - bank_forks, - ) = setup(num_shreds_per_slot); - let bank = new_child_bank(&parent_bank, 1); - let ticks = create_ticks(1, 0, genesis_config.hash()); - let receive_results = ReceiveResults { - component: BlockComponent::EntryBatch(ticks), - bank: bank.clone(), - last_tick_height: bank.tick_height() + 1, - }; - - let (votor_event_sender, _votor_event_receiver) = unbounded(); - let mut standard_broadcast_run = StandardBroadcastRun::new( - 0, - Arc::new(MigrationStatus::default()), - votor_event_sender, - test_leader_schedule_cache(&bank), - ); - standard_broadcast_run.max_data_shreds_per_slot = 1; - standard_broadcast_run.max_code_shreds_per_slot = 1; - standard_broadcast_run - .test_process_receive_results( - &leader_keypair, - &cluster_info, - &socket, - &blockstore, - receive_results, - &bank_forks, - ) - .unwrap(); - assert_eq!( - standard_broadcast_run.max_data_shreds_per_slot, - bank.max_data_shreds_per_slot() + broadcast_shred_limits_for_slot_time_features(std::iter::empty()), + ( + DEFAULT_MAX_DATA_SHREDS_PER_SLOT, + DEFAULT_MAX_CODE_SHREDS_PER_SLOT + ) ); + + for ((feature_id, _), expected_shreds_per_slot) in slot_time_feature_gates() + .into_iter() + .zip([28_672, 24_576, 20_480, 16_384]) + { + assert_eq!( + broadcast_shred_limits_for_slot_time_features([feature_id]), + (expected_shreds_per_slot, expected_shreds_per_slot) + ); + } + + let [reduce_to_350ms, _, _, reduce_to_200ms] = slot_time_feature_ids(); assert_eq!( - standard_broadcast_run.max_code_shreds_per_slot, - bank.max_code_shreds_per_slot() + broadcast_shred_limits_for_slot_time_features([reduce_to_350ms, reduce_to_200ms]), + (16_384, 16_384) ); } From 40587a459ed235dbace4b7e2871e8478d708afcd Mon Sep 17 00:00:00 2001 From: Yihau Chen Date: Fri, 5 Jun 2026 20:10:21 +0800 Subject: [PATCH 215/576] Revert "ci: use github app token in dependabot pr (#12942)" (#12975) This reverts commit 4da4a28081a70ee92e9b01f2332fa310ff375269. --- .github/workflows/dependabot-pr.yml | 23 +++-------------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/.github/workflows/dependabot-pr.yml b/.github/workflows/dependabot-pr.yml index d634c0109ad..7280cbe302e 100644 --- a/.github/workflows/dependabot-pr.yml +++ b/.github/workflows/dependabot-pr.yml @@ -8,33 +8,14 @@ on: jobs: dependabot: timeout-minutes: 10 - environment: dependency-update runs-on: ubuntu-24.04 if: github.event.pull_request.user.login == 'dependabot[bot]' steps: - - name: Create github app token - uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 - id: app-token - with: - client-id: ${{ vars.CLIENT_ID }} - private-key: ${{ secrets.PRIVATE_KEY }} - - - name: Set git config - run: | - git config --global user.email "${APP_ID}+${APP_SLUG}[bot]@users.noreply.github.com" - git config --global user.name "${APP_SLUG}[bot]" - git config --global url."https://x-access-token:${GITHUB_TOKEN}@github.com/".insteadOf https://github.com/ - env: - GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} - APP_ID: ${{ vars.APP_ID }} - APP_SLUG: ${{ steps.app-token.outputs.app-slug }} - - name: checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.event.pull_request.head.ref }} - token: ${{ steps.app-token.outputs.token }} - persist-credentials: false + token: ${{ secrets.PAT }} - name: update code env: @@ -64,6 +45,8 @@ jobs: if [ -z "$(git status --porcelain)" ]; then echo "✅ Nothing to update" else + git config user.email "49699333+dependabot[bot]@users.noreply.github.com" + git config user.name "dependabot[bot]" git add **/Cargo.toml git add **/Cargo.lock git commit -am "Update all workspaces" From 19cdbc133908e52b61bb683d3e51f56b3579e3c1 Mon Sep 17 00:00:00 2001 From: Akhilesh Singhania Date: Fri, 5 Jun 2026 14:56:09 +0200 Subject: [PATCH 216/576] Uses the new Block type in more places (#12973) --- core/src/repair/block_id_repair_service.rs | 120 +++++++++++---------- core/src/repair/serve_repair.rs | 12 +-- 2 files changed, 72 insertions(+), 60 deletions(-) diff --git a/core/src/repair/block_id_repair_service.rs b/core/src/repair/block_id_repair_service.rs index 5b523a26594..3e1e11dfab3 100644 --- a/core/src/repair/block_id_repair_service.rs +++ b/core/src/repair/block_id_repair_service.rs @@ -33,7 +33,6 @@ use { log::{debug, info}, solana_clock::Slot, solana_gossip::ping_pong::{Ping, Pong}, - solana_hash::Hash, solana_keypair::signable::Signable, solana_ledger::{ blockstore::{Blockstore, BlockstoreError, CompletedSlotsReceiver}, @@ -157,7 +156,7 @@ enum PendingRepairDecision { /// Action to perform as a result of a repair event enum RepairAction { - StartRepair { slot: Slot, block_id: Hash }, + StartRepair { block: Block }, QueueParent { slot: Slot, location: BlockLocation }, } @@ -189,7 +188,7 @@ struct RepairState { sent_requests: HashMap, /// Blocks we've previously requested. Used to avoid re-initiating repair for an in progress block. - requested_blocks: HashSet<(Slot, Hash)>, + requested_blocks: HashSet, // Stats response_stats: BlockIdRepairResponsesStats, @@ -408,7 +407,7 @@ impl BlockIdRepairService { let mut first_error = None; // Clean up old request tracking - state.requested_blocks.retain(|(slot, _)| *slot > root); + state.requested_blocks.retain(|block| block.slot > root); state.prune_expected_ping_responses(timestamp()); // Process responses, generate new requests / repair events @@ -601,7 +600,7 @@ impl BlockIdRepairService { .sent_requests .remove(&OutgoingMessage::Metadata(request)); - let (slot, block_id) = request.block(); + let Block { slot, block_id } = request.block(); match response { BlockIdRepairResponse::ParentFecSetCount { @@ -720,25 +719,23 @@ impl BlockIdRepairService { event: RepairEvent, root: Slot, blockstore: &Blockstore, - requested_blocks: &HashSet<(Slot, Hash)>, + requested_blocks: &HashSet, ) -> Result { if event.slot() <= root { return Ok(PendingRepairDecision::Drop); } match event { - RepairEvent::FetchBlock { - block: Block { slot, block_id }, - } => { - if requested_blocks.contains(&(slot, block_id)) { + RepairEvent::FetchBlock { block } => { + if requested_blocks.contains(&block) { return Ok(PendingRepairDecision::Drop); } // Check if we already have the block, if so queue fetching the parent // Note: when a block becomes full in blockstore -> we atomically calculate the DMR and populate location - if let Some(location) = blockstore.get_block_location(slot, block_id)? { + if let Some(location) = blockstore.get_block_location(block.slot, block.block_id)? { return Ok(PendingRepairDecision::Act(RepairAction::QueueParent { - slot, + slot: block.slot, location, })); } @@ -747,48 +744,47 @@ impl BlockIdRepairService { // Note: we require the invariant that Turbine + Eager repair will either: // - Eventually fill in all shreds for a slot (slot_meta.is_full()) resulting in the DMR calculation // - Mark the slot as dead - if blockstore.is_dead(slot) { + if blockstore.is_dead(block.slot) { info!( - "{my_pubkey}: FetchBlock: slot {slot} is dead, starting repair for \ - block_id={block_id:?}" + "{my_pubkey}: FetchBlock: slot {} is dead, starting repair for \ + block_id={:?}", + block.slot, block.block_id ); return Ok(PendingRepairDecision::Act(RepairAction::StartRepair { - slot, - block_id, + block, })); } // Turbine did not fail, check the progress - match blockstore.get_double_merkle_root(slot, BlockLocation::Original)? { + match blockstore.get_double_merkle_root(block.slot, BlockLocation::Original)? { None => { // Turbine has not completed, defer and check again later debug!( - "{my_pubkey}: FetchBlock: Turbine not complete for slot {slot}, \ - deferring" + "{my_pubkey}: FetchBlock: Turbine not complete for slot {}, deferring", + block.slot ); Ok(PendingRepairDecision::KeepPending) } - Some(turbine_block_id) if turbine_block_id != block_id => { + Some(turbine_block_id) if turbine_block_id != block.block_id => { // Turbine has a different block warn!( "{my_pubkey}: FetchBlock: Turbine has different block \ - {turbine_block_id:?} vs requested {block_id:?} for slot {slot}, \ - starting repair" + {turbine_block_id:?} vs requested block {block:?}, starting repair" ); Ok(PendingRepairDecision::Act(RepairAction::StartRepair { - slot, - block_id, + block, })) } Some(_) => { // Turbine completed between when we checked for the block above and here // Queue the parent debug!( - "{my_pubkey}: FetchBlock: Turbine has correct block for slot {slot}, \ - fetching parent" + "{my_pubkey}: FetchBlock: Turbine has correct block for slot {}, \ + fetching parent", + block.slot ); Ok(PendingRepairDecision::Act(RepairAction::QueueParent { - slot, + slot: block.slot, location: BlockLocation::Original, })) } @@ -805,8 +801,8 @@ impl BlockIdRepairService { state: &mut RepairState, ) -> Result<(), BlockstoreError> { match action { - RepairAction::StartRepair { slot, block_id } => { - if state.requested_blocks.contains(&(slot, block_id)) { + RepairAction::StartRepair { block } => { + if state.requested_blocks.contains(&block) { return Ok(()); } @@ -814,19 +810,20 @@ impl BlockIdRepairService { let alternate_blocks: Vec<_> = state .requested_blocks .iter() - .filter(|(s, _)| *s == slot) - .map(|(_, b)| *b) + .filter(|b| b.slot == block.slot) + .map(|b| b.block_id) .collect(); if alternate_blocks.len() >= MAX_ALTERNATE_BLOCKS_PER_SLOT { error!( - "{my_pubkey}: Too many alternate blocks for slot {slot}, ignoring request \ - for {block_id:?}, requested_blocks: {alternate_blocks:?}" + "{my_pubkey}: Too many alternate blocks for slot {}, ignoring request for \ + {:?}, requested_blocks: {alternate_blocks:?}", + block.slot, block.block_id ); datapoint_error!( "block_id_repair_service-too_many_alternate_blocks", - ("slot", slot, i64), - ("block_id", block_id.to_string(), String), + ("slot", block.slot, i64), + ("block_id", block.block_id.to_string(), String), ); return Ok(()); } @@ -834,9 +831,12 @@ impl BlockIdRepairService { state .pending_repair_requests .push(OutgoingMessage::Metadata( - BlockIdRepairType::ParentAndFecSetCount { slot, block_id }, + BlockIdRepairType::ParentAndFecSetCount { + slot: block.slot, + block_id: block.block_id, + }, )); - state.requested_blocks.insert((slot, block_id)); + state.requested_blocks.insert(block); Ok(()) } RepairAction::QueueParent { slot, location } => { @@ -1005,9 +1005,10 @@ impl BlockIdRepairService { ); }) else { - state - .requested_blocks - .remove(&(shred_request.slot(), shred_request.block_id().unwrap())); + state.requested_blocks.remove(&Block { + slot: shred_request.slot(), + block_id: shred_request.block_id().unwrap(), + }); continue; }; @@ -1744,7 +1745,7 @@ mod tests { } // Verify: block was added to requested_blocks - assert!(state.requested_blocks.contains(&(slot, block_id))); + assert!(state.requested_blocks.contains(&Block { slot, block_id })); // Verify: no deferred events assert!(state.pending_repair_events.is_empty()); @@ -1776,7 +1777,7 @@ mod tests { assert_eq!(block.block_id, block_id); // Verify: block was NOT added to requested_blocks (so it can be re-added when reprocessed) - assert!(!state.requested_blocks.contains(&(slot, block_id))); + assert!(!state.requested_blocks.contains(&Block { slot, block_id })); } #[test] @@ -1819,7 +1820,10 @@ mod tests { } // Verify: block was added to requested_blocks - assert!(state.requested_blocks.contains(&(slot, requested_block_id))); + assert!(state.requested_blocks.contains(&Block { + slot, + block_id: requested_block_id + })); // Verify: no deferred events assert!(state.pending_repair_events.is_empty()); @@ -1835,7 +1839,7 @@ mod tests { let block_id = Hash::new_unique(); // Pre-add block to requested_blocks - state.requested_blocks.insert((slot, block_id)); + state.requested_blocks.insert(Block { slot, block_id }); let event = RepairEvent::FetchBlock { block: Block { slot, block_id }, @@ -1866,7 +1870,7 @@ mod tests { // Verify: No request was added (slot at root is ignored) assert!(state.pending_repair_requests.is_empty()); - assert!(!state.requested_blocks.contains(&(slot, block_id))); + assert!(!state.requested_blocks.contains(&Block { slot, block_id })); } #[test] @@ -1879,7 +1883,10 @@ mod tests { // Fill up requested_blocks with MAX_ALTERNATE_BLOCKS_PER_SLOT blocks for this slot for _ in 0..MAX_ALTERNATE_BLOCKS_PER_SLOT { - state.requested_blocks.insert((slot, Hash::new_unique())); + state.requested_blocks.insert(Block { + slot, + block_id: Hash::new_unique(), + }); } let new_block_id = Hash::new_unique(); @@ -1896,7 +1903,10 @@ mod tests { // Verify: No new request was added assert!(state.pending_repair_requests.is_empty()); // Verify: new block was NOT added to requested_blocks - assert!(!state.requested_blocks.contains(&(slot, new_block_id))); + assert!(!state.requested_blocks.contains(&Block { + slot, + block_id: new_block_id + })); } #[test] @@ -1954,12 +1964,14 @@ mod tests { assert!( block_ids[..MAX_ALTERNATE_BLOCKS_PER_SLOT] .iter() - .all(|block_id| state.requested_blocks.contains(&(slot, *block_id))) - ); - assert!( - !state - .requested_blocks - .contains(&(slot, block_ids[MAX_ALTERNATE_BLOCKS_PER_SLOT])) + .all(|block_id| state.requested_blocks.contains(&Block { + slot, + block_id: *block_id + })) ); + assert!(!state.requested_blocks.contains(&Block { + slot, + block_id: block_ids[MAX_ALTERNATE_BLOCKS_PER_SLOT] + })); } } diff --git a/core/src/repair/serve_repair.rs b/core/src/repair/serve_repair.rs index dcf3900c9a0..ae6c539fcef 100644 --- a/core/src/repair/serve_repair.rs +++ b/core/src/repair/serve_repair.rs @@ -16,7 +16,7 @@ use { result::{Error, RepairVerifyError, Result}, }, }, - agave_votor_messages::migration::MigrationStatus, + agave_votor_messages::{consensus_message::Block, migration::MigrationStatus}, bincode::{Options, serialize}, crossbeam_channel::{Receiver, RecvTimeoutError}, lazy_lru::LruCache, @@ -223,15 +223,15 @@ pub enum BlockIdRepairType { } impl BlockIdRepairType { - pub(crate) fn block(&self) -> (Slot, Hash) { - match self { - BlockIdRepairType::ParentAndFecSetCount { slot, block_id } => (*slot, *block_id), - BlockIdRepairType::FecSetRoot { slot, block_id, .. } => (*slot, *block_id), + pub(crate) fn block(&self) -> Block { + match *self { + BlockIdRepairType::ParentAndFecSetCount { slot, block_id } => Block { slot, block_id }, + BlockIdRepairType::FecSetRoot { slot, block_id, .. } => Block { slot, block_id }, } } pub(crate) fn slot(&self) -> Slot { - self.block().0 + self.block().slot } } From 865271a2de5fd1b474883566a22a6d50123f5397 Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Fri, 5 Jun 2026 09:32:53 -0400 Subject: [PATCH 217/576] solana-keygen: display the compressed BLS pubkey (#12960) --- Cargo.lock | 2 + clap-utils/Cargo.toml | 1 + clap-utils/src/input_parsers.rs | 65 ++++++++++++++++++++++++++++----- dev-bins/Cargo.lock | 1 + genesis/Cargo.toml | 1 + genesis/src/main.rs | 44 ++++++++++------------ keygen/src/keygen.rs | 29 ++++++++------- programs/sbf/Cargo.lock | 1 + 8 files changed, 97 insertions(+), 47 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 931e69fa214..084e382db24 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7319,6 +7319,7 @@ name = "solana-clap-utils" version = "4.2.0-alpha.0" dependencies = [ "assert_matches", + "bs58", "chrono", "clap 2.33.3", "rpassword", @@ -8332,6 +8333,7 @@ dependencies = [ "agave-snapshots", "base64 0.22.1", "bincode", + "bs58", "clap 2.33.3", "itertools 0.14.0", "serde", diff --git a/clap-utils/Cargo.toml b/clap-utils/Cargo.toml index 92ce02ec88e..cc8b1ba6815 100644 --- a/clap-utils/Cargo.toml +++ b/clap-utils/Cargo.toml @@ -19,6 +19,7 @@ name = "solana_clap_utils" agave-unstable-api = [] [dependencies] +bs58 = { workspace = true, features = ["std"] } chrono = { workspace = true, features = ["default"] } clap = { version = "2.33.0", default-features = false, features = ["suggestions"] } rpassword = { workspace = true } diff --git a/clap-utils/src/input_parsers.rs b/clap-utils/src/input_parsers.rs index fa281c47358..300c7e9adb3 100644 --- a/clap-utils/src/input_parsers.rs +++ b/clap-utils/src/input_parsers.rs @@ -5,7 +5,10 @@ use { }, chrono::DateTime, clap::ArgMatches, - solana_bls_signatures::{Pubkey as BLSPubkey, PubkeyCompressed as BLSPubkeyCompressed}, + solana_bls_signatures::{ + BLS_PUBLIC_KEY_COMPRESSED_SIZE, Pubkey as BLSPubkey, + PubkeyCompressed as BLSPubkeyCompressed, + }, solana_clock::UnixTimestamp, solana_cluster_type::ClusterType, solana_commitment_config::CommitmentConfig, @@ -105,19 +108,45 @@ pub fn pubkeys_of(matches: &ArgMatches<'_>, name: &str) -> Option> { }) } +fn bls_pubkey_compressed_from_base58(value: &str) -> Result { + let bytes = bs58::decode(value) + .into_vec() + .map_err(|err| err.to_string())?; + let bytes: [u8; BLS_PUBLIC_KEY_COMPRESSED_SIZE] = + bytes.try_into().map_err(|bytes: Vec| { + format!( + "expected {BLS_PUBLIC_KEY_COMPRESSED_SIZE} decoded bytes, got {}", + bytes.len() + ) + })?; + Ok(BLSPubkeyCompressed(bytes)) +} + +pub fn bls_pubkey_compressed_from_str(value: &str) -> Result { + if let Ok(bls_pubkey) = BLSPubkeyCompressed::from_str(value) { + return Ok(bls_pubkey); + } + + if let Ok(bls_pubkey) = BLSPubkey::from_str(value) { + return bls_pubkey + .try_into() + .map_err(|err| format!("failed to convert BLS pubkey to compressed form: {err}")); + } + + bls_pubkey_compressed_from_base58(value).map_err(|err| { + format!( + "Failed to parse BLS pubkey as compressed string, uncompressed string, or base58 \ + compressed bytes: {err}" + ) + }) +} + pub fn bls_pubkeys_of(matches: &ArgMatches<'_>, name: &str) -> Option> { matches.values_of(name).map(|values| { values .map(|value| { - // Most of the case it is just a compressed BLS pubkey string - // If the conversion fails, we try to read it as uncompressed BLS pubkey string - BLSPubkeyCompressed::from_str(value).unwrap_or_else(|_| { - let bls_pubkey = BLSPubkey::from_str(value) - .expect("Failed to parse BLS pubkey or compressed BLS pubkey from string"); - bls_pubkey - .try_into() - .expect("Failed to convert to compressed BLS pubkey") - }) + bls_pubkey_compressed_from_str(value) + .expect("Failed to parse BLS pubkey or compressed BLS pubkey from string") }) .collect() }) @@ -466,6 +495,22 @@ mod tests { bls_pubkeys_of(&matches, "multiple"), Some(vec![bls_pubkey1_compressed, bls_pubkey2_compressed]) ); + + // Test that base58-encoded compressed BLS pubkey bytes also work, matching + // the vote-account display format. + let bls_pubkey1_base58 = bs58::encode(bls_pubkey1_compressed.0).into_string(); + let bls_pubkey2_base58 = bs58::encode(bls_pubkey2_compressed.0).into_string(); + let matches = app().get_matches_from(vec![ + "test", + "--multiple", + &bls_pubkey1_base58, + "--multiple", + &bls_pubkey2_base58, + ]); + assert_eq!( + bls_pubkeys_of(&matches, "multiple"), + Some(vec![bls_pubkey1_compressed, bls_pubkey2_compressed]) + ); } #[test] diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index daa68e55417..75b2eb92c8d 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -6378,6 +6378,7 @@ dependencies = [ name = "solana-clap-utils" version = "4.2.0-alpha.0" dependencies = [ + "bs58", "chrono", "clap", "rpassword", diff --git a/genesis/Cargo.toml b/genesis/Cargo.toml index 7792f763e62..1acda478a8a 100644 --- a/genesis/Cargo.toml +++ b/genesis/Cargo.toml @@ -68,6 +68,7 @@ solana-vote-program = { workspace = true } tempfile = { workspace = true } [dev-dependencies] +bs58 = { workspace = true, features = ["std"] } solana-borsh = { workspace = true } solana-genesis = { path = ".", features = ["agave-unstable-api"] } solana-pubkey = { workspace = true, features = ["rand"] } diff --git a/genesis/src/main.rs b/genesis/src/main.rs index 9804be3e5f5..35b31dd7c08 100644 --- a/genesis/src/main.rs +++ b/genesis/src/main.rs @@ -7,10 +7,10 @@ use { clap::{App, Arg, ArgMatches, crate_description, crate_name, value_t, value_t_or_exit}, itertools::Itertools, solana_account::{Account, AccountSharedData, ReadableAccount, WritableAccount}, - solana_bls_signatures::{Pubkey as BLSPubkey, PubkeyCompressed as BLSPubkeyCompressed}, + solana_bls_signatures::PubkeyCompressed as BLSPubkeyCompressed, solana_clap_utils::{ input_parsers::{ - bls_pubkeys_of, cluster_type_of, pubkey_of, pubkeys_of, + bls_pubkey_compressed_from_str, bls_pubkeys_of, cluster_type_of, pubkey_of, pubkeys_of, unix_timestamp_from_rfc3339_datetime, }, input_validators::{ @@ -83,13 +83,7 @@ fn pubkey_from_str(key_str: &str) -> Result> { } fn bls_pubkey_from_str(key_str: &str) -> Result> { - match BLSPubkeyCompressed::from_str(key_str) { - Ok(bls_pubkey) => Ok(bls_pubkey), - Err(_) => { - let bls_pubkey = BLSPubkey::from_str(key_str)?; - Ok(bls_pubkey.try_into()?) - } - } + bls_pubkey_compressed_from_str(key_str).map_err(Into::into) } pub fn load_genesis_accounts(file: &str, genesis_config: &mut GenesisConfig) -> io::Result { @@ -1001,7 +995,7 @@ fn main() -> Result<(), Box> { mod tests { use { super::*, - solana_bls_signatures::keypair::Keypair as BLSKeypair, + solana_bls_signatures::{Pubkey as BLSPubkey, keypair::Keypair as BLSKeypair}, solana_borsh::v1 as borsh1, solana_genesis_config::GenesisConfig, solana_stake_interface as stake, @@ -1357,12 +1351,14 @@ mod tests { assert_eq!(genesis_config.accounts.len(), 3); } - #[test_case(true, true; "add bls compressed pubkey")] - #[test_case(true, false; "add bls pubkey")] - #[test_case(false, false; "no bls pubkey")] + #[test_case(true, true, false; "add bls compressed pubkey")] + #[test_case(true, true, true; "add bls base58 compressed pubkey")] + #[test_case(true, false, false; "add bls pubkey")] + #[test_case(false, false, false; "no bls pubkey")] fn test_append_validator_accounts_to_genesis( add_bls_pubkey: bool, use_compressed_pubkey: bool, + use_base58_pubkey: bool, ) { // Test invalid file returns error assert!( @@ -1382,7 +1378,11 @@ mod tests { let bls_pubkey: BLSPubkey = BLSKeypair::new().public.into_inner().into(); if use_compressed_pubkey { let bls_pubkey_compressed: BLSPubkeyCompressed = bls_pubkey.try_into().unwrap(); - Some(bls_pubkey_compressed.to_string()) + if use_base58_pubkey { + Some(bs58::encode(bls_pubkey_compressed.0).into_string()) + } else { + Some(bls_pubkey_compressed.to_string()) + } } else { Some(bls_pubkey.to_string()) } @@ -1423,7 +1423,9 @@ mod tests { let filename = format!( "test_append_validator_accounts_to_genesis_{}_{}_bls.yml", if add_bls_pubkey { "with" } else { "without" }, - if use_compressed_pubkey { + if use_base58_pubkey { + "base58_compressed" + } else if use_compressed_pubkey { "compressed" } else { "uncompressed" @@ -1466,15 +1468,9 @@ mod tests { let authorized_voters = &vote_state.authorized_voters; assert_eq!(authorized_voters.first().unwrap().1, &identity_pk); if add_bls_pubkey { - let bls_pubkey_compressed_from_input = if use_compressed_pubkey { - BLSPubkeyCompressed::from_str(b64_account.bls_pubkey.as_ref().unwrap()) - .expect("failed to parse BLS pubkey from input") - } else { - BLSPubkey::from_str(b64_account.bls_pubkey.as_ref().unwrap()) - .expect("failed to parse BLS pubkey from input") - .try_into() - .expect("failed to convert BLS pubkey to compressed form") - }; + let bls_pubkey_compressed_from_input = + bls_pubkey_from_str(b64_account.bls_pubkey.as_ref().unwrap()) + .expect("failed to parse BLS pubkey from input"); let bls_pubkey_compressed_from_account = BLSPubkeyCompressed( vote_state .bls_pubkey_compressed diff --git a/keygen/src/keygen.rs b/keygen/src/keygen.rs index d893cac0e8d..b2c232fe94b 100644 --- a/keygen/src/keygen.rs +++ b/keygen/src/keygen.rs @@ -6,7 +6,7 @@ use { Arg, ArgAction, ArgMatches, Command, builder::ValueParser, crate_description, crate_name, value_parser, }, - solana_bls_signatures::{Pubkey as BLSPubkey, keypair::Keypair as BLSKeypair}, + solana_bls_signatures::keypair::Keypair as BLSKeypair, solana_clap_v3_utils::{ DisplayError, input_parsers::{ @@ -394,7 +394,7 @@ fn app<'a>(num_threads: &'a str, crate_version: &'a str) -> Command<'a> { ) .subcommand( Command::new("bls_pubkey") - .about("Display the BLS pubkey derived from given ed25519 keypair file") + .about("Display the compressed BLS pubkey derived from given ed25519 keypair file") .disable_version_flag(true) .arg( Arg::new("keypair") @@ -485,13 +485,17 @@ fn write_pubkey_file(outfile: &str, pubkey: Pubkey) -> Result<(), Box String { + bs58::encode(bls_pubkey_compressed).into_string() +} + fn write_bls_pubkey_file( outfile: &str, - bls_pubkey: BLSPubkey, + bls_pubkey_compressed: &[u8], ) -> Result<(), Box> { use std::io::Write; - let printable = format!("{bls_pubkey}"); + let printable = format_bls_pubkey_compressed(bls_pubkey_compressed); let serialized = serde_json::to_string(&printable)?; if let Some(outdir) = std::path::Path::new(&outfile).parent() { @@ -538,14 +542,14 @@ fn do_main(matches: &ArgMatches) -> Result<(), Box> { ("bls_pubkey", matches) => { let keypair = get_keypair_from_matches(matches, config, &mut wallet_manager)?; let bls_keypair = BLSKeypair::derive_from_signer(&keypair, BLS_KEYPAIR_DERIVE_SEED)?; - let bls_pubkey: BLSPubkey = bls_keypair.public.into_inner().into(); + let bls_pubkey_compressed = bls_keypair.public.to_bytes_compressed(); if matches.try_contains_id("outfile")? { let outfile = matches.get_one::("outfile").unwrap(); check_for_overwrite(outfile, matches)?; - write_bls_pubkey_file(outfile, bls_pubkey)?; + write_bls_pubkey_file(outfile, &bls_pubkey_compressed)?; } else { - println!("{bls_pubkey}"); + println!("{}", format_bls_pubkey_compressed(&bls_pubkey_compressed)); } } ("new", matches) => { @@ -907,12 +911,11 @@ mod tests { Ok(Pubkey::from_str(&printable)?) } - fn read_bls_pubkey_file(infile: &str) -> Result> { + fn read_bls_pubkey_file(infile: &str) -> Result, Box> { let f = std::fs::File::open(infile)?; let printable: String = serde_json::from_reader(f)?; - use std::str::FromStr; - Ok(BLSPubkey::from_str(&printable)?) + Ok(bs58::decode(printable).into_vec()?) } fn process_test_command(args: &[&str]) -> Result<(), Box> { @@ -1292,8 +1295,8 @@ mod tests { fn test_read_write_bls_pubkey() -> Result<(), std::boxed::Box> { let filename = "test_bls_pubkey.json"; let bls_keypair = BLSKeypair::new(); - let bls_pubkey: BLSPubkey = bls_keypair.public.into_inner().into(); - write_bls_pubkey_file(filename, bls_pubkey)?; + let bls_pubkey = bls_keypair.public.to_bytes_compressed(); + write_bls_pubkey_file(filename, &bls_pubkey)?; let read = read_bls_pubkey_file(filename)?; assert_eq!(read, bls_pubkey); std::fs::remove_file(filename)?; @@ -1323,7 +1326,7 @@ mod tests { let bls_keypair = BLSKeypair::derive_from_signer(&my_keypair, BLS_KEYPAIR_DERIVE_SEED).unwrap(); let read_bls_pubkey = read_bls_pubkey_file(&outfile_path).unwrap(); - assert_eq!(read_bls_pubkey, bls_keypair.public.into_inner().into()); + assert_eq!(read_bls_pubkey, bls_keypair.public.to_bytes_compressed()); } #[test] diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index 7c325a99906..0cfee5b8e99 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -6225,6 +6225,7 @@ dependencies = [ name = "solana-clap-utils" version = "4.2.0-alpha.0" dependencies = [ + "bs58", "chrono", "clap", "rpassword", From 71aadb6f9f0a59d1a195f8a881911670d577c73a Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Fri, 5 Jun 2026 09:44:49 -0400 Subject: [PATCH 218/576] votor: commitment cache update on rooted not finalization certificate (#12962) --- core/src/commitment_service.rs | 4 +- votor/src/commitment.rs | 8 ++-- votor/src/consensus_pool_service.rs | 64 ++++++++--------------------- votor/src/root_utils.rs | 13 +++++- votor/src/votor.rs | 1 - 5 files changed, 34 insertions(+), 56 deletions(-) diff --git a/core/src/commitment_service.rs b/core/src/commitment_service.rs index 356d96a3995..2c0170c84ae 100644 --- a/core/src/commitment_service.rs +++ b/core/src/commitment_service.rs @@ -193,9 +193,9 @@ impl AggregateCommitmentService { // Notarize (our first round vote in favor of a block) satisfies the Processed commitment level w_block_commitment_cache.set_slot(slot); } - AlpenglowCommitmentType::Finalized => { + AlpenglowCommitmentType::Rooted => { // There is no distinction of OC, root, or finalized in Alpengow commitment. - // When receiving a finalilzation certificate we set all of these values. + // Once votor selects a finalized bank as root, set all of these values. w_block_commitment_cache.set_highest_confirmed_slot(slot); w_block_commitment_cache.set_root(slot); w_block_commitment_cache.set_highest_super_majority_root(slot); diff --git a/votor/src/commitment.rs b/votor/src/commitment.rs index 8901191702a..06bfc23febf 100644 --- a/votor/src/commitment.rs +++ b/votor/src/commitment.rs @@ -14,8 +14,8 @@ pub enum CommitmentError { pub enum CommitmentType { /// Our node has voted notarize for the slot Notarize, - /// We have observed a finalization certificate for the slot - Finalized, + /// Votor has selected the slot as root + Rooted, } #[derive(Debug, PartialEq)] @@ -65,7 +65,7 @@ mod tests { } ); let slot = 5; - let commitment_type = CommitmentType::Finalized; + let commitment_type = CommitmentType::Rooted; update_commitment_cache(commitment_type, slot, &commitment_sender) .expect("Failed to send commitment data"); let received_data = commitment_receiver @@ -74,7 +74,7 @@ mod tests { assert_eq!( received_data, CommitmentAggregationData { - commitment_type: CommitmentType::Finalized, + commitment_type: CommitmentType::Rooted, slot, } ); diff --git a/votor/src/consensus_pool_service.rs b/votor/src/consensus_pool_service.rs index c662e7252c0..455eb6c1c86 100644 --- a/votor/src/consensus_pool_service.rs +++ b/votor/src/consensus_pool_service.rs @@ -5,7 +5,6 @@ mod stats; use { crate::{ - commitment::{CommitmentAggregationData, CommitmentType, update_commitment_cache}, common::DELTA_STANDSTILL, consensus_pool::{ AddVoteError, ConsensusPool, @@ -59,7 +58,6 @@ pub(crate) struct ConsensusPoolContext { pub(crate) bls_sender: Sender, pub(crate) event_sender: VotorEventSender, - pub(crate) commitment_sender: Sender, pub(crate) repair_event_sender: RepairEventSender, /// Used to communicate the highest finalization cert the pool has observed to the block creation loop. @@ -189,17 +187,15 @@ impl ConsensusPoolService { } } let root_bank = ctx.sharable_banks.root(); - let (new_finalized_slot, new_certificates_to_send) = - Self::add_message_and_maybe_update_commitment( - &root_bank, - &ctx.cluster_info.id(), - &ctx.my_vote_pubkey, - message, - consensus_pool, - events, - &ctx.commitment_sender, - stats, - )?; + let (new_finalized_slot, new_certificates_to_send) = Self::add_message( + &root_bank, + &ctx.cluster_info.id(), + &ctx.my_vote_pubkey, + message, + consensus_pool, + events, + stats, + )?; Self::maybe_update_root_and_send_new_certificates( consensus_pool, &ctx.sharable_banks, @@ -419,17 +415,16 @@ impl ConsensusPoolService { Ok(()) } - /// Adds a vote to the certificate pool and updates the commitment cache if necessary + /// Adds a vote to the certificate pool. /// /// If a new finalization slot was recognized, returns the slot - fn add_message_and_maybe_update_commitment( + fn add_message( root_bank: &Bank, my_pubkey: &Pubkey, my_vote_pubkey: &Pubkey, message: ConsensusMessage, consensus_pool: &mut ConsensusPool, votor_events: &mut Vec, - commitment_sender: &Sender, stats: &mut ConsensusPoolServiceStats, ) -> Result<(Option, Vec>), AddVoteError> { let (new_finalized_slot, new_certificates_to_send) = @@ -438,11 +433,7 @@ impl ConsensusPoolService { return Ok((None, new_certificates_to_send)); }; trace!("{my_pubkey}: new finalization certificate for {new_finalized_slot}"); - update_commitment_cache( - CommitmentType::Finalized, - new_finalized_slot, - commitment_sender, - )?; + // RPC-facing finalized commitment is updated after votor selects a root. stats.standstill = false; Ok((Some(new_finalized_slot), new_certificates_to_send)) } @@ -646,8 +637,6 @@ mod tests { consensus_pool: ConsensusPool, bls_sender: Sender, bls_receiver: Receiver, - commitment_sender: Sender, - commitment_receiver: Receiver, validator_keypairs: Vec, leader_schedule_cache: Arc, sharable_banks: SharableBanks, @@ -662,8 +651,6 @@ mod tests { impl Default for TestContext { fn default() -> Self { let (bls_sender, bls_receiver) = crossbeam_channel::unbounded(); - let (commitment_sender, commitment_receiver) = crossbeam_channel::unbounded(); - // Create 10 node validatorvotekeypairs vec let validator_keypairs = (0..10) .map(|_| ValidatorVoteKeypairs::new_rand()) @@ -709,8 +696,6 @@ mod tests { consensus_pool, bls_sender, bls_receiver, - commitment_sender, - commitment_receiver, validator_keypairs, leader_schedule_cache, sharable_banks, @@ -727,8 +712,7 @@ mod tests { /// Test the full consensus message flow: /// 1. Validators 0-7 send notarize votes for slot 2. After processing all /// votes, we expect a notarize/finalize certificate to be produced and - /// forwarded via the BLS channel, a finalized event to be emitted, and a - /// commitment update to be sent. + /// forwarded via the BLS channel and a finalized event to be emitted. /// 2. A skip certificate is then sent for slot 3 and we verify it is /// immediately forwarded via the BLS channel. #[test] @@ -760,14 +744,13 @@ mod tests { }); let mut stats = ConsensusPoolServiceStats::new(); - let result = ConsensusPoolService::add_message_and_maybe_update_commitment( + let result = ConsensusPoolService::add_message( &root_bank, &ctx.my_pubkey, &ctx.my_vote_pubkey, message, &mut ctx.consensus_pool, &mut events, - &ctx.commitment_sender, &mut stats, ); assert!(result.is_ok()); @@ -819,17 +802,6 @@ mod tests { "Should have received a finalized event" ); - // Verify that we received a commitment update - let commitment = ctx.commitment_receiver.try_recv(); - assert!(commitment.is_ok()); - let CommitmentAggregationData { - commitment_type, - slot, - .. - } = commitment.unwrap(); - assert_eq!(commitment_type, CommitmentType::Finalized); - assert_eq!(slot, target_slot); - // Now send a Skip certificate on slot 3, should be forwarded immediately let target_slot = 3; let skip_certificate = Certificate { @@ -840,14 +812,13 @@ mod tests { events.clear(); let mut stats = ConsensusPoolServiceStats::new(); - let result = ConsensusPoolService::add_message_and_maybe_update_commitment( + let result = ConsensusPoolService::add_message( &root_bank, &ctx.my_pubkey, &ctx.my_vote_pubkey, ConsensusMessage::Certificate(skip_certificate), &mut ctx.consensus_pool, &mut events, - &ctx.commitment_sender, &mut stats, ); assert!(result.is_ok()); @@ -904,14 +875,13 @@ mod tests { bitmap: vec![], }; - let result = ConsensusPoolService::add_message_and_maybe_update_commitment( + let result = ConsensusPoolService::add_message( &root_bank, &ctx.my_pubkey, &ctx.my_vote_pubkey, ConsensusMessage::Certificate(skip_certificate), &mut ctx.consensus_pool, &mut events, - &ctx.commitment_sender, &mut stats, ); assert!(result.is_ok()); @@ -932,7 +902,6 @@ mod tests { consensus_message_receiver: crossbeam_channel::unbounded().1, bls_sender: ctx.bls_sender.clone(), event_sender: crossbeam_channel::unbounded().0, - commitment_sender: ctx.commitment_sender.clone(), highest_finalized: ctx.highest_finalized.clone(), repair_event_sender, }; @@ -1003,7 +972,6 @@ mod tests { consensus_message_receiver, bls_sender: ctx.bls_sender.clone(), event_sender, - commitment_sender: ctx.commitment_sender.clone(), highest_finalized: ctx.highest_finalized.clone(), repair_event_sender, }; diff --git a/votor/src/root_utils.rs b/votor/src/root_utils.rs index eec62f26a9e..7aadfa4fcb1 100644 --- a/votor/src/root_utils.rs +++ b/votor/src/root_utils.rs @@ -1,5 +1,10 @@ use { - crate::{event_handler::PendingBlocks, voting_utils::VotingContext, votor::SharedContext}, + crate::{ + commitment::{CommitmentType, update_commitment_cache}, + event_handler::PendingBlocks, + voting_utils::VotingContext, + votor::SharedContext, + }, agave_votor_messages::consensus_message::Block, crossbeam_channel::Sender, solana_clock::Slot, @@ -64,6 +69,12 @@ pub(crate) fn set_root( ); } + if let Err(err) = + update_commitment_cache(CommitmentType::Rooted, new_root, &vctx.commitment_sender) + { + warn!("failed to update Alpenglow rooted commitment for root {new_root}: {err}"); + } + // It is critical to send the OC notification in order to keep compatibility with // the RPC API. Additionally the PrioritizationFeeCache relies on this notification // in order to perform cleanup. In the future we will look to deprecate OC and remove diff --git a/votor/src/votor.rs b/votor/src/votor.rs index f943f6961b8..2943d804919 100644 --- a/votor/src/votor.rs +++ b/votor/src/votor.rs @@ -239,7 +239,6 @@ impl Votor { consensus_message_receiver, bls_sender, event_sender, - commitment_sender, repair_event_sender, highest_finalized, }; From 5d5d512850be032824b119b9eb59d8d0b98e5b78 Mon Sep 17 00:00:00 2001 From: Yihau Chen Date: Fri, 5 Jun 2026 21:53:07 +0800 Subject: [PATCH 219/576] ci: use github app token in dependabot pr pipeline (#12979) * Reapply "ci: use github app token in dependabot pr (#12942)" (#12975) This reverts commit 40587a459ed235dbace4b7e2871e8478d708afcd. * udpate pipeline --- .github/workflows/dependabot-pr.yml | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/.github/workflows/dependabot-pr.yml b/.github/workflows/dependabot-pr.yml index 7280cbe302e..4d6ac65a503 100644 --- a/.github/workflows/dependabot-pr.yml +++ b/.github/workflows/dependabot-pr.yml @@ -11,11 +11,27 @@ jobs: runs-on: ubuntu-24.04 if: github.event.pull_request.user.login == 'dependabot[bot]' steps: + - name: Create github app token + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 + id: app-token + with: + client-id: ${{ secrets.CLIENT_ID }} + private-key: ${{ secrets.PRIVATE_KEY }} + + - name: Set git config + run: | + git config --global user.email "49699333+dependabot[bot]@users.noreply.github.com" + git config --global user.name "dependabot[bot]" + git config --global url."https://x-access-token:${GITHUB_TOKEN}@github.com/".insteadOf https://github.com/ + env: + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} + - name: checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.event.pull_request.head.ref }} - token: ${{ secrets.PAT }} + token: ${{ steps.app-token.outputs.token }} + persist-credentials: false - name: update code env: @@ -45,8 +61,6 @@ jobs: if [ -z "$(git status --porcelain)" ]; then echo "✅ Nothing to update" else - git config user.email "49699333+dependabot[bot]@users.noreply.github.com" - git config user.name "dependabot[bot]" git add **/Cargo.toml git add **/Cargo.lock git commit -am "Update all workspaces" From b9b20e454340abe4eec086e09e19a5f60e04a37f Mon Sep 17 00:00:00 2001 From: Akhilesh Singhania Date: Fri, 5 Jun 2026 16:15:35 +0200 Subject: [PATCH 220/576] Cleans up `BLSOp` and removes `Copy` derivation on `VoteMessage` (#12952) Cleans up BLSOp and removes Copy derivation on VoteMessage --- core/src/bls_sigverify/bls_vote_sigverify.rs | 20 ++++++------ core/src/replay_stage.rs | 7 ++-- votor-messages/src/consensus_message.rs | 2 +- votor/src/consensus_pool.rs | 6 ++-- votor/src/consensus_pool/vote_pool.rs | 20 ++++++------ votor/src/event_handler.rs | 22 ++++++------- votor/src/event_handler/stats.rs | 8 ++--- votor/src/voting_service.rs | 27 ++++++++-------- votor/src/voting_utils.rs | 34 +++++++++----------- 9 files changed, 69 insertions(+), 77 deletions(-) diff --git a/core/src/bls_sigverify/bls_vote_sigverify.rs b/core/src/bls_sigverify/bls_vote_sigverify.rs index af298633217..ae15d11a505 100644 --- a/core/src/bls_sigverify/bls_vote_sigverify.rs +++ b/core/src/bls_sigverify/bls_vote_sigverify.rs @@ -90,7 +90,7 @@ pub(super) fn verify_and_send_votes( stats.sig_verified_votes += verified_votes.len() as u64; let (votes_for_pool, msgs_for_repair, msg_for_reward, msg_for_metrics) = - process_verified_votes(&verified_votes, root_bank, cluster_info, leader_schedule); + process_verified_votes(verified_votes, root_bank, cluster_info, leader_schedule); send_votes_to_pool(votes_for_pool, &channels.channel_to_pool, &mut stats)?; send_votes_to_repair(msgs_for_repair, &channels.channel_to_repair, &mut stats)?; @@ -124,7 +124,7 @@ fn inspect_for_repair(vote: &VotePayload, msgs_for_repair: &mut HashMap, root_bank: &Bank, cluster_info: &ClusterInfo, leader_schedule: &LeaderScheduleCache, @@ -138,25 +138,23 @@ fn process_verified_votes( let mut msgs_for_repair = HashMap::new(); let mut votes_for_pool = Vec::with_capacity(verified_votes.len()); let mut votes_for_metrics = Vec::with_capacity(verified_votes.len()); - for vote in verified_votes { - let vote_message = vote.vote_message; + for payload in verified_votes { if crate::block_creation_loop::rewards::certs_builder::wants_vote( cluster_info, leader_schedule, root_bank.slot(), - &vote_message, + &payload.vote_message, ) { - votes_for_reward.push(vote_message); + votes_for_reward.push(payload.vote_message.clone()); } - inspect_for_repair(vote, &mut msgs_for_repair); - - votes_for_pool.push(ConsensusMessage::Vote(vote_message)); + inspect_for_repair(&payload, &mut msgs_for_repair); votes_for_metrics.push(ConsensusMetricsEvent::Vote { - id: vote.pubkey, - vote: vote.vote_message.vote, + id: payload.pubkey, + vote: payload.vote_message.vote, }); + votes_for_pool.push(ConsensusMessage::Vote(payload.vote_message)); } let msgs_for_repair = msgs_for_repair .into_iter() diff --git a/core/src/replay_stage.rs b/core/src/replay_stage.rs index b5891da9f44..794c800bf77 100644 --- a/core/src/replay_stage.rs +++ b/core/src/replay_stage.rs @@ -1748,17 +1748,16 @@ impl ReplayStage { None, &mut HashMap::new(), ) { - GenerateVoteTxResult::ConsensusMessage(message) => { + GenerateVoteTxResult::Vote(vote_msg) => { // Send vote to ConsensusPool and rest of cluster warn!( "{} Alpenglow migration: Casting genesis vote for ({block:?})", identity_keypair.pubkey() ); // If sending fails that means the channel is disconnected and we are shutting down - let _ = own_vote_sender.send(vec![message.clone()]); + let _ = own_vote_sender.send(vec![ConsensusMessage::Vote(vote_msg.clone())]); let _ = bls_sender.send(BLSOp::PushVote { - message: Arc::new(message), - slot: block.slot, + vote: Arc::new(vote_msg), saved_vote_history: agave_votor::vote_history_storage::SavedVoteHistoryVersions::Current( SavedVoteHistory::default(), diff --git a/votor-messages/src/consensus_message.rs b/votor-messages/src/consensus_message.rs index 369902f940b..c4be5913c45 100644 --- a/votor-messages/src/consensus_message.rs +++ b/votor-messages/src/consensus_message.rs @@ -54,7 +54,7 @@ pub struct Block { derive(AbiExample), frozen_abi(digest = "CTiXEk2aQbpf6TS6PNKcaTsGkLruDvAYsTLFhHKW2vsm") )] -#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, SchemaWrite, SchemaRead)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, SchemaWrite, SchemaRead)] pub struct VoteMessage { /// The type of the vote. pub vote: Vote, diff --git a/votor/src/consensus_pool.rs b/votor/src/consensus_pool.rs index 3464b480cc5..b0a82ceb3ee 100644 --- a/votor/src/consensus_pool.rs +++ b/votor/src/consensus_pool.rs @@ -408,7 +408,7 @@ impl ConsensusPool { vote_message: VoteMessage, events: &mut Vec, ) -> Result>, AddVoteError> { - let vote = &vote_message.vote; + let vote = vote_message.vote; let rank = vote_message.rank; let vote_slot = vote.slot(); let (validator_vote_key, validator_stake, total_stake) = @@ -452,7 +452,7 @@ impl ConsensusPool { .entry(vote_slot) .or_insert_with(|| SlotStakeCounters::new(total_stake)); fallback_vote_counters.add_vote( - vote, + &vote, entry_stake, my_vote_pubkey == &validator_vote_key, events, @@ -463,7 +463,7 @@ impl ConsensusPool { } self.stats.incr_ingested_vote_type(vote_type); - self.update_certificates(root_bank, vote, block_id, events, total_stake) + self.update_certificates(root_bank, &vote, block_id, events, total_stake) } fn add_certificate( diff --git a/votor/src/consensus_pool/vote_pool.rs b/votor/src/consensus_pool/vote_pool.rs index d4afa8638fa..c663a573c82 100644 --- a/votor/src/consensus_pool/vote_pool.rs +++ b/votor/src/consensus_pool/vote_pool.rs @@ -153,14 +153,14 @@ mod test { let my_pubkey = Pubkey::new_unique(); assert_eq!( - vote_pool.add_vote(my_pubkey, NonZero::new(10).unwrap(), vote_message), + vote_pool.add_vote(my_pubkey, NonZero::new(10).unwrap(), vote_message.clone()), Some(10) ); assert_eq!(vote_pool.total_stake(), 10); // Adding the same key again should fail assert_eq!( - vote_pool.add_vote(my_pubkey, NonZero::new(10).unwrap(), vote_message), + vote_pool.add_vote(my_pubkey, NonZero::new(10).unwrap(), vote_message.clone()), None ); assert_eq!(vote_pool.total_stake(), 10); @@ -186,20 +186,20 @@ mod test { rank: 1, }; assert_eq!( - vote_pool.add_vote(my_pubkey, NonZero::new(10).unwrap(), vote), + vote_pool.add_vote(my_pubkey, NonZero::new(10).unwrap(), vote.clone()), Some(10) ); assert_eq!(vote_pool.total_stake_by_block_id(&block_id), 10); // Adding the same key again should fail assert_eq!( - vote_pool.add_vote(my_pubkey, NonZero::new(10).unwrap(), vote), + vote_pool.add_vote(my_pubkey, NonZero::new(10).unwrap(), vote.clone()), None ); // Adding a different bankhash should fail assert_eq!( - vote_pool.add_vote(my_pubkey, NonZero::new(10).unwrap(), vote), + vote_pool.add_vote(my_pubkey, NonZero::new(10).unwrap(), vote.clone()), None ); @@ -235,7 +235,7 @@ mod test { // Adding the first 3 votes should succeed, but total_stake should remain at 10 for vote in votes.iter().take(3).cloned() { assert_eq!( - vote_pool.add_vote(my_pubkey, NonZero::new(10).unwrap(), vote), + vote_pool.add_vote(my_pubkey, NonZero::new(10).unwrap(), vote.clone()), Some(10) ); assert_eq!( @@ -245,7 +245,7 @@ mod test { } // Adding the 4th vote should fail assert_eq!( - vote_pool.add_vote(my_pubkey, NonZero::new(10).unwrap(), votes[3]), + vote_pool.add_vote(my_pubkey, NonZero::new(10).unwrap(), votes[3].clone()), None ); assert_eq!( @@ -257,7 +257,7 @@ mod test { let new_pubkey = Pubkey::new_unique(); for vote in votes.iter().skip(1).take(2).cloned() { assert_eq!( - vote_pool.add_vote(new_pubkey, NonZero::new(60).unwrap(), vote), + vote_pool.add_vote(new_pubkey, NonZero::new(60).unwrap(), vote.clone()), Some(70) ); assert_eq!( @@ -268,7 +268,7 @@ mod test { // The new key only added 2 votes, so adding block_ids[3] should succeed assert_eq!( - vote_pool.add_vote(new_pubkey, NonZero::new(60).unwrap(), votes[3]), + vote_pool.add_vote(new_pubkey, NonZero::new(60).unwrap(), votes[3].clone()), Some(60) ); assert_eq!( @@ -278,7 +278,7 @@ mod test { // Now if adding the same key again, it should fail assert_eq!( - vote_pool.add_vote(new_pubkey, NonZero::new(60).unwrap(), votes[0]), + vote_pool.add_vote(new_pubkey, NonZero::new(60).unwrap(), votes[0].clone()), None ); } diff --git a/votor/src/event_handler.rs b/votor/src/event_handler.rs index 7c7277690a0..687ef8432d3 100644 --- a/votor/src/event_handler.rs +++ b/votor/src/event_handler.rs @@ -1348,15 +1348,15 @@ mod tests { let expected_vote_serialized = wincode::serialize(v).unwrap(); let signature: BLSSignature = self.my_bls_keypair.sign(&expected_vote_serialized).into(); - let expected_message = ConsensusMessage::Vote(VoteMessage { + let expected_message = VoteMessage { vote: *v, rank: 0, signature, - }); + }; let prev_length = self.bls_ops.len(); - self.bls_ops.retain(|bls_op| { - !matches!(bls_op, BLSOp::PushVote { message, .. } if **message == expected_message) - }); + self.bls_ops.retain( + |bls_op| !matches!(bls_op, BLSOp::PushVote{ vote, .. } if **vote == expected_message), + ); assert!( self.bls_ops.len() < prev_length, "Did not find expected vote: {expected_message:?}", @@ -1368,22 +1368,22 @@ mod tests { let expected_vote_serialized = wincode::serialize(expected_vote).unwrap(); let signature: BLSSignature = self.my_bls_keypair.sign(&expected_vote_serialized).into(); - let expected_message = ConsensusMessage::Vote(VoteMessage { + let expected_message = VoteMessage { vote: *expected_vote, rank: 0, signature, - }); + }; let prev_length = self.bls_ops.len(); - self.bls_ops.retain(|bls_op| { - !matches!(bls_op, BLSOp::PushVote { message, .. } if **message == expected_message) - }); + self.bls_ops.retain( + |bls_op| !matches!(bls_op, BLSOp::PushVote { vote, .. } if **vote == expected_message), + ); assert!( self.bls_ops.len() < prev_length, "Did not find expected vote: {expected_message:?}", ); // Also check own_vote_receiver let own_vote = self.own_vote_receiver.try_recv().unwrap(); - assert_eq!(own_vote, vec![expected_message]); + assert_eq!(own_vote, vec![ConsensusMessage::Vote(expected_message)]); } fn check_for_commitment(&mut self, expected_type: CommitmentType, expected_slot: Slot) { diff --git a/votor/src/event_handler/stats.rs b/votor/src/event_handler/stats.rs index c1228e0f0cb..c1d6bdee9bf 100644 --- a/votor/src/event_handler/stats.rs +++ b/votor/src/event_handler/stats.rs @@ -1,6 +1,6 @@ use { crate::{event::VotorEvent, voting_service::BLSOp}, - agave_votor_messages::{consensus_message::ConsensusMessage, vote::VoteType}, + agave_votor_messages::vote::VoteType, solana_clock::Slot, solana_metrics::datapoint_info, std::{ @@ -185,11 +185,7 @@ impl EventHandlerStats { } pub fn incr_vote(&mut self, bls_op: &BLSOp) { - if let BLSOp::PushVote { message, .. } = bls_op { - let ConsensusMessage::Vote(vote) = **message else { - warn!("Unexpected BLS message type: {message:?}"); - return; - }; + if let BLSOp::PushVote { vote, .. } = bls_op { let vote_type = vote.vote.get_type(); let entry = self.sent_votes.entry(vote_type).or_insert(0); *entry = entry.saturating_add(1); diff --git a/votor/src/voting_service.rs b/votor/src/voting_service.rs index 4dbb3278814..fb0202b1cee 100644 --- a/votor/src/voting_service.rs +++ b/votor/src/voting_service.rs @@ -3,7 +3,10 @@ use { staked_validators_cache::StakedValidatorsCache, vote_history_storage::{SavedVoteHistoryVersions, VoteHistoryStorage}, }, - agave_votor_messages::{certificate::Certificate, consensus_message::ConsensusMessage}, + agave_votor_messages::{ + certificate::Certificate, + consensus_message::{ConsensusMessage, VoteMessage}, + }, crossbeam_channel::Receiver, solana_client::connection_cache::ConnectionCache, solana_clock::Slot, @@ -31,8 +34,7 @@ const STAKED_VALIDATORS_CACHE_NUM_EPOCH_TARGET: usize = 3; #[derive(Debug)] pub enum BLSOp { PushVote { - message: Arc, - slot: Slot, + vote: Arc, saved_vote_history: SavedVoteHistoryVersions, }, PushCertificate { @@ -202,8 +204,7 @@ impl VotingService { ) { match bls_op { BLSOp::PushVote { - message, - slot, + vote, saved_vote_history, } => { let mut measure = Measure::start("alpenglow vote history save"); @@ -213,21 +214,22 @@ impl VotingService { } measure.stop(); trace!("{measure}"); - + let slot = vote.vote.slot(); + let msg = ConsensusMessage::Vote(Arc::unwrap_or_clone(vote)); Self::broadcast_consensus_message( slot, cluster_info, - &message, + &msg, connection_cache, additional_listeners, staked_validators_cache, ); } BLSOp::PushCertificate { certificate } => { - let vote_slot = certificate.cert_type.slot(); - let message = ConsensusMessage::Certificate((*certificate).clone()); + let slot = certificate.cert_type.slot(); + let message = ConsensusMessage::Certificate(Arc::unwrap_or_clone(certificate)); Self::broadcast_consensus_message( - vote_slot, + slot, cluster_info, &message, connection_cache, @@ -323,12 +325,11 @@ mod tests { } #[test_case(BLSOp::PushVote { - message: Arc::new(ConsensusMessage::Vote(VoteMessage { + vote: Arc::new(VoteMessage { vote: Vote::new_skip_vote(5), signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), rank: 1, - })), - slot: 5, + }), saved_vote_history: SavedVoteHistoryVersions::Current(SavedVoteHistory::default()), }, ConsensusMessage::Vote(VoteMessage { vote: Vote::new_skip_vote(5), diff --git a/votor/src/voting_utils.rs b/votor/src/voting_utils.rs index b6190adf116..f336d79008d 100644 --- a/votor/src/voting_utils.rs +++ b/votor/src/voting_utils.rs @@ -53,8 +53,8 @@ pub enum GenerateVoteTxResult { // The following are the successful cases // Generated a vote transaction Tx(Transaction), - // Generated a ConsensusMessage - ConsensusMessage(ConsensusMessage), + // Generated a VoteMessage + Vote(VoteMessage), } impl GenerateVoteTxResult { @@ -74,7 +74,7 @@ impl GenerateVoteTxResult { | Self::WaitForStartupVerification | Self::WaitToVoteSlot(_) | Self::NoRankFound => false, - Self::Tx(_) | Self::ConsensusMessage(_) => false, + Self::Tx(_) | Self::Vote(_) => false, } } @@ -86,7 +86,7 @@ impl GenerateVoteTxResult { | Self::WaitForStartupVerification | Self::WaitToVoteSlot(_) | Self::NoRankFound => true, - Self::Tx(_) | Self::ConsensusMessage(_) => false, + Self::Tx(_) | Self::Vote(_) => false, } } } @@ -216,11 +216,11 @@ pub fn generate_vote_tx( }; let vote_serialized = wincode::serialize(&vote).unwrap(); - GenerateVoteTxResult::ConsensusMessage(ConsensusMessage::Vote(VoteMessage { + GenerateVoteTxResult::Vote(VoteMessage { vote, signature: bls_keypair.sign(&vote_serialized).into(), rank: my_rank, - })) + }) } /// Send an alpenglow vote as a BLSMessage @@ -244,7 +244,7 @@ fn insert_vote_and_create_bls_message( } let bank = context.sharable_banks.root(); - let message = match generate_vote_tx( + let vote_msg = match generate_vote_tx( vote, &bank, context.vote_account_pubkey, @@ -253,7 +253,7 @@ fn insert_vote_and_create_bls_message( context.wait_to_vote_slot, &mut context.derived_bls_keypairs, ) { - GenerateVoteTxResult::ConsensusMessage(m) => m, + GenerateVoteTxResult::Vote(vote) => vote, e => { if e.is_transient_error() { return Err(VoteError::TransientError(Box::new(e))); @@ -264,7 +264,7 @@ fn insert_vote_and_create_bls_message( }; context .own_vote_sender - .send(vec![message.clone()]) + .send(vec![ConsensusMessage::Vote(vote_msg.clone())]) .map_err(|_| SendError(()))?; // TODO: for refresh votes use a different BLSOp so we don't have to rewrite the same vote history to file @@ -273,8 +273,7 @@ fn insert_vote_and_create_bls_message( // Return vote for sending Ok(BLSOp::PushVote { - message: Arc::new(message), - slot: vote.slot(), + vote: Arc::new(vote_msg), saved_vote_history: SavedVoteHistoryVersions::from(saved_vote_history), }) } @@ -413,16 +412,15 @@ mod tests { .unwrap(); let expected_message = generate_expected_consensus_message(vote, &my_bls_keypair); if let BLSOp::PushVote { - message, - slot, + vote, saved_vote_history, - } = &result + } = result { - assert_eq!(slot, &vote_slot); - assert_eq!(**message, expected_message); + let msg = ConsensusMessage::Vote(Arc::unwrap_or_clone(vote)); + assert_eq!(msg, expected_message); assert_eq!( saved_vote_history, - &SavedVoteHistoryVersions::from( + SavedVoteHistoryVersions::from( SavedVoteHistory::new( &voting_context.vote_history, &voting_context.identity_keypair @@ -431,7 +429,7 @@ mod tests { ) ); } else { - panic!("Expected BLSOp::PushVote, got {result:?}"); + panic!("Expected BLSOp::VotePush, got {result:?}"); } // Check that own vote sender receives the vote From 33e3f3ba72d32c7e916e35fdcdfcb523508abc75 Mon Sep 17 00:00:00 2001 From: Akhilesh Singhania Date: Fri, 5 Jun 2026 17:14:31 +0200 Subject: [PATCH 221/576] Updates channel between bls sigverifier and consensus pool (#12955) --- core/src/bls_sigverify/bls_cert_sigverify.rs | 15 +- core/src/bls_sigverify/bls_sigverifier.rs | 193 ++++++++++++------- core/src/bls_sigverify/bls_vote_sigverify.rs | 7 +- core/src/bls_sigverify/utils.rs | 26 +-- core/src/replay_stage.rs | 16 +- ledger/src/blockstore_processor.rs | 6 +- runtime/src/block_component_processor.rs | 16 +- votor-messages/src/certificate.rs | 2 +- votor-messages/src/consensus_message.rs | 30 ++- votor/src/consensus_pool_service.rs | 82 ++++++-- votor/src/event_handler.rs | 6 +- votor/src/voting_utils.rs | 26 +-- votor/src/votor.rs | 6 +- 13 files changed, 280 insertions(+), 151 deletions(-) diff --git a/core/src/bls_sigverify/bls_cert_sigverify.rs b/core/src/bls_sigverify/bls_cert_sigverify.rs index 2dc83daf07c..63aa9554024 100644 --- a/core/src/bls_sigverify/bls_cert_sigverify.rs +++ b/core/src/bls_sigverify/bls_cert_sigverify.rs @@ -4,7 +4,7 @@ use { agave_bls_cert_verify::cert_verify::Error as BlsCertVerifyError, agave_votor_messages::{ certificate::{Certificate, CertificateType}, - consensus_message::ConsensusMessage, + consensus_message::SigVerifiedBatch, fraction::Fraction, }, crossbeam_channel::Sender, @@ -52,7 +52,7 @@ pub(super) fn verify_and_send_certificates( verified_certs_set: &mut HashSet, certs: Vec, root_bank: &Bank, - channel_to_pool: &Sender>, + channel_to_pool: &Sender, banlist: &SimpleQosBanlist, thread_pool: &ThreadPool, ) -> Result { @@ -89,7 +89,7 @@ pub(super) fn verify_and_send_certificates( /// /// The valid certs are inserted into the [`verified_certs_set`]. /// Invalid cert senders are banlisted. -/// Returns a Vec of [`ConsensusMessage`] constructed from the valid certs. +/// Returns a [`SigVerifiedBatch`] constructed from the valid certs. fn verify_certs( certs: Vec, root_bank: &Bank, @@ -97,7 +97,7 @@ fn verify_certs( stats: &mut SigVerifyCertStats, banlist: &SimpleQosBanlist, thread_pool: &ThreadPool, -) -> Vec { +) -> SigVerifiedBatch { let verified = thread_pool.install(|| { certs .into_par_iter() @@ -108,7 +108,7 @@ fn verify_certs( .collect::>() }); - verified + let certs = verified .into_iter() .filter_map(|(cert_payload, res)| match res { Ok(()) => { @@ -116,7 +116,7 @@ fn verify_certs( if !verified_certs_set.insert(cert.cert_type) { stats.unnecessary_certs_verified += 1; } - Some(ConsensusMessage::Certificate(cert)) + Some(cert) } Err(e) => { match &e { @@ -148,7 +148,8 @@ fn verify_certs( None } }) - .collect() + .collect(); + SigVerifiedBatch::Certificates(certs) } fn verify_cert(cert: &Certificate, root_bank: &Bank) -> Result<(), CertVerifyError> { diff --git a/core/src/bls_sigverify/bls_sigverifier.rs b/core/src/bls_sigverify/bls_sigverifier.rs index a49acfb9439..fb620beb366 100644 --- a/core/src/bls_sigverify/bls_sigverifier.rs +++ b/core/src/bls_sigverify/bls_sigverifier.rs @@ -16,7 +16,7 @@ use { }, agave_votor_messages::{ certificate::CertificateType, - consensus_message::{ConsensusMessage, VoteMessage}, + consensus_message::{ConsensusMessage, SigVerifiedBatch, VoteMessage}, migration::MigrationStatus, }, crossbeam_channel::{Receiver, RecvTimeoutError, Sender, TryRecvError}, @@ -64,7 +64,7 @@ pub(crate) struct SigVerifierChannels { pub(crate) packet_receiver: Receiver, pub(crate) channel_to_repair: VerifiedVoterSlotsSender, pub(crate) channel_to_reward: Sender, - pub(crate) channel_to_pool: Sender>, + pub(crate) channel_to_pool: Sender, pub(crate) channel_to_metrics: ConsensusMetricsEventSender, } @@ -376,7 +376,7 @@ mod tests { _packet_sender: Sender, repair_receiver: VerifiedVoterSlotsReceiver, _reward_receiver: Receiver, - pool_receiver: Receiver>, + pool_receiver: Receiver, _metrics_receiver: ConsensusMetricsEventReceiver, generated_cert_types: Arc, } @@ -388,8 +388,8 @@ mod tests { } fn new_with_pool_channel( - channel_to_pool: Sender>, - pool_receiver: Receiver>, + channel_to_pool: Sender, + pool_receiver: Receiver, ) -> Self { let num_validators = 10; let validator_keypairs = (0..num_validators) @@ -528,7 +528,7 @@ mod tests { ctx.verifier .verify_and_send_batches(messages_to_batches(&messages1)) .unwrap(); - assert_eq!(ctx.pool_receiver.try_iter().flatten().count(), 2); + assert_eq!(ctx.pool_receiver.try_iter().count(), 2); assert_eq!(ctx.verifier.stats.vote_stats.pool_sent, 1); assert_eq!(ctx.verifier.stats.cert_stats.pool_sent, 1); let received_verified_votes1 = ctx.repair_receiver.try_recv().unwrap(); @@ -555,7 +555,7 @@ mod tests { .verify_and_send_batches(messages_to_batches(&messages2)) .unwrap(); - assert_eq!(ctx.pool_receiver.try_iter().flatten().count(), 1); + assert_eq!(ctx.pool_receiver.try_iter().count(), 1); assert_eq!(ctx.verifier.stats.vote_stats.pool_sent, 1); assert_eq!(ctx.verifier.stats.cert_stats.pool_sent, 0); let received_verified_votes2 = ctx.repair_receiver.try_recv().unwrap(); @@ -581,7 +581,7 @@ mod tests { ctx.verifier .verify_and_send_batches(messages_to_batches(&messages3)) .unwrap(); - assert_eq!(ctx.pool_receiver.try_iter().flatten().count(), 1); + assert_eq!(ctx.pool_receiver.try_iter().count(), 1); assert_eq!(ctx.verifier.stats.vote_stats.pool_sent, 1); assert_eq!(ctx.verifier.stats.cert_stats.pool_sent, 0); let received_verified_votes3 = ctx.repair_receiver.try_recv().unwrap(); @@ -648,21 +648,20 @@ mod tests { let (channel_to_pool, pool_receiver) = crossbeam_channel::bounded(1); let mut ctx = TestContext::new_with_pool_channel(channel_to_pool, pool_receiver); - let msg1 = ConsensusMessage::Vote(create_signed_vote_message( - &ctx.validator_keypairs, - Vote::new_finalization_vote(5), - 0, - )); - let msg2 = ConsensusMessage::Vote(create_signed_vote_message( + let msg1 = + create_signed_vote_message(&ctx.validator_keypairs, Vote::new_finalization_vote(5), 0); + let msg2 = create_signed_vote_message( &ctx.validator_keypairs, Vote::new_notarization_fallback_vote(Block { slot: 6, block_id: Hash::new_unique(), }), 2, - )); + ); ctx.verifier - .verify_and_send_batches(messages_to_batches(std::slice::from_ref(&msg1))) + .verify_and_send_batches(messages_to_batches(std::slice::from_ref( + &ConsensusMessage::Vote(msg1.clone()), + ))) .unwrap(); // The cap-1 channel is now full. The second send hits Full and falls @@ -681,13 +680,15 @@ mod tests { }); ctx.verifier - .verify_and_send_batches(messages_to_batches(std::slice::from_ref(&msg2))) + .verify_and_send_batches(messages_to_batches(std::slice::from_ref( + &ConsensusMessage::Vote(msg2.clone()), + ))) .unwrap(); let (m1_recv, m2_recv) = drain.join().expect("drain joined"); // Both messages were eventually delivered (no silent drop). - assert_eq!(m1_recv, vec![msg1]); - assert_eq!(m2_recv, vec![msg2]); + assert_eq!(m1_recv, SigVerifiedBatch::Votes(vec![msg1])); + assert_eq!(m2_recv, SigVerifiedBatch::Votes(vec![msg2])); // pool_sent counts every message that made it onto the channel, // whether via try_send or the blocking fallback. assert_eq!(ctx.verifier.stats.vote_stats.pool_sent, 2); @@ -760,11 +761,14 @@ mod tests { ctx.verifier .verify_and_send_batches(packet_batches) .unwrap(); - assert_eq!( - ctx.pool_receiver.try_iter().flatten().count(), - num_votes, - "Did not send all valid packets" - ); + let batches = ctx.pool_receiver.try_iter().collect::>(); + assert_eq!(batches.len(), 1); + match &batches[0] { + SigVerifiedBatch::Votes(votes) => { + assert_eq!(votes.len(), num_votes); + } + rest => panic!("unexpected type: {rest:?}"), + } } #[test] @@ -819,11 +823,14 @@ mod tests { ctx.verifier .verify_and_send_batches(packet_batches) .unwrap(); - assert_eq!( - ctx.pool_receiver.try_iter().flatten().count(), - num_votes, - "Did not send all valid packets" - ); + let batches = ctx.pool_receiver.try_iter().collect::>(); + assert_eq!(batches.len(), 1); + match &batches[0] { + SigVerifiedBatch::Votes(votes) => { + assert_eq!(votes.len(), num_votes); + } + rest => panic!("unexpected type: {rest:?}"), + } assert_eq!( ctx.verifier.stats.vote_stats.distinct_votes_stats.count(), 1 @@ -883,19 +890,28 @@ mod tests { ctx.verifier .verify_and_send_batches(packet_batches) .unwrap(); - let sent_messages: Vec<_> = ctx.pool_receiver.try_iter().flatten().collect(); - assert_eq!( - sent_messages.len(), - num_votes - 1, - "Only valid votes should be sent" - ); - assert!(!sent_messages.iter().any(|msg| { - if let ConsensusMessage::Vote(vm) = msg { - vm.vote == vote2 && vm.rank == invalid_rank - } else { - false + let batches = ctx.pool_receiver.try_iter().collect::>(); + assert_eq!(batches.len(), 1); + match &batches[0] { + SigVerifiedBatch::Votes(votes) => { + assert_eq!(votes.len(), num_votes - 1); } - })); + rest => panic!("unexpected type: {rest:?}"), + } + + let mut found_msg = false; + match &batches[0] { + SigVerifiedBatch::Votes(votes) => { + for vote in votes { + if vote.vote == vote2 && vote.rank == invalid_rank { + found_msg = true; + break; + } + } + } + rest => panic!("unexpected type: {rest:?}"), + } + assert!(!found_msg); } #[test] @@ -937,21 +953,29 @@ mod tests { ctx.verifier .verify_and_send_batches(packet_batches) .unwrap(); - let sent_messages: Vec<_> = ctx.pool_receiver.try_iter().flatten().collect(); - assert_eq!( - sent_messages.len(), - num_votes - 1, - "Only valid votes should be sent" - ); + let batches: Vec<_> = ctx.pool_receiver.try_iter().collect(); + assert_eq!(batches.len(), 1); + match &batches[0] { + SigVerifiedBatch::Votes(votes) => { + assert_eq!(votes.len(), num_votes - 1); + } + rest => panic!("unexpected type: {rest:?}"), + } // Ensure the message with the invalid rank is not in the sent messages. - assert!(!sent_messages.iter().any(|msg| { - if let ConsensusMessage::Vote(vm) = msg { - vm.rank == invalid_rank - } else { - false + let mut found_msg = false; + match &batches[0] { + SigVerifiedBatch::Votes(votes) => { + for vote in votes { + if vote.rank == invalid_rank { + found_msg = true; + break; + } + } } - })); + rest => panic!("unexpected type: {rest:?}"), + } + assert!(!found_msg); } #[test] @@ -976,7 +1000,7 @@ mod tests { .verify_and_send_batches(packet_batches) .unwrap(); assert_eq!( - ctx.pool_receiver.try_iter().flatten().count(), + ctx.pool_receiver.try_iter().count(), 1, "Valid Base2 certificate should be sent" ); @@ -1004,7 +1028,7 @@ mod tests { .verify_and_send_batches(packet_batches) .unwrap(); assert_eq!( - ctx.pool_receiver.try_iter().flatten().count(), + ctx.pool_receiver.try_iter().count(), 1, "Valid Base2 certificate should be sent" ); @@ -1034,7 +1058,7 @@ mod tests { .verify_and_send_batches(packet_batches) .unwrap(); assert_eq!( - ctx.pool_receiver.try_iter().flatten().count(), + ctx.pool_receiver.try_iter().count(), 0, "This certificate should be invalid" ); @@ -1081,7 +1105,7 @@ mod tests { .verify_and_send_batches(packet_batches) .unwrap(); assert_eq!( - ctx.pool_receiver.try_iter().flatten().count(), + ctx.pool_receiver.try_iter().count(), 1, "Valid Base3 certificate should be sent" ); @@ -1127,7 +1151,7 @@ mod tests { .verify_and_send_batches(packet_batches) .unwrap(); assert_eq!( - ctx.pool_receiver.try_iter().flatten().count(), + ctx.pool_receiver.try_iter().count(), 1, "Valid Base3 certificate should be sent" ); @@ -1173,7 +1197,7 @@ mod tests { .verify_and_send_batches(packet_batches) .unwrap(); assert_eq!( - ctx.pool_receiver.try_iter().flatten().count(), + ctx.pool_receiver.try_iter().count(), 0, "This certificate should be invalid" ); @@ -1272,11 +1296,30 @@ mod tests { ctx.verifier .verify_and_send_batches(packet_batches) .unwrap(); - assert_eq!( - ctx.pool_receiver.try_iter().flatten().count(), - num_votes + 1, - "All valid messages in a mixed batch should be sent" - ); + let batches = ctx.pool_receiver.try_iter().collect::>(); + assert_eq!(batches.len(), 2); + + let batch_0_was_votes = match &batches[0] { + SigVerifiedBatch::Votes(votes) => { + assert_eq!(votes.len(), num_votes); + true + } + SigVerifiedBatch::Certificates(certs) => { + assert_eq!(certs.len(), 1); + false + } + }; + + match &batches[1] { + SigVerifiedBatch::Votes(votes) => { + assert!(!batch_0_was_votes); + assert_eq!(votes.len(), num_votes); + } + SigVerifiedBatch::Certificates(certs) => { + assert!(batch_0_was_votes); + assert_eq!(certs.len(), 1); + } + } assert_eq!(ctx.verifier.stats.vote_stats.pool_sent, num_votes as u64); assert_eq!(ctx.verifier.stats.cert_stats.pool_sent, 1); } @@ -1429,7 +1472,7 @@ mod tests { .verify_and_send_batches(packet_batches1) .unwrap(); - assert_eq!(ctx.pool_receiver.try_iter().flatten().count(), 1); + assert_eq!(ctx.pool_receiver.try_iter().count(), 1); assert_eq!(ctx.verifier.stats.num_verified_certs_received, 0); assert_eq!(ctx.verifier.stats.cert_stats.certs_to_sig_verify, 1); @@ -1478,7 +1521,7 @@ mod tests { ctx.verifier .verify_and_send_batches(packet_batches) .unwrap(); - assert_eq!(ctx.pool_receiver.try_iter().flatten().count(), 2); + assert_eq!(ctx.pool_receiver.try_iter().count(), 2); assert!(!ctx.banlist.is_banned(&vote_sender)); assert!(!ctx.banlist.is_banned(&cert_sender)); } @@ -1514,7 +1557,14 @@ mod tests { ctx.verifier .verify_and_send_batches(messages_to_batches_with_remote_pubkeys(&messages)) .unwrap(); - assert_eq!(ctx.pool_receiver.try_iter().flatten().count(), 3); + let batches = ctx.pool_receiver.try_iter().collect::>(); + assert_eq!(batches.len(), 1); + match &batches[0] { + SigVerifiedBatch::Votes(votes) => { + assert_eq!(votes.len(), 3); + } + rest => panic!("unexpected type: {rest:?}"), + } for (i, (_, sender)) in messages.iter().enumerate() { if invalid_indexes.contains(&i) { @@ -1558,7 +1608,14 @@ mod tests { ctx.verifier .verify_and_send_batches(messages_to_batches_with_remote_pubkeys(&messages)) .unwrap(); - assert_eq!(ctx.pool_receiver.try_iter().flatten().count(), 3); + let batches = ctx.pool_receiver.try_iter().collect::>(); + assert_eq!(batches.len(), 1); + match &batches[0] { + SigVerifiedBatch::Certificates(certs) => { + assert_eq!(certs.len(), 3); + } + rest => panic!("unexpected type: {rest:?}"), + } for (i, (_, sender)) in messages.iter().enumerate() { if invalid_indexes.contains(&i) { diff --git a/core/src/bls_sigverify/bls_vote_sigverify.rs b/core/src/bls_sigverify/bls_vote_sigverify.rs index ae15d11a505..6974e7867cf 100644 --- a/core/src/bls_sigverify/bls_vote_sigverify.rs +++ b/core/src/bls_sigverify/bls_vote_sigverify.rs @@ -14,7 +14,7 @@ use { }, agave_votor::consensus_metrics::ConsensusMetricsEvent, agave_votor_messages::{ - consensus_message::{ConsensusMessage, VoteMessage}, + consensus_message::{SigVerifiedBatch, VoteMessage}, vote::Vote, }, rayon::{ @@ -129,7 +129,7 @@ fn process_verified_votes( cluster_info: &ClusterInfo, leader_schedule: &LeaderScheduleCache, ) -> ( - Vec, + SigVerifiedBatch, HashMap>, AddVoteMessage, Vec, @@ -154,7 +154,7 @@ fn process_verified_votes( id: payload.pubkey, vote: payload.vote_message.vote, }); - votes_for_pool.push(ConsensusMessage::Vote(payload.vote_message)); + votes_for_pool.push(payload.vote_message); } let msgs_for_repair = msgs_for_repair .into_iter() @@ -164,6 +164,7 @@ fn process_verified_votes( (pubkey, slots) }) .collect(); + let votes_for_pool = SigVerifiedBatch::Votes(votes_for_pool); ( votes_for_pool, msgs_for_repair, diff --git a/core/src/bls_sigverify/utils.rs b/core/src/bls_sigverify/utils.rs index 0e51cb117f7..ef88b89fc4d 100644 --- a/core/src/bls_sigverify/utils.rs +++ b/core/src/bls_sigverify/utils.rs @@ -6,7 +6,7 @@ use { cluster_info_vote_listener::VerifiedVoterSlotsSender, }, agave_votor::consensus_metrics::{ConsensusMetricsEvent, ConsensusMetricsEventSender}, - agave_votor_messages::consensus_message::ConsensusMessage, + agave_votor_messages::consensus_message::SigVerifiedBatch, crossbeam_channel::{Sender, TrySendError}, solana_clock::Slot, solana_pubkey::Pubkey, @@ -52,18 +52,18 @@ pub(super) fn send_votes_to_rewards( } } -/// Sends the `votes` to the consensus pool. If the channel is full, then does a +/// Sends the `batch` to the consensus pool. If the channel is full, then does a /// blocking send. pub(super) fn send_votes_to_pool( - votes: Vec, - channel: &Sender>, + batch: SigVerifiedBatch, + channel: &Sender, stats: &mut SigVerifyVoteStats, ) -> Result<(), SigVerifyVoteError> { - if votes.is_empty() { + if batch.is_empty() { return Ok(()); } - let len = votes.len(); - match channel.try_send(votes) { + let len = batch.len(); + match channel.try_send(batch) { Ok(()) => { stats.pool_sent += len as u64; stats.pool_outstanding_msgs = channel.len() as u64; @@ -108,18 +108,18 @@ pub(super) fn send_votes_to_repair( Ok(()) } -/// Sends the `messages` to the consensus pool. If the channel is bounded and full, then does a +/// Sends the `batch` to the consensus pool. If the channel is bounded and full, then does a /// blocking send. pub(super) fn send_certs_to_pool( - messages: Vec, - channel_to_pool: &Sender>, + batch: SigVerifiedBatch, + channel_to_pool: &Sender, stats: &mut SigVerifyCertStats, ) -> Result<(), SigVerifyCertError> { - if messages.is_empty() { + if batch.is_empty() { return Ok(()); } - let len = messages.len(); - match channel_to_pool.try_send(messages) { + let len = batch.len(); + match channel_to_pool.try_send(batch) { Ok(()) => { stats.pool_sent += len as u64; stats.pool_outstanding_msgs = channel_to_pool.len() as u64; diff --git a/core/src/replay_stage.rs b/core/src/replay_stage.rs index 794c800bf77..ae8fbcae67c 100644 --- a/core/src/replay_stage.rs +++ b/core/src/replay_stage.rs @@ -42,7 +42,7 @@ use { voting_utils::{self, GenerateVoteTxResult}, }, agave_votor_messages::{ - consensus_message::{Block, ConsensusMessage}, + consensus_message::{Block, SigVerifiedBatch}, migration::{GENESIS_VOTE_REFRESH, MigrationStatus}, vote::Vote, }, @@ -445,7 +445,7 @@ pub struct ReplaySenders { pub block_metadata_notifier: Option, pub dumped_slots_sender: Sender>, pub votor_event_sender: VotorEventSender, - pub own_vote_sender: Sender>, + pub own_vote_sender: Sender, pub optimistic_parent_sender: Sender, pub lockouts_sender: Sender, } @@ -1730,7 +1730,7 @@ impl ReplayStage { vote_account: Pubkey, identity_keypair: &Arc, authorized_voter_keypairs: &Arc>>>, - own_vote_sender: &Sender>, + own_vote_sender: &Sender, bls_sender: &Sender, ) -> bool { let Some(block) = migration_status.eligible_genesis_block() else { @@ -1755,7 +1755,7 @@ impl ReplayStage { identity_keypair.pubkey() ); // If sending fails that means the channel is disconnected and we are shutting down - let _ = own_vote_sender.send(vec![ConsensusMessage::Vote(vote_msg.clone())]); + let _ = own_vote_sender.send(SigVerifiedBatch::Votes(vec![vote_msg.clone()])); let _ = bls_sender.send(BLSOp::PushVote { vote: Arc::new(vote_msg), saved_vote_history: @@ -2969,7 +2969,7 @@ impl ReplayStage { bank: &BankWithScheduler, replay_stats: &RwLock, replay_progress: &RwLock, - finalization_cert_sender: &Sender>, + finalization_cert_sender: &Sender, ) -> result::Result { let mut w_replay_stats = replay_stats.write().unwrap(); let mut w_replay_progress = replay_progress.write().unwrap(); @@ -3608,7 +3608,7 @@ impl ReplayStage { process_active_banks_context: &ProcessActiveBanksContext, bank_replay_result_tracker: BankReplayResultTracker, my_pubkey: &Pubkey, - finalization_cert_sender: &Sender>, + finalization_cert_sender: &Sender, ) -> (ReplaySlotFromBlockstore, Option) { let BankReplayResultTracker { mut replay_result, @@ -3682,7 +3682,7 @@ impl ReplayStage { bank_replay_result_trackers: Vec, replay_timing: &mut ReplayLoopTiming, my_pubkey: &Pubkey, - finalization_cert_sender: &Sender>, + finalization_cert_sender: &Sender, ) -> Vec { match &process_active_banks_context.replay_mode { // Skip the overhead of the threadpool if there is only one bank to play @@ -4168,7 +4168,7 @@ impl ReplayStage { my_pubkey: &Pubkey, vote_account: &Pubkey, replay_timing: &mut ReplayLoopTiming, - finalization_cert_sender: &Sender>, + finalization_cert_sender: &Sender, ) -> Vec /* completed slots */ { let bank_replay_result_trackers = Self::prepare_active_banks_for_replay( process_active_banks_context, diff --git a/ledger/src/blockstore_processor.rs b/ledger/src/blockstore_processor.rs index 85cfd6db909..5963e927db5 100644 --- a/ledger/src/blockstore_processor.rs +++ b/ledger/src/blockstore_processor.rs @@ -9,7 +9,7 @@ use { use_snapshot_archives_at_startup::UseSnapshotArchivesAtStartup, }, ExecuteTimingType::{NumExecuteBatches, TotalBatchesLen}, - agave_votor_messages::{consensus_message::ConsensusMessage, migration::MigrationStatus}, + agave_votor_messages::{consensus_message::SigVerifiedBatch, migration::MigrationStatus}, chrono_humanize::{Accuracy, HumanTime, Tense}, crossbeam_channel::{Receiver, Sender}, itertools::Itertools, @@ -1675,7 +1675,7 @@ pub fn confirm_slot( transaction_status_sender: Option<&TransactionStatusSender>, entry_notification_sender: Option<&EntryNotifierSender>, replay_vote_sender: Option<&ReplayVoteSender>, - finalization_cert_sender: Option<&Sender>>, + finalization_cert_sender: Option<&Sender>, allow_dead_slots: bool, log_messages_bytes_limit: Option, prioritization_fee_cache: Option<&PrioritizationFeeCache>, @@ -1778,7 +1778,7 @@ fn confirm_slot_with_components( transaction_status_sender: Option<&TransactionStatusSender>, entry_notification_sender: Option<&EntryNotifierSender>, replay_vote_sender: Option<&ReplayVoteSender>, - finalization_cert_sender: Option<&Sender>>, + finalization_cert_sender: Option<&Sender>, allow_dead_slots: bool, log_messages_bytes_limit: Option, prioritization_fee_cache: Option<&PrioritizationFeeCache>, diff --git a/runtime/src/block_component_processor.rs b/runtime/src/block_component_processor.rs index 5d7234e45ab..1245fdb77bc 100644 --- a/runtime/src/block_component_processor.rs +++ b/runtime/src/block_component_processor.rs @@ -12,7 +12,7 @@ use { }, agave_votor_messages::{ certificate::Certificate, - consensus_message::ConsensusMessage, + consensus_message::SigVerifiedBatch, fraction::Fraction, migration::{GENESIS_VOTE_THRESHOLD, MigrationStatus}, }, @@ -181,7 +181,7 @@ impl BlockComponentProcessor { parent_bank: Arc, marker: VersionedBlockMarker, allow_initial_update_parent: bool, - finalization_cert_sender: Option<&Sender>>, + finalization_cert_sender: Option<&Sender>, migration_status: &MigrationStatus, ) -> Result<(), BlockComponentProcessorError> { let slot = bank.slot(); @@ -314,7 +314,7 @@ impl BlockComponentProcessor { bank: Arc, parent_bank: Arc, footer: VersionedBlockFooter, - finalization_cert_sender: Option<&Sender>>, + finalization_cert_sender: Option<&Sender>, ) -> Result<(), BlockComponentProcessorError> { if !self.has_header && self.update_parent.is_none() { return Err(BlockComponentProcessorError::MissingParentMarker); @@ -374,15 +374,17 @@ impl BlockComponentProcessor { if let Some((finalize_cert, notarize_cert)) = pool_input { if let Some(sender) = finalization_cert_sender { if let Some(notarize_cert) = notarize_cert { + let certs = SigVerifiedBatch::Certificates(vec![notarize_cert]); // TODO blocking send. let _ = sender - .send(vec![ConsensusMessage::from(notarize_cert)]) - .inspect_err(|_| info!("ConsensusMessage sender disconnected")); + .send(certs) + .inspect_err(|_| info!("SigVerifiedBatch sender disconnected")); } + let certs = SigVerifiedBatch::Certificates(vec![finalize_cert]); // TODO blocking send. let _ = sender - .send(vec![ConsensusMessage::from(finalize_cert)]) - .inspect_err(|_| info!("ConsensusMessage sender disconnected")); + .send(certs) + .inspect_err(|_| info!("SigVerifiedBatch sender disconnected")); } } diff --git a/votor-messages/src/certificate.rs b/votor-messages/src/certificate.rs index 0648a676107..1d072694f43 100644 --- a/votor-messages/src/certificate.rs +++ b/votor-messages/src/certificate.rs @@ -26,7 +26,7 @@ pod_wrapper! { derive(AbiExample), frozen_abi(digest = "5WqvPnvSnVXQFrAs9o29szFGDiCk45Pgk8K1evTZSrwo") )] -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, SchemaWrite, SchemaRead)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SchemaWrite, SchemaRead)] pub struct Certificate { /// The certificate type. pub cert_type: CertificateType, diff --git a/votor-messages/src/consensus_message.rs b/votor-messages/src/consensus_message.rs index c4be5913c45..0146b6c2060 100644 --- a/votor-messages/src/consensus_message.rs +++ b/votor-messages/src/consensus_message.rs @@ -54,7 +54,7 @@ pub struct Block { derive(AbiExample), frozen_abi(digest = "CTiXEk2aQbpf6TS6PNKcaTsGkLruDvAYsTLFhHKW2vsm") )] -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, SchemaWrite, SchemaRead)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SchemaWrite, SchemaRead)] pub struct VoteMessage { /// The type of the vote. pub vote: Vote, @@ -109,3 +109,31 @@ impl From for ConsensusMessage { Self::Certificate(cert) } } + +/// A batch of vote or cert being sent within the node. They were either sig verified before being +/// sent or they were generated by the node itself so do not need to be sig verified. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SigVerifiedBatch { + /// Batch of votes. + Votes(Vec), + /// Batch of certs. + Certificates(Vec), +} + +impl SigVerifiedBatch { + /// Returns the length of the batch + pub fn len(&self) -> usize { + match self { + Self::Votes(votes) => votes.len(), + Self::Certificates(certs) => certs.len(), + } + } + + /// Returns true if the batch is empty. + pub fn is_empty(&self) -> bool { + match self { + Self::Votes(votes) => votes.is_empty(), + Self::Certificates(certs) => certs.is_empty(), + } + } +} diff --git a/votor/src/consensus_pool_service.rs b/votor/src/consensus_pool_service.rs index 455eb6c1c86..9e8c6e9bb81 100644 --- a/votor/src/consensus_pool_service.rs +++ b/votor/src/consensus_pool_service.rs @@ -16,7 +16,7 @@ use { }, agave_votor_messages::{ certificate::Certificate, - consensus_message::{Block, ConsensusMessage}, + consensus_message::{Block, ConsensusMessage, SigVerifiedBatch}, migration::MigrationStatus, }, crossbeam_channel::{Receiver, Sender, TrySendError, select}, @@ -54,7 +54,7 @@ pub(crate) struct ConsensusPoolContext { pub(crate) leader_schedule_cache: Arc, pub(crate) vote_history_highest_parent_ready: Option<(Slot, Block)>, - pub(crate) consensus_message_receiver: Receiver>, + pub(crate) consensus_message_receiver: Receiver, pub(crate) bls_sender: Sender, pub(crate) event_sender: VotorEventSender, @@ -170,6 +170,41 @@ impl ConsensusPoolService { Ok(()) } + fn process_batch( + ctx: &mut ConsensusPoolContext, + msgs: impl Iterator, + consensus_pool: &mut ConsensusPool, + events: &mut Vec, + standstill_timer: &mut Instant, + stats: &mut ConsensusPoolServiceStats, + ) -> Result<(), AddVoteError> { + for msg in msgs { + match Self::process_consensus_message( + ctx, + msg, + consensus_pool, + events, + standstill_timer, + stats, + ) { + Ok(()) => {} + Err(AddVoteError::ChannelDisconnected(channel_name)) => { + return Err(AddVoteError::ChannelDisconnected(channel_name)); + } + Err(e) => { + // This is a non critical error, a duplicate vote for example + trace!( + "{}: unable to push vote into pool {}", + ctx.cluster_info.id(), + e + ); + stats.add_message_failed += 1; + } + } + } + Ok(()) + } + fn process_consensus_message( ctx: &mut ConsensusPoolContext, message: ConsensusMessage, @@ -375,37 +410,42 @@ impl ConsensusPoolService { Duration::from_millis(20) }; - let messages: Vec = select! { + let batches: Vec = select! { recv(ctx.consensus_message_receiver) -> msg => { let Ok(first) = msg else { return Self::handle_channel_disconnected(&mut ctx, "consensus_message_receiver"); }; - std::iter::once(first).chain(ctx.consensus_message_receiver.try_iter()).flatten().collect() + std::iter::once(first).chain(ctx.consensus_message_receiver.try_iter()).collect() }, default(wait_timeout) => continue }; - for message in messages { - match Self::process_consensus_message( - &mut ctx, - message, - &mut consensus_pool, - &mut events, - &mut standstill_timer, - &mut stats, - ) { + for batch in batches { + let ret = match batch { + SigVerifiedBatch::Votes(votes) => Self::process_batch( + &mut ctx, + votes.into_iter().map(ConsensusMessage::Vote), + &mut consensus_pool, + &mut events, + &mut standstill_timer, + &mut stats, + ), + SigVerifiedBatch::Certificates(certs) => Self::process_batch( + &mut ctx, + certs.into_iter().map(ConsensusMessage::Certificate), + &mut consensus_pool, + &mut events, + &mut standstill_timer, + &mut stats, + ), + }; + match ret { Ok(()) => {} Err(AddVoteError::ChannelDisconnected(channel_name)) => { return Self::handle_channel_disconnected(&mut ctx, channel_name.as_str()); } - Err(e) => { - // This is a non critical error, a duplicate vote for example - trace!( - "{}: unable to push vote into pool {}", - ctx.cluster_info.id(), - e - ); - stats.add_message_failed += 1; + Err(_) => { + // error was handled in process_batch. } } } diff --git a/votor/src/event_handler.rs b/votor/src/event_handler.rs index 687ef8432d3..67bb521405d 100644 --- a/votor/src/event_handler.rs +++ b/votor/src/event_handler.rs @@ -934,7 +934,7 @@ mod tests { voting_service::BLSOp, }, agave_votor_messages::{ - consensus_message::{BLS_KEYPAIR_DERIVE_SEED, ConsensusMessage, VoteMessage}, + consensus_message::{BLS_KEYPAIR_DERIVE_SEED, SigVerifiedBatch, VoteMessage}, vote::Vote, }, crossbeam_channel::{Receiver, Sender, TryRecvError, unbounded}, @@ -970,7 +970,7 @@ mod tests { struct EventHandlerTestContext { bls_receiver: Receiver, commitment_receiver: Receiver, - own_vote_receiver: Receiver>, + own_vote_receiver: Receiver, bank_forks: Arc>, my_bls_keypair: BLSKeypair, timer_manager: Arc>, @@ -1383,7 +1383,7 @@ mod tests { ); // Also check own_vote_receiver let own_vote = self.own_vote_receiver.try_recv().unwrap(); - assert_eq!(own_vote, vec![ConsensusMessage::Vote(expected_message)]); + assert_eq!(own_vote, SigVerifiedBatch::Votes(vec![expected_message])); } fn check_for_commitment(&mut self, expected_type: CommitmentType, expected_slot: Slot) { diff --git a/votor/src/voting_utils.rs b/votor/src/voting_utils.rs index f336d79008d..20869c995b2 100644 --- a/votor/src/voting_utils.rs +++ b/votor/src/voting_utils.rs @@ -7,7 +7,7 @@ use { voting_service::BLSOp, }, agave_votor_messages::{ - consensus_message::{BLS_KEYPAIR_DERIVE_SEED, ConsensusMessage, VoteMessage}, + consensus_message::{BLS_KEYPAIR_DERIVE_SEED, SigVerifiedBatch, VoteMessage}, vote::Vote, }, crossbeam_channel::{SendError, Sender}, @@ -117,7 +117,7 @@ pub struct VotingContext { pub authorized_voter_keypairs: Arc>>>, // The BLS keypair should always change with authorized_voter_keypairs. pub derived_bls_keypairs: HashMap>, - pub own_vote_sender: Sender>, + pub own_vote_sender: Sender, pub bls_sender: Sender, pub commitment_sender: Sender, pub wait_to_vote_slot: Option, @@ -264,7 +264,7 @@ fn insert_vote_and_create_bls_message( }; context .own_vote_sender - .send(vec![ConsensusMessage::Vote(vote_msg.clone())]) + .send(SigVerifiedBatch::Votes(vec![vote_msg.clone()])) .map_err(|_| SendError(()))?; // TODO: for refresh votes use a different BLSOp so we don't have to rewrite the same vote history to file @@ -319,21 +319,18 @@ mod tests { std::sync::{Arc, RwLock}, }; - fn generate_expected_consensus_message( - vote: Vote, - my_bls_keypair: &BLSKeypair, - ) -> ConsensusMessage { + fn generate_expected_consensus_message(vote: Vote, my_bls_keypair: &BLSKeypair) -> VoteMessage { let vote_serialized = wincode::serialize(&vote).unwrap(); let signature = my_bls_keypair.sign(&vote_serialized); - ConsensusMessage::Vote(VoteMessage { + VoteMessage { vote, signature: signature.into(), rank: 0, - }) + } } fn setup_voting_context_and_bank_forks( - own_vote_sender: Sender>, + own_vote_sender: Sender, validator_keypairs: &[ValidatorVoteKeypairs], my_index: usize, ) -> VotingContext { @@ -346,7 +343,7 @@ mod tests { } fn setup_voting_context_and_bank_forks_with_forks( - own_vote_sender: Sender>, + own_vote_sender: Sender, validator_keypairs: &[ValidatorVoteKeypairs], my_index: usize, ) -> (VotingContext, Arc>) { @@ -416,7 +413,7 @@ mod tests { saved_vote_history, } = result { - let msg = ConsensusMessage::Vote(Arc::unwrap_or_clone(vote)); + let msg = Arc::unwrap_or_clone(vote); assert_eq!(msg, expected_message); assert_eq!( saved_vote_history, @@ -434,7 +431,10 @@ mod tests { // Check that own vote sender receives the vote let received_message = own_vote_receiver.recv().unwrap(); - assert_eq!(received_message, vec![expected_message]); + assert_eq!( + received_message, + SigVerifiedBatch::Votes(vec![expected_message]) + ); } #[test] diff --git a/votor/src/votor.rs b/votor/src/votor.rs index 2943d804919..7786d8d0d1a 100644 --- a/votor/src/votor.rs +++ b/votor/src/votor.rs @@ -63,7 +63,7 @@ use { voting_service::BLSOp, voting_utils::VotingContext, }, - agave_votor_messages::consensus_message::{Block, ConsensusMessage}, + agave_votor_messages::consensus_message::{Block, SigVerifiedBatch}, crossbeam_channel::{Receiver, Sender}, parking_lot::RwLock as PlRwLock, solana_clock::Slot, @@ -111,13 +111,13 @@ pub struct VotorConfig { pub leader_window_info_sender: Sender, pub highest_parent_ready: Arc>, pub event_sender: VotorEventSender, - pub own_vote_sender: Sender>, + pub own_vote_sender: Sender, pub repair_event_sender: RepairEventSender, pub latest_switch_request: LatestSwitchRequest, // Receivers pub event_receiver: VotorEventReceiver, - pub consensus_message_receiver: Receiver>, + pub consensus_message_receiver: Receiver, pub consensus_metrics_receiver: ConsensusMetricsEventReceiver, } From ded37f25cb840233b6c2db7d4b6f2c38a4b17699 Mon Sep 17 00:00:00 2001 From: Edvard Fagerholm Date: Fri, 5 Jun 2026 23:28:33 +0300 Subject: [PATCH 222/576] perf(tpu): bound the TPU forwards packet channel (#12875) Replace the unbounded forwarded-packet channel with a bounded one (TPU_FORWARD_CHANNEL_SIZE), matching the main TPU channel. The streamer uses try_send, so over-full drops are tracked via streamer metrics instead of growing the queue without limit. --- core/src/tpu.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/src/tpu.rs b/core/src/tpu.rs index 2fc899c9bc6..9400d1009a0 100644 --- a/core/src/tpu.rs +++ b/core/src/tpu.rs @@ -94,6 +94,11 @@ const TPU_CHANNEL_SIZE: usize = 50_000; /// Chosen based on nominal voting load for a cluster with ~2000 validators + some margin. pub(crate) const TPU_VOTE_CHANNEL_SIZE: usize = 4_000; +/// Size of the channel between the TPU forwards streamer and the fetch stage. +/// Mirrors `TPU_CHANNEL_SIZE`; the streamer uses `try_send`, so an over-full +/// channel drops packets (tracked via streamer metrics) rather than blocking. +const TPU_FORWARD_CHANNEL_SIZE: usize = 50_000; + pub struct Tpu { fetch_stage: FetchStage, cluster_info_vote_listener: ClusterInfoVoteListener, @@ -173,7 +178,8 @@ impl Tpu { let (vote_packet_sender, vote_packet_receiver) = bounded(TPU_VOTE_CHANNEL_SIZE); let evicting_vote_sender = EvictingSender::new(vote_packet_sender.clone(), vote_packet_receiver.clone()); - let (forwarded_packet_sender, forwarded_packet_receiver) = unbounded(); + let (forwarded_packet_sender, forwarded_packet_receiver) = + bounded(TPU_FORWARD_CHANNEL_SIZE); let fetch_stage = FetchStage::new_with_sender( tpu_vote_sockets, exit.clone(), From 62423c8118012cef832c2483473ee81b6efa5db6 Mon Sep 17 00:00:00 2001 From: steviez Date: Fri, 5 Jun 2026 15:39:32 -0500 Subject: [PATCH 223/576] blockstore: Remove redundant test function (#12988) The test function does exact same thing as "production" function --- ledger/src/blockstore/blockstore_purge.rs | 32 +++++++++-------------- ledger/src/blockstore/tests.rs | 14 +++++----- 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/ledger/src/blockstore/blockstore_purge.rs b/ledger/src/blockstore/blockstore_purge.rs index cfe49c1052a..253f1941d82 100644 --- a/ledger/src/blockstore/blockstore_purge.rs +++ b/ledger/src/blockstore/blockstore_purge.rs @@ -121,16 +121,6 @@ impl Blockstore { } } - #[cfg(test)] - pub(crate) fn run_purge( - &self, - from_slot: Slot, - to_slot: Slot, - purge_type: PurgeType, - ) -> Result<()> { - self.run_purge_with_stats(from_slot, to_slot, purge_type, &mut PurgeStats::default()) - } - /// Purges all columns relating to `slot`. /// /// Additionally, we cleanup the parent of `slot` by clearing `slot` from @@ -530,7 +520,7 @@ pub mod tests { } // Purging range outside of TransactionStatus max slots should not affect TransactionStatus data - blockstore.run_purge(10, 20, PurgeType::Exact).unwrap(); + blockstore.purge_slots(10, 20, PurgeType::Exact).unwrap(); let status_entries: Vec<_> = blockstore .transaction_status_cf @@ -541,7 +531,9 @@ pub mod tests { } fn clear_and_repopulate_transaction_statuses_for_test(blockstore: &Blockstore, max_slot: u64) { - blockstore.run_purge(0, max_slot, PurgeType::Exact).unwrap(); + blockstore + .purge_slots(0, max_slot, PurgeType::Exact) + .unwrap(); let mut iter = blockstore .transaction_status_cf .iter(IteratorMode::Start) @@ -632,12 +624,14 @@ pub mod tests { // Partially purge and ensure special columns are non-empty blockstore - .run_purge(0, max_slot - 5, PurgeType::Exact) + .purge_slots(0, max_slot - 5, PurgeType::Exact) .unwrap(); assert!(!blockstore.special_columns_empty().unwrap()); // Purge the rest and ensure the special columns are empty once again - blockstore.run_purge(0, max_slot, PurgeType::Exact).unwrap(); + blockstore + .purge_slots(0, max_slot, PurgeType::Exact) + .unwrap(); assert!(blockstore.special_columns_empty().unwrap()); } @@ -651,7 +645,7 @@ pub mod tests { // Test purge outside bounds clear_and_repopulate_transaction_statuses_for_test(&blockstore, max_slot); - blockstore.run_purge(10, 12, PurgeType::Exact).unwrap(); + blockstore.purge_slots(10, 12, PurgeType::Exact).unwrap(); let mut status_entry_iterator = blockstore .transaction_status_cf @@ -666,7 +660,7 @@ pub mod tests { // Test purge inside written range clear_and_repopulate_transaction_statuses_for_test(&blockstore, max_slot); - blockstore.run_purge(2, 4, PurgeType::Exact).unwrap(); + blockstore.purge_slots(2, 4, PurgeType::Exact).unwrap(); let mut status_entry_iterator = blockstore .transaction_status_cf @@ -683,7 +677,7 @@ pub mod tests { // Purge up to but not including max_slot clear_and_repopulate_transaction_statuses_for_test(&blockstore, max_slot); blockstore - .run_purge(0, max_slot - 1, PurgeType::Exact) + .purge_slots(0, max_slot - 1, PurgeType::Exact) .unwrap(); let mut status_entry_iterator = blockstore @@ -697,7 +691,7 @@ pub mod tests { // Test purge all clear_and_repopulate_transaction_statuses_for_test(&blockstore, max_slot); - blockstore.run_purge(0, 22, PurgeType::Exact).unwrap(); + blockstore.purge_slots(0, 22, PurgeType::Exact).unwrap(); let mut status_entry_iterator = blockstore .transaction_status_cf @@ -708,7 +702,7 @@ pub mod tests { fn purge_exact(blockstore: &Blockstore, oldest_slot: Slot) { blockstore - .run_purge(0, oldest_slot - 1, PurgeType::Exact) + .purge_slots(0, oldest_slot - 1, PurgeType::Exact) .unwrap(); } diff --git a/ledger/src/blockstore/tests.rs b/ledger/src/blockstore/tests.rs index 0504f9228e0..d9c13da5c69 100644 --- a/ledger/src/blockstore/tests.rs +++ b/ledger/src/blockstore/tests.rs @@ -498,7 +498,7 @@ fn test_shred_cleanup_check() { let max_purge_slot = 1; blockstore - .run_purge(0, max_purge_slot, PurgeType::Exact) + .purge_slots(0, max_purge_slot, PurgeType::Exact) .unwrap(); *blockstore.lowest_cleanup_slot.write().unwrap() = max_purge_slot; @@ -3748,7 +3748,7 @@ fn test_get_rooted_transaction() { } blockstore - .run_purge(0, slot, PurgeType::CompactionFilter) + .purge_slots(0, slot, PurgeType::CompactionFilter) .unwrap(); *blockstore.lowest_cleanup_slot.write().unwrap() = slot; for VersionedTransactionWithStatusMeta { transaction, .. } in expected_transactions { @@ -3865,7 +3865,7 @@ fn test_get_complete_transaction() { } blockstore - .run_purge(0, slot, PurgeType::CompactionFilter) + .purge_slots(0, slot, PurgeType::CompactionFilter) .unwrap(); *blockstore.lowest_cleanup_slot.write().unwrap() = slot; for VersionedTransactionWithStatusMeta { transaction, .. } in expected_transactions { @@ -4551,7 +4551,7 @@ fn test_lowest_slot() { blockstore.insert_shreds(shreds, None, false).unwrap(); } assert_eq!(blockstore.lowest_slot(), 1); - blockstore.run_purge(0, 5, PurgeType::Exact).unwrap(); + blockstore.purge_slots(0, 5, PurgeType::Exact).unwrap(); assert_eq!(blockstore.lowest_slot(), 6); } @@ -4567,10 +4567,10 @@ fn test_highest_slot() { blockstore.insert_shreds(shreds, None, false).unwrap(); assert_eq!(blockstore.highest_slot().unwrap(), Some(slot)); } - blockstore.run_purge(5, 10, PurgeType::Exact).unwrap(); + blockstore.purge_slots(5, 10, PurgeType::Exact).unwrap(); assert_eq!(blockstore.highest_slot().unwrap(), Some(4)); - blockstore.run_purge(0, 4, PurgeType::Exact).unwrap(); + blockstore.purge_slots(0, 4, PurgeType::Exact).unwrap(); assert_eq!(blockstore.highest_slot().unwrap(), None); } @@ -5635,7 +5635,7 @@ fn test_duplicate_last_index_mark_dead() { // Cleanup the slot blockstore - .run_purge(slot, slot, PurgeType::Exact) + .purge_slots(slot, slot, PurgeType::Exact) .expect("Purge database operations failed"); assert!(blockstore.meta(slot).unwrap().is_none()); From 0be04228e8fadee30c194563baa283be96a6050b Mon Sep 17 00:00:00 2001 From: amilz <85324096+amilz@users.noreply.github.com> Date: Fri, 5 Jun 2026 14:57:15 -0700 Subject: [PATCH 224/576] docs: update off-chain message signing proposal to v1 format (#12965) Make v1 the primary spec and move v0 to a legacy appendix. v1 drops the application domain, message format, and message length prefix; content is now a trailing variable-length field. Adds the signer ordering/uniqueness constraint for deterministic signatures. --- .../proposals/off-chain-message-signing.md | 254 ++++++++++++------ 1 file changed, 174 insertions(+), 80 deletions(-) diff --git a/docs/src/proposals/off-chain-message-signing.md b/docs/src/proposals/off-chain-message-signing.md index 7d5d25b10f7..9ef92ac6f76 100644 --- a/docs/src/proposals/off-chain-message-signing.md +++ b/docs/src/proposals/off-chain-message-signing.md @@ -1,105 +1,134 @@ # Off-chain message signing -## Motivation There is ecosystem demand for a method of signing non-transaction messages with a Solana wallet. Typically this is some kind of "proof of wallet ownership" for entry into a whitelisted system. +This document specifies **version 1** of the off-chain message format, the +current recommended format. The original **version 0** format remains supported +and is documented in [Appendix: version 0 (legacy)](#appendix-version-0-legacy). + Some inspiration can be gleaned from relevant portions of Ethereum's -[EIP-712](https://eips.ethereum.org/EIPS/eip-712) +[EIP-712](https://eips.ethereum.org/EIPS/eip-712). -## Considerations +## Motivation * Security - * Off-chain message signatures should not be valid transaction message signatures + * Off-chain message signatures must not be valid transaction message signatures * Future-proofing * Versioning * Co-exist with versioned transaction messages and extended length transactions * Compatibility * Hardware wallet signing - * Localization + * Determinism + * The same logical message must always produce the same bytes, and therefore + the same signature, regardless of the order in which signers are declared + +### What changed in version 1 + +Version 1 is a deliberate simplification of version 0. It removes three fields +that added complexity or ambiguity without proportional benefit: + +* **Application domain** (32 bytes) — provided limited utility and created a + spoofing surface. Removed. +* **Message format** (1 byte) — whether a given message is signable or + clear-signable by a hardware wallet depends on that wallet's implementation, + not on a byte in the message. Removed; clients may impose their own character + set or length constraints out of band. +* **Message length** (2-byte prefix) — the 16-bit length prefix capped messages + at 65,535 bytes and created a class of length-mismatch validation failures. + Removed; the message content is now a trailing, variable-length field that + consumes the remainder of the buffer. + +Version 1 also adds an ordering and uniqueness constraint on signer addresses so +that identical messages always serialize identically (see +[Required signers](#required-signers)). -## Message Preamble +## Message format -| Field | Start offset | Length (bytes) | Description -| :---- | :----------: | :------------: | :---------- -| Signing Domain | 0x00 | 16 | \[[1](#signing-domain-specifier)\] -| Header version | 0x10 | 1 | \[[2](#header-version)\] -| Application domain | 0x11 | 32 | \[[3](#application-domain)\] -| Message format | 0x31 | 1 | \[[4](#message-format)\] -| Signer count | 0x32 | 1 | Number of signers committing to the message. An 8-bit, unsigned integer. **MUST NOT** be zero. -| Signers | 0x33 | `SIGNER_COUNT` * 32 | `SIGNER_COUNT` ed25519 public keys of signers -| Message length | 0x33 + `SIGNER_CNT` * 32 | 2 | Length of message in bytes. A 16-bit, unsigned, little-endian integer. **MUST NOT** be zero. +A version 1 off-chain message is the byte string that is ed25519-signed. It is +composed of a preamble followed by the message content. -### Signing Domain Specifier +| Field | Start offset | Length (bytes) | Description | +| :------------- | :----------------------: | :-------------------: | :--------------------------------------------------------------------------- | +| Signing domain | 0x00 | 16 | \[[1](#signing-domain)\] | +| Version | 0x10 | 1 | An 8-bit unsigned integer. **MUST** equal `1`. \[[2](#version)\] | +| Signer count | 0x11 | 1 | Number of required signers. An 8-bit unsigned integer. **MUST NOT** be zero. | +| Signers | 0x12 | `SIGNER_COUNT` * 32 | `SIGNER_COUNT` ed25519 public keys. \[[3](#required-signers)\] | +| Content | 0x12 + `SIGNER_COUNT`*32 | remainder of buffer | The message content. UTF-8. **MUST NOT** be empty. \[[4](#content)\] | + +### Signing domain + +The signing domain is a fixed 16-byte prefix that gives common structure to all +off-chain message signatures: -The signing domain specifier is a prefix byte string used to give similar structure -to all off-chain message signatures. We assign its value to be: ``` b"\xffsolana offchain" ``` -The first byte, `\xff`, was chosen for the following reasons -1. It corresponds to a value that is illegal as the first byte in a transaction -`MessageHeader`. -1. It avoids unintentional misuse in languages with C-like, null-terminated strings. -The remaining bytes, `b"solana offchain"`, were chosen to be descriptive and -reasonably long, but are otherwise arbitrary. +In hexadecimal: `ff 73 6f 6c 61 6e 61 20 6f 66 66 63 68 61 69 6e`. -This field **SHOULD NOT** be displayed to users +The first byte, `\xff`, was chosen for the following reasons: -### Header +1. It corresponds to a value that is illegal as the first byte in a transaction + `MessageHeader` (see [Runtime considerations](#runtime-considerations)). +1. It avoids unintentional misuse in languages with C-like, null-terminated + strings. -#### Header version +The remaining bytes, `b"solana offchain"`, were chosen to be descriptive and +reasonably long, but are otherwise arbitrary. -The header version is represented as an 8-bit unsigned integer. Only the version -0 header format is specified in this document +This field **SHOULD NOT** be displayed to users. -This field **SHOULD NOT** be displayed to users +### Version -#### Application domain +The version is an 8-bit unsigned integer. For messages conforming to this +document it **MUST** equal `1`. -A 32-byte array identifying the application requesting off-chain message signing. -This may be any arbitrary bytes. For instance the on-chain address of a program, -DAO instance, Candy Machine, etc. +This field **SHOULD NOT** be displayed to users. -This field **SHOULD** be displayed to users as a base58-encoded ASCII string rather -than interpreted otherwise. +### Required signers -#### Message Format +The list of `SIGNER_COUNT` ed25519 public keys that **MUST** sign the message +for it to be valid. The list: -Version `0` headers specify three message formats allowing for trade-offs between -compatibility and composition of messages. +* **MUST NOT** be empty. +* **MUST NOT** contain duplicates. +* **MUST** be sorted in ascending lexicographical (byte-wise) order. -| ID | Encoding | Maximum Length \* | Hardware Wallet Support | -| :-: | :-------------------: | :---------------: | :---------------------: | -| 0 | Restricted ASCII \*\* | 1232 | Yes | -| 1 | UTF-8 | 1232 | Blind sign only | -| 2 | UTF-8 | 65535 | No | +The ordering and uniqueness constraints exist so that a given logical message +always serializes to the same bytes — and therefore the same signature — no +matter the order in which an application declares its signers. Signers and +verifiers **MUST** enforce both constraints. -\* Combined length of the [message preamble](#message-preamble) and message body
-\*\* Those characters for which [`isprint(3)`](https://linux.die.net/man/3/isprint) -returns true. That is, `0x20..=0x7e`. +These addresses **SHOULD** be displayed to users as base58-encoded strings. -Both the message encoding and maximum message length **MUST** be enforced by -signer and verifier. +### Content -Formats `0` and `1` are motivated by hardware wallet support where both RAM -to store the payload and font character support are limited. +The message content occupies the remainder of the buffer after the signers. It +is a UTF-8 encoded string and **MUST NOT** be empty. Unlike version 0, there is +no length prefix and no upper bound imposed by the format itself. -This field **SHOULD NOT** be displayed to users +Hardware-wallet signability and clear-signability are properties of the signing +device, not of the message format. Clients that need to target a specific +hardware wallet **MAY** impose their own additional restrictions — for example +limiting content to printable ASCII (`0x20..=0x7e`) or to a maximum byte length +— but the format does not require or encode these choices. ## Signing Solana off-chain messages **MUST** only be signed using the ed25519 digital signature scheme. Before signing, the message **MUST** be strictly checked to -conform to the associated preamble. The message body is then appended to the -[message preamble](#message-preamble). Finally the result is ed25519 signed. +conform to this specification, including the signer ordering and uniqueness +constraints. The full message byte string (signing domain, version, signers, and +content) is then ed25519-signed by each required signer. ## Verification -Upon successful ed25519 verification of _all_ attached signatures, the message -**MUST** be strictly checked to conform to the [message preamble](#message-preamble). -A message that does not conform to its preamble is invalid, regardless of the + +A message is valid only if, for **every** required signer in the preamble, a +corresponding ed25519 signature is present and verifies against the full message +byte string. The message **MUST** also be strictly checked to conform to this +specification — a message that does not conform is invalid regardless of the validity of any signatures. ## Envelope @@ -107,32 +136,97 @@ validity of any signatures. When passing around signed off-chain messages a common format is helpful. The recommended binary representation is as follows: -| Field | Start offset | Length (bytes) | Description -| :---- | :----------: | :------------: | :---------- -| Signature Count \* | 0x00 | 1 | Number of signatures. An 8-bit, unsigned integer -| Signatures | 0x01 | `SIG_COUNT` * 64 | `SIG_COUNT` ed25519 signatures -| Message Preamble | 0x01 + `SIG_COUNT` * 64 | `PREAMBLE_LEN` | The [message preamble](#message-preamble) -| Message Body | 0x01 + `SIG_COUNT * 64 + `PREAMBLE_LEN` | `MESSAGE_LEN` | The message content +| Field | Start offset | Length (bytes) | Description | +| :-------------- | :-----------------------: | :---------------: | :----------------------------------------------------------------------- | +| Signature count | 0x00 | 1 | Number of signatures. An 8-bit unsigned integer. **MUST NOT** be zero. | +| Signatures | 0x01 | `SIG_COUNT` * 64 | `SIG_COUNT` ed25519 signatures. | +| Message | 0x01 + `SIG_COUNT` * 64 | remainder | The signed [message](#message-format) (preamble and content). | + +* The signature count **MUST** equal the signer count from the message preamble. +* Signatures **MUST** be ordered to match their corresponding public keys as they + appear in the (sorted) signers list. +* A missing signature in a partially-signed envelope is represented by 64 zero + bytes. + +## Runtime considerations + +To prevent social attacks by which the signer is tricked into signing a +transaction, the runtime **MUST NOT** accept signed off-chain messages as +transactions under any circumstances. The first byte of the +[signing domain](#signing-domain) is chosen such that it corresponds to a value +(`0xff`) which is implicitly illegal as the first byte in a transaction +`MessageHeader` today. The property is implicit because the top bit in the first +byte of a `MessageHeader` being set signals a versioned transaction, but only a +value of +[zero is supported](https://github.com/solana-labs/solana/blob/b6ae6c1fe17e4b64c5051c651ca2585e4f55468c/sdk/program/src/message/versions/mod.rs#L269-L281) +at this time. The runtime reserves `127` as an illegal version number, making +this property explicit. -The signature count **MUST** match the value of signers count from the message -preamble. +## Reference -Signatures **MUST** be ordered to match their corresponding public keys as -specified in the message preamble. +* Specification discussion: + [SRFC #3](https://github.com/solana-foundation/SRFCs/discussions/3) +* Offchain messages SDK: + [`@solana/offchain-messages`](https://github.com/anza-xyz/kit/tree/main/packages/offchain-messages) + (exported via `@solana/kit`) +* The runtime check that reserves transaction version `127` (rejecting + `0xff`-prefixed bytes as transactions) was implemented in PR + [#29807](https://github.com/solana-labs/solana/pull/29807) -## Runtime Considerations +--- -To prevent social attacks by which the signer is tricked into signing a transaction, -the runtime **MUST NOT** accept signed off-chain messages as transactions under any -circumstances. The first byte of the [signing domain specifier](#signing-domain-specifier) -is chosen such that it corresponds to a value (`0xff`) which is implicitly illegal -as the first byte in a transaction `MessageHeader` today. The property is implicit -because the top bit in the first byte of a `MessageHeader` being set signals a -versioned transaction, but only a value of -[zero is supported](https://github.com/solana-labs/solana/blob/b6ae6c1fe17e4b64c5051c651ca2585e4f55468c/sdk/program/src/message/versions/mod.rs#L269-L281) -at this time. The runtime will need to be modified to reserve 127 as an illegal -version number, making this property explicit. +## Appendix: version 0 (legacy) + +Version 0 is the original off-chain message format. It remains supported for +backwards compatibility, but new applications **SHOULD** prefer +[version 1](#message-format). The differences are summarized in +[What changed in version 1](#what-changed-in-version-1). + +### Message preamble (v0) + +| Field | Start offset | Length (bytes) | Description | +| :----------------- | :----------------------: | :------------------: | :------------------------------------------------------------------------------------------------ | +| Signing Domain | 0x00 | 16 | Same as [v1](#signing-domain). | +| Header version | 0x10 | 1 | An 8-bit unsigned integer. `0` for this format. | +| Application domain | 0x11 | 32 | A 32-byte application identifier (e.g. a program address, DAO, etc.). \[[1](#application-domain-v0)\] | +| Message format | 0x31 | 1 | \[[2](#message-format-v0)\] | +| Signer count | 0x32 | 1 | Number of signers. An 8-bit unsigned integer. **MUST NOT** be zero. | +| Signers | 0x33 | `SIGNER_COUNT` * 32 | `SIGNER_COUNT` ed25519 public keys of signers. | +| Message length | 0x33 + `SIGNER_CNT` * 32 | 2 | Length of message in bytes. A 16-bit, unsigned, little-endian integer. **MUST NOT** be zero. | + +#### Application domain (v0) + +A 32-byte array identifying the application requesting off-chain message signing. +This may be any arbitrary bytes. This field **SHOULD** be displayed to users as a +base58-encoded ASCII string rather than interpreted otherwise. + +#### Message format (v0) + +Version `0` headers specify three message formats allowing for trade-offs between +compatibility and composition of messages. + +| ID | Encoding | Maximum Length \* | Hardware Wallet Support | +| :-: | :-------------------: | :---------------: | :---------------------: | +| 0 | Restricted ASCII \*\* | 1232 | Yes | +| 1 | UTF-8 | 1232 | Blind sign only | +| 2 | UTF-8 | 65535 | No | + +\* Combined length of the message preamble and message body
+\*\* Those characters for which [`isprint(3)`](https://linux.die.net/man/3/isprint) +returns true. That is, `0x20..=0x7e`. + +Both the message encoding and maximum message length **MUST** be enforced by +signer and verifier. Formats `0` and `1` are motivated by hardware wallet support +where both RAM to store the payload and font character support are limited. + +### Envelope (v0) -### Implementation +| Field | Start offset | Length (bytes) | Description | +| :---------------- | :-----------------------------------: | :---------------: | :--------------------------------------------------- | +| Signature Count | 0x00 | 1 | Number of signatures. An 8-bit, unsigned integer. | +| Signatures | 0x01 | `SIG_COUNT` * 64 | `SIG_COUNT` ed25519 signatures. | +| Message Preamble | 0x01 + `SIG_COUNT` * 64 | `PREAMBLE_LEN` | The message preamble. | +| Message Body | 0x01 + `SIG_COUNT` * 64 + `PREAMBLE_LEN` | `MESSAGE_LEN` | The message content. | -The runtime changes described above have been implemented in PR [#29807](https://github.com/solana-labs/solana/pull/29807) +The signature count **MUST** match the signer count from the message preamble, +and signatures **MUST** be ordered to match their corresponding public keys. From 4981c79da6ffb91fb4380903dd33afeaf7c35a5a Mon Sep 17 00:00:00 2001 From: Brennan Date: Fri, 5 Jun 2026 15:07:46 -0700 Subject: [PATCH 225/576] CostTrackerLimits struct (#12984) --- core/src/banking_stage/qos_service.rs | 3 +- cost-model/src/cost_tracker.rs | 105 ++++++++++++++++---------- ledger/src/blockstore_processor.rs | 3 +- runtime/src/bank.rs | 4 +- runtime/src/bank/tests.rs | 9 +-- runtime/src/slot_params.rs | 55 +++++++------- 6 files changed, 99 insertions(+), 80 deletions(-) diff --git a/core/src/banking_stage/qos_service.rs b/core/src/banking_stage/qos_service.rs index 69781da313c..c28aa55a251 100644 --- a/core/src/banking_stage/qos_service.rs +++ b/core/src/banking_stage/qos_service.rs @@ -211,6 +211,7 @@ mod tests { use { super::*, itertools::Itertools, + solana_cost_model::cost_tracker::CostTrackerLimits, solana_hash::Hash, solana_keypair::Keypair, solana_runtime::genesis_utils::{GenesisConfigInfo, create_genesis_config}, @@ -300,7 +301,7 @@ mod tests { let cost_limit = transfer_tx_cost + vote_tx_cost; bank.write_cost_tracker() .unwrap() - .set_limits(cost_limit, cost_limit, 0); + .set_limits(CostTrackerLimits::new(cost_limit, cost_limit, 0)); let (results, num_selected) = QosService::select_transactions_per_cost(txs.iter(), txs_costs.into_iter(), &bank); assert_eq!(num_selected, 2); diff --git a/cost-model/src/cost_tracker.rs b/cost-model/src/cost_tracker.rs index 708cf6e849a..9e612281234 100644 --- a/cost-model/src/cost_tracker.rs +++ b/cost-model/src/cost_tracker.rs @@ -63,13 +63,42 @@ pub struct UpdatedCosts { pub updated_costliest_account_cost: u64, } +#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct CostTrackerLimits { + pub account_cost: u64, + pub block_cost: u64, + // Maximum new account allocation data per block in bytes. + pub allocated_data_size: u64, +} + +impl CostTrackerLimits { + const MAX: Self = Self::new(u64::MAX, u64::MAX, u64::MAX); + + pub const fn new(account_cost: u64, block_cost: u64, allocated_data_size: u64) -> Self { + Self { + account_cost, + block_cost, + allocated_data_size, + } + } +} + +impl Default for CostTrackerLimits { + fn default() -> Self { + const _: () = assert!(MAX_WRITABLE_ACCOUNT_UNITS <= MAX_BLOCK_UNITS); + Self { + account_cost: MAX_WRITABLE_ACCOUNT_UNITS, + block_cost: MAX_BLOCK_UNITS, + allocated_data_size: MAX_BLOCK_ACCOUNTS_DATA_SIZE_DELTA, + } + } +} + #[cfg_attr(feature = "frozen-abi", derive(AbiExample))] #[derive(Debug)] pub struct CostTracker { - account_cost_limit: u64, - block_cost_limit: u64, - // Maximum new account allocation data per block in bytes. - allocated_data_size_limit: u64, + limits: CostTrackerLimits, cost_by_writable_accounts: HashMap, block_cost: SharedBlockCost, transaction_count: Saturating, @@ -86,12 +115,8 @@ pub struct CostTracker { impl Default for CostTracker { fn default() -> Self { - const _: () = assert!(MAX_WRITABLE_ACCOUNT_UNITS <= MAX_BLOCK_UNITS); - Self { - account_cost_limit: MAX_WRITABLE_ACCOUNT_UNITS, - block_cost_limit: MAX_BLOCK_UNITS, - allocated_data_size_limit: MAX_BLOCK_ACCOUNTS_DATA_SIZE_DELTA, + limits: CostTrackerLimits::default(), cost_by_writable_accounts: HashMap::with_capacity_and_hasher( WRITABLE_ACCOUNTS_PER_BLOCK, ahash::RandomState::new(), @@ -111,43 +136,37 @@ impl Default for CostTracker { impl CostTracker { pub fn new_from_parent_limits(&self) -> Self { let mut new = Self::default(); - new.set_limits( - self.account_cost_limit, - self.block_cost_limit, - self.allocated_data_size_limit, - ); + new.set_limits(self.limits); new } + /// Get the cost tracker limits. + pub fn get_limits(&self) -> CostTrackerLimits { + self.limits + } + /// Get the overall account limit. pub fn get_account_limit(&self) -> u64 { - self.account_cost_limit + self.limits.account_cost } /// Get the overall block limit. pub fn get_block_limit(&self) -> u64 { - self.block_cost_limit + self.limits.block_cost } /// Get the overall allocated account data size limit. pub fn get_allocated_data_size_limit(&self) -> u64 { - self.allocated_data_size_limit + self.limits.allocated_data_size } /// allows to adjust limits initiated during construction - pub fn set_limits( - &mut self, - account_cost_limit: u64, - block_cost_limit: u64, - allocated_data_size_limit: u64, - ) { - self.account_cost_limit = account_cost_limit; - self.block_cost_limit = block_cost_limit; - self.allocated_data_size_limit = allocated_data_size_limit; + pub fn set_limits(&mut self, limits: CostTrackerLimits) { + self.limits = limits; } pub fn set_limits_max(&mut self) { - self.set_limits(u64::MAX, u64::MAX, u64::MAX); + self.set_limits(CostTrackerLimits::MAX); } pub fn in_flight_transaction_count(&self) -> usize { @@ -289,7 +308,8 @@ impl CostTracker { fn find_number_of_contended_accounts(&self) -> usize { // accounts has more than 95% of account_cu_limit is considered as highly contended let contended_cost_mark: u64 = self - .account_cost_limit + .limits + .account_cost .saturating_mul(95) .saturating_div(100); @@ -305,20 +325,20 @@ impl CostTracker { ) -> Result<(), CostTrackerError> { let cost: u64 = tx_cost.sum(); - if self.block_cost().saturating_add(cost) > self.block_cost_limit { + if self.block_cost().saturating_add(cost) > self.limits.block_cost { // check against the total package cost return Err(CostTrackerError::WouldExceedBlockMaxLimit); } // check if the transaction itself is more costly than the account_cost_limit - if cost > self.account_cost_limit { + if cost > self.limits.account_cost { return Err(CostTrackerError::WouldExceedAccountMaxLimit); } let allocated_accounts_data_size = self.allocated_accounts_data_size + Saturating(tx_cost.allocated_accounts_data_size()); - if allocated_accounts_data_size.0 > self.allocated_data_size_limit { + if allocated_accounts_data_size.0 > self.limits.allocated_data_size { return Err(CostTrackerError::WouldExceedAccountDataBlockLimit); } @@ -326,7 +346,7 @@ impl CostTracker { for account_key in tx_cost.writable_accounts() { match self.cost_by_writable_accounts.get(account_key) { Some(chained_cost) => { - if chained_cost.saturating_add(cost) > self.account_cost_limit { + if chained_cost.saturating_add(cost) > self.limits.account_cost { return Err(CostTrackerError::WouldExceedAccountMaxLimit); } else { continue; @@ -456,11 +476,13 @@ mod tests { impl CostTracker { fn new(account_cost_limit: u64, block_cost_limit: u64) -> Self { assert!(account_cost_limit <= block_cost_limit); - Self { - account_cost_limit, - block_cost_limit, - ..Self::default() - } + let mut cost_tracker = Self::default(); + cost_tracker.set_limits(CostTrackerLimits { + account_cost: account_cost_limit, + block_cost: block_cost_limit, + ..CostTrackerLimits::default() + }); + cost_tracker } } @@ -522,8 +544,8 @@ mod tests { #[test] fn test_cost_tracker_initialization() { let testee = CostTracker::new(10, 11); - assert_eq!(10, testee.account_cost_limit); - assert_eq!(11, testee.block_cost_limit); + assert_eq!(10, testee.limits.account_cost); + assert_eq!(11, testee.limits.block_cost); assert_eq!(0, testee.cost_by_writable_accounts.len()); assert_eq!(0, testee.block_cost()); } @@ -748,7 +770,10 @@ mod tests { assert_eq!(testee.would_fit(&tx_cost), Ok(()),); // Transaction does not fit with 1B limit. - testee.allocated_data_size_limit = 1; + testee.set_limits(CostTrackerLimits { + allocated_data_size: 1, + ..testee.get_limits() + }); assert_eq!( testee.would_fit(&tx_cost), Err(CostTrackerError::WouldExceedAccountDataBlockLimit), diff --git a/ledger/src/blockstore_processor.rs b/ledger/src/blockstore_processor.rs index 5963e927db5..4835261faa0 100644 --- a/ledger/src/blockstore_processor.rs +++ b/ledger/src/blockstore_processor.rs @@ -2976,6 +2976,7 @@ pub mod tests { rayon::ThreadPoolBuilder, solana_account::{AccountSharedData, WritableAccount}, solana_bls_signatures::{BLS_SIGNATURE_AFFINE_SIZE, Signature as BLSSignature}, + solana_cost_model::cost_tracker::CostTrackerLimits, solana_entry::{ block_component::{BlockComponent, BlockFooterV1, BlockHeaderV1, VersionedBlockMarker}, entry::{create_ticks, next_entry, next_entry_mut}, @@ -6293,7 +6294,7 @@ pub mod tests { let block_limit = tx_cost.sum(); bank.write_cost_tracker() .unwrap() - .set_limits(u64::MAX, block_limit, u64::MAX); + .set_limits(CostTrackerLimits::new(u64::MAX, block_limit, u64::MAX)); let tx_costs = vec![None, Some(tx_cost), None]; // The transaction will fit when added the first time diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 5b7aec6608d..7df2975225b 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -4735,11 +4735,11 @@ impl Bank { /// Recomputes cost tracker limits from active feature state. fn apply_cost_tracker_limits_for_active_features(&mut self) { let params = self.current_slot_params(); - let (account_cost_limit, block_cost_limit, data_size_limit) = + let cost_limits = params.cost_limits(self.feature_set.snapshot().raise_block_limits_to_100m); let mut cost_tracker = self.write_cost_tracker().unwrap(); - cost_tracker.set_limits(account_cost_limit, block_cost_limit, data_size_limit); + cost_tracker.set_limits(cost_limits); } /// Recomputes this bank's effective partitioned-reward write budget. diff --git a/runtime/src/bank/tests.rs b/runtime/src/bank/tests.rs index 12f7d48811a..4651ddbab17 100644 --- a/runtime/src/bank/tests.rs +++ b/runtime/src/bank/tests.rs @@ -6326,16 +6326,11 @@ fn assert_slot_time_bank_state(bank: &Bank, params: SlotParams) { params.max_entry_bytes_per_slot() ); let cost_tracker = bank.read_cost_tracker().unwrap(); - let (account_limit, block_limit, data_size_limit) = params.cost_limits( + let cost_limits = params.cost_limits( bank.feature_set .is_active(&feature_set::raise_block_limits_to_100m::id()), ); - assert_eq!(cost_tracker.get_account_limit(), account_limit); - assert_eq!(cost_tracker.get_block_limit(), block_limit); - assert_eq!( - cost_tracker.get_allocated_data_size_limit(), - data_size_limit - ); + assert_eq!(cost_tracker.get_limits(), cost_limits); } #[test] diff --git a/runtime/src/slot_params.rs b/runtime/src/slot_params.rs index 84e91d9c8b2..819ffdbd62b 100644 --- a/runtime/src/slot_params.rs +++ b/runtime/src/slot_params.rs @@ -1,6 +1,7 @@ use { agave_feature_set::{self as feature_set, FeatureSet}, solana_clock::Slot, + solana_cost_model::cost_tracker::CostTrackerLimits, solana_epoch_schedule::EpochSchedule, solana_pubkey::Pubkey, std::{ @@ -20,9 +21,7 @@ pub struct SlotParams { pub(crate) ns_per_slot: u128, pub(crate) slots_per_year: f64, pub(crate) hashes_per_tick: Option, - pub(crate) max_block_units: u64, - pub(crate) max_writable_account_units: u64, - pub(crate) max_block_accounts_data_size_delta: u64, + pub(crate) cost_tracker_limits: CostTrackerLimits, pub(crate) max_data_shreds_per_slot: u32, pub(crate) max_code_shreds_per_slot: u32, pub(crate) max_entry_bytes_per_slot: u64, @@ -86,22 +85,30 @@ impl SlotParams { } /// Returns the per-bank cost limits for these params. - pub(crate) const fn cost_limits(self, raise_block_limits_to_100m: bool) -> (u64, u64, u64) { - let (max_writable_account_units, max_block_units) = if raise_block_limits_to_100m { + pub(crate) const fn cost_limits(self, raise_block_limits_to_100m: bool) -> CostTrackerLimits { + let cost_tracker_limits = self.cost_tracker_limits; + let (account_cost, block_cost) = if raise_block_limits_to_100m { ( - self.max_writable_account_units + cost_tracker_limits + .account_cost + .saturating_mul(100) + .saturating_div(60), + cost_tracker_limits + .block_cost .saturating_mul(100) .saturating_div(60), - self.max_block_units.saturating_mul(100).saturating_div(60), ) } else { - (self.max_writable_account_units, self.max_block_units) + ( + cost_tracker_limits.account_cost, + cost_tracker_limits.block_cost, + ) }; - ( - max_writable_account_units, - max_block_units, - self.max_block_accounts_data_size_delta, + CostTrackerLimits::new( + account_cost, + block_cost, + cost_tracker_limits.allocated_data_size, ) } } @@ -111,9 +118,7 @@ pub(crate) const LEGACY_SLOT_PARAMS: SlotParams = SlotParams { ns_per_slot: 400_000_000, slots_per_year: 78_892_314.984, hashes_per_tick: Some(LEGACY_HASHES_PER_TICK), - max_block_units: 60_000_000, - max_writable_account_units: 24_000_000, - max_block_accounts_data_size_delta: 100_000_000, + cost_tracker_limits: CostTrackerLimits::new(24_000_000, 60_000_000, 100_000_000), max_data_shreds_per_slot: 32_768, max_code_shreds_per_slot: 32_768, max_entry_bytes_per_slot: 20 * 1024 * 1024, @@ -124,9 +129,7 @@ pub(crate) const SLOT_PARAMS_350MS: SlotParams = SlotParams { ns_per_slot: 350_000_000, slots_per_year: 90_162_645.696, hashes_per_tick: Some(54_687), - max_block_units: 52_500_000, - max_writable_account_units: 21_000_000, - max_block_accounts_data_size_delta: 87_500_000, + cost_tracker_limits: CostTrackerLimits::new(21_000_000, 52_500_000, 87_500_000), max_data_shreds_per_slot: 28_672, max_code_shreds_per_slot: 28_672, max_entry_bytes_per_slot: 18_350_080, @@ -137,9 +140,7 @@ pub(crate) const SLOT_PARAMS_300MS: SlotParams = SlotParams { ns_per_slot: 300_000_000, slots_per_year: 105_189_753.312, hashes_per_tick: Some(46_875), - max_block_units: 45_000_000, - max_writable_account_units: 18_000_000, - max_block_accounts_data_size_delta: 75_000_000, + cost_tracker_limits: CostTrackerLimits::new(18_000_000, 45_000_000, 75_000_000), max_data_shreds_per_slot: 24_576, max_code_shreds_per_slot: 24_576, max_entry_bytes_per_slot: 15_728_640, @@ -150,9 +151,7 @@ pub(crate) const SLOT_PARAMS_250MS: SlotParams = SlotParams { ns_per_slot: 250_000_000, slots_per_year: 126_227_703.974, hashes_per_tick: Some(39_062), - max_block_units: 37_500_000, - max_writable_account_units: 15_000_000, - max_block_accounts_data_size_delta: 62_500_000, + cost_tracker_limits: CostTrackerLimits::new(15_000_000, 37_500_000, 62_500_000), max_data_shreds_per_slot: 20_480, max_code_shreds_per_slot: 20_480, max_entry_bytes_per_slot: 13_107_200, @@ -163,9 +162,7 @@ pub(crate) const SLOT_PARAMS_200MS: SlotParams = SlotParams { ns_per_slot: 200_000_000, slots_per_year: 157_784_629.968, hashes_per_tick: Some(31_250), - max_block_units: 30_000_000, - max_writable_account_units: 12_000_000, - max_block_accounts_data_size_delta: 50_000_000, + cost_tracker_limits: CostTrackerLimits::new(12_000_000, 30_000_000, 50_000_000), max_data_shreds_per_slot: 16_384, max_code_shreds_per_slot: 16_384, max_entry_bytes_per_slot: 10_485_760, @@ -354,10 +351,10 @@ mod tests { (SLOT_PARAMS_250MS, 25_000_000, 62_500_000), (SLOT_PARAMS_200MS, 20_000_000, 50_000_000), ] { - let (_, _, data_size_limit) = params.cost_limits(false); + let data_size_limit = params.cost_limits(false).allocated_data_size; assert_eq!( params.cost_limits(true), - ( + CostTrackerLimits::new( expected_account_limit, expected_block_limit, data_size_limit From 8a19c87b41a18163057eca5453d0fd4aca82b3c0 Mon Sep 17 00:00:00 2001 From: devinschmitz <94090407+devinschmitz@users.noreply.github.com> Date: Fri, 5 Jun 2026 23:15:18 -0400 Subject: [PATCH 226/576] fix(consume_worker): use leader bank if available for check messages (#12954) check_batch validated max_working_slot against the highest working bank from bank_forks, while execute_batch uses the active leader bank. When the highest fork is not the leader fork, checks were spuriously rejected with MAX_WORKING_SLOT_EXCEEDED. Prefer the active leader bank when leader, falling back to the highest working bank otherwise. Co-authored-by: Devin Schmitz --- core/src/banking_stage/consume_worker.rs | 39 ++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/core/src/banking_stage/consume_worker.rs b/core/src/banking_stage/consume_worker.rs index 0a018b93a65..f22be6998b7 100644 --- a/core/src/banking_stage/consume_worker.rs +++ b/core/src/banking_stage/consume_worker.rs @@ -468,6 +468,10 @@ pub(crate) mod external { root_bank, working_bank, } = self.sharable_banks.load(); + // Prefer the leader bank over the highest working fork when leader + let working_bank = active_leader_state(&self.shared_leader_state) + .and_then(|leader_state| leader_state.working_bank().cloned()) + .unwrap_or(working_bank); if working_bank.slot() > message.max_working_slot { return self.return_unprocessed_message( @@ -1144,7 +1148,7 @@ pub(crate) mod external { mint_keypair: Keypair, genesis_config: GenesisConfig, bank: Arc, - _bank_forks: Arc>, + bank_forks: Arc>, _replay_vote_receiver: ReplayVoteReceiver, record_receiver: RecordReceiver, allocator: rts_alloc::Allocator, @@ -1349,7 +1353,7 @@ pub(crate) mod external { mint_keypair, genesis_config, bank, - _bank_forks: bank_forks, + bank_forks, _replay_vote_receiver: replay_vote_receiver, record_receiver, allocator: client_session.allocators.pop().unwrap(), @@ -1980,6 +1984,37 @@ pub(crate) mod external { test_frame.free_batch(batch); } + #[test] + fn test_run_check_prefers_active_leader_bank() { + let mut test_frame = setup_external_test_frame(); + let batch = test_frame.allocate_batch(&[test_serialized_transaction( + test_frame.bank.confirmed_last_blockhash(), + )]); + + // Insert a higher fork bank so the highest working bank exceeds + // `max_working_slot` while the active leader bank does not. + let higher_bank = Bank::new_from_parent( + test_frame.bank.clone(), + SlotLeader::new_unique(), + test_frame.bank.slot() + 1, + ); + test_frame.bank_forks.write().unwrap().insert(higher_bank); + test_frame.set_active_bank(); + + test_frame.send_message(PackToWorkerMessage { + flags: pack_message_flags::CHECK | check_flags::STATUS_CHECKS, + max_working_slot: test_frame.bank.slot(), + batch: batch.region, + }); + test_frame.iterate().unwrap(); + let response = test_frame.recv_response(); + assert_eq!(response.processed_code, processed_codes::PROCESSED); + let responses = test_frame.check_responses(&response.responses); + assert_eq!(responses.len(), 1); + + test_frame.free_batch(batch); + } + #[test] fn test_run_check_resolve_without_loaded_addresses() { let mut test_frame = setup_external_test_frame(); From fed083ba3fb0ac5f0fb5ec93a9c98eff8cf32ee6 Mon Sep 17 00:00:00 2001 From: kirill lykov Date: Sat, 6 Jun 2026 10:16:08 +0200 Subject: [PATCH 227/576] move XdpSender from Turbine to net-utils (#12897) --- Cargo.lock | 3 ++- core/src/tvu.rs | 4 ++-- core/src/validator.rs | 4 ++-- dev-bins/Cargo.lock | 3 ++- net-utils/Cargo.toml | 2 ++ net-utils/src/lib.rs | 2 ++ .../src/pinned_xdp_sender.rs | 11 +++++------ programs/sbf/Cargo.lock | 3 ++- turbine/Cargo.toml | 1 - turbine/src/lib.rs | 4 +--- 10 files changed, 20 insertions(+), 17 deletions(-) rename turbine/src/xdp_sender.rs => net-utils/src/pinned_xdp_sender.rs (73%) diff --git a/Cargo.lock b/Cargo.lock index 084e382db24..aa7da96fd9e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9041,10 +9041,12 @@ name = "solana-net-utils" version = "4.2.0-alpha.0" dependencies = [ "agave-logger", + "agave-xdp", "anyhow", "bincode", "bytes", "cfg-if 1.0.4", + "crossbeam-channel", "dashmap", "hxdmp", "itertools 0.14.0", @@ -11216,7 +11218,6 @@ dependencies = [ "agave-logger", "agave-votor", "agave-votor-messages", - "agave-xdp", "assert_matches", "bencher", "bs58", diff --git a/core/src/tvu.rs b/core/src/tvu.rs index c119efc2aa4..0469a562ea8 100644 --- a/core/src/tvu.rs +++ b/core/src/tvu.rs @@ -77,7 +77,7 @@ use { quic::{QuicStreamerConfig, SpawnServerResult, spawn_simple_qos_server}, streamer::StakedNodes, }, - solana_turbine::{XdpSender, retransmit_stage::RetransmitStage}, + solana_turbine::{XdpSender as TurbineXdpSender, retransmit_stage::RetransmitStage}, std::{ collections::HashSet, net::UdpSocket, @@ -143,7 +143,7 @@ pub struct TvuConfig { pub replay_transactions_threads: NonZeroUsize, pub shred_sigverify_threads: NonZeroUsize, pub bls_sigverify_threads: NonZeroUsize, - pub turbine_xdp_sender: Option, + pub turbine_xdp_sender: Option, } impl Default for TvuConfig { diff --git a/core/src/validator.rs b/core/src/validator.rs index 95d2466f09e..160eb923341 100644 --- a/core/src/validator.rs +++ b/core/src/validator.rs @@ -144,7 +144,7 @@ use { }, solana_time_utils::timestamp, solana_tpu_client::tpu_client::{DEFAULT_TPU_CONNECTION_POOL_SIZE, DEFAULT_VOTE_USE_QUIC}, - solana_turbine::{self, XdpSender, broadcast_stage::BroadcastStageType}, + solana_turbine::{self, XdpSender as TurbineXdpSender, broadcast_stage::BroadcastStageType}, solana_unified_scheduler_pool::DefaultSchedulerPool, solana_validator_exit::Exit, solana_vote_program::vote_state::{VoteStateV4, handler::VoteStateHandler}, @@ -1560,7 +1560,7 @@ impl Validator { let (transmitter, sender) = xdp_transmit_builder.build(); ( Some(transmitter), - Some(XdpSender::new(sender.clone(), src_addr)), + Some(TurbineXdpSender::new(sender.clone(), src_addr)), Some((sender, *src_addr.ip())), ) } else { diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 75b2eb92c8d..4c56d75df5c 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -7655,9 +7655,11 @@ checksum = "ae8dd4c280dca9d046139eb5b7a5ac9ad10403fbd64964c7d7571214950d758f" name = "solana-net-utils" version = "4.2.0-alpha.0" dependencies = [ + "agave-xdp", "bincode", "bytes", "cfg-if 1.0.4", + "crossbeam-channel", "dashmap", "itertools 0.14.0", "log", @@ -9384,7 +9386,6 @@ dependencies = [ "agave-feature-set", "agave-votor", "agave-votor-messages", - "agave-xdp", "bytes", "crossbeam-channel", "itertools 0.14.0", diff --git a/net-utils/Cargo.toml b/net-utils/Cargo.toml index 16f2b2f2e93..1ca335d3dd6 100644 --- a/net-utils/Cargo.toml +++ b/net-utils/Cargo.toml @@ -21,10 +21,12 @@ dev-context-only-utils = ["dep:anyhow", "dep:pcap-file", "dep:hxdmp"] shuttle-test = ["dep:shuttle", "solana-svm-type-overrides/shuttle-test"] [dependencies] +agave-xdp = { workspace = true } anyhow = { workspace = true, optional = true } bincode = { workspace = true } bytes = { workspace = true } cfg-if = { workspace = true } +crossbeam-channel = { workspace = true } dashmap = { workspace = true, features = ["raw-api"] } hxdmp = { version = "0.2.1", optional = true } itertools = { workspace = true } diff --git a/net-utils/src/lib.rs b/net-utils/src/lib.rs index 321f9e51f4d..1053d0978bf 100644 --- a/net-utils/src/lib.rs +++ b/net-utils/src/lib.rs @@ -5,6 +5,7 @@ pub mod banlist; mod ip_echo_client; mod ip_echo_server; pub mod multihomed_sockets; +pub mod pinned_xdp_sender; pub mod socket_addr_space; pub mod sockets; #[cfg(any(target_os = "android", target_os = "windows"))] @@ -22,6 +23,7 @@ pub use { ip_echo_server::{ DEFAULT_IP_ECHO_SERVER_THREADS, IpEchoServer, MAX_PORT_COUNT_PER_MESSAGE, ip_echo_server, }, + pinned_xdp_sender::PinnedXdpSender, socket_addr_space::SocketAddrSpace, }; use { diff --git a/turbine/src/xdp_sender.rs b/net-utils/src/pinned_xdp_sender.rs similarity index 73% rename from turbine/src/xdp_sender.rs rename to net-utils/src/pinned_xdp_sender.rs index 1f6d734a731..54ee6251996 100644 --- a/turbine/src/xdp_sender.rs +++ b/net-utils/src/pinned_xdp_sender.rs @@ -1,20 +1,19 @@ -//! This module defines [`XdpSender`] which is a convenience wrapper around -//! [`agave_xdp::transmitter::XdpSender`] for the case when source address is fixed for all -//! items like it is in turbine. +//! This module defines [`PinnedXdpSender`] which is a convenience wrapper around +//! [`agave_xdp::transmitter::XdpSender`] for the case when source address is fixed. use { agave_xdp::transmitter as tx, bytes::Bytes, crossbeam_channel::TrySendError, std::net::SocketAddrV4, }; -/// [`XdpSender`] is a structure that simplifies sending packets over XDP with `XdpSender` +/// [`PinnedXdpSender`] simplifies sending packets over XDP /// when source address is fixed for all items. #[derive(Clone)] -pub struct XdpSender { +pub struct PinnedXdpSender { sender: tx::XdpSender, src_addr: SocketAddrV4, } -impl XdpSender { +impl PinnedXdpSender { pub fn new(sender: tx::XdpSender, src_addr: SocketAddrV4) -> Self { Self { sender, src_addr } } diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index 0cfee5b8e99..0e9b5083389 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -7421,9 +7421,11 @@ checksum = "ae8dd4c280dca9d046139eb5b7a5ac9ad10403fbd64964c7d7571214950d758f" name = "solana-net-utils" version = "4.2.0-alpha.0" dependencies = [ + "agave-xdp", "bincode", "bytes", "cfg-if 1.0.4", + "crossbeam-channel", "dashmap", "itertools 0.14.0", "log", @@ -9877,7 +9879,6 @@ dependencies = [ "agave-feature-set", "agave-votor", "agave-votor-messages", - "agave-xdp", "bytes", "crossbeam-channel", "itertools 0.14.0", diff --git a/turbine/Cargo.toml b/turbine/Cargo.toml index eea5f3fe7fe..9a7f462de87 100644 --- a/turbine/Cargo.toml +++ b/turbine/Cargo.toml @@ -16,7 +16,6 @@ agave-unstable-api = [] agave-feature-set = { workspace = true } agave-votor = { workspace = true } agave-votor-messages = { workspace = true } -agave-xdp = { workspace = true } bytes = { workspace = true } crossbeam-channel = { workspace = true } itertools = { workspace = true } diff --git a/turbine/src/lib.rs b/turbine/src/lib.rs index 19fe5b7e514..186149df020 100644 --- a/turbine/src/lib.rs +++ b/turbine/src/lib.rs @@ -11,9 +11,7 @@ pub mod retransmit_stage; pub mod sigverify_shreds; -pub mod xdp_sender; - -pub use crate::xdp_sender::XdpSender; +pub use solana_net_utils::PinnedXdpSender as XdpSender; #[macro_use] extern crate log; From 0c83788f092e2cdaca9ee220665aa210fc5893fc Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Sun, 7 Jun 2026 22:24:18 -0600 Subject: [PATCH 228/576] net: add alpenglow mode to net scripts (#12980) --- net/net.sh | 16 ++++++++++++---- net/remote/remote-node.sh | 9 +++++++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/net/net.sh b/net/net.sh index eb615bca60a..3ba42e3f592 100755 --- a/net/net.sh +++ b/net/net.sh @@ -297,6 +297,7 @@ startBootstrapLeader() { declare ipAddress=$1 declare nodeIndex="$2" declare logFile="$3" + declare alpenglow="$4" echo "--- Starting bootstrap validator: $ipAddress" echo "start log: $logFile" @@ -332,6 +333,7 @@ startBootstrapLeader() { \"$TMPFS_ACCOUNTS\" \ \"$disableQuic\" \ \"$enableUdp\" \ + \"$alpenglow\" \ " ) >> "$logFile" 2>&1 || { @@ -345,6 +347,7 @@ startNode() { declare ipAddress=$1 declare nodeType=$2 declare nodeIndex="$3" + declare alpenglow="$4" initLogDir declare logFile="$netLogDir/validator-$ipAddress.log" @@ -405,6 +408,7 @@ startNode() { \"$TMPFS_ACCOUNTS\" \ \"$disableQuic\" \ \"$enableUdp\" \ + \"$alpenglow\" \ " ) >> "$logFile" 2>&1 & declare pid=$! @@ -585,7 +589,7 @@ deploy() { if $bootstrapLeader; then SECONDS=0 declare bootstrapNodeDeployTime= - startBootstrapLeader "$nodeAddress" "$nodeIndex" "$netLogDir/bootstrap-validator-$ipAddress.log" + startBootstrapLeader "$nodeAddress" "$nodeIndex" "$netLogDir/bootstrap-validator-$ipAddress.log" "$alpenglow" bootstrapNodeDeployTime=$SECONDS $metricsWriteDatapoint "testnet-deploy net-bootnode-leader-started=1" @@ -593,7 +597,7 @@ deploy() { SECONDS=0 pids=() else - startNode "$ipAddress" "$nodeType" "$nodeIndex" + startNode "$ipAddress" "$nodeType" "$nodeIndex" "$alpenglow" # Stagger additional node start time. If too many nodes start simultaneously # the bootstrap node gets more rsync requests from the additional nodes than @@ -789,6 +793,7 @@ disableQuic=false enableUdp=false clientType=tpu-client maybeUseUnstakedConnection="" +alpenglow=false command=$1 [[ -n $command ]] || usage @@ -917,6 +922,9 @@ while [[ -n $1 ]]; do elif [[ $1 = --use-unstaked-connection ]]; then maybeUseUnstakedConnection="$1" shift 1 + elif [[ $1 = --alpenglow ]]; then + alpenglow=true + shift 1 else usage "Unknown long option: $1" fi @@ -1026,7 +1034,7 @@ if [[ "$numClientsRequested" -eq 0 ]]; then numClientsRequested=$numClients else if [[ "$numClientsRequested" -gt "$numClients" ]]; then - echo "Error: More clients requested ($numClientsRequested) then available ($numClients)" + echo "Error: More clients requested ($numClientsRequested) than available ($numClients)" exit 1 fi fi @@ -1099,7 +1107,7 @@ startnode) nodeType= nodeIndex= getNodeType - startNode "$nodeAddress" "$nodeType" "$nodeIndex" + startNode "$nodeAddress" "$nodeType" "$nodeIndex" "$alpenglow" ;; startclients) startClients diff --git a/net/remote/remote-node.sh b/net/remote/remote-node.sh index a89a3851602..0ed7417c12f 100755 --- a/net/remote/remote-node.sh +++ b/net/remote/remote-node.sh @@ -29,6 +29,7 @@ extraPrimordialStakes="${20:=0}" tmpfsAccounts="${21:false}" disableQuic="${22}" enableUdp="${23}" +alpenglow="${24:-false}" set +x @@ -203,9 +204,17 @@ EOF "$(solana-keygen pubkey "config/validator-vote-$i.json")" "$(solana-keygen pubkey "config/validator-stake-$i.json")" ) + args+=(--bootstrap-validator-bls-pubkey "$(solana-keygen bls_pubkey "config/validator-identity-$i.json")") done fi + if $alpenglow; then + echo "Consensus method: Alpenglow" + args+=(--alpenglow) + else + echo "Consensus method: TowerBFT" + fi + multinode-demo/setup.sh "${args[@]}" maybeWaitForSupermajority= From 20a67b36bddc821f21904175d7207941e9ce57a0 Mon Sep 17 00:00:00 2001 From: Nikita Belenkov Date: Mon, 8 Jun 2026 01:02:22 -0400 Subject: [PATCH 229/576] Adding anza crates to 7 day exception (#12927) adding anza crates to 7 day exception --- .github/dependabot.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 200c581e0ab..2d6592bd6cd 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -10,6 +10,14 @@ updates: open-pull-requests-limit: 6 cooldown: default-days: 7 + # crates published by anza-team bypass the cooldown + exclude: + - "solana-*" + - "agave-*" + - "spl-*" + - "rts-alloc" + - "shaq" + - "wincode" # split from root directory schedule to avoid duplicate checks - package-ecosystem: cargo @@ -24,6 +32,14 @@ updates: open-pull-requests-limit: 6 cooldown: default-days: 7 + # crates published by anza-team bypass the cooldown + exclude: + - "solana-*" + - "agave-*" + - "spl-*" + - "rts-alloc" + - "shaq" + - "wincode" - package-ecosystem: "npm" directory: "/docs" From bfe243de9f2c6275407259ef39b1a17803acbced Mon Sep 17 00:00:00 2001 From: steviez Date: Mon, 8 Jun 2026 00:36:50 -0500 Subject: [PATCH 230/576] blockstore: Cleanup empty column test function (#13005) Among other things, the function has unnecessary public scope and fails to properly check several columns. This PR cleans it up --- ledger/src/blockstore.rs | 117 ----------- ledger/src/blockstore/blockstore_purge.rs | 227 +++++++++++++++++++++- 2 files changed, 222 insertions(+), 122 deletions(-) diff --git a/ledger/src/blockstore.rs b/ledger/src/blockstore.rs index cdb82191f71..5dc397828d0 100644 --- a/ledger/src/blockstore.rs +++ b/ledger/src/blockstore.rs @@ -6465,123 +6465,6 @@ pub fn make_many_slot_entries( (shreds, entries) } -#[cfg(feature = "dev-context-only-utils")] -pub fn test_all_empty_or_min(blockstore: &Blockstore, min_slot: Slot) { - let condition_met = blockstore - .meta_cf - .iter(IteratorMode::Start) - .unwrap() - .next() - .map(|(slot, _)| slot >= min_slot) - .unwrap_or(true) - & blockstore - .roots_cf - .iter(IteratorMode::Start) - .unwrap() - .next() - .map(|(slot, _)| slot >= min_slot) - .unwrap_or(true) - & blockstore - .data_shred_cf - .iter(IteratorMode::Start) - .unwrap() - .next() - .map(|((slot, _), _)| slot >= min_slot) - .unwrap_or(true) - & blockstore - .code_shred_cf - .iter(IteratorMode::Start) - .unwrap() - .next() - .map(|((slot, _), _)| slot >= min_slot) - .unwrap_or(true) - & blockstore - .dead_slots_cf - .iter(IteratorMode::Start) - .unwrap() - .next() - .map(|(slot, _)| slot >= min_slot) - .unwrap_or(true) - & blockstore - .duplicate_slots_cf - .iter(IteratorMode::Start) - .unwrap() - .next() - .map(|(slot, _)| slot >= min_slot) - .unwrap_or(true) - & blockstore - .erasure_meta_cf - .iter(IteratorMode::Start) - .unwrap() - .next() - .map(|((slot, _), _)| slot >= min_slot) - .unwrap_or(true) - & blockstore - .orphans_cf - .iter(IteratorMode::Start) - .unwrap() - .next() - .map(|(slot, _)| slot >= min_slot) - .unwrap_or(true) - & blockstore - .index_cf - .iter(IteratorMode::Start) - .unwrap() - .next() - .map(|(slot, _)| slot >= min_slot) - .unwrap_or(true) - & blockstore - .transaction_status_cf - .iter(IteratorMode::Start) - .unwrap() - .next() - .map(|((_, slot), _)| slot >= min_slot || slot == 0) - .unwrap_or(true) - & blockstore - .address_signatures_cf - .iter(IteratorMode::Start) - .unwrap() - .next() - .map(|((_, slot, _, _), _)| slot >= min_slot || slot == 0) - .unwrap_or(true) - & blockstore - .rewards_cf - .iter(IteratorMode::Start) - .unwrap() - .next() - .map(|(slot, _)| slot >= min_slot) - .unwrap_or(true) - & blockstore - .alt_meta_cf - .iter(IteratorMode::Start) - .unwrap() - .next() - .map(|((slot, _), _)| slot >= min_slot) - .unwrap_or(true) - & blockstore - .alt_index_cf - .iter(IteratorMode::Start) - .unwrap() - .next() - .map(|((slot, _), _)| slot >= min_slot) - .unwrap_or(true) - & blockstore - .alt_data_shred_cf - .iter(IteratorMode::Start) - .unwrap() - .next() - .map(|((slot, _, _), _)| slot >= min_slot) - .unwrap_or(true) - & blockstore - .alt_merkle_root_meta_cf - .iter(IteratorMode::Start) - .unwrap() - .next() - .map(|((slot, _, _), _)| slot >= min_slot) - .unwrap_or(true); - assert!(condition_met); -} - // Create shreds for slots that have a parent-child relationship defined by the input `chain` // used for tests only #[cfg(feature = "dev-context-only-utils")] diff --git a/ledger/src/blockstore/blockstore_purge.rs b/ledger/src/blockstore/blockstore_purge.rs index 253f1941d82..a243ced9d2d 100644 --- a/ledger/src/blockstore/blockstore_purge.rs +++ b/ledger/src/blockstore/blockstore_purge.rs @@ -475,6 +475,225 @@ pub mod tests { test_case::test_case, }; + fn all_columns_empty_or_greater_than_slot(blockstore: &Blockstore, min_slot: Slot) { + assert!( + blockstore + .data_shred_cf + .iter(IteratorMode::Start) + .unwrap() + .next() + .map(|((slot, _), _)| slot >= min_slot) + .unwrap_or(true) + ); + assert!( + blockstore + .code_shred_cf + .iter(IteratorMode::Start) + .unwrap() + .next() + .map(|((slot, _), _)| slot >= min_slot) + .unwrap_or(true) + ); + assert!( + blockstore + .meta_cf + .iter(IteratorMode::Start) + .unwrap() + .next() + .map(|(slot, _)| slot >= min_slot) + .unwrap_or(true) + ); + assert!( + blockstore + .index_cf + .iter(IteratorMode::Start) + .unwrap() + .next() + .map(|(slot, _)| slot >= min_slot) + .unwrap_or(true) + ); + assert!( + blockstore + .erasure_meta_cf + .iter(IteratorMode::Start) + .unwrap() + .next() + .map(|((slot, _), _)| slot >= min_slot) + .unwrap_or(true) + ); + assert!( + blockstore + .merkle_root_meta_cf + .iter(IteratorMode::Start) + .unwrap() + .next() + .map(|((slot, _), _)| slot >= min_slot) + .unwrap_or(true) + ); + assert!( + blockstore + .double_merkle_meta_cf + .iter(IteratorMode::Start) + .unwrap() + .next() + .map(|((slot, _), _)| slot >= min_slot) + .unwrap_or(true) + ); + assert!( + blockstore + .orphans_cf + .iter(IteratorMode::Start) + .unwrap() + .next() + .map(|(slot, _)| slot >= min_slot) + .unwrap_or(true) + ); + assert!( + blockstore + .duplicate_slots_cf + .iter(IteratorMode::Start) + .unwrap() + .next() + .map(|(slot, _)| slot >= min_slot) + .unwrap_or(true) + ); + + assert!( + blockstore + .alt_data_shred_cf + .iter(IteratorMode::Start) + .unwrap() + .next() + .map(|((slot, _, _), _)| slot >= min_slot) + .unwrap_or(true) + ); + assert!( + blockstore + .alt_meta_cf + .iter(IteratorMode::Start) + .unwrap() + .next() + .map(|((slot, _), _)| slot >= min_slot) + .unwrap_or(true) + ); + assert!( + blockstore + .alt_index_cf + .iter(IteratorMode::Start) + .unwrap() + .next() + .map(|((slot, _), _)| slot >= min_slot) + .unwrap_or(true) + ); + assert!( + blockstore + .alt_merkle_root_meta_cf + .iter(IteratorMode::Start) + .unwrap() + .next() + .map(|((slot, _, _), _)| slot >= min_slot) + .unwrap_or(true) + ); + + assert!( + blockstore + .bank_hash_cf + .iter(IteratorMode::Start) + .unwrap() + .next() + .map(|(slot, _)| slot >= min_slot) + .unwrap_or(true) + ); + assert!( + blockstore + .optimistic_slots_cf + .iter(IteratorMode::Start) + .unwrap() + .next() + .map(|(slot, _)| slot >= min_slot) + .unwrap_or(true) + ); + assert!( + blockstore + .roots_cf + .iter(IteratorMode::Start) + .unwrap() + .next() + .map(|(slot, _)| slot >= min_slot) + .unwrap_or(true) + ); + assert!( + blockstore + .dead_slots_cf + .iter(IteratorMode::Start) + .unwrap() + .next() + .map(|(slot, _)| slot >= min_slot) + .unwrap_or(true) + ); + + assert!( + blockstore + .block_height_cf + .iter(IteratorMode::Start) + .unwrap() + .next() + .map(|(slot, _)| slot >= min_slot) + .unwrap_or(true) + ); + assert!( + blockstore + .blocktime_cf + .iter(IteratorMode::Start) + .unwrap() + .next() + .map(|(slot, _)| slot >= min_slot) + .unwrap_or(true) + ); + assert!( + blockstore + .rewards_cf + .iter(IteratorMode::Start) + .unwrap() + .next() + .map(|(slot, _)| slot >= min_slot) + .unwrap_or(true) + ); + // The slot is not stored in the leading bytes for keys in the + // `TransactionStatus`, `TransactionMemos`, and `AddressSignatures` + // columns so the entire column must be checked + assert!( + blockstore + .transaction_status_cf + .iter(IteratorMode::Start) + .unwrap() + .all(|((_, slot), _)| slot >= min_slot) + ); + assert!( + blockstore + .transaction_memos_cf + .iter(IteratorMode::Start) + .unwrap() + .all(|((_, slot), _)| slot >= min_slot) + ); + assert!( + blockstore + .address_signatures_cf + .iter(IteratorMode::Start) + .unwrap() + .all(|((_, slot, _, _), _)| slot >= min_slot) + ); + assert!( + blockstore + .perf_samples_cf + .iter(IteratorMode::Start) + .unwrap() + .next() + .map(|(slot, _)| slot >= min_slot) + .unwrap_or(true) + ); + } + #[test] fn test_purge_slots() { let ledger_path = get_tmp_ledger_path_auto_delete!(); @@ -484,14 +703,12 @@ pub mod tests { blockstore.insert_shreds(shreds, None, false).unwrap(); blockstore.purge_slots(0, 5, PurgeType::Exact).unwrap(); - - test_all_empty_or_min(&blockstore, 6); + all_columns_empty_or_greater_than_slot(&blockstore, 6); blockstore.purge_slots(0, 50, PurgeType::Exact).unwrap(); - // min slot shouldn't matter, blockstore should be empty - test_all_empty_or_min(&blockstore, 100); - test_all_empty_or_min(&blockstore, 0); + all_columns_empty_or_greater_than_slot(&blockstore, 100); + all_columns_empty_or_greater_than_slot(&blockstore, 0); assert_eq!(blockstore.slot_meta_iterator(0).unwrap().next(), None); } From a9f758c6364326a272ba1336ca3dbb8c3e956399 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 18:00:32 +0800 Subject: [PATCH 231/576] chore(deps): bump actions/create-github-app-token from 3.1.1 to 3.2.0 (#13014) Bumps [actions/create-github-app-token](https://github.com/actions/create-github-app-token) from 3.1.1 to 3.2.0. - [Release notes](https://github.com/actions/create-github-app-token/releases) - [Changelog](https://github.com/actions/create-github-app-token/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/create-github-app-token/compare/1b10c78c7865c340bc4f6099eb2f838309f1e8c3...bcd2ba49218906704ab6c1aa796996da409d3eb1) --- updated-dependencies: - dependency-name: actions/create-github-app-token dependency-version: 3.2.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/dependabot-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependabot-pr.yml b/.github/workflows/dependabot-pr.yml index 4d6ac65a503..704e18053b9 100644 --- a/.github/workflows/dependabot-pr.yml +++ b/.github/workflows/dependabot-pr.yml @@ -12,7 +12,7 @@ jobs: if: github.event.pull_request.user.login == 'dependabot[bot]' steps: - name: Create github app token - uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 id: app-token with: client-id: ${{ secrets.CLIENT_ID }} From 4cae3c5058b2044f625fc0fc0c190e3083e989e5 Mon Sep 17 00:00:00 2001 From: Gabe Rodriguez Date: Mon, 8 Jun 2026 12:31:57 +0200 Subject: [PATCH 232/576] SIMD-0391: Feature-gate fixed-point stake math (#9637) * SIMD-0391: Feature-gate fixed-point stake warmup/cooldown math * use latest stake program * simply test cases --- cli/src/cluster_query.rs | 9 + cli/src/stake.rs | 42 ++++- feature-set/src/lib.rs | 14 ++ fetch-core-bpf.sh | 2 +- program-binaries/src/lib.rs | 2 +- .../src/programs/core_bpf_stake-4.0.0.so | Bin 202752 -> 0 bytes .../src/programs/core_bpf_stake-5.1.0.so | Bin 0 -> 212056 bytes program-test/tests/warp.rs | 3 +- runtime/src/bank.rs | 32 +++- .../partitioned_epoch_rewards/calculation.rs | 6 + .../partitioned_epoch_rewards/distribution.rs | 13 +- runtime/src/bank/tests.rs | 28 ++- runtime/src/inflation_rewards/mod.rs | 38 +++- runtime/src/inflation_rewards/points.rs | 46 ++++- runtime/src/lib.rs | 1 + runtime/src/stake_delegation.rs | 57 ++++++ runtime/src/stakes.rs | 176 ++++++++++++------ runtime/src/stakes/serde_stakes.rs | 4 +- 18 files changed, 383 insertions(+), 90 deletions(-) delete mode 100644 program-binaries/src/programs/core_bpf_stake-4.0.0.so create mode 100644 program-binaries/src/programs/core_bpf_stake-5.1.0.so create mode 100644 runtime/src/stake_delegation.rs diff --git a/cli/src/cluster_query.rs b/cli/src/cluster_query.rs index 048b60194bc..fc8175c2c4d 100644 --- a/cli/src/cluster_query.rs +++ b/cli/src/cluster_query.rs @@ -1714,6 +1714,13 @@ pub async fn process_show_stakes( &agave_feature_set::reduce_stake_warmup_cooldown::id(), ) .await?; + let fixed_point_activation_epoch = get_feature_activation_epoch( + rpc_client, + &agave_feature_set::upgrade_bpf_stake_program_to_v5_1::id(), + ) + .await?; + let use_fixed_point_stake_math = fixed_point_activation_epoch + .is_some_and(|activation_epoch| clock.epoch >= activation_epoch); stake_account_progress_bar.finish_and_clear(); let mut stake_accounts: Vec = vec![]; @@ -1738,6 +1745,7 @@ pub async fn process_show_stakes( new_rate_activation_epoch, rent_exempt_balance, false, + use_fixed_point_stake_math, ), }); } @@ -1756,6 +1764,7 @@ pub async fn process_show_stakes( new_rate_activation_epoch, rent_exempt_balance, false, + use_fixed_point_stake_math, ), }); } diff --git a/cli/src/stake.rs b/cli/src/stake.rs index 1de40fe80ba..209604dd5c3 100644 --- a/cli/src/stake.rs +++ b/cli/src/stake.rs @@ -55,7 +55,10 @@ use { error::StakeError, instruction::{self as stake_instruction, LockupArgs}, stake_history::StakeHistory, - state::{Authorized, Lockup, Meta, StakeActivationStatus, StakeAuthorize, StakeStateV2}, + state::{ + Authorized, Delegation, Lockup, Meta, StakeActivationStatus, StakeAuthorize, + StakeStateV2, + }, tools::{acceptable_reference_epoch_credits, eligible_for_deactivate_delinquent}, }, solana_system_interface::{error::SystemError, instruction as system_instruction}, @@ -2434,6 +2437,29 @@ fn u64_some_if_not_zero(n: u64) -> Option { if n > 0 { Some(n) } else { None } } +fn stake_activation_status( + delegation: &Delegation, + current_epoch: Epoch, + stake_history: &StakeHistory, + new_rate_activation_epoch: Option, + use_fixed_point_stake_math: bool, +) -> StakeActivationStatus { + if use_fixed_point_stake_math { + delegation.stake_activating_and_deactivating_v2( + current_epoch, + stake_history, + new_rate_activation_epoch, + ) + } else { + #[allow(deprecated)] + delegation.stake_activating_and_deactivating( + current_epoch, + stake_history, + new_rate_activation_epoch, + ) + } +} + pub fn build_stake_state( account_balance: u64, stake_state: &StakeStateV2, @@ -2443,6 +2469,7 @@ pub fn build_stake_state( new_rate_activation_epoch: Option, rent_exempt_reserve: u64, use_csv: bool, + use_fixed_point_stake_math: bool, ) -> CliStakeState { match stake_state { StakeStateV2::Stake( @@ -2456,15 +2483,16 @@ pub fn build_stake_state( _, ) => { let current_epoch = clock.epoch; - #[allow(deprecated)] let StakeActivationStatus { effective, activating, deactivating, - } = stake.delegation.stake_activating_and_deactivating( + } = stake_activation_status( + &stake.delegation, current_epoch, stake_history, new_rate_activation_epoch, + use_fixed_point_stake_math, ); let lockup = if lockup.is_in_force(clock, None) { Some(lockup.into()) @@ -2733,6 +2761,13 @@ pub async fn get_account_stake_state( &agave_feature_set::reduce_stake_warmup_cooldown::id(), ) .await?; + let fixed_point_activation_epoch = get_feature_activation_epoch( + rpc_client, + &agave_feature_set::upgrade_bpf_stake_program_to_v5_1::id(), + ) + .await?; + let use_fixed_point_stake_math = fixed_point_activation_epoch + .is_some_and(|activation_epoch| clock.epoch >= activation_epoch); let rent_exempt_balance = rpc_client .get_minimum_balance_for_rent_exemption(stake_account.data.len()) .await?; @@ -2745,6 +2780,7 @@ pub async fn get_account_stake_state( new_rate_activation_epoch, rent_exempt_balance, use_csv, + use_fixed_point_stake_math, ); if state.stake_type == CliStakeType::Stake && state.activation_epoch.is_some() { diff --git a/feature-set/src/lib.rs b/feature-set/src/lib.rs index 1a3ae6bb64f..bf50f378d75 100644 --- a/feature-set/src/lib.rs +++ b/feature-set/src/lib.rs @@ -87,6 +87,7 @@ pub struct FeatureSnapshot { pub enable_tx_v1: bool, pub define_ltds_fee_only_semantics: bool, pub validate_chained_block_id_2: bool, + pub upgrade_bpf_stake_program_to_v5_1: bool, } impl From<&AHashMap> for FeatureSnapshot { @@ -200,6 +201,7 @@ impl From<&AHashMap> for FeatureSnapshot { enable_tx_v1: is_active(&enable_tx_v1::ID), define_ltds_fee_only_semantics: is_active(&define_ltds_fee_only_semantics::ID), validate_chained_block_id_2: is_active(&validate_chained_block_id_2::ID), + upgrade_bpf_stake_program_to_v5_1: is_active(&upgrade_bpf_stake_program_to_v5_1::ID), } } } @@ -1545,6 +1547,14 @@ pub mod reduce_slot_time_to_200ms { solana_pubkey::declare_id!("iBRLypKvvj9VEvwoTeRpbLhbW55NFR4T3GE9BUR8A16"); } +pub mod upgrade_bpf_stake_program_to_v5_1 { + solana_pubkey::declare_id!("s51VGwCAgebo2745DSUris72RavoLkXGUmVJosESCXr"); + + pub mod buffer { + solana_pubkey::declare_id!("p51x11QCYMHwuVS1MBcLHKb3MezWyqGS5BEB41CA1dk"); + } +} + pub static FEATURE_NAMES: LazyLock> = LazyLock::new(|| { [ (secp256k1_program_enabled::id(), "secp256k1 program"), @@ -2623,6 +2633,10 @@ pub static FEATURE_NAMES: LazyLock> = LazyLock::n validate_chained_block_id_2::id(), "SIMD-340: Encompassing check for validate chained block ID", ), + ( + upgrade_bpf_stake_program_to_v5_1::id(), + "SIMD-0391: Upgrade BPF Stake Program to v5.1.0 (fixed-point warmup/cooldown)", + ), /*************** ADD NEW FEATURES HERE ***************/ /***** ADD NEW FEATURE BOOL TO `FeatureSnapshot` *****/ ] diff --git a/fetch-core-bpf.sh b/fetch-core-bpf.sh index 02a294af278..a4300b25872 100755 --- a/fetch-core-bpf.sh +++ b/fetch-core-bpf.sh @@ -29,6 +29,6 @@ add_core_bpf_program_to_fetch() { add_core_bpf_program_to_fetch address-lookup-table 3.0.0 AddressLookupTab1e1111111111111111111111111 BPFLoaderUpgradeab1e11111111111111111111111 add_core_bpf_program_to_fetch config 3.0.0 Config1111111111111111111111111111111111111 BPFLoaderUpgradeab1e11111111111111111111111 add_core_bpf_program_to_fetch feature-gate 0.0.1 Feature111111111111111111111111111111111111 BPFLoaderUpgradeab1e11111111111111111111111 -add_core_bpf_program_to_fetch stake 4.0.0 Stake11111111111111111111111111111111111111 BPFLoaderUpgradeab1e11111111111111111111111 +add_core_bpf_program_to_fetch stake 5.1.0 Stake11111111111111111111111111111111111111 BPFLoaderUpgradeab1e11111111111111111111111 fetch_programs "$PREFIX" "${programs[@]}" diff --git a/program-binaries/src/lib.rs b/program-binaries/src/lib.rs index 24f163aaddc..bc3fdfe16fc 100644 --- a/program-binaries/src/lib.rs +++ b/program-binaries/src/lib.rs @@ -75,7 +75,7 @@ static CORE_BPF_PROGRAMS: &[(Pubkey, Option, &[u8])] = &[ ( solana_sdk_ids::stake::ID, None, - include_bytes!("programs/core_bpf_stake-4.0.0.so"), + include_bytes!("programs/core_bpf_stake-5.1.0.so"), ), // Add more programs here post-migration... ]; diff --git a/program-binaries/src/programs/core_bpf_stake-4.0.0.so b/program-binaries/src/programs/core_bpf_stake-4.0.0.so deleted file mode 100644 index 22e5ff9cf8e50c64b05c3f50a716d5635f64b245..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 202752 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 m5^J%C(>^{|NjMF>G(eY diff --git a/program-binaries/src/programs/core_bpf_stake-5.1.0.so b/program-binaries/src/programs/core_bpf_stake-5.1.0.so new file mode 100644 index 0000000000000000000000000000000000000000..3c66f8c8d98f04a97cd6fbc8edb3aa634d3734a8 GIT binary patch literal 212056 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 z z+B1xCx*qA|Tl&#}#)r9E1c2XzX@+uUkNW(EeEdQ3x6QA)e#Lqi-+=fq=#}#EjOV1D z*uGa@u@4`Y_&)nyFW*&U0m2cVg8`i1ulM^YwTLH$-H!0}BDXV=b;tqbi=IoAzMZ#g zKlwa7Hao67^dC}h=dz2WzVgubEvy+h-(`Jqi|IRCmu!23>hEYs`68@V_@npe!0PJ_ zh4%^Afc-!?-9A6;)cCf0bX-rqPomSXU`l@4spaaIssBXH8-$<6Zq)GD`_-qNM2yBP zzkdg0%AJ<9gZJ$M({WfL;=M z#x`FX3$@;kO*7<2T0e4~fpTz^#;iH=({{cW)~|e2Sx;B~iH22gMT6cy8t?lWzCJN> zleNd%@ArhA?|5&TRaW@%IepLF-j$CAk7_%T^I^)bes3Y!|4Pn>DLlVdnYVSs$mBLI zD_^=ic>nvlLN=eTw>lo3vq)XYB$=w>4tjk%s#I_IK3dj(bbQm})&F=tbzcc^g?}DY z;9QqJnCX|lL>ER2v>kDa(kHrtrNDO|)^a0z6hY@_-@mHp!)c}ujlK^mJo14zjoje+ zFdvi!Fx~}|{_t~9$vL=|BNCtJ!A8Zy^)Tg`82UN;z8T6deKS<=#VhZV@?rIxrJ(CK zzmMeokhH_}?{L)`rPrX=$9g^we)e?|x8rLut}I3t?{9_|B0Le^3We8c@IVAvC$s(- zd4th)sir%<>#e=R)k}gRlA>cukFhnHuW+&Ykq(1_pPTS=Y~8bfI9!j7Z+onLdt3Bg ze)$qrP~}-b2re43e(ThJ>umXbp&#D06~A-+XT^_PZiWtMdLtGuS$>ux6#IS7i)?+# z@J#E6zX3hd>xBIb9}_SE=ii@U_vnpvAD19LUafEo%4W&= zV9FIa)YrWec{DkoK3j}*(q|FYrMzER5AkyQqNf1?xQEnB^w<8XoCFlBnm*0=2Jw{5=E)qNXK?LCH4UW@i zxazBdS7KMaPxColFQoVV9QSbbmlV$JTE1|%`jjuW+sBKM>#hIRSUnm3_U}&**L+^! z#ao|KIt?qox;^RZqS3Vm$LvubSE5TazHsmO^k`V&C45if!BKDXXTm4ln&9-uyioAy z2z9(j>qqWS(SA6SvRAx+;`NV9Ir9B>oX3fdX*lXvc#aR|-;&SmYCnhJ?^*eIsTlRp z#jBNn;vL2>bsCNapB6Z64WAG{IbZgGmK%vat@U(kI3N6;glQK_KONS1+Fy^S+C%(* z19Kt&E}ws=aBQu?U1t5-s$tjj^&2((1fWxIjd79-=laF-jqJ4Y+a)=0zN+Z|8r6HI zrzh%v?U!i&`0{?fHnRCg0sKf8U86pm$exau@-O^V=HEzaRP=Xeg9I!3d%ot2iki>Y zi$~h69Yz;lS9LlJ<8`1SKt=omSJj%?s zmGgPVr?ZR5MLAP_8ryvWpC81@eFLrE&*AyJWy1dIYyk)S&j=s)+4;l18K*4#xcO=~ z`F-TR87e20d6?=2r^9|5f70!DJ1E`${nIvzeXs-LCFzj712U+1(S8uS(fWbo#C9Z+ zj?D`y_JH?GW`5P~`~Ukfj?m;Fen+_vKVa}dbn2fA_Y0+ubXPr5p}Y4Bu}}Al@jp8i zE=zaT_sifx=c~xhHHFomkrL6&qLiZ^xnoz)J_dHZ`?elmH#xaXl7lE*nB;r+Z98S~ zFYmu?r+@$Oq|NW04#_yRM$3=cc+(61e?6kQ&`etT8?Li-(>EvYwD3;#nVFkH{{#}C4uCD0kPAYklb4ZbH`voVB0M!3IyKa}=kR3aB-F~u3;kf?pMS2lY zU7!d34UPB%myBn?N5Y%bXa2?b*##Q2>J1Lh+I4cX<||lxT#ueSsPWl#l(CyM{^W

0^iqWexdING#~drg}zs4 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 bool { + self.feature_set + .snapshot() + .upgrade_bpf_stake_program_to_v5_1 + } + /// Get cached vote account state from the past few epochs so that some vote /// state configuration changes are delayed before being used in reward /// calculation. @@ -1729,7 +1735,8 @@ impl Bank { self.epoch(), thread_pool, self.new_warmup_cooldown_rate_epoch(), - &stake_delegations + &stake_delegations, + self.use_fixed_point_stake_math(), )); // Apply stake rewards and commission using the distribution vote-account @@ -3132,6 +3139,7 @@ impl Bank { self.stakes_cache = StakesCache::new(Stakes::new_from_accounts_for_genesis( self.new_warmup_cooldown_rate_epoch(), genesis_config.accounts.iter(), + self.use_fixed_point_stake_math(), )); // After storing genesis accounts, the bank stakes cache will be warmed @@ -4642,12 +4650,15 @@ impl Bank { assert!(!self.freeze_started()); let mut m = Measure::start("stakes_cache.check_and_store"); let new_warmup_cooldown_rate_epoch = self.new_warmup_cooldown_rate_epoch(); + let use_fixed_point_stake_math = self.use_fixed_point_stake_math(); + (0..accounts.len()).for_each(|i| { accounts.account(i, |account| { self.stakes_cache.check_and_store( account.pubkey(), &account, new_warmup_cooldown_rate_epoch, + use_fixed_point_stake_math, ) }) }); @@ -5660,6 +5671,7 @@ impl Bank { ) { debug_assert_eq!(txs.len(), processing_results.len()); let new_warmup_cooldown_rate_epoch = self.new_warmup_cooldown_rate_epoch(); + let use_fixed_point_stake_math = self.use_fixed_point_stake_math(); txs.iter() .zip(processing_results) .filter_map(|(tx, processing_result)| { @@ -5681,8 +5693,12 @@ impl Bank { .for_each(|(pubkey, account)| { // note that this could get timed to: self.rc.accounts.accounts_db.stats.stakes_cache_check_and_store_us, // but this code path is captured separately in ExecuteTimingType::UpdateStakesCacheUs - self.stakes_cache - .check_and_store(pubkey, account, new_warmup_cooldown_rate_epoch); + self.stakes_cache.check_and_store( + pubkey, + account, + new_warmup_cooldown_rate_epoch, + use_fixed_point_stake_math, + ); }); } @@ -6129,6 +6145,16 @@ impl Bank { error!("Failed to upgrade Core BPF Stake program: {e}"); } } + + if new_feature_activations.contains(&feature_set::upgrade_bpf_stake_program_to_v5_1::id()) { + if let Err(e) = self.upgrade_core_bpf_program( + &solana_sdk_ids::stake::id(), + &feature_set::upgrade_bpf_stake_program_to_v5_1::buffer::id(), + "upgrade_stake_program_to_v5_1", + ) { + error!("Failed to upgrade Core BPF Stake program: {e}"); + } + } } fn apply_new_builtin_program_feature_transitions( diff --git a/runtime/src/bank/partitioned_epoch_rewards/calculation.rs b/runtime/src/bank/partitioned_epoch_rewards/calculation.rs index d1029a7acbd..5c77851c412 100644 --- a/runtime/src/bank/partitioned_epoch_rewards/calculation.rs +++ b/runtime/src/bank/partitioned_epoch_rewards/calculation.rs @@ -523,6 +523,7 @@ impl Bank { adjust_delegations_for_rent: bool, ag_epoch_type: &AlpenglowEpochType, custom_commission_collector: bool, + use_fixed_point_stake_math: bool, ) -> Option { // curry closure to add the contextual stake_pubkey let reward_calc_tracer = reward_calc_tracer.as_ref().map(|outer| { @@ -627,6 +628,7 @@ impl Bank { new_rate_activation_epoch, commission_rate_in_basis_points, adjust_delegations_for_rent, + use_fixed_point_stake_math, }, reward_calc_tracer, ag_epoch_type, @@ -685,6 +687,7 @@ impl Bank { ) -> (RewardCommissions, StakeRewardCalculation) { let new_warmup_cooldown_rate_epoch = self.new_warmup_cooldown_rate_epoch(); let feature_snapshot = self.feature_set.snapshot(); + let use_fixed_point_stake_math = feature_snapshot.upgrade_bpf_stake_program_to_v5_1; let delay_commission_updates = feature_snapshot.delay_commission_updates; let commission_rate_in_basis_points = feature_snapshot.commission_rate_in_basis_points; // Name intentionally doesn't match -- "adjust delegations for rent" is @@ -723,6 +726,7 @@ impl Bank { adjust_delegations_for_rent, ag_epoch_type, custom_commission_collector, + use_fixed_point_stake_math, ); let (stake_reward, maybe_reward_record) = match maybe_reward_record { @@ -826,6 +830,7 @@ impl Bank { } } + let use_fixed_point_stake_math = self.use_fixed_point_stake_math(); let (points, measure_us) = measure_us!(thread_pool.install(|| { stake_delegations .par_iter() @@ -846,6 +851,7 @@ impl Bank { stake_history, new_warmup_cooldown_rate_epoch, &self.epoch_stakes, + use_fixed_point_stake_math, ) .unwrap_or(0) }) diff --git a/runtime/src/bank/partitioned_epoch_rewards/distribution.rs b/runtime/src/bank/partitioned_epoch_rewards/distribution.rs index ec4d1b31ffe..005a6613645 100644 --- a/runtime/src/bank/partitioned_epoch_rewards/distribution.rs +++ b/runtime/src/bank/partitioned_epoch_rewards/distribution.rs @@ -9,6 +9,7 @@ use { partitioned_epoch_rewards::EpochRewardPhase, }, stake_account::StakeAccount, + stake_delegation::delegation_effective_stake, }, log::error, serde::{Deserialize, Serialize}, @@ -202,6 +203,7 @@ impl Bank { partitioned_stake_reward: &PartitionedStakeReward, rent: &Rent, adjust_delegations_for_rent: bool, + use_fixed_point_stake_math: bool, ) -> Result { let stake_account = stakes_cache_accounts .get(&partitioned_stake_reward.stake_pubkey) @@ -246,11 +248,12 @@ impl Bank { )) .map_err(|_| DistributionError::UnableToSetState)?; - #[allow(deprecated)] - let stake_at_distribution_epoch = partitioned_stake_reward.stake.delegation.stake( + let stake_at_distribution_epoch = delegation_effective_stake( + &partitioned_stake_reward.stake.delegation, distribution_epoch, stake_history, new_warmup_cooldown_rate_epoch, + use_fixed_point_stake_math, ); let reward_type = if stake_at_distribution_epoch == 0 { RewardType::DeactivatedStake @@ -287,6 +290,7 @@ impl Bank { // Name intentionally doesn't match -- "adjust delegations for rent" is // part of relaxing post-exec min balance checks. let adjust_delegations_for_rent = feature_snapshot.relax_post_exec_min_balance_check; + let use_fixed_point_stake_math = feature_snapshot.upgrade_bpf_stake_program_to_v5_1; let mut lamports_distributed = 0; let mut lamports_burned = 0; @@ -329,6 +333,7 @@ impl Bank { partitioned_stake_reward, rent, adjust_delegations_for_rent, + use_fixed_point_stake_math, ) { Ok(stake_reward) => { lamports_distributed += reward_amount; @@ -725,6 +730,7 @@ mod tests { &partitioned_stake_reward, &rent, adjust_delegations_for_rent, + true, ) .unwrap_err(), DistributionError::AccountNotFound @@ -762,6 +768,7 @@ mod tests { &partitioned_stake_reward, &rent, adjust_delegations_for_rent, + true, ) .unwrap_err(), DistributionError::ArithmeticOverflow @@ -833,6 +840,7 @@ mod tests { &partitioned_stake_reward, &rent, adjust_delegations_for_rent, + true, ) .unwrap(), expected_stake_reward @@ -914,6 +922,7 @@ mod tests { &partitioned_stake_reward, &rent, adjust_delegations_for_rent, + true, ) .unwrap(), expected_stake_reward diff --git a/runtime/src/bank/tests.rs b/runtime/src/bank/tests.rs index 4651ddbab17..a44016ecf9e 100644 --- a/runtime/src/bank/tests.rs +++ b/runtime/src/bank/tests.rs @@ -24,6 +24,7 @@ use { SLOT_PARAMS_200MS, SLOT_PARAMS_250MS, SLOT_PARAMS_300MS, SLOT_PARAMS_350MS, SlotParams, slot_time_feature_gates, slot_time_feature_ids, }, + stake_delegation::effective_stake, stake_history::StakeHistory, stake_utils, stakes::{DeserializableStakes, InvalidCacheEntryReason, SerdeStakesToStakeFormat, Stakes}, @@ -2932,8 +2933,13 @@ fn test_bank_epoch_vote_accounts() { // epoch_stakes are a snapshot at the leader_schedule_slot_offset boundary // in the prior epoch (0 in this case) - #[allow(deprecated)] - let expected_stake = leader_stake.stake(0, &StakeHistory::default(), None); + let expected_stake = effective_stake( + &leader_stake, + 0, + &StakeHistory::default(), + None, + parent.use_fixed_point_stake_math(), + ); assert_eq!( expected_stake, vote_accounts.unwrap().get(&leader_vote_account).unwrap().0 @@ -2950,8 +2956,13 @@ fn test_bank_epoch_vote_accounts() { ); assert!(child.epoch_vote_accounts(epoch).is_some()); - #[allow(deprecated)] - let expected_stake = leader_stake.stake(child.epoch(), &StakeHistory::default(), None); + let expected_stake = effective_stake( + &leader_stake, + child.epoch(), + &StakeHistory::default(), + None, + child.use_fixed_point_stake_math(), + ); assert_eq!( expected_stake, child @@ -2970,8 +2981,13 @@ fn test_bank_epoch_vote_accounts() { SLOTS_PER_EPOCH - (LEADER_SCHEDULE_SLOT_OFFSET % SLOTS_PER_EPOCH) + 1, ); assert!(child.epoch_vote_accounts(epoch).is_some()); - #[allow(deprecated)] - let expected_stake = leader_stake.stake(child.epoch(), &StakeHistory::default(), None); + let expected_stake = effective_stake( + &leader_stake, + child.epoch(), + &StakeHistory::default(), + None, + child.use_fixed_point_stake_math(), + ); assert_eq!( expected_stake, child diff --git a/runtime/src/inflation_rewards/mod.rs b/runtime/src/inflation_rewards/mod.rs index 91db3995940..6151b55d861 100644 --- a/runtime/src/inflation_rewards/mod.rs +++ b/runtime/src/inflation_rewards/mod.rs @@ -6,7 +6,10 @@ use { CalculatedStakePoints, CalculationEnvironment, DelegatedVoteState, InflationPointCalculationEvent, SkippedReason, calculate_stake_points_and_credits, }, - crate::{alpenglow_epoch_type::AlpenglowEpochType, epoch_stakes::VersionedEpochStakes}, + crate::{ + alpenglow_epoch_type::AlpenglowEpochType, epoch_stakes::VersionedEpochStakes, + stake_delegation::effective_stake, + }, solana_clock::Epoch, solana_instruction::error::InstructionError, solana_stake_interface::{ @@ -48,11 +51,16 @@ pub(crate) fn redeem_rewards<'a>( stake_history, new_rate_activation_epoch, commission_rate_in_basis_points, + use_fixed_point_stake_math, .. } = calculation_environment; - #[allow(deprecated)] - let effective_stake_at_rewarded_epoch = - stake.stake(rewarded_epoch, stake_history, new_rate_activation_epoch); + let effective_stake_at_rewarded_epoch = effective_stake( + &stake, + rewarded_epoch, + stake_history, + new_rate_activation_epoch, + use_fixed_point_stake_math, + ); inflation_point_calc_tracer( &InflationPointCalculationEvent::EffectiveStakeAtRewardedEpoch( effective_stake_at_rewarded_epoch, @@ -209,6 +217,7 @@ fn calculate_stake_rewards<'a>( new_rate_activation_epoch, point_value, rewarded_epoch, + use_fixed_point_stake_math, .. } = calculation_environment; @@ -226,6 +235,7 @@ fn calculate_stake_rewards<'a>( new_rate_activation_epoch, ag_epoch_type, epoch_stakes, + use_fixed_point_stake_math, ); // Drive credits_observed forward unconditionally when rewards are disabled @@ -508,6 +518,7 @@ mod tests { new_rate_activation_epoch, commission_rate_in_basis_points, adjust_delegations_for_rent, + use_fixed_point_stake_math: true, }, null_tracer(), &ag_epoch_type, @@ -537,6 +548,7 @@ mod tests { new_rate_activation_epoch, commission_rate_in_basis_points, adjust_delegations_for_rent, + use_fixed_point_stake_math: true, }, null_tracer(), &ag_epoch_type, @@ -600,6 +612,7 @@ mod tests { new_rate_activation_epoch, commission_rate_in_basis_points, adjust_delegations_for_rent, + use_fixed_point_stake_math: true, }, null_tracer(), &ag_epoch_type, @@ -631,6 +644,7 @@ mod tests { new_rate_activation_epoch, commission_rate_in_basis_points, adjust_delegations_for_rent, + use_fixed_point_stake_math: true, }, null_tracer(), &ag_epoch_type, @@ -660,6 +674,7 @@ mod tests { new_rate_activation_epoch, commission_rate_in_basis_points, adjust_delegations_for_rent, + use_fixed_point_stake_math: true, }, null_tracer(), &ag_epoch_type, @@ -692,6 +707,7 @@ mod tests { new_rate_activation_epoch, commission_rate_in_basis_points, adjust_delegations_for_rent, + use_fixed_point_stake_math: true, }, null_tracer(), &ag_epoch_type, @@ -722,6 +738,7 @@ mod tests { new_rate_activation_epoch, commission_rate_in_basis_points, adjust_delegations_for_rent, + use_fixed_point_stake_math: true, }, null_tracer(), &ag_epoch_type, @@ -754,6 +771,7 @@ mod tests { new_rate_activation_epoch, commission_rate_in_basis_points, adjust_delegations_for_rent, + use_fixed_point_stake_math: true, }, null_tracer(), &ag_epoch_type, @@ -780,6 +798,7 @@ mod tests { new_rate_activation_epoch, commission_rate_in_basis_points, adjust_delegations_for_rent, + use_fixed_point_stake_math: true, }, null_tracer(), &ag_epoch_type, @@ -803,6 +822,7 @@ mod tests { new_rate_activation_epoch, commission_rate_in_basis_points, adjust_delegations_for_rent, + use_fixed_point_stake_math: true, }, null_tracer(), &ag_epoch_type, @@ -833,6 +853,7 @@ mod tests { new_rate_activation_epoch, commission_rate_in_basis_points, adjust_delegations_for_rent, + use_fixed_point_stake_math: true, }, null_tracer(), &ag_epoch_type, @@ -863,6 +884,7 @@ mod tests { new_rate_activation_epoch, commission_rate_in_basis_points, adjust_delegations_for_rent, + use_fixed_point_stake_math: true, }, null_tracer(), &ag_epoch_type, @@ -885,6 +907,7 @@ mod tests { None, &ag_epoch_type, &epoch_stakes, + true, ) ); @@ -907,6 +930,7 @@ mod tests { None, &ag_epoch_type, &epoch_stakes, + true, ) ); // this is new behavior 2; don't hint when credits both from stake and vote are identical @@ -926,6 +950,7 @@ mod tests { None, &ag_epoch_type, &epoch_stakes, + true, ) ); @@ -953,6 +978,7 @@ mod tests { new_rate_activation_epoch, commission_rate_in_basis_points, adjust_delegations_for_rent, + use_fixed_point_stake_math: true, }, null_tracer(), &ag_epoch_type, @@ -984,6 +1010,7 @@ mod tests { new_rate_activation_epoch, commission_rate_in_basis_points, adjust_delegations_for_rent, + use_fixed_point_stake_math: true, }, null_tracer(), &ag_epoch_type, @@ -1023,6 +1050,7 @@ mod tests { new_rate_activation_epoch, commission_rate_in_basis_points, adjust_delegations_for_rent, + use_fixed_point_stake_math: true, }, null_tracer(), &AlpenglowEpochType::Tower, @@ -1283,6 +1311,7 @@ mod tests { new_rate_activation_epoch, commission_rate_in_basis_points, adjust_delegations_for_rent, + use_fixed_point_stake_math: true, }, null_tracer(), &AlpenglowEpochType::Tower, @@ -1331,6 +1360,7 @@ mod tests { new_rate_activation_epoch, commission_rate_in_basis_points, adjust_delegations_for_rent, + use_fixed_point_stake_math: true, }, null_tracer(), &ag_stake_state, diff --git a/runtime/src/inflation_rewards/points.rs b/runtime/src/inflation_rewards/points.rs index b8359e4146e..5877197f17b 100644 --- a/runtime/src/inflation_rewards/points.rs +++ b/runtime/src/inflation_rewards/points.rs @@ -1,7 +1,10 @@ //! Information about points calculation based on stake state. use { - crate::{alpenglow_epoch_type::AlpenglowEpochType, epoch_stakes::VersionedEpochStakes}, + crate::{ + alpenglow_epoch_type::AlpenglowEpochType, epoch_stakes::VersionedEpochStakes, + stake_delegation::delegation_effective_stake, + }, agave_votor_messages::migration::AG_MIGRATION_EPOCH_CREDIT, log::{error, trace}, solana_clock::Epoch, @@ -41,6 +44,7 @@ pub(crate) struct CalculationEnvironment<'a> { pub(crate) new_rate_activation_epoch: Option, pub(crate) commission_rate_in_basis_points: bool, pub(crate) adjust_delegations_for_rent: bool, + pub(crate) use_fixed_point_stake_math: bool, } #[derive(Debug)] @@ -104,6 +108,7 @@ pub(crate) fn calculate_points_for_tower( stake_history: &StakeHistory, new_rate_activation_epoch: Option, epoch_stakes: &HashMap, + use_fixed_point_stake_math: bool, ) -> Result { if let StakeStateV2::Stake(_meta, stake, _stake_flags) = stake_state { Ok(calculate_stake_points_for_tower( @@ -113,6 +118,7 @@ pub(crate) fn calculate_points_for_tower( null_tracer(), new_rate_activation_epoch, epoch_stakes, + use_fixed_point_stake_math, )) } else { Err(InstructionError::InvalidAccountData) @@ -128,6 +134,7 @@ fn calculate_stake_points_for_tower( inflation_point_calc_tracer: Option, new_rate_activation_epoch: Option, epoch_stakes: &HashMap, + use_fixed_point_stake_math: bool, ) -> u128 { calculate_stake_points_and_credits( stake, @@ -137,6 +144,7 @@ fn calculate_stake_points_for_tower( new_rate_activation_epoch, &AlpenglowEpochType::Tower, epoch_stakes, + use_fixed_point_stake_math, ) .tower_points } @@ -226,6 +234,7 @@ fn tower_epoch_credits_iter( stake_history: &StakeHistory, inflation_point_calc_tracer: Option, new_rate_activation_epoch: Option, + use_fixed_point_stake_math: bool, ) -> (u128, u64, bool) { let mut points = 0; let credits_in_stake = stake.credits_observed; @@ -244,11 +253,12 @@ fn tower_epoch_credits_iter( initial_epoch_credits, &mut new_credits_observed, ); - #[allow(deprecated)] - let stake_amount = u128::from(stake.delegation.stake( + let stake_amount = u128::from(delegation_effective_stake( + &stake.delegation, epoch, stake_history, new_rate_activation_epoch, + use_fixed_point_stake_math, )); // finally calculate points for this epoch @@ -276,6 +286,7 @@ fn ag_epoch_credits_iter( inflation_point_calc_tracer: Option, new_rate_activation_epoch: Option, epoch_stakes: &HashMap, + use_fixed_point_stake_math: bool, ) -> Result<(u128, u64), CalculatedStakePoints> { let mut points = 0; let credits_in_stake = stake.credits_observed; @@ -301,11 +312,12 @@ fn ag_epoch_credits_iter( initial_epoch_credits, &mut new_credits_observed, ); - #[allow(deprecated)] - let stake_amount = u128::from(stake.delegation.stake( + let stake_amount = u128::from(delegation_effective_stake( + &stake.delegation, epoch, stake_history, new_rate_activation_epoch, + use_fixed_point_stake_math, )); let earned_points = { @@ -346,6 +358,7 @@ fn migrating_epoch_credits_iter( inflation_point_calc_tracer: Option, new_rate_activation_epoch: Option, epoch_stakes: &HashMap, + use_fixed_point_stake_math: bool, ) -> Result<(u128, u128, u64), CalculatedStakePoints> { let (tower_points, tower_new_credits_observed, saw_marker) = tower_epoch_credits_iter( stake, @@ -353,6 +366,7 @@ fn migrating_epoch_credits_iter( stake_history, inflation_point_calc_tracer.as_ref(), new_rate_activation_epoch, + use_fixed_point_stake_math, ); let (ag_points, ag_new_credits_observed) = ag_epoch_credits_iter( migration_epoch, @@ -363,6 +377,7 @@ fn migrating_epoch_credits_iter( inflation_point_calc_tracer, new_rate_activation_epoch, epoch_stakes, + use_fixed_point_stake_math, )?; let new_credits_observed = tower_new_credits_observed.max(ag_new_credits_observed); @@ -380,6 +395,7 @@ pub(crate) fn calculate_stake_points_and_credits( new_rate_activation_epoch: Option, ag_epoch_type: &AlpenglowEpochType, epoch_stakes: &HashMap, + use_fixed_point_stake_math: bool, ) -> CalculatedStakePoints { let credits_in_stake = stake.credits_observed; let credits_in_vote = vote_state.credits; @@ -437,6 +453,7 @@ pub(crate) fn calculate_stake_points_and_credits( stake_history, inflation_point_calc_tracer, new_rate_activation_epoch, + use_fixed_point_stake_math, ); (points, 0, credits) } @@ -451,6 +468,7 @@ pub(crate) fn calculate_stake_points_and_credits( inflation_point_calc_tracer, new_rate_activation_epoch, epoch_stakes, + use_fixed_point_stake_math, ) { Ok(r) => r, Err(e) => return e, @@ -466,6 +484,7 @@ pub(crate) fn calculate_stake_points_and_credits( inflation_point_calc_tracer, new_rate_activation_epoch, epoch_stakes, + use_fixed_point_stake_math, ) { Ok(r) => r, Err(e) => return e, @@ -540,7 +559,8 @@ mod tests { &StakeHistory::default(), null_tracer(), None, - &HashMap::new() + &HashMap::new(), + true, ) ); } @@ -565,6 +585,7 @@ mod tests { &StakeHistory::default(), null_tracer(), None, + true, ); assert_eq!(points, credits as u128 * stake_lamports as u128 * 2); assert_eq!(new_credits, credits * 2); @@ -583,6 +604,7 @@ mod tests { &StakeHistory::default(), null_tracer(), None, + true, ); assert_eq!(points, credits as u128 * stake_lamports as u128 * 2); assert_eq!(new_credits, credits * 2); @@ -602,6 +624,7 @@ mod tests { &StakeHistory::default(), null_tracer(), None, + true, ); assert_eq!(points, credits as u128 * stake_lamports as u128 * 2); assert_eq!(new_credits, credits * 2); @@ -645,6 +668,7 @@ mod tests { null_tracer(), None, &epoch_stakes, + true, ) .unwrap(); assert_eq!(points, 0); @@ -662,6 +686,7 @@ mod tests { null_tracer(), None, &epoch_stakes, + true, ) .unwrap(); if saw_marker { @@ -693,6 +718,7 @@ mod tests { null_tracer(), None, &epoch_stakes, + true, ) .unwrap(); assert_eq!( @@ -712,6 +738,7 @@ mod tests { null_tracer(), None, &epoch_stakes, + true, ) .unwrap(); assert_eq!( @@ -753,6 +780,7 @@ mod tests { null_tracer(), None, &epoch_stakes, + true, ) .unwrap(); assert_eq!(tower_points, credits as u128 * stake_lamports as u128 * 2); @@ -772,6 +800,7 @@ mod tests { null_tracer(), None, &epoch_stakes, + true, ) .unwrap(); assert_eq!(tower_points, credits as u128 * stake_lamports as u128); @@ -790,6 +819,7 @@ mod tests { null_tracer(), None, &epoch_stakes, + true, ) .unwrap(); assert_eq!(tower_points, 0); @@ -859,6 +889,7 @@ mod tests { null_tracer(), None, &epoch_stakes, + true, ) .unwrap(); assert_eq!(new_credits, credits * 4); @@ -893,6 +924,7 @@ mod tests { &StakeHistory::default(), null_tracer(), None, + true, ); assert_eq!(points, credits as u128 * stake_lamports as u128); assert_eq!(new_credits, credits * 2); @@ -938,6 +970,7 @@ mod tests { null_tracer(), None, &epoch_stakes, + true, ) .unwrap(); assert_eq!( @@ -984,6 +1017,7 @@ mod tests { null_tracer(), None, &epoch_stakes, + true, ) .unwrap(); if activation_epoch == 0 { diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 2bfa1afd769..d1d02ed6258 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -40,6 +40,7 @@ pub mod snapshot_utils; mod stake_account; #[cfg(feature = "dev-context-only-utils")] pub mod stake_account; +mod stake_delegation; pub mod stake_history; pub mod stake_utils; pub mod stake_weighted_timestamp; diff --git a/runtime/src/stake_delegation.rs b/runtime/src/stake_delegation.rs new file mode 100644 index 00000000000..a9ed035e677 --- /dev/null +++ b/runtime/src/stake_delegation.rs @@ -0,0 +1,57 @@ +//! Dispatch helpers for stake delegation warmup/cooldown math. + +use { + solana_clock::Epoch, + solana_stake_interface::{ + stake_history::StakeHistoryGetEntry, + state::{Delegation, Stake, StakeActivationStatus}, + }, +}; + +#[inline] +pub(crate) fn delegation_effective_stake( + delegation: &Delegation, + epoch: Epoch, + history: &T, + new_rate_activation_epoch: Option, + use_fixed_point_stake_math: bool, +) -> u64 { + if use_fixed_point_stake_math { + delegation.stake_v2(epoch, history, new_rate_activation_epoch) + } else { + #[allow(deprecated)] + delegation.stake(epoch, history, new_rate_activation_epoch) + } +} + +#[inline] +pub(crate) fn delegation_activation_status( + delegation: &Delegation, + epoch: Epoch, + history: &T, + new_rate_activation_epoch: Option, + use_fixed_point_stake_math: bool, +) -> StakeActivationStatus { + if use_fixed_point_stake_math { + delegation.stake_activating_and_deactivating_v2(epoch, history, new_rate_activation_epoch) + } else { + #[allow(deprecated)] + delegation.stake_activating_and_deactivating(epoch, history, new_rate_activation_epoch) + } +} + +#[inline] +pub(crate) fn effective_stake( + stake: &Stake, + epoch: Epoch, + history: &T, + new_rate_activation_epoch: Option, + use_fixed_point_stake_math: bool, +) -> u64 { + if use_fixed_point_stake_math { + stake.stake_v2(epoch, history, new_rate_activation_epoch) + } else { + #[allow(deprecated)] + stake.stake(epoch, history, new_rate_activation_epoch) + } +} diff --git a/runtime/src/stakes.rs b/runtime/src/stakes.rs index 09c60dfb84d..4f0bf641ac1 100644 --- a/runtime/src/stakes.rs +++ b/runtime/src/stakes.rs @@ -1,7 +1,11 @@ //! Stakes serve as a cache of stake and vote accounts to derive //! node stakes use { - crate::{stake_account, stake_history::StakeHistory}, + crate::{ + stake_account, + stake_delegation::{delegation_activation_status, delegation_effective_stake}, + stake_history::StakeHistory, + }, imbl::HashMap as ImblHashMap, log::error, num_derive::ToPrimitive, @@ -81,6 +85,7 @@ impl StakesCache { pubkey: &Pubkey, account: &impl ReadableAccount, new_rate_activation_epoch: Option, + use_fixed_point_stake_math: bool, ) { // TODO: If the account is already cached as a vote or stake account // but the owner changes, then this needs to evict the account from @@ -97,7 +102,11 @@ impl StakesCache { }; } else if stake_program::check_id(owner) { let mut stakes = self.0.write().unwrap(); - stakes.remove_stake_delegation(pubkey, new_rate_activation_epoch); + stakes.remove_stake_delegation( + pubkey, + new_rate_activation_epoch, + use_fixed_point_stake_math, + ); } return; } @@ -113,6 +122,7 @@ impl StakesCache { pubkey, vote_account, new_rate_activation_epoch, + use_fixed_point_stake_math, ) }; } @@ -139,11 +149,16 @@ impl StakesCache { *pubkey, stake_account, new_rate_activation_epoch, + use_fixed_point_stake_math, ); } Err(_) => { let mut stakes = self.0.write().unwrap(); - stakes.remove_stake_delegation(pubkey, new_rate_activation_epoch); + stakes.remove_stake_delegation( + pubkey, + new_rate_activation_epoch, + use_fixed_point_stake_math, + ); } } } @@ -244,6 +259,7 @@ impl Stakes { pub(crate) fn new_from_accounts_for_genesis<'a, T: ReadableAccount + 'a>( new_rate_activation_epoch: Option, accounts: impl IntoIterator, + use_fixed_point_stake_math: bool, ) -> Self { let stake_history = StakeHistory::default(); let mut vote_accounts = VoteAccountsHashMap::default(); @@ -269,8 +285,13 @@ impl Stakes { StakeAccount::try_from(create_account_shared_data(account)) { let delegation = stake_account.delegation(); - #[allow(deprecated)] - let stake = delegation.stake(epoch, &stake_history, new_rate_activation_epoch); + let stake = delegation_effective_stake( + delegation, + epoch, + &stake_history, + new_rate_activation_epoch, + use_fixed_point_stake_math, + ); *delegated_stakes.entry(delegation.voter_pubkey).or_default() += stake; stake_delegations.insert(*pubkey, stake_account); } @@ -389,6 +410,7 @@ impl Stakes { thread_pool: &ThreadPool, new_rate_activation_epoch: Option, stake_delegations: &[(&Pubkey, &StakeAccount)], + use_fixed_point_stake_math: bool, ) -> (StakeHistory, VoteAccounts) { // Wrap up the prev epoch by adding new stake history entry for the // prev epoch. @@ -399,11 +421,12 @@ impl Stakes { StakeActivationStatus::default, |acc, (_stake_pubkey, stake_account)| { let delegation = stake_account.delegation(); - #[allow(deprecated)] - let activation_status = delegation.stake_activating_and_deactivating( + let activation_status = delegation_activation_status( + delegation, self.epoch, &self.stake_history, new_rate_activation_epoch, + use_fixed_point_stake_math, ); acc + activation_status }, @@ -421,6 +444,7 @@ impl Stakes { stake_delegations, &stake_history, new_rate_activation_epoch, + use_fixed_point_stake_math, ); (stake_history, vote_accounts) } @@ -437,19 +461,27 @@ impl Stakes { } /// Sum the stakes that point to the given voter_pubkey - #[allow(deprecated)] fn calculate_stake( stake_delegations: &ImblHashMap, voter_pubkey: &Pubkey, epoch: Epoch, stake_history: &StakeHistory, new_rate_activation_epoch: Option, + use_fixed_point_stake_math: bool, ) -> u64 { stake_delegations .values() .map(StakeAccount::delegation) .filter(|delegation| &delegation.voter_pubkey == voter_pubkey) - .map(|delegation| delegation.stake(epoch, stake_history, new_rate_activation_epoch)) + .map(|delegation| { + delegation_effective_stake( + delegation, + epoch, + stake_history, + new_rate_activation_epoch, + use_fixed_point_stake_math, + ) + }) .sum() } @@ -461,14 +493,16 @@ impl Stakes { &mut self, stake_pubkey: &Pubkey, new_rate_activation_epoch: Option, + use_fixed_point_stake_math: bool, ) { if let Some(stake_account) = self.stake_delegations.remove(stake_pubkey) { let removed_delegation = stake_account.delegation(); - #[allow(deprecated)] - let removed_stake = removed_delegation.stake( + let removed_stake = delegation_effective_stake( + removed_delegation, self.epoch, &self.stake_history, new_rate_activation_epoch, + use_fixed_point_stake_math, ); self.vote_accounts .sub_stake(&removed_delegation.voter_pubkey, removed_stake); @@ -480,6 +514,7 @@ impl Stakes { vote_pubkey: &Pubkey, vote_account: VoteAccount, new_rate_activation_epoch: Option, + use_fixed_point_stake_math: bool, ) -> Option { debug_assert_ne!(vote_account.lamports(), 0u64); @@ -491,6 +526,7 @@ impl Stakes { self.epoch, &self.stake_history, new_rate_activation_epoch, + use_fixed_point_stake_math, ) }) } @@ -500,22 +536,29 @@ impl Stakes { stake_pubkey: Pubkey, stake_account: StakeAccount, new_rate_activation_epoch: Option, + use_fixed_point_stake_math: bool, ) { debug_assert_ne!(stake_account.lamports(), 0u64); let delegation = stake_account.delegation(); let voter_pubkey = delegation.voter_pubkey; - #[allow(deprecated)] - let stake = delegation.stake(self.epoch, &self.stake_history, new_rate_activation_epoch); + let stake = delegation_effective_stake( + delegation, + self.epoch, + &self.stake_history, + new_rate_activation_epoch, + use_fixed_point_stake_math, + ); match self.stake_delegations.insert(stake_pubkey, stake_account) { None => self.vote_accounts.add_stake(&voter_pubkey, stake), Some(old_stake_account) => { let old_delegation = old_stake_account.delegation(); let old_voter_pubkey = old_delegation.voter_pubkey; - #[allow(deprecated)] - let old_stake = old_delegation.stake( + let old_stake = delegation_effective_stake( + old_delegation, self.epoch, &self.stake_history, new_rate_activation_epoch, + use_fixed_point_stake_math, ); if voter_pubkey != old_voter_pubkey || stake != old_stake { self.vote_accounts.sub_stake(&old_voter_pubkey, old_stake); @@ -632,6 +675,7 @@ fn refresh_vote_accounts( stake_delegations: &[(&Pubkey, &StakeAccount)], stake_history: &StakeHistory, new_rate_activation_epoch: Option, + use_fixed_point_stake_math: bool, ) -> VoteAccounts { type StakesHashMap = HashMap; fn merge(mut stakes: StakesHashMap, other: StakesHashMap) -> StakesHashMap { @@ -651,8 +695,13 @@ fn refresh_vote_accounts( |mut delegated_stakes, (_stake_pubkey, stake_account)| { let delegation = stake_account.delegation(); let entry = delegated_stakes.entry(delegation.voter_pubkey).or_default(); - #[allow(deprecated)] - let stake = delegation.stake(epoch, stake_history, new_rate_activation_epoch); + let stake = delegation_effective_stake( + delegation, + epoch, + stake_history, + new_rate_activation_epoch, + use_fixed_point_stake_math, + ); *entry += stake; delegated_stakes }, @@ -675,7 +724,7 @@ fn refresh_vote_accounts( pub(crate) mod tests { use { super::*, - crate::stake_utils, + crate::{stake_delegation::effective_stake, stake_utils}, rayon::ThreadPoolBuilder, solana_account::WritableAccount, solana_pubkey::Pubkey, @@ -766,8 +815,8 @@ pub(crate) mod tests { let ((vote_pubkey, vote_account), (stake_pubkey, mut stake_account)) = create_staked_node_accounts(10, &rent); - stakes_cache.check_and_store(&vote_pubkey, &vote_account, None); - stakes_cache.check_and_store(&stake_pubkey, &stake_account, None); + stakes_cache.check_and_store(&vote_pubkey, &vote_account, None, true); + stakes_cache.check_and_store(&stake_pubkey, &stake_account, None, true); let stake = stake_account .deserialize_data::() .unwrap() @@ -777,8 +826,8 @@ pub(crate) mod tests { let stakes = stakes_cache.stakes(); let vote_accounts = stakes.vote_accounts(); assert!(vote_accounts.get(&vote_pubkey).is_some()); - #[allow(deprecated)] - let expected_stake = stake.stake(i, &StakeHistory::default(), None); + let expected_stake = + effective_stake(&stake, i, &StakeHistory::default(), None, true); assert_eq!( vote_accounts.get_delegated_stake(&vote_pubkey), expected_stake @@ -786,13 +835,13 @@ pub(crate) mod tests { } stake_account.set_lamports(42); - stakes_cache.check_and_store(&stake_pubkey, &stake_account, None); + stakes_cache.check_and_store(&stake_pubkey, &stake_account, None, true); { let stakes = stakes_cache.stakes(); let vote_accounts = stakes.vote_accounts(); assert!(vote_accounts.get(&vote_pubkey).is_some()); - #[allow(deprecated)] - let expected_stake = stake.stake(i, &StakeHistory::default(), None); + let expected_stake = + effective_stake(&stake, i, &StakeHistory::default(), None, true); assert_eq!( vote_accounts.get_delegated_stake(&vote_pubkey), expected_stake @@ -802,7 +851,7 @@ pub(crate) mod tests { // activate more let mut stake_account = create_stake_account(42, &vote_pubkey, &solana_pubkey::new_rand(), &rent); - stakes_cache.check_and_store(&stake_pubkey, &stake_account, None); + stakes_cache.check_and_store(&stake_pubkey, &stake_account, None, true); let stake = stake_account .deserialize_data::() .unwrap() @@ -812,8 +861,8 @@ pub(crate) mod tests { let stakes = stakes_cache.stakes(); let vote_accounts = stakes.vote_accounts(); assert!(vote_accounts.get(&vote_pubkey).is_some()); - #[allow(deprecated)] - let expected_stake = stake.stake(i, &StakeHistory::default(), None); + let expected_stake = + effective_stake(&stake, i, &StakeHistory::default(), None, true); assert_eq!( vote_accounts.get_delegated_stake(&vote_pubkey), expected_stake @@ -821,7 +870,7 @@ pub(crate) mod tests { } stake_account.set_lamports(0); - stakes_cache.check_and_store(&stake_pubkey, &stake_account, None); + stakes_cache.check_and_store(&stake_pubkey, &stake_account, None, true); { let stakes = stakes_cache.stakes(); let vote_accounts = stakes.vote_accounts(); @@ -841,14 +890,14 @@ pub(crate) mod tests { let ((vote_pubkey, vote_account), (stake_pubkey, stake_account)) = create_staked_node_accounts(10, &rent); - stakes_cache.check_and_store(&vote_pubkey, &vote_account, None); - stakes_cache.check_and_store(&stake_pubkey, &stake_account, None); + stakes_cache.check_and_store(&vote_pubkey, &vote_account, None, true); + stakes_cache.check_and_store(&stake_pubkey, &stake_account, None, true); let ((vote11_pubkey, vote11_account), (stake11_pubkey, stake11_account)) = create_staked_node_accounts(20, &rent); - stakes_cache.check_and_store(&vote11_pubkey, &vote11_account, None); - stakes_cache.check_and_store(&stake11_pubkey, &stake11_account, None); + stakes_cache.check_and_store(&vote11_pubkey, &vote11_account, None, true); + stakes_cache.check_and_store(&stake11_pubkey, &stake11_account, None, true); let vote11_node_pubkey = VoteStateV4::deserialize(vote11_account.data(), &vote11_pubkey) .unwrap() @@ -875,8 +924,8 @@ pub(crate) mod tests { let ((vote_pubkey, mut vote_account), (stake_pubkey, stake_account)) = create_staked_node_accounts(10, &rent); - stakes_cache.check_and_store(&vote_pubkey, &vote_account, None); - stakes_cache.check_and_store(&stake_pubkey, &stake_account, None); + stakes_cache.check_and_store(&vote_pubkey, &vote_account, None, true); + stakes_cache.check_and_store(&stake_pubkey, &stake_account, None, true); { let stakes = stakes_cache.stakes(); @@ -886,7 +935,7 @@ pub(crate) mod tests { } vote_account.set_lamports(0); - stakes_cache.check_and_store(&vote_pubkey, &vote_account, None); + stakes_cache.check_and_store(&vote_pubkey, &vote_account, None, true); { let stakes = stakes_cache.stakes(); @@ -896,7 +945,7 @@ pub(crate) mod tests { } vote_account.set_lamports(1); - stakes_cache.check_and_store(&vote_pubkey, &vote_account, None); + stakes_cache.check_and_store(&vote_pubkey, &vote_account, None, true); { let stakes = stakes_cache.stakes(); @@ -910,7 +959,7 @@ pub(crate) mod tests { let mut pushed = vote_account.data().to_vec(); pushed.push(0); vote_account.set_data(pushed); - stakes_cache.check_and_store(&vote_pubkey, &vote_account, None); + stakes_cache.check_and_store(&vote_pubkey, &vote_account, None, true); { let stakes = stakes_cache.stakes(); @@ -921,7 +970,7 @@ pub(crate) mod tests { // Vote account uninitialized vote_account.set_data(vec![0; VoteStateV4::size_of()]); - stakes_cache.check_and_store(&vote_pubkey, &vote_account, None); + stakes_cache.check_and_store(&vote_pubkey, &vote_account, None, true); { let stakes = stakes_cache.stakes(); @@ -931,7 +980,7 @@ pub(crate) mod tests { } vote_account.set_data(cache_data); - stakes_cache.check_and_store(&vote_pubkey, &vote_account, None); + stakes_cache.check_and_store(&vote_pubkey, &vote_account, None, true); { let stakes = stakes_cache.stakes(); @@ -955,11 +1004,11 @@ pub(crate) mod tests { let ((vote_pubkey2, vote_account2), (_stake_pubkey2, stake_account2)) = create_staked_node_accounts(10, &rent); - stakes_cache.check_and_store(&vote_pubkey, &vote_account, None); - stakes_cache.check_and_store(&vote_pubkey2, &vote_account2, None); + stakes_cache.check_and_store(&vote_pubkey, &vote_account, None, true); + stakes_cache.check_and_store(&vote_pubkey2, &vote_account2, None, true); // delegates to vote_pubkey - stakes_cache.check_and_store(&stake_pubkey, &stake_account, None); + stakes_cache.check_and_store(&stake_pubkey, &stake_account, None, true); let stake = stake_account .deserialize_data::() @@ -971,8 +1020,8 @@ pub(crate) mod tests { let stakes = stakes_cache.stakes(); let vote_accounts = stakes.vote_accounts(); assert!(vote_accounts.get(&vote_pubkey).is_some()); - #[allow(deprecated)] - let expected_stake = stake.stake(stakes.epoch, &stakes.stake_history, None); + let expected_stake = + effective_stake(&stake, stakes.epoch, &stakes.stake_history, None, true); assert_eq!( vote_accounts.get_delegated_stake(&vote_pubkey), expected_stake @@ -982,7 +1031,7 @@ pub(crate) mod tests { } // delegates to vote_pubkey2 - stakes_cache.check_and_store(&stake_pubkey, &stake_account2, None); + stakes_cache.check_and_store(&stake_pubkey, &stake_account2, None, true); { let stakes = stakes_cache.stakes(); @@ -990,8 +1039,8 @@ pub(crate) mod tests { assert!(vote_accounts.get(&vote_pubkey).is_some()); assert_eq!(vote_accounts.get_delegated_stake(&vote_pubkey), 0); assert!(vote_accounts.get(&vote_pubkey2).is_some()); - #[allow(deprecated)] - let expected_stake = stake.stake(stakes.epoch, &stakes.stake_history, None); + let expected_stake = + effective_stake(&stake, stakes.epoch, &stakes.stake_history, None, true); assert_eq!( vote_accounts.get_delegated_stake(&vote_pubkey2), expected_stake @@ -1012,11 +1061,11 @@ pub(crate) mod tests { let stake_pubkey2 = solana_pubkey::new_rand(); let stake_account2 = create_stake_account(10, &vote_pubkey, &stake_pubkey2, &rent); - stakes_cache.check_and_store(&vote_pubkey, &vote_account, None); + stakes_cache.check_and_store(&vote_pubkey, &vote_account, None, true); // delegates to vote_pubkey - stakes_cache.check_and_store(&stake_pubkey, &stake_account, None); - stakes_cache.check_and_store(&stake_pubkey2, &stake_account2, None); + stakes_cache.check_and_store(&stake_pubkey, &stake_account, None, true); + stakes_cache.check_and_store(&stake_pubkey2, &stake_account2, None, true); { let stakes = stakes_cache.stakes(); @@ -1034,8 +1083,8 @@ pub(crate) mod tests { let ((vote_pubkey, vote_account), (stake_pubkey, stake_account)) = create_staked_node_accounts(10, &rent); - stakes_cache.check_and_store(&vote_pubkey, &vote_account, None); - stakes_cache.check_and_store(&stake_pubkey, &stake_account, None); + stakes_cache.check_and_store(&vote_pubkey, &vote_account, None, true); + stakes_cache.check_and_store(&stake_pubkey, &stake_account, None, true); let stake = stake_account .deserialize_data::() .unwrap() @@ -1045,8 +1094,8 @@ pub(crate) mod tests { { let stakes = stakes_cache.stakes(); let vote_accounts = stakes.vote_accounts(); - #[allow(deprecated)] - let expected_stake = stake.stake(stakes.epoch, &stakes.stake_history, None); + let expected_stake = + effective_stake(&stake, stakes.epoch, &stakes.stake_history, None, true); assert_eq!( vote_accounts.get_delegated_stake(&vote_pubkey), expected_stake @@ -1057,14 +1106,20 @@ pub(crate) mod tests { let (stake_history, vote_accounts) = { let stakes = stakes_cache.stakes(); let stake_delegations = stakes.stake_delegations_vec(); - stakes.calculate_activated_stake(next_epoch, &thread_pool, None, &stake_delegations) + stakes.calculate_activated_stake( + next_epoch, + &thread_pool, + None, + &stake_delegations, + true, + ) }; stakes_cache.activate_epoch(next_epoch, stake_history, vote_accounts); { let stakes = stakes_cache.stakes(); let vote_accounts = stakes.vote_accounts(); - #[allow(deprecated)] - let expected_stake = stake.stake(stakes.epoch, &stakes.stake_history, None); + let expected_stake = + effective_stake(&stake, stakes.epoch, &stakes.stake_history, None, true); assert_eq!( vote_accounts.get_delegated_stake(&vote_pubkey), expected_stake @@ -1083,8 +1138,8 @@ pub(crate) mod tests { let ((vote_pubkey, vote_account), (stake_pubkey, stake_account)) = create_staked_node_accounts(10, &rent); - stakes_cache.check_and_store(&vote_pubkey, &vote_account, None); - stakes_cache.check_and_store(&stake_pubkey, &stake_account, None); + stakes_cache.check_and_store(&vote_pubkey, &vote_account, None, true); + stakes_cache.check_and_store(&stake_pubkey, &stake_account, None, true); { let stakes = stakes_cache.stakes(); @@ -1098,6 +1153,7 @@ pub(crate) mod tests { &stake_pubkey, &AccountSharedData::new(1, 0, &stake::program::id()), None, + true, ); { let stakes = stakes_cache.stakes(); diff --git a/runtime/src/stakes/serde_stakes.rs b/runtime/src/stakes/serde_stakes.rs index 65312975ed2..96eb866b3ec 100644 --- a/runtime/src/stakes/serde_stakes.rs +++ b/runtime/src/stakes/serde_stakes.rs @@ -288,7 +288,7 @@ mod tests { &node_pubkey, rng.random_range(0..1_000_000), // lamports ); - stakes_cache.check_and_store(&vote_pubkey, &vote_account, None); + stakes_cache.check_and_store(&vote_pubkey, &vote_account, None, true); for _ in 0..rng.random_range(10usize..20) { let stake_pubkey = solana_pubkey::new_rand(); let rent = Rent::free(); @@ -299,7 +299,7 @@ mod tests { &rent, rng.random_range(0..1_000_000), // lamports ); - stakes_cache.check_and_store(&stake_pubkey, &stake_account, None); + stakes_cache.check_and_store(&stake_pubkey, &stake_account, None, true); } } let stakes: Stakes = stakes_cache.stakes().clone(); From c1602bb430a665f022b983a77e36fa19c02686b0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 19:45:59 +0800 Subject: [PATCH 233/576] chore(deps): bump solana-address from 2.6.0 to 2.6.1 (#13019) * chore(deps): bump solana-address from 2.6.0 to 2.6.1 in /dev-bins Bumps [solana-address](https://github.com/anza-xyz/solana-sdk) from 2.6.0 to 2.6.1. - [Release notes](https://github.com/anza-xyz/solana-sdk/releases) - [Commits](https://github.com/anza-xyz/solana-sdk/compare/address@v2.6.0...address@v2.6.1) --- updated-dependencies: - dependency-name: solana-address dependency-version: 2.6.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Update all workspaces --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 36 ++++++++++++++++++------------------ Cargo.toml | 2 +- dev-bins/Cargo.lock | 36 ++++++++++++++++++------------------ programs/sbf/Cargo.lock | 40 ++++++++++++++++++++-------------------- programs/sbf/Cargo.toml | 2 +- 5 files changed, 58 insertions(+), 58 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aa7da96fd9e..861c775d635 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -491,7 +491,7 @@ dependencies = [ "agave-votor-messages", "log", "serde", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-bls-signatures", "solana-clock", "solana-epoch-schedule", @@ -6816,7 +6816,7 @@ checksum = "a9cf16495d9eb53e3d04e72366a33bb1c20c24e78c171d8b8f5978357b63ae95" dependencies = [ "bincode", "serde_core", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-program-error", "solana-program-memory", ] @@ -6944,14 +6944,14 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2ecac8e1b7f74c2baa9e774c42817e3e75b20787134b76cc4d45e8a604488f5" dependencies = [ - "solana-address 2.6.0", + "solana-address 2.6.1", ] [[package]] name = "solana-address" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1384b52c435a750cc9c538760fc7bb472fd78e65a9900a2d07312c5bb335b72" +checksum = "39c93e262f671bf402e1040e4a7e40b05d81da5956c7681948c975a0997517bb" dependencies = [ "arbitrary", "borsh", @@ -8079,7 +8079,7 @@ dependencies = [ "rand 0.9.4", "rayon", "smallvec", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-bls-signatures", "solana-clock", "solana-cost-model", @@ -8134,7 +8134,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ee8beac9bff4db9225e57d532d169b0be5e447f1e6601a2f50f27a01bf5518f" dependencies = [ "siphasher 0.3.11", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-hash 4.3.0", ] @@ -8695,7 +8695,7 @@ dependencies = [ "five8", "five8_core", "rand 0.9.4", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-derivation-path", "solana-seed-derivable", "solana-seed-phrase", @@ -8992,7 +8992,7 @@ dependencies = [ "lazy_static", "serde", "serde_derive", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-hash 4.3.0", "solana-instruction", "solana-sanitize", @@ -9480,7 +9480,7 @@ dependencies = [ "solana-account 4.3.0", "solana-account-info", "solana-accounts-db", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-banks-client", "solana-banks-interface", "solana-banks-server", @@ -9546,7 +9546,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7db719574990de7e8b0f55a8593ac92a5ccb42c8ce67b3e4bf05b139d5d9ee71" dependencies = [ "rand 0.9.4", - "solana-address 2.6.0", + "solana-address 2.6.1", ] [[package]] @@ -9875,7 +9875,7 @@ dependencies = [ "serde", "serde_json", "solana-account-decoder-client-types", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-clock", "solana-commitment-config", "solana-fee-calculator", @@ -10131,7 +10131,7 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "def234c1956ff616d46c9dd953f251fa7096ddbaa6d52b165218de97882b7280" dependencies = [ - "solana-address 2.6.0", + "solana-address 2.6.1", ] [[package]] @@ -10763,7 +10763,7 @@ dependencies = [ "num-traits", "serde", "serde_derive", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-instruction", "solana-msg", "solana-program-error", @@ -10858,7 +10858,7 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17358d1e9a13e5b9c2264d301102126cf11a47fd394cdf3dec174fe7bc96e1de" dependencies = [ - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-sdk-ids", ] @@ -11090,7 +11090,7 @@ checksum = "45eb9e481a70f582be316d0bf2f8f8704079664e636a4e5d0c0d077f1c9de8f3" dependencies = [ "serde", "serde_derive", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-hash 4.3.0", "solana-instruction", "solana-instruction-error", @@ -11568,7 +11568,7 @@ dependencies = [ "serde_derive", "serde_json", "sha3", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-derivation-path", "solana-instruction", "solana-sdk-ids", @@ -11760,7 +11760,7 @@ dependencies = [ "num-derive", "num-traits", "num_enum", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-instruction", "solana-nullable", "solana-program-error", diff --git a/Cargo.toml b/Cargo.toml index 6d21e43eb94..8c05973993b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -343,7 +343,7 @@ solana-account-decoder = { path = "account-decoder", version = "=4.2.0-alpha.0", solana-account-decoder-client-types = { path = "account-decoder-client-types", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-account-info = "3.1.1" solana-accounts-db = { path = "accounts-db", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-address = "2.6.0" +solana-address = "2.6.1" solana-address-lookup-table-interface = "3.1.0" solana-banks-client = { path = "banks-client", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-banks-interface = { path = "banks-interface", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 4c56d75df5c..a178b0bc4e4 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -395,7 +395,7 @@ dependencies = [ "agave-feature-set", "log", "serde", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-bls-signatures", "solana-clock", "solana-epoch-schedule", @@ -5950,7 +5950,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9cf16495d9eb53e3d04e72366a33bb1c20c24e78c171d8b8f5978357b63ae95" dependencies = [ - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-program-error", "solana-program-memory", ] @@ -6013,14 +6013,14 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2ecac8e1b7f74c2baa9e774c42817e3e75b20787134b76cc4d45e8a604488f5" dependencies = [ - "solana-address 2.6.0", + "solana-address 2.6.1", ] [[package]] name = "solana-address" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1384b52c435a750cc9c538760fc7bb472fd78e65a9900a2d07312c5bb335b72" +checksum = "39c93e262f671bf402e1040e4a7e40b05d81da5956c7681948c975a0997517bb" dependencies = [ "borsh", "bytemuck", @@ -6890,7 +6890,7 @@ dependencies = [ "num_cpus", "rayon", "smallvec", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-bls-signatures", "solana-clock", "solana-cost-model", @@ -6940,7 +6940,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ee8beac9bff4db9225e57d532d169b0be5e447f1e6601a2f50f27a01bf5518f" dependencies = [ "siphasher 0.3.11", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-hash 4.3.0", ] @@ -7343,7 +7343,7 @@ dependencies = [ "five8", "five8_core", "rand 0.9.4", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-derivation-path", "solana-seed-derivable", "solana-seed-phrase", @@ -7612,7 +7612,7 @@ dependencies = [ "lazy_static", "serde", "serde_derive", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-hash 4.3.0", "solana-instruction", "solana-sanitize", @@ -7980,7 +7980,7 @@ dependencies = [ "solana-account 4.3.0", "solana-account-info", "solana-accounts-db", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-banks-client", "solana-banks-interface", "solana-banks-server", @@ -8042,7 +8042,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7db719574990de7e8b0f55a8593ac92a5ccb42c8ce67b3e4bf05b139d5d9ee71" dependencies = [ "rand 0.9.4", - "solana-address 2.6.0", + "solana-address 2.6.1", ] [[package]] @@ -8309,7 +8309,7 @@ dependencies = [ "serde", "serde_json", "solana-account-decoder-client-types", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-clock", "solana-commitment-config", "solana-fee-calculator", @@ -8502,7 +8502,7 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "def234c1956ff616d46c9dd953f251fa7096ddbaa6d52b165218de97882b7280" dependencies = [ - "solana-address 2.6.0", + "solana-address 2.6.1", ] [[package]] @@ -9021,7 +9021,7 @@ dependencies = [ "num-traits", "serde", "serde_derive", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-instruction", "solana-msg", "solana-program-error", @@ -9103,7 +9103,7 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17358d1e9a13e5b9c2264d301102126cf11a47fd394cdf3dec174fe7bc96e1de" dependencies = [ - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-sdk-ids", ] @@ -9273,7 +9273,7 @@ checksum = "45eb9e481a70f582be316d0bf2f8f8704079664e636a4e5d0c0d077f1c9de8f3" dependencies = [ "serde", "serde_derive", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-hash 4.3.0", "solana-instruction", "solana-instruction-error", @@ -9670,7 +9670,7 @@ dependencies = [ "serde_derive", "serde_json", "sha3", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-derivation-path", "solana-instruction", "solana-sdk-ids", @@ -9874,7 +9874,7 @@ dependencies = [ "num-derive", "num-traits", "num_enum", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-instruction", "solana-nullable", "solana-program-error", diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index 0e9b5083389..fb8b14fc6d5 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -383,7 +383,7 @@ dependencies = [ "agave-feature-set", "log", "serde", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-bls-signatures", "solana-clock", "solana-epoch-schedule", @@ -5840,7 +5840,7 @@ checksum = "a9cf16495d9eb53e3d04e72366a33bb1c20c24e78c171d8b8f5978357b63ae95" dependencies = [ "bincode", "serde_core", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-program-error", "solana-program-memory", ] @@ -5851,7 +5851,7 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc141b940560430425ebaadb7645496c45f6a10fad9911d719bd03eab7f4d422" dependencies = [ - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-program-error", ] @@ -5913,14 +5913,14 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2ecac8e1b7f74c2baa9e774c42817e3e75b20787134b76cc4d45e8a604488f5" dependencies = [ - "solana-address 2.6.0", + "solana-address 2.6.1", ] [[package]] name = "solana-address" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1384b52c435a750cc9c538760fc7bb472fd78e65a9900a2d07312c5bb335b72" +checksum = "39c93e262f671bf402e1040e4a7e40b05d81da5956c7681948c975a0997517bb" dependencies = [ "borsh", "bytemuck", @@ -6743,7 +6743,7 @@ dependencies = [ "num_cpus", "rayon", "smallvec", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-bls-signatures", "solana-clock", "solana-cost-model", @@ -6793,7 +6793,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ee8beac9bff4db9225e57d532d169b0be5e447f1e6601a2f50f27a01bf5518f" dependencies = [ "siphasher 0.3.11", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-hash 4.3.0", ] @@ -7177,7 +7177,7 @@ dependencies = [ "five8", "five8_core", "rand 0.9.4", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-derivation-path", "solana-seed-derivable", "solana-seed-phrase", @@ -7378,7 +7378,7 @@ dependencies = [ "lazy_static", "serde", "serde_derive", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-hash 4.3.0", "solana-instruction", "solana-sanitize", @@ -7787,7 +7787,7 @@ dependencies = [ "solana-account 4.3.0", "solana-account-info", "solana-accounts-db", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-banks-client", "solana-banks-interface", "solana-banks-server", @@ -7849,7 +7849,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7db719574990de7e8b0f55a8593ac92a5ccb42c8ce67b3e4bf05b139d5d9ee71" dependencies = [ "rand 0.9.4", - "solana-address 2.6.0", + "solana-address 2.6.1", ] [[package]] @@ -8116,7 +8116,7 @@ dependencies = [ "serde", "serde_json", "solana-account-decoder-client-types", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-clock", "solana-commitment-config", "solana-fee-calculator", @@ -8482,7 +8482,7 @@ version = "4.2.0-alpha.0" dependencies = [ "solana-account-info", "solana-account-view", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-cpi", "solana-program-entrypoint", "solana-program-error", @@ -9024,7 +9024,7 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "def234c1956ff616d46c9dd953f251fa7096ddbaa6d52b165218de97882b7280" dependencies = [ - "solana-address 2.6.0", + "solana-address 2.6.1", ] [[package]] @@ -9542,7 +9542,7 @@ dependencies = [ "num-traits", "serde", "serde_derive", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-instruction", "solana-msg", "solana-program-error", @@ -9626,7 +9626,7 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17358d1e9a13e5b9c2264d301102126cf11a47fd394cdf3dec174fe7bc96e1de" dependencies = [ - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-sdk-ids", ] @@ -9766,7 +9766,7 @@ checksum = "45eb9e481a70f582be316d0bf2f8f8704079664e636a4e5d0c0d077f1c9de8f3" dependencies = [ "serde", "serde_derive", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-hash 4.3.0", "solana-instruction", "solana-instruction-error", @@ -10163,7 +10163,7 @@ dependencies = [ "serde_derive", "serde_json", "sha3", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-derivation-path", "solana-instruction", "solana-sdk-ids", @@ -10355,7 +10355,7 @@ dependencies = [ "num-derive", "num-traits", "num_enum", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-instruction", "solana-nullable", "solana-program-error", diff --git a/programs/sbf/Cargo.toml b/programs/sbf/Cargo.toml index 6804bb42220..990e184639d 100644 --- a/programs/sbf/Cargo.toml +++ b/programs/sbf/Cargo.toml @@ -105,7 +105,7 @@ serde = { version = "1.0.112", features = ["derive"] } serde_json = "1.0.150" solana-account-info = "=3.1.1" solana-account-view = "=2.0.0" -solana-address = "=2.6.0" +solana-address = "=2.6.1" solana-blake3-hasher = { version = "=3.1.0", features = ["blake3"] } solana-bn254 = "=3.2.1" solana-clock = { version = "=3.1.0", features = ["serde", "sysvar"] } From 7cf0a6c2cd1b975f9e8f5d88eff2665f1dceaf04 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 19:53:36 +0800 Subject: [PATCH 234/576] chore(deps): bump serial_test from 3.4.0 to 3.5.0 (#13018) * chore(deps): bump serial_test from 3.4.0 to 3.5.0 Bumps [serial_test](https://github.com/palfrey/serial_test) from 3.4.0 to 3.5.0. - [Release notes](https://github.com/palfrey/serial_test/releases) - [Commits](https://github.com/palfrey/serial_test/compare/v3.4.0...v3.5.0) --- updated-dependencies: - dependency-name: serial_test dependency-version: 3.5.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * Update all workspaces --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 24 ++++-------------------- Cargo.toml | 2 +- dev-bins/Cargo.lock | 24 ++++-------------------- 3 files changed, 9 insertions(+), 41 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 861c775d635..a233647db62 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6218,15 +6218,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "scc" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" -dependencies = [ - "sdd", -] - [[package]] name = "schannel" version = "0.1.19" @@ -6249,12 +6240,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sdd" -version = "3.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" - [[package]] name = "sec1" version = "0.7.3" @@ -6449,24 +6434,23 @@ dependencies = [ [[package]] name = "serial_test" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "911bd979bf1070a3f3aa7b691a3b3e9968f339ceeec89e08c280a8a22207a32f" +checksum = "699f4197115b8a7e7ff19c9a315a4bd6fffec26cc4626ef45ecaea389e081c6d" dependencies = [ "futures-executor", "futures-util", "log", "once_cell", "parking_lot 0.12.3", - "scc", "serial_test_derive", ] [[package]] name = "serial_test_derive" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a7d91949b85b0d2fb687445e448b40d322b6b3e4af6b44a29b21d9a5f33e6d9" +checksum = "94e153fc76e1c6a068703d6d29c508a0b15c061c4b7e43da59cc097bc342673c" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 8c05973993b..23185ab5568 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -328,7 +328,7 @@ serde_bytes = "0.11.19" serde_json = "1.0.150" serde_with = { version = "3.20.0", default-features = false } serde_yaml = "0.9.34" -serial_test = "3.4.0" +serial_test = "3.5.0" shaq = { version = "3.0.0" } shuttle = "0.7.1" signal-hook = "0.4.4" diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index a178b0bc4e4..64e4f61406d 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -5421,15 +5421,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "scc" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" -dependencies = [ - "sdd", -] - [[package]] name = "schannel" version = "0.1.29" @@ -5445,12 +5436,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sdd" -version = "3.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" - [[package]] name = "sec1" version = "0.7.3" @@ -5618,24 +5603,23 @@ dependencies = [ [[package]] name = "serial_test" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "911bd979bf1070a3f3aa7b691a3b3e9968f339ceeec89e08c280a8a22207a32f" +checksum = "699f4197115b8a7e7ff19c9a315a4bd6fffec26cc4626ef45ecaea389e081c6d" dependencies = [ "futures-executor", "futures-util", "log", "once_cell", "parking_lot 0.12.5", - "scc", "serial_test_derive", ] [[package]] name = "serial_test_derive" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a7d91949b85b0d2fb687445e448b40d322b6b3e4af6b44a29b21d9a5f33e6d9" +checksum = "94e153fc76e1c6a068703d6d29c508a0b15c061c4b7e43da59cc097bc342673c" dependencies = [ "proc-macro2", "quote", From 2f26917613a35d96a0034d9e61800e2d0d8072cf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 19:54:18 +0800 Subject: [PATCH 235/576] chore(deps): bump solana-fee-calculator from 3.2.0 to 3.2.1 (#13012) * chore(deps): bump solana-fee-calculator from 3.2.0 to 3.2.1 Bumps [solana-fee-calculator](https://github.com/anza-xyz/solana-sdk) from 3.2.0 to 3.2.1. - [Release notes](https://github.com/anza-xyz/solana-sdk/releases) - [Commits](https://github.com/anza-xyz/solana-sdk/compare/bn254@v3.2.0...bn254@v3.2.1) --- updated-dependencies: - dependency-name: solana-fee-calculator dependency-version: 3.2.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Update all workspaces --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- dev-bins/Cargo.lock | 4 ++-- dev-bins/Cargo.toml | 2 +- programs/sbf/Cargo.lock | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a233647db62..7f4f5dba34f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8236,9 +8236,9 @@ dependencies = [ [[package]] name = "solana-fee-calculator" -version = "3.2.0" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57e8add96b5741573e9f7529c4bb7719cfcfa999c3847a68cdfaef0cb6adf567" +checksum = "97ee18959f176ba6229105c6c2a2ddaaa04bd53615af9277d834b113571bd205" dependencies = [ "log", "serde", diff --git a/Cargo.toml b/Cargo.toml index 23185ab5568..bab229e91d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -389,7 +389,7 @@ solana-epoch-schedule = "3.1.0" solana-faucet = { path = "faucet", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-feature-gate-interface = "4.0.0" solana-fee = { path = "fee", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-fee-calculator = "3.2.0" +solana-fee-calculator = "3.2.1" solana-fee-structure = "3.0.0" solana-file-download = "3.1.4" solana-frozen-abi = "3.5.0" diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 64e4f61406d..8e58a2008c7 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -6996,9 +6996,9 @@ dependencies = [ [[package]] name = "solana-fee-calculator" -version = "3.2.0" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57e8add96b5741573e9f7529c4bb7719cfcfa999c3847a68cdfaef0cb6adf567" +checksum = "97ee18959f176ba6229105c6c2a2ddaaa04bd53615af9277d834b113571bd205" dependencies = [ "log", "serde", diff --git a/dev-bins/Cargo.toml b/dev-bins/Cargo.toml index dddce3f1cc1..cb8b9c23bd8 100644 --- a/dev-bins/Cargo.toml +++ b/dev-bins/Cargo.toml @@ -83,7 +83,7 @@ solana-cost-model = { path = "../cost-model", version = "=4.2.0-alpha.0", featur solana-entry = { path = "../entry", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-faucet = { path = "../faucet", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-feature-gate-interface = "4.0.0" -solana-fee-calculator = "3.2.0" +solana-fee-calculator = "3.2.1" solana-genesis = { path = "../genesis", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-genesis-config = "4.0.0" solana-genesis-utils = { path = "../genesis-utils", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index fb8b14fc6d5..0cdf83a543b 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -6892,9 +6892,9 @@ dependencies = [ [[package]] name = "solana-fee-calculator" -version = "3.2.0" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57e8add96b5741573e9f7529c4bb7719cfcfa999c3847a68cdfaef0cb6adf567" +checksum = "97ee18959f176ba6229105c6c2a2ddaaa04bd53615af9277d834b113571bd205" dependencies = [ "log", "serde", From cacfa3d4adc4ed77b3cbb9f050de9ad5635e1893 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 19:54:45 +0800 Subject: [PATCH 236/576] chore(deps): bump rpassword from 7.5.3 to 7.5.4 (#13011) * chore(deps): bump rpassword from 7.5.3 to 7.5.4 Bumps [rpassword](https://github.com/conradkleinespel/rpassword) from 7.5.3 to 7.5.4. - [Release notes](https://github.com/conradkleinespel/rpassword/releases) - [Commits](https://github.com/conradkleinespel/rpassword/compare/v7.5.3...v7.5.4) --- updated-dependencies: - dependency-name: rpassword dependency-version: 7.5.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Update all workspaces --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 8 ++++---- dev-bins/Cargo.lock | 20 ++++++++++---------- programs/sbf/Cargo.lock | 10 +++++----- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7f4f5dba34f..d745f62fb0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5991,9 +5991,9 @@ dependencies = [ [[package]] name = "rpassword" -version = "7.5.3" +version = "7.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "835a57a69104632d64deb0df2e09a69945cd7a6eab4070fc9b1d7e50cf6c3edc" +checksum = "2da316a15f47e3d053de9cb2c439650bd8fa4aaeb9365f2e5f27f492ff73c196" dependencies = [ "libc", "rtoolbox", @@ -6098,7 +6098,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.0", + "windows-sys 0.52.0", ] [[package]] @@ -12026,7 +12026,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix 1.1.4", - "windows-sys 0.61.0", + "windows-sys 0.52.0", ] [[package]] diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 8e58a2008c7..a3babbb19b5 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -531,7 +531,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -542,7 +542,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -2363,7 +2363,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -5228,9 +5228,9 @@ dependencies = [ [[package]] name = "rpassword" -version = "7.5.3" +version = "7.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "835a57a69104632d64deb0df2e09a69945cd7a6eab4070fc9b1d7e50cf6c3edc" +checksum = "2da316a15f47e3d053de9cb2c439650bd8fa4aaeb9365f2e5f27f492ff73c196" dependencies = [ "libc", "rtoolbox", @@ -5313,7 +5313,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -5371,7 +5371,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -5825,7 +5825,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -10120,7 +10120,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -11097,7 +11097,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index 0cdf83a543b..23659aeb94e 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -5136,9 +5136,9 @@ dependencies = [ [[package]] name = "rpassword" -version = "7.5.3" +version = "7.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "835a57a69104632d64deb0df2e09a69945cd7a6eab4070fc9b1d7e50cf6c3edc" +checksum = "2da316a15f47e3d053de9cb2c439650bd8fa4aaeb9365f2e5f27f492ff73c196" dependencies = [ "libc", "rtoolbox", @@ -5234,7 +5234,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.0", + "windows-sys 0.52.0", ] [[package]] @@ -5729,7 +5729,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.61.0", + "windows-sys 0.60.2", ] [[package]] @@ -10617,7 +10617,7 @@ dependencies = [ "getrandom 0.3.1", "once_cell", "rustix 1.1.4", - "windows-sys 0.61.0", + "windows-sys 0.52.0", ] [[package]] From 673a4afb97b9403f2cd263da27cb7d017392ba31 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 20:13:07 +0800 Subject: [PATCH 237/576] chore(deps): bump protosol from 8.0.0 to 8.2.0 (#13010) Bumps [protosol](https://github.com/firedancer-io/protosol) from 8.0.0 to 8.2.0. - [Commits](https://github.com/firedancer-io/protosol/compare/v8.0.0...v8.2.0) --- updated-dependencies: - dependency-name: protosol dependency-version: 8.2.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d745f62fb0d..f6e50158b05 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5476,9 +5476,9 @@ dependencies = [ [[package]] name = "protosol" -version = "8.0.0" +version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4917daeaee8127b0a7e0b9c68ad8ed834b4b0eb8c8c186eb0fba8d3ecc2cd6d5" +checksum = "87417d0270466f8324148ed75569b940464e70e0aa8aaac574be560f582421ad" dependencies = [ "prost 0.11.9", ] diff --git a/Cargo.toml b/Cargo.toml index bab229e91d3..0a8e1e5a1ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -305,7 +305,7 @@ proptest = "1.11" prost = "0.14.3" prost-types = "0.14.3" protobuf-src = "1.1.0" -protosol = "=8.0.0" +protosol = "=8.2.0" qualifier_attr = { version = "0.2.2", default-features = false } quinn = "0.11.9" rand = "0.9.4" From 850a174d5ac7f32d3ac8a54ab20391a9b7c8e74b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 20:13:19 +0800 Subject: [PATCH 238/576] chore(deps): bump solana-signature from 3.4.0 to 3.4.1 (#13017) * chore(deps): bump solana-signature from 3.4.0 to 3.4.1 in /dev-bins Bumps [solana-signature](https://github.com/anza-xyz/solana-sdk) from 3.4.0 to 3.4.1. - [Release notes](https://github.com/anza-xyz/solana-sdk/releases) - [Commits](https://github.com/anza-xyz/solana-sdk/compare/account@v3.4.0...signature@v3.4.1) --- updated-dependencies: - dependency-name: solana-signature dependency-version: 3.4.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Update all workspaces --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- dev-bins/Cargo.lock | 4 ++-- dev-bins/Cargo.toml | 2 +- install/Cargo.toml | 2 +- programs/sbf/Cargo.lock | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f6e50158b05..668bcb8fbca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10298,9 +10298,9 @@ dependencies = [ [[package]] name = "solana-signature" -version = "3.4.0" +version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a73c6e97cc2108be0adf6a6ea326434f8398df9d7eed81da2a4548b69e971c" +checksum = "b0364c7577c3c82a693ce28a1febc8d1b5d1b0a175fdc2114ae6186b69effe1e" dependencies = [ "ed25519-dalek 2.2.0", "five8", diff --git a/Cargo.toml b/Cargo.toml index 0a8e1e5a1ce..a56ad552b78 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -473,7 +473,7 @@ solana-sha256-hasher = "3.1.0" solana-sha512-hasher = "1.0.1" solana-short-vec = "3.2.1" solana-shred-version = "3.0.1" -solana-signature = { version = "3.4.0", default-features = false } +solana-signature = { version = "3.4.1", default-features = false } solana-signer = "3.0.0" solana-signer-store = "0.1.0" solana-slot-hashes = "3.0.2" diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index a3babbb19b5..3337f8adfe3 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -8655,9 +8655,9 @@ dependencies = [ [[package]] name = "solana-signature" -version = "3.4.0" +version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a73c6e97cc2108be0adf6a6ea326434f8398df9d7eed81da2a4548b69e971c" +checksum = "b0364c7577c3c82a693ce28a1febc8d1b5d1b0a175fdc2114ae6186b69effe1e" dependencies = [ "ed25519-dalek 2.2.0", "five8", diff --git a/dev-bins/Cargo.toml b/dev-bins/Cargo.toml index cb8b9c23bd8..3b26d146f2b 100644 --- a/dev-bins/Cargo.toml +++ b/dev-bins/Cargo.toml @@ -116,7 +116,7 @@ solana-runtime-transaction = { path = "../runtime-transaction", version = "=4.2. solana-sbpf = { version = "=0.21.0", default-features = false } solana-sdk-ids = "3.1.0" solana-shred-version = "3.0.1" -solana-signature = { version = "3.4.0", default-features = false } +solana-signature = { version = "3.4.1", default-features = false } solana-signer = "3.0.0" solana-stake-interface = "4.2.0" solana-storage-bigtable = { path = "../storage-bigtable", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } diff --git a/install/Cargo.toml b/install/Cargo.toml index 3bc59ff0689..016a0a1c127 100644 --- a/install/Cargo.toml +++ b/install/Cargo.toml @@ -47,7 +47,7 @@ solana-message = "=4.2.0" solana-pubkey = { version = "=4.2.0", default-features = false } solana-rpc-client = { workspace = true } solana-sha256-hasher = { workspace = true } -solana-signature = { version = "=3.4.0", default-features = false } +solana-signature = { version = "=3.4.1", default-features = false } solana-signer = "=3.0.0" solana-transaction = { version = "=4.1.2", features = ["wincode"] } solana-version = { workspace = true } diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index 23659aeb94e..9f3d091658e 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -9193,9 +9193,9 @@ dependencies = [ [[package]] name = "solana-signature" -version = "3.4.0" +version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a73c6e97cc2108be0adf6a6ea326434f8398df9d7eed81da2a4548b69e971c" +checksum = "b0364c7577c3c82a693ce28a1febc8d1b5d1b0a175fdc2114ae6186b69effe1e" dependencies = [ "ed25519-dalek 2.2.0", "five8", From 4e0bda14799831d6848d2a018446569dd5757ffd Mon Sep 17 00:00:00 2001 From: Akhilesh Singhania Date: Mon, 8 Jun 2026 17:02:12 +0200 Subject: [PATCH 239/576] Updates BLSOp to send a Vec of certs (#13001) --- votor/src/consensus_pool.rs | 13 -- votor/src/consensus_pool_service.rs | 179 ++++++++++++---------------- votor/src/voting_service.rs | 44 +++---- 3 files changed, 101 insertions(+), 135 deletions(-) diff --git a/votor/src/consensus_pool.rs b/votor/src/consensus_pool.rs index b0a82ceb3ee..2462a9e2320 100644 --- a/votor/src/consensus_pool.rs +++ b/votor/src/consensus_pool.rs @@ -1,7 +1,6 @@ //! Defines ConsensusPool to store received and generated votes and certificates. use { crate::{ - commitment::CommitmentError, common::{ MAX_ENTRIES_PER_PUBKEY_FOR_NOTARIZE_LITE, MAX_ENTRIES_PER_PUBKEY_FOR_OTHER_TYPES, Stake, conflicting_types, vote_to_cert_types, @@ -56,22 +55,10 @@ pub(crate) enum AddVoteError { #[error("Certificate error: {0}")] Certificate(#[from] CertificateBuildError), - #[error("{0} channel disconnected")] - ChannelDisconnected(String), - - #[error("Voting Service queue full")] - VotingServiceQueueFull, - #[error("Invalid rank: {0}")] InvalidRank(u16), } -impl From for AddVoteError { - fn from(_: CommitmentError) -> Self { - AddVoteError::ChannelDisconnected("CommitmentSender".to_string()) - } -} - fn get_key_and_stakes( root_bank: &Bank, slot: Slot, diff --git a/votor/src/consensus_pool_service.rs b/votor/src/consensus_pool_service.rs index 9e8c6e9bb81..d52d6138809 100644 --- a/votor/src/consensus_pool_service.rs +++ b/votor/src/consensus_pool_service.rs @@ -73,9 +73,8 @@ impl ConsensusPoolService { let t_ingest = Builder::new() .name("solCertPoolIngest".to_string()) .spawn(move || { - if let Err(e) = Self::consensus_pool_ingest_loop(ctx) { - info!("Certificate pool service exited: {e:?}. Shutting down"); - } + Self::consensus_pool_ingest_loop(ctx); + info!("consensus pool service exited."); }) .unwrap(); @@ -92,7 +91,7 @@ impl ConsensusPoolService { standstill_timer: &mut Instant, stats: &mut ConsensusPoolServiceStats, highest_finalized: &RwLock>, - ) -> Result<(), AddVoteError> { + ) -> Result<(), ()> { // If we have a new finalized slot, update the root and send new certificates if new_finalized_slot.is_some() { // Reset standstill timer @@ -120,7 +119,7 @@ impl ConsensusPoolService { bls_sender: &Sender, certificates_to_send: Vec>, stats: &mut ConsensusPoolServiceStats, - ) -> Result<(), AddVoteError> { + ) -> Result<(), ()> { let num_certs = certificates_to_send.len(); if num_certs == 0 { return Ok(()); @@ -132,7 +131,7 @@ impl ConsensusPoolService { stats.certificates_skipped_unstaked += num_certs; return Ok(()); } - Self::enqueue_certificates(bls_sender, certificates_to_send, stats) + Self::enqueue_certificates(my_pubkey, bls_sender, certificates_to_send, stats) } fn is_current_identity_staked(sharable_banks: &SharableBanks, my_pubkey: &Pubkey) -> bool { @@ -144,30 +143,24 @@ impl ConsensusPoolService { } fn enqueue_certificates( + my_pubkey: &Pubkey, bls_sender: &Sender, - certificates_to_send: Vec>, + certificates: Vec>, stats: &mut ConsensusPoolServiceStats, - ) -> Result<(), AddVoteError> { - let num_certs = certificates_to_send.len(); - for (i, certificate) in certificates_to_send.into_iter().enumerate() { - // The buffer should normally be large enough, so we don't handle - // certificate re-send here. - match bls_sender.try_send(BLSOp::PushCertificate { certificate }) { - Ok(_) => { - stats.certificates_sent += 1; - } - Err(TrySendError::Disconnected(_)) => { - return Err(AddVoteError::ChannelDisconnected( - "VotingService".to_string(), - )); - } - Err(TrySendError::Full(_)) => { - stats.certificates_dropped += num_certs.saturating_sub(i); - return Err(AddVoteError::VotingServiceQueueFull); - } + ) -> Result<(), ()> { + let num_certs = certificates.len(); + match bls_sender.try_send(BLSOp::PushCertificates { certificates }) { + Ok(()) => { + stats.certificates_sent += num_certs; + Ok(()) + } + Err(TrySendError::Disconnected(_)) => Err(()), + Err(TrySendError::Full(_)) => { + stats.certificates_dropped += num_certs; + warn!("{my_pubkey}: channel is full, dropping {num_certs} certs"); + Ok(()) } } - Ok(()) } fn process_batch( @@ -177,30 +170,16 @@ impl ConsensusPoolService { events: &mut Vec, standstill_timer: &mut Instant, stats: &mut ConsensusPoolServiceStats, - ) -> Result<(), AddVoteError> { + ) -> Result<(), ()> { for msg in msgs { - match Self::process_consensus_message( + Self::process_consensus_message( ctx, msg, consensus_pool, events, standstill_timer, stats, - ) { - Ok(()) => {} - Err(AddVoteError::ChannelDisconnected(channel_name)) => { - return Err(AddVoteError::ChannelDisconnected(channel_name)); - } - Err(e) => { - // This is a non critical error, a duplicate vote for example - trace!( - "{}: unable to push vote into pool {}", - ctx.cluster_info.id(), - e - ); - stats.add_message_failed += 1; - } - } + )?; } Ok(()) } @@ -212,7 +191,7 @@ impl ConsensusPoolService { events: &mut Vec, standstill_timer: &mut Instant, stats: &mut ConsensusPoolServiceStats, - ) -> Result<(), AddVoteError> { + ) -> Result<(), ()> { match message { ConsensusMessage::Certificate(_) => { stats.received_certificates += 1; @@ -222,7 +201,7 @@ impl ConsensusPoolService { } } let root_bank = ctx.sharable_banks.root(); - let (new_finalized_slot, new_certificates_to_send) = Self::add_message( + let (new_finalized_slot, new_certificates_to_send) = match Self::add_message( &root_bank, &ctx.cluster_info.id(), &ctx.my_vote_pubkey, @@ -230,7 +209,17 @@ impl ConsensusPoolService { consensus_pool, events, stats, - )?; + ) { + Ok(res) => res, + Err(err) => { + stats.add_message_failed += 1; + trace!( + "{}, consensus_pool.add_message() failed with {err}", + ctx.cluster_info.id() + ); + return Ok(()); + } + }; Self::maybe_update_root_and_send_new_certificates( consensus_pool, &ctx.sharable_banks, @@ -244,17 +233,12 @@ impl ConsensusPoolService { ) } - fn handle_channel_disconnected( - ctx: &mut ConsensusPoolContext, - channel_name: &str, - ) -> Result<(), ()> { + fn handle_channel_disconnected(ctx: &mut ConsensusPoolContext, channel_name: &str) { info!( - "{}: {} disconnected. Exiting", - ctx.cluster_info.id(), - channel_name + "{}: {channel_name} disconnected. Exiting", + ctx.cluster_info.id() ); ctx.exit.store(true, Ordering::Relaxed); - Err(()) } /// Finds the initial parent ready that we should use for instantiating the ConsensusPool or kicking off votor @@ -292,7 +276,7 @@ impl ConsensusPoolService { } // Main loop for the certificate pool service, it only exits when any channel is disconnected - fn consensus_pool_ingest_loop(mut ctx: ConsensusPoolContext) -> Result<(), ()> { + fn consensus_pool_ingest_loop(mut ctx: ConsensusPoolContext) { let mut events = vec![]; let root_bank = ctx.sharable_banks.root(); @@ -373,33 +357,29 @@ impl ConsensusPoolService { &mut stats, ) { Ok(()) => (), - Err(AddVoteError::ChannelDisconnected(channel_name)) => { - return Self::handle_channel_disconnected(&mut ctx, channel_name.as_str()); - } - Err(e) => { - trace!( - "{}: unable to push standstill certificates into pool {e}", - ctx.cluster_info.id() - ); + Err(()) => { + return Self::handle_channel_disconnected(&mut ctx, "bls_sender channel"); } } } // Process pending safe-to-notar blocks for intrawindow slots - Self::process_pending_safe_to_notar( + if let Err(()) = Self::process_pending_safe_to_notar( &ctx, &mut consensus_pool, &mut pending_safe_to_notar, &mut events, &mut stats, - )?; + ) { + return Self::handle_channel_disconnected(&mut ctx, "repair_event_sender channel"); + } if events .drain(..) .try_for_each(|event| ctx.event_sender.send(event)) .is_err() { - return Self::handle_channel_disconnected(&mut ctx, "event_sender"); + return Self::handle_channel_disconnected(&mut ctx, "event_sender channel"); } let wait_timeout = if pending_safe_to_notar.is_empty() { @@ -413,7 +393,7 @@ impl ConsensusPoolService { let batches: Vec = select! { recv(ctx.consensus_message_receiver) -> msg => { let Ok(first) = msg else { - return Self::handle_channel_disconnected(&mut ctx, "consensus_message_receiver"); + return Self::handle_channel_disconnected(&mut ctx, "consensus_message_receiver channel"); }; std::iter::once(first).chain(ctx.consensus_message_receiver.try_iter()).collect() }, @@ -441,18 +421,14 @@ impl ConsensusPoolService { }; match ret { Ok(()) => {} - Err(AddVoteError::ChannelDisconnected(channel_name)) => { - return Self::handle_channel_disconnected(&mut ctx, channel_name.as_str()); - } - Err(_) => { - // error was handled in process_batch. + Err(()) => { + return Self::handle_channel_disconnected(&mut ctx, "bls_sender channel"); } } } stats.maybe_report(); consensus_pool.maybe_report(); } - Ok(()) } /// Adds a vote to the certificate pool. @@ -816,19 +792,18 @@ mod tests { } // Verify that we received certificates via the bls channel - let mut found_certificate = false; - while let Ok(event) = ctx.bls_receiver.try_recv() { - if let BLSOp::PushCertificate { certificate } = event { - assert_eq!(certificate.cert_type.slot(), target_slot); - assert!( - matches!(certificate.cert_type, CertificateType::Notarize(_)) - || matches!(certificate.cert_type, CertificateType::FinalizeFast(_)) - || matches!(certificate.cert_type, CertificateType::NotarizeFallback(_,)) - ); - found_certificate = true; - } + let BLSOp::PushCertificates { certificates } = ctx.bls_receiver.recv().unwrap() else { + panic!("invalid type"); + }; + assert_eq!(certificates.len(), 2); + for cert in certificates { + assert_eq!(cert.cert_type.slot(), target_slot); + assert!( + matches!(&cert.cert_type, CertificateType::Notarize(_)) + || matches!(&cert.cert_type, CertificateType::FinalizeFast(_)) + || matches!(&cert.cert_type, CertificateType::NotarizeFallback(_,)) + ); } - assert!(found_certificate, "Should have received a certificate"); // Verify that we received a finalized slot event let finalized_event = events.iter().find(|event| match event { @@ -882,8 +857,9 @@ mod tests { // Verify skip certificate was forwarded let mut found_skip = false; while let Ok(event) = ctx.bls_receiver.try_recv() { - if let BLSOp::PushCertificate { certificate } = event { - if matches!(certificate.cert_type, CertificateType::Skip(slot) if slot == target_slot) + if let BLSOp::PushCertificates { certificates } = event { + assert_eq!(certificates.len(), 1); + if matches!(certificates[0].cert_type, CertificateType::Skip(slot) if slot == target_slot) { found_skip = true; } @@ -1041,7 +1017,7 @@ mod tests { exit.store(true, Ordering::Relaxed); drop(consensus_message_sender); - let _ = handle.join().unwrap(); + handle.join().unwrap(); assert!( saw_parent_ready, @@ -1122,17 +1098,19 @@ mod tests { assert_eq!(stats.certificates_sent.0, 2); // Verify certificates were received - let cert1 = ctx.bls_receiver.try_recv(); - assert!(cert1.is_ok()); - if let BLSOp::PushCertificate { certificate } = cert1.unwrap() { - assert!(matches!(certificate.cert_type, CertificateType::Skip(1))); - } - - let cert2 = ctx.bls_receiver.try_recv(); - assert!(cert2.is_ok()); - if let BLSOp::PushCertificate { certificate } = cert2.unwrap() { - assert!(matches!(certificate.cert_type, CertificateType::Skip(2))); - } + let cert1 = ctx.bls_receiver.try_recv().unwrap(); + let BLSOp::PushCertificates { certificates } = cert1 else { + panic!("invalid type"); + }; + assert_eq!(certificates.len(), 2); + assert!(matches!( + certificates[0].cert_type, + CertificateType::Skip(1) + )); + assert!(matches!( + certificates[1].cert_type, + CertificateType::Skip(2) + )); } #[test] @@ -1186,8 +1164,7 @@ mod tests { certificates, &mut stats, ); - - assert!(matches!(result, Err(AddVoteError::ChannelDisconnected(_)))); + assert_eq!(result.unwrap_err(), ()); } #[test] diff --git a/votor/src/voting_service.rs b/votor/src/voting_service.rs index fb0202b1cee..e3af4017776 100644 --- a/votor/src/voting_service.rs +++ b/votor/src/voting_service.rs @@ -37,15 +37,15 @@ pub enum BLSOp { vote: Arc, saved_vote_history: SavedVoteHistoryVersions, }, - PushCertificate { - certificate: Arc, + PushCertificates { + certificates: Vec>, }, } fn send_message( buf: Vec, socket: &SocketAddr, - connection_cache: &Arc, + connection_cache: &ConnectionCache, ) -> Result<(), TransportError> { let client = connection_cache.get_connection(socket); @@ -151,7 +151,7 @@ impl VotingService { &cluster_info, vote_history_storage.as_ref(), bls_op, - connection_cache.clone(), + &connection_cache, &additional_listeners, &mut staked_validators_cache, ); @@ -166,7 +166,7 @@ impl VotingService { slot: Slot, cluster_info: &ClusterInfo, message: &ConsensusMessage, - connection_cache: Arc, + connection_cache: &ConnectionCache, additional_listeners: &[SocketAddr], staked_validators_cache: &mut StakedValidatorsCache, ) { @@ -188,7 +188,7 @@ impl VotingService { // will cause a packet spike and overwhelm the network. If we later find out that this is // not an issue, we can optimize this by using multi_targret_send or similar methods. for socket in sockets { - if let Err(e) = send_message(buf.clone(), socket, &connection_cache) { + if let Err(e) = send_message(buf.clone(), socket, connection_cache) { warn!("Failed to send alpenglow message to {socket}: {e:?}"); } } @@ -198,7 +198,7 @@ impl VotingService { cluster_info: &ClusterInfo, vote_history_storage: &dyn VoteHistoryStorage, bls_op: BLSOp, - connection_cache: Arc, + connection_cache: &ConnectionCache, additional_listeners: &[SocketAddr], staked_validators_cache: &mut StakedValidatorsCache, ) { @@ -225,17 +225,19 @@ impl VotingService { staked_validators_cache, ); } - BLSOp::PushCertificate { certificate } => { - let slot = certificate.cert_type.slot(); - let message = ConsensusMessage::Certificate(Arc::unwrap_or_clone(certificate)); - Self::broadcast_consensus_message( - slot, - cluster_info, - &message, - connection_cache, - additional_listeners, - staked_validators_cache, - ); + BLSOp::PushCertificates { certificates } => { + for certificate in certificates { + let slot = certificate.cert_type.slot(); + let message = ConsensusMessage::Certificate(Arc::unwrap_or_clone(certificate)); + Self::broadcast_consensus_message( + slot, + cluster_info, + &message, + connection_cache, + additional_listeners, + staked_validators_cache, + ); + } } } } @@ -336,12 +338,12 @@ mod tests { signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), rank: 1, }))] - #[test_case(BLSOp::PushCertificate { - certificate: Arc::new(Certificate { + #[test_case(BLSOp::PushCertificates { + certificates: vec![Arc::new(Certificate { cert_type: CertificateType::Skip(5), signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), bitmap: Vec::new(), - }), + })], }, ConsensusMessage::Certificate(Certificate { cert_type: CertificateType::Skip(5), signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), From 4e8086eb25e9a0677a1068dcfed83faf229e1418 Mon Sep 17 00:00:00 2001 From: steviez Date: Mon, 8 Jun 2026 10:05:35 -0500 Subject: [PATCH 240/576] blockstore: Remove useless test (#13009) The test was inserting shreds from multiple threads. It is well documented that we have a lock to ensure only a single thread can insert at a given time. We don't need a unit test to ensure std::sync::Mutex is functioning properly. If we ever do try to allow parallel insertion, we'll need much more testing than what this test offered --- ledger/tests/blockstore.rs | 50 -------------------------------------- 1 file changed, 50 deletions(-) delete mode 100644 ledger/tests/blockstore.rs diff --git a/ledger/tests/blockstore.rs b/ledger/tests/blockstore.rs deleted file mode 100644 index c6fba680839..00000000000 --- a/ledger/tests/blockstore.rs +++ /dev/null @@ -1,50 +0,0 @@ -use { - solana_entry::entry, - solana_hash::Hash, - solana_ledger::{ - blockstore::{self, Blockstore, PurgeType}, - get_tmp_ledger_path_auto_delete, - }, - std::{sync::Arc, thread::Builder}, -}; - -#[test] -fn test_multiple_threads_insert_shred() { - let ledger_path = get_tmp_ledger_path_auto_delete!(); - let blockstore = Arc::new(Blockstore::open(ledger_path.path()).unwrap()); - - for _ in 0..100 { - let num_threads = 10; - - // Create `num_threads` different ticks in slots 1..num_threads + 1, all - // with parent = slot 0 - let threads: Vec<_> = (0..num_threads) - .map(|i| { - let entries = entry::create_ticks(1, 0, Hash::default()); - let shreds = blockstore::entries_to_test_shreds(&entries, i + 1, 0, false, 0); - let blockstore_ = blockstore.clone(); - Builder::new() - .name("blockstore-writer".to_string()) - .spawn(move || { - blockstore_.insert_shreds(shreds, None, false).unwrap(); - }) - .unwrap() - }) - .collect(); - - for t in threads { - t.join().unwrap() - } - - // Check slot 0 has the correct children - let mut meta0 = blockstore.meta(0).unwrap().unwrap(); - meta0.next_slots.sort_unstable(); - let expected_next_slots: Vec<_> = (1..num_threads + 1).collect(); - assert_eq!(meta0.next_slots, expected_next_slots); - - // Delete slots for next iteration - blockstore - .purge_slots(0, num_threads + 1, PurgeType::Exact) - .unwrap(); - } -} From b90bb91c6738602c95aaa51b8dcc589ef4141c5a Mon Sep 17 00:00:00 2001 From: Yihau Chen Date: Mon, 8 Jun 2026 23:27:00 +0800 Subject: [PATCH 241/576] chore: bump solana-hash from 4.3.0 to 4.4.0 (#13045) --- Cargo.lock | 170 ++++++++++++++++++++-------------------- Cargo.toml | 2 +- dev-bins/Cargo.lock | 146 +++++++++++++++++----------------- dev-bins/Cargo.toml | 2 +- install/Cargo.toml | 2 +- programs/sbf/Cargo.lock | 140 ++++++++++++++++----------------- programs/sbf/Cargo.toml | 2 +- watchtower/Cargo.toml | 2 +- 8 files changed, 233 insertions(+), 233 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 668bcb8fbca..e278ac5e0b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -74,7 +74,7 @@ dependencies = [ "qualifier_attr", "rayon", "solana-bls-signatures", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-signer-store", "thiserror 2.0.18", "wincode", @@ -88,7 +88,7 @@ dependencies = [ "solana-epoch-schedule", "solana-frozen-abi", "solana-frozen-abi-macro", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-pubkey 4.2.0", "solana-sha256-hasher", @@ -117,7 +117,7 @@ version = "4.2.0-alpha.0" dependencies = [ "log", "solana-clock", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-message", "solana-signature", "solana-transaction", @@ -147,7 +147,7 @@ dependencies = [ "serde_yaml", "solana-clap-utils", "solana-config-interface", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-message", "solana-pubkey 4.2.0", @@ -227,7 +227,7 @@ dependencies = [ "assert_matches", "rand 0.9.4", "rand_chacha 0.9.0", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-sha256-hasher", "test-case", ] @@ -289,7 +289,7 @@ dependencies = [ "solana-accounts-db", "solana-clock", "solana-genesis-config", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-lattice-hash", "solana-measure", "solana-metrics", @@ -317,7 +317,7 @@ dependencies = [ "agave-transaction-view", "bincode", "criterion", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-message", @@ -386,7 +386,7 @@ dependencies = [ "solana-genesis-utils", "solana-geyser-plugin-manager", "solana-gossip", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-inflation", "solana-keypair", "solana-ledger", @@ -455,7 +455,7 @@ dependencies = [ "solana-frozen-abi", "solana-frozen-abi-macro", "solana-gossip", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-leader-schedule", "solana-ledger", @@ -497,7 +497,7 @@ dependencies = [ "solana-epoch-schedule", "solana-frozen-abi", "solana-frozen-abi-macro", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-pubkey 4.2.0", "solana-short-vec", "solana-signer-store", @@ -517,7 +517,7 @@ dependencies = [ "solana-clap-utils", "solana-cli-config", "solana-cli-output", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-metrics", "solana-native-token", "solana-notifier", @@ -6098,7 +6098,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.52.0", + "windows-sys 0.61.0", ] [[package]] @@ -6755,7 +6755,7 @@ dependencies = [ "solana-config-interface", "solana-epoch-schedule", "solana-fee-calculator", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-loader-v3-interface", "solana-nonce", @@ -6823,7 +6823,7 @@ dependencies = [ "solana-core", "solana-faucet", "solana-gossip", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-local-cluster", @@ -6885,7 +6885,7 @@ dependencies = [ "solana-fee-calculator", "solana-frozen-abi", "solana-frozen-abi-macro", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-lattice-hash", @@ -6997,7 +6997,7 @@ dependencies = [ "solana-banks-server", "solana-clock", "solana-commitment-config", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-message", "solana-program-pack", "solana-pubkey 4.2.0", @@ -7024,7 +7024,7 @@ dependencies = [ "solana-account 4.3.0", "solana-clock", "solana-commitment-config", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-message", "solana-pubkey 4.2.0", "solana-signature", @@ -7046,7 +7046,7 @@ dependencies = [ "solana-banks-interface", "solana-clock", "solana-commitment-config", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-message", "solana-net-utils", "solana-pubkey 4.2.0", @@ -7092,7 +7092,7 @@ checksum = "7116e1d942a2432ca3f514625104757ab8a56233787e95144c93950029e31176" dependencies = [ "blake3", "solana-define-syscall 4.0.1", - "solana-hash 4.3.0", + "solana-hash 4.4.0", ] [[package]] @@ -7109,7 +7109,7 @@ dependencies = [ "solana-bloom", "solana-frozen-abi", "solana-frozen-abi-macro", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-sanitize", "solana-sha256-hasher", "solana-signature", @@ -7276,7 +7276,7 @@ dependencies = [ "agave-feature-set", "solana-bpf-loader-program", "solana-compute-budget-program", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-program-runtime", "solana-pubkey 4.2.0", "solana-sdk-ids", @@ -7312,7 +7312,7 @@ dependencies = [ "solana-cluster-type", "solana-commitment-config", "solana-derivation-path", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-message", "solana-native-token", @@ -7344,7 +7344,7 @@ dependencies = [ "solana-cluster-type", "solana-commitment-config", "solana-derivation-path", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-message", "solana-native-token", @@ -7404,7 +7404,7 @@ dependencies = [ "solana-feature-gate-interface", "solana-fee-calculator", "solana-fee-structure", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-loader-v3-interface", @@ -7490,7 +7490,7 @@ dependencies = [ "solana-clock", "solana-epoch-info", "solana-epoch-schedule", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-message", "solana-native-token", @@ -7524,7 +7524,7 @@ dependencies = [ "log", "solana-commitment-config", "solana-connection-cache", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-measure", "solana-message", @@ -7588,7 +7588,7 @@ dependencies = [ "solana-account 4.3.0", "solana-commitment-config", "solana-epoch-info", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-message", @@ -7621,7 +7621,7 @@ checksum = "3a494cf8eda7d98d9f0144b288bb409c88308d2e86f15cc1045aa77b83304718" dependencies = [ "serde", "serde_derive", - "solana-hash 4.3.0", + "solana-hash 4.4.0", ] [[package]] @@ -7655,7 +7655,7 @@ dependencies = [ "solana-compute-budget", "solana-compute-budget-instruction", "solana-compute-budget-interface", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-message", @@ -7820,7 +7820,7 @@ dependencies = [ "solana-geyser-plugin-manager", "solana-gossip", "solana-hard-forks", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-leader-schedule", @@ -7913,7 +7913,7 @@ dependencies = [ "solana-cost-model", "solana-frozen-abi", "solana-frozen-abi-macro", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-message", @@ -8068,7 +8068,7 @@ dependencies = [ "solana-clock", "solana-cost-model", "solana-entry", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-measure", "solana-merkle-tree", @@ -8105,7 +8105,7 @@ checksum = "1cddf2388b28291210d9aa60690740733cab527531f06ed153c4d388951e407c" dependencies = [ "serde", "serde_derive", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-sdk-ids", "solana-sdk-macro", "solana-sysvar-id", @@ -8119,7 +8119,7 @@ checksum = "1ee8beac9bff4db9225e57d532d169b0be5e447f1e6601a2f50f27a01bf5518f" dependencies = [ "siphasher 0.3.11", "solana-address 2.6.1", - "solana-hash 4.3.0", + "solana-hash 4.4.0", ] [[package]] @@ -8155,7 +8155,7 @@ checksum = "0eb265ff95e28eceda117e2e3d2d2a611ecbbfe911dfeeeecd1521814540ffab" dependencies = [ "serde", "serde_derive", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-nonce", "solana-pubkey 4.2.0", @@ -8172,7 +8172,7 @@ dependencies = [ "log", "solana-cli-output", "solana-faucet", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-message", @@ -8376,7 +8376,7 @@ dependencies = [ "solana-cluster-type", "solana-epoch-schedule", "solana-fee-calculator", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-inflation", "solana-keypair", "solana-poh-config", @@ -8397,7 +8397,7 @@ dependencies = [ "log", "solana-download-utils", "solana-genesis-config", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-rpc-client", "thiserror 2.0.18", ] @@ -8421,7 +8421,7 @@ dependencies = [ "solana-clock", "solana-entry", "solana-gossip", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-ledger", "solana-measure", "solana-message", @@ -8474,7 +8474,7 @@ dependencies = [ "solana-frozen-abi", "solana-frozen-abi-macro", "solana-gossip", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-ledger", "solana-measure", @@ -8542,14 +8542,14 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "337c246447142f660f778cf6cb582beba8e28deb05b3b24bfb9ffd7c562e5f41" dependencies = [ - "solana-hash 4.3.0", + "solana-hash 4.4.0", ] [[package]] name = "solana-hash" -version = "4.3.0" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1b113239362cee7093bfb250467138f079a2a03673181dc15bff6ccd677912d" +checksum = "fe51db00ac3aa9f950d1e6201a126acfa26e6d81bc4a183ba64ec02effcad883" dependencies = [ "borsh", "bytemuck", @@ -8639,7 +8639,7 @@ checksum = "ed1c0d16d6fdeba12291a1f068cdf0d479d9bff1141bf44afd7aa9d485f65ef8" dependencies = [ "sha3", "solana-define-syscall 4.0.1", - "solana-hash 4.3.0", + "solana-hash 4.4.0", ] [[package]] @@ -8783,7 +8783,7 @@ dependencies = [ "solana-frozen-abi-macro", "solana-genesis-config", "solana-genesis-utils", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-leader-schedule", @@ -8912,7 +8912,7 @@ dependencies = [ "solana-genesis-utils", "solana-gossip", "solana-hard-forks", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-leader-schedule", "solana-ledger", @@ -8962,7 +8962,7 @@ version = "4.2.0-alpha.0" dependencies = [ "fast-math", "hex", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-sha256-hasher", ] @@ -8977,7 +8977,7 @@ dependencies = [ "serde", "serde_derive", "solana-address 2.6.1", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-sanitize", "solana-sdk-ids", @@ -9064,7 +9064,7 @@ dependencies = [ "serde", "serde_derive", "solana-fee-calculator", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-pubkey 4.2.0", "solana-sha256-hasher", "wincode", @@ -9077,7 +9077,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cda945f4ccb317791c26379ca8ec410c5938aa7a5eff211fe5fe0bb428c3a75f" dependencies = [ "solana-account 4.3.0", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-nonce", "solana-sdk-ids", "wincode", @@ -9090,7 +9090,7 @@ dependencies = [ "log", "reqwest 0.12.28", "serde_json", - "solana-hash 4.3.0", + "solana-hash 4.4.0", ] [[package]] @@ -9169,7 +9169,7 @@ dependencies = [ "solana-clock", "solana-frozen-abi", "solana-frozen-abi-macro", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-message", "solana-metrics", @@ -9210,7 +9210,7 @@ dependencies = [ "shuttle", "solana-clock", "solana-entry", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-leader-schedule", "solana-ledger", @@ -9305,7 +9305,7 @@ dependencies = [ "solana-epoch-stake", "solana-example-mocks", "solana-fee-calculator", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-instruction-error", "solana-instructions-sysvar", @@ -9415,7 +9415,7 @@ dependencies = [ "solana-fee-structure", "solana-frozen-abi", "solana-frozen-abi-macro", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-instruction-error", "solana-keypair", @@ -9477,7 +9477,7 @@ dependencies = [ "solana-epoch-schedule", "solana-fee-calculator", "solana-genesis-config", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-loader-v3-interface", @@ -9691,7 +9691,7 @@ dependencies = [ "solana-fee-structure", "solana-genesis-config", "solana-gossip", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-leader-schedule", @@ -9781,7 +9781,7 @@ dependencies = [ "solana-epoch-info", "solana-epoch-schedule", "solana-feature-gate-interface", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-message", @@ -9832,7 +9832,7 @@ dependencies = [ "solana-clap-utils", "solana-commitment-config", "solana-fee-calculator", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-message", "solana-nonce", @@ -9892,7 +9892,7 @@ dependencies = [ "solana-clock", "solana-commitment-config", "solana-connection-cache", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-net-utils", "solana-pubkey 4.2.0", @@ -9986,7 +9986,7 @@ dependencies = [ "solana-frozen-abi-macro", "solana-genesis-config", "solana-hard-forks", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-inflation", "solana-instruction", "solana-instruction-error", @@ -10065,7 +10065,7 @@ dependencies = [ "solana-compute-budget", "solana-compute-budget-instruction", "solana-compute-budget-interface", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-message", @@ -10202,7 +10202,7 @@ dependencies = [ "solana-account 4.3.0", "solana-fee-calculator", "solana-genesis-config", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-measure", "solana-metrics", @@ -10259,7 +10259,7 @@ checksum = "db7dc3011ea4c0334aaaa7e7128cb390ecf546b28d412e9bf2064680f57f588f" dependencies = [ "sha2 0.10.9", "solana-define-syscall 4.0.1", - "solana-hash 4.3.0", + "solana-hash 4.4.0", ] [[package]] @@ -10292,7 +10292,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6c79722e299d957958bf33695f7cd1ef6724ff55563c60fd9e3e24487cccde2" dependencies = [ "solana-hard-forks", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-sha256-hasher", ] @@ -10344,7 +10344,7 @@ checksum = "0a57c158c35629f9e302ab385f16b15813f4927a31c27dda72f3df828bb08d93" dependencies = [ "serde", "serde_derive", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-sdk-ids", "solana-sysvar-id", ] @@ -10445,7 +10445,7 @@ dependencies = [ "serde", "smpl_jwt", "solana-clock", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-message", "solana-metrics", @@ -10476,7 +10476,7 @@ dependencies = [ "protobuf-src", "serde", "solana-account-decoder", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-message", "solana-pubkey 4.2.0", @@ -10566,7 +10566,7 @@ dependencies = [ "solana-fee-structure", "solana-frozen-abi", "solana-frozen-abi-macro", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-instruction-error", "solana-instructions-sysvar", @@ -10654,7 +10654,7 @@ dependencies = [ name = "solana-svm-transaction" version = "4.2.0-alpha.0" dependencies = [ - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-message", "solana-nonce", "solana-pubkey 4.2.0", @@ -10694,7 +10694,7 @@ dependencies = [ "solana-epoch-rewards", "solana-epoch-schedule", "solana-fee-calculator", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-hash-512", "solana-instruction", "solana-keccak-hasher", @@ -10766,7 +10766,7 @@ dependencies = [ "solana-bincode", "solana-compute-budget", "solana-fee-calculator", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-nonce", "solana-nonce-account", @@ -10791,7 +10791,7 @@ version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a39522012be4127884bc611d65f58f1cf33a3afb7baee7e6774be90b48edc3c" dependencies = [ - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-message", "solana-pubkey 4.2.0", @@ -10821,7 +10821,7 @@ dependencies = [ "solana-fee-calculator", "solana-frozen-abi", "solana-frozen-abi-macro", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-last-restart-slot", "solana-program-entrypoint", @@ -10942,7 +10942,7 @@ dependencies = [ "solana-cli-output", "solana-clock", "solana-commitment-config", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-message", @@ -10981,7 +10981,7 @@ dependencies = [ "solana-commitment-config", "solana-connection-cache", "solana-epoch-info", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-message", "solana-net-utils", @@ -11075,7 +11075,7 @@ dependencies = [ "serde", "serde_derive", "solana-address 2.6.1", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-instruction-error", "solana-message", @@ -11143,7 +11143,7 @@ dependencies = [ "solana-account-decoder", "solana-address-lookup-table-interface", "solana-clock", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-loader-v2-interface", "solana-loader-v3-interface", @@ -11220,7 +11220,7 @@ dependencies = [ "solana-entry", "solana-genesis-config", "solana-gossip", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-leader-schedule", "solana-ledger", @@ -11267,7 +11267,7 @@ version = "4.2.0-alpha.0" dependencies = [ "assert_matches", "solana-clock", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-message", "solana-pubkey 4.2.0", @@ -11295,7 +11295,7 @@ dependencies = [ "solana-clock", "solana-cost-model", "solana-entry", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-ledger", "solana-metrics", @@ -11356,7 +11356,7 @@ dependencies = [ "solana-clock", "solana-frozen-abi", "solana-frozen-abi-macro", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-packet 4.1.0", @@ -11392,7 +11392,7 @@ dependencies = [ "solana-clock", "solana-frozen-abi", "solana-frozen-abi-macro", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-instruction-error", "solana-pubkey 4.2.0", @@ -11423,7 +11423,7 @@ dependencies = [ "solana-fee-calculator", "solana-frozen-abi", "solana-frozen-abi-macro", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-packet 4.1.0", "solana-program-runtime", @@ -12026,7 +12026,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix 1.1.4", - "windows-sys 0.52.0", + "windows-sys 0.61.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index a56ad552b78..d588879bf19 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -400,7 +400,7 @@ solana-genesis-utils = { path = "genesis-utils", version = "=4.2.0-alpha.0", fea solana-geyser-plugin-manager = { path = "geyser-plugin-manager", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-gossip = { path = "gossip", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-hard-forks = "3.1.0" -solana-hash = "4.3.0" +solana-hash = "4.4.0" solana-hash-512 = "1.1.0" solana-inflation = "3.1.0" solana-instruction = "3.4.0" diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 3337f8adfe3..0c7a00690ca 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -82,7 +82,7 @@ version = "4.2.0-alpha.0" dependencies = [ "ahash 0.8.12", "solana-epoch-schedule", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-pubkey 4.2.0", "solana-sha256-hasher", @@ -109,7 +109,7 @@ version = "4.2.0-alpha.0" dependencies = [ "log", "solana-clock", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-message", "solana-signature", "solana-transaction", @@ -168,7 +168,7 @@ dependencies = [ "solana-genesis-config", "solana-genesis-utils", "solana-geyser-plugin-manager", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-inflation", "solana-instruction", "solana-keypair", @@ -298,7 +298,7 @@ dependencies = [ "solana-accounts-db", "solana-clock", "solana-genesis-config", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-lattice-hash", "solana-measure", "solana-metrics", @@ -329,7 +329,7 @@ dependencies = [ name = "agave-transaction-view" version = "4.2.0-alpha.0" dependencies = [ - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-message", "solana-packet 4.1.0", "solana-program-runtime", @@ -366,7 +366,7 @@ dependencies = [ "solana-connection-cache", "solana-epoch-schedule", "solana-gossip", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-leader-schedule", "solana-ledger", @@ -399,7 +399,7 @@ dependencies = [ "solana-bls-signatures", "solana-clock", "solana-epoch-schedule", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-pubkey 4.2.0", "solana-short-vec", "solana-signer-store", @@ -531,7 +531,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -542,7 +542,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2363,7 +2363,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -5313,7 +5313,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -5371,7 +5371,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -5825,7 +5825,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -5964,7 +5964,7 @@ dependencies = [ "solana-clock", "solana-epoch-schedule", "solana-fee-calculator", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-lattice-hash", "solana-measure", @@ -6062,7 +6062,7 @@ dependencies = [ "solana-banks-interface", "solana-clock", "solana-commitment-config", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-message", "solana-program-pack", "solana-pubkey 4.2.0", @@ -6086,7 +6086,7 @@ dependencies = [ "solana-account 4.3.0", "solana-clock", "solana-commitment-config", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-message", "solana-pubkey 4.2.0", "solana-signature", @@ -6108,7 +6108,7 @@ dependencies = [ "solana-banks-interface", "solana-clock", "solana-commitment-config", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-message", "solana-net-utils", "solana-pubkey 4.2.0", @@ -6154,7 +6154,7 @@ dependencies = [ "solana-fee-calculator", "solana-genesis", "solana-genesis-config", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-local-cluster", @@ -6207,7 +6207,7 @@ checksum = "7116e1d942a2432ca3f514625104757ab8a56233787e95144c93950029e31176" dependencies = [ "blake3", "solana-define-syscall 4.0.1", - "solana-hash 4.3.0", + "solana-hash 4.4.0", ] [[package]] @@ -6338,7 +6338,7 @@ dependencies = [ "agave-feature-set", "solana-bpf-loader-program", "solana-compute-budget-program", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-program-runtime", "solana-pubkey 4.2.0", "solana-sdk-ids", @@ -6371,7 +6371,7 @@ dependencies = [ "solana-cluster-type", "solana-commitment-config", "solana-derivation-path", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-message", "solana-native-token", @@ -6426,7 +6426,7 @@ dependencies = [ "solana-clock", "solana-epoch-info", "solana-epoch-schedule", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-message", "solana-native-token", "solana-packet 4.1.0", @@ -6455,7 +6455,7 @@ dependencies = [ "log", "solana-commitment-config", "solana-connection-cache", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-measure", "solana-message", @@ -6487,7 +6487,7 @@ dependencies = [ "solana-account 4.3.0", "solana-commitment-config", "solana-epoch-info", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-message", @@ -6520,7 +6520,7 @@ checksum = "3a494cf8eda7d98d9f0144b288bb409c88308d2e86f15cc1045aa77b83304718" dependencies = [ "serde", "serde_derive", - "solana-hash 4.3.0", + "solana-hash 4.4.0", ] [[package]] @@ -6681,7 +6681,7 @@ dependencies = [ "solana-geyser-plugin-manager", "solana-gossip", "solana-hard-forks", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-leader-schedule", @@ -6878,7 +6878,7 @@ dependencies = [ "solana-bls-signatures", "solana-clock", "solana-cost-model", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-measure", "solana-merkle-tree", "solana-message", @@ -6911,7 +6911,7 @@ checksum = "1cddf2388b28291210d9aa60690740733cab527531f06ed153c4d388951e407c" dependencies = [ "serde", "serde_derive", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-sdk-ids", "solana-sdk-macro", "solana-sysvar-id", @@ -6925,7 +6925,7 @@ checksum = "1ee8beac9bff4db9225e57d532d169b0be5e447f1e6601a2f50f27a01bf5518f" dependencies = [ "siphasher 0.3.11", "solana-address 2.6.1", - "solana-hash 4.3.0", + "solana-hash 4.4.0", ] [[package]] @@ -6948,7 +6948,7 @@ dependencies = [ "crossbeam-channel", "log", "solana-cli-output", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-message", @@ -7091,7 +7091,7 @@ dependencies = [ "solana-cluster-type", "solana-epoch-schedule", "solana-fee-calculator", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-inflation", "solana-keypair", "solana-poh-config", @@ -7112,7 +7112,7 @@ dependencies = [ "log", "solana-download-utils", "solana-genesis-config", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-rpc-client", "thiserror 2.0.18", ] @@ -7136,7 +7136,7 @@ dependencies = [ "solana-clock", "solana-entry", "solana-gossip", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-ledger", "solana-measure", "solana-message", @@ -7178,7 +7178,7 @@ dependencies = [ "solana-clock", "solana-entry", "solana-epoch-schedule", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-ledger", "solana-measure", @@ -7223,14 +7223,14 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "337c246447142f660f778cf6cb582beba8e28deb05b3b24bfb9ffd7c562e5f41" dependencies = [ - "solana-hash 4.3.0", + "solana-hash 4.4.0", ] [[package]] name = "solana-hash" -version = "4.3.0" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1b113239362cee7093bfb250467138f079a2a03673181dc15bff6ccd677912d" +checksum = "fe51db00ac3aa9f950d1e6201a126acfa26e6d81bc4a183ba64ec02effcad883" dependencies = [ "bytemuck", "bytemuck_derive", @@ -7313,7 +7313,7 @@ checksum = "ed1c0d16d6fdeba12291a1f068cdf0d479d9bff1141bf44afd7aa9d485f65ef8" dependencies = [ "sha3", "solana-define-syscall 4.0.1", - "solana-hash 4.3.0", + "solana-hash 4.4.0", ] [[package]] @@ -7417,7 +7417,7 @@ dependencies = [ "solana-epoch-schedule", "solana-genesis-config", "solana-genesis-utils", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-leader-schedule", @@ -7534,7 +7534,7 @@ dependencies = [ "solana-genesis-config", "solana-gossip", "solana-hard-forks", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-leader-schedule", "solana-ledger", @@ -7582,7 +7582,7 @@ name = "solana-merkle-tree" version = "4.2.0-alpha.0" dependencies = [ "fast-math", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-sha256-hasher", ] @@ -7597,7 +7597,7 @@ dependencies = [ "serde", "serde_derive", "solana-address 2.6.1", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-sanitize", "solana-sdk-ids", @@ -7673,7 +7673,7 @@ dependencies = [ "serde", "serde_derive", "solana-fee-calculator", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-pubkey 4.2.0", "solana-sha256-hasher", "wincode", @@ -7686,7 +7686,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cda945f4ccb317791c26379ca8ec410c5938aa7a5eff211fe5fe0bb428c3a75f" dependencies = [ "solana-account 4.3.0", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-nonce", "solana-sdk-ids", "wincode", @@ -7758,7 +7758,7 @@ dependencies = [ "rayon", "serde", "solana-clock", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-message", "solana-metrics", @@ -7789,7 +7789,7 @@ dependencies = [ "qualifier_attr", "solana-clock", "solana-entry", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-leader-schedule", "solana-ledger", "solana-measure", @@ -7921,7 +7921,7 @@ dependencies = [ "solana-epoch-rewards", "solana-epoch-schedule", "solana-fee-structure", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-last-restart-slot", "solana-loader-v3-interface", @@ -7976,7 +7976,7 @@ dependencies = [ "solana-epoch-schedule", "solana-fee-calculator", "solana-genesis-config", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-loader-v3-interface", @@ -8166,7 +8166,7 @@ dependencies = [ "solana-faucet", "solana-genesis-config", "solana-gossip", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-leader-schedule", "solana-ledger", @@ -8236,7 +8236,7 @@ dependencies = [ "solana-epoch-info", "solana-epoch-schedule", "solana-feature-gate-interface", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-message", "solana-pubkey 4.2.0", @@ -8274,7 +8274,7 @@ version = "4.2.0-alpha.0" dependencies = [ "solana-account 4.3.0", "solana-commitment-config", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-message", "solana-nonce", "solana-pubkey 4.2.0", @@ -8372,7 +8372,7 @@ dependencies = [ "solana-fee-structure", "solana-genesis-config", "solana-hard-forks", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-inflation", "solana-instruction", "solana-keypair", @@ -8443,7 +8443,7 @@ dependencies = [ "agave-transaction-view", "solana-compute-budget", "solana-compute-budget-instruction", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-message", "solana-program-entrypoint", "solana-pubkey 4.2.0", @@ -8566,7 +8566,7 @@ dependencies = [ "crossbeam-channel", "itertools 0.14.0", "log", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-measure", "solana-metrics", @@ -8618,7 +8618,7 @@ checksum = "db7dc3011ea4c0334aaaa7e7128cb390ecf546b28d412e9bf2064680f57f588f" dependencies = [ "sha2 0.10.9", "solana-define-syscall 4.0.1", - "solana-hash 4.3.0", + "solana-hash 4.4.0", ] [[package]] @@ -8649,7 +8649,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6c79722e299d957958bf33695f7cd1ef6724ff55563c60fd9e3e24487cccde2" dependencies = [ "solana-hard-forks", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-sha256-hasher", ] @@ -8698,7 +8698,7 @@ checksum = "0a57c158c35629f9e302ab385f16b15813f4927a31c27dda72f3df828bb08d93" dependencies = [ "serde", "serde_derive", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-sdk-ids", "solana-sysvar-id", ] @@ -8791,7 +8791,7 @@ dependencies = [ "protobuf-src", "serde", "solana-account-decoder", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-message", "solana-pubkey 4.2.0", @@ -8856,7 +8856,7 @@ dependencies = [ "solana-compute-budget", "solana-compute-budget-program", "solana-fee-structure", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-instruction-error", "solana-instructions-sysvar", @@ -8927,7 +8927,7 @@ dependencies = [ name = "solana-svm-transaction" version = "4.2.0-alpha.0" dependencies = [ - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-message", "solana-pubkey 4.2.0", "solana-sdk-ids", @@ -8957,7 +8957,7 @@ dependencies = [ "solana-clock", "solana-cpi", "solana-curve25519 4.0.1", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-hash-512", "solana-instruction", "solana-keccak-hasher", @@ -9040,7 +9040,7 @@ version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a39522012be4127884bc611d65f58f1cf33a3afb7baee7e6774be90b48edc3c" dependencies = [ - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-message", "solana-pubkey 4.2.0", @@ -9066,7 +9066,7 @@ dependencies = [ "solana-epoch-rewards", "solana-epoch-schedule", "solana-fee-calculator", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-last-restart-slot", "solana-program-entrypoint", @@ -9174,7 +9174,7 @@ dependencies = [ "solana-commitment-config", "solana-connection-cache", "solana-epoch-info", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-message", "solana-net-utils", @@ -9258,7 +9258,7 @@ dependencies = [ "serde", "serde_derive", "solana-address 2.6.1", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-instruction-error", "solana-message", @@ -9315,7 +9315,7 @@ dependencies = [ "solana-account-decoder", "solana-address-lookup-table-interface", "solana-clock", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-loader-v2-interface", "solana-loader-v3-interface", @@ -9384,7 +9384,7 @@ dependencies = [ "solana-cost-model", "solana-entry", "solana-gossip", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-leader-schedule", "solana-ledger", @@ -9427,7 +9427,7 @@ version = "4.2.0-alpha.0" dependencies = [ "assert_matches", "solana-clock", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-pubkey 4.2.0", "solana-runtime-transaction", "solana-transaction", @@ -9498,7 +9498,7 @@ dependencies = [ "solana-bincode", "solana-bls-signatures", "solana-clock", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-packet 4.1.0", @@ -9527,7 +9527,7 @@ dependencies = [ "serde_derive", "serde_with", "solana-clock", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-instruction-error", "solana-pubkey 4.2.0", @@ -9551,7 +9551,7 @@ dependencies = [ "solana-bls-signatures", "solana-clock", "solana-epoch-schedule", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-packet 4.1.0", "solana-program-runtime", @@ -10120,7 +10120,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -11097,7 +11097,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/dev-bins/Cargo.toml b/dev-bins/Cargo.toml index 3b26d146f2b..2c32c7fc883 100644 --- a/dev-bins/Cargo.toml +++ b/dev-bins/Cargo.toml @@ -88,7 +88,7 @@ solana-genesis = { path = "../genesis", version = "=4.2.0-alpha.0", features = [ solana-genesis-config = "4.0.0" solana-genesis-utils = { path = "../genesis-utils", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-geyser-plugin-manager = { path = "../geyser-plugin-manager", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-hash = "4.3.0" +solana-hash = "4.4.0" solana-inflation = "3.1.0" solana-instruction = "3.4.0" solana-instruction-error = "2.3.0" diff --git a/install/Cargo.toml b/install/Cargo.toml index 016a0a1c127..85edd66434f 100644 --- a/install/Cargo.toml +++ b/install/Cargo.toml @@ -41,7 +41,7 @@ serde = { workspace = true } serde_yaml = { workspace = true } solana-clap-utils = { workspace = true } solana-config-interface = { version = "=2.0.0", features = ["bincode"] } -solana-hash = "=4.3.0" +solana-hash = "=4.4.0" solana-keypair = "=3.1.2" solana-message = "=4.2.0" solana-pubkey = { version = "=4.2.0", default-features = false } diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index 9f3d091658e..71ed14646f3 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -82,7 +82,7 @@ version = "4.2.0-alpha.0" dependencies = [ "ahash 0.8.12", "solana-epoch-schedule", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-pubkey 4.2.0", "solana-sha256-hasher", @@ -109,7 +109,7 @@ version = "4.2.0-alpha.0" dependencies = [ "log", "solana-clock", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-message", "solana-signature", "solana-transaction", @@ -219,7 +219,7 @@ dependencies = [ "solana-accounts-db", "solana-clock", "solana-genesis-config", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-lattice-hash", "solana-measure", "solana-metrics", @@ -236,7 +236,7 @@ dependencies = [ name = "agave-transaction-view" version = "4.2.0-alpha.0" dependencies = [ - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-message", "solana-packet 4.1.0", "solana-program-runtime", @@ -294,7 +294,7 @@ dependencies = [ "solana-genesis-utils", "solana-geyser-plugin-manager", "solana-gossip", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-inflation", "solana-keypair", "solana-ledger", @@ -354,7 +354,7 @@ dependencies = [ "solana-connection-cache", "solana-epoch-schedule", "solana-gossip", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-leader-schedule", "solana-ledger", @@ -387,7 +387,7 @@ dependencies = [ "solana-bls-signatures", "solana-clock", "solana-epoch-schedule", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-pubkey 4.2.0", "solana-short-vec", "solana-signer-store", @@ -5234,7 +5234,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.52.0", + "windows-sys 0.61.0", ] [[package]] @@ -5729,7 +5729,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] @@ -5880,7 +5880,7 @@ dependencies = [ "solana-clock", "solana-epoch-schedule", "solana-fee-calculator", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-lattice-hash", "solana-measure", @@ -5978,7 +5978,7 @@ dependencies = [ "solana-banks-interface", "solana-clock", "solana-commitment-config", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-message", "solana-program-pack", "solana-pubkey 4.2.0", @@ -6002,7 +6002,7 @@ dependencies = [ "solana-account 4.3.0", "solana-clock", "solana-commitment-config", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-message", "solana-pubkey 4.2.0", "solana-signature", @@ -6024,7 +6024,7 @@ dependencies = [ "solana-banks-interface", "solana-clock", "solana-commitment-config", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-message", "solana-net-utils", "solana-pubkey 4.2.0", @@ -6070,7 +6070,7 @@ checksum = "7116e1d942a2432ca3f514625104757ab8a56233787e95144c93950029e31176" dependencies = [ "blake3", "solana-define-syscall 4.0.1", - "solana-hash 4.3.0", + "solana-hash 4.4.0", ] [[package]] @@ -6201,7 +6201,7 @@ dependencies = [ "agave-feature-set", "solana-bpf-loader-program", "solana-compute-budget-program", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-program-runtime", "solana-pubkey 4.2.0", "solana-sdk-ids", @@ -6234,7 +6234,7 @@ dependencies = [ "solana-cluster-type", "solana-commitment-config", "solana-derivation-path", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-message", "solana-native-token", @@ -6289,7 +6289,7 @@ dependencies = [ "solana-clock", "solana-epoch-info", "solana-epoch-schedule", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-message", "solana-native-token", "solana-packet 4.1.0", @@ -6318,7 +6318,7 @@ dependencies = [ "log", "solana-commitment-config", "solana-connection-cache", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-measure", "solana-message", @@ -6350,7 +6350,7 @@ dependencies = [ "solana-account 4.3.0", "solana-commitment-config", "solana-epoch-info", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-message", @@ -6383,7 +6383,7 @@ checksum = "3a494cf8eda7d98d9f0144b288bb409c88308d2e86f15cc1045aa77b83304718" dependencies = [ "serde", "serde_derive", - "solana-hash 4.3.0", + "solana-hash 4.4.0", ] [[package]] @@ -6544,7 +6544,7 @@ dependencies = [ "solana-geyser-plugin-manager", "solana-gossip", "solana-hard-forks", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-leader-schedule", @@ -6747,7 +6747,7 @@ dependencies = [ "solana-bls-signatures", "solana-clock", "solana-cost-model", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-measure", "solana-merkle-tree", "solana-message", @@ -6780,7 +6780,7 @@ checksum = "1cddf2388b28291210d9aa60690740733cab527531f06ed153c4d388951e407c" dependencies = [ "serde", "serde_derive", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-sdk-ids", "solana-sdk-macro", "solana-sysvar-id", @@ -6794,7 +6794,7 @@ checksum = "1ee8beac9bff4db9225e57d532d169b0be5e447f1e6601a2f50f27a01bf5518f" dependencies = [ "siphasher 0.3.11", "solana-address 2.6.1", - "solana-hash 4.3.0", + "solana-hash 4.4.0", ] [[package]] @@ -6828,7 +6828,7 @@ checksum = "0eb265ff95e28eceda117e2e3d2d2a611ecbbfe911dfeeeecd1521814540ffab" dependencies = [ "serde", "serde_derive", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-nonce", "solana-pubkey 4.2.0", @@ -6844,7 +6844,7 @@ dependencies = [ "crossbeam-channel", "log", "solana-cli-output", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-message", @@ -6940,7 +6940,7 @@ dependencies = [ "solana-cluster-type", "solana-epoch-schedule", "solana-fee-calculator", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-inflation", "solana-keypair", "solana-poh-config", @@ -6961,7 +6961,7 @@ dependencies = [ "log", "solana-download-utils", "solana-genesis-config", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-rpc-client", "thiserror 2.0.18", ] @@ -6985,7 +6985,7 @@ dependencies = [ "solana-clock", "solana-entry", "solana-gossip", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-ledger", "solana-measure", "solana-message", @@ -7027,7 +7027,7 @@ dependencies = [ "solana-clock", "solana-entry", "solana-epoch-schedule", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-ledger", "solana-measure", @@ -7072,14 +7072,14 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "337c246447142f660f778cf6cb582beba8e28deb05b3b24bfb9ffd7c562e5f41" dependencies = [ - "solana-hash 4.3.0", + "solana-hash 4.4.0", ] [[package]] name = "solana-hash" -version = "4.3.0" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1b113239362cee7093bfb250467138f079a2a03673181dc15bff6ccd677912d" +checksum = "fe51db00ac3aa9f950d1e6201a126acfa26e6d81bc4a183ba64ec02effcad883" dependencies = [ "borsh", "bytemuck", @@ -7163,7 +7163,7 @@ checksum = "ed1c0d16d6fdeba12291a1f068cdf0d479d9bff1141bf44afd7aa9d485f65ef8" dependencies = [ "sha3", "solana-define-syscall 4.0.1", - "solana-hash 4.3.0", + "solana-hash 4.4.0", ] [[package]] @@ -7267,7 +7267,7 @@ dependencies = [ "solana-epoch-schedule", "solana-genesis-config", "solana-genesis-utils", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-leader-schedule", @@ -7364,7 +7364,7 @@ name = "solana-merkle-tree" version = "4.2.0-alpha.0" dependencies = [ "fast-math", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-sha256-hasher", ] @@ -7379,7 +7379,7 @@ dependencies = [ "serde", "serde_derive", "solana-address 2.6.1", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-sanitize", "solana-sdk-ids", @@ -7455,7 +7455,7 @@ dependencies = [ "serde", "serde_derive", "solana-fee-calculator", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-pubkey 4.2.0", "solana-sha256-hasher", "wincode", @@ -7468,7 +7468,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cda945f4ccb317791c26379ca8ec410c5938aa7a5eff211fe5fe0bb428c3a75f" dependencies = [ "solana-account 4.3.0", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-nonce", "solana-sdk-ids", "wincode", @@ -7539,7 +7539,7 @@ dependencies = [ "rand 0.9.4", "rayon", "serde", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-message", "solana-metrics", "solana-packet 4.1.0", @@ -7563,7 +7563,7 @@ dependencies = [ "qualifier_attr", "solana-clock", "solana-entry", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-leader-schedule", "solana-ledger", "solana-measure", @@ -7638,7 +7638,7 @@ dependencies = [ "solana-epoch-stake", "solana-example-mocks", "solana-fee-calculator", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-instruction-error", "solana-instructions-sysvar", @@ -7744,7 +7744,7 @@ dependencies = [ "solana-epoch-rewards", "solana-epoch-schedule", "solana-fee-structure", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-last-restart-slot", "solana-loader-v3-interface", @@ -7799,7 +7799,7 @@ dependencies = [ "solana-epoch-schedule", "solana-fee-calculator", "solana-genesis-config", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-loader-v3-interface", @@ -7989,7 +7989,7 @@ dependencies = [ "solana-faucet", "solana-genesis-config", "solana-gossip", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-leader-schedule", "solana-ledger", @@ -8059,7 +8059,7 @@ dependencies = [ "solana-epoch-info", "solana-epoch-schedule", "solana-feature-gate-interface", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-message", "solana-pubkey 4.2.0", @@ -8097,7 +8097,7 @@ version = "4.2.0-alpha.0" dependencies = [ "solana-account 4.3.0", "solana-commitment-config", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-message", "solana-nonce", "solana-pubkey 4.2.0", @@ -8195,7 +8195,7 @@ dependencies = [ "solana-fee-structure", "solana-genesis-config", "solana-hard-forks", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-inflation", "solana-instruction", "solana-keypair", @@ -8266,7 +8266,7 @@ dependencies = [ "agave-transaction-view", "solana-compute-budget", "solana-compute-budget-instruction", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-message", "solana-program-entrypoint", "solana-pubkey 4.2.0", @@ -8306,7 +8306,7 @@ dependencies = [ "solana-fee", "solana-fee-calculator", "solana-fee-structure", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-loader-v3-interface", @@ -8865,7 +8865,7 @@ name = "solana-sbf-rust-secp256k1-recover" version = "4.2.0-alpha.0" dependencies = [ "libsecp256k1", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keccak-hasher", "solana-msg", "solana-program-entrypoint", @@ -8878,7 +8878,7 @@ version = "4.2.0-alpha.0" dependencies = [ "blake3", "solana-blake3-hasher", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keccak-hasher", "solana-msg", "solana-program-entrypoint", @@ -9104,7 +9104,7 @@ dependencies = [ "crossbeam-channel", "itertools 0.14.0", "log", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-measure", "solana-metrics", @@ -9156,7 +9156,7 @@ checksum = "db7dc3011ea4c0334aaaa7e7128cb390ecf546b28d412e9bf2064680f57f588f" dependencies = [ "sha2 0.10.9", "solana-define-syscall 4.0.1", - "solana-hash 4.3.0", + "solana-hash 4.4.0", ] [[package]] @@ -9187,7 +9187,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6c79722e299d957958bf33695f7cd1ef6724ff55563c60fd9e3e24487cccde2" dependencies = [ "solana-hard-forks", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-sha256-hasher", ] @@ -9236,7 +9236,7 @@ checksum = "0a57c158c35629f9e302ab385f16b15813f4927a31c27dda72f3df828bb08d93" dependencies = [ "serde", "serde_derive", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-sdk-ids", "solana-sysvar-id", ] @@ -9328,7 +9328,7 @@ dependencies = [ "protobuf-src", "serde", "solana-account-decoder", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-message", "solana-pubkey 4.2.0", @@ -9393,7 +9393,7 @@ dependencies = [ "solana-compute-budget", "solana-compute-budget-program", "solana-fee-structure", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-instruction-error", "solana-instructions-sysvar", @@ -9464,7 +9464,7 @@ dependencies = [ name = "solana-svm-transaction" version = "4.2.0-alpha.0" dependencies = [ - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-message", "solana-pubkey 4.2.0", "solana-sdk-ids", @@ -9494,7 +9494,7 @@ dependencies = [ "solana-clock", "solana-cpi", "solana-curve25519 4.0.1", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-hash-512", "solana-instruction", "solana-keccak-hasher", @@ -9577,7 +9577,7 @@ version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a39522012be4127884bc611d65f58f1cf33a3afb7baee7e6774be90b48edc3c" dependencies = [ - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-message", "solana-pubkey 4.2.0", @@ -9605,7 +9605,7 @@ dependencies = [ "solana-epoch-rewards", "solana-epoch-schedule", "solana-fee-calculator", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-last-restart-slot", "solana-program-entrypoint", @@ -9767,7 +9767,7 @@ dependencies = [ "serde", "serde_derive", "solana-address 2.6.1", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-instruction-error", "solana-message", @@ -9824,7 +9824,7 @@ dependencies = [ "solana-account-decoder", "solana-address-lookup-table-interface", "solana-clock", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-loader-v2-interface", "solana-loader-v3-interface", @@ -9893,7 +9893,7 @@ dependencies = [ "solana-cost-model", "solana-entry", "solana-gossip", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-leader-schedule", "solana-ledger", @@ -9936,7 +9936,7 @@ version = "4.2.0-alpha.0" dependencies = [ "assert_matches", "solana-clock", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-pubkey 4.2.0", "solana-runtime-transaction", "solana-transaction", @@ -10007,7 +10007,7 @@ dependencies = [ "solana-bincode", "solana-bls-signatures", "solana-clock", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-packet 4.1.0", @@ -10036,7 +10036,7 @@ dependencies = [ "serde_derive", "serde_with", "solana-clock", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-instruction-error", "solana-pubkey 4.2.0", @@ -10060,7 +10060,7 @@ dependencies = [ "solana-bls-signatures", "solana-clock", "solana-epoch-schedule", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-packet 4.1.0", "solana-program-runtime", @@ -10617,7 +10617,7 @@ dependencies = [ "getrandom 0.3.1", "once_cell", "rustix 1.1.4", - "windows-sys 0.52.0", + "windows-sys 0.61.0", ] [[package]] diff --git a/programs/sbf/Cargo.toml b/programs/sbf/Cargo.toml index 990e184639d..a29a1d930af 100644 --- a/programs/sbf/Cargo.toml +++ b/programs/sbf/Cargo.toml @@ -115,7 +115,7 @@ solana-cpi = "=3.1.0" solana-curve25519 = "=4.0.1" solana-define-syscall = "=3.0.0" solana-fee = { path = "../../fee", version = "=4.2.0-alpha.0" } -solana-hash = { version = "=4.3.0", features = ["bytemuck", "serde", "std"] } +solana-hash = { version = "=4.4.0", features = ["bytemuck", "serde", "std"] } solana-instruction = "=3.4.0" solana-instructions-sysvar = "=3.0.0" solana-keccak-hasher = { version = "=3.1.0", features = ["sha3"] } diff --git a/watchtower/Cargo.toml b/watchtower/Cargo.toml index 52bc6021d70..055510618be 100644 --- a/watchtower/Cargo.toml +++ b/watchtower/Cargo.toml @@ -23,7 +23,7 @@ log = { workspace = true } solana-clap-utils = { workspace = true } solana-cli-config = { workspace = true } solana-cli-output = { workspace = true } -solana-hash = "=4.3.0" +solana-hash = "=4.4.0" solana-metrics = { workspace = true } solana-native-token = "=3.0.0" solana-notifier = { workspace = true } From ba9aaf52c1146442cf27b949fffce82c733fcdc0 Mon Sep 17 00:00:00 2001 From: Akhilesh Singhania Date: Mon, 8 Jun 2026 18:09:20 +0200 Subject: [PATCH 242/576] rewards: prints data if assertion fires (#13051) --- runtime/src/bank/partitioned_epoch_rewards/calculation.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/runtime/src/bank/partitioned_epoch_rewards/calculation.rs b/runtime/src/bank/partitioned_epoch_rewards/calculation.rs index 5c77851c412..fa39573dfc1 100644 --- a/runtime/src/bank/partitioned_epoch_rewards/calculation.rs +++ b/runtime/src/bank/partitioned_epoch_rewards/calculation.rs @@ -320,7 +320,11 @@ impl Bank { >= distributed_lamports + distributed_to_incinerator_lamports + burned_lamports - + total_stake_rewards_lamports + + total_stake_rewards_lamports, + "point_value={point_value:?}, distributed_lamports={distributed_lamports}, \ + distributed_to_incinerator_lamports={distributed_to_incinerator_lamports} \ + burned_lamports={burned_lamports}, \ + total_stake_rewards_lamports={total_stake_rewards_lamports}" ); info!( "distributed reward commissions: {} out of {}, remaining {}", From 98b0575a6485a014bf8d4f0fdbce23be5b1a6e08 Mon Sep 17 00:00:00 2001 From: steviez Date: Mon, 8 Jun 2026 11:37:00 -0500 Subject: [PATCH 243/576] Add testing section to pull request template (#13052) --- .github/PULL_REQUEST_TEMPLATE.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index cd44ff2b883..28e8c24f3dc 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -4,4 +4,7 @@ #### Summary of Changes +#### Testing + + Fixes # From b28275ac62f40b09991b50bf0691e4503ffb1eee Mon Sep 17 00:00:00 2001 From: Edvard Fagerholm Date: Mon, 8 Jun 2026 20:04:57 +0300 Subject: [PATCH 244/576] test(validator): bound previously-unbounded test-only channels (#13042) test: bound previously-unbounded test-only channels in validator Replace unbounded() with crossbeam_channel::bounded(1024) at all test-only channel call sites. Production channels are left unbounded. --- validator/src/admin_rpc_service.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/validator/src/admin_rpc_service.rs b/validator/src/admin_rpc_service.rs index 13366d1598e..90b1d334966 100644 --- a/validator/src/admin_rpc_service.rs +++ b/validator/src/admin_rpc_service.rs @@ -1103,7 +1103,7 @@ mod tests { agave_snapshots::snapshot_config::SnapshotConfig, agave_votor::event::VotorEventSender, assert_matches::assert_matches, - crossbeam_channel::unbounded, + crossbeam_channel::bounded, serde_json::Value, solana_accounts_db::{ accounts_db::{ACCOUNTS_DB_CONFIG_FOR_TESTING, AccountsDbConfig}, @@ -1169,7 +1169,7 @@ mod tests { }, }); - let (snapshot_request_sender, _) = unbounded(); + let (snapshot_request_sender, _) = bounded(1024); let snapshot_controller = Arc::new(SnapshotController::new( snapshot_request_sender.clone(), SnapshotConfig::default(), @@ -1183,7 +1183,7 @@ mod tests { let start_progress = Arc::new(RwLock::new(ValidatorStartProgress::default())); let repair_whitelist = Arc::new(RwLock::new(HashSet::new())); let votor_event_sender = config.votor_event_sender.unwrap_or_else(|| { - let (votor_event_sender, _) = unbounded(); + let (votor_event_sender, _) = bounded(1024); votor_event_sender }); let meta = AdminRpcRequestMetadata { @@ -1243,7 +1243,7 @@ mod tests { // Bank but without validator. #[test] fn test_set_identity() { - let (votor_event_sender, votor_event_receiver) = unbounded(); + let (votor_event_sender, votor_event_receiver) = bounded(1024); let rpc = RpcHandler::start_with_config(TestConfig { account_indexes: AccountSecondaryIndexes::default(), votor_event_sender: Some(votor_event_sender), From 8d30984e70f94fc413776b9c4b657f2bb0cc4434 Mon Sep 17 00:00:00 2001 From: Edvard Fagerholm Date: Mon, 8 Jun 2026 20:17:09 +0300 Subject: [PATCH 245/576] test(votor): bound previously-unbounded test-only channels (#13043) test: bound previously-unbounded test-only channels in votor Replace unbounded() with crossbeam_channel::bounded(1024) at all test-only channel call sites. Production channels are left unbounded. --- votor/src/commitment.rs | 4 ++-- votor/src/consensus_metrics.rs | 4 ++-- votor/src/consensus_pool_service.rs | 16 ++++++++-------- votor/src/event_handler.rs | 18 +++++++++--------- votor/src/timer_manager.rs | 4 ++-- votor/src/timer_manager/timers.rs | 4 ++-- votor/src/voting_service.rs | 5 +++-- votor/src/voting_utils.rs | 22 +++++++++++----------- 8 files changed, 39 insertions(+), 38 deletions(-) diff --git a/votor/src/commitment.rs b/votor/src/commitment.rs index 06bfc23febf..8a49dfff09e 100644 --- a/votor/src/commitment.rs +++ b/votor/src/commitment.rs @@ -45,11 +45,11 @@ pub fn update_commitment_cache( #[cfg(test)] mod tests { - use {super::*, crossbeam_channel::unbounded}; + use {super::*, crossbeam_channel::bounded}; #[test] fn test_update_commitment_cache() { - let (commitment_sender, commitment_receiver) = unbounded(); + let (commitment_sender, commitment_receiver) = bounded(1024); let slot = 3; let commitment_type = CommitmentType::Notarize; update_commitment_cache(commitment_type, slot, &commitment_sender) diff --git a/votor/src/consensus_metrics.rs b/votor/src/consensus_metrics.rs index fc74514f1a6..2b4310d41a3 100644 --- a/votor/src/consensus_metrics.rs +++ b/votor/src/consensus_metrics.rs @@ -343,14 +343,14 @@ mod tests { use { super::*, agave_votor_messages::vote::{SkipVote, Vote}, - crossbeam_channel::unbounded, + crossbeam_channel::bounded, solana_keypair::Keypair, solana_signer::Signer, std::thread::sleep, }; fn new_metrics() -> ConsensusMetrics { - let (_, rx) = unbounded(); + let (_, rx) = bounded(1024); ConsensusMetrics::new(EpochSchedule::custom(100, 100, false), rx) // 100 slots/epoch } diff --git a/votor/src/consensus_pool_service.rs b/votor/src/consensus_pool_service.rs index d52d6138809..0bf68a14058 100644 --- a/votor/src/consensus_pool_service.rs +++ b/votor/src/consensus_pool_service.rs @@ -631,7 +631,7 @@ mod tests { consensus_message::{BLS_KEYPAIR_DERIVE_SEED, VoteMessage}, vote::Vote, }, - crossbeam_channel::unbounded, + crossbeam_channel::bounded, solana_bls_signatures::{ BLS_SIGNATURE_AFFINE_SIZE, keypair::Keypair as BLSKeypair, signature::Signature as BLSSignature, @@ -666,7 +666,7 @@ mod tests { impl Default for TestContext { fn default() -> Self { - let (bls_sender, bls_receiver) = crossbeam_channel::unbounded(); + let (bls_sender, bls_receiver) = bounded(1024); // Create 10 node validatorvotekeypairs vec let validator_keypairs = (0..10) .map(|_| ValidatorVoteKeypairs::new_rand()) @@ -871,7 +871,7 @@ mod tests { #[test] fn test_send_produce_block_event() { let mut ctx = TestContext::default(); - let (repair_event_sender, _repair_event_receiver) = unbounded(); + let (repair_event_sender, _repair_event_receiver) = bounded(1024); // Find when is the next leader slot for me (validator 0) let next_leader_slot = ctx @@ -915,9 +915,9 @@ mod tests { sharable_banks: ctx.sharable_banks.clone(), leader_schedule_cache: ctx.leader_schedule_cache.clone(), vote_history_highest_parent_ready: None, - consensus_message_receiver: crossbeam_channel::unbounded().1, + consensus_message_receiver: bounded(1024).1, bls_sender: ctx.bls_sender.clone(), - event_sender: crossbeam_channel::unbounded().0, + event_sender: bounded(1024).0, highest_finalized: ctx.highest_finalized.clone(), repair_event_sender, }; @@ -956,9 +956,9 @@ mod tests { #[test] fn test_can_produce_window_immediately_on_restart() { let ctx = TestContext::default(); - let (repair_event_sender, _repair_event_receiver) = unbounded(); - let (event_sender, event_receiver) = unbounded(); - let (consensus_message_sender, consensus_message_receiver) = unbounded(); + let (repair_event_sender, _repair_event_receiver) = bounded(1024); + let (event_sender, event_receiver) = bounded(1024); + let (consensus_message_sender, consensus_message_receiver) = bounded(1024); let root_bank = ctx.sharable_banks.root(); let next_leader_slot = ctx diff --git a/votor/src/event_handler.rs b/votor/src/event_handler.rs index 67bb521405d..bcc85bbfa86 100644 --- a/votor/src/event_handler.rs +++ b/votor/src/event_handler.rs @@ -937,7 +937,7 @@ mod tests { consensus_message::{BLS_KEYPAIR_DERIVE_SEED, SigVerifiedBatch, VoteMessage}, vote::Vote, }, - crossbeam_channel::{Receiver, Sender, TryRecvError, unbounded}, + crossbeam_channel::{Receiver, Sender, TryRecvError, bounded}, parking_lot::RwLock as PlRwLock, solana_bls_signatures::{ keypair::Keypair as BLSKeypair, signature::Signature as BLSSignature, @@ -1035,15 +1035,15 @@ mod tests { } fn setup() -> EventHandlerTestContext { - let (bls_sender, bls_receiver) = unbounded(); - let (commitment_sender, commitment_receiver) = unbounded(); - let (own_vote_sender, own_vote_receiver) = unbounded(); - let (drop_bank_sender, drop_bank_receiver) = unbounded(); + let (bls_sender, bls_receiver) = bounded(1024); + let (commitment_sender, commitment_receiver) = bounded(1024); + let (own_vote_sender, own_vote_receiver) = bounded(1024); + let (drop_bank_sender, drop_bank_receiver) = bounded(1024); let exit = Arc::new(AtomicBool::new(false)); - let (event_sender, _event_receiver) = unbounded(); - let (consensus_metrics_sender, consensus_metrics_receiver) = unbounded(); - let (leader_window_info_sender, leader_window_info_receiver) = unbounded(); - let (repair_event_sender, repair_event_receiver) = unbounded(); + let (event_sender, _event_receiver) = bounded(1024); + let (consensus_metrics_sender, consensus_metrics_receiver) = bounded(1024); + let (leader_window_info_sender, leader_window_info_receiver) = bounded(1024); + let (repair_event_sender, repair_event_receiver) = bounded(1024); let latest_switch_request = LatestSwitchRequest::default(); let timer_manager = Arc::new(PlRwLock::new(TimerManager::new( event_sender, diff --git a/votor/src/timer_manager.rs b/votor/src/timer_manager.rs index e3b8ce252e8..3f830f12dbc 100644 --- a/votor/src/timer_manager.rs +++ b/votor/src/timer_manager.rs @@ -86,13 +86,13 @@ impl TimerManager { #[cfg(test)] mod tests { use { - super::*, crate::event::VotorEvent, crossbeam_channel::unbounded, + super::*, crate::event::VotorEvent, crossbeam_channel::bounded, solana_clock::DEFAULT_MS_PER_SLOT, std::time::Duration, }; #[test] fn test_timer_manager() { - let (event_sender, event_receiver) = unbounded(); + let (event_sender, event_receiver) = bounded(1024); let exit = Arc::new(AtomicBool::new(false)); let timer_manager = TimerManager::new( event_sender, diff --git a/votor/src/timer_manager/timers.rs b/votor/src/timer_manager/timers.rs index 1fd9846a820..eb775cb1951 100644 --- a/votor/src/timer_manager/timers.rs +++ b/votor/src/timer_manager/timers.rs @@ -275,7 +275,7 @@ impl Timers { #[cfg(test)] mod tests { use { - super::*, crate::common::DELTA_TIMEOUT, crossbeam_channel::unbounded, + super::*, crate::common::DELTA_TIMEOUT, crossbeam_channel::bounded, solana_clock::DEFAULT_MS_PER_SLOT, }; @@ -331,7 +331,7 @@ mod tests { fn timers_progress() { let one_micro = Duration::from_micros(1); let mut now = Instant::now(); - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); let mut timers = Timers::new(one_micro, sender); assert!(timers.progress(now).is_none()); assert!(receiver.try_recv().unwrap_err().is_empty()); diff --git a/votor/src/voting_service.rs b/votor/src/voting_service.rs index e3af4017776..09cf5db9fa9 100644 --- a/votor/src/voting_service.rs +++ b/votor/src/voting_service.rs @@ -259,6 +259,7 @@ mod tests { consensus_message::{ConsensusMessage, VoteMessage}, vote::Vote, }, + crossbeam_channel::bounded, solana_bls_signatures::{BLS_SIGNATURE_AFFINE_SIZE, Signature as BLSSignature}, solana_gossip::{cluster_info::ClusterInfo, contact_info::ContactInfo}, solana_keypair::Keypair, @@ -351,7 +352,7 @@ mod tests { }))] fn test_send_message(bls_op: BLSOp, expected_message: ConsensusMessage) { agave_logger::setup(); - let (bls_sender, bls_receiver) = crossbeam_channel::unbounded(); + let (bls_sender, bls_receiver) = bounded(1024); // Create listener thread on a random port we allocated and return SocketAddr to create VotingService // Bind to a random UDP port @@ -365,7 +366,7 @@ mod tests { assert!(bls_sender.send(bls_op).is_ok()); // Start a quick streamer to handle quick control packets - let (sender, receiver) = crossbeam_channel::unbounded(); + let (sender, receiver) = bounded(1024); let stakes = validator_keypairs .iter() .map(|x| (x.node_keypair.pubkey(), 100)) diff --git a/votor/src/voting_utils.rs b/votor/src/voting_utils.rs index 20869c995b2..44b0a78a680 100644 --- a/votor/src/voting_utils.rs +++ b/votor/src/voting_utils.rs @@ -306,7 +306,7 @@ mod tests { use { super::*, agave_votor_messages::consensus_message::Block, - crossbeam_channel::unbounded, + crossbeam_channel::bounded, solana_hash::Hash, solana_runtime::{ bank::{Bank, SlotLeader}, @@ -359,9 +359,9 @@ mod tests { let my_keys = &validator_keypairs[my_index]; let sharable_banks = bank_forks.read().unwrap().sharable_banks(); - let bls_sender = unbounded().0; - let commitment_sender = unbounded().0; - let consensus_metrics_sender = unbounded().0; + let bls_sender = bounded(1024).0; + let commitment_sender = bounded(1024).0; + let consensus_metrics_sender = bounded(1024).0; let voting_context = VotingContext { vote_history: VoteHistory::new(my_keys.node_keypair.pubkey(), 0), vote_account_pubkey: my_keys.vote_keypair.pubkey(), @@ -382,7 +382,7 @@ mod tests { #[test] fn test_generate_own_vote_message() { - let (own_vote_sender, own_vote_receiver) = crossbeam_channel::unbounded(); + let (own_vote_sender, own_vote_receiver) = bounded(1024); // Create 10 node validatorvotekeypairs vec let validator_keypairs = (0..10) .map(|_| ValidatorVoteKeypairs::new(Keypair::new(), Keypair::new(), Keypair::new())) @@ -439,7 +439,7 @@ mod tests { #[test] fn test_wait_to_vote_slot() { - let (own_vote_sender, _own_vote_receiver) = crossbeam_channel::unbounded(); + let (own_vote_sender, _own_vote_receiver) = bounded(1024); // Create 10 node validatorvotekeypairs vec let validator_keypairs = (0..10) .map(|_| ValidatorVoteKeypairs::new(Keypair::new(), Keypair::new(), Keypair::new())) @@ -468,7 +468,7 @@ mod tests { #[test] fn test_non_voting_node() { - let (own_vote_sender, _own_vote_receiver) = crossbeam_channel::unbounded(); + let (own_vote_sender, _own_vote_receiver) = bounded(1024); // Create 10 node validatorvotekeypairs vec let validator_keypairs = (0..10) .map(|_| ValidatorVoteKeypairs::new(Keypair::new(), Keypair::new(), Keypair::new())) @@ -506,7 +506,7 @@ mod tests { #[test] fn test_wrong_identity_keypair() { - let (own_vote_sender, _own_vote_receiver) = crossbeam_channel::unbounded(); + let (own_vote_sender, _own_vote_receiver) = bounded(1024); // Create 10 node validatorvotekeypairs vec let validator_keypairs = (0..10) .map(|_| ValidatorVoteKeypairs::new(Keypair::new(), Keypair::new(), Keypair::new())) @@ -537,7 +537,7 @@ mod tests { #[test] fn test_wrong_vote_account_pubkey() { - let (own_vote_sender, _own_vote_receiver) = crossbeam_channel::unbounded(); + let (own_vote_sender, _own_vote_receiver) = bounded(1024); // Create 10 node validatorvotekeypairs vec let validator_keypairs = (0..10) .map(|_| ValidatorVoteKeypairs::new(Keypair::new(), Keypair::new(), Keypair::new())) @@ -571,7 +571,7 @@ mod tests { #[should_panic(expected = "could not find rank map for slot 1000000000")] fn test_panic_on_future_slot() { agave_logger::setup(); - let (own_vote_sender, _own_vote_receiver) = crossbeam_channel::unbounded(); + let (own_vote_sender, _own_vote_receiver) = bounded(1024); // Create 10 node validatorvotekeypairs vec let validator_keypairs = (0..10) .map(|_| ValidatorVoteKeypairs::new(Keypair::new(), Keypair::new(), Keypair::new())) @@ -591,7 +591,7 @@ mod tests { #[test] fn test_zero_staked_validator_fails_voting() { agave_logger::setup(); - let (own_vote_sender, _own_vote_receiver) = crossbeam_channel::unbounded(); + let (own_vote_sender, _own_vote_receiver) = bounded(1024); // Create 10 node validatorvotekeypairs vec let validator_keypairs = (0..10) .map(|_| ValidatorVoteKeypairs::new(Keypair::new(), Keypair::new(), Keypair::new())) From 7451e2144314e780640df2ba21a9be6828a1aa8d Mon Sep 17 00:00:00 2001 From: Edvard Fagerholm Date: Mon, 8 Jun 2026 20:37:01 +0300 Subject: [PATCH 246/576] test(local-cluster): bound previously-unbounded test-only channels (#13032) test: bound previously-unbounded test-only channels in local-cluster Replace unbounded() with crossbeam_channel::bounded(1024) at all test-only channel call sites. Production channels are left unbounded. --- local-cluster/src/cluster_tests.rs | 3 ++- local-cluster/tests/local_cluster.rs | 14 +++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/local-cluster/src/cluster_tests.rs b/local-cluster/src/cluster_tests.rs index 855dc4d5dba..89c6cab12ba 100644 --- a/local-cluster/src/cluster_tests.rs +++ b/local-cluster/src/cluster_tests.rs @@ -6,6 +6,7 @@ use log::*; use { crate::{cluster::QuicTpuClient, local_cluster::LocalCluster}, agave_votor_messages::consensus_message::ConsensusMessage, + crossbeam_channel::bounded, rand::{Rng, rng}, rayon::{ThreadPool, prelude::*}, solana_client::connection_cache::ConnectionCache, @@ -445,7 +446,7 @@ pub fn start_quic_streamer_to_listen_for_votes_and_certs( JoinHandle<()>, crossbeam_channel::Receiver, ) { - let (sender, receiver) = crossbeam_channel::unbounded(); + let (sender, receiver) = bounded(1024); let cancel = CancellationToken::new(); let stakes = validator_keys .iter() diff --git a/local-cluster/tests/local_cluster.rs b/local-cluster/tests/local_cluster.rs index 2b82b8a0391..94ab61b8af7 100644 --- a/local-cluster/tests/local_cluster.rs +++ b/local-cluster/tests/local_cluster.rs @@ -7,7 +7,7 @@ use { agave_votor::voting_service::{AlpenglowPortOverride, VotingServiceOverride}, agave_votor_messages::migration::MIGRATION_SLOT_OFFSET, assert_matches::assert_matches, - crossbeam_channel::{Receiver, unbounded}, + crossbeam_channel::{Receiver, bounded}, gag::BufferRedirect, itertools::Itertools, log::*, @@ -2539,8 +2539,8 @@ fn test_run_test_load_program_accounts_partition_root() { num_slots_per_validator, ]); - let (update_client_sender, update_client_receiver) = unbounded(); - let (scan_client_sender, scan_client_receiver) = unbounded(); + let (update_client_sender, update_client_receiver) = bounded(1024); + let (scan_client_sender, scan_client_receiver) = bounded(1024); let exit = Arc::new(AtomicBool::new(false)); let (t_update, t_scan, additional_accounts) = setup_transfer_scan_threads( @@ -2980,8 +2980,8 @@ fn run_test_load_program_accounts(scan_commitment: CommitmentConfig) { let num_starting_accounts = 100; let exit = Arc::new(AtomicBool::new(false)); - let (update_client_sender, update_client_receiver) = unbounded(); - let (scan_client_sender, scan_client_receiver) = unbounded(); + let (update_client_sender, update_client_receiver) = bounded(1024); + let (scan_client_sender, scan_client_receiver) = bounded(1024); // Setup the update/scan threads let (t_update, t_scan, starting_accounts) = setup_transfer_scan_threads( @@ -3826,7 +3826,7 @@ fn run_duplicate_shreds_broadcast_leader(vote_on_duplicate: bool) { // for the partition. assert!(partition_node_stake < our_node_stake && partition_node_stake < good_node_stake); - let (duplicate_slot_sender, duplicate_slot_receiver) = unbounded(); + let (duplicate_slot_sender, duplicate_slot_receiver) = bounded(1024); // 1) Set up the cluster let (mut cluster, validator_keys) = test_faulty_node( @@ -5371,7 +5371,7 @@ fn test_duplicate_shreds_switch_failure() { let leader_schedule = create_custom_leader_schedule(validator_to_slots.into_iter()); // 1) Set up the cluster - let (duplicate_slot_sender, duplicate_slot_receiver) = unbounded(); + let (duplicate_slot_sender, duplicate_slot_receiver) = bounded(1024); let validator_configs = validator_keypairs .into_iter() .map(|(validator_keys, in_genesis)| { From ffe236b685a5374500c90017814de7ab67afe6c3 Mon Sep 17 00:00:00 2001 From: steviez Date: Tue, 9 Jun 2026 00:06:05 -0500 Subject: [PATCH 247/576] geyser: Remove metrics counter from plugin-manager (#13064) The counter is at debug level so effectively disabled; just rip it out --- Cargo.lock | 1 - dev-bins/Cargo.lock | 1 - geyser-plugin-manager/Cargo.toml | 1 - geyser-plugin-manager/src/deshred_transaction_notifier.rs | 7 ------- programs/sbf/Cargo.lock | 1 - 5 files changed, 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e278ac5e0b1..a266bb37a19 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8425,7 +8425,6 @@ dependencies = [ "solana-ledger", "solana-measure", "solana-message", - "solana-metrics", "solana-pubkey 4.2.0", "solana-rpc", "solana-runtime", diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 0c7a00690ca..a71e2ca2f5d 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -7140,7 +7140,6 @@ dependencies = [ "solana-ledger", "solana-measure", "solana-message", - "solana-metrics", "solana-pubkey 4.2.0", "solana-rpc", "solana-runtime", diff --git a/geyser-plugin-manager/Cargo.toml b/geyser-plugin-manager/Cargo.toml index cbdbb948be7..46fc359f46b 100644 --- a/geyser-plugin-manager/Cargo.toml +++ b/geyser-plugin-manager/Cargo.toml @@ -35,7 +35,6 @@ solana-hash = { workspace = true } solana-ledger = { workspace = true } solana-measure = { workspace = true } solana-message = { workspace = true } -solana-metrics = { workspace = true } solana-pubkey = { workspace = true } solana-rpc = { workspace = true } solana-runtime = { workspace = true } diff --git a/geyser-plugin-manager/src/deshred_transaction_notifier.rs b/geyser-plugin-manager/src/deshred_transaction_notifier.rs index e9538905b54..301ed28e1b7 100644 --- a/geyser-plugin-manager/src/deshred_transaction_notifier.rs +++ b/geyser-plugin-manager/src/deshred_transaction_notifier.rs @@ -10,7 +10,6 @@ use { solana_ledger::deshred_transaction_notifier_interface::DeshredTransactionNotifier, solana_measure::measure::Measure, solana_message::v0::LoadedAddresses, - solana_metrics::*, solana_signature::Signature, solana_transaction::versioned::VersionedTransaction, std::sync::Arc, @@ -77,12 +76,6 @@ impl DeshredTransactionNotifier for DeshredTransactionNotifierImpl { } } measure.stop(); - inc_new_counter_debug!( - "geyser-plugin-notify_plugins_of_deshred_transaction_info-us", - measure.as_us() as usize, - 10000, - 10000 - ); } fn alt_resolution_enabled(&self) -> bool { diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index 71ed14646f3..7e0d67d131a 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -6989,7 +6989,6 @@ dependencies = [ "solana-ledger", "solana-measure", "solana-message", - "solana-metrics", "solana-pubkey 4.2.0", "solana-rpc", "solana-runtime", From f9ec90a60cf52df1aaca81168e43030f57746d6c Mon Sep 17 00:00:00 2001 From: Kamil Skalski Date: Tue, 9 Jun 2026 08:25:49 +0200 Subject: [PATCH 248/576] clippy: allow new_without_default for workspaces (#12972) --- Cargo.toml | 4 ++++ dev-bins/Cargo.toml | 4 ++++ fs/src/buffered_reader.rs | 1 - fs/src/io_uring/sequential_file_reader.rs | 1 - local-cluster/src/integration_tests.rs | 1 - 5 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d588879bf19..cf30a1e0504 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -156,11 +156,15 @@ check-cfg = [ # Clippy lint configuration that can not be applied in clippy.toml [workspace.lints.clippy] +# Denied lints arithmetic_side_effects = "deny" default_trait_access = "deny" manual_let_else = "deny" used_underscore_binding = "deny" +# Allowed lints +new_without_default = "allow" + [workspace.dependencies] Inflector = "0.11.4" agave-banking-stage-ingress-types = { path = "banking-stage-ingress-types", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } diff --git a/dev-bins/Cargo.toml b/dev-bins/Cargo.toml index 2c32c7fc883..3106c2ca859 100644 --- a/dev-bins/Cargo.toml +++ b/dev-bins/Cargo.toml @@ -21,11 +21,15 @@ check-cfg = [ # Clippy lint configuration that can not be applied in clippy.toml [workspace.lints.clippy] +# Denied lints arithmetic_side_effects = "deny" default_trait_access = "deny" manual_let_else = "deny" used_underscore_binding = "deny" +# Allowed lints +new_without_default = "allow" + [workspace.dependencies] agave-feature-set = { path = "../feature-set", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } agave-logger = { path = "../logger", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } diff --git a/fs/src/buffered_reader.rs b/fs/src/buffered_reader.rs index 584d9fb55b1..f6ea8765952 100644 --- a/fs/src/buffered_reader.rs +++ b/fs/src/buffered_reader.rs @@ -146,7 +146,6 @@ pub struct BufferedReader<'a, const N: usize> { } impl<'a, const N: usize> BufferedReader<'a, N> { - #[allow(clippy::new_without_default)] pub const fn new() -> Self { Self { file_offset_of_next_read: 0, diff --git a/fs/src/io_uring/sequential_file_reader.rs b/fs/src/io_uring/sequential_file_reader.rs index bfe4b283868..3daf6082bbd 100644 --- a/fs/src/io_uring/sequential_file_reader.rs +++ b/fs/src/io_uring/sequential_file_reader.rs @@ -48,7 +48,6 @@ pub struct SequentialFileReaderBuilder<'sp> { } impl<'sp> SequentialFileReaderBuilder<'sp> { - #[allow(clippy::new_without_default)] pub fn new() -> Self { Self { read_capacity: DEFAULT_READ_SIZE, diff --git a/local-cluster/src/integration_tests.rs b/local-cluster/src/integration_tests.rs index 4b12ec0a938..1a361a29508 100644 --- a/local-cluster/src/integration_tests.rs +++ b/local-cluster/src/integration_tests.rs @@ -74,7 +74,6 @@ pub struct ValidatorKeys { } impl ValidatorKeys { - #[allow(clippy::new_without_default)] pub fn new() -> Self { Self { node_keypair: Arc::new(Keypair::new()), From fa333f539f15b9f5f4bfa5a19a4b963bc40a61f7 Mon Sep 17 00:00:00 2001 From: Zeref <148282517+0xzrf@users.noreply.github.com> Date: Tue, 9 Jun 2026 13:11:10 +0530 Subject: [PATCH 249/576] feat(multinode-demo): adds solana-transaction-bench as a primary in scripts (#12877) Migrate multinode-demo and net load-test scripts to support solana-transaction-bench while retaining solana-bench-tps during the transition. Add a local txs-bench wrapper, wire transaction-bench into net client orchestration, stage the external binary for deployments, and keep legacy bench-tps account generation scoped to bench-tps clients. --- multinode-demo/bench-tps.sh | 18 ++- multinode-demo/common.sh | 5 +- multinode-demo/txs-bench.sh | 215 ++++++++++++++++++++++++++++++++++++ net/net.sh | 103 ++++++++++++++--- net/remote/remote-client.sh | 139 +++++++++++++++++++++++ 5 files changed, 460 insertions(+), 20 deletions(-) create mode 100755 multinode-demo/txs-bench.sh diff --git a/multinode-demo/bench-tps.sh b/multinode-demo/bench-tps.sh index 32f15810931..0a7d3e1a2ac 100755 --- a/multinode-demo/bench-tps.sh +++ b/multinode-demo/bench-tps.sh @@ -5,20 +5,34 @@ here=$(dirname "$0") # shellcheck source=multinode-demo/common.sh source "$here"/common.sh +deprecation_notice() { + echo "WARNING: bench-tps is deprecated and will be removed in a future release." + echo "Please use multinode-demo/txs-bench.sh / solana-transaction-bench instead." +} + usage() { - if [[ -n $1 ]]; then - echo "$*" + declare message=${1:-} + if [[ -n $message ]]; then + echo "$message" echo fi echo "usage: $0 [extra args]" echo echo " Run bench-tps " echo + deprecation_notice + echo echo " extra args: additional arguments are passed along to solana-bench-tps" echo exit 1 } +if [[ ${1:-} = "-h" || ${1:-} = "--help" ]]; then + usage "" +fi + +deprecation_notice >&2 + args=("$@") default_arg --url "http://127.0.0.1:8899" default_arg --faucet "127.0.0.1:9900" diff --git a/multinode-demo/common.sh b/multinode-demo/common.sh index 67e44e3cf6c..e29755262b7 100644 --- a/multinode-demo/common.sh +++ b/multinode-demo/common.sh @@ -5,10 +5,12 @@ # The following directive disable complaints about unused variables in this # file: # shellcheck disable=2034 +# shellcheck shell=bash # here="$(cd "$(dirname "${BASH_SOURCE[0]}")"/.. || exit 1; pwd)" -# shellcheck source=net/common.sh +# shellcheck disable=SC1091 +# shellcheck source=../net/common.sh source "$here"/net/common.sh prebuild= @@ -64,6 +66,7 @@ else fi solana_bench_tps=$(solana_program bench-tps) +solana_transaction_bench=${SOLANA_TRANSACTION_BENCH:-solana-transaction-bench} solana_faucet=$(solana_program faucet) agave_validator=$(solana_program validator) solana_genesis=$(solana_program genesis) diff --git a/multinode-demo/txs-bench.sh b/multinode-demo/txs-bench.sh new file mode 100755 index 00000000000..6cbb1c9c55d --- /dev/null +++ b/multinode-demo/txs-bench.sh @@ -0,0 +1,215 @@ +#!/usr/bin/env bash +set -e + +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=multinode-demo/common.sh +source "$here"/common.sh + +program="${solana_transaction_bench}" +default_staked_identity_file_count=4 + +usage() { + if [[ -n $1 ]]; then + echo "$*" + echo + fi + cat < + + Examples: + $0 --duration 30 --target-tps 5000 + $0 --num-payers 1024 --send-fanout 2 ws-leader-tracker + +EOF + exit 1 +} + +contains_arg() { + declare name=$1 + shift + + for arg in "$@"; do + if [[ $arg = "$name" || $arg = "$name="* ]]; then + return 0 + fi + done + + return 1 +} + +contains_url_arg() { + for arg in "$@"; do + if [[ $arg = --url || $arg = --url=* || $arg = -u || $arg = -u?* ]]; then + return 0 + fi + done + + return 1 +} + +add_default_arg() { + declare -n target_args=$1 + declare name=$2 + declare value=$3 + + if ! contains_arg "$name" "${target_args[@]}"; then + target_args+=("$name" "$value") + fi +} + +get_rpc_url() { + declare arg + declare -i expect_value=0 + + for arg in "${global_args[@]}"; do + if ((expect_value)); then + printf '%s' "$arg" + return 0 + fi + case "$arg" in + --url=*) + printf '%s' "${arg#--url=}" + return 0 + ;; + -u?*) + printf '%s' "${arg#-u}" + return 0 + ;; + --url | -u) + expect_value=1 + ;; + esac + done + + printf '%s' 'http://127.0.0.1:8899' +} + +# Query getClusterNodes over RPC and return host:port for the bootstrap validator's +# QUIC TPU (tpuQuic), falling back to UDP tpu. Requires curl and jq. +resolve_pinned_leader_target() { + declare rpc_url=$1 + declare identity="${SOLANA_CONFIG_DIR}/bootstrap-validator/identity.json" + declare identity_pubkey= + declare nodes_json tpu + + if [[ -f $identity ]]; then + identity_pubkey=$($solana_keygen pubkey "$identity" 2> /dev/null) || identity_pubkey= + fi + + if ! command -v jq > /dev/null 2>&1; then + echo "$0: jq is required to parse getClusterNodes (e.g. brew install jq)" >&2 + return 1 + fi + + if ! nodes_json=$(curl -sf -X POST -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","id":1,"method":"getClusterNodes"}' \ + "$rpc_url"); then + echo "$0: getClusterNodes request to $rpc_url failed (is the validator RPC up?)" >&2 + return 1 + fi + + if [[ -n $identity_pubkey ]]; then + tpu=$(echo "$nodes_json" | jq -r --arg pk "$identity_pubkey" ' + [.result[] | select(.pubkey == $pk) | .tpuQuic // .tpu // empty][0] // empty + ') + fi + + if [[ -z $tpu || $tpu = null ]]; then + tpu=$(echo "$nodes_json" | jq -r ' + [.result[] | .tpuQuic // .tpu // empty][0] // empty + ') + fi + + if [[ -z $tpu || $tpu = null ]]; then + echo "$0: getClusterNodes returned no tpuQuic/tpu address" >&2 + return 1 + fi + + printf '%s' "$tpu" +} + +global_args=() +run_args=() +leader_tracker_args=() + +while [[ -n $1 ]]; do + case "$1" in + -h | --help) + usage + ;; + --url | -u | --commitment-config | --authority) + [[ -n ${2:-} ]] || usage "Missing value for $1" + global_args+=("$1" "$2") + shift 2 + ;; + --url=* | --commitment-config=* | --authority=* | -u?*) + global_args+=("$1") + shift + ;; + --validate-accounts | --mock-rpc) + global_args+=("$1") + shift + ;; + run | read-accounts-run | write-accounts) + usage "Do not pass a solana-transaction-bench subcommand; this wrapper always uses run." + ;; + pinned-leader-tracker | legacy-leader-tracker | ws-leader-tracker | \ + yellowstone-leader-tracker | custom-leader-tracker) + leader_tracker_args=("$@") + break + ;; + *) + run_args+=("$1") + shift + ;; + esac +done + +if ! command -v "$program" > /dev/null 2>&1; then + echo "$program not found." + echo "Install it with: cargo install solana-transaction-bench" + echo "Or set SOLANA_TRANSACTION_BENCH=/path/to/solana-transaction-bench." + exit 1 +fi + +if ! contains_url_arg "${global_args[@]}"; then + global_args+=(--url "http://127.0.0.1:8899") +fi +add_default_arg global_args --authority "${SOLANA_CONFIG_DIR}/faucet.json" + +add_default_arg run_args --num-payers 256 +add_default_arg run_args --payer-account-balance 10SOL +add_default_arg run_args --duration 90 +add_default_arg run_args --bind "127.0.0.1:0" + +for ((i = 0; i < default_staked_identity_file_count; i++)); do + run_args+=(--staked-identity-file "${SOLANA_CONFIG_DIR}/bootstrap-validator/identity.json") +done + +add_default_arg run_args --transfer-tx-cu-budget 600 + +if [[ ${#leader_tracker_args[@]} -eq 0 ]]; then + rpc_url=$(get_rpc_url) + pinned_leader_target=$(resolve_pinned_leader_target "$rpc_url") || exit 1 + leader_tracker_args=(pinned-leader-tracker "$pinned_leader_target") +fi + +"$program" "${global_args[@]}" run "${run_args[@]}" "${leader_tracker_args[@]}" diff --git a/net/net.sh b/net/net.sh index 3ba42e3f592..28f7acdbe83 100755 --- a/net/net.sh +++ b/net/net.sh @@ -15,19 +15,22 @@ usage() { fi CLIENT_OPTIONS=$(cat << EOM -c clientType=numClients=extraArgs - Number of clientTypes to start. This options can be specified - more than once. Defaults to bench-tps for all clients if not - specified. + more than once. Defaults to transaction-bench for all clients + if not specified. Valid client types are: idle - bench-tps + transaction-bench + bench-tps (deprecated) User can optionally provide extraArgs that are transparently supplied to the client program as command line parameters. + extraArgs use the argument names of the selected client program + (solana-transaction-bench or, for bench-tps, solana-bench-tps). For example, - -c bench-tps=2="--tx_count 25000" - This will start 2 bench-tps clients, and supply "--tx_count 25000" - to the bench-tps client. + -c transaction-bench=2="--target-tps 5000 ws-leader-tracker" + This will start 2 solana-transaction-bench clients, and supply + "--target-tps 5000 ws-leader-tracker" to each of them. --use-unstaked-connection - Use unstaked connection. By default, staked connection with - bootstrap node credendials is used. + bootstrap node credentials is used. EOM ) cat <> "$logFile" 2>&1 || { cat "$logFile" @@ -441,11 +451,15 @@ startClient() { } startClients() { - for ((i=0; i < "$numClients" && i < "$numClientsRequested"; i++)) do - if [[ $i -lt "$numBenchTpsClients" ]]; then - startClient "${clientIpList[$i]}" "solana-bench-tps" "$i" + benchTpsIndex=0 + for ((i=0; i < numClients && i < numClientsRequested; i++)) do + if [[ $i -lt $numTransactionBenchClients ]]; then + startClient "${clientIpList[$i]}" "solana-transaction-bench" "$i" + elif [[ $i -lt $((numTransactionBenchClients + numBenchTpsClients)) ]]; then + startClient "${clientIpList[$i]}" "solana-bench-tps" "$benchTpsIndex" + ((benchTpsIndex++)) else - startClient "${clientIpList[$i]}" "idle" + startClient "${clientIpList[$i]}" "idle" "$i" fi done } @@ -511,6 +525,52 @@ getNodeType() { exit 1 } +# solana-transaction-bench lives in the external anza-xyz/tpu-tools repository +# and is therefore not produced by the agave build. When transaction-bench +# clients are requested, stage the binary into the deploy artifacts so it ships +# to the nodes alongside the agave binaries (clients fetch ~/.cargo/bin/* from +# the entrypoint). +stageTransactionBenchBinary() { + [[ $numTransactionBenchClients -gt 0 ]] || return 0 + + declare destDir + case $deployMethod in + local) destDir="$SOLANA_ROOT"/farf/bin ;; + tar) destDir="$SOLANA_ROOT"/solana-release/bin ;; + *) return 0 ;; + esac + mkdir -p "$destDir" + + if [[ -x "$destDir"/solana-transaction-bench ]]; then + echo "solana-transaction-bench already staged in $destDir" + return 0 + fi + + declare src="${SOLANA_TRANSACTION_BENCH:-}" + if [[ -z $src ]]; then + src="$(command -v solana-transaction-bench || true)" + fi + if [[ -n $src && -x $src ]]; then + echo "Staging solana-transaction-bench from $src" + cp -f "$src" "$destDir"/solana-transaction-bench + return 0 + fi + + echo "solana-transaction-bench not found locally, installing it from crates.io" + declare -a cargoArgs=(install --root "$SOLANA_ROOT"/farf --locked) + [[ -z ${SOLANA_TRANSACTION_BENCH_VERSION:-} ]] || cargoArgs+=(--version "$SOLANA_TRANSACTION_BENCH_VERSION") + cargoArgs+=(solana-transaction-bench) + if cargo "${cargoArgs[@]}"; then + if [[ $destDir != "$SOLANA_ROOT"/farf/bin ]]; then + cp -f "$SOLANA_ROOT"/farf/bin/solana-transaction-bench "$destDir"/solana-transaction-bench + fi + else + echo "Warning: failed to stage solana-transaction-bench." + echo " Clients will try to install it themselves at start time." + echo " Alternatively set SOLANA_TRANSACTION_BENCH to a prebuilt binary path before deploying." + fi +} + prepareDeploy() { case $deployMethod in tar) @@ -547,6 +607,8 @@ prepareDeploy() { ;; esac + stageTransactionBenchBinary + if [[ -n $deployIfNewer ]]; then if [[ $deployMethod != tar ]]; then echo "Error: --deploy-if-newer only supported for tar deployments" @@ -764,6 +826,8 @@ nodeAddress= numIdleClients=0 numBenchTpsClients=0 benchTpsExtraArgs= +numTransactionBenchClients=0 +transactionBenchExtraArgs= failOnValidatorBootupFailure=true genesisOptions= numValidatorsRequested= @@ -992,6 +1056,10 @@ while getopts "h?T:t:o:f:rc:Fn:i:d" opt "${shortArgs[@]}"; do numIdleClients=$numClients # $extraArgs ignored for 'idle' ;; + transaction-bench) + numTransactionBenchClients=$numClients + transactionBenchExtraArgs=$extraArgs + ;; bench-tps) numBenchTpsClients=$numClients benchTpsExtraArgs=$extraArgs @@ -1028,9 +1096,10 @@ if [[ -n $numValidatorsRequested ]]; then fi numClients=${#clientIpList[@]} -numClientsRequested=$((numBenchTpsClients + numIdleClients)) +numClientsRequested=$((numTransactionBenchClients + numBenchTpsClients + numIdleClients)) if [[ "$numClientsRequested" -eq 0 ]]; then - numBenchTpsClients=$numClients + # Default to solana-transaction-bench on every available client node. + numTransactionBenchClients=$numClients numClientsRequested=$numClients else if [[ "$numClientsRequested" -gt "$numClients" ]]; then diff --git a/net/remote/remote-client.sh b/net/remote/remote-client.sh index c66f3c78388..ea145a49e33 100755 --- a/net/remote/remote-client.sh +++ b/net/remote/remote-client.sh @@ -57,7 +57,146 @@ case "$clientType" in ;; esac +# Does "$@" (beyond the first arg, which is the name to look for) contain $name, +# either as a bare flag or as --flag=value? +contains_arg() { + declare name=$1 + shift + declare arg + for arg in "$@"; do + if [[ $arg = "$name" || $arg = "$name="* ]]; then + return 0 + fi + done + return 1 +} + +contains_url_arg() { + declare arg + for arg in "$@"; do + case "$arg" in + --url | --url=* | -u | -u?*) + return 0 + ;; + esac + done + return 1 +} + +# Ensure the external solana-transaction-bench binary is available. Prefer a +# path provided via SOLANA_TRANSACTION_BENCH, then one already on PATH (e.g. +# rsynced from the entrypoint's ~/.cargo/bin), and finally fall back to +# installing it from crates.io. +ensureTransactionBench() { + transactionBench="${SOLANA_TRANSACTION_BENCH:-solana-transaction-bench}" + if command -v "$transactionBench" > /dev/null 2>&1; then + return 0 + fi + + echo "$transactionBench not found, installing solana-transaction-bench from crates.io" + declare -a cargoArgs=(install --locked) + [[ -z ${SOLANA_TRANSACTION_BENCH_VERSION:-} ]] || cargoArgs+=(--version "$SOLANA_TRANSACTION_BENCH_VERSION") + cargoArgs+=(solana-transaction-bench) + if ! cargo "${cargoArgs[@]}"; then + echo "Error: unable to obtain solana-transaction-bench." + echo "Stage it into the entrypoint's ~/.cargo/bin or set SOLANA_TRANSACTION_BENCH to a prebuilt binary path." + exit 1 + fi + transactionBench=solana-transaction-bench +} + case $clientToRun in +solana-transaction-bench) + ensureTransactionBench + + # transaction-bench creates and funds ephemeral payer accounts from the + # faucet (the genesis mint) at runtime, so unlike bench-tps it needs no + # pre-generated client account keys. We only need the faucet keypair as the + # funding authority and, for a staked QUIC connection, a staked validator + # identity. + net/scripts/rsync-retry.sh -vPrc \ + "$entrypointIp":~/solana/config/faucet.json ./faucet.json + + net/scripts/rsync-retry.sh -vPrc \ + "$entrypointIp":~/solana/config/validator-identity-1.json ./validator-identity.json + + if [[ $clientType = rpc-client ]]; then + echo "Note: clientType=rpc-client is ignored by solana-transaction-bench; it always sends over QUIC to the TPU." + fi + + # solana-transaction-bench expects its arguments in positional buckets: + # run + # Split the operator-supplied extra args into those buckets so that, e.g., + # a --url override lands before "run" while --duration lands after it. + globalArgs=() + runArgs=() + leaderTrackerArgs=() + + # shellcheck disable=SC2206 # Intentional word-splitting of the extra args string + extraArgs=($benchTpsExtraArgs) + argIndex=0 + while [[ $argIndex -lt ${#extraArgs[@]} ]]; do + arg="${extraArgs[$argIndex]}" + case "$arg" in + -u | --url | --authority | --commitment-config) + if [[ $((argIndex + 1)) -ge ${#extraArgs[@]} ]]; then + echo "Error: missing value for $arg in extra args" + exit 1 + fi + globalArgs+=("$arg" "${extraArgs[$((argIndex + 1))]}") + argIndex=$((argIndex + 2)) + ;; + --url=* | --authority=* | --commitment-config=* | -u?*) + globalArgs+=("$arg") + argIndex=$((argIndex + 1)) + ;; + --validate-accounts | --mock-rpc) + globalArgs+=("$arg") + argIndex=$((argIndex + 1)) + ;; + run | read-accounts-run | write-accounts) + echo "Error: do not pass a solana-transaction-bench subcommand in extra args; this wrapper always uses 'run'." + exit 1 + ;; + pinned-leader-tracker | legacy-leader-tracker | ws-leader-tracker | \ + yellowstone-leader-tracker | custom-leader-tracker) + leaderTrackerArgs=("${extraArgs[@]:$argIndex}") + break + ;; + *) + runArgs+=("$arg") + argIndex=$((argIndex + 1)) + ;; + esac + done + + contains_url_arg "${globalArgs[@]}" || globalArgs+=(--url "http://$entrypointIp:8899") + contains_arg --authority "${globalArgs[@]}" || globalArgs+=(--authority ./faucet.json) + + contains_arg --duration "${runArgs[@]}" || runArgs+=(--duration 7500) + contains_arg --num-payers "${runArgs[@]}" || runArgs+=(--num-payers 256) + contains_arg --payer-account-balance "${runArgs[@]}" || runArgs+=(--payer-account-balance 10SOL) + contains_arg --transfer-tx-cu-budget "${runArgs[@]}" || runArgs+=(--transfer-tx-cu-budget 600) + + if [[ -z "$maybeUseUnstakedConnection" ]]; then + contains_arg --staked-identity-file "${runArgs[@]}" || runArgs+=(--staked-identity-file ./validator-identity.json) + fi + + # In a multinode cluster the leader rotates, so follow the leader schedule + # over the RPC websocket by default. Operators can override by passing a + # leader-tracker subcommand in the extra args. + if [[ ${#leaderTrackerArgs[@]} -eq 0 ]]; then + leaderTrackerArgs=(ws-leader-tracker) + fi + + clientCommand="\ + $transactionBench \ + ${globalArgs[*]} \ + run \ + ${runArgs[*]} \ + ${leaderTrackerArgs[*]} \ + " + ;; solana-bench-tps) net/scripts/rsync-retry.sh -vPrc \ "$entrypointIp":~/solana/config/bench-tps"$clientIndex".yml ./client-accounts.yml From 4e523f2a7ef8102e4935ade1e94202a88098a0fc Mon Sep 17 00:00:00 2001 From: Kamil Skalski Date: Tue, 9 Jun 2026 09:51:20 +0200 Subject: [PATCH 250/576] feat: switch SlotHashes sysvar to wincode (de)serialization (#13046) Switch SlotHashes sysvar to wincode (de)serialization --- Cargo.lock | 4 ++++ account-decoder/Cargo.toml | 3 ++- account-decoder/src/parse_sysvar.rs | 22 ++++++++++++---------- core/src/banking_stage/vote_storage.rs | 4 ++-- dev-bins/Cargo.lock | 4 ++++ program-runtime/Cargo.toml | 3 ++- program-runtime/src/sysvar_cache.rs | 4 ++-- programs/sbf/Cargo.lock | 4 ++++ runtime/Cargo.toml | 2 +- runtime/src/bank.rs | 2 +- runtime/src/bank/tests.rs | 4 ++-- syscalls/Cargo.toml | 3 ++- syscalls/src/lib.rs | 4 ++-- 13 files changed, 40 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a266bb37a19..e10d643d92c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6776,6 +6776,7 @@ dependencies = [ "spl-token-interface", "spl-token-metadata-interface", "thiserror 2.0.18", + "wincode", "zstd", ] @@ -9445,6 +9446,7 @@ dependencies = [ "solana-transaction-context", "test-case", "thiserror 2.0.18", + "wincode", ] [[package]] @@ -10346,6 +10348,7 @@ dependencies = [ "solana-hash 4.4.0", "solana-sdk-ids", "solana-sysvar-id", + "wincode", ] [[package]] @@ -10720,6 +10723,7 @@ dependencies = [ "solana-transaction-context", "test-case", "thiserror 2.0.18", + "wincode", ] [[package]] diff --git a/account-decoder/Cargo.toml b/account-decoder/Cargo.toml index 578b5275a8c..02d7d7b2e00 100644 --- a/account-decoder/Cargo.toml +++ b/account-decoder/Cargo.toml @@ -41,7 +41,7 @@ solana-program-pack = { workspace = true } solana-pubkey = { workspace = true } solana-rent = { workspace = true } solana-sdk-ids = { workspace = true } -solana-slot-hashes = { workspace = true } +solana-slot-hashes = { workspace = true, features = ["wincode"] } solana-slot-history = { workspace = true } solana-stake-interface = { workspace = true, features = ["bincode", "sysvar"] } solana-sysvar = { workspace = true } @@ -52,6 +52,7 @@ spl-token-group-interface = { workspace = true } spl-token-interface = { workspace = true } spl-token-metadata-interface = { workspace = true } thiserror = { workspace = true } +wincode = { workspace = true } zstd = { workspace = true } [dev-dependencies] diff --git a/account-decoder/src/parse_sysvar.rs b/account-decoder/src/parse_sysvar.rs index 36219ec2594..b225036a126 100644 --- a/account-decoder/src/parse_sysvar.rs +++ b/account-decoder/src/parse_sysvar.rs @@ -56,16 +56,18 @@ pub fn parse_sysvar(data: &[u8], pubkey: &Pubkey) -> Result(data).ok().map(|slot_hashes| { - let slot_hashes = slot_hashes - .iter() - .map(|slot_hash| UiSlotHashEntry { - slot: slot_hash.0, - hash: slot_hash.1.to_string(), - }) - .collect(); - SysvarAccountType::SlotHashes(slot_hashes) - }) + wincode::deserialize::(data) + .ok() + .map(|slot_hashes| { + let slot_hashes = slot_hashes + .iter() + .map(|slot_hash| UiSlotHashEntry { + slot: slot_hash.0, + hash: slot_hash.1.to_string(), + }) + .collect(); + SysvarAccountType::SlotHashes(slot_hashes) + }) } else if pubkey == &sysvar::slot_history::id() { deserialize::(data).ok().map(|slot_history| { SysvarAccountType::SlotHistory(UiSlotHistory { diff --git a/core/src/banking_stage/vote_storage.rs b/core/src/banking_stage/vote_storage.rs index b1562a6edca..144667fa20f 100644 --- a/core/src/banking_stage/vote_storage.rs +++ b/core/src/banking_stage/vote_storage.rs @@ -5,7 +5,7 @@ use { ahash::HashMap, itertools::Itertools, rand::{Rng, rng}, - solana_account::from_account, + solana_account::ReadableAccount as _, solana_clock::Epoch, solana_pubkey::Pubkey, solana_runtime::{ @@ -146,7 +146,7 @@ impl VoteStorage { pub fn drain_unprocessed(&mut self, bank: &Bank) -> Vec> { let slot_hashes = bank .get_account(&sysvar::slot_hashes::id()) - .and_then(|account| from_account::(&account)); + .and_then(|account| wincode::deserialize::(account.data()).ok()); if slot_hashes.is_none() { error!( "Slot hashes sysvar doesn't exist on bank {}. Including all votes without \ diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index a71e2ca2f5d..05247b52b7a 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -5912,6 +5912,7 @@ dependencies = [ "spl-token-interface", "spl-token-metadata-interface", "thiserror 2.0.18", + "wincode", "zstd", ] @@ -7945,6 +7946,7 @@ dependencies = [ "solana-sysvar-id", "solana-transaction-context", "thiserror 2.0.18", + "wincode", ] [[package]] @@ -8700,6 +8702,7 @@ dependencies = [ "solana-hash 4.4.0", "solana-sdk-ids", "solana-sysvar-id", + "wincode", ] [[package]] @@ -8978,6 +8981,7 @@ dependencies = [ "solana-sysvar-id", "solana-transaction-context", "thiserror 2.0.18", + "wincode", ] [[package]] diff --git a/program-runtime/Cargo.toml b/program-runtime/Cargo.toml index 2662a3ec2f7..5355cf3be4d 100644 --- a/program-runtime/Cargo.toml +++ b/program-runtime/Cargo.toml @@ -59,7 +59,7 @@ solana-pubkey = { workspace = true } solana-rent = { workspace = true } solana-sbpf = { workspace = true, features = ["jit"] } solana-sdk-ids = { workspace = true } -solana-slot-hashes = { workspace = true } +solana-slot-hashes = { workspace = true, features = ["wincode"] } solana-stable-layout = { workspace = true } solana-stake-interface = { workspace = true, features = ["bincode", "sysvar"] } solana-svm-callback = { workspace = true } @@ -74,6 +74,7 @@ solana-sysvar = { workspace = true, features = ["bincode"] } solana-sysvar-id = { workspace = true } solana-transaction-context = { workspace = true } thiserror = { workspace = true } +wincode = { workspace = true } [dev-dependencies] assert_matches = { workspace = true } diff --git a/program-runtime/src/sysvar_cache.rs b/program-runtime/src/sysvar_cache.rs index a7a82da782d..bef6ef4d260 100644 --- a/program-runtime/src/sysvar_cache.rs +++ b/program-runtime/src/sysvar_cache.rs @@ -91,7 +91,7 @@ impl SysvarCache { } sysvar::slot_hashes::ID => { let slot_hashes: SlotHashes = - bincode::deserialize(&data).expect("Failed to deserialize SlotHashes sysvar."); + wincode::deserialize(&data).expect("Failed to deserialize SlotHashes sysvar."); self.slot_hashes = Some(data); self.slot_hashes_obj = Some(Arc::new(slot_hashes)); } @@ -229,7 +229,7 @@ impl SysvarCache { if self.slot_hashes.is_none() { get_account_data(&SlotHashes::id(), &mut |data: &[u8]| { - if let Ok(obj) = bincode::deserialize::(data) { + if let Ok(obj) = wincode::deserialize::(data) { self.slot_hashes = Some(data.to_vec()); self.slot_hashes_obj = Some(Arc::new(obj)); } diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index 7e0d67d131a..0d1b1697645 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -5816,6 +5816,7 @@ dependencies = [ "spl-token-interface", "spl-token-metadata-interface", "thiserror 2.0.18", + "wincode", "zstd", ] @@ -7768,6 +7769,7 @@ dependencies = [ "solana-sysvar-id", "solana-transaction-context", "thiserror 2.0.18", + "wincode", ] [[package]] @@ -9238,6 +9240,7 @@ dependencies = [ "solana-hash 4.4.0", "solana-sdk-ids", "solana-sysvar-id", + "wincode", ] [[package]] @@ -9515,6 +9518,7 @@ dependencies = [ "solana-sysvar-id", "solana-transaction-context", "thiserror 2.0.18", + "wincode", ] [[package]] diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 0f4c7984e33..cfd0bf20f10 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -152,7 +152,7 @@ solana-sha256-hasher = { workspace = true } solana-signature = { workspace = true } solana-signer = { workspace = true } solana-signer-store = { workspace = true } -solana-slot-hashes = { workspace = true } +solana-slot-hashes = { workspace = true, features = ["wincode"] } solana-slot-history = { workspace = true } solana-stake-interface = { workspace = true } solana-svm = { workspace = true, features = ["metrics"] } diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 89e2cd02526..7ff9624623c 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -2546,7 +2546,7 @@ impl Bank { self.update_sysvar_account(&sysvar::slot_hashes::id(), |account| { let mut slot_hashes = account .as_ref() - .map(|account| from_account::(account).unwrap()) + .map(|account| wincode::deserialize::(account.data()).unwrap()) .unwrap_or_default(); slot_hashes.add(self.parent_slot, self.parent_hash); create_account( diff --git a/runtime/src/bank/tests.rs b/runtime/src/bank/tests.rs index a44016ecf9e..d4f60913f49 100644 --- a/runtime/src/bank/tests.rs +++ b/runtime/src/bank/tests.rs @@ -12378,7 +12378,7 @@ fn test_new_for_txn_tests_system_transfer() { ), ( solana_sdk_ids::sysvar::slot_hashes::id(), - make_sysvar(bincode::serialize(&solana_slot_hashes::SlotHashes::default()).unwrap()), + make_sysvar(wincode::serialize(&solana_slot_hashes::SlotHashes::default()).unwrap()), ), (system_program::id(), { let mut acct = AccountSharedData::new(1, 14, &native_loader::id()); @@ -12551,7 +12551,7 @@ fn test_new_for_block_tests_with_vote_account() { ), ( solana_sdk_ids::sysvar::slot_hashes::id(), - make_sysvar(bincode::serialize(&solana_slot_hashes::SlotHashes::default()).unwrap()), + make_sysvar(wincode::serialize(&solana_slot_hashes::SlotHashes::default()).unwrap()), ), (system_program::id(), { let mut acct = AccountSharedData::new(1, 14, &native_loader::id()); diff --git a/syscalls/Cargo.toml b/syscalls/Cargo.toml index a05da9d8eb0..d8cad2fbf8d 100644 --- a/syscalls/Cargo.toml +++ b/syscalls/Cargo.toml @@ -35,7 +35,7 @@ solana-bn254 = { workspace = true } solana-clock = { workspace = true } solana-cpi = { workspace = true } solana-curve25519 = { workspace = true } -solana-hash = { workspace = true } +solana-hash = { workspace = true, features = ["wincode"] } solana-hash-512 = { workspace = true } solana-instruction = { workspace = true } solana-keccak-hasher = { workspace = true, features = ["sha3"] } @@ -57,6 +57,7 @@ solana-sysvar = { workspace = true } solana-sysvar-id = { workspace = true } solana-transaction-context = { workspace = true, features = ["bincode"] } thiserror = { workspace = true } +wincode = { workspace = true } [dev-dependencies] assert_matches = { workspace = true } diff --git a/syscalls/src/lib.rs b/syscalls/src/lib.rs index 020b69e495a..22df6344d10 100644 --- a/syscalls/src/lib.rs +++ b/syscalls/src/lib.rs @@ -4604,7 +4604,7 @@ mod tests { let src_hashes = src_hashes; let mut src_hashes_buf = vec![0; SlotHashes::size_of()]; - bincode::serialize_into(&mut src_hashes_buf, &src_hashes).unwrap(); + wincode::serialize_into(&mut src_hashes_buf, &src_hashes).unwrap(); let transaction_accounts = vec![( sysvar::slot_hashes::id(), @@ -4643,7 +4643,7 @@ mod tests { ); assert_eq!(result.unwrap(), 0); - let hashes_from_buf = bincode::deserialize::(&got_hashes_buf).unwrap(); + let hashes_from_buf = wincode::deserialize::(&got_hashes_buf).unwrap(); assert_eq!(hashes_from_buf, src_hashes); } } From 5f356cd55049b32b57a2f1589dc0d3219857de18 Mon Sep 17 00:00:00 2001 From: Joe C Date: Tue, 9 Jun 2026 15:51:57 +0800 Subject: [PATCH 251/576] svm: decouple callback traits (#13060) SVM currently requires two traits, one for transaction processing (`TransactionProcessingCallback`) and one for invoking the VM (`InvokeContextCallback`). The latter is currently a requirement of the former, but we don't actually need this constraint for any particular reason. Decouple the traits, update any function signatures to only require the trait bounds they need, and update any mock callbacks who unnecessarily implement both. --- svm-callback/src/lib.rs | 2 +- svm/src/account_loader.rs | 12 +----------- svm/src/conformance/programs.rs | 4 +--- svm/src/program_loader.rs | 3 --- svm/src/transaction_processor.rs | 8 +++++--- 5 files changed, 8 insertions(+), 21 deletions(-) diff --git a/svm-callback/src/lib.rs b/svm-callback/src/lib.rs index 4073fb37cf1..744ad2c75e3 100644 --- a/svm-callback/src/lib.rs +++ b/svm-callback/src/lib.rs @@ -33,7 +33,7 @@ pub trait InvokeContextCallback { } /// Runtime callbacks for transaction processing. -pub trait TransactionProcessingCallback: InvokeContextCallback { +pub trait TransactionProcessingCallback { fn get_account_shared_data(&self, pubkey: &Pubkey) -> Option<(AccountSharedData, Slot)>; fn inspect_account(&self, _address: &Pubkey, _account_state: AccountState, _is_writable: bool) { diff --git a/svm/src/account_loader.rs b/svm/src/account_loader.rs index 3bc26a8512c..fa81bf6cda3 100644 --- a/svm/src/account_loader.rs +++ b/svm/src/account_loader.rs @@ -321,14 +321,6 @@ impl TransactionProcessingCallback for Accoun } } -// NOTE this is a required subtrait of TransactionProcessingCallback. -// It may make sense to break out a second subtrait just for the above two functions, -// but this would be a nontrivial breaking change and require careful consideration. -impl solana_svm_callback::InvokeContextCallback - for AccountLoader<'_, CB> -{ -} - /// Set the rent epoch to u64::MAX if the account is rent exempt. /// /// TODO: This function is used to update the rent epoch of an account. Once we @@ -689,7 +681,7 @@ mod tests { }, solana_signature::Signature, solana_signer::Signer, - solana_svm_callback::{InvokeContextCallback, TransactionProcessingCallback}, + solana_svm_callback::TransactionProcessingCallback, solana_system_transaction::transfer, solana_transaction::{Transaction, sanitized::SanitizedTransaction}, solana_transaction_context::{ @@ -730,8 +722,6 @@ mod tests { } } - impl InvokeContextCallback for TestCallbacks {} - impl TransactionProcessingCallback for TestCallbacks { fn get_account_shared_data(&self, pubkey: &Pubkey) -> Option<(AccountSharedData, Slot)> { self.accounts_map diff --git a/svm/src/conformance/programs.rs b/svm/src/conformance/programs.rs index d32a50ca92c..e9e30e3f7e5 100644 --- a/svm/src/conformance/programs.rs +++ b/svm/src/conformance/programs.rs @@ -16,7 +16,7 @@ use { solana_pubkey::Pubkey, solana_rent::Rent, solana_sdk_ids::{bpf_loader, bpf_loader_deprecated, bpf_loader_upgradeable}, - solana_svm_callback::{InvokeContextCallback, TransactionProcessingCallback}, + solana_svm_callback::TransactionProcessingCallback, solana_svm_feature_set::SVMFeatureSet, solana_svm_timings::ExecuteTimings, solana_syscalls::create_program_runtime_environment, @@ -178,8 +178,6 @@ pub fn fill_program_cache_from_accounts( struct FillFromAccountsCallback<'a>(&'a [(Pubkey, Account)]); -impl InvokeContextCallback for FillFromAccountsCallback<'_> {} - impl TransactionProcessingCallback for FillFromAccountsCallback<'_> { fn get_account_shared_data(&self, pubkey: &Pubkey) -> Option<(AccountSharedData, u64)> { self.0 diff --git a/svm/src/program_loader.rs b/svm/src/program_loader.rs index 9bc992d188b..8ed45edbab9 100644 --- a/svm/src/program_loader.rs +++ b/svm/src/program_loader.rs @@ -322,7 +322,6 @@ mod tests { solana_sbpf::program::BuiltinProgram, }, solana_sdk_ids::{bpf_loader, bpf_loader_upgradeable, native_loader}, - solana_svm_callback::InvokeContextCallback, solana_svm_type_overrides::sync::atomic::AtomicU64, solana_transaction::{Transaction, sanitized::SanitizedTransaction}, std::{ @@ -347,8 +346,6 @@ mod tests { pub(crate) account_shared_data: RefCell>, } - impl InvokeContextCallback for MockBankCallback {} - impl TransactionProcessingCallback for MockBankCallback { fn get_account_shared_data(&self, pubkey: &Pubkey) -> Option<(AccountSharedData, Slot)> { self.account_shared_data.borrow().get(pubkey).cloned() diff --git a/svm/src/transaction_processor.rs b/svm/src/transaction_processor.rs index 006445a7566..1cb1f381e01 100644 --- a/svm/src/transaction_processor.rs +++ b/svm/src/transaction_processor.rs @@ -52,7 +52,7 @@ use { }, solana_pubkey::Pubkey, solana_rent::Rent, - solana_svm_callback::TransactionProcessingCallback, + solana_svm_callback::{InvokeContextCallback, TransactionProcessingCallback}, solana_svm_feature_set::SVMFeatureSet, solana_svm_log_collector::LogCollector, solana_svm_measure::{measure::Measure, measure_us}, @@ -391,7 +391,9 @@ impl TransactionBatchProcessor { } /// Main entrypoint to the SVM. - pub fn load_and_execute_sanitized_transactions( + pub fn load_and_execute_sanitized_transactions< + CB: TransactionProcessingCallback + InvokeContextCallback, + >( &self, callbacks: &CB, sanitized_txs: &[impl SVMTransaction], @@ -895,7 +897,7 @@ impl TransactionBatchProcessor { /// Execute a transaction using the provided loaded accounts and update /// the executors cache if the transaction was successful. - fn execute_loaded_transaction( + fn execute_loaded_transaction( &self, callback: &CB, tx: &impl SVMTransaction, From e7b18a8487b9c95b20e111ee77910401dacfb08b Mon Sep 17 00:00:00 2001 From: Yihau Chen Date: Tue, 9 Jun 2026 16:11:14 +0800 Subject: [PATCH 252/576] ci: trigger docs workflow on pre-release tags (#13063) --- .github/workflows/docs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 7fef5aafd94..222e22a9da9 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -7,6 +7,7 @@ on: - v[0-9]+.[0-9]+ tags: - v[0-9]+.[0-9]+.[0-9]+ + - v[0-9]+.[0-9]+.[0-9]+-* pull_request: branches: - master From 48a04a9b76fc8a3a8edd24da1ca0cb97ed0d88d8 Mon Sep 17 00:00:00 2001 From: Yihau Chen Date: Tue, 9 Jun 2026 16:19:27 +0800 Subject: [PATCH 253/576] ci: rename docs to Docs (#13062) --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 222e22a9da9..313b946e066 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,4 +1,4 @@ -name: docs +name: Docs on: push: From 5a4a80fa2c79e05deb1c903d890644f4d0939a97 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 17:05:11 +0800 Subject: [PATCH 254/576] chore(deps): bump actions/checkout from 6.0.2 to 6.0.3 (#13068) Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.2 to 6.0.3. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/de0fac2e4500dabe0009e67214ff5f5447ce83dd...df4cb1c069e1874edd31b4311f1884172cec0e10) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 6.0.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/benchmark.yml | 2 +- .github/workflows/bump-version.yml | 2 +- .github/workflows/cargo.yml | 4 ++-- .github/workflows/changelog-label.yml | 2 +- .github/workflows/crate-check.yml | 2 +- .github/workflows/dependabot-pr.yml | 2 +- .github/workflows/docs.yml | 6 +++--- .github/workflows/publish-windows-tarball.yml | 2 +- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 99b4093a1ac..8f44a4be4a1 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -61,7 +61,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Before Command if: ${{ matrix.test.before_command != '' }} diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml index 6c76ae94229..07217572aed 100644 --- a/.github/workflows/bump-version.yml +++ b/.github/workflows/bump-version.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 0 token: ${{ secrets.VERSION_BUMP_PAT }} diff --git a/.github/workflows/cargo.yml b/.github/workflows/cargo.yml index 506bd063b89..16fcb037447 100644 --- a/.github/workflows/cargo.yml +++ b/.github/workflows/cargo.yml @@ -53,7 +53,7 @@ jobs: apk update apk add bash git - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10 with: @@ -72,7 +72,7 @@ jobs: if: github.repository_owner == 'anza-xyz' && github.event_name == 'push' runs-on: windows-2025 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10 with: diff --git a/.github/workflows/changelog-label.yml b/.github/workflows/changelog-label.yml index cbf79d12615..c0d03ebce9e 100644 --- a/.github/workflows/changelog-label.yml +++ b/.github/workflows/changelog-label.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 0 - name: Check if changes to CHANGELOG.md diff --git a/.github/workflows/crate-check.yml b/.github/workflows/crate-check.yml index e7db9c251cf..caf39ef0a37 100644 --- a/.github/workflows/crate-check.yml +++ b/.github/workflows/crate-check.yml @@ -18,7 +18,7 @@ jobs: if: github.repository_owner == 'anza-xyz' runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 0 diff --git a/.github/workflows/dependabot-pr.yml b/.github/workflows/dependabot-pr.yml index 704e18053b9..081f09bcfeb 100644 --- a/.github/workflows/dependabot-pr.yml +++ b/.github/workflows/dependabot-pr.yml @@ -27,7 +27,7 @@ jobs: GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} - name: checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.event.pull_request.head.ref }} token: ${{ steps.app-token.outputs.token }} diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 313b946e066..bd8b4c4bc94 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 0 @@ -76,7 +76,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Setup pnpm uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 @@ -112,7 +112,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Setup Node uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 diff --git a/.github/workflows/publish-windows-tarball.yml b/.github/workflows/publish-windows-tarball.yml index 2f847e4d187..ddabe7dd085 100644 --- a/.github/workflows/publish-windows-tarball.yml +++ b/.github/workflows/publish-windows-tarball.yml @@ -16,7 +16,7 @@ jobs: channel: ${{ steps.build.outputs.channel }} steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.event.inputs.commit }} fetch-depth: 0 From 0bc1d5a2b0c88a12f016eb9f254fe5142f784a35 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 18:39:30 +0800 Subject: [PATCH 255/576] chore(deps): bump solana-short-vec from 3.2.1 to 3.2.2 (#13069) * chore(deps): bump solana-short-vec from 3.2.1 to 3.2.2 Bumps [solana-short-vec](https://github.com/anza-xyz/solana-sdk) from 3.2.1 to 3.2.2. - [Release notes](https://github.com/anza-xyz/solana-sdk/releases) - [Commits](https://github.com/anza-xyz/solana-sdk/compare/bn254@v3.2.1...short-vec@v3.2.2) --- updated-dependencies: - dependency-name: solana-short-vec dependency-version: 3.2.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Update all workspaces --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- dev-bins/Cargo.lock | 4 ++-- programs/sbf/Cargo.lock | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e10d643d92c..af3147db922 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10276,9 +10276,9 @@ dependencies = [ [[package]] name = "solana-short-vec" -version = "3.2.1" +version = "3.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bb8cc883fc7b8ce4a7814cb1441b48c06437049ec11847005cf63bcfa85c546" +checksum = "7d8250a4495aad49ad20556a607da53bdcb20de78da10b65afbf918b7f1de647" dependencies = [ "serde_core", "solana-frozen-abi", diff --git a/Cargo.toml b/Cargo.toml index cf30a1e0504..6ce8ae06bc5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -475,7 +475,7 @@ solana-serde-varint = "3.0.1" solana-serialize-utils = "3.1.2" solana-sha256-hasher = "3.1.0" solana-sha512-hasher = "1.0.1" -solana-short-vec = "3.2.1" +solana-short-vec = "3.2.2" solana-shred-version = "3.0.1" solana-signature = { version = "3.4.1", default-features = false } solana-signer = "3.0.0" diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 05247b52b7a..86d9fc52a7d 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -8635,9 +8635,9 @@ dependencies = [ [[package]] name = "solana-short-vec" -version = "3.2.1" +version = "3.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bb8cc883fc7b8ce4a7814cb1441b48c06437049ec11847005cf63bcfa85c546" +checksum = "7d8250a4495aad49ad20556a607da53bdcb20de78da10b65afbf918b7f1de647" dependencies = [ "serde_core", "wincode", diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index 0d1b1697645..b5f748a3526 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -9173,9 +9173,9 @@ dependencies = [ [[package]] name = "solana-short-vec" -version = "3.2.1" +version = "3.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bb8cc883fc7b8ce4a7814cb1441b48c06437049ec11847005cf63bcfa85c546" +checksum = "7d8250a4495aad49ad20556a607da53bdcb20de78da10b65afbf918b7f1de647" dependencies = [ "serde_core", "wincode", From db94b49f17895f545bf7fbb93e873f165e774dc6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 18:49:03 +0800 Subject: [PATCH 256/576] chore(deps): bump solana-transaction-error from 3.2.0 to 3.2.1 (#13070) * chore(deps): bump solana-transaction-error from 3.2.0 to 3.2.1 Bumps [solana-transaction-error](https://github.com/anza-xyz/solana-sdk) from 3.2.0 to 3.2.1. - [Release notes](https://github.com/anza-xyz/solana-sdk/releases) - [Commits](https://github.com/anza-xyz/solana-sdk/compare/bn254@v3.2.0...bn254@v3.2.1) --- updated-dependencies: - dependency-name: solana-transaction-error dependency-version: 3.2.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Update all workspaces --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- dev-bins/Cargo.lock | 4 ++-- programs/sbf/Cargo.lock | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index af3147db922..b49f1e12077 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11116,9 +11116,9 @@ dependencies = [ [[package]] name = "solana-transaction-error" -version = "3.2.0" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a2165ad25b694c654d5395fc7a049452a192376e4c96a7fad05580f6ba5ba1c" +checksum = "2441d6dcd51100e7d97c3fb3b723e08aa701066ff7afc00026fd8d8e222cb95b" dependencies = [ "serde", "serde_derive", diff --git a/Cargo.toml b/Cargo.toml index 6ce8ae06bc5..f6b344a216c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -508,7 +508,7 @@ solana-tpu-client = { path = "tpu-client", version = "=4.2.0-alpha.0", default-f solana-tpu-client-next = { path = "tpu-client-next", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-transaction = "4.1.2" solana-transaction-context = { path = "transaction-context", version = "=4.2.0-alpha.0", features = ["agave-unstable-api", "bincode"] } -solana-transaction-error = "3.2.0" +solana-transaction-error = "3.2.1" solana-transaction-status = { path = "transaction-status", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-transaction-status-client-types = { path = "transaction-status-client-types", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-turbine = { path = "turbine", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 86d9fc52a7d..a9b39b49fad 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -9292,9 +9292,9 @@ dependencies = [ [[package]] name = "solana-transaction-error" -version = "3.2.0" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a2165ad25b694c654d5395fc7a049452a192376e4c96a7fad05580f6ba5ba1c" +checksum = "2441d6dcd51100e7d97c3fb3b723e08aa701066ff7afc00026fd8d8e222cb95b" dependencies = [ "serde", "serde_derive", diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index b5f748a3526..f7c29c34723 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -9801,9 +9801,9 @@ dependencies = [ [[package]] name = "solana-transaction-error" -version = "3.2.0" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a2165ad25b694c654d5395fc7a049452a192376e4c96a7fad05580f6ba5ba1c" +checksum = "2441d6dcd51100e7d97c3fb3b723e08aa701066ff7afc00026fd8d8e222cb95b" dependencies = [ "serde", "serde_derive", From 6fea7938449dda2d1bf72d052c12337bba7975ad Mon Sep 17 00:00:00 2001 From: Yihau Chen Date: Tue, 9 Jun 2026 18:49:32 +0800 Subject: [PATCH 257/576] ci: check actor in dependabot-pr.yml (#13006) --- .github/workflows/dependabot-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependabot-pr.yml b/.github/workflows/dependabot-pr.yml index 081f09bcfeb..acd045e9b61 100644 --- a/.github/workflows/dependabot-pr.yml +++ b/.github/workflows/dependabot-pr.yml @@ -9,7 +9,7 @@ jobs: dependabot: timeout-minutes: 10 runs-on: ubuntu-24.04 - if: github.event.pull_request.user.login == 'dependabot[bot]' + if: github.actor == 'dependabot[bot]' steps: - name: Create github app token uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 From 8a17d497d397add144cb1d7fe1ca0d5263026a11 Mon Sep 17 00:00:00 2001 From: Kamil Skalski Date: Tue, 9 Jun 2026 14:47:11 +0200 Subject: [PATCH 258/576] feat: switch SlotHistory sysvar to wincode (de)serialization (#13074) --- Cargo.lock | 2 ++ account-decoder/Cargo.toml | 2 +- account-decoder/src/parse_sysvar.rs | 12 +++++++----- cli/Cargo.toml | 3 ++- cli/src/cluster_query.rs | 5 ++--- dev-bins/Cargo.lock | 1 + programs/sbf/Cargo.lock | 1 + runtime/Cargo.toml | 2 +- runtime/src/bank.rs | 7 ++++--- 9 files changed, 21 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b49f1e12077..0ff58ac53bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7449,6 +7449,7 @@ dependencies = [ "thiserror 2.0.18", "tiny-bip39", "tokio", + "wincode", ] [[package]] @@ -10362,6 +10363,7 @@ dependencies = [ "serde_derive", "solana-sdk-ids", "solana-sysvar-id", + "wincode", ] [[package]] diff --git a/account-decoder/Cargo.toml b/account-decoder/Cargo.toml index 02d7d7b2e00..98907ea0413 100644 --- a/account-decoder/Cargo.toml +++ b/account-decoder/Cargo.toml @@ -42,7 +42,7 @@ solana-pubkey = { workspace = true } solana-rent = { workspace = true } solana-sdk-ids = { workspace = true } solana-slot-hashes = { workspace = true, features = ["wincode"] } -solana-slot-history = { workspace = true } +solana-slot-history = { workspace = true, features = ["wincode"] } solana-stake-interface = { workspace = true, features = ["bincode", "sysvar"] } solana-sysvar = { workspace = true } solana-vote-interface = { workspace = true, features = ["bincode"] } diff --git a/account-decoder/src/parse_sysvar.rs b/account-decoder/src/parse_sysvar.rs index b225036a126..cdd4c0e0eb5 100644 --- a/account-decoder/src/parse_sysvar.rs +++ b/account-decoder/src/parse_sysvar.rs @@ -69,12 +69,14 @@ pub fn parse_sysvar(data: &[u8], pubkey: &Pubkey) -> Result(data).ok().map(|slot_history| { - SysvarAccountType::SlotHistory(UiSlotHistory { - next_slot: slot_history.next_slot, - bits: format!("{:?}", SlotHistoryBits(slot_history.bits)), + wincode::deserialize::(data) + .ok() + .map(|slot_history| { + SysvarAccountType::SlotHistory(UiSlotHistory { + next_slot: slot_history.next_slot, + bits: format!("{:?}", SlotHistoryBits(slot_history.bits)), + }) }) - }) } else if pubkey == &sysvar::stake_history::id() { deserialize::(data).ok().map(|stake_history| { let stake_history = stake_history diff --git a/cli/Cargo.toml b/cli/Cargo.toml index ff398bbb5d2..cdacbdc020a 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -81,7 +81,7 @@ solana-sdk-ids = { workspace = true } solana-signature = { workspace = true } solana-signer = { workspace = true } solana-signer-store = { workspace = true } -solana-slot-history = { workspace = true } +solana-slot-history = { workspace = true, features = ["wincode"] } solana-stake-interface = { workspace = true } solana-syscalls = { workspace = true } solana-system-interface = { workspace = true, features = ["bincode"] } @@ -95,6 +95,7 @@ spl-memo-interface = { workspace = true } thiserror = { workspace = true } tiny-bip39 = { workspace = true } tokio = { workspace = true } +wincode = { workspace = true } [dev-dependencies] assert_matches = { workspace = true } diff --git a/cli/src/cluster_query.rs b/cli/src/cluster_query.rs index fc8175c2c4d..2ef0613f12d 100644 --- a/cli/src/cluster_query.rs +++ b/cli/src/cluster_query.rs @@ -1258,9 +1258,8 @@ pub async fn process_show_block_production( .value .unwrap(); - let slot_history: SlotHistory = from_account(&slot_history_account).ok_or_else(|| { - CliError::RpcRequestError("Failed to deserialize slot history".to_string()) - })?; + let slot_history: SlotHistory = wincode::deserialize(&slot_history_account.data) + .map_err(|_| CliError::RpcRequestError("Failed to deserialize slot history".to_string()))?; let (confirmed_blocks, start_slot) = if start_slot >= slot_history.oldest() && end_slot <= slot_history.newest() { diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index a9b39b49fad..f7fbb103142 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -8716,6 +8716,7 @@ dependencies = [ "serde_derive", "solana-sdk-ids", "solana-sysvar-id", + "wincode", ] [[package]] diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index f7c29c34723..afdb413e2f8 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -9254,6 +9254,7 @@ dependencies = [ "serde_derive", "solana-sdk-ids", "solana-sysvar-id", + "wincode", ] [[package]] diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index cfd0bf20f10..e3b87dd60a5 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -153,7 +153,7 @@ solana-signature = { workspace = true } solana-signer = { workspace = true } solana-signer-store = { workspace = true } solana-slot-hashes = { workspace = true, features = ["wincode"] } -solana-slot-history = { workspace = true } +solana-slot-history = { workspace = true, features = ["wincode"] } solana-stake-interface = { workspace = true } solana-svm = { workspace = true, features = ["metrics"] } solana-svm-callback = { workspace = true } diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 7ff9624623c..f218fb890ec 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -2532,7 +2532,7 @@ impl Bank { self.update_sysvar_account(&sysvar::slot_history::id(), |account| { let mut slot_history = account .as_ref() - .map(|account| from_account::(account).unwrap()) + .map(|account| wincode::deserialize::(account.data()).unwrap()) .unwrap_or_default(); slot_history.add(self.slot()); create_account( @@ -2557,7 +2557,8 @@ impl Bank { } pub fn get_slot_history(&self) -> Option { - from_account(&self.get_account(&sysvar::slot_history::id())?) + wincode::deserialize::(self.get_account(&sysvar::slot_history::id())?.data()) + .ok() } fn update_epoch_stakes( @@ -3875,7 +3876,7 @@ impl Bank { let current_account = self.get_account_with_fixed_root(&slot_history_id); let slot_history = current_account .as_ref() - .map(|account| from_account::(account).unwrap()) + .map(|account| wincode::deserialize::(account.data()).unwrap()) .unwrap_or_default(); if slot_history.check(self.slot()) == Check::Found { let ancestors = Ancestors::from(self.proper_ancestors().collect::>()); From c7b6911ef9cc835ea3d54f1fe17d32f442146f07 Mon Sep 17 00:00:00 2001 From: Akhilesh Singhania Date: Tue, 9 Jun 2026 15:56:35 +0200 Subject: [PATCH 259/576] cli: only look up AG cert if AG feature is active (#13077) --- cli/src/vote.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/cli/src/vote.rs b/cli/src/vote.rs index 40225ae0c9d..4975a0c4e8f 100644 --- a/cli/src/vote.rs +++ b/cli/src/vote.rs @@ -14,7 +14,9 @@ use { spend_utils::{SpendAmount, resolve_spend_tx_and_check_account_balances}, stake::check_current_authority, }, - agave_feature_set::{bls_pubkey_management_in_vote_account, vote_account_initialize_v2}, + agave_feature_set::{ + alpenglow, bls_pubkey_management_in_vote_account, vote_account_initialize_v2, + }, agave_votor_messages::consensus_message::BLS_KEYPAIR_DERIVE_SEED, clap::{App, Arg, ArgMatches, SubCommand, value_t_or_exit}, solana_account::Account, @@ -1616,7 +1618,14 @@ pub async fn process_show_vote_account( .and_then(|feature| feature.activated_at); let tvc_activation_epoch = tvc_activation_slot.map(|s| epoch_schedule.get_epoch(s)); - let ag_genesis_cert = rpc_client.get_ag_genesis_cert().await?; + let ag_is_active = get_feature_is_active(rpc_client, &alpenglow::id()) + .await + .unwrap_or(false); + let ag_genesis_cert = if ag_is_active { + rpc_client.get_ag_genesis_cert().await? + } else { + None + }; let votes_observed = VotesObserved::new(&vote_state, &ag_genesis_cert); let epoch_voting_history = get_epoch_history( &epoch_schedule, From 24f8ac48fb06a5b46fd73a64f32e34ae867c5640 Mon Sep 17 00:00:00 2001 From: steviez Date: Tue, 9 Jun 2026 10:06:57 -0500 Subject: [PATCH 260/576] core: Remove metrics counters from WindowService (#13066) These counters were used to report errors and the configured metrics rate means a datapoint will have exact same reporting semantics --- core/src/window_service.rs | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/core/src/window_service.rs b/core/src/window_service.rs index 68e745d20e4..b0fd4ae6eb9 100644 --- a/core/src/window_service.rs +++ b/core/src/window_service.rs @@ -25,7 +25,6 @@ use { shred::{self, ReedSolomonCache, Shred, filter::ShredRecoveryContext}, }, solana_measure::measure::Measure, - solana_metrics::inc_new_counter_error, solana_rayon_threadlimit::get_thread_count, solana_runtime::bank_forks::{BankForks, SharableBanks}, solana_streamer::evicting_sender::EvictingSender, @@ -373,9 +372,6 @@ impl WindowService { duplicate_slots_sender: DuplicateSlotSender, bank_forks: Arc>, ) -> JoinHandle<()> { - let handle_error = || { - inc_new_counter_error!("solana-check-duplicate-error", 1, 1); - }; Builder::new() .name("solWinCheckDup".to_string()) .spawn(move || { @@ -387,7 +383,7 @@ impl WindowService { &duplicate_slots_sender, &bank_forks, ) { - if Self::should_exit_on_error(e, &handle_error) { + if Self::should_exit_on_error(e) { break; } } @@ -407,9 +403,6 @@ impl WindowService { completed_data_sets_sender: Option, retransmit_sender: EvictingSender>, ) -> JoinHandle<()> { - let handle_error = || { - inc_new_counter_error!("solana-window-insert-error", 1, 1); - }; let reed_solomon_cache = ReedSolomonCache::default(); Builder::new() .name("solWinInsert".to_string()) @@ -453,7 +446,7 @@ impl WindowService { completed_data_sets_sender.as_ref(), ) { ws_metrics.record_error(&e); - if Self::should_exit_on_error(e, &handle_error) { + if Self::should_exit_on_error(e) { break; } } @@ -471,16 +464,19 @@ impl WindowService { .unwrap() } - fn should_exit_on_error(e: Error, handle_error: &H) -> bool - where - H: Fn(), - { + fn should_exit_on_error(e: Error) -> bool { match e { Error::RecvTimeout(RecvTimeoutError::Disconnected) => true, Error::RecvTimeout(RecvTimeoutError::Timeout) => false, Error::Send => true, _ => { - handle_error(); + let version = solana_version::version!(); + datapoint_error!( + "error", + ("thread", thread::current().name().unwrap_or("?"), String), + ("message", format!("{e}"), String), + ("version", version, String) + ); error!("thread {:?} error {:?}", thread::current().name(), e); false } From 8d6ad05ea1424cb40e7bed7830759347b1b1571b Mon Sep 17 00:00:00 2001 From: Brennan Date: Tue, 9 Jun 2026 09:16:54 -0700 Subject: [PATCH 261/576] SIMD-0525: Dynamic VAT (#12990) --- feature-set/src/lib.rs | 8 +-- genesis/src/main.rs | 6 +- runtime/src/bank.rs | 21 +++++-- .../partitioned_epoch_rewards/calculation.rs | 4 +- runtime/src/bank/tests.rs | 62 +++++++++++++++++++ .../block_component_processor/vote_reward.rs | 13 ++-- .../vote_reward/migration_test.rs | 12 ++-- runtime/src/genesis_utils.rs | 13 ++-- runtime/src/slot_params.rs | 11 ++++ runtime/tests/vote_account.rs | 8 +-- 10 files changed, 122 insertions(+), 36 deletions(-) diff --git a/feature-set/src/lib.rs b/feature-set/src/lib.rs index bf50f378d75..79beb9eec73 100644 --- a/feature-set/src/lib.rs +++ b/feature-set/src/lib.rs @@ -1532,19 +1532,19 @@ pub mod set_lamports_per_byte_to_6960 { } pub mod reduce_slot_time_to_350ms { - solana_pubkey::declare_id!("iBRL2iJvhLssJveF1utbmmQGmjonmNYZALcJFEHTbUF"); + solana_pubkey::declare_id!("iBRL5RuWhw4yqaAZu96RUULHckHTZAoe2b77qaV38JZ"); } pub mod reduce_slot_time_to_300ms { - solana_pubkey::declare_id!("iBRLA3zvd6x9445cK1vS7xt8n6Y7DS3otfRcDdW8JRW"); + solana_pubkey::declare_id!("iBRLL3k18HST852F1Mf3Lv83waTNQmmqvKDxvYGwQFL"); } pub mod reduce_slot_time_to_250ms { - solana_pubkey::declare_id!("iBRLR6nG3fDi8YD4mpPTUVTgo5NaiYfZgrzokCfADP2"); + solana_pubkey::declare_id!("iBRLMc81UjRa8fn8A6eE8bJTnRbgQoPTynM51akENCV"); } pub mod reduce_slot_time_to_200ms { - solana_pubkey::declare_id!("iBRLypKvvj9VEvwoTeRpbLhbW55NFR4T3GE9BUR8A16"); + solana_pubkey::declare_id!("iBRLjhJnkmDZgNoZRDMW11d8ZV7HvsL3vAyRjZB5npW"); } pub mod upgrade_bpf_stake_program_to_v5_1 { diff --git a/genesis/src/main.rs b/genesis/src/main.rs index 35b31dd7c08..d92fd925476 100644 --- a/genesis/src/main.rs +++ b/genesis/src/main.rs @@ -42,7 +42,7 @@ use { solana_rpc_client::rpc_client::RpcClient, solana_rpc_client_api::request::MAX_MULTIPLE_ACCOUNTS, solana_runtime::{ - bank::VAT_TO_BURN_PER_EPOCH, + bank::DEFAULT_VAT_TO_BURN_PER_EPOCH, genesis_utils::{add_genesis_epoch_rewards_account, add_genesis_stake_config_account}, stake_utils, }, @@ -66,7 +66,7 @@ use { /// In order to satisfy the VAT we need to fund all vote accounts /// This corresponds to 100 epochs worth of VAT -const VAT_MINIMUM_LAMPORTS: u64 = VAT_TO_BURN_PER_EPOCH * 100; +const DEFAULT_VAT_MINIMUM_LAMPORTS: u64 = DEFAULT_VAT_TO_BURN_PER_EPOCH * 100; pub enum AccountFileFormat { Pubkey, @@ -279,7 +279,7 @@ fn add_validator_accounts( .unwrap_or([0u8; BLS_PUBLIC_KEY_COMPRESSED_SIZE]); // Vote account needs enough lamports for rent exemption plus VAT let vote_account_lamports = - rent.minimum_balance(VoteStateV4::size_of()) + VAT_MINIMUM_LAMPORTS; + rent.minimum_balance(VoteStateV4::size_of()) + DEFAULT_VAT_MINIMUM_LAMPORTS; let vote_account = vote_state::create_v4_account_with_authorized( identity_pubkey, identity_pubkey, diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index f218fb890ec..fec59ccd9d3 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -251,9 +251,12 @@ pub const MAX_LEADER_SCHEDULE_STAKES: Epoch = 5; /// only the top 2000 validators by stake will be present in vote account structures. pub const MAX_ALPENGLOW_VOTE_ACCOUNTS: usize = 2000; -/// The admission cost in lamports that is burned from the vote account of -/// validators that are a part of the voting set -pub const VAT_TO_BURN_PER_EPOCH: u64 = 1_600_000_000; // 1.6 SOL +/// Default 400ms-slot Validator Admission Ticket burn amount. +/// +/// Use this for conservative genesis/test funding defaults. Runtime VAT +/// filtering and burns must use the bank's effective slot params instead. +pub const DEFAULT_VAT_TO_BURN_PER_EPOCH: u64 = + crate::slot_params::LEGACY_SLOT_PARAMS.vat_to_burn_per_epoch(); /// The off-curve account where we store the Alpenglow clock. The clock sysvar has seconds /// resolution while the Alpenglow clock has nanosecond resolution. @@ -2623,6 +2626,7 @@ impl Bank { return; } + let vat_to_burn_per_epoch = self.vat_to_burn_per_epoch(); let vote_accounts = epoch_stakes.stakes().vote_accounts(); debug_assert!(vote_accounts.len() <= 2000); // +1 for the incinerator account @@ -2634,11 +2638,11 @@ impl Bank { // accounts with non-zero stake and sufficient balance. for (vote_pubkey, _stake) in vote_accounts.delegated_stakes() { let mut account = self.get_account(vote_pubkey).unwrap(); - total_vat += VAT_TO_BURN_PER_EPOCH; + total_vat += vat_to_burn_per_epoch; account.set_lamports( account .lamports() - .checked_sub(VAT_TO_BURN_PER_EPOCH) + .checked_sub(vat_to_burn_per_epoch) .expect( "Vote accounts should have already been filtered to contain enough \ balance for the VAT", @@ -2800,6 +2804,11 @@ impl Bank { self.slot_params_at_slot(self.slot) } + /// Returns the Validator Admission Ticket burn for this bank's slot params. + pub(crate) fn vat_to_burn_per_epoch(&self) -> u64 { + self.current_slot_params().vat_to_burn_per_epoch() + } + /// Returns the effective slot duration for `slot`. pub fn ns_per_slot_at_slot(&self, slot: Slot) -> u128 { self.slot_params_at_slot(slot).ns_per_slot() @@ -6528,7 +6537,7 @@ impl Bank { .rent .minimum_balance(VoteStateV4::size_of()); if self.feature_set.snapshot().alpenglow { - vote_account_rent_exempt_minimum + VAT_TO_BURN_PER_EPOCH + vote_account_rent_exempt_minimum + self.vat_to_burn_per_epoch() } else { vote_account_rent_exempt_minimum } diff --git a/runtime/src/bank/partitioned_epoch_rewards/calculation.rs b/runtime/src/bank/partitioned_epoch_rewards/calculation.rs index fa39573dfc1..87e4e022c0a 100644 --- a/runtime/src/bank/partitioned_epoch_rewards/calculation.rs +++ b/runtime/src/bank/partitioned_epoch_rewards/calculation.rs @@ -3024,7 +3024,7 @@ mod tests { let thread_pool = ThreadPoolBuilder::new().num_threads(1).build().unwrap(); let pubkey = solana_pubkey::new_rand(); - let pre_burn_balance = 10 * crate::bank::VAT_TO_BURN_PER_EPOCH; + let pre_burn_balance = 10 * crate::bank::DEFAULT_VAT_TO_BURN_PER_EPOCH; let commission_lamports = 12_345; // Commission is planned against the pre-burn account state. @@ -3044,7 +3044,7 @@ mod tests { // Simulate the VAT burn that would run in `update_epoch_stakes` // between reward calculation and distribution. - let post_burn_balance = pre_burn_balance - crate::bank::VAT_TO_BURN_PER_EPOCH; + let post_burn_balance = pre_burn_balance - crate::bank::DEFAULT_VAT_TO_BURN_PER_EPOCH; let mut burned_account = commission_account.clone(); burned_account.set_lamports(post_burn_balance); bank.store_account_and_update_capitalization(&pubkey, &burned_account); diff --git a/runtime/src/bank/tests.rs b/runtime/src/bank/tests.rs index d4f60913f49..956a6d7eff2 100644 --- a/runtime/src/bank/tests.rs +++ b/runtime/src/bank/tests.rs @@ -6337,6 +6337,7 @@ fn assert_slot_time_bank_state(bank: &Bank, params: SlotParams) { bank.partitioned_rewards_stake_account_stores_per_block, params.partitioned_epoch_rewards_stake_account_stores_per_block() ); + assert_eq!(bank.vat_to_burn_per_epoch(), params.vat_to_burn_per_epoch()); assert_eq!( bank.entry_bytes_budget().slot_limit(), params.max_entry_bytes_per_slot() @@ -6393,6 +6394,63 @@ fn test_reduce_slot_time_features() { } } +#[test] +fn test_vat_burn_slot_params() { + let voting_keypair = ValidatorVoteKeypairs::new_rand(); + let validator_keypairs = [&voting_keypair]; + let vote_pubkey = voting_keypair.vote_keypair.pubkey(); + + // Loop through slot reduction features one at a time. + for (slot_time_feature_id, params) in std::iter::once((None, LEGACY_SLOT_PARAMS)) + .chain(slot_time_feature_gates().map(|(feature_id, params)| (Some(feature_id), params))) + { + // Genesis with slot reduction feature active (if applicable). + let GenesisConfigInfo { + mut genesis_config, .. + } = genesis_utils::create_genesis_config_with_vote_accounts_and_cluster_type( + 1_000 * LAMPORTS_PER_SOL, + &validator_keypairs, + vec![minimum_vote_account_balance_for_vat(100)], + ClusterType::Development, + &FeatureSet::default(), + false, + ); + activate_feature(&mut genesis_config, feature_set::alpenglow::id()); + activate_feature( + &mut genesis_config, + feature_set::validator_admission_ticket::id(), + ); + if let Some(feature_id) = slot_time_feature_id { + activate_feature(&mut genesis_config, feature_id); + } + + // Advance forward such that feature goes effective. + let (parent_bank, _bank_forks) = Bank::new_with_bank_forks_for_tests(&genesis_config); + let bank_slot = if slot_time_feature_id.is_some() { + parent_bank.epoch_schedule().get_first_slot_in_epoch(1) + } else { + parent_bank.slot().saturating_add(1) + }; + let mut bank = Bank::new_from_parent(parent_bank, SlotLeader::default(), bank_slot); + assert_eq!(bank.vat_to_burn_per_epoch(), params.vat_to_burn_per_epoch()); + + // Verify correct VAT amount is burned. + let vote_lamports_before = bank.get_balance(&vote_pubkey); + let incinerator_lamports_before = bank.get_balance(&incinerator::id()); + let stakes = SerdeStakesToStakeFormat::from(bank.get_top_epoch_stakes()); + let epoch_stakes = VersionedEpochStakes::new(stakes, bank.epoch()); + bank.maybe_burn_vat_from_staked_accounts(&epoch_stakes); + assert_eq!( + bank.get_balance(&vote_pubkey), + vote_lamports_before - params.vat_to_burn_per_epoch() + ); + assert_eq!( + bank.get_balance(&incinerator::id()), + incinerator_lamports_before + params.vat_to_burn_per_epoch() + ); + } +} + #[test] fn test_reduce_slot_time_features_active_at_genesis() { let (mut genesis_config, _) = create_genesis_config_with_legacy_hashes(1_000_000); @@ -6956,6 +7014,10 @@ fn test_slot_params_use_genesis_baseline_without_features() { baseline_params.partitioned_epoch_rewards_stake_account_stores_per_block(), stake_account_stores_per_block ); + assert_eq!( + baseline_params.vat_to_burn_per_epoch(), + DEFAULT_VAT_TO_BURN_PER_EPOCH + ); assert_eq!( bank.max_data_shreds_per_slot(), DEFAULT_MAX_DATA_SHREDS_PER_SLOT diff --git a/runtime/src/block_component_processor/vote_reward.rs b/runtime/src/block_component_processor/vote_reward.rs index 1de3939db1e..8197781fc6f 100644 --- a/runtime/src/block_component_processor/vote_reward.rs +++ b/runtime/src/block_component_processor/vote_reward.rs @@ -602,7 +602,6 @@ mod tests { use { super::*, crate::{ - bank::VAT_TO_BURN_PER_EPOCH, bank_forks::BankForks, genesis_utils::{ ValidatorVoteKeypairs, activate_all_features_alpenglow, @@ -1304,11 +1303,11 @@ mod tests { let (initial_validator_lamports, final_validator_lamports) = self.get_initial_and_final_lamports(reward_bank, payout_bank, voter_pubkey); + let vat_burn = + payout_bank.vat_to_burn_per_epoch() * (payout_bank.epoch() - reward_bank.epoch()); assert_eq!( expected_validator_reward, - final_validator_lamports - + VAT_TO_BURN_PER_EPOCH * (payout_bank.epoch() - reward_bank.epoch()) - - initial_validator_lamports + final_validator_lamports + vat_burn - initial_validator_lamports ); leader_reward } @@ -1336,11 +1335,11 @@ mod tests { let (initial_validator_lamports, final_validator_lamports) = self.get_initial_and_final_lamports(reward_bank, payout_bank, &leader); + let vat_burn = + payout_bank.vat_to_burn_per_epoch() * (payout_bank.epoch() - reward_bank.epoch()); assert_eq!( expected_validator_reward, - final_validator_lamports - + VAT_TO_BURN_PER_EPOCH * (payout_bank.epoch() - reward_bank.epoch()) - - initial_validator_lamports + final_validator_lamports + vat_burn - initial_validator_lamports ); } diff --git a/runtime/src/block_component_processor/vote_reward/migration_test.rs b/runtime/src/block_component_processor/vote_reward/migration_test.rs index 33fd50a1b60..f6ba256d2f6 100644 --- a/runtime/src/block_component_processor/vote_reward/migration_test.rs +++ b/runtime/src/block_component_processor/vote_reward/migration_test.rs @@ -3,7 +3,7 @@ mod tests { use { crate::{ - bank::{Bank, VAT_TO_BURN_PER_EPOCH}, + bank::Bank, block_component_processor::vote_reward::{ VoteState, increment_credits, tests::{new_bank_from_parent, set_commission}, @@ -388,10 +388,12 @@ mod tests { let (initial_lamports, final_lamports) = self.get_initial_and_final_lamports(reward_bank, payout_bank, &vote_pubkey); - let diff = final_lamports - + VAT_TO_BURN_PER_EPOCH * (payout_bank.epoch() - reward_bank.epoch()) - - initial_lamports; - assert_eq!(expected_validator_reward, diff); + let vat_burn = + payout_bank.vat_to_burn_per_epoch() * (payout_bank.epoch() - reward_bank.epoch()); + assert_eq!( + expected_validator_reward, + final_lamports + vat_burn - initial_lamports + ); } fn validate_rewards(&self, reward_bank: &Bank, payout_bank: &Bank) { diff --git a/runtime/src/genesis_utils.rs b/runtime/src/genesis_utils.rs index ccd0f635443..5d18ceade73 100644 --- a/runtime/src/genesis_utils.rs +++ b/runtime/src/genesis_utils.rs @@ -2,7 +2,7 @@ use solana_stake_interface::config::Config as StakeConfig; use { crate::{ - bank::VAT_TO_BURN_PER_EPOCH, + bank::DEFAULT_VAT_TO_BURN_PER_EPOCH, block_component_processor::vote_reward::epoch_inflation_account_state::EpochInflationAccountState, stake_utils, }, @@ -50,10 +50,11 @@ use { // Default amount received by the validator const VALIDATOR_LAMPORTS: u64 = 890_880; -// Minimum vote account balance required for VAT (SIMD-0357). -// Vote accounts need this minimum to pass VAT filtering. +// Default minimum vote account balance used by tests/genesis helpers. This is +// conservative once shorter slot-time regimes lower the live bank VAT burn. pub fn minimum_vote_account_balance_for_vat(num_epochs: Epoch) -> u64 { - VAT_TO_BURN_PER_EPOCH * num_epochs + Rent::default().minimum_balance(VoteStateV4::size_of()) + DEFAULT_VAT_TO_BURN_PER_EPOCH * num_epochs + + Rent::default().minimum_balance(VoteStateV4::size_of()) } // Minimum stake lamports required for a valid stake account with non-zero stake. @@ -449,7 +450,9 @@ pub fn create_genesis_config_with_leader_ex_no_features( mut initial_accounts: Vec<(Pubkey, AccountSharedData)>, ) -> GenesisConfig { // Ensure minimum lamports for VAT filtering, but only when stake > 0. - // VAT requires: non-zero stake, BLS pubkey, and lamports >= VAT_TO_BURN_PER_EPOCH + rent_exempt_minimum. + // VAT requires non-zero stake, a BLS pubkey, and lamports >= the bank's + // current VAT burn plus rent-exempt minimum. This helper funds with the + // conservative default burn amount. let (vote_account_lamports, stake_lamports) = if validator_stake_lamports > 0 { ( validator_stake_lamports.max(minimum_vote_account_balance_for_vat(100)), diff --git a/runtime/src/slot_params.rs b/runtime/src/slot_params.rs index 819ffdbd62b..7333e44d69d 100644 --- a/runtime/src/slot_params.rs +++ b/runtime/src/slot_params.rs @@ -26,6 +26,7 @@ pub struct SlotParams { pub(crate) max_code_shreds_per_slot: u32, pub(crate) max_entry_bytes_per_slot: u64, pub(crate) partitioned_epoch_rewards_stake_account_stores_per_block: u64, + pub(crate) vat_to_burn_per_epoch: u64, } impl SlotParams { @@ -84,6 +85,11 @@ impl SlotParams { self.partitioned_epoch_rewards_stake_account_stores_per_block } + /// Validator Admission Ticket burn amount for one epoch. + pub const fn vat_to_burn_per_epoch(&self) -> u64 { + self.vat_to_burn_per_epoch + } + /// Returns the per-bank cost limits for these params. pub(crate) const fn cost_limits(self, raise_block_limits_to_100m: bool) -> CostTrackerLimits { let cost_tracker_limits = self.cost_tracker_limits; @@ -123,6 +129,7 @@ pub(crate) const LEGACY_SLOT_PARAMS: SlotParams = SlotParams { max_code_shreds_per_slot: 32_768, max_entry_bytes_per_slot: 20 * 1024 * 1024, partitioned_epoch_rewards_stake_account_stores_per_block: 4096, + vat_to_burn_per_epoch: 1_600_000_000, }; pub(crate) const SLOT_PARAMS_350MS: SlotParams = SlotParams { @@ -134,6 +141,7 @@ pub(crate) const SLOT_PARAMS_350MS: SlotParams = SlotParams { max_code_shreds_per_slot: 28_672, max_entry_bytes_per_slot: 18_350_080, partitioned_epoch_rewards_stake_account_stores_per_block: 3_584, + vat_to_burn_per_epoch: 1_400_000_000, }; pub(crate) const SLOT_PARAMS_300MS: SlotParams = SlotParams { @@ -145,6 +153,7 @@ pub(crate) const SLOT_PARAMS_300MS: SlotParams = SlotParams { max_code_shreds_per_slot: 24_576, max_entry_bytes_per_slot: 15_728_640, partitioned_epoch_rewards_stake_account_stores_per_block: 3_072, + vat_to_burn_per_epoch: 1_200_000_000, }; pub(crate) const SLOT_PARAMS_250MS: SlotParams = SlotParams { @@ -156,6 +165,7 @@ pub(crate) const SLOT_PARAMS_250MS: SlotParams = SlotParams { max_code_shreds_per_slot: 20_480, max_entry_bytes_per_slot: 13_107_200, partitioned_epoch_rewards_stake_account_stores_per_block: 2_560, + vat_to_burn_per_epoch: 1_000_000_000, }; pub(crate) const SLOT_PARAMS_200MS: SlotParams = SlotParams { @@ -167,6 +177,7 @@ pub(crate) const SLOT_PARAMS_200MS: SlotParams = SlotParams { max_code_shreds_per_slot: 16_384, max_entry_bytes_per_slot: 10_485_760, partitioned_epoch_rewards_stake_account_stores_per_block: 2_048, + vat_to_burn_per_epoch: 800_000_000, }; /// Slot-time reduction gates in the intended activation order. diff --git a/runtime/tests/vote_account.rs b/runtime/tests/vote_account.rs index d1ffcdad0ed..cd427cde8d6 100644 --- a/runtime/tests/vote_account.rs +++ b/runtime/tests/vote_account.rs @@ -8,7 +8,7 @@ use { solana_account::{AccountSharedData, ReadableAccount, WritableAccount}, solana_pubkey::Pubkey, solana_runtime::{ - bank::{MAX_ALPENGLOW_VOTE_ACCOUNTS, VAT_TO_BURN_PER_EPOCH}, + bank::{DEFAULT_VAT_TO_BURN_PER_EPOCH, MAX_ALPENGLOW_VOTE_ACCOUNTS}, test_utils::{ new_rand_vote_account, new_rand_vote_accounts, new_staked_vote_accounts, staked_nodes, }, @@ -445,14 +445,14 @@ fn test_clone_and_filter_for_vat_not_enough_lamports() { MAX_STAKE_FOR_STAKED_ACCOUNT, |index| { if index < entries_to_modify { - VAT_TO_BURN_PER_EPOCH - 1 + DEFAULT_VAT_TO_BURN_PER_EPOCH - 1 } else { 10_000_000_000 } }, ); - let filtered = - vote_accounts.clone_and_filter_for_vat(MAX_ALPENGLOW_VOTE_ACCOUNTS, VAT_TO_BURN_PER_EPOCH); + let filtered = vote_accounts + .clone_and_filter_for_vat(MAX_ALPENGLOW_VOTE_ACCOUNTS, DEFAULT_VAT_TO_BURN_PER_EPOCH); assert!(filtered.len() <= MAX_ALPENGLOW_VOTE_ACCOUNTS - entries_to_modify); } From d95ccc5d028d35e89c32f638cd80f67c82536347 Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Tue, 9 Jun 2026 12:27:37 -0400 Subject: [PATCH 262/576] local-cluster: upstream alpenglow partition tests (#13008) --- .config/nextest.toml | 4 +- local-cluster/src/integration_tests.rs | 147 +++++++++++++++++++++---- local-cluster/src/local_cluster.rs | 3 + local-cluster/tests/local_cluster.rs | 38 ++++++- 4 files changed, 163 insertions(+), 29 deletions(-) diff --git a/.config/nextest.toml b/.config/nextest.toml index 7caa4a35e28..cba96681ec8 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -53,7 +53,7 @@ filter = 'package(solana-local-cluster) & test(/^test_fork_choice_refresh_old_vo slow-timeout = { period = "60s", terminate-after = 5 } [[profile.ci.overrides]] -filter = 'package(solana-local-cluster) & test(/^test_cluster_partition$/)' +filter = 'package(solana-local-cluster) & test(/^test_cluster_partition/)' slow-timeout = { period = "60s", terminate-after = 4 } [[profile.ci.overrides]] @@ -65,7 +65,7 @@ filter = 'package(solana-local-cluster) & test(/^test_slot_hash_expiry$/)' slow-timeout = { period = "60s", terminate-after = 4 } [[profile.ci.overrides]] -filter = 'package(solana-local-cluster) & test(/^test_kill_heaviest_partition$/)' +filter = 'package(solana-local-cluster) & test(/^test_kill_heaviest_partition/)' slow-timeout = { period = "60s", terminate-after = 4 } [[profile.ci.overrides]] diff --git a/local-cluster/src/integration_tests.rs b/local-cluster/src/integration_tests.rs index 1a361a29508..ba97bd8f276 100644 --- a/local-cluster/src/integration_tests.rs +++ b/local-cluster/src/integration_tests.rs @@ -17,6 +17,7 @@ use { validator_configs::*, }, agave_snapshots::{SnapshotInterval, snapshot_config::SnapshotConfig}, + agave_votor::voting_service::{AlpenglowPortOverride, VotingServiceOverride}, log::*, solana_account::AccountSharedData, solana_accounts_db::utils::create_accounts_run_and_snapshot_dirs, @@ -38,15 +39,16 @@ use { shred::filter::{TurbineMode, TurbineModeKind}, }, solana_native_token::LAMPORTS_PER_SOL, - solana_net_utils::SocketAddrSpace, + solana_net_utils::{SocketAddrSpace, sockets::bind_to_localhost_unique}, solana_pubkey::Pubkey, solana_rpc_client::rpc_client::RpcClient, solana_signer::Signer, solana_turbine::broadcast_stage::BroadcastStageType, static_assertions, std::{ - collections::HashSet, + collections::{HashMap, HashSet}, fs, iter, + net::SocketAddr, num::{NonZeroU64, NonZeroUsize}, path::{Path, PathBuf}, sync::{Arc, atomic::AtomicBool}, @@ -218,12 +220,26 @@ pub fn ms_for_n_slots(num_blocks: u64, ticks_per_slot: u64) -> u64 { (ticks_per_slot * DEFAULT_MS_PER_SLOT * num_blocks).div_ceil(DEFAULT_TICKS_PER_SLOT) } -// Test runner that performs the following steps: -// 1) Defines validator stake partitions based on input parameters -// 2) Defines leader schedule based on input parameters -// 3) Appends routine to kill specified validators on partition start -// 4) Runs cluster partition -pub fn run_kill_partition_switch_threshold( +/// Implements a test scenario that creates a network partition by killing validator nodes. +/// +/// # Arguments +/// * `stakes_to_kill` - Validators to remove from the network, where each tuple contains: +/// * First element (usize): The stake weight/size of the validator +/// * Second element (usize): The number of slots assigned to the validator +/// * `alive_stakes` - Validators to keep alive, where each tuple contains: +/// * First element (usize): The stake weight/size of the validator +/// * Second element (usize): The number of slots assigned to the validator +/// * `ticks_per_slot` - Optional override for the default ticks per slot +/// * `partition_context` - Test-specific context object that will be passed to callbacks +/// * `on_partition_start` - Callback executed when the partition begins +/// * `on_before_partition_resolved` - Callback executed right before the partition is resolved +/// * `on_partition_resolved` - Callback executed after the partition is resolved +/// +/// This function simulates a network partition by killing specified validator nodes, +/// waiting for a period, resolving the partition, and then verifying the network +/// can recover and reach consensus. The IS_ALPENGLOW parameter determines whether +/// to use Alpenglow-specific cluster initialization. +fn run_kill_partition_switch_threshold_impl( stakes_to_kill: &[(usize, usize)], alive_stakes: &[(usize, usize)], ticks_per_slot: Option, @@ -283,6 +299,47 @@ pub fn run_kill_partition_switch_threshold( ticks_per_slot, true, vec![], + IS_ALPENGLOW, + ) +} + +pub fn run_kill_partition_switch_threshold_alpenglow( + stakes_to_kill: &[(usize, usize)], + alive_stakes: &[(usize, usize)], + ticks_per_slot: Option, + partition_context: C, + on_partition_start: impl Fn(&mut LocalCluster, &[Pubkey], Vec, &mut C), + on_before_partition_resolved: impl Fn(&mut LocalCluster, &mut C), + on_partition_resolved: impl Fn(&mut LocalCluster, &mut C), +) { + run_kill_partition_switch_threshold_impl::( + stakes_to_kill, + alive_stakes, + ticks_per_slot, + partition_context, + on_partition_start, + on_before_partition_resolved, + on_partition_resolved, + ) +} + +pub fn run_kill_partition_switch_threshold( + stakes_to_kill: &[(usize, usize)], + alive_stakes: &[(usize, usize)], + ticks_per_slot: Option, + partition_context: C, + on_partition_start: impl Fn(&mut LocalCluster, &[Pubkey], Vec, &mut C), + on_before_partition_resolved: impl Fn(&mut LocalCluster, &mut C), + on_partition_resolved: impl Fn(&mut LocalCluster, &mut C), +) { + run_kill_partition_switch_threshold_impl::( + stakes_to_kill, + alive_stakes, + ticks_per_slot, + partition_context, + on_partition_start, + on_before_partition_resolved, + on_partition_resolved, ) } @@ -315,19 +372,36 @@ pub fn create_custom_leader_schedule_with_random_keys( (leader_schedule, validator_keys) } -/// This function runs a network, initiates a partition based on a -/// configuration, resolves the partition, then checks that the network -/// continues to achieve consensus. +/// Simulates a network partition test scenario by creating a cluster, triggering a partition, +/// allowing the partition to heal, and then verifying the network's ability to recover and +/// achieve consensus after the partition is resolved. /// -/// # Arguments: -/// * `partitions` - A slice of partition configurations, where each partition -/// configuration is a usize representing a node's relative stake -/// * `leader_schedule` - An option that specifies whether the cluster should -/// run with a fixed, predetermined leader schedule -/// * `no_wait_for_vote_to_start_leader` - provide option to only allow the -/// bootstrap to build blocks at first to minimize forking during cluster -/// startup. -#[allow(clippy::cognitive_complexity)] +/// This function: +/// 1. Creates a local cluster with nodes configured according to the provided stakes +/// 2. Induces a network partition by disabling communication between validators +/// 3. Runs the partition for a predetermined duration +/// 4. Resolves the partition by re-enabling communication +/// 5. Verifies the network can recover and continue to make progress +/// +/// # Arguments +/// * `partitions` - A slice of partition configurations, where each usize represents a validator's +/// stake weight. This determines the relative voting power of each node in the network. +/// * `leader_schedule` - An option that specifies whether the cluster should run with a fixed, +/// predetermined leader schedule. If provided, the partition will last for one complete +/// iteration of the leader schedule. +/// * `context` - A user-defined context object that is passed to the callback functions. +/// * `on_partition_start` - Callback function that runs when the partition begins. Can be used +/// to perform custom actions or checks at the start of the partition. +/// * `on_before_partition_resolved` - Callback function that runs just before the partition +/// is resolved. Can be used to verify partition state or prepare for resolution. +/// * `on_partition_resolved` - Callback function that runs after the partition is resolved and +/// the network has had time to recover. Can be used to verify recovery. +/// * `ticks_per_slot` - Optional override for the default ticks per slot. Controls the +/// rate at which slots advance in the cluster. +/// * `additional_accounts` - Additional accounts to be added to the genesis configuration. +/// * `is_alpenglow` - Boolean flag indicating whether to initialize the `LocalCluster` in Alpenglow +/// mode. +#[allow(clippy::cognitive_complexity, clippy::too_many_arguments)] pub fn run_cluster_partition( partitions: &[usize], leader_schedule: Option<(LeaderSchedule, Vec)>, @@ -338,8 +412,13 @@ pub fn run_cluster_partition( ticks_per_slot: Option, no_wait_for_vote_to_start_leader: bool, additional_accounts: Vec<(Pubkey, AccountSharedData)>, + is_alpenglow: bool, ) { - agave_logger::setup_with_default(RUST_LOG_FILTER); + if is_alpenglow { + agave_logger::setup_with_default(AG_DEBUG_LOG_FILTER); + } else { + agave_logger::setup_with_default(RUST_LOG_FILTER); + } info!("PARTITION_TEST!"); let num_nodes = partitions.len(); let node_stakes: Vec<_> = partitions @@ -401,6 +480,13 @@ pub fn run_cluster_partition( .unwrap() .no_wait_for_vote_to_start_leader = true; let slots_per_epoch = 2048; + let alpenglow_port_override = AlpenglowPortOverride::default(); + for config in &mut validator_configs { + config.voting_service_test_override = Some(VotingServiceOverride { + additional_listeners: vec![], + alpenglow_port_override: alpenglow_port_override.clone(), + }); + } let mut config = ClusterConfig { mint_lamports, node_stakes, @@ -424,7 +510,12 @@ pub fn run_cluster_partition( "PARTITION_TEST starting cluster with {:?} partitions slots_per_epoch: {}", partitions, config.slots_per_epoch, ); - let mut cluster = LocalCluster::new(&mut config, SocketAddrSpace::Unspecified); + + let mut cluster = if is_alpenglow { + LocalCluster::new_alpenglow(&mut config, SocketAddrSpace::Unspecified) + } else { + LocalCluster::new(&mut config, SocketAddrSpace::Unspecified) + }; info!("PARTITION_TEST spend_and_verify_all_nodes(), ensure all nodes are caught up"); cluster_tests::spend_and_verify_all_nodes( @@ -456,11 +547,23 @@ pub fn run_cluster_partition( on_partition_start(&mut cluster, &mut context); turbine_mode.set(TurbineModeKind::TurbineAndRepairDisabled); + // Make all to all votes/certs not able to reach each other by overriding the + // alpenglow port override to SocketAddr which no one is listening on. + let blackhole_socket = bind_to_localhost_unique().unwrap(); + let blackhole_addr: SocketAddr = blackhole_socket.local_addr().unwrap(); + let new_override = HashMap::from_iter( + cluster_nodes + .iter() + .map(|node| (*node.pubkey(), blackhole_addr)), + ); + alpenglow_port_override.update_override(new_override); sleep(partition_duration); on_before_partition_resolved(&mut cluster, &mut context); info!("PARTITION_TEST remove partition"); turbine_mode.set(TurbineModeKind::Enabled); + // Restore the alpenglow port override to the default, so that the nodes can communicate again. + alpenglow_port_override.clear(); // Give partitions time to propagate their blocks from during the partition // after the partition resolves diff --git a/local-cluster/src/local_cluster.rs b/local-cluster/src/local_cluster.rs index e0bf90f1628..37e9d6e10b6 100644 --- a/local-cluster/src/local_cluster.rs +++ b/local-cluster/src/local_cluster.rs @@ -7,6 +7,7 @@ use { }, agave_feature_set::{FeatureSet, bls_pubkey_management_in_vote_account}, agave_snapshots::{paths::BANK_SNAPSHOTS_DIR, snapshot_config::SnapshotConfig}, + agave_votor::vote_history_storage::FileVoteHistoryStorage, itertools::izip, log::*, solana_account::{Account, AccountSharedData, ReadableAccount}, @@ -189,6 +190,8 @@ impl LocalCluster { ]; config.tower_storage = Arc::new(FileTowerStorage::new(ledger_path.to_path_buf())); config.accounts_db_config.bank_hash_details_dir = ledger_path.to_path_buf(); + config.vote_history_storage = + Arc::new(FileVoteHistoryStorage::new(ledger_path.to_path_buf())); let snapshot_config = &mut config.snapshot_config; let dummy: PathBuf = DUMMY_SNAPSHOT_CONFIG_PATH_MARKER.into(); diff --git a/local-cluster/tests/local_cluster.rs b/local-cluster/tests/local_cluster.rs index 94ab61b8af7..8f76a822f2b 100644 --- a/local-cluster/tests/local_cluster.rs +++ b/local-cluster/tests/local_cluster.rs @@ -15,6 +15,7 @@ use { serial_test::serial, solana_account::AccountSharedData, solana_accounts_db::utils::create_accounts_run_and_snapshot_dirs, + solana_client::rpc_client::RpcClient, solana_client_traits::AsyncClient, solana_clock::{DEFAULT_SLOTS_PER_EPOCH, DEFAULT_TICKS_PER_SLOT, MAX_PROCESSING_AGE, Slot}, solana_cluster_type::ClusterType, @@ -72,7 +73,6 @@ use { solana_poh_config::PohConfig, solana_pubkey::Pubkey, solana_pubsub_client::pubsub_client::PubsubClient, - solana_rpc_client::rpc_client::RpcClient, solana_rpc_client_api::{ config::{ RpcBlockSubscribeConfig, RpcBlockSubscribeFilter, RpcProgramAccountsConfig, @@ -90,7 +90,10 @@ use { BroadcastStageType, broadcast_duplicates_run::{BroadcastDuplicatesConfig, ClusterPartition}, }, - solana_vote::{vote_parser, vote_transaction}, + solana_vote::{ + vote_parser::{self}, + vote_transaction, + }, solana_vote_interface::state::TowerSync, solana_vote_program::vote_state::MAX_LOCKOUT_HISTORY, std::{ @@ -2585,6 +2588,7 @@ fn test_run_test_load_program_accounts_partition_root() { None, false, additional_accounts, + false, ); } @@ -3621,7 +3625,17 @@ fn test_fork_choice_refresh_old_votes() { /// locked-out survivors still converge on a rooted fork. #[test] #[serial] -fn test_kill_heaviest_partition() { +fn test_kill_heaviest_partition_tower() { + test_kill_heaviest_partition(false); +} + +#[test] +#[serial] +fn test_kill_heaviest_partition_alpenglow() { + test_kill_heaviest_partition(true); +} + +fn test_kill_heaviest_partition(is_alpenglow: bool) { // This test: // 1) Spins up four partitions, the heaviest being the first with more stake // 2) Schedules the other validators for sufficient slots in the schedule @@ -3661,6 +3675,7 @@ fn test_kill_heaviest_partition() { None, true, vec![], + is_alpenglow, ) } @@ -4242,13 +4257,25 @@ fn find_latest_replayed_slot_from_ledger( /// Stricter fork-choice assertions are covered by separate partition tests. #[test] #[serial] -fn test_cluster_partition() { +fn test_cluster_partition_1_1_1() { + run_test_cluster_partition(3, false); +} + +/// Partition/recovery test for Alpenglow +#[test] +#[serial] +fn test_cluster_partition_1_1_1_alpenglow() { + run_test_cluster_partition(3, true); +} + +fn run_test_cluster_partition(num_partitions: usize, is_alpenglow: bool) { let empty = |_: &mut LocalCluster, _: &mut ()| {}; let on_partition_resolved = |cluster: &mut LocalCluster, _: &mut ()| { cluster.check_for_new_roots(16, "PARTITION_TEST", SocketAddrSpace::Unspecified); }; + let partition_sizes = vec![1; num_partitions]; run_cluster_partition( - &[1, 1, 1], + &partition_sizes, None, (), empty, @@ -4257,6 +4284,7 @@ fn test_cluster_partition() { None, false, vec![], + is_alpenglow, ) } From a4a1d7f743d02a99f02c5a30c0bdda919ac3b345 Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Tue, 9 Jun 2026 13:14:27 -0400 Subject: [PATCH 263/576] votor: do not reinsert votes / save VoteHistory for Standstill votes (#13004) --- votor/src/event_handler.rs | 80 ++++++++++++++-------- votor/src/voting_service.rs | 30 ++++++++- votor/src/voting_utils.rs | 130 +++++++++++++++++++++--------------- 3 files changed, 156 insertions(+), 84 deletions(-) diff --git a/votor/src/event_handler.rs b/votor/src/event_handler.rs index bcc85bbfa86..08777d6ea1c 100644 --- a/votor/src/event_handler.rs +++ b/votor/src/event_handler.rs @@ -14,7 +14,10 @@ use { timer_manager::TimerManager, vote_history::{VoteHistory, VoteHistoryError}, voting_service::BLSOp, - voting_utils::{VoteError, VotingContext, generate_vote_message}, + voting_utils::{ + VoteError, VotingContext, generate_refresh_vote_message, + insert_vote_and_create_bls_message, + }, votor::SharedContext, }, agave_votor_messages::{consensus_message::Block, migration::MigrationStatus, vote::Vote}, @@ -399,9 +402,10 @@ impl EventHandler { return Ok(votes); } info!("{my_pubkey}: Voting notarize-fallback for {block:?}"); - if let Some(bls_op) = - generate_vote_message(Vote::new_notarization_fallback_vote(block), false, vctx)? - { + if let Some(bls_op) = insert_vote_and_create_bls_message( + Vote::new_notarization_fallback_vote(block), + vctx, + )? { votes.push(bls_op); } } @@ -415,7 +419,7 @@ impl EventHandler { } info!("{my_pubkey}: Voting skip-fallback for {slot}"); if let Some(bls_op) = - generate_vote_message(Vote::new_skip_fallback_vote(slot), false, vctx)? + insert_vote_and_create_bls_message(Vote::new_skip_fallback_vote(slot), vctx)? { votes.push(bls_op); } @@ -674,9 +678,8 @@ impl EventHandler { } info!("{my_pubkey}: Voting notarize for {slot} {block_id}"); - if let Some(bls_op) = generate_vote_message( + if let Some(bls_op) = insert_vote_and_create_bls_message( Vote::new_notarization_vote(Block { slot, block_id }), - false, voting_context, )? { votes.push(bls_op); @@ -750,9 +753,8 @@ impl EventHandler { } info!("{my_pubkey}: Voting finalize for {}", block.slot); - if let Some(bls_op) = generate_vote_message( + if let Some(bls_op) = insert_vote_and_create_bls_message( Vote::new_finalization_vote(block.slot), - false, voting_context, )? { votes.push(bls_op); @@ -783,7 +785,7 @@ impl EventHandler { } info!("{my_pubkey}: Voting skip for {s}"); if let Some(bls_op) = - generate_vote_message(Vote::new_skip_vote(s), false, voting_context)? + insert_vote_and_create_bls_message(Vote::new_skip_vote(s), voting_context)? { votes.push(bls_op); } @@ -798,15 +800,21 @@ impl EventHandler { voting_context: &mut VotingContext, votes: &mut Vec, ) -> Result<(), VoteError> { + let mut refreshed_votes = Vec::new(); for vote in voting_context .vote_history .votes_cast_since(highest_finalized_slot) { info!("{my_pubkey}: Refreshing vote {vote:?}"); - if let Some(bls_op) = generate_vote_message(vote, true, voting_context)? { - votes.push(bls_op); + if let Some(vote_msg) = generate_refresh_vote_message(vote, voting_context)? { + refreshed_votes.push(Arc::new(vote_msg)); } } + if !refreshed_votes.is_empty() { + votes.push(BLSOp::RefreshVotes { + votes: refreshed_votes, + }); + } Ok(()) } @@ -1353,14 +1361,22 @@ mod tests { rank: 0, signature, }; - let prev_length = self.bls_ops.len(); - self.bls_ops.retain( - |bls_op| !matches!(bls_op, BLSOp::PushVote{ vote, .. } if **vote == expected_message), - ); - assert!( - self.bls_ops.len() < prev_length, - "Did not find expected vote: {expected_message:?}", - ); + let mut found = false; + self.bls_ops.retain_mut(|bls_op| match bls_op { + BLSOp::PushVote { vote, .. } => { + let keep = vote.as_ref() != &expected_message; + found |= !keep; + keep + } + BLSOp::RefreshVotes { votes } => { + let previous_len = votes.len(); + votes.retain(|vote| vote.as_ref() != &expected_message); + found |= votes.len() != previous_len; + !votes.is_empty() + } + BLSOp::PushCertificates { .. } => true, + }); + assert!(found, "Did not find expected vote: {expected_message:?}"); } } @@ -1373,14 +1389,22 @@ mod tests { rank: 0, signature, }; - let prev_length = self.bls_ops.len(); - self.bls_ops.retain( - |bls_op| !matches!(bls_op, BLSOp::PushVote { vote, .. } if **vote == expected_message), - ); - assert!( - self.bls_ops.len() < prev_length, - "Did not find expected vote: {expected_message:?}", - ); + let mut found = false; + self.bls_ops.retain_mut(|bls_op| match bls_op { + BLSOp::PushVote { vote, .. } => { + let keep = vote.as_ref() != &expected_message; + found |= !keep; + keep + } + BLSOp::RefreshVotes { votes } => { + let previous_len = votes.len(); + votes.retain(|vote| vote.as_ref() != &expected_message); + found |= votes.len() != previous_len; + !votes.is_empty() + } + BLSOp::PushCertificates { .. } => true, + }); + assert!(found, "Did not find expected vote: {expected_message:?}"); // Also check own_vote_receiver let own_vote = self.own_vote_receiver.try_recv().unwrap(); assert_eq!(own_vote, SigVerifiedBatch::Votes(vec![expected_message])); diff --git a/votor/src/voting_service.rs b/votor/src/voting_service.rs index 09cf5db9fa9..cbf8588bde5 100644 --- a/votor/src/voting_service.rs +++ b/votor/src/voting_service.rs @@ -40,6 +40,9 @@ pub enum BLSOp { PushCertificates { certificates: Vec>, }, + RefreshVotes { + votes: Vec>, + }, } fn send_message( @@ -239,6 +242,20 @@ impl VotingService { ); } } + BLSOp::RefreshVotes { votes } => { + for vote in votes { + let slot = vote.vote.slot(); + let msg = ConsensusMessage::Vote(Arc::unwrap_or_clone(vote)); + Self::broadcast_consensus_message( + slot, + cluster_info, + &msg, + connection_cache, + additional_listeners, + staked_validators_cache, + ); + } + } } } @@ -341,7 +358,7 @@ mod tests { }))] #[test_case(BLSOp::PushCertificates { certificates: vec![Arc::new(Certificate { - cert_type: CertificateType::Skip(5), + cert_type: CertificateType::Skip(5), signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), bitmap: Vec::new(), })], @@ -350,6 +367,17 @@ mod tests { signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), bitmap: Vec::new(), }))] + #[test_case(BLSOp::RefreshVotes { + votes: vec![Arc::new(VoteMessage { + vote: Vote::new_skip_vote(6), + signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), + rank: 1, + })], + }, ConsensusMessage::Vote(VoteMessage { + vote: Vote::new_skip_vote(6), + signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), + rank: 1, + }))] fn test_send_message(bls_op: BLSOp, expected_message: ConsensusMessage) { agave_logger::setup(); let (bls_sender, bls_receiver) = bounded(1024); diff --git a/votor/src/voting_utils.rs b/votor/src/voting_utils.rs index 44b0a78a680..ec2121383ed 100644 --- a/votor/src/voting_utils.rs +++ b/votor/src/voting_utils.rs @@ -223,28 +223,9 @@ pub fn generate_vote_tx( }) } -/// Send an alpenglow vote as a BLSMessage -/// `bank` will be used for: -/// - startup verification -/// - vote account checks -/// - authorized voter checks -/// -/// We also update the vote history and send the vote to -/// the certificate pool thread for ingestion. -/// -/// Returns false if we are currently a non-voting node -fn insert_vote_and_create_bls_message( - vote: Vote, - is_refresh: bool, - context: &mut VotingContext, -) -> Result { - // Update and save the vote history - if !is_refresh { - context.vote_history.add_vote(vote); - } - +fn create_vote_message(vote: Vote, context: &mut VotingContext) -> Result { let bank = context.sharable_banks.root(); - let vote_msg = match generate_vote_tx( + match generate_vote_tx( vote, &bank, context.vote_account_pubkey, @@ -253,52 +234,79 @@ fn insert_vote_and_create_bls_message( context.wait_to_vote_slot, &mut context.derived_bls_keypairs, ) { - GenerateVoteTxResult::Vote(vote) => vote, + GenerateVoteTxResult::Vote(vote) => Ok(vote), e => { if e.is_transient_error() { - return Err(VoteError::TransientError(Box::new(e))); + Err(VoteError::TransientError(Box::new(e))) } else { - return Err(VoteError::InvalidConfig(Box::new(e))); + Err(VoteError::InvalidConfig(Box::new(e))) } } + } +} + +fn handle_skippable_vote_error(err: VoteError, action: &str) -> Result<(), VoteError> { + match err { + VoteError::InvalidConfig(e) => { + warn!("Failed to {action}: {e:?}"); + // These are not fatal errors, just skip the vote for now. But they are + // misconfigurations that should be warned about. + Ok(()) + } + VoteError::TransientError(e) => { + info!("Failed to {action}: {e:?}"); + // These are transient errors, just skip the vote for now. + Ok(()) + } + e => Err(e), + } +} + +/// Build a normal push-vote BLS op. +/// +/// This updates vote history, sends the vote to the certificate pool thread for ingestion, and +/// saves vote history for persistence. +pub fn insert_vote_and_create_bls_message( + vote: Vote, + context: &mut VotingContext, +) -> Result, VoteError> { + // Update and save the vote history + context.vote_history.add_vote(vote); + + let vote_msg = match create_vote_message(vote, context) { + Ok(vote_msg) => vote_msg, + Err(e) => { + handle_skippable_vote_error(e, "generate vote and push to votes")?; + return Ok(None); + } }; + context .own_vote_sender .send(SigVerifiedBatch::Votes(vec![vote_msg.clone()])) .map_err(|_| SendError(()))?; - // TODO: for refresh votes use a different BLSOp so we don't have to rewrite the same vote history to file let saved_vote_history = SavedVoteHistory::new(&context.vote_history, &context.identity_keypair)?; // Return vote for sending - Ok(BLSOp::PushVote { + Ok(Some(BLSOp::PushVote { vote: Arc::new(vote_msg), saved_vote_history: SavedVoteHistoryVersions::from(saved_vote_history), - }) + })) } -pub fn generate_vote_message( +pub fn generate_refresh_vote_message( vote: Vote, - is_refresh: bool, vctx: &mut VotingContext, -) -> Result, VoteError> { - let bls_op = match insert_vote_and_create_bls_message(vote, is_refresh, vctx) { - Ok(bls_op) => bls_op, - Err(VoteError::InvalidConfig(e)) => { - warn!("Failed to generate vote and push to votes: {e:?}"); - // These are not fatal errors, just skip the vote for now. But they are misconfigurations - // that should be warned about. - return Ok(None); - } - Err(VoteError::TransientError(e)) => { - info!("Failed to generate vote and push to votes: {e:?}"); - // These are transient errors, just skip the vote for now. - return Ok(None); +) -> Result, VoteError> { + match create_vote_message(vote, vctx) { + Ok(vote_msg) => Ok(Some(vote_msg)), + Err(e) => { + handle_skippable_vote_error(e, "generate refresh vote message")?; + Ok(None) } - Err(e) => return Err(e), - }; - Ok(Some(bls_op)) + } } #[cfg(test)] @@ -403,7 +411,7 @@ mod tests { slot: vote_slot, block_id, }); - let result = generate_vote_message(vote, false, &mut voting_context) + let result = insert_vote_and_create_bls_message(vote, &mut voting_context) .ok() .unwrap() .unwrap(); @@ -433,8 +441,20 @@ mod tests { let received_message = own_vote_receiver.recv().unwrap(); assert_eq!( received_message, - SigVerifiedBatch::Votes(vec![expected_message]) + SigVerifiedBatch::Votes(vec![expected_message.clone()]) ); + + let refresh_vote = Vote::new_notarization_vote(Block { + slot: vote_slot, + block_id, + }); + let refresh_result = generate_refresh_vote_message(refresh_vote, &mut voting_context) + .ok() + .unwrap() + .unwrap(); + assert_eq!(refresh_result.vote.slot(), vote_slot); + assert_eq!(refresh_result, expected_message); + assert!(own_vote_receiver.try_recv().is_err()); } #[test] @@ -452,7 +472,7 @@ mod tests { voting_context.wait_to_vote_slot = Some(4); let vote = Vote::new_finalization_vote(2); assert!( - generate_vote_message(vote, false, &mut voting_context) + insert_vote_and_create_bls_message(vote, &mut voting_context) .unwrap() .is_none() ); @@ -460,7 +480,7 @@ mod tests { // If we have reached wait_to_vote_slot, we should be able to vote voting_context.wait_to_vote_slot = Some(1); assert!( - generate_vote_message(vote, false, &mut voting_context) + insert_vote_and_create_bls_message(vote, &mut voting_context) .unwrap() .is_some() ); @@ -498,7 +518,7 @@ mod tests { validator_keypairs[my_index].vote_keypair.insecure_clone(), )])); assert!( - generate_vote_message(vote, false, &mut voting_context) + insert_vote_and_create_bls_message(vote, &mut voting_context) .unwrap() .is_some() ); @@ -553,7 +573,7 @@ mod tests { block_id: Hash::new_unique(), }); assert!( - generate_vote_message(vote, true, &mut voting_context) + generate_refresh_vote_message(vote, &mut voting_context) .unwrap() .is_none() ); @@ -561,7 +581,7 @@ mod tests { // Recover correct value to vote again voting_context.vote_account_pubkey = validator_keypairs[my_index].vote_keypair.pubkey(); assert!( - generate_vote_message(vote, true, &mut voting_context) + generate_refresh_vote_message(vote, &mut voting_context) .unwrap() .is_some() ); @@ -585,7 +605,7 @@ mod tests { slot: 1_000_000_000, block_id: Hash::new_unique(), }); - let _ = generate_vote_message(vote, false, &mut voting_context); + let _ = insert_vote_and_create_bls_message(vote, &mut voting_context); } #[test] @@ -644,7 +664,7 @@ mod tests { block_id: Hash::new_unique(), }); assert!( - generate_vote_message(vote, false, &mut voting_context) + insert_vote_and_create_bls_message(vote, &mut voting_context) .unwrap() .is_some() ); @@ -660,7 +680,7 @@ mod tests { block_id: Hash::new_unique(), }); assert!( - generate_vote_message(vote, false, &mut voting_context) + insert_vote_and_create_bls_message(vote, &mut voting_context) .unwrap() .is_none() ); From 23e95fca5bab951066e025091114578e4a382c5b Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Tue, 9 Jun 2026 13:14:44 -0400 Subject: [PATCH 264/576] rank_map: reject vote accounts with duplicate pubkeys (#12966) --- runtime/src/bank.rs | 2 +- runtime/src/epoch_stakes.rs | 286 ++++++++++++++++++++++++++++++++---- votor/src/consensus_pool.rs | 8 +- 3 files changed, 264 insertions(+), 32 deletions(-) diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index fec59ccd9d3..6da14f51981 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -5752,7 +5752,7 @@ impl Bank { .epoch_stakes_from_slot(slot) .ok_or(CertVerifyError::MissingRankMap)?; let key_to_rank_map = epoch_stakes.bls_pubkey_to_rank_map(); - let total_stake = epoch_stakes.total_stake(); + let total_stake = key_to_rank_map.total_stake(); let stake = cert_verify::verify_certificate(cert, key_to_rank_map.len(), |rank| { key_to_rank_map diff --git a/runtime/src/epoch_stakes.rs b/runtime/src/epoch_stakes.rs index 980f89c6527..6dbb2c87970 100644 --- a/runtime/src/epoch_stakes.rs +++ b/runtime/src/epoch_stakes.rs @@ -49,6 +49,7 @@ pub struct BLSPubkeyToRankMap { rank_map: HashMap, vote_pubkey_to_rank: HashMap, sorted_pubkeys: Vec, + total_stake: u64, } // We cannot auto derive `AbiExample` for `BLSPubkeyToRankMap` because @@ -60,6 +61,7 @@ impl solana_frozen_abi::abi_example::AbiExample for BLSPubkeyToRankMap { rank_map: HashMap::new(), vote_pubkey_to_rank: HashMap::new(), sorted_pubkeys: Vec::new(), + total_stake: 0, } } } @@ -78,32 +80,43 @@ pub(crate) fn bls_pubkey_compressed_bytes_to_bls_pubkey( impl BLSPubkeyToRankMap { pub fn new(epoch_vote_accounts_hash_map: &VoteAccountsHashMap) -> Self { + let mut candidates = Vec::with_capacity(epoch_vote_accounts_hash_map.len()); + let mut bls_pubkey_counts = HashMap::new(); + let mut node_pubkey_counts = HashMap::new(); + for (&vote_account_pubkey, (stake, account)) in epoch_vote_accounts_hash_map { + if *stake == 0 { + continue; + } + let node_pubkey = *account.vote_state_view().node_pubkey(); + let Some((bls_pubkey_compressed, bls_pubkey)) = account + .vote_state_view() + .bls_pubkey_compressed() + .and_then(bls_pubkey_compressed_bytes_to_bls_pubkey) + else { + continue; + }; + let entry = BLSPubkeyStakeEntry { + vote_account_pubkey, + node_pubkey, + bls_pubkey, + stake: *stake, + }; + *bls_pubkey_counts.entry(bls_pubkey_compressed).or_insert(0) += 1; + *node_pubkey_counts.entry(node_pubkey).or_insert(0) += 1; + candidates.push((entry, bls_pubkey_compressed)); + } let mut keys_stake_entry_with_compressed: Vec<(BLSPubkeyStakeEntry, BLSPubkeyCompressed)> = - epoch_vote_accounts_hash_map - .iter() - .filter_map(|(&vote_account_pubkey, (stake, account))| { - if *stake > 0 { - let node_pubkey = *account.vote_state_view().node_pubkey(); - account - .vote_state_view() - .bls_pubkey_compressed() - .and_then(bls_pubkey_compressed_bytes_to_bls_pubkey) - .map(|(bls_pubkey_compressed, bls_pubkey)| { - ( - BLSPubkeyStakeEntry { - vote_account_pubkey, - node_pubkey, - bls_pubkey, - stake: *stake, - }, - bls_pubkey_compressed, - ) - }) - } else { - None - } + candidates + .into_iter() + .filter_map(|(entry, bls_pubkey_compressed)| { + (bls_pubkey_counts[&bls_pubkey_compressed] == 1 + && node_pubkey_counts[&entry.node_pubkey] == 1) + .then_some((entry, bls_pubkey_compressed)) }) .collect(); + let total_stake = keys_stake_entry_with_compressed + .iter() + .fold(0u64, |stake, (entry, _)| stake.saturating_add(entry.stake)); keys_stake_entry_with_compressed.sort_by( |(a_entry, a_pubkey_compressed), (b_entry, b_pubkey_compressed)| { b_entry @@ -128,6 +141,7 @@ impl BLSPubkeyToRankMap { rank_map: bls_pubkey_to_rank_map, vote_pubkey_to_rank: vote_pubkey_to_rank_map, sorted_pubkeys, + total_stake, } } @@ -136,7 +150,11 @@ impl BLSPubkeyToRankMap { } pub fn len(&self) -> usize { - self.rank_map.len() + self.sorted_pubkeys.len() + } + + pub fn total_stake(&self) -> u64 { + self.total_stake } pub fn get_rank(&self, bls_pubkey: &PopVerified) -> Option<&u16> { @@ -662,7 +680,18 @@ pub(crate) mod tests { }); let epoch_stakes = VersionedEpochStakes::new_for_tests(epoch_vote_accounts.clone(), 0); let bls_pubkey_to_rank_map = epoch_stakes.bls_pubkey_to_rank_map(); - assert_eq!(bls_pubkey_to_rank_map.len(), num_vote_accounts); + let expected_num_vote_accounts = if num_vote_accounts_per_node == 1 { + num_vote_accounts + } else { + 0 + }; + assert_eq!(bls_pubkey_to_rank_map.len(), expected_num_vote_accounts); + let expected_total_stake = if num_vote_accounts_per_node == 1 { + epoch_stakes.total_stake() + } else { + 0 + }; + assert_eq!(bls_pubkey_to_rank_map.total_stake(), expected_total_stake); for (vote_account_pubkey, (stake, vote_account)) in epoch_vote_accounts { let vote_state_view = vote_account.vote_state_view(); let (_comp, bls_pubkey) = bls_pubkey_compressed_bytes_to_bls_pubkey( @@ -670,8 +699,17 @@ pub(crate) mod tests { ) .unwrap(); let node_pubkey = *vote_state_view.node_pubkey(); + if num_vote_accounts_per_node > 1 { + assert!(bls_pubkey_to_rank_map.get_rank(&bls_pubkey).is_none()); + assert!( + bls_pubkey_to_rank_map + .get_rank_for_vote_pubkey(&vote_account_pubkey) + .is_none() + ); + continue; + } let index = bls_pubkey_to_rank_map.get_rank(&bls_pubkey).unwrap(); - assert!(index >= &0 && index < &(num_vote_accounts as u16)); + assert!(index >= &0 && index < &(expected_num_vote_accounts as u16)); assert_eq!( bls_pubkey_to_rank_map.get_pubkey_stake_entry(*index as usize), Some(&BLSPubkeyStakeEntry { @@ -693,6 +731,202 @@ pub(crate) mod tests { assert_eq!(bls_pubkey_to_rank_map2, bls_pubkey_to_rank_map); } + #[test] + fn test_bls_pubkey_rank_map_excludes_duplicate_bls_and_identity() { + let new_bls_pubkey = || { + let bls_pubkey_compressed: BLSPubkeyCompressed = (*BLSKeypair::new().public).into(); + let bls_pubkey_compressed_serialized = bincode::serialize(&bls_pubkey_compressed) + .unwrap() + .try_into() + .unwrap(); + let (_bls_pubkey_compressed, bls_pubkey) = + bls_pubkey_compressed_bytes_to_bls_pubkey(bls_pubkey_compressed_serialized) + .unwrap(); + (bls_pubkey_compressed_serialized, bls_pubkey) + }; + + let (duplicate_bls_pubkey_serialized, duplicate_bls_pubkey) = new_bls_pubkey(); + let (duplicate_node_bls_pubkey_serialized, duplicate_node_bls_pubkey) = new_bls_pubkey(); + let (duplicate_node_bls_pubkey_serialized_2, duplicate_node_bls_pubkey_2) = + new_bls_pubkey(); + let (shared_voter_bls_pubkey_serialized, shared_voter_bls_pubkey) = new_bls_pubkey(); + let (shared_voter_bls_pubkey_serialized_2, shared_voter_bls_pubkey_2) = new_bls_pubkey(); + let (unique_bls_pubkey_serialized, unique_bls_pubkey) = new_bls_pubkey(); + + let duplicate_bls_vote_pubkey = Pubkey::new_unique(); + let duplicate_bls_vote_pubkey_2 = Pubkey::new_unique(); + let duplicate_node_vote_pubkey = Pubkey::new_unique(); + let duplicate_node_vote_pubkey_2 = Pubkey::new_unique(); + let shared_voter_vote_pubkey = Pubkey::new_unique(); + let shared_voter_vote_pubkey_2 = Pubkey::new_unique(); + let unique_vote_pubkey = Pubkey::new_unique(); + + let duplicate_node_pubkey = Pubkey::new_unique(); + let shared_authorized_voter = Pubkey::new_unique(); + let shared_voter_node_pubkey = Pubkey::new_unique(); + let shared_voter_node_pubkey_2 = Pubkey::new_unique(); + let unique_node_pubkey = Pubkey::new_unique(); + let unique_voter = Pubkey::new_unique(); + + let account = |node_pubkey, authorized_voter, bls_pubkey| { + VoteAccount::try_from(create_v4_account_with_authorized( + &node_pubkey, + &authorized_voter, + bls_pubkey, + &node_pubkey, + 0, + &node_pubkey, + 0, + &node_pubkey, + 100, + )) + .unwrap() + }; + let epoch_vote_accounts = VoteAccountsHashMap::from([ + ( + duplicate_bls_vote_pubkey, + ( + 100, + account( + Pubkey::new_unique(), + Pubkey::new_unique(), + duplicate_bls_pubkey_serialized, + ), + ), + ), + ( + duplicate_bls_vote_pubkey_2, + ( + 100, + account( + Pubkey::new_unique(), + Pubkey::new_unique(), + duplicate_bls_pubkey_serialized, + ), + ), + ), + ( + duplicate_node_vote_pubkey, + ( + 100, + account( + duplicate_node_pubkey, + Pubkey::new_unique(), + duplicate_node_bls_pubkey_serialized, + ), + ), + ), + ( + duplicate_node_vote_pubkey_2, + ( + 100, + account( + duplicate_node_pubkey, + Pubkey::new_unique(), + duplicate_node_bls_pubkey_serialized_2, + ), + ), + ), + ( + shared_voter_vote_pubkey, + ( + 100, + account( + shared_voter_node_pubkey, + shared_authorized_voter, + shared_voter_bls_pubkey_serialized, + ), + ), + ), + ( + shared_voter_vote_pubkey_2, + ( + 100, + account( + shared_voter_node_pubkey_2, + shared_authorized_voter, + shared_voter_bls_pubkey_serialized_2, + ), + ), + ), + ( + unique_vote_pubkey, + ( + 50, + account( + unique_node_pubkey, + unique_voter, + unique_bls_pubkey_serialized, + ), + ), + ), + ]); + + let rank_map = BLSPubkeyToRankMap::new(&epoch_vote_accounts); + + assert_eq!(rank_map.len(), 3); + assert_eq!(rank_map.total_stake(), 250); + for bls_pubkey in [ + duplicate_bls_pubkey, + duplicate_node_bls_pubkey, + duplicate_node_bls_pubkey_2, + ] { + assert!(rank_map.get_rank(&bls_pubkey).is_none()); + } + for vote_pubkey in [ + duplicate_bls_vote_pubkey, + duplicate_bls_vote_pubkey_2, + duplicate_node_vote_pubkey, + duplicate_node_vote_pubkey_2, + ] { + assert!(rank_map.get_rank_for_vote_pubkey(&vote_pubkey).is_none()); + } + + for (vote_account_pubkey, node_pubkey, bls_pubkey) in [ + ( + shared_voter_vote_pubkey, + shared_voter_node_pubkey, + shared_voter_bls_pubkey, + ), + ( + shared_voter_vote_pubkey_2, + shared_voter_node_pubkey_2, + shared_voter_bls_pubkey_2, + ), + ] { + let rank = *rank_map.get_rank(&bls_pubkey).unwrap(); + assert_eq!( + rank_map.get_rank_for_vote_pubkey(&vote_account_pubkey), + Some(&rank) + ); + assert_eq!( + rank_map.get_pubkey_stake_entry(rank as usize), + Some(&BLSPubkeyStakeEntry { + vote_account_pubkey, + node_pubkey, + bls_pubkey, + stake: 100, + }) + ); + } + + let unique_rank = *rank_map.get_rank(&unique_bls_pubkey).unwrap(); + assert_eq!( + rank_map.get_rank_for_vote_pubkey(&unique_vote_pubkey), + Some(&unique_rank) + ); + assert_eq!( + rank_map.get_pubkey_stake_entry(unique_rank as usize), + Some(&BLSPubkeyStakeEntry { + vote_account_pubkey: unique_vote_pubkey, + node_pubkey: unique_node_pubkey, + bls_pubkey: unique_bls_pubkey, + stake: 50, + }) + ); + assert!(rank_map.get_pubkey_stake_entry(rank_map.len()).is_none()); + } + #[test] fn test_versioned_epoch_stakes_does_not_serialize_delegations() { // test-only types to get the serialized EpochStakes that still have stake delegations diff --git a/votor/src/consensus_pool.rs b/votor/src/consensus_pool.rs index 2462a9e2320..411dd0326d5 100644 --- a/votor/src/consensus_pool.rs +++ b/votor/src/consensus_pool.rs @@ -67,10 +67,8 @@ fn get_key_and_stakes( let epoch_stakes = root_bank.epoch_stakes_from_slot(slot).ok_or_else(|| { AddVoteError::EpochStakesNotFound(root_bank.epoch_schedule().get_epoch(slot)) })?; - let Some(entry) = epoch_stakes - .bls_pubkey_to_rank_map() - .get_pubkey_stake_entry(rank as usize) - else { + let rank_map = epoch_stakes.bls_pubkey_to_rank_map(); + let Some(entry) = rank_map.get_pubkey_stake_entry(rank as usize) else { return Err(AddVoteError::InvalidRank(rank)); }; Ok(( @@ -81,7 +79,7 @@ fn get_key_and_stakes( entry.vote_account_pubkey, ) }), - NonZero::new(epoch_stakes.total_stake()).expect("expect total stakes to not be 0"), + NonZero::new(rank_map.total_stake()).expect("expect rank-map total stake to not be 0"), )) } From 22326522aadeb59691f642b81ac4b5e184741dd5 Mon Sep 17 00:00:00 2001 From: steviez Date: Tue, 9 Jun 2026 12:36:53 -0500 Subject: [PATCH 265/576] rpc-subscriptions: Consolidate metrics counters into datapoint (#13065) There is already a datapoint so move several counters into that datapoint. There is one counter that is removed that would be annoying to plumb through. Given that the desire is for RPC code to go away long term and that the removed counters provide information that can be roughly inferred from other metrics, rip them out --- rpc/src/rpc_subscriptions.rs | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/rpc/src/rpc_subscriptions.rs b/rpc/src/rpc_subscriptions.rs index a8f41bce01a..66f19391296 100644 --- a/rpc/src/rpc_subscriptions.rs +++ b/rpc/src/rpc_subscriptions.rs @@ -318,9 +318,6 @@ impl RpcNotifier { // just as the notifier generates a notification for it. let _ = self.sender.send(notification); - inc_new_counter_info!("rpc-pubsub-messages", 1); - inc_new_counter_info!("rpc-pubsub-bytes", buf_arc.len()); - self.recent_items.lock().unwrap().push(buf_arc); } } @@ -483,6 +480,11 @@ fn initial_last_notified_slot( #[derive(Default)] struct PubsubNotificationStats { since: Option, + notify_slot_count: u64, + notify_slot_update_count: u64, + notify_vote_count: u64, + notify_root_count: u64, + notify_signature_count: u64, notification_entry_processing_count: u64, notification_entry_processing_time_us: u64, } @@ -496,6 +498,15 @@ impl PubsubNotificationStats { } datapoint_info!( "pubsub_notification_entries", + ("notify_slot_count", self.notify_slot_count, i64), + ( + "notify_slot_update_count", + self.notify_slot_update_count, + i64 + ), + ("notify_vote_count", self.notify_vote_count, i64), + ("notify_root_count", self.notify_root_count, i64), + ("notify_signature_count", self.notify_signature_count, i64), ( "notification_entry_processing_count", self.notification_entry_processing_count, @@ -777,7 +788,7 @@ impl RpcSubscriptions { .get(&SubscriptionParams::Slot) { debug!("slot notify: {slot_info:?}"); - inc_new_counter_info!("rpc-subscription-notify-slot", 1); + stats.notify_slot_count += 1; notifier.notify(slot_info, sub, false); } } @@ -786,7 +797,8 @@ impl RpcSubscriptions { .node_progress_watchers() .get(&SubscriptionParams::SlotsUpdates) { - inc_new_counter_info!("rpc-subscription-notify-slots-updates", 1); + debug!("slot update notify: {slot_update:?}"); + stats.notify_slot_update_count += 1; notifier.notify(slot_update, sub, false); } } @@ -806,7 +818,7 @@ impl RpcSubscriptions { signature: signature.to_string(), }; debug!("vote notify: {vote_info:?}"); - inc_new_counter_info!("rpc-subscription-notify-vote", 1); + stats.notify_vote_count += 1; notifier.notify(&rpc_vote, sub, false); } } @@ -816,7 +828,7 @@ impl RpcSubscriptions { .get(&SubscriptionParams::Root) { debug!("root notify: {root:?}"); - inc_new_counter_info!("rpc-subscription-notify-root", 1); + stats.notify_root_count += 1; notifier.notify(root, sub, false); } } @@ -857,6 +869,7 @@ impl RpcSubscriptions { subscription.params() { if params.enable_received_notification { + stats.notify_signature_count += 1; notifier.notify( RpcResponse::from(RpcNotificationResponse { context: RpcNotificationContext { slot }, From a7c8a59acdbbaba8a63be02f534dbb018b43d101 Mon Sep 17 00:00:00 2001 From: Nye Liu Date: Tue, 9 Jun 2026 11:14:42 -0700 Subject: [PATCH 266/576] deps: Migrate from tiny-bip39 to canonical upstream bip39 (#12134) chore(deps): replace tiny-bip39 with bip39 crate tiny-bip39 is no longer being maintained. --- Cargo.lock | 80 ++++++++++++++-------------- Cargo.toml | 2 +- clap-utils/Cargo.toml | 2 +- clap-utils/src/keypair.rs | 14 ++--- clap-v3-utils/Cargo.toml | 2 +- clap-v3-utils/src/keygen/mnemonic.rs | 8 +-- clap-v3-utils/src/keypair.rs | 14 ++--- cli/Cargo.toml | 2 +- cli/src/program.rs | 10 ++-- dev-bins/Cargo.lock | 74 +++++++++++++------------ keygen/Cargo.toml | 2 +- keygen/src/keygen.rs | 28 +++++----- programs/sbf/Cargo.lock | 74 +++++++++++++------------ 13 files changed, 153 insertions(+), 159 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0ff58ac53bd..0f742a61790 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1320,11 +1320,24 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash 2.1.1", + "rustc-hash", "shlex", "syn 2.0.117", ] +[[package]] +name = "bip39" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" +dependencies = [ + "bitcoin_hashes", + "rand 0.8.6", + "rand_core 0.6.4", + "serde", + "unicode-normalization", +] + [[package]] name = "bit-set" version = "0.8.0" @@ -1340,6 +1353,15 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "bitcoin_hashes" +version = "0.14.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9901a56e133a1fc86eeb1113e2591f45f4682451ca893bff494d2f88918e3f" +dependencies = [ + "hex-conservative", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -3347,6 +3369,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hex-conservative" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +dependencies = [ + "arrayvec", +] + [[package]] name = "hidapi" version = "2.6.6" @@ -5030,16 +5061,6 @@ dependencies = [ "digest 0.10.7", ] -[[package]] -name = "pbkdf2" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" -dependencies = [ - "digest 0.10.7", - "hmac 0.12.1", -] - [[package]] name = "pcap-file" version = "2.0.0" @@ -5540,7 +5561,7 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash 2.1.1", + "rustc-hash", "rustls", "socket2 0.6.3", "thiserror 2.0.18", @@ -5561,7 +5582,7 @@ dependencies = [ "lru-slab", "rand 0.9.4", "ring", - "rustc-hash 2.1.1", + "rustc-hash", "rustls", "rustls-pki-types", "rustls-platform-verifier", @@ -6036,12 +6057,6 @@ version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - [[package]] name = "rustc-hash" version = "2.1.1" @@ -7304,6 +7319,7 @@ name = "solana-clap-utils" version = "4.2.0-alpha.0" dependencies = [ "assert_matches", + "bip39", "bs58", "chrono", "clap 2.33.3", @@ -7326,7 +7342,6 @@ dependencies = [ "solana-system-interface 3.2.0", "tempfile", "thiserror 2.0.18", - "tiny-bip39", "uriparse", "url 2.5.8", ] @@ -7336,6 +7351,7 @@ name = "solana-clap-v3-utils" version = "4.2.0-alpha.0" dependencies = [ "assert_matches", + "bip39", "chrono", "clap 3.2.23", "five8", @@ -7360,7 +7376,6 @@ dependencies = [ "solana-zk-sdk 5.0.1", "tempfile", "thiserror 2.0.18", - "tiny-bip39", "uriparse", "url 2.5.8", ] @@ -7374,6 +7389,7 @@ dependencies = [ "agave-votor-messages", "assert_matches", "bincode", + "bip39", "bs58", "clap 2.33.3", "console 0.16.3", @@ -7447,7 +7463,6 @@ dependencies = [ "tempfile", "test-case", "thiserror 2.0.18", - "tiny-bip39", "tokio", "wincode", ] @@ -8648,6 +8663,7 @@ name = "solana-keygen" version = "4.2.0-alpha.0" dependencies = [ "agave-votor-messages", + "bip39", "bs58", "clap 3.2.23", "dirs-next", @@ -8666,7 +8682,6 @@ dependencies = [ "solana-signer", "solana-version", "tempfile", - "tiny-bip39", ] [[package]] @@ -10188,7 +10203,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc905b200a95f2ea9146e43f2a7181e3aeb55de6bc12afb36462d00a3c7310de" dependencies = [ "hmac 0.12.1", - "pbkdf2 0.11.0", + "pbkdf2", "sha2 0.10.9", ] @@ -12208,23 +12223,6 @@ dependencies = [ "time-core", ] -[[package]] -name = "tiny-bip39" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a30fd743a02bf35236f6faf99adb03089bb77e91c998dac2c2ad76bb424f668c" -dependencies = [ - "once_cell", - "pbkdf2 0.12.2", - "rand 0.8.6", - "rustc-hash 1.1.0", - "sha2 0.10.9", - "thiserror 1.0.69", - "unicode-normalization", - "wasm-bindgen", - "zeroize", -] - [[package]] name = "tinystr" version = "0.7.6" diff --git a/Cargo.toml b/Cargo.toml index f6b344a216c..0f7c84c7eb5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -202,6 +202,7 @@ aya-ebpf = "0.1.1" base64 = "0.22.1" bencher = "0.1.5" bincode = "1.3.3" +bip39 = { version = "2.2.2", features = ["rand", "all-languages"] } bitflags = { version = "2.11.1" } bitvec = { version = "1.0.1", features = ["serde"] } blake3 = "1.8.5" @@ -546,7 +547,6 @@ tarpc = "0.29.0" tempfile = "3.27.0" test-case = "3.3.1" thiserror = "2.0.18" -tiny-bip39 = "2.0.0" tokio = "1.52.3" tokio-serde = "0.8" tokio-stream = "0.1.18" diff --git a/clap-utils/Cargo.toml b/clap-utils/Cargo.toml index cc8b1ba6815..0c7b32c12b2 100644 --- a/clap-utils/Cargo.toml +++ b/clap-utils/Cargo.toml @@ -19,6 +19,7 @@ name = "solana_clap_utils" agave-unstable-api = [] [dependencies] +bip39 = { workspace = true } bs58 = { workspace = true, features = ["std"] } chrono = { workspace = true, features = ["default"] } clap = { version = "2.33.0", default-features = false, features = ["suggestions"] } @@ -39,7 +40,6 @@ solana-seed-phrase = { workspace = true } solana-signature = { workspace = true } solana-signer = { workspace = true } thiserror = { workspace = true } -tiny-bip39 = { workspace = true } uriparse = { workspace = true } url = { workspace = true } diff --git a/clap-utils/src/keypair.rs b/clap-utils/src/keypair.rs index 835759789ac..d97f0247615 100644 --- a/clap-utils/src/keypair.rs +++ b/clap-utils/src/keypair.rs @@ -15,7 +15,7 @@ use { input_parsers::{STDOUT_OUTFILE_TOKEN, pubkeys_sigs_of}, offline::{SIGN_ONLY_ARG, SIGNER_ARG}, }, - bip39::{Language, Mnemonic, Seed}, + bip39::{Language, Mnemonic}, clap::ArgMatches, rpassword::prompt_password, solana_derivation_path::{DerivationPath, DerivationPathError}, @@ -1065,15 +1065,15 @@ pub fn keypair_from_seed_phrase( let parse_language_fn = || { for language in &[ Language::English, - Language::ChineseSimplified, - Language::ChineseTraditional, + Language::SimplifiedChinese, + Language::TraditionalChinese, Language::Japanese, Language::Spanish, Language::Korean, Language::French, Language::Italian, ] { - if let Ok(mnemonic) = Mnemonic::from_phrase(&sanitized, *language) { + if let Ok(mnemonic) = Mnemonic::parse_in(*language, &sanitized) { return Ok(mnemonic); } } @@ -1081,11 +1081,11 @@ pub fn keypair_from_seed_phrase( }; let mnemonic = parse_language_fn()?; let passphrase = prompt_passphrase(&passphrase_prompt)?; - let seed = Seed::new(&mnemonic, &passphrase); + let seed = mnemonic.to_seed(&passphrase); if legacy { - keypair_from_seed(seed.as_bytes())? + keypair_from_seed(&seed)? } else { - keypair_from_seed_and_derivation_path(seed.as_bytes(), derivation_path)? + keypair_from_seed_and_derivation_path(&seed, derivation_path)? } }; diff --git a/clap-v3-utils/Cargo.toml b/clap-v3-utils/Cargo.toml index 7880dcd34a7..efddfb47a0f 100644 --- a/clap-v3-utils/Cargo.toml +++ b/clap-v3-utils/Cargo.toml @@ -20,6 +20,7 @@ agave-unstable-api = [] elgamal = ["dep:solana-zk-sdk"] [dependencies] +bip39 = { workspace = true } chrono = { workspace = true, features = ["default"] } clap = { version = "3.2.23", default-features = false, features = ["cargo", "std", "suggestions"] } five8 = { workspace = true } @@ -42,7 +43,6 @@ solana-signature = { workspace = true } solana-signer = { workspace = true } solana-zk-sdk = { workspace = true, optional = true } thiserror = { workspace = true } -tiny-bip39 = { workspace = true } uriparse = { workspace = true } url = { workspace = true } diff --git a/clap-v3-utils/src/keygen/mnemonic.rs b/clap-v3-utils/src/keygen/mnemonic.rs index 8cac1972654..35870b5aedf 100644 --- a/clap-v3-utils/src/keygen/mnemonic.rs +++ b/clap-v3-utils/src/keygen/mnemonic.rs @@ -85,8 +85,8 @@ pub fn acquire_language(matches: &ArgMatches) -> Language { let language_name = LANGUAGE_ARG.name; match matches.get_one::(language_name).unwrap().as_str() { "english" => Language::English, - "chinese-simplified" => Language::ChineseSimplified, - "chinese-traditional" => Language::ChineseTraditional, + "chinese-simplified" => Language::SimplifiedChinese, + "chinese-traditional" => Language::TraditionalChinese, "japanese" => Language::Japanese, "spanish" => Language::Spanish, "korean" => Language::Korean, @@ -101,8 +101,8 @@ pub fn try_get_language(matches: &ArgMatches) -> Result, Box(LANGUAGE_ARG.name)? .map(|language| match language.as_str() { "english" => Language::English, - "chinese-simplified" => Language::ChineseSimplified, - "chinese-traditional" => Language::ChineseTraditional, + "chinese-simplified" => Language::SimplifiedChinese, + "chinese-traditional" => Language::TraditionalChinese, "japanese" => Language::Japanese, "spanish" => Language::Spanish, "korean" => Language::Korean, diff --git a/clap-v3-utils/src/keypair.rs b/clap-v3-utils/src/keypair.rs index 0a6972e3b73..27bc4729713 100644 --- a/clap-v3-utils/src/keypair.rs +++ b/clap-v3-utils/src/keypair.rs @@ -17,7 +17,7 @@ use { input_parsers::signer::{SignerSource, SignerSourceKind, try_pubkeys_sigs_of}, offline::{SIGN_ONLY_ARG, SIGNER_ARG}, }, - bip39::{Language, Mnemonic, Seed}, + bip39::{Language, Mnemonic}, clap::ArgMatches, rpassword::prompt_password, solana_derivation_path::DerivationPath, @@ -1186,15 +1186,15 @@ fn encodable_key_from_seed_phrase( let parse_language_fn = || { for language in &[ Language::English, - Language::ChineseSimplified, - Language::ChineseTraditional, + Language::SimplifiedChinese, + Language::TraditionalChinese, Language::Japanese, Language::Spanish, Language::Korean, Language::French, Language::Italian, ] { - if let Ok(mnemonic) = Mnemonic::from_phrase(&sanitized, *language) { + if let Ok(mnemonic) = Mnemonic::parse_in(*language, &sanitized) { return Ok(mnemonic); } } @@ -1202,11 +1202,11 @@ fn encodable_key_from_seed_phrase( }; let mnemonic = parse_language_fn()?; let passphrase = prompt_passphrase(&passphrase_prompt)?; - let seed = Seed::new(&mnemonic, &passphrase); + let seed = mnemonic.to_seed(&passphrase); if legacy { - K::from_seed(seed.as_bytes())? + K::from_seed(&seed)? } else { - K::from_seed_and_derivation_path(seed.as_bytes(), derivation_path)? + K::from_seed_and_derivation_path(&seed, derivation_path)? } }; Ok(key) diff --git a/cli/Cargo.toml b/cli/Cargo.toml index cdacbdc020a..a88ea538e65 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -31,6 +31,7 @@ agave-feature-set = { workspace = true } agave-logger = { workspace = true } agave-votor-messages = { workspace = true } bincode = { workspace = true } +bip39 = { workspace = true } bs58 = { workspace = true } clap = { workspace = true } console = { workspace = true } @@ -93,7 +94,6 @@ solana-version = { workspace = true } solana-vote-program = { workspace = true } spl-memo-interface = { workspace = true } thiserror = { workspace = true } -tiny-bip39 = { workspace = true } tokio = { workspace = true } wincode = { workspace = true } diff --git a/cli/src/program.rs b/cli/src/program.rs index cd2942cb3d0..3445c6cab0c 100644 --- a/cli/src/program.rs +++ b/cli/src/program.rs @@ -12,7 +12,7 @@ use { feature::{CliFeatureStatus, status_from_account}, }, agave_feature_set::{FEATURE_NAMES, FeatureSet}, - bip39::{Language, Mnemonic, MnemonicType, Seed}, + bip39::{Language, Mnemonic}, clap::{App, AppSettings, Arg, ArgMatches, SubCommand}, log::*, solana_account::state_traits::StateMut, @@ -3259,15 +3259,15 @@ async fn send_deploy_messages( fn create_ephemeral_keypair() -> Result<(usize, bip39::Mnemonic, Keypair), Box> { const WORDS: usize = 12; - let mnemonic = Mnemonic::new(MnemonicType::for_word_count(WORDS)?, Language::English); - let seed = Seed::new(&mnemonic, ""); - let new_keypair = keypair_from_seed(seed.as_bytes())?; + let mnemonic = Mnemonic::generate_in(Language::English, WORDS)?; + let seed = mnemonic.to_seed(""); + let new_keypair = keypair_from_seed(&seed)?; Ok((WORDS, mnemonic, new_keypair)) } fn report_ephemeral_mnemonic(words: usize, mnemonic: bip39::Mnemonic, ephemeral_pubkey: &Pubkey) { - let phrase: &str = mnemonic.phrase(); + let phrase = mnemonic.to_string(); let divider = String::from_utf8(vec![b'='; phrase.len()]).unwrap(); eprintln!("{divider}\nRecover the intermediate account's ephemeral keypair file with"); eprintln!("`solana-keygen recover` and the following {words}-word seed phrase:"); diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index f7fbb103142..648c5d2c3e0 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -1144,11 +1144,33 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash 2.1.1", + "rustc-hash", "shlex", "syn 2.0.117", ] +[[package]] +name = "bip39" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" +dependencies = [ + "bitcoin_hashes", + "rand 0.8.6", + "rand_core 0.6.4", + "serde", + "unicode-normalization", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.14.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9901a56e133a1fc86eeb1113e2591f45f4682451ca893bff494d2f88918e3f" +dependencies = [ + "hex-conservative", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -2877,6 +2899,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hex-conservative" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +dependencies = [ + "arrayvec", +] + [[package]] name = "histogram" version = "0.6.9" @@ -4406,16 +4437,6 @@ dependencies = [ "digest 0.10.7", ] -[[package]] -name = "pbkdf2" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" -dependencies = [ - "digest 0.10.7", - "hmac 0.12.1", -] - [[package]] name = "pem" version = "1.1.1" @@ -4798,7 +4819,7 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash 2.1.1", + "rustc-hash", "rustls", "socket2 0.6.3", "thiserror 2.0.18", @@ -4819,7 +4840,7 @@ dependencies = [ "lru-slab", "rand 0.9.4", "ring", - "rustc-hash 2.1.1", + "rustc-hash", "rustls", "rustls-pki-types", "rustls-platform-verifier", @@ -5273,12 +5294,6 @@ version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - [[package]] name = "rustc-hash" version = "2.1.1" @@ -6363,6 +6378,7 @@ dependencies = [ name = "solana-clap-utils" version = "4.2.0-alpha.0" dependencies = [ + "bip39", "bs58", "chrono", "clap", @@ -6383,7 +6399,6 @@ dependencies = [ "solana-signature", "solana-signer", "thiserror 2.0.18", - "tiny-bip39", "uriparse", "url 2.5.8", ] @@ -8555,7 +8570,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc905b200a95f2ea9146e43f2a7181e3aeb55de6bc12afb36462d00a3c7310de" dependencies = [ "hmac 0.12.1", - "pbkdf2 0.11.0", + "pbkdf2", "sha2 0.10.9", ] @@ -10262,23 +10277,6 @@ dependencies = [ "time-core", ] -[[package]] -name = "tiny-bip39" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a30fd743a02bf35236f6faf99adb03089bb77e91c998dac2c2ad76bb424f668c" -dependencies = [ - "once_cell", - "pbkdf2 0.12.2", - "rand 0.8.6", - "rustc-hash 1.1.0", - "sha2 0.10.9", - "thiserror 1.0.69", - "unicode-normalization", - "wasm-bindgen", - "zeroize", -] - [[package]] name = "tinystr" version = "0.8.2" diff --git a/keygen/Cargo.toml b/keygen/Cargo.toml index aa270a8a7cb..817f5b9b377 100644 --- a/keygen/Cargo.toml +++ b/keygen/Cargo.toml @@ -24,6 +24,7 @@ remote-wallet-libusb = ["solana-remote-wallet/linux-static-libusb"] [dependencies] agave-votor-messages = { workspace = true } +bip39 = { workspace = true } bs58 = { workspace = true, features = ["std"] } clap = { version = "3.1.5", default-features = false, features = ["cargo", "std", "suggestions"] } dirs-next = { workspace = true } @@ -41,7 +42,6 @@ solana-remote-wallet = { workspace = true } solana-seed-derivable = { workspace = true } solana-signer = "=3.0.0" solana-version = { workspace = true } -tiny-bip39 = { workspace = true } [dev-dependencies] solana-pubkey = { workspace = true, features = ["rand"] } diff --git a/keygen/src/keygen.rs b/keygen/src/keygen.rs index b2c232fe94b..65377b89494 100644 --- a/keygen/src/keygen.rs +++ b/keygen/src/keygen.rs @@ -1,7 +1,7 @@ #![allow(clippy::arithmetic_side_effects)] use { agave_votor_messages::consensus_message::BLS_KEYPAIR_DERIVE_SEED, - bip39::{Mnemonic, MnemonicType, Seed}, + bip39::Mnemonic, clap::{ Arg, ArgAction, ArgMatches, Command, builder::ValueParser, crate_description, crate_name, value_parser, @@ -570,7 +570,6 @@ fn do_main(matches: &ArgMatches) -> Result<(), Box> { } let word_count = try_get_word_count(matches)?.unwrap(); - let mnemonic_type = MnemonicType::for_word_count(word_count)?; let language = try_get_language(matches)?.unwrap(); let silent = matches.try_contains_id("silent")?; @@ -580,14 +579,15 @@ fn do_main(matches: &ArgMatches) -> Result<(), Box> { let derivation_path = acquire_derivation_path(matches)?; - let mnemonic = Mnemonic::new(mnemonic_type, language); + let mnemonic = Mnemonic::generate_in(language, word_count) + .map_err(|e| format!("Unable to generate mnemonic: {e}"))?; let (passphrase, passphrase_message) = acquire_passphrase_and_message(matches) .map_err(|err| format!("Unable to acquire passphrase: {err}"))?; - let seed = Seed::new(&mnemonic, &passphrase); + let seed = mnemonic.to_seed(&passphrase); let keypair = match derivation_path { - Some(_) => keypair_from_seed_and_derivation_path(seed.as_bytes(), derivation_path), - None => keypair_from_seed(seed.as_bytes()), + Some(_) => keypair_from_seed_and_derivation_path(&seed, derivation_path), + None => keypair_from_seed(&seed), }?; if let Some(outfile) = outfile { @@ -596,7 +596,7 @@ fn do_main(matches: &ArgMatches) -> Result<(), Box> { } if !silent { - let phrase: &str = mnemonic.phrase(); + let phrase = mnemonic.to_string(); let divider = String::from_utf8(vec![b'='; phrase.len()]).unwrap(); println!( "{}\npubkey: {}\n{}\nSave this seed phrase{} to recover your new \ @@ -605,7 +605,7 @@ fn do_main(matches: &ArgMatches) -> Result<(), Box> { keypair.pubkey(), ÷r, passphrase_message, - phrase, + &phrase, ÷r ); } @@ -709,7 +709,6 @@ fn do_main(matches: &ArgMatches) -> Result<(), Box> { let derivation_path = acquire_derivation_path(matches)?; let word_count = try_get_word_count(matches)?.unwrap(); - let mnemonic_type = MnemonicType::for_word_count(word_count)?; let language = try_get_language(matches)?.unwrap(); let (passphrase, passphrase_message) = if use_mnemonic { @@ -777,17 +776,18 @@ fn do_main(matches: &ArgMatches) -> Result<(), Box> { ); } let (keypair, phrase) = if use_mnemonic { - let mnemonic = Mnemonic::new(mnemonic_type, language); - let seed = Seed::new(&mnemonic, &passphrase); + let mnemonic = Mnemonic::generate_in(language, word_count) + .expect("CLI word count is always a valid BIP39 length"); + let seed = mnemonic.to_seed(&passphrase); let keypair = match derivation_path { Some(_) => keypair_from_seed_and_derivation_path( - seed.as_bytes(), + &seed, derivation_path.clone(), ), - None => keypair_from_seed(seed.as_bytes()), + None => keypair_from_seed(&seed), } .unwrap(); - (keypair, mnemonic.phrase().to_string()) + (keypair, mnemonic.to_string()) } else { (Keypair::new(), "".to_string()) }; diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index afdb413e2f8..7e529a28ea9 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -1114,11 +1114,33 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash 2.1.1", + "rustc-hash", "shlex", "syn 2.0.117", ] +[[package]] +name = "bip39" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" +dependencies = [ + "bitcoin_hashes", + "rand 0.8.6", + "rand_core 0.6.4", + "serde", + "unicode-normalization", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.14.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9901a56e133a1fc86eeb1113e2591f45f4682451ca893bff494d2f88918e3f" +dependencies = [ + "hex-conservative", +] + [[package]] name = "bitflags" version = "1.2.1" @@ -2776,6 +2798,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hex-conservative" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +dependencies = [ + "arrayvec", +] + [[package]] name = "histogram" version = "0.6.9" @@ -4347,16 +4378,6 @@ dependencies = [ "digest 0.10.7", ] -[[package]] -name = "pbkdf2" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" -dependencies = [ - "digest 0.10.7", - "hmac 0.12.1", -] - [[package]] name = "pem" version = "1.1.1" @@ -4723,7 +4744,7 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash 2.1.1", + "rustc-hash", "rustls", "socket2 0.6.3", "thiserror 2.0.18", @@ -4744,7 +4765,7 @@ dependencies = [ "lru-slab", "rand 0.9.4", "ring", - "rustc-hash 2.1.1", + "rustc-hash", "rustls", "rustls-pki-types", "rustls-platform-verifier", @@ -5181,12 +5202,6 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783" -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - [[package]] name = "rustc-hash" version = "2.1.1" @@ -6226,6 +6241,7 @@ dependencies = [ name = "solana-clap-utils" version = "4.2.0-alpha.0" dependencies = [ + "bip39", "bs58", "chrono", "clap", @@ -6246,7 +6262,6 @@ dependencies = [ "solana-signature", "solana-signer", "thiserror 2.0.18", - "tiny-bip39", "uriparse", "url 2.5.8", ] @@ -9093,7 +9108,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc905b200a95f2ea9146e43f2a7181e3aeb55de6bc12afb36462d00a3c7310de" dependencies = [ "hmac 0.12.1", - "pbkdf2 0.11.0", + "pbkdf2", "sha2 0.10.9", ] @@ -10793,23 +10808,6 @@ dependencies = [ "time-core", ] -[[package]] -name = "tiny-bip39" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a30fd743a02bf35236f6faf99adb03089bb77e91c998dac2c2ad76bb424f668c" -dependencies = [ - "once_cell", - "pbkdf2 0.12.2", - "rand 0.8.6", - "rustc-hash 1.1.0", - "sha2 0.10.9", - "thiserror 1.0.69", - "unicode-normalization", - "wasm-bindgen", - "zeroize", -] - [[package]] name = "tinystr" version = "0.7.6" From 7edf961117d8993fde3a33318d0b54f2667fdcb9 Mon Sep 17 00:00:00 2001 From: Alex Pyattaev Date: Tue, 9 Jun 2026 21:22:57 +0300 Subject: [PATCH 267/576] turbine: tighter filter of shred admission (#12654) ledger: cap future shred ingest to 1/2 epoch Add also changelog entry to notify users of the change. --- CHANGELOG.md | 1 + ledger/src/shred/filter.rs | 4 ++-- ledger/tests/shred.rs | 16 +++++++++++++--- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d072600f8f..e37e6476767 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ Release channels have their own copy of this changelog: deprecated in v4.0.0; mmap mode has now been removed entirely). The flag is still accepted for backward compatibility, but account storages are always accessed via file I/O. #### Changes +* Turbine shred ingestion now rejects shreds more than half an epoch in the future (previously up to 2 full epochs ahead was accepted). ### CLI #### Breaking #### Changes diff --git a/ledger/src/shred/filter.rs b/ledger/src/shred/filter.rs index 65df4c45821..41d8993a091 100644 --- a/ledger/src/shred/filter.rs +++ b/ledger/src/shred/filter.rs @@ -443,8 +443,8 @@ fn max_shred_slot(root: Slot, slots_per_epoch: Slot) -> Slot { // while being small enough to keep the overhead small on deduper, blockstore, // etc. const MAX_SHRED_DISTANCE_MINIMUM: Slot = 500; - // Allow shreds up to 2 epochs into the future to support catching up to the tip of the cluster. - root.saturating_add(MAX_SHRED_DISTANCE_MINIMUM.max(2 * slots_per_epoch)) + // Allow shreds up to half an epoch into the future to support catching up to the tip of the cluster. + root.saturating_add(MAX_SHRED_DISTANCE_MINIMUM.max(slots_per_epoch / 2)) } /// Returns true if `index` and `fec_set_index` are valid under the assumption that diff --git a/ledger/tests/shred.rs b/ledger/tests/shred.rs index 9f795b06752..010fc2f8815 100644 --- a/ledger/tests/shred.rs +++ b/ledger/tests/shred.rs @@ -5,6 +5,7 @@ use { solana_epoch_schedule::{EpochSchedule, MINIMUM_SLOTS_PER_EPOCH}, solana_hash::Hash, solana_keypair::Keypair, + solana_leader_schedule::SlotLeader, solana_ledger::{ genesis_utils::create_genesis_config, shred::{ @@ -32,7 +33,16 @@ fn new_shred_recovery_context(shreds: &[Shred]) -> ShredRecoveryContext { let shred_slot = shreds.first().map(Shred::slot).unwrap_or_default(); let slots_per_epoch = shred_slot.max(MINIMUM_SLOTS_PER_EPOCH); genesis_config.epoch_schedule = EpochSchedule::custom(slots_per_epoch, slots_per_epoch, false); - let root_bank = Arc::new(Bank::new_for_tests(&genesis_config)); + let (genesis_bank, _bank_forks) = + Bank::new_for_tests(&genesis_config).wrap_with_bank_forks_for_tests(); + // Set the root bank close to the shred slot to avoid the recovery filter + // from discarding the shreds for being too far in the future. + let warp_slot = shred_slot.saturating_sub(10).max(1); + let root_bank = Arc::new(Bank::warp_from_parent( + genesis_bank, + SlotLeader::default(), + warp_slot, + )); let (dummy_retransmit_sender, _) = EvictingSender::new_bounded(0); ShredRecoveryContext::new( ReedSolomonCache::default(), @@ -46,7 +56,7 @@ fn new_shred_recovery_context(shreds: &[Shred]) -> ShredRecoveryContext { #[test_case(true)] fn test_multi_fec_block_coding(is_last_in_slot: bool) { let keypair = Arc::new(Keypair::new()); - let slot = 0x1234_5678_9abc_def0; + let slot = 10000; let shredder = Shredder::new(slot, slot - 5, 0, 0).unwrap(); let num_fec_sets = 100; let num_data_shreds = DATA_SHREDS_PER_FEC_BLOCK * num_fec_sets; @@ -145,7 +155,7 @@ fn test_multi_fec_block_coding(is_last_in_slot: bool) { #[test] fn test_multi_fec_block_different_size_coding() { - let slot = 0x1234_5678_9abc_def0; + let slot = 10000; let parent_slot = slot - 5; let keypair = Arc::new(Keypair::new()); let (fec_data, fec_coding, num_shreds_per_iter) = From a35f019a6513168e413b87d752464a29d81e4b55 Mon Sep 17 00:00:00 2001 From: alex-lind1 Date: Tue, 9 Jun 2026 15:27:08 -0400 Subject: [PATCH 268/576] Add Observability about VAT in Replay Stage Loop (#12918) VAT health check --- core/src/replay_stage.rs | 91 +++++++++++++++++++++++++++++++++++++++- runtime/src/bank.rs | 2 +- 2 files changed, 91 insertions(+), 2 deletions(-) diff --git a/core/src/replay_stage.rs b/core/src/replay_stage.rs index ae8fbcae67c..3c55938e0bd 100644 --- a/core/src/replay_stage.rs +++ b/core/src/replay_stage.rs @@ -81,7 +81,7 @@ use { slot_status_notifier::SlotStatusNotifier, }, solana_runtime::{ - bank::{Bank, NewBankOptions, bank_hash_details}, + bank::{Bank, MAX_ALPENGLOW_VOTE_ACCOUNTS, NewBankOptions, bank_hash_details}, bank_forks::BankForks, bank_forks_controller::{BankForksCommand, BankForksCommandReceiver, SetRootCommand}, block_component_processor::BlockComponentProcessorError, @@ -109,6 +109,7 @@ use { thread::{self, Builder, JoinHandle}, time::{Duration, Instant}, }, + thiserror::Error, }; mod dead_slots; @@ -138,6 +139,18 @@ const MAX_REPAIR_RETRY_LOOP_ATTEMPTS: usize = 10; // Give at least 4 leaders the chance to pack our vote const REFRESH_VOTE_BLOCKHEIGHT: usize = 16; +const VAT_STATUS_CHECK_INTERVAL_SECS: u64 = 30; + +#[derive(Error, Debug)] +enum VATHealthError { + #[error("vote account not found")] + VoteAccountNotFound, + #[error("missing BLS pubkey")] + NoBLSPubkey, + #[error("insufficient lamports in vote account: {0} < {1}")] + InsufficientFundsInVoteAccount(u64, u64), +} + #[derive(PartialEq, Eq, Debug)] pub enum HeaviestForkFailures { LockedOut(u64), @@ -856,6 +869,7 @@ impl ReplayStage { last_print_time: Instant::now(), }; let mut last_genesis_vote_refresh_time = Instant::now(); + let mut last_vat_status_check = Instant::now(); let mut tbft_structs = TowerBFTStructures { heaviest_subtree_fork_choice, duplicate_slots_tracker, @@ -1007,6 +1021,14 @@ impl ReplayStage { let did_complete_bank = !new_frozen_slots.is_empty(); replay_active_banks_time.stop(); + // VAT health check + Self::maybe_report_vat_health( + &mut last_vat_status_check, + &authorized_voter_keypairs, + &bank_forks, + &vote_account, + ); + // Check if we've completed the migration conditions if migration_status.is_ready_to_enable() { Self::enable_alpenglow( @@ -3126,6 +3148,73 @@ impl ReplayStage { ); } + fn maybe_report_vat_health( + last_vat_status_check: &mut Instant, + authorized_voter_keypairs: &Arc>>>, + bank_forks: &Arc>, + vote_account: &Pubkey, + ) { + if last_vat_status_check.elapsed().as_secs() < VAT_STATUS_CHECK_INTERVAL_SECS { + return; + } + *last_vat_status_check = Instant::now(); + + let bank = bank_forks.read().unwrap().root_bank(); + if !bank.feature_set.snapshot().validator_admission_ticket { + return; + } + + let is_voting_validator = !authorized_voter_keypairs.read().unwrap().is_empty(); + if !is_voting_validator { + return; + } + + let epoch = bank.epoch(); + if let Err(vat_failure_reason) = Self::check_vat_health(&bank, vote_account) { + warn!( + "VAT Health Check: Currently you will fail the VAT check at the start of epoch {} \ + meaning that you will be unable to vote or produce blocks in epoch {}. Reason: {}", + epoch.saturating_add(1), + epoch.saturating_add(2), + vat_failure_reason, + ); + } else { + info!( + "VAT Health Check: Passing local VAT checks. Assuming you are in the top {} of \ + stake, you will be included in voting and block building in epoch {}", + MAX_ALPENGLOW_VOTE_ACCOUNTS, + epoch.saturating_add(2), + ); + } + } + + fn check_vat_health(bank: &Bank, vote_account_pubkey: &Pubkey) -> Result<(), VATHealthError> { + let vote_accounts = bank.vote_accounts(); + + let Some((_, vote_account)) = vote_accounts.get(vote_account_pubkey) else { + return Err(VATHealthError::VoteAccountNotFound); + }; + + if vote_account + .vote_state_view() + .bls_pubkey_compressed() + .is_none() + { + return Err(VATHealthError::NoBLSPubkey); + } + + let my_balance = vote_account.lamports(); + let minimum_vote_account_balance_for_vat = bank.minimum_vote_account_balance_for_vat(); + if vote_account.lamports() < minimum_vote_account_balance_for_vat { + return Err(VATHealthError::InsufficientFundsInVoteAccount( + my_balance, + minimum_vote_account_balance_for_vat, + )); + } + + Ok(()) + } + fn generate_vote_tx( node_keypair: &Keypair, bank: &Bank, diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 6da14f51981..382b0b09cd7 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -6531,7 +6531,7 @@ impl Bank { /// Minimum balance a vote account must hold to survive SIMD-0357 filtering /// under the current feature set. When `alpenglow` is active the threshold /// also includes one epoch's worth of VAT burn. - fn minimum_vote_account_balance_for_vat(&self) -> u64 { + pub fn minimum_vote_account_balance_for_vat(&self) -> u64 { let vote_account_rent_exempt_minimum = self .rent_collector .rent From 3b39e7198b789099293eb1c7b4bdb7cb0c86d275 Mon Sep 17 00:00:00 2001 From: Akhilesh Singhania Date: Tue, 9 Jun 2026 21:59:50 +0200 Subject: [PATCH 269/576] Consensus pool service cleanups (#13090) --- votor/src/consensus_pool_service.rs | 275 +++++++++++----------------- 1 file changed, 112 insertions(+), 163 deletions(-) diff --git a/votor/src/consensus_pool_service.rs b/votor/src/consensus_pool_service.rs index 0bf68a14058..2068a763c30 100644 --- a/votor/src/consensus_pool_service.rs +++ b/votor/src/consensus_pool_service.rs @@ -1,5 +1,9 @@ -//! Service in charge of ingesting new messages into the certificate pool -//! and notifying votor of new events that occur +//! Runs the ingest side of the votor consensus pool. +//! +//! - verified vote and certificate batches from sigverify are sent to the pool; +//! - new `VotorEvent`s are forwarded to the voting/event loop; +//! - newly constructed certificates and standstill refreshes are queued for broadcast; +//! - pending intrawindow `SafeToNotar` blocks are repaired and rechecked; mod stats; @@ -41,7 +45,7 @@ use { }, }; -/// Inputs for the certificate pool thread +/// Inputs for the consensus pool and consensus pool service pub(crate) struct ConsensusPoolContext { pub(crate) exit: Arc, pub(crate) migration_status: Arc, @@ -65,32 +69,31 @@ pub(crate) struct ConsensusPoolContext { } pub(crate) struct ConsensusPoolService { - t_ingest: JoinHandle<()>, + t_consensus_pool_service: JoinHandle<()>, } impl ConsensusPoolService { pub(crate) fn new(ctx: ConsensusPoolContext) -> Self { - let t_ingest = Builder::new() - .name("solCertPoolIngest".to_string()) + let t_consensus_pool_service = Builder::new() + .name("solConPoolService".to_string()) .spawn(move || { - Self::consensus_pool_ingest_loop(ctx); + Self::main_loop(ctx); info!("consensus pool service exited."); }) .unwrap(); - Self { t_ingest } + Self { + t_consensus_pool_service, + } } fn maybe_update_root_and_send_new_certificates( + ctx: &ConsensusPoolContext, consensus_pool: &mut ConsensusPool, - sharable_banks: &SharableBanks, - my_pubkey: &Pubkey, - bls_sender: &Sender, new_finalized_slot: Option, new_certificates_to_send: Vec>, standstill_timer: &mut Instant, stats: &mut ConsensusPoolServiceStats, - highest_finalized: &RwLock>, ) -> Result<(), ()> { // If we have a new finalized slot, update the root and send new certificates if new_finalized_slot.is_some() { @@ -98,25 +101,18 @@ impl ConsensusPoolService { *standstill_timer = Instant::now(); stats.new_finalized_slot += 1; - *highest_finalized.write().unwrap() = consensus_pool.get_highest_finalization_certs(); + *ctx.highest_finalized.write().unwrap() = + consensus_pool.get_highest_finalization_certs(); } - let bank = sharable_banks.root(); + let bank = ctx.sharable_banks.root(); consensus_pool.maybe_prune(bank.slot()); stats.prune_old_state_called += 1; // Send new certificates to peers - Self::send_certificates( - sharable_banks, - my_pubkey, - bls_sender, - new_certificates_to_send, - stats, - ) + Self::send_certificates(ctx, new_certificates_to_send, stats) } fn send_certificates( - sharable_banks: &SharableBanks, - my_pubkey: &Pubkey, - bls_sender: &Sender, + ctx: &ConsensusPoolContext, certificates_to_send: Vec>, stats: &mut ConsensusPoolServiceStats, ) -> Result<(), ()> { @@ -127,29 +123,31 @@ impl ConsensusPoolService { // If we are not a staked identity (hot spare / RPC / new validator / failed VAT) // we should not send out the certificate. A2A quic only accepts connections // from staked identities - if !Self::is_current_identity_staked(sharable_banks, my_pubkey) { + if !Self::is_current_identity_staked(ctx) { stats.certificates_skipped_unstaked += num_certs; return Ok(()); } - Self::enqueue_certificates(my_pubkey, bls_sender, certificates_to_send, stats) + Self::enqueue_certificates(ctx, certificates_to_send, stats) } - fn is_current_identity_staked(sharable_banks: &SharableBanks, my_pubkey: &Pubkey) -> bool { - sharable_banks + fn is_current_identity_staked(ctx: &ConsensusPoolContext) -> bool { + ctx.sharable_banks .root() .current_epoch_staked_nodes() - .get(my_pubkey) + .get(&ctx.cluster_info.id()) .is_some_and(|stake| *stake > 0) } fn enqueue_certificates( - my_pubkey: &Pubkey, - bls_sender: &Sender, + ctx: &ConsensusPoolContext, certificates: Vec>, stats: &mut ConsensusPoolServiceStats, ) -> Result<(), ()> { let num_certs = certificates.len(); - match bls_sender.try_send(BLSOp::PushCertificates { certificates }) { + match ctx + .bls_sender + .try_send(BLSOp::PushCertificates { certificates }) + { Ok(()) => { stats.certificates_sent += num_certs; Ok(()) @@ -157,7 +155,10 @@ impl ConsensusPoolService { Err(TrySendError::Disconnected(_)) => Err(()), Err(TrySendError::Full(_)) => { stats.certificates_dropped += num_certs; - warn!("{my_pubkey}: channel is full, dropping {num_certs} certs"); + warn!( + "{}: channel is full, dropping {num_certs} certs", + ctx.cluster_info.id() + ); Ok(()) } } @@ -221,15 +222,12 @@ impl ConsensusPoolService { } }; Self::maybe_update_root_and_send_new_certificates( + ctx, consensus_pool, - &ctx.sharable_banks, - &ctx.cluster_info.id(), - &ctx.bls_sender, new_finalized_slot, new_certificates_to_send, standstill_timer, stats, - &ctx.highest_finalized, ) } @@ -275,8 +273,8 @@ impl ConsensusPoolService { } } - // Main loop for the certificate pool service, it only exits when any channel is disconnected - fn consensus_pool_ingest_loop(mut ctx: ConsensusPoolContext) { + // Main loop for the consensus pool service + fn main_loop(mut ctx: ConsensusPoolContext) { let mut events = vec![]; let root_bank = ctx.sharable_banks.root(); @@ -309,7 +307,7 @@ impl ConsensusPoolService { // Track pending safe-to-notar blocks for intrawindow slots. let mut pending_safe_to_notar = HashSet::new(); - // Ingest votes into certificate pool and notify voting loop of new events + // Ingest votes into consensus pool and notify voting loop of new events while !ctx.exit.load(Ordering::Relaxed) { // Kick off parent ready event, this either happens: // - When we first migrate to alpenglow from TowerBFT - kick off with genesis block @@ -350,9 +348,7 @@ impl ConsensusPoolService { stats.standstill = true; standstill_timer = Instant::now(); match Self::send_certificates( - &ctx.sharable_banks, - &ctx.cluster_info.id(), - &ctx.bls_sender, + &ctx, consensus_pool.get_certs_for_standstill(), &mut stats, ) { @@ -431,7 +427,7 @@ impl ConsensusPoolService { } } - /// Adds a vote to the certificate pool. + /// Adds a vote to the consensus pool. /// /// If a new finalization slot was recognized, returns the slot fn add_message( @@ -617,7 +613,7 @@ impl ConsensusPoolService { } pub(crate) fn join(self) -> thread::Result<()> { - self.t_ingest.join() + self.t_consensus_pool_service.join() } } @@ -631,37 +627,31 @@ mod tests { consensus_message::{BLS_KEYPAIR_DERIVE_SEED, VoteMessage}, vote::Vote, }, - crossbeam_channel::bounded, + crossbeam_channel::{bounded, unbounded}, solana_bls_signatures::{ BLS_SIGNATURE_AFFINE_SIZE, keypair::Keypair as BLSKeypair, signature::Signature as BLSSignature, }, - solana_gossip::cluster_info::ClusterInfo, solana_hash::Hash, + solana_keypair::Keypair, solana_ledger::get_tmp_ledger_path_auto_delete, solana_runtime::{ - bank_forks::{BankForks, SharableBanks}, + bank_forks::BankForks, genesis_utils::{ ValidatorVoteKeypairs, create_genesis_config_with_alpenglow_vote_accounts, }, }, - solana_signer::Signer, std::sync::Arc, }; struct TestContext { consensus_pool: ConsensusPool, - bls_sender: Sender, + ctx: ConsensusPoolContext, bls_receiver: Receiver, + _consensus_message_sender: Sender, + event_receiver: Receiver, + _repair_event_receiver: Receiver, validator_keypairs: Vec, - leader_schedule_cache: Arc, - sharable_banks: SharableBanks, - my_pubkey: Pubkey, - my_vote_pubkey: Pubkey, - blockstore: Arc, - exit: Arc, - cluster_info: Arc, - highest_finalized: Arc>>, } impl Default for TestContext { @@ -682,7 +672,6 @@ mod tests { stake, ); let my_keypair = validator_keypairs[0].node_keypair.insecure_clone(); - let my_pubkey = my_keypair.pubkey(); let bank0 = Bank::new_for_tests(&genesis.genesis_config); let bank_forks = BankForks::new_rw_arc(bank0); @@ -699,28 +688,45 @@ mod tests { block_id: root_bank.block_id().unwrap_or_default(), }; let initial_parent_ready = (root_bank.slot().checked_add(1).unwrap(), root_block); + let generated_cert_types = Arc::new(GeneratedCertTypes::default()); + let migration_status = Arc::new(MigrationStatus::post_migration_status()); let consensus_pool = ConsensusPool::new( cluster_info.clone(), &root_bank, - Arc::new(GeneratedCertTypes::default()), - Arc::new(MigrationStatus::post_migration_status()), + generated_cert_types.clone(), + migration_status.clone(), initial_parent_ready, ); let my_vote_pubkey = Pubkey::new_unique(); + let (consensus_message_sender, consensus_message_receiver) = unbounded(); + let (event_sender, event_receiver) = unbounded(); + let (repair_event_sender, repair_event_receiver) = unbounded(); + + let ctx = ConsensusPoolContext { + exit: Arc::new(AtomicBool::new(false)), + migration_status, + generated_cert_types, + cluster_info, + my_vote_pubkey, + blockstore, + sharable_banks, + leader_schedule_cache, + vote_history_highest_parent_ready: None, + consensus_message_receiver, + bls_sender, + event_sender, + repair_event_sender, + highest_finalized: Arc::new(RwLock::new(None)), + }; TestContext { consensus_pool, - bls_sender, + ctx, bls_receiver, + _consensus_message_sender: consensus_message_sender, + event_receiver, + _repair_event_receiver: repair_event_receiver, validator_keypairs, - leader_schedule_cache, - sharable_banks, - my_pubkey, - my_vote_pubkey, - blockstore, - exit: Arc::new(AtomicBool::new(false)), - cluster_info, - highest_finalized: Arc::new(RwLock::new(None)), } } } @@ -745,7 +751,7 @@ mod tests { }); let mut events = vec![]; - let root_bank = ctx.sharable_banks.root(); + let root_bank = ctx.ctx.sharable_banks.root(); // Process votes from validators 0-7 for my_rank in 0..8 { @@ -762,8 +768,8 @@ mod tests { let mut stats = ConsensusPoolServiceStats::new(); let result = ConsensusPoolService::add_message( &root_bank, - &ctx.my_pubkey, - &ctx.my_vote_pubkey, + &ctx.ctx.cluster_info.id(), + &ctx.ctx.my_vote_pubkey, message, &mut ctx.consensus_pool, &mut events, @@ -777,15 +783,12 @@ mod tests { // Send certificates if any were produced if !new_certificates_to_send.is_empty() || new_finalized_slot.is_some() { ConsensusPoolService::maybe_update_root_and_send_new_certificates( + &ctx.ctx, &mut ctx.consensus_pool, - &ctx.sharable_banks, - &ctx.my_pubkey, - &ctx.bls_sender, new_finalized_slot, new_certificates_to_send, &mut standstill_timer, &mut stats, - &ctx.highest_finalized, ) .unwrap(); } @@ -829,8 +832,8 @@ mod tests { let mut stats = ConsensusPoolServiceStats::new(); let result = ConsensusPoolService::add_message( &root_bank, - &ctx.my_pubkey, - &ctx.my_vote_pubkey, + &ctx.ctx.cluster_info.id(), + &ctx.ctx.my_vote_pubkey, ConsensusMessage::Certificate(skip_certificate), &mut ctx.consensus_pool, &mut events, @@ -842,15 +845,12 @@ mod tests { let mut standstill_timer = Instant::now(); ConsensusPoolService::maybe_update_root_and_send_new_certificates( + &ctx.ctx, &mut ctx.consensus_pool, - &ctx.sharable_banks, - &ctx.my_pubkey, - &ctx.bls_sender, new_finalized_slot, new_certificates_to_send, &mut standstill_timer, &mut stats, - &ctx.highest_finalized, ) .unwrap(); @@ -871,15 +871,21 @@ mod tests { #[test] fn test_send_produce_block_event() { let mut ctx = TestContext::default(); - let (repair_event_sender, _repair_event_receiver) = bounded(1024); // Find when is the next leader slot for me (validator 0) let next_leader_slot = ctx + .ctx .leader_schedule_cache - .next_leader_slot(&ctx.my_pubkey, 0, &ctx.sharable_banks.root(), None, 1000000) + .next_leader_slot( + &ctx.ctx.cluster_info.id(), + 0, + &ctx.ctx.sharable_banks.root(), + None, + 1000000, + ) .expect("Should find a leader slot"); - let root_bank = ctx.sharable_banks.root(); + let root_bank = ctx.ctx.sharable_banks.root(); let mut events = vec![]; // Send skip certificates for all slots up to the next leader slot @@ -893,8 +899,8 @@ mod tests { let result = ConsensusPoolService::add_message( &root_bank, - &ctx.my_pubkey, - &ctx.my_vote_pubkey, + &ctx.ctx.cluster_info.id(), + &ctx.ctx.my_vote_pubkey, ConsensusMessage::Certificate(skip_certificate), &mut ctx.consensus_pool, &mut events, @@ -905,22 +911,6 @@ mod tests { // Now call add_produce_block_event to generate ProduceWindow event let mut highest_parent_ready = root_bank.slot(); - let mut pool_ctx = ConsensusPoolContext { - exit: ctx.exit.clone(), - migration_status: Arc::new(MigrationStatus::post_migration_status()), - generated_cert_types: Arc::new(GeneratedCertTypes::default()), - cluster_info: ctx.cluster_info.clone(), - my_vote_pubkey: ctx.my_vote_pubkey, - blockstore: ctx.blockstore.clone(), - sharable_banks: ctx.sharable_banks.clone(), - leader_schedule_cache: ctx.leader_schedule_cache.clone(), - vote_history_highest_parent_ready: None, - consensus_message_receiver: bounded(1024).1, - bls_sender: ctx.bls_sender.clone(), - event_sender: bounded(1024).0, - highest_finalized: ctx.highest_finalized.clone(), - repair_event_sender, - }; // Add a ParentReady event for the slot before our leader slot events.push(VotorEvent::ParentReady { @@ -934,7 +924,7 @@ mod tests { ConsensusPoolService::add_produce_block_event( &mut highest_parent_ready, &ctx.consensus_pool, - &mut pool_ctx, + &mut ctx.ctx, &mut events, &mut stats, ); @@ -955,15 +945,13 @@ mod tests { #[test] fn test_can_produce_window_immediately_on_restart() { - let ctx = TestContext::default(); - let (repair_event_sender, _repair_event_receiver) = bounded(1024); - let (event_sender, event_receiver) = bounded(1024); - let (consensus_message_sender, consensus_message_receiver) = bounded(1024); + let mut ctx = TestContext::default(); - let root_bank = ctx.sharable_banks.root(); + let root_bank = ctx.ctx.sharable_banks.root(); let next_leader_slot = ctx + .ctx .leader_schedule_cache - .next_leader_slot(&ctx.my_pubkey, 0, &root_bank, None, 1000000) + .next_leader_slot(&ctx.ctx.cluster_info.id(), 0, &root_bank, None, 1000000) .expect("Should find a leader slot") .0; let restored_parent_ready = ( @@ -973,34 +961,17 @@ mod tests { block_id: Hash::new_unique(), }, ); - let exit = ctx.exit.clone(); - - let pool_ctx = ConsensusPoolContext { - exit: exit.clone(), - migration_status: Arc::new(MigrationStatus::post_migration_status()), - generated_cert_types: Arc::new(GeneratedCertTypes::default()), - cluster_info: ctx.cluster_info.clone(), - my_vote_pubkey: ctx.my_vote_pubkey, - blockstore: ctx.blockstore.clone(), - sharable_banks: ctx.sharable_banks.clone(), - leader_schedule_cache: ctx.leader_schedule_cache.clone(), - vote_history_highest_parent_ready: Some(restored_parent_ready), - consensus_message_receiver, - bls_sender: ctx.bls_sender.clone(), - event_sender, - highest_finalized: ctx.highest_finalized.clone(), - repair_event_sender, - }; + ctx.ctx.vote_history_highest_parent_ready = Some(restored_parent_ready); + let exit = ctx.ctx.exit.clone(); - let handle = - thread::spawn(move || ConsensusPoolService::consensus_pool_ingest_loop(pool_ctx)); + let handle = thread::spawn(move || ConsensusPoolService::main_loop(ctx.ctx)); let deadline = Instant::now() + Duration::from_secs(5); let mut saw_parent_ready = false; let mut saw_produce_window = false; while Instant::now() < deadline && !saw_produce_window { let timeout = deadline.saturating_duration_since(Instant::now()); - let Ok(event) = event_receiver.recv_timeout(timeout) else { + let Ok(event) = ctx.event_receiver.recv_timeout(timeout) else { break; }; @@ -1016,7 +987,6 @@ mod tests { } exit.store(true, Ordering::Relaxed); - drop(consensus_message_sender); handle.join().unwrap(); assert!( @@ -1087,13 +1057,8 @@ mod tests { ]; let mut stats = ConsensusPoolServiceStats::new(); - let result = ConsensusPoolService::send_certificates( - &ctx.sharable_banks, - &ctx.my_pubkey, - &ctx.bls_sender, - certificates.clone(), - &mut stats, - ); + let result = + ConsensusPoolService::send_certificates(&ctx.ctx, certificates.clone(), &mut stats); assert!(result.is_ok()); assert_eq!(stats.certificates_sent.0, 2); @@ -1115,7 +1080,7 @@ mod tests { #[test] fn test_send_certificates_skips_unstaked_identity() { - let ctx = TestContext::default(); + let mut ctx = TestContext::default(); let certificates = vec![ Arc::new(Certificate { cert_type: CertificateType::Skip(1), @@ -1128,18 +1093,11 @@ mod tests { bitmap: vec![], }), ]; - let unstaked_identity = Pubkey::new_unique(); + let unstaked_identity = Keypair::new(); + let cluster_info = get_cluster_info(unstaked_identity); + ctx.ctx.cluster_info = cluster_info; let mut stats = ConsensusPoolServiceStats::new(); - - let result = ConsensusPoolService::send_certificates( - &ctx.sharable_banks, - &unstaked_identity, - &ctx.bls_sender, - certificates, - &mut stats, - ); - - assert!(result.is_ok()); + ConsensusPoolService::send_certificates(&ctx.ctx, certificates, &mut stats).unwrap(); assert_eq!(stats.certificates_sent.0, 0); assert_eq!(stats.certificates_skipped_unstaked.0, 2); assert!(ctx.bls_receiver.try_recv().is_err()); @@ -1157,13 +1115,7 @@ mod tests { })]; let mut stats = ConsensusPoolServiceStats::new(); - let result = ConsensusPoolService::send_certificates( - &ctx.sharable_banks, - &ctx.my_pubkey, - &ctx.bls_sender, - certificates, - &mut stats, - ); + let result = ConsensusPoolService::send_certificates(&ctx.ctx, certificates, &mut stats); assert_eq!(result.unwrap_err(), ()); } @@ -1182,15 +1134,12 @@ mod tests { // Test with new_finalized_slot = Some let result = ConsensusPoolService::maybe_update_root_and_send_new_certificates( + &ctx.ctx, &mut ctx.consensus_pool, - &ctx.sharable_banks, - &ctx.my_pubkey, - &ctx.bls_sender, Some(5), // new finalized slot certificates, &mut standstill_timer, &mut stats, - &ctx.highest_finalized, ); assert!(result.is_ok()); From efe74704c80167ce1d9a63214d95d8c0fa8a842c Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Tue, 9 Jun 2026 17:37:50 -0400 Subject: [PATCH 270/576] votor: remove outdated TODO (#13099) --- votor/src/event_handler.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/votor/src/event_handler.rs b/votor/src/event_handler.rs index 08777d6ea1c..3435d782a5d 100644 --- a/votor/src/event_handler.rs +++ b/votor/src/event_handler.rs @@ -390,7 +390,6 @@ impl EventHandler { } // We have observed the safe to notar condition, and can send a notar fallback vote - // TODO: update cert pool to check parent block id for intra window slots VotorEvent::SafeToNotar(block) => { info!("{my_pubkey}: SafeToNotar {block:?}"); Self::try_skip_window(my_pubkey, block.slot, vctx, &mut votes)?; From 97f86d684d5b08534b2d38928a27b92a38c2bc6e Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Tue, 9 Jun 2026 19:15:07 -0400 Subject: [PATCH 271/576] votor: initialize certificate pool with our own votes (#13094) * votor: initialize certificate pool with our own votes * pr feedback --- votor/src/event_handler.rs | 155 +++++++++++++++++++++++++++++++++---- votor/src/voting_utils.rs | 50 ++++++++---- 2 files changed, 179 insertions(+), 26 deletions(-) diff --git a/votor/src/event_handler.rs b/votor/src/event_handler.rs index 3435d782a5d..2f87b338a3f 100644 --- a/votor/src/event_handler.rs +++ b/votor/src/event_handler.rs @@ -15,8 +15,8 @@ use { vote_history::{VoteHistory, VoteHistoryError}, voting_service::BLSOp, voting_utils::{ - VoteError, VotingContext, generate_refresh_vote_message, - insert_vote_and_create_bls_message, + VoteError, VotingContext, create_and_send_own_vote_message, + generate_refresh_vote_message, insert_vote_and_create_bls_message, }, votor::SharedContext, }, @@ -155,6 +155,7 @@ impl EventHandler { ); return Err(EventLoopError::SetIdentityError(e)); } + Self::send_vote_history_to_consensus_pool(&local_context.my_pubkey, &mut vctx)?; while !exit.load(Ordering::Relaxed) { let mut receive_event_time = Measure::start("receive_event"); @@ -518,6 +519,7 @@ impl EventHandler { ); return Err(EventLoopError::SetIdentityError(e)); } + Self::send_vote_history_to_consensus_pool(my_pubkey, vctx)?; } } Ok(votes) @@ -614,6 +616,23 @@ impl EventHandler { Ok(()) } + fn send_vote_history_to_consensus_pool( + my_pubkey: &Pubkey, + vctx: &mut VotingContext, + ) -> Result<(), VoteError> { + let root = vctx + .vote_history + .root() + .max(vctx.sharable_banks.root().slot()); + let votes = vctx.vote_history.votes_cast_since(root.saturating_sub(1)); + for vote in votes { + info!("{my_pubkey}: Initializing consensus pool with restored vote {vote:?}"); + create_and_send_own_vote_message(vote, vctx, /* respect_wait_to_vote */ false)?; + } + + Ok(()) + } + fn get_block_parent_block(bank: &Bank) -> (Block, Block) { let slot = bank.slot(); let block = Block { @@ -972,6 +991,7 @@ mod tests { sync::{Arc, RwLock}, time::Instant, }, + tempfile::TempDir, }; struct EventHandlerTestContext { @@ -1352,14 +1372,7 @@ mod tests { fn check_for_votes(&mut self, expected_votes: &[Vote]) { for v in expected_votes { - let expected_vote_serialized = wincode::serialize(v).unwrap(); - let signature: BLSSignature = - self.my_bls_keypair.sign(&expected_vote_serialized).into(); - let expected_message = VoteMessage { - vote: *v, - rank: 0, - signature, - }; + let expected_message = self.expected_vote_message(v); let mut found = false; self.bls_ops.retain_mut(|bls_op| match bls_op { BLSOp::PushVote { vote, .. } => { @@ -1379,15 +1392,19 @@ mod tests { } } - fn check_for_vote(&mut self, expected_vote: &Vote) { + fn expected_vote_message(&self, expected_vote: &Vote) -> VoteMessage { let expected_vote_serialized = wincode::serialize(expected_vote).unwrap(); let signature: BLSSignature = self.my_bls_keypair.sign(&expected_vote_serialized).into(); - let expected_message = VoteMessage { + VoteMessage { vote: *expected_vote, rank: 0, signature, - }; + } + } + + fn check_for_vote(&mut self, expected_vote: &Vote) { + let expected_message = self.expected_vote_message(expected_vote); let mut found = false; self.bls_ops.retain_mut(|bls_op| match bls_op { BLSOp::PushVote { vote, .. } => { @@ -1409,6 +1426,40 @@ mod tests { assert_eq!(own_vote, SigVerifiedBatch::Votes(vec![expected_message])); } + fn check_for_own_vote(&self, expected_vote: &Vote) { + let expected_message = self.expected_vote_message(expected_vote); + let own_vote = self.own_vote_receiver.try_recv().unwrap(); + assert_eq!(own_vote, SigVerifiedBatch::Votes(vec![expected_message])); + } + + fn check_for_own_votes(&self, expected_votes: &[Vote]) { + let mut received_messages = Vec::with_capacity(expected_votes.len()); + for _ in expected_votes { + let SigVerifiedBatch::Votes(votes) = self.own_vote_receiver.try_recv().unwrap() + else { + panic!("expected own vote batch"); + }; + received_messages.extend(votes); + } + + for expected_vote in expected_votes { + let expected_message = self.expected_vote_message(expected_vote); + let index = received_messages + .iter() + .position(|message| *message == expected_message) + .unwrap_or_else(|| panic!("missing own vote {expected_vote:?}")); + received_messages.remove(index); + } + assert!(received_messages.is_empty()); + } + + fn check_no_own_vote(&self) { + assert_eq!( + self.own_vote_receiver.try_recv().err(), + Some(TryRecvError::Empty) + ); + } + fn check_for_commitment(&mut self, expected_type: CommitmentType, expected_slot: Slot) { let commitment = self.commitment_receiver.try_recv().unwrap(); assert_eq!(commitment.commitment_type, expected_type); @@ -2075,6 +2126,40 @@ mod tests { test_context.check_for_votes(&[Vote::new_skip_vote(2), Vote::new_skip_vote(3)]); } + #[test] + fn test_startup_replays_vote_history_to_consensus_pool() { + let mut test_context = setup(); + let notarize_vote = Vote::new_notarization_vote(Block { + slot: 1, + block_id: Hash::new_unique(), + }); + let skip_vote = Vote::new_skip_vote(2); + let fallback_vote = Vote::new_skip_fallback_vote(2); + test_context + .voting_context + .vote_history + .add_vote(notarize_vote); + test_context.voting_context.vote_history.add_vote(skip_vote); + test_context + .voting_context + .vote_history + .add_vote(fallback_vote); + + EventHandler::send_vote_history_to_consensus_pool( + &test_context.local_context.my_pubkey, + &mut test_context.voting_context, + ) + .unwrap(); + + test_context.check_for_own_votes(&[notarize_vote, skip_vote, fallback_vote]); + test_context.check_no_own_vote(); + assert!(test_context.bls_ops.is_empty()); + assert_eq!( + test_context.bls_receiver.try_recv().err(), + Some(TryRecvError::Empty) + ); + } + #[test] fn test_received_set_identity() { let mut test_context = setup(); @@ -2135,6 +2220,50 @@ mod tests { } } + #[test] + fn test_set_identity_replays_restored_vote_history_to_consensus_pool() { + let mut test_context = setup(); + let old_identity = test_context.cluster_info.keypair().insecure_clone(); + let new_identity = Keypair::new(); + let temp_dir = TempDir::new().unwrap(); + let vote_history_storage = + Arc::new(FileVoteHistoryStorage::new(temp_dir.path().to_path_buf())); + test_context.shared_context.vote_history_storage = vote_history_storage.clone(); + + let new_vote_history = VoteHistory::new(new_identity.pubkey(), 0); + let saved_vote_history = SavedVoteHistory::new(&new_vote_history, &new_identity).unwrap(); + vote_history_storage + .store(&SavedVoteHistoryVersions::from(saved_vote_history)) + .unwrap(); + + let restored_vote = Vote::new_notarization_vote(Block { + slot: 1, + block_id: Hash::new_unique(), + }); + let mut old_vote_history = VoteHistory::new(old_identity.pubkey(), 0); + old_vote_history.add_vote(restored_vote); + let saved_vote_history = SavedVoteHistory::new(&old_vote_history, &old_identity).unwrap(); + vote_history_storage + .store(&SavedVoteHistoryVersions::from(saved_vote_history)) + .unwrap(); + + test_context + .cluster_info + .set_keypair(Arc::new(new_identity.insecure_clone())); + test_context.send_set_identity_event(); + test_context.check_no_own_vote(); + + test_context + .cluster_info + .set_keypair(Arc::new(old_identity.insecure_clone())); + test_context.send_set_identity_event(); + + assert_eq!(test_context.voting_context.vote_history, old_vote_history); + test_context.check_for_own_vote(&restored_vote); + test_context.check_no_own_vote(); + assert!(test_context.bls_ops.is_empty()); + } + #[test] fn test_standstill_slot_tracking() { let mut test_context = setup(); diff --git a/votor/src/voting_utils.rs b/votor/src/voting_utils.rs index ec2121383ed..35f4cf29133 100644 --- a/votor/src/voting_utils.rs +++ b/votor/src/voting_utils.rs @@ -223,15 +223,25 @@ pub fn generate_vote_tx( }) } -fn create_vote_message(vote: Vote, context: &mut VotingContext) -> Result { +/// Creates a vote message from `vote`, respecting `context.wait_to_vote_slot` only if `respect_wait_to_vote` is true +fn create_vote_message( + vote: Vote, + context: &mut VotingContext, + respect_wait_to_vote: bool, +) -> Result { let bank = context.sharable_banks.root(); + let wait_to_vote_slot = if respect_wait_to_vote { + context.wait_to_vote_slot + } else { + None + }; match generate_vote_tx( vote, &bank, context.vote_account_pubkey, &context.identity_keypair, &context.authorized_voter_keypairs, - context.wait_to_vote_slot, + wait_to_vote_slot, &mut context.derived_bls_keypairs, ) { GenerateVoteTxResult::Vote(vote) => Ok(vote), @@ -273,10 +283,31 @@ pub fn insert_vote_and_create_bls_message( // Update and save the vote history context.vote_history.add_vote(vote); - let vote_msg = match create_vote_message(vote, context) { + let Some(vote_msg) = + create_and_send_own_vote_message(vote, context, /* respect_wait_to_vote */ true)? + else { + return Ok(None); + }; + + let saved_vote_history = + SavedVoteHistory::new(&context.vote_history, &context.identity_keypair)?; + + // Return vote for sending + Ok(Some(BLSOp::PushVote { + vote: Arc::new(vote_msg), + saved_vote_history: SavedVoteHistoryVersions::from(saved_vote_history), + })) +} + +pub(crate) fn create_and_send_own_vote_message( + vote: Vote, + context: &mut VotingContext, + respect_wait_to_vote: bool, +) -> Result, VoteError> { + let vote_msg = match create_vote_message(vote, context, respect_wait_to_vote) { Ok(vote_msg) => vote_msg, Err(e) => { - handle_skippable_vote_error(e, "generate vote and push to votes")?; + handle_skippable_vote_error(e, "generate vote message")?; return Ok(None); } }; @@ -286,21 +317,14 @@ pub fn insert_vote_and_create_bls_message( .send(SigVerifiedBatch::Votes(vec![vote_msg.clone()])) .map_err(|_| SendError(()))?; - let saved_vote_history = - SavedVoteHistory::new(&context.vote_history, &context.identity_keypair)?; - - // Return vote for sending - Ok(Some(BLSOp::PushVote { - vote: Arc::new(vote_msg), - saved_vote_history: SavedVoteHistoryVersions::from(saved_vote_history), - })) + Ok(Some(vote_msg)) } pub fn generate_refresh_vote_message( vote: Vote, vctx: &mut VotingContext, ) -> Result, VoteError> { - match create_vote_message(vote, vctx) { + match create_vote_message(vote, vctx, /* respect_wait_to_vote */ true) { Ok(vote_msg) => Ok(Some(vote_msg)), Err(e) => { handle_skippable_vote_error(e, "generate refresh vote message")?; From f1684c147e78f827c60927ee5bfe967437cb0285 Mon Sep 17 00:00:00 2001 From: Kamil Skalski Date: Wed, 10 Jun 2026 07:28:25 +0200 Subject: [PATCH 272/576] feat(snapshots): stop using slot->storages len mapping from snapshot fields (#13028) * feat(snapshots): stop using slot->storages len mapping from snapshot fields * Remove SerializableStorage trait * Cleanup stored_size fields and guard code with DCOU * Use super:: instead of crate:: --- accounts-db/src/accounts_file.rs | 8 +- accounts-db/src/append_vec.rs | 43 +++------- accounts-db/src/append_vec/meta.rs | 14 +++- runtime/src/serde_snapshot.rs | 78 +++++-------------- runtime/src/serde_snapshot/storage.rs | 10 --- runtime/src/snapshot_utils.rs | 9 --- .../snapshot_storage_rebuilder.rs | 19 +---- snapshots/src/error.rs | 3 - 8 files changed, 47 insertions(+), 137 deletions(-) diff --git a/accounts-db/src/accounts_file.rs b/accounts-db/src/accounts_file.rs index 8dfa8eaa7d3..3e0d5a67c9b 100644 --- a/accounts-db/src/accounts_file.rs +++ b/accounts-db/src/accounts_file.rs @@ -62,10 +62,10 @@ impl AccountsFile { /// Creates a new AccountsFile for the underlying storage at `file_info` /// /// This version of `new()` may only be called when reconstructing storages as part of startup. - /// It trusts the snapshot's value for `current_len`, and relies on later index generation or - /// accounts verification to ensure it is valid. - pub fn new_for_startup(file_info: FileInfo, current_len: usize) -> Result { - let av = AppendVec::new_for_startup(file_info, current_len)?; + /// The storage length is taken to be the full file size; this is trusted and relies on later + /// index generation or accounts verification to ensure it is valid. + pub fn new_for_startup(file_info: FileInfo) -> Result { + let av = AppendVec::new_for_startup(file_info)?; Ok(Self::AppendVec(av)) } diff --git a/accounts-db/src/append_vec.rs b/accounts-db/src/append_vec.rs index ef0c354d817..a31715f7a4f 100644 --- a/accounts-db/src/append_vec.rs +++ b/accounts-db/src/append_vec.rs @@ -342,36 +342,11 @@ impl AppendVec { /// Creates a new AppendVec for the underlying storage at `file_info` /// /// This version of `new()` may only be called when reconstructing storages as part of startup. - /// It trusts the snapshot's value for `current_len`, and relies on later index generation or - /// accounts verification to ensure it is valid. - pub fn new_for_startup(file_info: FileInfo, current_len: usize) -> Result { - let new = Self::new_from_file_info_unchecked(file_info, current_len)?; - - // The current_len is allowed to be either exactly the same as file_size, or - // u64-aligned-equivalent to file_size. This is because `flush` and `shink` compute the - // required file size with *aligned* stored size per account, including the very last - // account. So the file size may have padding to the next u64-alignment. - // For our usage, this is still safe as these padding bytes could never be used for an - // account. This renders the `get_` and `scan_` functions safe. - if (current_len as u64 == new.file_size) - || (u64_align!(current_len) as u64 == new.file_size) - { - Ok(new) - } else { - // However, if opening a minimized snapshot, the file sizes can be - // larger than current length [^1]. So when the `if` condition fails, - // fallback to the old/slow impl that does the full sanitization. - // [^1]: https://github.com/anza-xyz/agave/issues/6797 - info!( - "Could not optimistically create new AppendVec, falling back to pessimistic impl: \ - file size ({}) and current length ({}) do not match for '{}'", - new.file_size, - current_len, - new.path.display(), - ); - let _num_accounts = new.sanitize_layout_and_length()?; - Ok(new) - } + /// The storage length is taken to be the full file size; this is trusted and relies on later + /// index generation or accounts verification to ensure it is valid. + pub fn new_for_startup(file_info: FileInfo) -> Result { + let current_len = file_info.size as usize; + Self::new_from_file_info_unchecked(file_info, current_len) } /// Creates an appendvec in read-only mode from existing `FileInfo` and without full data checks @@ -402,6 +377,7 @@ impl AppendVec { } /// Checks that all accounts layout is correct and returns the number of accounts. + #[cfg(feature = "dev-context-only-utils")] fn sanitize_layout_and_length(&self) -> Result { // This discards allocated accounts immediately after check at each loop iteration. // @@ -636,13 +612,13 @@ impl AppendVec { ValidSlice(unsafe { slice::from_raw_parts(buf.as_ptr() as *const u8, bytes_read) }); let (meta, next) = Self::get_type::(valid_bytes, 0)?; let (account_meta, _) = Self::get_type::(valid_bytes, next)?; - let stored_size = Self::calculate_stored_size_checked(meta.data_len as usize)?; + // Guard against a corrupt `data_len` whose stored size would overflow. + Self::calculate_stored_size_checked(meta.data_len as usize)?; Some(callback(StoredAccountNoData { meta, account_meta, offset, - stored_size, })) } @@ -992,7 +968,6 @@ impl AppendVec { meta: stored_meta, account_meta, offset, - stored_size, }); reader.consume_or_skip(stored_size); } @@ -2009,7 +1984,7 @@ mod tests { let offset = account_offsets.get(i).unwrap(); assert_eq!( - stored_account.stored_size, + stored_account.stored_size(), AppendVec::calculate_stored_size(account.data().len()), ); assert_eq!(stored_account.offset(), *offset); diff --git a/accounts-db/src/append_vec/meta.rs b/accounts-db/src/append_vec/meta.rs index dc901183e27..f36275e3fcf 100644 --- a/accounts-db/src/append_vec/meta.rs +++ b/accounts-db/src/append_vec/meta.rs @@ -1,6 +1,6 @@ use { crate::is_zero_lamport::IsZeroLamport, solana_account::ReadableAccount, solana_clock::Epoch, - solana_pubkey::Pubkey, std::ptr, + solana_pubkey::Pubkey, }; /// Meta contains enough context to recover the index from storage itself @@ -96,7 +96,6 @@ pub struct StoredAccountNoData<'append_vec> { pub meta: &'append_vec StoredMeta, pub account_meta: &'append_vec AccountMeta, pub offset: usize, - pub stored_size: usize, } impl<'append_vec> StoredAccountNoData<'append_vec> { @@ -115,6 +114,7 @@ impl<'append_vec> StoredAccountNoData<'append_vec> { &self.meta.pubkey } + #[cfg(feature = "dev-context-only-utils")] pub fn sanitize(&self) -> bool { self.sanitize_executable() && self.sanitize_lamports() } @@ -124,9 +124,12 @@ impl<'append_vec> StoredAccountNoData<'append_vec> { self.offset } + /// The on-disk size of this account, derived from its data length. The view is transient and + /// not persisted, so the size is computed on demand rather than stored. + #[cfg(feature = "dev-context-only-utils")] #[inline(always)] pub fn stored_size(&self) -> usize { - self.stored_size + super::AppendVec::calculate_stored_size(self.meta.data_len as usize) } #[inline(always)] @@ -144,6 +147,7 @@ impl<'append_vec> StoredAccountNoData<'append_vec> { self.account_meta.rent_epoch } + #[cfg(feature = "dev-context-only-utils")] pub fn sanitize_executable(&self) -> bool { // Sanitize executable to ensure higher 7-bits are cleared correctly. self.ref_executable_byte() & !1 == 0 @@ -153,6 +157,7 @@ impl<'append_vec> StoredAccountNoData<'append_vec> { /// /// Note that we are not comparing against AccountSharedData::default() because we do not have access to the account data, /// so we compare data _length_ in lieu of actual data. This check otherwise identical to AccountSharedData::default(). + #[cfg(feature = "dev-context-only-utils")] pub fn is_default_account(&self) -> bool { self.account_meta.lamports == 0 && self.meta.data_len == 0 @@ -161,12 +166,15 @@ impl<'append_vec> StoredAccountNoData<'append_vec> { && self.account_meta.owner == Pubkey::default() } + #[cfg(feature = "dev-context-only-utils")] pub fn sanitize_lamports(&self) -> bool { // Check if the account data matches that of a default account if it has 0 lamports. self.account_meta.lamports != 0 || self.is_default_account() } + #[cfg(feature = "dev-context-only-utils")] pub fn ref_executable_byte(&self) -> &u8 { + use std::ptr; // Use extra references to avoid value silently clamped to 1 (=true) and 0 (=false) // Yes, this really happens; see test_new_from_file_crafted_executable let executable_bool: &bool = &self.account_meta.executable; diff --git a/runtime/src/serde_snapshot.rs b/runtime/src/serde_snapshot.rs index ef0ade509d2..150c83fc949 100644 --- a/runtime/src/serde_snapshot.rs +++ b/runtime/src/serde_snapshot.rs @@ -55,7 +55,6 @@ use { thread, time::Instant, }, - storage::SerializableStorage, types::{SerdeAccountsLtHash, UnusedRentCollector}, wincode::{ SchemaReadOwned, SchemaWrite, @@ -96,38 +95,6 @@ pub(crate) struct AccountsDbFields( Vec<(Slot, Hash)>, ); -impl AccountsDbFields { - /// Get snapshot storage lengths filtering to slots above base slot (if provided). - /// - /// Returns an error if storage slots exceed snapshot slot indicating inconsistency of data. - pub(crate) fn get_storage_lengths_for_snapshot_slots( - &self, - base_slot: Option, - ) -> Result, SnapshotError> { - let AccountsDbFields(snapshot_storage, _, snapshot_slot, ..) = self; - let filtered_min_slot = base_slot.map(|slot| slot + 1).unwrap_or(Slot::MIN); - let mut lengths = HashMap::with_capacity(snapshot_storage.len()); - - for (slot, slot_storage) in snapshot_storage { - if slot > snapshot_slot { - return Err(SnapshotError::MismatchedSnapshotStorageSlot( - *slot, - *snapshot_slot, - )); - } - if *slot < filtered_min_slot { - // Serialized bank includes storage mapping for all slots, but it might be used for - // rebuilding storages only up from `base_slot`, so this case is not an error. - continue; - } - assert_eq!(slot_storage.len(), 1, "invalid storage count (slot={slot})"); - let storage_entry = &slot_storage[0]; - lengths.insert(*slot, storage_entry.current_len()); - } - Ok(lengths) - } -} - #[cfg_attr(feature = "frozen-abi", derive(AbiExample))] #[cfg_attr(feature = "dev-context-only-utils", derive(Default, PartialEq))] #[derive(Serialize, Deserialize, Clone, Debug)] @@ -813,10 +780,7 @@ pub(crate) fn reconstruct_bank_from_fields( accounts_db_config: AccountsDbConfig, accounts_update_notifier: Option, exit: Arc, -) -> Result<(Bank, ReconstructedBankInfo), Error> -where - E: SerializableStorage + std::marker::Sync, -{ +) -> Result<(Bank, ReconstructedBankInfo), Error> { let mut bank_fields = bank_fields.collapse_into(); // Epoch stakes take several seconds to reconstruct, do it in parallel with loading accountsdb let deserializable_epoch_stakes = std::mem::take(&mut bank_fields.versioned_epoch_stakes); @@ -867,28 +831,31 @@ where pub(crate) fn reconstruct_single_storage( slot: &Slot, append_vec_file_info: FileInfo, - current_len: usize, id: AccountsFileId, obsolete_accounts: Option<(ObsoleteAccounts, AccountsFileId, usize)>, ) -> Result, SnapshotError> { - // When restoring from an archive, obsolete accounts will always be `None` + // The storage length is taken directly from the on-disk file size (see + // `AccountsFile::new_for_startup`). When restoring from an archive the obsolete accounts have + // been physically removed during serialization, and when restoring from a snapshot directory + // they are still present in the file. In both cases the file size already reflects the exact + // number of bytes the storage spans, so there is no need to carry the length separately in the + // snapshot fields. + // + // When restoring from an archive, obsolete accounts will always be `None`. // When restoring from fastboot, obsolete accounts will be 'Some' if the storage contained // accounts marked obsolete at the time the snapshot was taken. - let (current_len, obsolete_accounts) = if let Some(obsolete_accounts) = obsolete_accounts { - let updated_len = current_len + obsolete_accounts.2; - if obsolete_accounts.1 != id { - return Err(SnapshotError::MismatchedAccountsFileId( - id, - obsolete_accounts.1, - )); - } + let obsolete_accounts = + if let Some((obsolete_accounts, obsolete_id, _obsolete_bytes)) = obsolete_accounts { + if obsolete_id != id { + return Err(SnapshotError::MismatchedAccountsFileId(id, obsolete_id)); + } - (updated_len, obsolete_accounts.0) - } else { - (current_len, ObsoleteAccounts::default()) - }; + obsolete_accounts + } else { + ObsoleteAccounts::default() + }; - let accounts_file = AccountsFile::new_for_startup(append_vec_file_info, current_len)?; + let accounts_file = AccountsFile::new_for_startup(append_vec_file_info)?; Ok(Arc::new(AccountStorageEntry::new_existing( *slot, id, @@ -982,7 +949,6 @@ pub(crate) fn remap_append_vec_file( pub(crate) fn remap_and_reconstruct_single_storage( slot: Slot, old_append_vec_id: SerializedAccountsFileId, - current_len: usize, append_vec_file_info: FileInfo, next_append_vec_id: &AtomicAccountsFileId, num_collisions: &mut usize, @@ -997,7 +963,6 @@ pub(crate) fn remap_and_reconstruct_single_storage( let storage = reconstruct_single_storage( &slot, remapped_append_vec_file_info, - current_len, remapped_append_vec_id, None, )?; @@ -1025,10 +990,7 @@ fn reconstruct_accountsdb_from_fields( accounts_db_config: AccountsDbConfig, accounts_update_notifier: Option, exit: Arc, -) -> Result<(AccountsDb, ReconstructedAccountsDbInfo), Error> -where - E: SerializableStorage + std::marker::Sync, -{ +) -> Result<(AccountsDb, ReconstructedAccountsDbInfo), Error> { let mut accounts_db = AccountsDb::new_with_config( account_paths.to_vec(), accounts_db_config, diff --git a/runtime/src/serde_snapshot/storage.rs b/runtime/src/serde_snapshot/storage.rs index 714c2f65a33..cf180ca8243 100644 --- a/runtime/src/serde_snapshot/storage.rs +++ b/runtime/src/serde_snapshot/storage.rs @@ -33,15 +33,5 @@ impl SerializableAccountStorageEntry { } } -pub(crate) trait SerializableStorage { - fn current_len(&self) -> usize; -} - -impl SerializableStorage for SerializableAccountStorageEntry { - fn current_len(&self) -> usize { - self.accounts_current_len - } -} - #[cfg(feature = "frozen-abi")] impl solana_frozen_abi::abi_example::TransparentAsHelper for SerializableAccountStorageEntry {} diff --git a/runtime/src/snapshot_utils.rs b/runtime/src/snapshot_utils.rs index 7e9bc3d5bff..fb01790d209 100644 --- a/runtime/src/snapshot_utils.rs +++ b/runtime/src/snapshot_utils.rs @@ -1011,7 +1011,6 @@ pub fn verify_and_unarchive_snapshots( account_paths, full_snapshot_archive_info.archive_format(), next_append_vec_id.clone(), - None, io_setup, )?; @@ -1038,7 +1037,6 @@ pub fn verify_and_unarchive_snapshots( account_paths, incremental_snapshot_archive_info.archive_format(), next_append_vec_id.clone(), - Some(incremental_snapshot_archive_info.base_slot()), io_setup, )?; ( @@ -1230,7 +1228,6 @@ fn unarchive_snapshot( account_paths: &[PathBuf], archive_format: ArchiveFormat, next_append_vec_id: Arc, - base_slot: Option, io_setup: &IoSetupState, ) -> Result { let unpack_dir = tempfile::Builder::new() @@ -1258,11 +1255,8 @@ fn unarchive_snapshot( append_vec_files, .. }| { - let snapshot_storage_lengths = - accounts_db_fields.get_storage_lengths_for_snapshot_slots(base_slot)?; let (storage, measure_untar) = measure_time!( SnapshotStorageRebuilder::rebuild_storages( - snapshot_storage_lengths, append_vec_files.into_iter().chain(file_receiver), next_append_vec_id, SnapshotFrom::Archive, @@ -1536,10 +1530,7 @@ pub(crate) fn rebuild_storages_from_snapshot_dir( append_vec_files, .. }| { - let snapshot_storage_lengths = - accounts_db_fields.get_storage_lengths_for_snapshot_slots(None)?; let storage = SnapshotStorageRebuilder::rebuild_storages( - snapshot_storage_lengths, append_vec_files.into_iter().chain(file_receiver), next_append_vec_id, SnapshotFrom::Dir, diff --git a/runtime/src/snapshot_utils/snapshot_storage_rebuilder.rs b/runtime/src/snapshot_utils/snapshot_storage_rebuilder.rs index a03af4b726c..7b4ead28357 100644 --- a/runtime/src/snapshot_utils/snapshot_storage_rebuilder.rs +++ b/runtime/src/snapshot_utils/snapshot_storage_rebuilder.rs @@ -27,8 +27,6 @@ const PROGRESS_LOG_INTERVAL: Duration = Duration::from_secs(2); /// Stores state for rebuilding snapshot storages #[derive(Debug)] pub(crate) struct SnapshotStorageRebuilder { - /// Snapshot storage lengths - from the snapshot file - snapshot_storage_lengths: HashMap, /// Container for storing rebuilt snapshot storages storage: AccountStorageMap, /// Tracks next append_vec_id @@ -46,15 +44,13 @@ pub(crate) struct SnapshotStorageRebuilder { impl SnapshotStorageRebuilder { /// Rebuild snapshot storages on the current thread by consuming `files`. pub(crate) fn rebuild_storages( - snapshot_storage_lengths: HashMap, files: impl IntoIterator, next_append_vec_id: Arc, snapshot_from: SnapshotFrom, obsolete_accounts: Option, ) -> Result { let mut rebuilder = Self { - storage: AccountStorageMap::with_capacity(snapshot_storage_lengths.len()), - snapshot_storage_lengths, + storage: AccountStorageMap::default(), next_append_vec_id, num_collisions: 0, processed_slot_count: 0, @@ -80,10 +76,8 @@ impl SnapshotStorageRebuilder { fn log_progress(&self) { info!( - "rebuilt storages for {}/{} slots with {} collisions", - self.processed_slot_count, - self.snapshot_storage_lengths.len(), - self.num_collisions, + "rebuilt storages for {} slots with {} collisions", + self.processed_slot_count, self.num_collisions, ); } @@ -112,17 +106,11 @@ impl SnapshotStorageRebuilder { ) -> Result<(), SnapshotError> { let filename = file_info.path.file_name().unwrap().to_str().unwrap(); let (_, old_append_vec_id) = get_slot_and_append_vec_id(filename)?; - let Some(¤t_len) = self.snapshot_storage_lengths.get(&slot) else { - return Err(SnapshotError::RebuildStorages(format!( - "account storage file '{filename}' for slot outside of expected range in snapshot" - ))); - }; let storage_entry = match &self.snapshot_from { SnapshotFrom::Archive => remap_and_reconstruct_single_storage( slot, old_append_vec_id, - current_len, file_info, &self.next_append_vec_id, &mut self.num_collisions, @@ -130,7 +118,6 @@ impl SnapshotStorageRebuilder { SnapshotFrom::Dir => reconstruct_single_storage( &slot, file_info, - current_len, old_append_vec_id as AccountsFileId, self.obsolete_accounts .remove(&slot) diff --git a/snapshots/src/error.rs b/snapshots/src/error.rs index 7c04fceeef4..ce92a94a05d 100644 --- a/snapshots/src/error.rs +++ b/snapshots/src/error.rs @@ -75,9 +75,6 @@ pub enum SnapshotError { #[error("snapshot epoch stakes are invalid: {0}")] VerifyEpochStakes(#[from] VerifyEpochStakesError), - #[error("slot in storages map {0} exceeds snapshot slot: {1}")] - MismatchedSnapshotStorageSlot(Slot, Slot), - #[error("bank_snapshot_info new_from_dir failed: {0}")] NewFromDir(#[from] SnapshotNewFromDirError), From 13704921bea69ed3344c03de743fc9308aa95597 Mon Sep 17 00:00:00 2001 From: Edvard Fagerholm Date: Wed, 10 Jun 2026 09:03:14 +0300 Subject: [PATCH 273/576] test(core): bound previously-unbounded test-only channels (#13027) test: bound previously-unbounded test-only channels in core Replace unbounded() with crossbeam_channel::bounded(1024) at all test-only channel call sites (cfg(test) modules, tests/ integration tests, benches). Production channels are left unbounded. --- core/benches/sigverify_stage.rs | 8 +- core/src/banking_stage.rs | 10 +- core/src/banking_stage/consume_worker.rs | 12 +-- core/src/banking_stage/consumer.rs | 10 +- .../transaction_scheduler/greedy_scheduler.rs | 6 +- .../receive_and_buffer.rs | 28 +++--- .../transaction_scheduler/scheduler_common.rs | 14 +-- .../scheduler_controller.rs | 8 +- .../src/banking_stage/vote_packet_receiver.rs | 4 +- core/src/banking_stage/vote_worker.rs | 4 +- core/src/block_creation_loop.rs | 14 +-- core/src/bls_sigverify/bls_sigverifier.rs | 22 ++--- core/src/cluster_info_vote_listener.rs | 45 ++++----- core/src/forwarding_stage.rs | 4 +- core/src/repair/ancestor_hashes_service.rs | 19 ++-- .../src/repair/cluster_slot_state_verifier.rs | 22 ++--- core/src/repair/serve_repair.rs | 4 +- core/src/replay_stage/tests.rs | 95 +++++++++---------- core/src/result.rs | 4 +- core/src/sigverify_stage.rs | 14 +-- core/src/tvu.rs | 18 ++-- core/src/vote_simulator.rs | 4 +- core/src/window_service.rs | 9 +- core/tests/snapshots.rs | 14 +-- core/tests/unified_scheduler.rs | 6 +- 25 files changed, 200 insertions(+), 198 deletions(-) diff --git a/core/benches/sigverify_stage.rs b/core/benches/sigverify_stage.rs index 66134d0777f..7c9075d0b0c 100644 --- a/core/benches/sigverify_stage.rs +++ b/core/benches/sigverify_stage.rs @@ -2,7 +2,7 @@ use { bencher::{Bencher, TDynBenchFn, TestDesc, TestDescAndFn, TestFn, benchmark_main}, - crossbeam_channel::unbounded, + crossbeam_channel::bounded, log::*, rand::{ Rng, @@ -74,11 +74,11 @@ fn bench_sigverify_stage(bencher: &mut Bencher, use_same_tx: bool) { let (_bank, bank_forks) = Bank::new_with_bank_forks_for_tests(&create_genesis_config(1).genesis_config); let sharable_banks = bank_forks.read().unwrap().sharable_banks(); - let (packet_s, packet_r) = unbounded(); - let (vote_packet_s, vote_packet_r) = unbounded(); + let (packet_s, packet_r) = bounded(1024); + let (vote_packet_s, vote_packet_r) = bounded(1024); let (verified_s, verified_r) = BankingTracer::channel_for_test(); let (tpu_vote_s, _tpu_vote_r) = BankingTracer::channel_for_test(); - let (forward_stage_s, _forward_stage_r) = unbounded(); + let (forward_stage_s, _forward_stage_r) = bounded(1024); let (stage, gossip_sigverify_handle) = SigVerifyStage::new( packet_r, vote_packet_r, diff --git a/core/src/banking_stage.rs b/core/src/banking_stage.rs index 951553a896d..7104cff1f90 100644 --- a/core/src/banking_stage.rs +++ b/core/src/banking_stage.rs @@ -830,7 +830,7 @@ mod tests { validator::SchedulerPacing, }, agave_banking_stage_ingress_types::BankingPacketBatch, - crossbeam_channel::unbounded, + crossbeam_channel::bounded, itertools::Itertools, solana_entry::{ entry::{self, EntrySlice}, @@ -896,7 +896,7 @@ mod tests { poh_service, _entry_receiever, ) = create_test_recorder(bank, blockstore, None, None); - let (replay_vote_sender, _replay_vote_receiver) = unbounded(); + let (replay_vote_sender, _replay_vote_receiver) = bounded(1024); let banking_stage = BankingStage::new_num_threads( BlockProductionMethod::CentralSchedulerGreedy, @@ -958,7 +958,7 @@ mod tests { poh_service, entry_receiver, ) = create_test_recorder(bank, blockstore, None, None); - let (replay_vote_sender, _replay_vote_receiver) = unbounded(); + let (replay_vote_sender, _replay_vote_receiver) = bounded(1024); let banking_stage = BankingStage::new_num_threads( BlockProductionMethod::CentralSchedulerGreedy, @@ -1104,7 +1104,7 @@ mod tests { .expect("Expected to be able to open database ledger"), ); - let (replay_vote_sender, _replay_vote_receiver) = unbounded(); + let (replay_vote_sender, _replay_vote_receiver) = bounded(1024); let entry_receiver = { // start a banking_stage to eat verified receiver let (bank, bank_forks) = Bank::new_with_bank_forks_for_tests(&genesis_config); @@ -1269,7 +1269,7 @@ mod tests { poh_service, _entry_receiver, ) = create_test_recorder(bank.clone(), blockstore, None, None); - let (replay_vote_sender, _replay_vote_receiver) = unbounded(); + let (replay_vote_sender, _replay_vote_receiver) = bounded(1024); let banking_stage = BankingStage::new_num_threads( BlockProductionMethod::CentralSchedulerGreedy, diff --git a/core/src/banking_stage/consume_worker.rs b/core/src/banking_stage/consume_worker.rs index f22be6998b7..91489c2047f 100644 --- a/core/src/banking_stage/consume_worker.rs +++ b/core/src/banking_stage/consume_worker.rs @@ -1116,7 +1116,7 @@ pub(crate) mod external { handshake::{ClientLogon, client, server::Server}, responses_region::{CheckResponsesPtr, ExecutionResponsesPtr}, }, - crossbeam_channel::unbounded, + crossbeam_channel::bounded, solana_account::AccountSharedData, solana_genesis_config::GenesisConfig, solana_keypair::Keypair, @@ -1333,7 +1333,7 @@ pub(crate) mod external { let (record_sender, record_receiver) = record_channels(false); let recorder = TransactionRecorder::new(record_sender); - let (replay_vote_sender, replay_vote_receiver) = unbounded(); + let (replay_vote_sender, replay_vote_receiver) = bounded(1024); let committer = Committer::new(None, replay_vote_sender, None); let consumer = Consumer::new(committer, recorder, None); let shared_leader_state = SharedLeaderState::new(0, None, None); @@ -2817,7 +2817,7 @@ mod tests { scheduler_messages::{MaxAge, TransactionBatchId}, tests::{create_slow_genesis_config, sanitize_transactions}, }, - crossbeam_channel::unbounded, + crossbeam_channel::bounded, solana_clock::Slot, solana_genesis_config::GenesisConfig, solana_keypair::Keypair, @@ -2887,13 +2887,13 @@ mod tests { let (record_sender, record_receiver) = record_channels(false); let recorder = TransactionRecorder::new(record_sender); - let (replay_vote_sender, replay_vote_receiver) = unbounded(); + let (replay_vote_sender, replay_vote_receiver) = bounded(1024); let committer = Committer::new(None, replay_vote_sender, None); let consumer = Consumer::new(committer, recorder, None); let shared_leader_state = SharedLeaderState::new(0, None, None); - let (consume_sender, consume_receiver) = unbounded(); - let (consumed_sender, consumed_receiver) = unbounded(); + let (consume_sender, consume_receiver) = bounded(1024); + let (consumed_sender, consumed_receiver) = bounded(1024); let worker = ConsumeWorker::new( 0, Arc::new(AtomicBool::new(false)), diff --git a/core/src/banking_stage/consumer.rs b/core/src/banking_stage/consumer.rs index 897052f5876..9d8b79dae2d 100644 --- a/core/src/banking_stage/consumer.rs +++ b/core/src/banking_stage/consumer.rs @@ -508,7 +508,7 @@ mod tests { super::*, crate::banking_stage::tests::{create_slow_genesis_config, sanitize_transactions}, agave_reserved_account_keys::ReservedAccountKeys, - crossbeam_channel::unbounded, + crossbeam_channel::bounded, solana_account::{AccountSharedData, state_traits::StateMut}, solana_address_lookup_table_interface::{ self as address_lookup_table, @@ -577,7 +577,7 @@ mod tests { let recorder = TransactionRecorder::new(record_sender); record_receiver.restart(bank.bank_id()); - let (replay_vote_sender, _replay_vote_receiver) = unbounded(); + let (replay_vote_sender, _replay_vote_receiver) = bounded(1024); let committer = Committer::new(transaction_status_sender, replay_vote_sender, None); let consumer = Consumer::new(committer, recorder, None); @@ -600,7 +600,7 @@ mod tests { let recorder = TransactionRecorder::new(record_sender); record_receiver.restart(bank.bank_id()); - let (replay_vote_sender, _replay_vote_receiver) = unbounded(); + let (replay_vote_sender, _replay_vote_receiver) = bounded(1024); let committer = Committer::new(None, replay_vote_sender, None); let consumer = Consumer::new(committer, recorder, None); consumer.process_and_record_transactions(&bank, &transactions) @@ -1255,7 +1255,7 @@ mod tests { #[test] fn test_write_persist_transaction_status() { - let (transaction_status_sender, transaction_status_receiver) = unbounded(); + let (transaction_status_sender, transaction_status_receiver) = bounded(1024); let tss = Some(TransactionStatusSender { sender: transaction_status_sender, dependency_tracker: None, @@ -1326,7 +1326,7 @@ mod tests { #[test] fn test_write_persist_loaded_addresses() { - let (transaction_status_sender, transaction_status_receiver) = unbounded(); + let (transaction_status_sender, transaction_status_receiver) = bounded(1024); let tss = Some(TransactionStatusSender { sender: transaction_status_sender, dependency_tracker: None, diff --git a/core/src/banking_stage/transaction_scheduler/greedy_scheduler.rs b/core/src/banking_stage/transaction_scheduler/greedy_scheduler.rs index 202d4a51807..3219032760f 100644 --- a/core/src/banking_stage/transaction_scheduler/greedy_scheduler.rs +++ b/core/src/banking_stage/transaction_scheduler/greedy_scheduler.rs @@ -292,7 +292,7 @@ mod test { scheduler_messages::{MaxAge, TransactionId}, transaction_scheduler::transaction_state_container::TransactionStateContainer, }, - crossbeam_channel::unbounded, + crossbeam_channel::bounded, itertools::Itertools, solana_compute_budget_interface::ComputeBudgetInstruction, solana_hash::Hash, @@ -316,8 +316,8 @@ mod test { Sender>>, ) { let (consume_work_senders, consume_work_receivers) = - (0..num_threads).map(|_| unbounded()).unzip(); - let (finished_consume_work_sender, finished_consume_work_receiver) = unbounded(); + (0..num_threads).map(|_| bounded(1024)).unzip(); + let (finished_consume_work_sender, finished_consume_work_receiver) = bounded(1024); let scheduler = GreedyScheduler::new(consume_work_senders, finished_consume_work_receiver, config); ( diff --git a/core/src/banking_stage/transaction_scheduler/receive_and_buffer.rs b/core/src/banking_stage/transaction_scheduler/receive_and_buffer.rs index 2a95354cf3a..d07e95adee2 100644 --- a/core/src/banking_stage/transaction_scheduler/receive_and_buffer.rs +++ b/core/src/banking_stage/transaction_scheduler/receive_and_buffer.rs @@ -548,7 +548,7 @@ mod tests { use { super::*, crate::banking_stage::tests::create_slow_genesis_config, - crossbeam_channel::{Receiver, unbounded}, + crossbeam_channel::{Receiver, bounded}, solana_hash::Hash, solana_keypair::Keypair, solana_ledger::genesis_utils::GenesisConfigInfo, @@ -657,7 +657,7 @@ mod tests { #[test] fn test_receive_and_buffer_disconnected_channel() { - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); let (bank_forks, _mint_keypair) = test_bank_forks(); let (mut receive_and_buffer, mut container) = setup_transaction_view_receive_and_buffer(receiver, bank_forks); @@ -670,7 +670,7 @@ mod tests { #[test] fn test_receive_and_buffer_no_hold() { - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); let (bank_forks, mint_keypair) = test_bank_forks(); let (mut receive_and_buffer, mut container) = setup_transaction_view_receive_and_buffer(receiver, bank_forks.clone()); @@ -720,7 +720,7 @@ mod tests { #[test] fn test_receive_and_buffer_discard() { - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); let (bank_forks, mint_keypair) = test_bank_forks(); let (mut receive_and_buffer, mut container) = setup_transaction_view_receive_and_buffer(receiver, bank_forks.clone()); @@ -773,7 +773,7 @@ mod tests { #[test] fn test_receive_and_buffer_invalid_transaction_format() { - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); let (bank_forks, _mint_keypair) = test_bank_forks(); let (mut receive_and_buffer, mut container) = setup_transaction_view_receive_and_buffer(receiver, bank_forks); @@ -817,7 +817,7 @@ mod tests { #[test] fn test_receive_and_buffer_invalid_blockhash() { - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); let (bank_forks, mint_keypair) = test_bank_forks(); let (mut receive_and_buffer, mut container) = setup_transaction_view_receive_and_buffer(receiver, bank_forks); @@ -860,7 +860,7 @@ mod tests { #[test] fn test_receive_and_buffer_simple_transfer_unfunded_fee_payer() { - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); let (bank_forks, _mint_keypair) = test_bank_forks(); let (mut receive_and_buffer, mut container) = setup_transaction_view_receive_and_buffer(receiver, bank_forks.clone()); @@ -908,7 +908,7 @@ mod tests { #[test] fn test_receive_and_buffer_failed_alt_resolve() { - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); let (bank_forks, mint_keypair) = test_bank_forks(); let (mut receive_and_buffer, mut container) = setup_transaction_view_receive_and_buffer(receiver, bank_forks.clone()); @@ -971,7 +971,7 @@ mod tests { #[test] fn test_receive_and_buffer_simple_transfer() { - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); let (bank_forks, mint_keypair) = test_bank_forks(); let (mut receive_and_buffer, mut container) = setup_transaction_view_receive_and_buffer(receiver, bank_forks.clone()); @@ -1019,7 +1019,7 @@ mod tests { #[test] fn test_receive_and_buffer_filters_fee_payer() { - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); let (bank_forks, mint_keypair) = test_bank_forks(); let (mut receive_and_buffer, mut container) = setup_transaction_view_receive_and_buffer_with_filter_keys( @@ -1049,7 +1049,7 @@ mod tests { #[test] fn test_receive_and_buffer_filters_account_key() { - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); let (bank_forks, mint_keypair) = test_bank_forks(); let filtered_key = Pubkey::new_unique(); let (mut receive_and_buffer, mut container) = @@ -1080,7 +1080,7 @@ mod tests { #[test] fn test_receive_and_buffer_does_not_filter_unmatched_keys() { - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); let (bank_forks, mint_keypair) = test_bank_forks(); let (mut receive_and_buffer, mut container) = setup_transaction_view_receive_and_buffer_with_filter_keys( @@ -1110,7 +1110,7 @@ mod tests { #[test] fn test_receive_and_buffer_overfull() { - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); let (bank_forks, mint_keypair) = test_bank_forks(); let (mut receive_and_buffer, mut container) = setup_transaction_view_receive_and_buffer(receiver, bank_forks.clone()); @@ -1189,7 +1189,7 @@ mod tests { .unwrap() } - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); let (bank_forks, mint_keypair) = test_bank_forks(); let (mut receive_and_buffer, mut container) = setup_transaction_view_receive_and_buffer(receiver, bank_forks.clone()); diff --git a/core/src/banking_stage/transaction_scheduler/scheduler_common.rs b/core/src/banking_stage/transaction_scheduler/scheduler_common.rs index 57ea609acea..3184c0e00b4 100644 --- a/core/src/banking_stage/transaction_scheduler/scheduler_common.rs +++ b/core/src/banking_stage/transaction_scheduler/scheduler_common.rs @@ -313,7 +313,7 @@ mod tests { consumer::RetryableIndex, transaction_scheduler::transaction_state_container::TransactionStateContainer, }, - crossbeam_channel::unbounded, + crossbeam_channel::bounded, solana_hash::Hash, solana_keypair::Keypair, solana_pubkey::Pubkey, @@ -470,8 +470,8 @@ mod tests { add_transactions_to_container(&mut container, 3); let (work_senders, work_receivers): (Vec>, Vec>) = - (0..NUM_WORKERS).map(|_| unbounded()).unzip(); - let (_finished_work_sender, finished_work_receiver) = unbounded(); + (0..NUM_WORKERS).map(|_| bounded(1024)).unzip(); + let (_finished_work_sender, finished_work_receiver) = bounded(1024); let mut common = SchedulingCommon::new(work_senders, finished_work_receiver, 10); assert_eq!( common.batches.entry_bytes(), @@ -524,8 +524,8 @@ mod tests { add_transactions_to_container(&mut container, 1); let (work_senders, work_receivers): (Vec>, Vec>) = - (0..NUM_WORKERS).map(|_| unbounded()).unzip(); - let (finished_work_sender, finished_work_receiver) = unbounded(); + (0..NUM_WORKERS).map(|_| bounded(1024)).unzip(); + let (finished_work_sender, finished_work_receiver) = bounded(1024); let mut common = SchedulingCommon::new(work_senders, finished_work_receiver, 10); // Send a batch. Return completed work. @@ -579,8 +579,8 @@ mod tests { let mut container = TransactionStateContainer::with_capacity(1024); let (work_senders, work_receivers): (Vec>, Vec>) = - (0..NUM_WORKERS).map(|_| unbounded()).unzip(); - let (finished_work_sender, finished_work_receiver) = unbounded(); + (0..NUM_WORKERS).map(|_| bounded(1024)).unzip(); + let (finished_work_sender, finished_work_receiver) = bounded(1024); let mut common = SchedulingCommon::new(work_senders, finished_work_receiver, 10); // Retryable indexes out-of-order. add_transactions_to_container(&mut container, 2); diff --git a/core/src/banking_stage/transaction_scheduler/scheduler_controller.rs b/core/src/banking_stage/transaction_scheduler/scheduler_controller.rs index 9013121a08b..101a9a50924 100644 --- a/core/src/banking_stage/transaction_scheduler/scheduler_controller.rs +++ b/core/src/banking_stage/transaction_scheduler/scheduler_controller.rs @@ -536,7 +536,7 @@ mod tests { transaction_scheduler::greedy_scheduler::{GreedyScheduler, GreedySchedulerConfig}, }, agave_banking_stage_ingress_types::{BankingPacketBatch, BankingPacketReceiver}, - crossbeam_channel::{Receiver, Sender, unbounded}, + crossbeam_channel::{Receiver, Sender, bounded}, itertools::Itertools, solana_compute_budget_interface::ComputeBudgetInstruction, solana_fee_calculator::FeeRateGovernor, @@ -556,7 +556,7 @@ mod tests { }; fn create_channels(num: usize) -> (Vec>, Vec>) { - (0..num).map(|_| unbounded()).unzip() + (0..num).map(|_| bounded(1024)).unzip() } // Helper struct to create tests that hold channels, files, etc. @@ -603,12 +603,12 @@ mod tests { let decision_maker = DecisionMaker::new(shared_leader_state.clone()); - let (banking_packet_sender, banking_packet_receiver) = unbounded(); + let (banking_packet_sender, banking_packet_receiver) = bounded(1024); let receive_and_buffer = create_receive_and_buffer(banking_packet_receiver, bank_forks.clone()); let (consume_work_senders, consume_work_receivers) = create_channels(num_threads); - let (finished_consume_work_sender, finished_consume_work_receiver) = unbounded(); + let (finished_consume_work_sender, finished_consume_work_receiver) = bounded(1024); let test_frame = TestFrame { bank, diff --git a/core/src/banking_stage/vote_packet_receiver.rs b/core/src/banking_stage/vote_packet_receiver.rs index 856d270a0a6..aae5a8efa3a 100644 --- a/core/src/banking_stage/vote_packet_receiver.rs +++ b/core/src/banking_stage/vote_packet_receiver.rs @@ -280,7 +280,7 @@ mod tests { leader_slot_metrics::LeaderSlotMetricsTracker, vote_storage::{VoteStorage, tests::packet_from_slots}, }, - crossbeam_channel::unbounded, + crossbeam_channel::bounded, solana_perf::packet::PacketBatch, solana_runtime::{ bank::Bank, @@ -294,7 +294,7 @@ mod tests { filter_keys: Arc>, ) -> VoteStorage { let vote_packet = packet_from_slots(vec![(1, 1)], keypairs, None); - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); sender .send(Arc::new(vec![PacketBatch::from(vec![vote_packet])])) .unwrap(); diff --git a/core/src/banking_stage/vote_worker.rs b/core/src/banking_stage/vote_worker.rs index 3c9060a0de0..8adee756a75 100644 --- a/core/src/banking_stage/vote_worker.rs +++ b/core/src/banking_stage/vote_worker.rs @@ -577,7 +577,7 @@ mod tests { tests::{create_slow_genesis_config, sanitize_transactions}, vote_storage::tests::packet_from_slots, }, - crossbeam_channel::unbounded, + crossbeam_channel::bounded, solana_ledger::genesis_utils::GenesisConfigInfo, solana_perf::packet::BytesPacket, solana_poh::record_channels::record_channels, @@ -764,7 +764,7 @@ mod tests { let recorder = solana_poh::transaction_recorder::TransactionRecorder::new(record_sender); record_receiver.restart(bank.bank_id()); - let (replay_vote_sender, _replay_vote_receiver) = unbounded(); + let (replay_vote_sender, _replay_vote_receiver) = bounded(1024); let committer = Committer::new(None, replay_vote_sender, None); let consumer = Consumer::new(committer, recorder, None); diff --git a/core/src/block_creation_loop.rs b/core/src/block_creation_loop.rs index 078bcd287f9..699163e3f11 100644 --- a/core/src/block_creation_loop.rs +++ b/core/src/block_creation_loop.rs @@ -1342,7 +1342,7 @@ mod tests { super::*, crate::banking_trace::BankingTracer, agave_banking_stage_ingress_types::BankingPacketReceiver, - crossbeam_channel::unbounded, + crossbeam_channel::bounded, solana_bls_signatures::{BLS_SIGNATURE_AFFINE_SIZE, Signature as BLSSignature}, solana_entry::{block_component::VersionedUpdateParent, entry_or_marker::EntryOrMarker}, solana_keypair::Keypair, @@ -1536,7 +1536,7 @@ mod tests { let poh_recorder = Arc::new(RwLock::new(poh_recorder)); let (_record_sender, record_receiver) = record_channels(false); - let (_leader_window_info_sender, leader_window_info_receiver) = unbounded(); + let (_leader_window_info_sender, leader_window_info_receiver) = bounded(1024); let (banking_stage_sender, _banking_stage_receiver) = BankingTracer::channel_for_test(); let bank_forks_controller = test_bank_forks_controller(bank_forks.clone()); let (reward_certs_requestor, _receiver) = CertsRequestor::new(); @@ -1579,8 +1579,8 @@ mod tests { let in_flight_commit = bank.freeze_lock(); ctx.record_receiver.shutdown(); for _ in ctx.record_receiver.drain_after_shutdown() {} - let (abort_started_sender, abort_started_receiver) = unbounded(); - let (abort_done_sender, abort_done_receiver) = unbounded(); + let (abort_started_sender, abort_started_receiver) = bounded(1024); + let (abort_done_sender, abort_done_receiver) = bounded(1024); std::thread::scope(|scope| { scope.spawn(|| { abort_started_sender.send(()).unwrap(); @@ -1652,7 +1652,7 @@ mod tests { let poh_recorder = Arc::new(RwLock::new(poh_recorder)); let (_record_sender, record_receiver) = record_channels(false); - let (_leader_window_info_sender, leader_window_info_receiver) = unbounded(); + let (_leader_window_info_sender, leader_window_info_receiver) = bounded(1024); let (banking_stage_sender, _banking_stage_receiver) = BankingTracer::channel_for_test(); let mut genesis_cert_block_marker = test_genesis_cert_block_marker(); genesis_cert_block_marker.slot = parent_bank.slot(); @@ -1729,7 +1729,7 @@ mod tests { let poh_recorder = Arc::new(RwLock::new(poh_recorder)); let (record_sender, record_receiver) = record_channels(false); - let (_leader_window_info_sender, leader_window_info_receiver) = unbounded(); + let (_leader_window_info_sender, leader_window_info_receiver) = bounded(1024); let (banking_stage_sender, _banking_stage_receiver) = BankingTracer::channel_for_test(); let bank_forks_controller = test_bank_forks_controller(bank_forks.clone()); let (reward_certs_requestor, _reward_request_receiver) = CertsRequestor::new(); @@ -1838,7 +1838,7 @@ mod tests { let poh_recorder = Arc::new(RwLock::new(poh_recorder)); let (record_sender, record_receiver) = record_channels(false); - let (leader_window_info_sender, leader_window_info_receiver) = unbounded(); + let (leader_window_info_sender, leader_window_info_receiver) = bounded(1024); let (banking_stage_sender, banking_stage_receiver) = BankingTracer::channel_for_test(); let bank_forks_controller = test_bank_forks_controller(bank_forks.clone()); let (reward_certs_requestor, _receiver) = CertsRequestor::new(); diff --git a/core/src/bls_sigverify/bls_sigverifier.rs b/core/src/bls_sigverify/bls_sigverifier.rs index fb620beb366..f07cdc96a25 100644 --- a/core/src/bls_sigverify/bls_sigverifier.rs +++ b/core/src/bls_sigverify/bls_sigverifier.rs @@ -343,7 +343,7 @@ mod tests { vote::Vote, }, bitvec::prelude::{BitVec, Lsb0}, - crossbeam_channel::{Receiver, TryRecvError}, + crossbeam_channel::{Receiver, TryRecvError, bounded}, solana_bls_signatures::{BLS_SIGNATURE_AFFINE_SIZE, Signature}, solana_epoch_schedule::EpochSchedule, solana_gossip::contact_info::ContactInfo, @@ -383,7 +383,7 @@ mod tests { impl TestContext { fn new() -> Self { - let (channel_to_pool, pool_receiver) = crossbeam_channel::unbounded(); + let (channel_to_pool, pool_receiver) = bounded(1024); Self::new_with_pool_channel(channel_to_pool, pool_receiver) } @@ -417,10 +417,10 @@ mod tests { let leader_schedule = Arc::new(LeaderScheduleCache::new_from_bank(&sharable_banks.root())); - let (channel_to_repair, repair_receiver) = crossbeam_channel::unbounded(); - let (channel_to_reward, reward_receiver) = crossbeam_channel::unbounded(); - let (packet_sender, packet_receiver) = crossbeam_channel::unbounded(); - let (channel_to_metrics, metrics_receiver) = crossbeam_channel::unbounded(); + let (channel_to_repair, repair_receiver) = bounded(1024); + let (channel_to_reward, reward_receiver) = bounded(1024); + let (packet_sender, packet_receiver) = bounded(1024); + let (channel_to_metrics, metrics_receiver) = bounded(1024); let generated_cert_types = Arc::new(GeneratedCertTypes::default()); let banlist = new_test_banlist(); @@ -1350,10 +1350,10 @@ mod tests { #[test] fn test_verify_old_vote_and_cert() { - let (message_sender, message_receiver) = crossbeam_channel::unbounded(); - let (votes_for_repair_sender, _) = crossbeam_channel::unbounded(); - let (consensus_metrics_sender, _) = crossbeam_channel::unbounded(); - let (reward_votes_sender, _reward_votes_receiver) = crossbeam_channel::unbounded(); + let (message_sender, message_receiver) = bounded(1024); + let (votes_for_repair_sender, _) = bounded(1024); + let (consensus_metrics_sender, _) = bounded(1024); + let (reward_votes_sender, _reward_votes_receiver) = bounded(1024); let validator_keypairs = (0..10) .map(|_| ValidatorVoteKeypairs::new_rand()) .collect::>(); @@ -1381,7 +1381,7 @@ mod tests { SocketAddrSpace::Unspecified, )); let leader_schedule = Arc::new(LeaderScheduleCache::new_from_bank(&sharable_banks.root())); - let (_packet_sender, packet_receiver) = crossbeam_channel::unbounded(); + let (_packet_sender, packet_receiver) = bounded(1024); let mut sig_verifier = SigVerifier::new( SigVerifierContext { migration_status: Arc::new(MigrationStatus::default()), diff --git a/core/src/cluster_info_vote_listener.rs b/core/src/cluster_info_vote_listener.rs index 2c95f696d70..1b41eb3ac48 100644 --- a/core/src/cluster_info_vote_listener.rs +++ b/core/src/cluster_info_vote_listener.rs @@ -997,6 +997,7 @@ mod tests { use { super::*, crate::sigverify::GossipVerifiedVoteBatch, + crossbeam_channel::bounded, itertools::Itertools, solana_hash::Hash, solana_keypair::Keypair, @@ -1053,8 +1054,8 @@ mod tests { votes: Vec, sharable_banks: &SharableBanks, ) -> (Vec, Vec) { - let (worker_sender, _worker_receiver) = unbounded(); - let (verified_vote_sender, verified_vote_receiver) = unbounded(); + let (worker_sender, _worker_receiver) = bounded(1024); + let (verified_vote_sender, verified_vote_receiver) = bounded(1024); let mut gossip_sigverify_handle = GossipSigVerifyHandle::new_for_tests(worker_sender, verified_vote_receiver); @@ -1160,10 +1161,10 @@ mod tests { subscriptions, .. } = setup(); - let (votes_sender, votes_receiver) = unbounded(); - let (verified_voter_slots_sender, _verified_voter_slots_receiver) = unbounded(); - let (gossip_verified_vote_hash_sender, _gossip_verified_vote_hash_receiver) = unbounded(); - let (replay_votes_sender, replay_votes_receiver) = unbounded(); + let (votes_sender, votes_receiver) = bounded(1024); + let (verified_voter_slots_sender, _verified_voter_slots_receiver) = bounded(1024); + let (gossip_verified_vote_hash_sender, _gossip_verified_vote_hash_receiver) = bounded(1024); + let (replay_votes_sender, replay_votes_receiver) = bounded(1024); let mut latest_vote_slot_per_validator = HashMap::new(); let GenesisConfigInfo { genesis_config, .. } = @@ -1288,10 +1289,10 @@ mod tests { bank: bank0, .. } = setup(); - let (votes_txs_sender, votes_txs_receiver) = unbounded(); - let (replay_votes_sender, replay_votes_receiver) = unbounded(); - let (gossip_verified_vote_hash_sender, gossip_verified_vote_hash_receiver) = unbounded(); - let (verified_voter_slots_sender, verified_voter_slots_receiver) = unbounded(); + let (votes_txs_sender, votes_txs_receiver) = bounded(1024); + let (replay_votes_sender, replay_votes_receiver) = bounded(1024); + let (gossip_verified_vote_hash_sender, gossip_verified_vote_hash_receiver) = bounded(1024); + let (verified_voter_slots_sender, verified_voter_slots_receiver) = bounded(1024); let mut latest_vote_slot_per_validator = HashMap::new(); let gossip_vote_slots = vec![1, 2]; @@ -1437,10 +1438,10 @@ mod tests { } = setup(); // Send some votes to process - let (votes_txs_sender, votes_txs_receiver) = unbounded(); - let (gossip_verified_vote_hash_sender, _gossip_verified_vote_hash_receiver) = unbounded(); - let (verified_voter_slots_sender, verified_voter_slots_receiver) = unbounded(); - let (_replay_votes_sender, replay_votes_receiver) = unbounded(); + let (votes_txs_sender, votes_txs_receiver) = bounded(1024); + let (gossip_verified_vote_hash_sender, _gossip_verified_vote_hash_receiver) = bounded(1024); + let (verified_voter_slots_sender, verified_voter_slots_receiver) = bounded(1024); + let (_replay_votes_sender, replay_votes_receiver) = bounded(1024); let mut latest_vote_slot_per_validator = HashMap::new(); let mut expected_voter_slots = vec![]; @@ -1530,11 +1531,11 @@ mod tests { } fn run_test_process_votes3(switch_proof_hash: Option) { - let (votes_sender, votes_receiver) = unbounded(); - let (verified_voter_slots_sender, _verified_voter_slots_receiver) = unbounded(); - let (gossip_verified_vote_hash_sender, _gossip_verified_vote_hash_receiver) = unbounded(); + let (votes_sender, votes_receiver) = bounded(1024); + let (verified_voter_slots_sender, _verified_voter_slots_receiver) = bounded(1024); + let (gossip_verified_vote_hash_sender, _gossip_verified_vote_hash_receiver) = bounded(1024); let (replay_votes_sender, replay_votes_receiver): (ReplayVoteSender, ReplayVoteReceiver) = - unbounded(); + bounded(1024); let mut latest_vote_slot_per_validator = HashMap::new(); let vote_slot = 1; @@ -1933,8 +1934,8 @@ mod tests { None, )]; - let (verified_voter_slots_sender, _verified_voter_slots_receiver) = unbounded(); - let (gossip_verified_vote_hash_sender, _gossip_verified_vote_hash_receiver) = unbounded(); + let (verified_voter_slots_sender, _verified_voter_slots_receiver) = bounded(1024); + let (gossip_verified_vote_hash_sender, _gossip_verified_vote_hash_receiver) = bounded(1024); let notifiers = ConfirmationNotifiers { gossip_verified_vote_hash_sender: gossip_verified_vote_hash_sender.clone(), verified_voter_slots_sender: verified_voter_slots_sender.clone(), @@ -2183,8 +2184,8 @@ mod tests { )); let mut latest_vote_slot_per_validator = HashMap::new(); - let (verified_voter_slots_sender, _verified_voter_slots_receiver) = unbounded(); - let (gossip_verified_vote_hash_sender, _gossip_verified_vote_hash_receiver) = unbounded(); + let (verified_voter_slots_sender, _verified_voter_slots_receiver) = bounded(1024); + let (gossip_verified_vote_hash_sender, _gossip_verified_vote_hash_receiver) = bounded(1024); let mut diff = HashMap::default(); let mut new_optimistic_confirmed_slots = vec![]; diff --git a/core/src/forwarding_stage.rs b/core/src/forwarding_stage.rs index 59f99145e43..5b380707f16 100644 --- a/core/src/forwarding_stage.rs +++ b/core/src/forwarding_stage.rs @@ -765,7 +765,7 @@ fn initial_packet_meta_filter(meta: &packet::Meta) -> bool { mod tests { use { super::*, - crossbeam_channel::unbounded, + crossbeam_channel::bounded, packet::PacketFlags, solana_hash::Hash, solana_keypair::Keypair, @@ -842,7 +842,7 @@ mod tests { #[test] fn test_forwarding() { - let (packet_batch_sender, packet_batch_receiver) = unbounded(); + let (packet_batch_sender, packet_batch_receiver) = bounded(1024); let (_bank, bank_forks) = Bank::new_with_bank_forks_for_tests(&create_genesis_config(1).genesis_config); diff --git a/core/src/repair/ancestor_hashes_service.rs b/core/src/repair/ancestor_hashes_service.rs index c17d607b07e..a49f79cb9d1 100644 --- a/core/src/repair/ancestor_hashes_service.rs +++ b/core/src/repair/ancestor_hashes_service.rs @@ -915,7 +915,7 @@ mod test { #[test] pub fn test_ancestor_hashes_service_process_replay_updates() { let (ancestor_hashes_replay_update_sender, ancestor_hashes_replay_update_receiver) = - unbounded(); + bounded(1024); let ancestor_hashes_request_statuses = DashMap::new(); let mut dead_slot_pool = HashSet::new(); let mut repairable_dead_slot_pool = HashSet::new(); @@ -1056,7 +1056,7 @@ mod test { #[test] pub fn test_ancestor_hashes_service_process_pruned_replay_updates() { let (ancestor_hashes_replay_update_sender, ancestor_hashes_replay_update_receiver) = - unbounded(); + bounded(1024); let ancestor_hashes_request_statuses = DashMap::new(); let mut dead_slot_pool = HashSet::new(); let mut repairable_dead_slot_pool = HashSet::new(); @@ -1259,8 +1259,8 @@ mod test { // Set up thread to give us responses let exit = Arc::new(AtomicBool::new(false)); - let (requests_sender, requests_receiver) = unbounded(); - let (response_sender, response_receiver) = unbounded(); + let (requests_sender, requests_receiver) = bounded(1024); + let (response_sender, response_receiver) = bounded(1024); // Create slots [slot - MAX_ANCESTOR_RESPONSES, slot) with 5 shreds apiece let (shreds, _) = make_many_slot_entries( @@ -1354,7 +1354,8 @@ mod test { bank_forks_r.migration_status(), ) }; - let (ancestor_duplicate_slots_sender, _ancestor_duplicate_slots_receiver) = unbounded(); + let (ancestor_duplicate_slots_sender, _ancestor_duplicate_slots_receiver) = + bounded(1024); let repair_info = RepairInfo { bank_forks, cluster_info: requester_cluster_info, @@ -1366,8 +1367,8 @@ mod test { }; let (ancestor_hashes_replay_update_sender, ancestor_hashes_replay_update_receiver) = - unbounded(); - let (retryable_slots_sender, retryable_slots_receiver) = unbounded(); + bounded(1024); + let (retryable_slots_sender, retryable_slots_receiver) = bounded(1024); Self { ancestor_hashes_request_statuses, ancestor_hashes_request_socket, @@ -1978,7 +1979,7 @@ mod test { ref cluster_slots, .. } = repair_info; - let (dumped_slots_sender, _dumped_slots_receiver) = unbounded(); + let (dumped_slots_sender, _dumped_slots_receiver) = bounded(1024); // Add the responder to the eligible list for requests let responder_id = *responder_info.pubkey(); @@ -2204,7 +2205,7 @@ mod test { #[test] fn test_process_replay_updates_continue_after_skipped_update() { - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); let ancestor_hashes_request_statuses = DashMap::new(); let mut dead_slot_pool = HashSet::new(); let mut repairable_dead_slot_pool = HashSet::new(); diff --git a/core/src/repair/cluster_slot_state_verifier.rs b/core/src/repair/cluster_slot_state_verifier.rs index 63b0de897ed..6e7764b32ed 100644 --- a/core/src/repair/cluster_slot_state_verifier.rs +++ b/core/src/repair/cluster_slot_state_verifier.rs @@ -954,7 +954,7 @@ mod test { use { super::*, crate::{consensus::progress_map::ProgressMap, replay_stage::tests::setup_forks_from_tree}, - crossbeam_channel::unbounded, + crossbeam_channel::bounded, solana_runtime::bank_forks::BankForks, std::{ collections::{HashMap, HashSet}, @@ -1616,7 +1616,7 @@ mod test { .unwrap() .hash(); let (ancestor_hashes_replay_update_sender, _ancestor_hashes_replay_update_receiver) = - unbounded(); + bounded(1024); apply_state_changes( duplicate_slot, &mut heaviest_subtree_fork_choice, @@ -1699,7 +1699,7 @@ mod test { // BankFrozen should mark it down in Blockstore. assert!(blockstore.get_bank_hash(duplicate_slot).is_none()); let (ancestor_hashes_replay_update_sender, _ancestor_hashes_replay_update_receiver) = - unbounded(); + bounded(1024); apply_state_changes( duplicate_slot, &mut heaviest_subtree_fork_choice, @@ -1780,7 +1780,7 @@ mod test { )]; modify_state_changes(our_duplicate_slot_hash, &mut state_changes); let (ancestor_hashes_replay_update_sender, _ancestor_hashes_replay_update_receiver) = - unbounded(); + bounded(1024); apply_state_changes( duplicate_slot, &mut heaviest_subtree_fork_choice, @@ -1864,7 +1864,7 @@ mod test { || initial_bank_hash, ); let (ancestor_hashes_replay_update_sender, _ancestor_hashes_replay_update_receiver) = - unbounded(); + bounded(1024); check_slot_agrees_with_cluster( duplicate_slot, root, @@ -1976,7 +1976,7 @@ mod test { || Some(slot2_hash), ); let (ancestor_hashes_replay_update_sender, _ancestor_hashes_replay_update_receiver) = - unbounded(); + bounded(1024); check_slot_agrees_with_cluster( 2, root, @@ -2098,7 +2098,7 @@ mod test { || Some(slot2_hash), ); let (ancestor_hashes_replay_update_sender, _ancestor_hashes_replay_update_receiver) = - unbounded(); + bounded(1024); check_slot_agrees_with_cluster( 2, root, @@ -2222,7 +2222,7 @@ mod test { || Some(slot3_hash), ); let (ancestor_hashes_replay_update_sender, _ancestor_hashes_replay_update_receiver) = - unbounded(); + bounded(1024); check_slot_agrees_with_cluster( 3, root, @@ -2308,7 +2308,7 @@ mod test { false, ); let (ancestor_hashes_replay_update_sender, _ancestor_hashes_replay_update_receiver) = - unbounded(); + bounded(1024); check_slot_agrees_with_cluster( 3, root, @@ -2338,7 +2338,7 @@ mod test { || Some(slot3_hash), ); let (ancestor_hashes_replay_update_sender, _ancestor_hashes_replay_update_receiver) = - unbounded(); + bounded(1024); check_slot_agrees_with_cluster( 3, root, @@ -2402,7 +2402,7 @@ mod test { false, ); let (ancestor_hashes_replay_update_sender, _ancestor_hashes_replay_update_receiver) = - unbounded(); + bounded(1024); check_slot_agrees_with_cluster( 3, root, diff --git a/core/src/repair/serve_repair.rs b/core/src/repair/serve_repair.rs index ae6c539fcef..1b39d1ad414 100644 --- a/core/src/repair/serve_repair.rs +++ b/core/src/repair/serve_repair.rs @@ -1940,6 +1940,7 @@ mod tests { super::*, crate::repair::repair_response, agave_feature_set::FeatureSet, + crossbeam_channel::bounded, solana_gossip::{contact_info::ContactInfo, socketaddr, socketaddr_any}, solana_hash::Hash, solana_keypair::Keypair, @@ -2545,8 +2546,7 @@ mod tests { .root_bank() .epoch_schedule() .clone(); - let (ancestor_duplicate_slots_sender, _ancestor_duplicate_slots_receiver) = - crossbeam_channel::unbounded(); + let (ancestor_duplicate_slots_sender, _ancestor_duplicate_slots_receiver) = bounded(1024); RepairInfo { bank_forks, cluster_info, diff --git a/core/src/replay_stage/tests.rs b/core/src/replay_stage/tests.rs index f3081a06f90..b19cc2ecee2 100644 --- a/core/src/replay_stage/tests.rs +++ b/core/src/replay_stage/tests.rs @@ -19,7 +19,7 @@ use { ConfirmationProgress, ProcessOptions, confirm_full_slot, fill_blockstore_slot_with_ticks, process_bank_0, }, - crossbeam_channel::{bounded, unbounded}, + crossbeam_channel::bounded, itertools::Itertools, solana_account::{ReadableAccount, state_traits::StateMut}, solana_accounts_db::accounts_db::{ACCOUNTS_DB_CONFIG_FOR_TESTING, AccountsDbConfig}, @@ -93,11 +93,10 @@ impl ProcessActiveBanksContext { blockstore: Arc, replay_vote_sender: ReplayVoteSender, ) -> Self { - use crossbeam_channel::unbounded; - let (cluster_slots_update_sender, _) = unbounded(); - let (cost_update_sender, _) = unbounded(); - let (ancestor_hashes_replay_update_sender, _) = unbounded(); - let (votor_event_sender, _) = unbounded(); + let (cluster_slots_update_sender, _) = bounded(1024); + let (cost_update_sender, _) = bounded(1024); + let (ancestor_hashes_replay_update_sender, _) = bounded(1024); + let (votor_event_sender, _) = bounded(1024); let migration_status = Arc::new(MigrationStatus::default()); let replay_tx_thread_pool = rayon::ThreadPoolBuilder::new() .num_threads(1) @@ -535,7 +534,7 @@ fn test_handle_new_root() { .into_iter() .map(|slot| (slot, Hash::default())) .collect(); - let (drop_bank_sender, _drop_bank_receiver) = unbounded(); + let (drop_bank_sender, _drop_bank_receiver) = bounded(1024); let mut tbft_structs = TowerBFTStructures { heaviest_subtree_fork_choice, duplicate_slots_tracker, @@ -626,7 +625,7 @@ fn test_handle_new_root_ahead_of_highest_super_majority_root() { ForkProgress::new(Hash::default(), None, None, 0, 0, None), ); } - let (drop_bank_sender, _drop_bank_receiver) = unbounded(); + let (drop_bank_sender, _drop_bank_receiver) = bounded(1024); let mut tbft_structs = TowerBFTStructures { heaviest_subtree_fork_choice: HeaviestSubtreeForkChoice::new((root, root_hash)), duplicate_slots_tracker: DuplicateSlotsTracker::default(), @@ -958,8 +957,8 @@ fn do_test_dead_slot_on_complete_bank(failure: CompleteBankFailure) { let bank = bank_forks.write().unwrap().insert(child_bank); let slot = bank.slot(); - let (replay_vote_sender, _replay_vote_receiver) = unbounded(); - let (finalization_cert_sender, _finalization_cert_receiver) = unbounded(); + let (replay_vote_sender, _replay_vote_receiver) = bounded(1024); + let (finalization_cert_sender, _finalization_cert_receiver) = bounded(1024); let process_active_banks_context = ProcessActiveBanksContext::new_for_tests( bank_forks.clone(), blockstore.clone(), @@ -1084,7 +1083,7 @@ fn test_cmr_mismatch_hard_dead() { ForkProgress::new(bank.last_blockhash(), Some(0), None, 0, 0, None), ); - let (replay_vote_sender, replay_vote_receiver) = unbounded(); + let (replay_vote_sender, replay_vote_receiver) = bounded(1024); let process_active_banks_context = ProcessActiveBanksContext::new_for_tests( bank_forks.clone(), blockstore.clone(), @@ -1146,7 +1145,7 @@ fn test_abandon_invalidates() { ForkProgress::new(bank.last_blockhash(), Some(0), None, 0, 0, None), ); - let (replay_vote_sender, replay_vote_receiver) = unbounded(); + let (replay_vote_sender, replay_vote_receiver) = bounded(1024); let process_active_banks_context = ProcessActiveBanksContext::new_for_tests( bank_forks.clone(), blockstore, @@ -1224,8 +1223,8 @@ where blockstore.insert_shreds(shreds, None, false).unwrap(); let block_commitment_cache = Arc::new(RwLock::new(BlockCommitmentCache::default())); let exit = Arc::new(AtomicBool::new(false)); - let (replay_vote_sender, _replay_vote_receiver) = unbounded(); - let (finalization_cert_sender, _finalization_cert_receiver) = unbounded(); + let (replay_vote_sender, _replay_vote_receiver) = bounded(1024); + let (finalization_cert_sender, _finalization_cert_receiver) = bounded(1024); let process_active_banks_context = ProcessActiveBanksContext::new_for_tests( bank_forks.clone(), blockstore.clone(), @@ -1262,7 +1261,7 @@ where block_commitment_cache, OptimisticallyConfirmedBank::locked_from_bank_forks_root(&bank_forks), )); - let (replay_vote_sender, replay_vote_receiver) = unbounded(); + let (replay_vote_sender, replay_vote_receiver) = bounded(1024); let dead_slots = Arc::new(Mutex::new(HashSet::default())); let slot_status_notifier: Option = Some(Arc::new(RwLock::new( @@ -2843,7 +2842,7 @@ fn test_update_parent_restart() { progress.insert(slot, p); } - let (tx, rx) = crossbeam_channel::unbounded(); + let (tx, rx) = bounded(1024); for slot in [4, 8] { let parent_block_id = Hash::new_unique(); insert_update_parent_slot( @@ -2858,7 +2857,7 @@ fn test_update_parent_restart() { } let cleared_bank_id = bank_forks.read().unwrap().get(4).unwrap().bank_id(); - let (replay_vote_sender, replay_vote_receiver) = unbounded(); + let (replay_vote_sender, replay_vote_receiver) = bounded(1024); let mut async_verification_freelist = Vec::new(); handle_update_parent_interrupts( &Pubkey::new_unique(), @@ -2986,10 +2985,10 @@ fn test_update_parent_tower_gated() { p.replay_progress.write().unwrap().num_shreds = 5; progress.insert(slot, p); - let (tx, rx) = crossbeam_channel::unbounded(); + let (tx, rx) = bounded(1024); tx.send(UpdateParentSignal { slot }).unwrap(); - let (replay_vote_sender, _replay_vote_receiver) = unbounded(); + let (replay_vote_sender, _replay_vote_receiver) = bounded(1024); let mut async_verification_freelist = Vec::new(); handle_update_parent_interrupts( &Pubkey::new_unique(), @@ -3030,10 +3029,10 @@ fn test_update_parent_interrupt_ignores_non_first_leader_window_slot() { p.replay_progress.write().unwrap().num_shreds = 5; progress.insert(slot, p); - let (tx, rx) = crossbeam_channel::unbounded(); + let (tx, rx) = bounded(1024); tx.send(UpdateParentSignal { slot }).unwrap(); - let (replay_vote_sender, _replay_vote_receiver) = unbounded(); + let (replay_vote_sender, _replay_vote_receiver) = bounded(1024); let mut async_verification_freelist = Vec::new(); handle_update_parent_interrupts( &Pubkey::new_unique(), @@ -3077,10 +3076,10 @@ fn test_update_parent_keeps_hard() { p.mark_dead(DeadSlotReason::Hard); progress.insert(slot, p); - let (tx, rx) = crossbeam_channel::unbounded(); + let (tx, rx) = bounded(1024); tx.send(UpdateParentSignal { slot }).unwrap(); - let (replay_vote_sender, _replay_vote_receiver) = unbounded(); + let (replay_vote_sender, _replay_vote_receiver) = bounded(1024); let mut async_verification_freelist = Vec::new(); handle_update_parent_interrupts( &Pubkey::new_unique(), @@ -3122,8 +3121,8 @@ fn test_pre_update_soft_dead() { p.replay_progress.write().unwrap().num_shreds = 5; progress.insert(slot, p); - let (replay_vote_sender, replay_vote_receiver) = unbounded(); - let (ancestor_hashes_replay_update_sender, _) = unbounded(); + let (replay_vote_sender, replay_vote_receiver) = bounded(1024); + let (ancestor_hashes_replay_update_sender, _) = bounded(1024); let mut duplicate_slots_to_repair = DuplicateSlotsToRepair::default(); let mut purge_repair_slot_counter = PurgeRepairSlotCounter::default(); let migration_status = post_migration_status_for_tests(); @@ -3181,8 +3180,8 @@ fn test_after_update_hard_dead() { p.replay_progress.write().unwrap().num_shreds = 5; progress.insert(slot, p); - let (replay_vote_sender, _replay_vote_receiver) = unbounded(); - let (ancestor_hashes_replay_update_sender, _) = unbounded(); + let (replay_vote_sender, _replay_vote_receiver) = bounded(1024); + let (ancestor_hashes_replay_update_sender, _) = bounded(1024); let mut duplicate_slots_to_repair = DuplicateSlotsToRepair::default(); let mut purge_repair_slot_counter = PurgeRepairSlotCounter::default(); let migration_status = post_migration_status_for_tests(); @@ -3244,8 +3243,8 @@ fn test_before_update_soft_dead() { p.replay_progress.write().unwrap().num_shreds = 5; progress.insert(slot, p); - let (replay_vote_sender, _replay_vote_receiver) = unbounded(); - let (ancestor_hashes_replay_update_sender, _) = unbounded(); + let (replay_vote_sender, _replay_vote_receiver) = bounded(1024); + let (ancestor_hashes_replay_update_sender, _) = bounded(1024); let mut duplicate_slots_to_repair = DuplicateSlotsToRepair::default(); let mut purge_repair_slot_counter = PurgeRepairSlotCounter::default(); let migration_status = post_migration_status_for_tests(); @@ -3299,7 +3298,7 @@ fn test_soft_dead_restarts() { p.mark_dead(DeadSlotReason::ReplayFailureBeforeUpdateParent); progress.insert(slot, p); - let (replay_vote_sender, _replay_vote_receiver) = unbounded(); + let (replay_vote_sender, _replay_vote_receiver) = bounded(1024); let mut async_verification_freelist = Vec::new(); process_soft_dead_slots( &Pubkey::new_unique(), @@ -3342,7 +3341,7 @@ fn test_full_soft_dead_hardens() { p.mark_dead(DeadSlotReason::ReplayFailureBeforeUpdateParent); progress.insert(slot, p); - let (replay_vote_sender, _replay_vote_receiver) = unbounded(); + let (replay_vote_sender, _replay_vote_receiver) = bounded(1024); let mut async_verification_freelist = Vec::new(); process_soft_dead_slots( &Pubkey::new_unique(), @@ -3727,7 +3726,7 @@ fn test_unconfirmed_duplicate_slots_and_lockouts_for_non_heaviest_fork() { || Some(bank5_hash), ); let (ancestor_hashes_replay_update_sender, _ancestor_hashes_replay_update_receiver) = - unbounded(); + bounded(1024); check_slot_agrees_with_cluster( 5, bank_forks.read().unwrap().root(), @@ -3938,7 +3937,7 @@ fn test_unconfirmed_duplicate_slots_and_lockouts() { || Some(bank4_hash), ); let (ancestor_hashes_replay_update_sender, _ancestor_hashes_replay_update_receiver) = - unbounded(); + bounded(1024); check_slot_agrees_with_cluster( 4, bank_forks.read().unwrap().root(), @@ -4073,7 +4072,7 @@ fn test_dump_then_repair_correct_slots() { duplicate_slots_to_repair.insert(1, Hash::new_unique()); duplicate_slots_to_repair.insert(2, Hash::new_unique()); let mut purge_repair_slot_counter = PurgeRepairSlotCounter::default(); - let (dumped_slots_sender, dumped_slots_receiver) = unbounded(); + let (dumped_slots_sender, dumped_slots_receiver) = bounded(1024); let should_be_dumped = duplicate_slots_to_repair .iter() .map(|(&s, &h)| (s, h)) @@ -4176,7 +4175,7 @@ fn setup_vote_then_rollback( || Some(our_bank2_hash), ); let (ancestor_hashes_replay_update_sender, _ancestor_hashes_replay_update_receiver) = - unbounded(); + bounded(1024); check_slot_agrees_with_cluster( 2, bank_forks.read().unwrap().root(), @@ -4196,7 +4195,7 @@ fn setup_vote_then_rollback( let mut ancestors = bank_forks.read().unwrap().ancestors(); let mut descendants = bank_forks.read().unwrap().descendants(); let old_descendants_of_2 = descendants.get(&2).unwrap().clone(); - let (dumped_slots_sender, _dumped_slots_receiver) = unbounded(); + let (dumped_slots_sender, _dumped_slots_receiver) = bounded(1024); ReplayStage::dump_then_repair_correct_slots( &mut duplicate_slots_to_repair, @@ -4487,7 +4486,7 @@ fn test_gossip_vote_doesnt_affect_fork_choice() { let vote_pubkey = vote_pubkeys[0]; let mut unfrozen_gossip_verified_vote_hashes = UnfrozenGossipVerifiedVoteHashes::default(); - let (gossip_verified_vote_hash_sender, gossip_verified_vote_hash_receiver) = unbounded(); + let (gossip_verified_vote_hash_sender, gossip_verified_vote_hash_receiver) = bounded(1024); // Best slot is 4 assert_eq!( @@ -4575,7 +4574,7 @@ fn test_replay_stage_refresh_last_vote() { ), ); - let (voting_sender, voting_receiver) = unbounded(); + let (voting_sender, voting_receiver) = bounded(1024); // Simulate landing a vote for slot 0 landing in slot 1 let bank1 = Bank::new_from_parent_with_bank_forks( @@ -5104,7 +5103,7 @@ fn test_replay_stage_last_vote_outside_slot_hashes() { ) }); - let (voting_sender, voting_receiver) = unbounded(); + let (voting_sender, voting_receiver) = bounded(1024); let mut cursor = Cursor::default(); let mut new_bank = send_vote_in_new_bank( @@ -5272,7 +5271,7 @@ fn test_retransmit_latest_unpropagated_leader_slot() { } = vote_simulator; let poh_recorder = Arc::new(poh_recorder); - let (retransmit_slots_sender, retransmit_slots_receiver) = unbounded(); + let (retransmit_slots_sender, retransmit_slots_receiver) = bounded(1024); let bank1 = Bank::new_from_parent( bank_forks.read().unwrap().get(0).unwrap(), @@ -5446,7 +5445,7 @@ fn test_maybe_retransmit_unpropagated_slots() { .. } = vote_simulator; - let (retransmit_slots_sender, retransmit_slots_receiver) = unbounded(); + let (retransmit_slots_sender, retransmit_slots_receiver) = bounded(1024); let retry_time = Instant::now() .checked_sub(Duration::from_millis(RETRANSMIT_BASE_DELAY_MS + 1)) .unwrap(); @@ -5534,7 +5533,7 @@ fn test_dumped_slot_not_causing_panic() { } = vote_simulator; let poh_recorder = Arc::new(poh_recorder); - let (retransmit_slots_sender, _) = unbounded(); + let (retransmit_slots_sender, _) = bounded(1024); // Use a bank slot when I was not leader to avoid panic for dumping my own slot let slot_to_dump = (1..100) @@ -5590,7 +5589,7 @@ fn test_dumped_slot_not_causing_panic() { let bank_to_dump_bad_hash = Hash::new_unique(); duplicate_slots_to_repair.insert(slot_to_dump, bank_to_dump_bad_hash); let mut purge_repair_slot_counter = PurgeRepairSlotCounter::default(); - let (dumped_slots_sender, dumped_slots_receiver) = unbounded(); + let (dumped_slots_sender, dumped_slots_receiver) = bounded(1024); ReplayStage::dump_then_repair_correct_slots( &mut duplicate_slots_to_repair, @@ -5675,7 +5674,7 @@ fn test_dump_own_slots_fails() { duplicate_slots_to_repair.insert(1, Hash::new_unique()); duplicate_slots_to_repair.insert(2, Hash::new_unique()); let mut purge_repair_slot_counter = PurgeRepairSlotCounter::default(); - let (dumped_slots_sender, _) = unbounded(); + let (dumped_slots_sender, _) = bounded(1024); ReplayStage::dump_then_repair_correct_slots( &mut duplicate_slots_to_repair, @@ -6291,7 +6290,7 @@ fn test_skip_leader_slot_for_existing_slot() { .unwrap(); let poh_recorder = Arc::new(poh_recorder); - let (retransmit_slots_sender, _) = unbounded(); + let (retransmit_slots_sender, _) = bounded(1024); let (banking_tracer, _) = BankingTracer::new(None).unwrap(); // A vote has not technically been rooted, but it doesn't matter for // this test to use true to avoid skipping the leader slot @@ -6384,7 +6383,7 @@ fn test_mark_slots_duplicate_confirmed() { .. } = vote_simulator; - let (ancestor_hashes_replay_update_sender, _) = unbounded(); + let (ancestor_hashes_replay_update_sender, _) = bounded(1024); let mut duplicate_confirmed_slots = DuplicateConfirmedSlots::default(); let bank_hash_0 = bank_forks.read().unwrap().bank_hash(0).unwrap(); bank_forks.write().unwrap().set_root(1, None, None); @@ -6503,8 +6502,8 @@ fn test_process_duplicate_confirmed_slots(same_batch: bool) { .. } = vote_simulator; - let (ancestor_hashes_replay_update_sender, _) = unbounded(); - let (sender, receiver) = unbounded(); + let (ancestor_hashes_replay_update_sender, _) = bounded(1024); + let (sender, receiver) = bounded(1024); let mut duplicate_confirmed_slots = DuplicateConfirmedSlots::default(); let bank_hash_0 = bank_forks.read().unwrap().bank_hash(0).unwrap(); bank_forks.write().unwrap().set_root(1, None, None); diff --git a/core/src/result.rs b/core/src/result.rs index 65a5959ff46..f794785d481 100644 --- a/core/src/result.rs +++ b/core/src/result.rs @@ -44,12 +44,12 @@ impl std::convert::From> for Error { mod tests { use { crate::result::{Error, Result}, - crossbeam_channel::{RecvError, RecvTimeoutError, unbounded}, + crossbeam_channel::{RecvError, RecvTimeoutError, bounded}, std::{io, io::Write, panic}, }; fn send_error() -> Result<()> { - let (s, r) = unbounded(); + let (s, r) = bounded(1024); drop(r); s.send(())?; Ok(()) diff --git a/core/src/sigverify_stage.rs b/core/src/sigverify_stage.rs index d078d739767..bdbcd11a949 100644 --- a/core/src/sigverify_stage.rs +++ b/core/src/sigverify_stage.rs @@ -354,7 +354,7 @@ mod tests { use { super::*, crate::banking_trace::BankingTracer, - crossbeam_channel::unbounded, + crossbeam_channel::bounded, solana_hash::Hash, solana_keypair::Keypair, solana_message::{VersionedMessage, v1}, @@ -410,11 +410,11 @@ mod tests { let (_bank, bank_forks) = Bank::new_with_bank_forks_for_tests(&create_genesis_config(1).genesis_config); let sharable_banks = bank_forks.read().unwrap().sharable_banks(); - let (packet_s, packet_r) = unbounded(); - let (vote_packet_s, vote_packet_r) = unbounded(); + let (packet_s, packet_r) = bounded(1024); + let (vote_packet_s, vote_packet_r) = bounded(1024); let (verified_s, verified_r) = BankingTracer::channel_for_test(); let (tpu_vote_s, _tpu_vote_r) = BankingTracer::channel_for_test(); - let (forward_stage_s, _forward_stage_r) = unbounded(); + let (forward_stage_s, _forward_stage_r) = bounded(1024); let (stage, gossip_sigverify_handle) = SigVerifyStage::new( packet_r, vote_packet_r, @@ -485,11 +485,11 @@ mod tests { } let (_bank, bank_forks) = bank.wrap_with_bank_forks_for_tests(); let sharable_banks = bank_forks.read().unwrap().sharable_banks(); - let (packet_s, packet_r) = unbounded(); - let (vote_packet_s, vote_packet_r) = unbounded(); + let (packet_s, packet_r) = bounded(1024); + let (vote_packet_s, vote_packet_r) = bounded(1024); let (verified_s, verified_r) = BankingTracer::channel_for_test(); let (tpu_vote_s, _tpu_vote_r) = BankingTracer::channel_for_test(); - let (forward_stage_s, _forward_stage_r) = unbounded(); + let (forward_stage_s, _forward_stage_r) = bounded(1024); let (stage, gossip_sigverify_handle) = SigVerifyStage::new( packet_r, vote_packet_r, diff --git a/core/src/tvu.rs b/core/src/tvu.rs index 0469a562ea8..604cf319f53 100644 --- a/core/src/tvu.rs +++ b/core/src/tvu.rs @@ -780,11 +780,11 @@ pub mod tests { let vote_keypair = Keypair::new(); let leader_schedule_cache = Arc::new(LeaderScheduleCache::new_from_bank(&bank)); let block_commitment_cache = Arc::new(RwLock::new(BlockCommitmentCache::default())); - let (retransmit_slots_sender, _retransmit_slots_receiver) = unbounded(); - let (_gossip_verified_vote_hash_sender, gossip_verified_vote_hash_receiver) = unbounded(); - let (verified_voter_slots_sender, verified_voter_slots_receiver) = unbounded(); - let (replay_vote_sender, _replay_vote_receiver) = unbounded(); - let (_, gossip_confirmed_slots_receiver) = unbounded(); + let (retransmit_slots_sender, _retransmit_slots_receiver) = bounded(1024); + let (_gossip_verified_vote_hash_sender, gossip_verified_vote_hash_receiver) = bounded(1024); + let (verified_voter_slots_sender, verified_voter_slots_receiver) = bounded(1024); + let (replay_vote_sender, _replay_vote_receiver) = bounded(1024); + let (_, gossip_confirmed_slots_receiver) = bounded(1024); let max_complete_transaction_status_slot = Arc::new(AtomicU64::default()); let outstanding_repair_requests = Arc::>::default(); let cluster_slots = Arc::new(ClusterSlots::default_for_tests()); @@ -804,8 +804,8 @@ pub mod tests { DEFAULT_TPU_CONNECTION_POOL_SIZE, ); let replay_highest_frozen = Arc::new(ReplayHighestFrozen::default()); - let (leader_window_info_sender, _leader_window_info_receiver) = unbounded(); - let (optimistic_parent_sender, optimistic_parent_receiver) = unbounded(); + let (leader_window_info_sender, _leader_window_info_receiver) = bounded(1024); + let (optimistic_parent_sender, optimistic_parent_receiver) = bounded(1024); let highest_parent_ready = Arc::new(RwLock::new(( 0, Block { @@ -814,7 +814,7 @@ pub mod tests { }, ))); let (votor_event_sender, votor_event_receiver): (VotorEventSender, VotorEventReceiver) = - unbounded(); + bounded(1024); let staked_nodes = Arc::new(RwLock::new(StakedNodes::default())); let key_notifiers = Arc::new(RwLock::new(KeyUpdaters::default())); let cancel = CancellationToken::new(); @@ -832,7 +832,7 @@ pub mod tests { let (bank_forks_controller, bank_forks_controller_receiver) = BankForksControllerHandle::new(); let bank_forks_controller = Arc::new(bank_forks_controller); - let (reward_votes_sender, _reward_votes_receiver) = unbounded(); + let (reward_votes_sender, _reward_votes_receiver) = bounded(1024); let tvu = Tvu::new( &vote_keypair.pubkey(), diff --git a/core/src/vote_simulator.rs b/core/src/vote_simulator.rs index b020e44f91a..b6377f83229 100644 --- a/core/src/vote_simulator.rs +++ b/core/src/vote_simulator.rs @@ -17,7 +17,7 @@ use { replay_stage::{HeaviestForkFailures, ReplayStage, TowerBFTStructures}, unfrozen_gossip_verified_vote_hashes::UnfrozenGossipVerifiedVoteHashes, }, - crossbeam_channel::unbounded, + crossbeam_channel::bounded, solana_clock::Slot, solana_epoch_schedule::EpochSchedule, solana_hash::Hash, @@ -247,7 +247,7 @@ impl VoteSimulator { } pub fn set_root(&mut self, new_root: Slot) { - let (drop_bank_sender, _drop_bank_receiver) = unbounded(); + let (drop_bank_sender, _drop_bank_receiver) = bounded(1024); ReplayStage::handle_new_root( new_root, &self.bank_forks, diff --git a/core/src/window_service.rs b/core/src/window_service.rs index b0fd4ae6eb9..1a7088f7857 100644 --- a/core/src/window_service.rs +++ b/core/src/window_service.rs @@ -495,6 +495,7 @@ impl WindowService { mod test { use { super::*, + crossbeam_channel::bounded, rand::Rng, solana_entry::entry::{Entry, create_ticks}, solana_gossip::contact_info::ContactInfo, @@ -554,8 +555,8 @@ mod test { let genesis_config = create_genesis_config(10_000).genesis_config; let bank_forks = BankForks::new_rw_arc(Bank::new_for_tests(&genesis_config)); let blockstore = Arc::new(Blockstore::open(ledger_path.path()).unwrap()); - let (sender, receiver) = unbounded(); - let (duplicate_slot_sender, duplicate_slot_receiver) = unbounded(); + let (sender, receiver) = bounded(1024); + let (duplicate_slot_sender, duplicate_slot_receiver) = bounded(1024); let (shreds, _) = make_many_slot_entries(5, 5, 10); blockstore .insert_shreds(shreds.clone(), None, false) @@ -604,8 +605,8 @@ mod test { fn test_store_duplicate_shreds_same_batch() { let ledger_path = get_tmp_ledger_path_auto_delete!(); let blockstore = Arc::new(Blockstore::open(ledger_path.path()).unwrap()); - let (duplicate_shred_sender, duplicate_shred_receiver) = unbounded(); - let (duplicate_slot_sender, duplicate_slot_receiver) = unbounded(); + let (duplicate_shred_sender, duplicate_shred_receiver) = bounded(1024); + let (duplicate_slot_sender, duplicate_slot_receiver) = bounded(1024); let exit = Arc::new(AtomicBool::new(false)); let keypair = Keypair::new(); let contact_info = ContactInfo::new_localhost(&keypair.pubkey(), timestamp()); diff --git a/core/tests/snapshots.rs b/core/tests/snapshots.rs index 762b788e8b0..d30d51f0cb9 100644 --- a/core/tests/snapshots.rs +++ b/core/tests/snapshots.rs @@ -6,7 +6,7 @@ use { SnapshotInterval, SnapshotKind, paths as snapshot_paths, snapshot_archive_info::FullSnapshotArchiveInfo, snapshot_config::SnapshotConfig, }, - crossbeam_channel::unbounded, + crossbeam_channel::bounded, itertools::Itertools, log::{info, trace}, solana_accounts_db::accounts_db::ACCOUNTS_DB_CONFIG_FOR_TESTING, @@ -172,7 +172,7 @@ where let mint_keypair = &snapshot_test_config.genesis_config_info.mint_keypair; let pending_snapshot_packages = Arc::new(Mutex::new(PendingSnapshotPackages::default())); - let (snapshot_request_sender, snapshot_request_receiver) = unbounded(); + let (snapshot_request_sender, snapshot_request_receiver) = bounded(1024); let snapshot_controller = Arc::new(SnapshotController::new( snapshot_request_sender, snapshot_test_config.snapshot_config.clone(), @@ -276,7 +276,7 @@ fn test_slots_to_snapshot() { let num_set_roots = status_cache_max_entries * 2; for add_root_interval in &[1, 3, 9] { - let (snapshot_sender, _snapshot_receiver) = unbounded(); + let (snapshot_sender, _snapshot_receiver) = bounded(1024); // Make sure this test never clears bank.slots_since_snapshot let snapshot_test_config = SnapshotTestConfig::new( SnapshotInterval::Slots( @@ -405,7 +405,7 @@ fn test_bank_forks_incremental_snapshot() { let mint_keypair = &snapshot_test_config.genesis_config_info.mint_keypair; let pending_snapshot_packages = Arc::new(Mutex::new(PendingSnapshotPackages::default())); - let (snapshot_request_sender, snapshot_request_receiver) = unbounded(); + let (snapshot_request_sender, snapshot_request_receiver) = bounded(1024); let snapshot_controller = Arc::new(SnapshotController::new( snapshot_request_sender, snapshot_test_config.snapshot_config.clone(), @@ -594,8 +594,8 @@ fn test_snapshots_with_background_services() { SocketAddrSpace::Unspecified, )); - let (pruned_banks_sender, pruned_banks_receiver) = unbounded(); - let (snapshot_request_sender, snapshot_request_receiver) = unbounded(); + let (pruned_banks_sender, pruned_banks_receiver) = bounded(1024); + let (snapshot_request_sender, snapshot_request_receiver) = bounded(1024); let pending_snapshot_packages = Arc::new(Mutex::new(PendingSnapshotPackages::default())); let bank_forks = snapshot_test_config.bank_forks.clone(); @@ -777,7 +777,7 @@ fn test_fastboot_snapshots_teardown(exit_backpressure: bool) { SocketAddrSpace::Unspecified, )); - let (snapshot_request_sender, _snapshot_request_receiver) = unbounded(); + let (snapshot_request_sender, _snapshot_request_receiver) = bounded(1024); let pending_snapshot_packages = Arc::new(Mutex::new(PendingSnapshotPackages::default())); let bank_forks = snapshot_test_config.bank_forks.clone(); diff --git a/core/tests/unified_scheduler.rs b/core/tests/unified_scheduler.rs index 39b1f1cbd57..bc34ff94417 100644 --- a/core/tests/unified_scheduler.rs +++ b/core/tests/unified_scheduler.rs @@ -1,5 +1,5 @@ use { - crossbeam_channel::unbounded, + crossbeam_channel::bounded, itertools::Itertools, log::*, solana_core::{ @@ -109,8 +109,8 @@ fn test_scheduler_waited_by_drop_bank_service() { drop(lock_to_stall); // Create 2 channels to check actual pruned banks - let (drop_bank_sender1, drop_bank_receiver1) = unbounded(); - let (drop_bank_sender2, drop_bank_receiver2) = unbounded(); + let (drop_bank_sender1, drop_bank_receiver1) = bounded(1024); + let (drop_bank_sender2, drop_bank_receiver2) = bounded(1024); let drop_bank_service = DropBankService::new(drop_bank_receiver2); info!("calling handle_new_root()..."); From 749cd74ceba4d5f3492026458e90e6034bb313a6 Mon Sep 17 00:00:00 2001 From: Tao Zhu <82401714+tao-stones@users.noreply.github.com> Date: Wed, 10 Jun 2026 01:14:44 -0500 Subject: [PATCH 274/576] reduce vote batch size (#12963) --- core/src/banking_stage/vote_worker.rs | 32 ++++++++++++--------------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/core/src/banking_stage/vote_worker.rs b/core/src/banking_stage/vote_worker.rs index 8adee756a75..02e3fa47fc5 100644 --- a/core/src/banking_stage/vote_worker.rs +++ b/core/src/banking_stage/vote_worker.rs @@ -50,10 +50,10 @@ mod transaction { pub use solana_transaction_error::TransactionResult as Result; } -// This vote batch size was selected to balance the following two things: -// 1. Amortize execution overhead (Larger is better) -// 2. Constrain max entry size for FEC set packing (Smaller is better) -pub const UNPROCESSED_BUFFER_STEP_SIZE: usize = 16; +// Process vote packets one at a time to avoid over reserving block CUs during packing, +// also keep each recorded vote batch as small as possible, which favors entry/FEC-set +// packing. +pub const UNPROCESSED_BUFFER_STEP_SIZE: usize = 1; pub struct VoteWorker { exit: Arc, @@ -501,8 +501,8 @@ impl VoteWorker { .collect() } - fn extract_retryable( - vote_packets: &mut ArrayVec, + fn extract_retryable( + vote_packets: &mut ArrayVec, retryable_vote_indices: Vec, ) -> impl Iterator + '_ { debug_assert!(retryable_vote_indices.is_sorted()); @@ -659,11 +659,9 @@ mod tests { #[test] fn extract_retryable_one_all_retryable() { let keypair_a = ValidatorVoteKeypairs::new_rand(); - let mut packets = ArrayVec::from_iter([to_runtime_transaction_view(packet_from_slots( - vec![(1, 1)], - &keypair_a, - None, - ))]); + let mut packets: ArrayVec<_, 1> = ArrayVec::from_iter([to_runtime_transaction_view( + packet_from_slots(vec![(1, 1)], &keypair_a, None), + )]); let retryable_indices = vec![0]; // Assert - Able to extract exactly one packet. @@ -676,11 +674,9 @@ mod tests { #[test] fn extract_retryable_one_none_retryable() { let keypair_a = ValidatorVoteKeypairs::new_rand(); - let mut packets = ArrayVec::from_iter([to_runtime_transaction_view(packet_from_slots( - vec![(1, 1)], - &keypair_a, - None, - ))]); + let mut packets: ArrayVec<_, 1> = ArrayVec::from_iter([to_runtime_transaction_view( + packet_from_slots(vec![(1, 1)], &keypair_a, None), + )]); let retryable_indices = vec![]; // Assert - Able to extract exactly zero packets. @@ -691,7 +687,7 @@ mod tests { #[test] fn extract_retryable_three_last_retryable() { let keypair_a = ValidatorVoteKeypairs::new_rand(); - let mut packets = ArrayVec::from_iter( + let mut packets: ArrayVec<_, 3> = ArrayVec::from_iter( [ packet_from_slots(vec![(5, 3)], &keypair_a, None), packet_from_slots(vec![(6, 2)], &keypair_a, None), @@ -712,7 +708,7 @@ mod tests { #[test] fn extract_retryable_three_first_last_retryable() { let keypair_a = ValidatorVoteKeypairs::new_rand(); - let mut packets = ArrayVec::from_iter( + let mut packets: ArrayVec<_, 3> = ArrayVec::from_iter( [ packet_from_slots(vec![(5, 3)], &keypair_a, None), packet_from_slots(vec![(6, 2)], &keypair_a, None), From cfd4d8bf3d5f63ac8fac5aa408698ba1ba8aa39f Mon Sep 17 00:00:00 2001 From: Jon C Date: Wed, 10 Jun 2026 12:10:02 +0200 Subject: [PATCH 275/576] rpc-client: Make it work with v1 transactions (#12977) * rpc-client: Make it work with v1 transactions #### Problem v1 transactions don't work with RPC client, since it's always using bincode to serialize the transaction. #### Summary of changes Parametrize everything via wincode instead. * Add a test --------- Co-authored-by: Callum --- Cargo.lock | 2 ++ dev-bins/Cargo.lock | 1 + programs/sbf/Cargo.lock | 1 + rpc-client/Cargo.toml | 2 ++ rpc-client/src/mock_sender.rs | 4 +-- rpc-client/src/nonblocking/rpc_client.rs | 6 ++-- rpc-client/src/rpc_client.rs | 39 ++++++++++++++++++++++-- 7 files changed, 47 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0f742a61790..7b557fc1859 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9806,6 +9806,7 @@ dependencies = [ "solana-rpc-client-api", "solana-signature", "solana-signer", + "solana-system-interface 3.2.0", "solana-system-transaction", "solana-transaction", "solana-transaction-error", @@ -9815,6 +9816,7 @@ dependencies = [ "static_assertions", "test-case", "tokio", + "wincode", ] [[package]] diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 648c5d2c3e0..81f20b6bfc8 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -8264,6 +8264,7 @@ dependencies = [ "solana-version", "solana-vote-interface", "tokio", + "wincode", ] [[package]] diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index 7e529a28ea9..b4fb11c1571 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -8087,6 +8087,7 @@ dependencies = [ "solana-version", "solana-vote-interface", "tokio", + "wincode", ] [[package]] diff --git a/rpc-client/Cargo.toml b/rpc-client/Cargo.toml index a8261b3626a..c9e52e92c04 100644 --- a/rpc-client/Cargo.toml +++ b/rpc-client/Cargo.toml @@ -54,6 +54,7 @@ solana-transaction-status-client-types = { workspace = true } solana-version = { workspace = true } solana-vote-interface = { workspace = true } tokio = { workspace = true, features = ["full"] } +wincode = { workspace = true } [dev-dependencies] assert_matches = { workspace = true } @@ -66,6 +67,7 @@ solana-hash = { workspace = true, features = ["atomic"] } solana-keypair = { workspace = true } solana-pubkey = { workspace = true, features = ["rand"] } solana-signer = { workspace = true } +solana-system-interface = { workspace = true, features = ["bincode"] } solana-system-transaction = { workspace = true } static_assertions = { workspace = true } test-case = { workspace = true } diff --git a/rpc-client/src/mock_sender.rs b/rpc-client/src/mock_sender.rs index 218404cb851..3285dc3755b 100644 --- a/rpc-client/src/mock_sender.rs +++ b/rpc-client/src/mock_sender.rs @@ -34,7 +34,7 @@ use { }, }, solana_signature::Signature, - solana_transaction::{Transaction, versioned::TransactionVersion}, + solana_transaction::versioned::{TransactionVersion, VersionedTransaction}, solana_transaction_error::{TransactionError, TransactionResult}, solana_transaction_status_client_types::{ EncodedConfirmedBlock, EncodedConfirmedTransactionWithStatusMeta, EncodedTransaction, @@ -364,7 +364,7 @@ impl RpcSender for MockSender { } else { let tx_str = params.as_array().unwrap()[0].as_str().unwrap().to_string(); let data = BASE64_STANDARD.decode(tx_str).unwrap(); - let tx: Transaction = bincode::deserialize(&data).unwrap(); + let tx: VersionedTransaction = wincode::deserialize(&data).unwrap(); tx.signatures[0].to_string() }; Value::String(signature) diff --git a/rpc-client/src/nonblocking/rpc_client.rs b/rpc-client/src/nonblocking/rpc_client.rs index 43a3eb874f2..ed64d44455d 100644 --- a/rpc-client/src/nonblocking/rpc_client.rs +++ b/rpc-client/src/nonblocking/rpc_client.rs @@ -21,7 +21,6 @@ use { }, agave_votor_messages::certificate::Certificate, base64::{Engine, prelude::BASE64_STANDARD}, - bincode::serialize, futures::join, log::*, serde_json::{Value, json}, @@ -57,6 +56,7 @@ use { time::{Duration, Instant}, }, tokio::time::sleep, + wincode::{SchemaWrite, config::DefaultConfig}, }; /// A client of a remote Solana node. @@ -5111,9 +5111,9 @@ impl RpcClient { fn serialize_and_encode(input: &T, encoding: UiTransactionEncoding) -> ClientResult where - T: serde::ser::Serialize, + T: SchemaWrite, { - let serialized = serialize(input) + let serialized = wincode::serialize(input) .map_err(|e| ClientErrorKind::Custom(format!("Serialization failed: {e}")))?; let encoded = match encoding { UiTransactionEncoding::Base58 => bs58::encode(serialized).into_string(), diff --git a/rpc-client/src/rpc_client.rs b/rpc-client/src/rpc_client.rs index c17944d9e64..b1a2d547e66 100644 --- a/rpc-client/src/rpc_client.rs +++ b/rpc-client/src/rpc_client.rs @@ -18,7 +18,6 @@ use { rpc_sender::*, }, agave_votor_messages::certificate::Certificate, - serde::Serialize, serde_json::Value, solana_account::{Account, ReadableAccount}, solana_account_decoder::UiAccount, @@ -46,6 +45,7 @@ use { }, solana_vote_interface::state::MAX_LOCKOUT_HISTORY, std::{net::SocketAddr, str::FromStr, sync::Arc, time::Duration}, + wincode::{SchemaWrite, config::DefaultConfig}, }; #[derive(Default)] @@ -95,7 +95,7 @@ impl SerializableMessage for v0::Message { /// Trait used to add support for versioned transactions to RPC APIs while /// retaining backwards compatibility -pub trait SerializableTransaction: Serialize { +pub trait SerializableTransaction: SchemaWrite { fn get_signature(&self) -> &Signature; fn get_recent_blockhash(&self) -> &Hash; fn uses_durable_nonce(&self) -> bool; @@ -4372,9 +4372,12 @@ mod tests { solana_hash::Hash, solana_instruction::error::InstructionError, solana_keypair::Keypair, - solana_message::{MessageHeader, compiled_instruction::CompiledInstruction}, + solana_message::{ + MessageHeader, VersionedMessage, compiled_instruction::CompiledInstruction, v1, + }, solana_rpc_client_api::client_error::ErrorKind, solana_signer::Signer, + solana_system_interface::instruction as system_instruction, solana_system_transaction as system_transaction, solana_transaction_error::TransactionError, std::{io, thread}, @@ -5182,4 +5185,34 @@ mod tests { .unwrap(); assert_eq!(value, response.value); } + + #[test] + fn test_send_transaction_v1() { + let rpc_client = RpcClient::new_mock("succeeds".to_string()); + + let key = Keypair::new(); + let to = Pubkey::new_unique(); + let blockhash = Hash::default(); + let ix = system_instruction::transfer(&key.pubkey(), &to, 50); + let tx = VersionedTransaction::try_new( + VersionedMessage::V1( + v1::Message::try_compile(&key.pubkey(), &[ix], blockhash).unwrap(), + ), + &[key], + ) + .unwrap(); + + let signature = rpc_client.send_transaction(&tx); + assert_eq!(signature.unwrap(), tx.signatures[0]); + + let rpc_client = RpcClient::new_mock("fails".to_string()); + + let signature = rpc_client.send_transaction(&tx); + assert!(signature.is_err()); + + // Test bad signature returned from rpc node + let rpc_client = RpcClient::new_mock("malicious".to_string()); + let signature = rpc_client.send_transaction(&tx); + assert!(signature.is_err()); + } } From c8ce5a285c38cbc5fc8b1a10c9949bf5a2aaadc5 Mon Sep 17 00:00:00 2001 From: Alex Pyattaev Date: Wed, 10 Jun 2026 13:25:49 +0300 Subject: [PATCH 276/576] Rm connection cache tpu senders (#12916) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit local-cluster: replace ConnectionCache with tpu-client-next in TPU-sending functions Introduces TpuSender — a struct packaging a 4-thread tokio runtime and tpu-client-next ClientBuilder — as the synchronous interface for sending transactions in cluster tests. The runtime is created once per test and shared across multiple calls, avoiding short-lived runtimes per send. - spend_and_verify_all_nodes, send_many_transactions, and kill_entry_and_spend_and_verify_rest drop the connection_cache param in favour of &TpuSender - Private send_and_confirm_transaction helper replicates the sign→send→confirm→resign retry loop using TpuSender + RpcClient --- Cargo.lock | 3 + dev-bins/Cargo.lock | 3 + local-cluster/Cargo.toml | 12 +- local-cluster/src/cluster_tests.rs | 341 ++++++++++++++++--------- local-cluster/src/integration_tests.rs | 2 +- local-cluster/src/local_cluster.rs | 10 +- local-cluster/tests/local_cluster.rs | 23 +- 7 files changed, 253 insertions(+), 141 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7b557fc1859..fbb3f593f30 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8955,6 +8955,7 @@ dependencies = [ "solana-system-transaction", "solana-time-utils", "solana-tpu-client", + "solana-tpu-client-next", "solana-transaction", "solana-transaction-error", "solana-turbine", @@ -8965,7 +8966,9 @@ dependencies = [ "static_assertions", "strum", "tempfile", + "tokio", "tokio-util 0.7.18", + "wincode", ] [[package]] diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 81f20b6bfc8..74adc41bd41 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -7575,6 +7575,7 @@ dependencies = [ "solana-system-transaction", "solana-time-utils", "solana-tpu-client", + "solana-tpu-client-next", "solana-transaction", "solana-transaction-error", "solana-turbine", @@ -7585,7 +7586,9 @@ dependencies = [ "static_assertions", "strum", "tempfile", + "tokio", "tokio-util 0.7.18", + "wincode", ] [[package]] diff --git a/local-cluster/Cargo.toml b/local-cluster/Cargo.toml index cf9a9ab1c49..3d72c0a9957 100644 --- a/local-cluster/Cargo.toml +++ b/local-cluster/Cargo.toml @@ -14,17 +14,14 @@ targets = ["x86_64-unknown-linux-gnu"] [features] agave-unstable-api = [] -dev-context-only-utils = [ - "solana-core/dev-context-only-utils", - "agave-votor-messages/dev-context-only-utils", -] +dev-context-only-utils = [] [dependencies] agave-feature-set = { workspace = true } agave-logger = { workspace = true } agave-snapshots = { workspace = true } agave-votor = { workspace = true } -agave-votor-messages = { workspace = true } +agave-votor-messages = { workspace = true, features = ["dev-context-only-utils"] } crossbeam-channel = { workspace = true } itertools = { workspace = true } log = { workspace = true } @@ -37,7 +34,7 @@ solana-client-traits = { workspace = true } solana-clock = { workspace = true } solana-cluster-type = { workspace = true } solana-commitment-config = { workspace = true } -solana-core = { workspace = true } +solana-core = { workspace = true, features = ["dev-context-only-utils"] } solana-entry = { workspace = true } solana-epoch-schedule = { workspace = true } solana-feature-gate-interface = { workspace = true } @@ -71,6 +68,7 @@ solana-system-interface = { workspace = true } solana-system-transaction = { workspace = true } solana-time-utils = { workspace = true } solana-tpu-client = { workspace = true } +solana-tpu-client-next = { workspace = true, features = ["dev-context-only-utils"] } solana-transaction = { workspace = true } solana-transaction-error = { workspace = true } solana-turbine = { workspace = true } @@ -81,7 +79,9 @@ solana-vote-program = { workspace = true } static_assertions = { workspace = true } strum = { workspace = true, features = ["derive"] } tempfile = { workspace = true } +tokio = { workspace = true, features = ["full"] } tokio-util = { workspace = true } +wincode = { workspace = true } [dev-dependencies] assert_matches = { workspace = true } diff --git a/local-cluster/src/cluster_tests.rs b/local-cluster/src/cluster_tests.rs index 89c6cab12ba..4cade87eb80 100644 --- a/local-cluster/src/cluster_tests.rs +++ b/local-cluster/src/cluster_tests.rs @@ -12,11 +12,14 @@ use { solana_client::connection_cache::ConnectionCache, solana_clock::{self as clock, Slot}, solana_commitment_config::CommitmentConfig, + solana_core::consensus::tower_storage::{ + FileTowerStorage, SavedTower, SavedTowerVersions, TowerStorage, + }, solana_entry::entry::{self, Entry, EntrySlice}, solana_epoch_schedule::MINIMUM_SLOTS_PER_EPOCH, solana_gossip::{ cluster_info::{self, ClusterInfo}, - contact_info::ContactInfo, + contact_info::{ContactInfo, Protocol}, crds::Cursor, crds_data::{self, CrdsData}, crds_value::{CrdsValue, CrdsValueLabel}, @@ -26,7 +29,7 @@ use { solana_hash::Hash, solana_keypair::Keypair, solana_ledger::blockstore::Blockstore, - solana_net_utils::SocketAddrSpace, + solana_net_utils::{SocketAddrSpace, sockets::bind_to_localhost_unique}, solana_poh_config::PohConfig, solana_pubkey::Pubkey, solana_rpc_client::rpc_client::RpcClient, @@ -39,6 +42,10 @@ use { solana_system_transaction as system_transaction, solana_time_utils::timestamp, solana_tpu_client::tpu_client::{TpuClient, TpuClientConfig, TpuSenderError}, + solana_tpu_client_next::{ + client_builder::{ClientBuilder, TransactionSender}, + leader_updater::create_pinned_leader_updater, + }, solana_transaction::Transaction, solana_transaction_error::TransportError, solana_validator_exit::Exit, @@ -47,7 +54,7 @@ use { std::{ collections::{HashMap, HashSet, VecDeque}, net::{SocketAddr, TcpListener, UdpSocket}, - path::Path, + path::{Path, PathBuf}, sync::{ Arc, RwLock, atomic::{AtomicBool, Ordering}, @@ -56,14 +63,87 @@ use { time::{Duration, Instant}, }, tokio_util::sync::CancellationToken, + wincode, }; -#[cfg(feature = "dev-context-only-utils")] -use { - solana_core::consensus::tower_storage::{ - FileTowerStorage, SavedTower, SavedTowerVersions, TowerStorage, - }, - std::path::PathBuf, -}; + +/// Packages a multi-threaded tokio runtime with a tpu-client-next sender, providing +/// a synchronous interface for sending transactions in local-cluster tests. +pub struct TpuSender { + runtime: Arc, +} + +impl TpuSender { + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + Self { + runtime: Arc::new( + tokio::runtime::Builder::new_multi_thread() + .worker_threads(4) + .enable_all() + .build() + .expect("TpuSender: should create tokio runtime"), + ), + } + } + + /// Open a QUIC connection to `tpu_addr`, run `f` with the live sender, then shut down. + /// + /// Keeps the tpu-client-next scheduler alive for the duration of `f` so that any + /// transactions enqueued via [`send_wire_transaction`] are not dropped before wire + /// transmission. Shutdown happens synchronously after `f` returns. + pub fn with_connection(&self, tpu_addr: SocketAddr, f: F) -> R + where + F: FnOnce(&TransactionSender) -> R, + { + let bind_socket = bind_to_localhost_unique().expect("TpuSender: should bind UDP socket"); + let (sender, client) = self + .runtime + .block_on(async { + ClientBuilder::new(create_pinned_leader_updater(tpu_addr)) + .bind_socket(bind_socket) + .build() + }) + .expect("TpuSender: should build tpu-client-next client"); + let result = f(&sender); + self.runtime + .block_on(async { client.shutdown().await }) + .ok(); + result + } + + fn send_wire_transaction(&self, sender: &TransactionSender, wire_tx: Vec) { + self.runtime + .block_on(async { sender.send_transactions_in_batch(vec![wire_tx]).await }) + .expect("TpuSender: should send transactions in batch"); + } + + /// Send and confirm `transaction` with retries via `sender`, using `rpc_client` for + /// confirmation and blockhash refresh. + fn send_and_confirm_transaction( + &self, + sender: &TransactionSender, + rpc_client: &RpcClient, + signers: &T, + transaction: &mut Transaction, + max_attempts: usize, + ) -> Result<(), TransportError> { + for attempt in 1..=max_attempts { + let wire_tx = wincode::serialize(transaction) + .expect("send_and_confirm_transaction: should serialize transaction"); + self.send_wire_transaction(sender, wire_tx); + if LocalCluster::poll_for_successfully_processed_transaction(rpc_client, transaction)? + .is_some() + { + return Ok(()); + } + let (blockhash, _) = + rpc_client.get_latest_blockhash_with_commitment(CommitmentConfig::processed())?; + transaction.sign(signers, blockhash); + warn!("send_and_confirm_transaction: attempt {attempt} failed, retrying"); + } + Err(std::io::Error::other("failed to confirm transaction after max retries").into()) + } +} /// Spend and verify from every node in the network pub fn spend_and_verify_all_nodes( @@ -72,7 +152,7 @@ pub fn spend_and_verify_all_nodes( nodes: usize, ignore_nodes: HashSet, socket_addr_space: SocketAddrSpace, - connection_cache: &Arc, + tpu_sender: &TpuSender, ) { let cluster_nodes = discover_validators( &entry_point_info.gossip().unwrap(), @@ -88,34 +168,48 @@ pub fn spend_and_verify_all_nodes( return; } let random_keypair = Keypair::new(); - let client = new_tpu_quic_client(ingress_node, connection_cache.clone()).unwrap(); - let bal = client - .rpc_client() + let rpc_client = RpcClient::new(format!("http://{}", ingress_node.rpc().unwrap())); + let bal = rpc_client .poll_get_balance_with_commitment( &funding_keypair.pubkey(), CommitmentConfig::processed(), ) .expect("balance in source"); assert!(bal > 0); - let (blockhash, _) = client - .rpc_client() + let (blockhash, _) = rpc_client .get_latest_blockhash_with_commitment(CommitmentConfig::confirmed()) .unwrap(); let mut transaction = system_transaction::transfer(funding_keypair, &random_keypair.pubkey(), 1, blockhash); - LocalCluster::send_transaction_with_retries( - &client, - &[funding_keypair], - &mut transaction, - 10, - ) - .unwrap(); + // Like send_many_transactions: use ingress_node for RPC but send to the + // current slot leader, matching old TpuClient behaviour. + let leader_pubkey = rpc_client + .get_slot_leader() + .expect("spend_and_verify_all_nodes: get_slot_leader"); + let tpu_addr = rpc_client + .get_cluster_nodes() + .expect("spend_and_verify_all_nodes: get_cluster_nodes") + .into_iter() + .find(|n| n.pubkey == leader_pubkey.to_string()) + .and_then(|n| n.tpu_quic) + .or_else(|| ingress_node.tpu(Protocol::QUIC)) + .expect("spend_and_verify_all_nodes: leader has no QUIC TPU address"); + tpu_sender.with_connection(tpu_addr, |sender| { + tpu_sender + .send_and_confirm_transaction( + sender, + &rpc_client, + &[funding_keypair], + &mut transaction, + 10, + ) + .unwrap(); + }); for validator in &cluster_nodes { if ignore_nodes.contains(validator.pubkey()) { continue; } - let client = new_tpu_quic_client(ingress_node, connection_cache.clone()).unwrap(); - LocalCluster::poll_for_successfully_processed_transaction(&client, &transaction) + LocalCluster::poll_for_successfully_processed_transaction(&rpc_client, &transaction) .unwrap() .unwrap(); } @@ -138,47 +232,59 @@ pub fn verify_balances( pub fn send_many_transactions( node: &ContactInfo, funding_keypair: &Keypair, - connection_cache: &Arc, + tpu_sender: &TpuSender, max_tokens_per_transfer: u64, num_txs: u64, ) -> HashMap { - let client = new_tpu_quic_client(node, connection_cache.clone()).unwrap(); - let mut expected_balances = HashMap::new(); - for _ in 0..num_txs { - let random_keypair = Keypair::new(); - let bal = client - .rpc_client() - .poll_get_balance_with_commitment( - &funding_keypair.pubkey(), - CommitmentConfig::processed(), - ) - .expect("balance in source"); - assert!(bal > 0); - let (blockhash, _) = client - .rpc_client() - .get_latest_blockhash_with_commitment(CommitmentConfig::processed()) - .unwrap(); - let transfer_amount = rng().random_range(1..max_tokens_per_transfer); - - let mut transaction = system_transaction::transfer( - funding_keypair, - &random_keypair.pubkey(), - transfer_amount, - blockhash, - ); - - LocalCluster::send_transaction_with_retries( - &client, - &[funding_keypair], - &mut transaction, - 5, - ) - .unwrap(); - - expected_balances.insert(random_keypair.pubkey(), transfer_amount); - } - - expected_balances + let rpc_client = RpcClient::new(format!("http://{}", node.rpc().unwrap())); + // Replicate old TpuClient behaviour: send to the current slot leader, not to `node`. + // `node` is used only as the RPC endpoint; the leader is discovered via RPC + cluster nodes. + let leader_pubkey = rpc_client + .get_slot_leader() + .expect("send_many_transactions: get_slot_leader"); + let tpu_addr = rpc_client + .get_cluster_nodes() + .expect("send_many_transactions: get_cluster_nodes") + .into_iter() + .find(|n| n.pubkey == leader_pubkey.to_string()) + .and_then(|n| n.tpu_quic) + .or_else(|| node.tpu(Protocol::QUIC)) + .expect("send_many_transactions: leader has no QUIC TPU address"); + // One persistent QUIC connection for all transactions to avoid per-tx handshake overhead. + tpu_sender.with_connection(tpu_addr, |sender| { + let mut expected_balances = HashMap::new(); + for _ in 0..num_txs { + let random_keypair = Keypair::new(); + let bal = rpc_client + .poll_get_balance_with_commitment( + &funding_keypair.pubkey(), + CommitmentConfig::processed(), + ) + .expect("balance in source"); + assert!(bal > 0); + let (blockhash, _) = rpc_client + .get_latest_blockhash_with_commitment(CommitmentConfig::processed()) + .unwrap(); + let transfer_amount = rng().random_range(1..max_tokens_per_transfer); + let mut transaction = system_transaction::transfer( + funding_keypair, + &random_keypair.pubkey(), + transfer_amount, + blockhash, + ); + tpu_sender + .send_and_confirm_transaction( + sender, + &rpc_client, + &[funding_keypair], + &mut transaction, + 5, + ) + .unwrap(); + expected_balances.insert(random_keypair.pubkey(), transfer_amount); + } + expected_balances + }) } pub fn verify_ledger_ticks(ledger_path: &Path, ticks_per_slot: usize) { @@ -232,7 +338,7 @@ pub fn kill_entry_and_spend_and_verify_rest( entry_point_info: &ContactInfo, entry_point_validator_exit: &Arc>, funding_keypair: &Keypair, - connection_cache: &Arc, + tpu_sender: &TpuSender, nodes: usize, slot_millis: u64, socket_addr_space: SocketAddrSpace, @@ -248,10 +354,9 @@ pub fn kill_entry_and_spend_and_verify_rest( ) .unwrap(); assert!(cluster_nodes.len() >= nodes); - let client = new_tpu_quic_client(entry_point_info, connection_cache.clone()).unwrap(); + let ep_rpc = RpcClient::new(format!("http://{}", entry_point_info.rpc().unwrap())); for ingress_node in &cluster_nodes { - client - .rpc_client() + ep_rpc .poll_get_balance_with_commitment(ingress_node.pubkey(), CommitmentConfig::processed()) .unwrap_or_else(|err| panic!("Node {} has no balance: {}", ingress_node.pubkey(), err)); } @@ -272,9 +377,8 @@ pub fn kill_entry_and_spend_and_verify_rest( } // Ensure the current ingress node is still funded. - let client = new_tpu_quic_client(ingress_node, connection_cache.clone()).unwrap(); - let balance = client - .rpc_client() + let rpc_client = RpcClient::new(format!("http://{}", ingress_node.rpc().unwrap())); + let balance = rpc_client .poll_get_balance_with_commitment( &funding_keypair.pubkey(), CommitmentConfig::processed(), @@ -282,63 +386,62 @@ pub fn kill_entry_and_spend_and_verify_rest( .expect("balance in source"); assert_ne!(balance, 0); - let mut result = Ok(()); - let mut retries = 0; + let tpu_addr = ingress_node + .tpu(Protocol::QUIC) + .expect("ingress_node has no QUIC TPU address"); // Retry sending a transaction to the current ingress node until it is // observed by the entire cluster or we exhaust all retries. - loop { - retries += 1; - if retries > 5 { - result.unwrap(); - } - - // Send a simple transfer transaction to the current ingress node. - let random_keypair = Keypair::new(); - let (blockhash, _) = client - .rpc_client() - .get_latest_blockhash_with_commitment(CommitmentConfig::processed()) - .unwrap(); - let mut transaction = system_transaction::transfer( - funding_keypair, - &random_keypair.pubkey(), - 1, - blockhash, - ); + tpu_sender.with_connection(tpu_addr, |sender| { + let mut result = Ok(()); + let mut retries = 0; + loop { + retries += 1; + if retries > 5 { + result.unwrap(); + } - if let Err(err) = LocalCluster::send_transaction_with_retries( - &client, - &[funding_keypair], - &mut transaction, - 5, - ) { - result = Err(err); - continue; - } + // Send a simple transfer transaction to the current ingress node. + let random_keypair = Keypair::new(); + let (blockhash, _) = rpc_client + .get_latest_blockhash_with_commitment(CommitmentConfig::processed()) + .unwrap(); + let mut transaction = system_transaction::transfer( + funding_keypair, + &random_keypair.pubkey(), + 1, + blockhash, + ); - // Ensure all non-entry point nodes are able to confirm the - // transaction. - info!("poll_all_nodes_for_signature()"); - match poll_all_nodes_for_signature( - entry_point_info, - &cluster_nodes, - connection_cache, - &transaction, - ) { - Err(e) => { - info!("poll_all_nodes_for_signature() failed {e:?}"); - result = Err(e); + if let Err(err) = tpu_sender.send_and_confirm_transaction( + sender, + &rpc_client, + &[funding_keypair], + &mut transaction, + 5, + ) { + result = Err(err); + continue; } - Ok(()) => { - info!("poll_all_nodes_for_signature() succeeded, done."); - break; + + // Ensure all non-entry point nodes are able to confirm the + // transaction. + info!("poll_all_nodes_for_signature()"); + match poll_all_nodes_for_signature(entry_point_info, &cluster_nodes, &transaction) { + Err(e) => { + info!("poll_all_nodes_for_signature() failed {e:?}"); + result = Err(e); + } + Ok(()) => { + info!("poll_all_nodes_for_signature() succeeded, done."); + break; + } } } - } + }); } } -#[cfg(feature = "dev-context-only-utils")] pub fn apply_votes_to_tower(node_keypair: &Keypair, votes: Vec<(Slot, Hash)>, tower_path: PathBuf) { let tower_storage = FileTowerStorage::new(tower_path); let mut tower = tower_storage.load(&node_keypair.pubkey()).unwrap(); @@ -634,15 +737,15 @@ pub fn check_no_new_roots( fn poll_all_nodes_for_signature( entry_point_info: &ContactInfo, cluster_nodes: &[ContactInfo], - connection_cache: &Arc, transaction: &Transaction, ) -> Result<(), TransportError> { for validator in cluster_nodes { if validator.pubkey() == entry_point_info.pubkey() { continue; } - let client = new_tpu_quic_client(validator, connection_cache.clone()).unwrap(); - LocalCluster::poll_for_successfully_processed_transaction(&client, transaction)?.unwrap(); + let rpc_client = RpcClient::new(format!("http://{}", validator.rpc().unwrap())); + LocalCluster::poll_for_successfully_processed_transaction(&rpc_client, transaction)? + .unwrap(); } Ok(()) diff --git a/local-cluster/src/integration_tests.rs b/local-cluster/src/integration_tests.rs index ba97bd8f276..e5f242dc007 100644 --- a/local-cluster/src/integration_tests.rs +++ b/local-cluster/src/integration_tests.rs @@ -524,7 +524,7 @@ pub fn run_cluster_partition( num_nodes, HashSet::new(), SocketAddrSpace::Unspecified, - &cluster.connection_cache, + &cluster_tests::TpuSender::new(), ); let cluster_nodes = discover_validators( diff --git a/local-cluster/src/local_cluster.rs b/local-cluster/src/local_cluster.rs index 37e9d6e10b6..c82ca1e2459 100644 --- a/local-cluster/src/local_cluster.rs +++ b/local-cluster/src/local_cluster.rs @@ -963,14 +963,14 @@ impl LocalCluster { /// Ok(Some(())) if the transaction was processed successfully. Return /// Ok(None) if the transaction was not processed. pub fn poll_for_successfully_processed_transaction( - client: &QuicTpuClient, + rpc_client: &RpcClient, transaction: &Transaction, ) -> std::result::Result, TransportError> { loop { // Some local cluster tests create conditions where confirmation // is unable to be reached. So rather than checking for confirmation, // check for the transaction being processed. - let status = client.rpc_client().get_signature_status_with_commitment( + let status = rpc_client.get_signature_status_with_commitment( &transaction.signatures[0], CommitmentConfig::processed(), )?; @@ -984,7 +984,7 @@ impl LocalCluster { } } - if !client.rpc_client().is_blockhash_valid( + if !rpc_client.is_blockhash_valid( &transaction.message.recent_blockhash, CommitmentConfig::processed(), )? { @@ -1012,7 +1012,9 @@ impl LocalCluster { // in LocalCluster integration tests for attempt in 1..=attempts { client.send_transaction_to_upcoming_leaders(transaction)?; - if Self::poll_for_successfully_processed_transaction(client, transaction)?.is_some() { + if Self::poll_for_successfully_processed_transaction(client.rpc_client(), transaction)? + .is_some() + { return Ok(()); } diff --git a/local-cluster/tests/local_cluster.rs b/local-cluster/tests/local_cluster.rs index 8f76a822f2b..687f63edd3e 100644 --- a/local-cluster/tests/local_cluster.rs +++ b/local-cluster/tests/local_cluster.rs @@ -163,7 +163,7 @@ fn test_spend_and_verify_all_nodes() { num_nodes, HashSet::new(), SocketAddrSpace::Unspecified, - &local.connection_cache, + &cluster_tests::TpuSender::new(), ); } @@ -327,7 +327,7 @@ fn test_forwarding() { cluster_tests::send_many_transactions( validator_info, &cluster.funding_keypair, - &cluster.connection_cache, + &cluster_tests::TpuSender::new(), 10, 20, ); @@ -372,7 +372,7 @@ fn test_restart_node() { cluster_tests::send_many_transactions( &cluster.entry_point_info, &cluster.funding_keypair, - &cluster.connection_cache, + &cluster_tests::TpuSender::new(), 10, 1, ); @@ -1281,7 +1281,7 @@ fn test_snapshot_restart_tower() { 2, HashSet::new(), SocketAddrSpace::Unspecified, - &cluster.connection_cache, + &cluster_tests::TpuSender::new(), ); } @@ -1432,6 +1432,7 @@ fn test_snapshots_restart_validity() { let num_runs = 3; let mut expected_balances = HashMap::new(); let mut cluster = LocalCluster::new(&mut config, SocketAddrSpace::Unspecified); + let tpu_sender = cluster_tests::TpuSender::new(); for i in 1..num_runs { info!("run {i}"); // Push transactions to one of the nodes and confirm that transactions were @@ -1440,7 +1441,7 @@ fn test_snapshots_restart_validity() { let new_balances = cluster_tests::send_many_transactions( &cluster.entry_point_info, &cluster.funding_keypair, - &cluster.connection_cache, + &tpu_sender, 10, 10, ); @@ -1480,7 +1481,7 @@ fn test_snapshots_restart_validity() { 1, HashSet::new(), SocketAddrSpace::Unspecified, - &cluster.connection_cache, + &tpu_sender, ); } } @@ -4334,7 +4335,7 @@ fn test_leader_failure_4() { .config .validator_exit, &local.funding_keypair, - &local.connection_cache, + &cluster_tests::TpuSender::new(), num_nodes, config.ticks_per_slot * config.poh_config.target_tick_duration.as_millis() as u64, SocketAddrSpace::Unspecified, @@ -5660,7 +5661,7 @@ fn test_randomly_mixed_block_verification_methods_between_bootstrap_and_not() { num_nodes, HashSet::new(), SocketAddrSpace::Unspecified, - &local.connection_cache, + &cluster_tests::TpuSender::new(), ); } @@ -5694,7 +5695,7 @@ fn test_randomly_mixed_block_production_methods_between_bootstrap_and_not() { num_nodes, HashSet::new(), SocketAddrSpace::Unspecified, - &local.connection_cache, + &cluster_tests::TpuSender::new(), ); } @@ -5892,7 +5893,7 @@ fn test_alpenglow_nodes_basic(num_nodes: usize, num_offline_nodes: usize) { num_nodes, HashSet::new(), SocketAddrSpace::Unspecified, - &cluster.connection_cache, + &cluster_tests::TpuSender::new(), ); if num_offline_nodes > 0 { @@ -5978,7 +5979,7 @@ fn test_restart_node_alpenglow() { cluster_tests::send_many_transactions( &cluster.entry_point_info, &cluster.funding_keypair, - &cluster.connection_cache, + &cluster_tests::TpuSender::new(), 10, 1, ); From 4cce4c0dd3399941d202dd97bc30ae8d508718b0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 18:41:10 +0800 Subject: [PATCH 277/576] chore(deps): bump solana-message from 4.2.0 to 4.2.1 (#13072) * chore(deps): bump solana-message from 4.2.0 to 4.2.1 Bumps [solana-message](https://github.com/anza-xyz/solana-sdk) from 4.2.0 to 4.2.1. - [Release notes](https://github.com/anza-xyz/solana-sdk/releases) - [Commits](https://github.com/anza-xyz/solana-sdk/compare/hash@v4.2.0...message@v4.2.1) --- updated-dependencies: - dependency-name: solana-message dependency-version: 4.2.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Update all workspaces --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 5 ++--- Cargo.toml | 2 +- dev-bins/Cargo.lock | 5 ++--- install/Cargo.toml | 2 +- keygen/Cargo.toml | 2 +- programs/sbf/Cargo.lock | 5 ++--- 6 files changed, 9 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fbb3f593f30..675a35bd042 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8987,12 +8987,11 @@ dependencies = [ [[package]] name = "solana-message" -version = "4.2.0" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a81a5ae2aa5c27925de70bc94fc42f1ca80a46c951ae101b419aa4eac9abb56a" +checksum = "705bae842f1d02819db46ae9051613d78576c7cdabc0edb5cba4d7fe9482acaf" dependencies = [ "blake3", - "lazy_static", "serde", "serde_derive", "solana-address 2.6.1", diff --git a/Cargo.toml b/Cargo.toml index 0f7c84c7eb5..a92c4e971c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -423,7 +423,7 @@ solana-loader-v4-interface = "3.1.0" solana-local-cluster = { path = "local-cluster", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-measure = { path = "measure", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-merkle-tree = { path = "merkle-tree", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-message = "4.2.0" +solana-message = "4.2.1" solana-metrics = { path = "metrics", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-msg = "3.1.0" solana-native-token = "3.0.0" diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 74adc41bd41..c7ac045694f 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -7606,12 +7606,11 @@ dependencies = [ [[package]] name = "solana-message" -version = "4.2.0" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a81a5ae2aa5c27925de70bc94fc42f1ca80a46c951ae101b419aa4eac9abb56a" +checksum = "705bae842f1d02819db46ae9051613d78576c7cdabc0edb5cba4d7fe9482acaf" dependencies = [ "blake3", - "lazy_static", "serde", "serde_derive", "solana-address 2.6.1", diff --git a/install/Cargo.toml b/install/Cargo.toml index 85edd66434f..d40eb9a253d 100644 --- a/install/Cargo.toml +++ b/install/Cargo.toml @@ -43,7 +43,7 @@ solana-clap-utils = { workspace = true } solana-config-interface = { version = "=2.0.0", features = ["bincode"] } solana-hash = "=4.4.0" solana-keypair = "=3.1.2" -solana-message = "=4.2.0" +solana-message = "=4.2.1" solana-pubkey = { version = "=4.2.0", default-features = false } solana-rpc-client = { workspace = true } solana-sha256-hasher = { workspace = true } diff --git a/keygen/Cargo.toml b/keygen/Cargo.toml index 817f5b9b377..e5dcc809808 100644 --- a/keygen/Cargo.toml +++ b/keygen/Cargo.toml @@ -36,7 +36,7 @@ solana-cli-config = { workspace = true } solana-derivation-path = { workspace = true } solana-instruction = { version = "=3.4.0", features = ["bincode"] } solana-keypair = "=3.1.2" -solana-message = { version = "=4.2.0", features = ["wincode"] } +solana-message = { version = "=4.2.1", features = ["wincode"] } solana-pubkey = { version = "=4.2.0", default-features = false } solana-remote-wallet = { workspace = true } solana-seed-derivable = { workspace = true } diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index b4fb11c1571..42218fcce66 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -7385,12 +7385,11 @@ dependencies = [ [[package]] name = "solana-message" -version = "4.2.0" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a81a5ae2aa5c27925de70bc94fc42f1ca80a46c951ae101b419aa4eac9abb56a" +checksum = "705bae842f1d02819db46ae9051613d78576c7cdabc0edb5cba4d7fe9482acaf" dependencies = [ "blake3", - "lazy_static", "serde", "serde_derive", "solana-address 2.6.1", From a4ec202dd5a0690f38e50d13c861dd86371e795a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 18:44:19 +0800 Subject: [PATCH 278/576] chore(deps): bump bitflags from 2.11.1 to 2.12.1 (#13081) * chore(deps): bump bitflags from 2.11.1 to 2.12.1 in /dev-bins Bumps [bitflags](https://github.com/bitflags/bitflags) from 2.11.1 to 2.12.1. - [Release notes](https://github.com/bitflags/bitflags/releases) - [Changelog](https://github.com/bitflags/bitflags/blob/main/CHANGELOG.md) - [Commits](https://github.com/bitflags/bitflags/compare/2.11.1...2.12.1) --- updated-dependencies: - dependency-name: bitflags dependency-version: 2.12.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * Update all workspaces --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 40 +++++++++++++++++++-------------------- Cargo.toml | 2 +- ci/xtask/Cargo.lock | 4 ++-- dev-bins/Cargo.lock | 42 ++++++++++++++++++++--------------------- programs/sbf/Cargo.lock | 36 +++++++++++++++++------------------ 5 files changed, 62 insertions(+), 62 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 675a35bd042..9079f633bac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1206,7 +1206,7 @@ checksum = "d18bc4e506fbb85ab7392ed993a7db4d1a452c71b75a246af4a80ab8c9d2dd50" dependencies = [ "assert_matches", "aya-obj", - "bitflags 2.11.1", + "bitflags 2.12.1", "bytes", "libc", "log", @@ -1313,7 +1313,7 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "cexpr", "clang-sys", "itertools 0.13.0", @@ -1370,9 +1370,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.1" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a" dependencies = [ "serde_core", ] @@ -2536,7 +2536,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "block2", "libc", "objc2", @@ -3926,7 +3926,7 @@ version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d09b98f7eace8982db770e4408e7470b028ce513ac28fecdc6bf4c30fe92b62" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "cfg-if 1.0.4", "libc", ] @@ -4656,7 +4656,7 @@ version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "cfg-if 1.0.4", "cfg_aliases", "libc", @@ -4875,7 +4875,7 @@ version = "0.10.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "cfg-if 1.0.4", "foreign-types", "libc", @@ -5371,7 +5371,7 @@ checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.11.1", + "bitflags 2.12.1", "num-traits", "rand 0.9.4", "rand_chacha 0.9.0", @@ -5510,7 +5510,7 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83c41efbf8f90ac44de7f3a868f0867851d261b56291732d0cbf7cceaaeb55a6" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "memchr", "unicase", ] @@ -6096,7 +6096,7 @@ version = "0.38.39" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "375116bee2be9ed569afe2154ea6a99dfdffd257f533f187498c2a8f5feaf4ee" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "errno", "libc", "linux-raw-sys 0.4.14", @@ -6109,7 +6109,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "errno", "libc", "linux-raw-sys 0.12.1", @@ -6275,7 +6275,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -6288,7 +6288,7 @@ version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "core-foundation 0.10.0", "core-foundation-sys", "libc", @@ -8635,7 +8635,7 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ddf67876c541aa1e21ee1acae35c95c6fbc61119814bfef70579317a5e26955" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "solana-account-info", "solana-instruction", "solana-instruction-error", @@ -8757,7 +8757,7 @@ dependencies = [ "anyhow", "assert_matches", "bincode", - "bitflags 2.11.1", + "bitflags 2.12.1", "bs58", "bytes", "bzip2", @@ -9142,7 +9142,7 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6edf2f25743c95229ac0fdc32f8f5893ef738dbf332c669e9861d33ddb0f469d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", ] [[package]] @@ -9152,7 +9152,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ad62e1045c2347a0c0e219a6ceb0abfe904be622920996bfcac8d116fabe3c7" dependencies = [ "bincode", - "bitflags 2.11.1", + "bitflags 2.12.1", "cfg_eval", "serde", "serde_derive", @@ -11966,7 +11966,7 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cca424247104946a59dacd27eaad296223b7feec3d168a6dd04585183091eb0b" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "byteorder", "enum-as-inner", "libc", @@ -12504,7 +12504,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ "async-compression", - "bitflags 2.11.1", + "bitflags 2.12.1", "bytes", "futures-core", "futures-util", diff --git a/Cargo.toml b/Cargo.toml index a92c4e971c8..a5d9a268bc1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -203,7 +203,7 @@ base64 = "0.22.1" bencher = "0.1.5" bincode = "1.3.3" bip39 = { version = "2.2.2", features = ["rand", "all-languages"] } -bitflags = { version = "2.11.1" } +bitflags = { version = "2.12.1" } bitvec = { version = "1.0.1", features = ["serde"] } blake3 = "1.8.5" blst = "0.3.16" diff --git a/ci/xtask/Cargo.lock b/ci/xtask/Cargo.lock index 6059891382b..7358517b8c1 100644 --- a/ci/xtask/Cargo.lock +++ b/ci/xtask/Cargo.lock @@ -128,9 +128,9 @@ checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "bitflags" -version = "2.11.1" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a" [[package]] name = "block-buffer" diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index c7ac045694f..9c168510cc5 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -1022,7 +1022,7 @@ checksum = "d18bc4e506fbb85ab7392ed993a7db4d1a452c71b75a246af4a80ab8c9d2dd50" dependencies = [ "assert_matches", "aya-obj", - "bitflags 2.11.1", + "bitflags 2.13.0", "bytes", "libc", "log", @@ -1137,7 +1137,7 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "cexpr", "clang-sys", "itertools 0.13.0", @@ -1179,9 +1179,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" dependencies = [ "serde_core", ] @@ -3391,7 +3391,7 @@ version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d09b98f7eace8982db770e4408e7470b028ce513ac28fecdc6bf4c30fe92b62" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "cfg-if 1.0.4", "libc", ] @@ -3716,7 +3716,7 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "libc", "plain", "redox_syscall 0.7.3", @@ -4090,7 +4090,7 @@ version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "cfg-if 1.0.4", "cfg_aliases", "libc", @@ -4287,7 +4287,7 @@ version = "0.10.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "cfg-if 1.0.4", "foreign-types", "libc", @@ -4774,7 +4774,7 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83c41efbf8f90ac44de7f3a868f0867851d261b56291732d0cbf7cceaaeb55a6" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "memchr", "unicase", ] @@ -5045,7 +5045,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", ] [[package]] @@ -5054,7 +5054,7 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", ] [[package]] @@ -5324,7 +5324,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "errno", "libc", "linux-raw-sys", @@ -5471,7 +5471,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "core-foundation", "core-foundation-sys", "libc", @@ -7308,7 +7308,7 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ddf67876c541aa1e21ee1acae35c95c6fbc61119814bfef70579317a5e26955" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "solana-account-info", "solana-instruction", "solana-instruction-error", @@ -7396,7 +7396,7 @@ dependencies = [ "anyhow", "assert_matches", "bincode", - "bitflags 2.11.1", + "bitflags 2.13.0", "bytes", "bzip2", "chrono", @@ -7739,7 +7739,7 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6edf2f25743c95229ac0fdc32f8f5893ef738dbf332c669e9861d33ddb0f469d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", ] [[package]] @@ -7749,7 +7749,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ad62e1045c2347a0c0e219a6ceb0abfe904be622920996bfcac8d116fabe3c7" dependencies = [ "bincode", - "bitflags 2.11.1", + "bitflags 2.13.0", "cfg_eval", "serde", "serde_derive", @@ -10072,7 +10072,7 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cca424247104946a59dacd27eaad296223b7feec3d168a6dd04585183091eb0b" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "byteorder", "enum-as-inner", "libc", @@ -10560,7 +10560,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ "async-compression", - "bitflags 2.11.1", + "bitflags 2.13.0", "bytes", "futures-core", "futures-util", @@ -11017,7 +11017,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "hashbrown 0.15.5", "indexmap", "semver", @@ -11495,7 +11495,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.11.1", + "bitflags 2.13.0", "indexmap", "log", "serde", diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index 42218fcce66..b8edd6bd51a 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -1006,7 +1006,7 @@ checksum = "d18bc4e506fbb85ab7392ed993a7db4d1a452c71b75a246af4a80ab8c9d2dd50" dependencies = [ "assert_matches", "aya-obj", - "bitflags 2.11.1", + "bitflags 2.12.1", "bytes", "libc", "log", @@ -1107,7 +1107,7 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "cexpr", "clang-sys", "itertools 0.13.0", @@ -1149,9 +1149,9 @@ checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" [[package]] name = "bitflags" -version = "2.11.1" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a" dependencies = [ "serde_core", ] @@ -3309,7 +3309,7 @@ version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d09b98f7eace8982db770e4408e7470b028ce513ac28fecdc6bf4c30fe92b62" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "cfg-if 1.0.4", "libc", ] @@ -4017,7 +4017,7 @@ version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "cfg-if 1.0.4", "cfg_aliases", "libc", @@ -4214,7 +4214,7 @@ version = "0.10.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "cfg-if 1.0.4", "foreign-types", "libc", @@ -4699,7 +4699,7 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83c41efbf8f90ac44de7f3a868f0867851d261b56291732d0cbf7cceaaeb55a6" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "memchr", "unicase", ] @@ -5232,7 +5232,7 @@ version = "0.38.39" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "375116bee2be9ed569afe2154ea6a99dfdffd257f533f187498c2a8f5feaf4ee" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "errno", "libc", "linux-raw-sys 0.4.14", @@ -5245,7 +5245,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "errno", "libc", "linux-raw-sys 0.12.1", @@ -5405,7 +5405,7 @@ version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "core-foundation 0.10.0", "core-foundation-sys", "libc", @@ -7158,7 +7158,7 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ddf67876c541aa1e21ee1acae35c95c6fbc61119814bfef70579317a5e26955" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "solana-account-info", "solana-instruction", "solana-instruction-error", @@ -7246,7 +7246,7 @@ dependencies = [ "anyhow", "assert_matches", "bincode", - "bitflags 2.11.1", + "bitflags 2.12.1", "bytes", "bzip2", "chrono", @@ -7518,7 +7518,7 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6edf2f25743c95229ac0fdc32f8f5893ef738dbf332c669e9861d33ddb0f469d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", ] [[package]] @@ -7528,7 +7528,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ad62e1045c2347a0c0e219a6ceb0abfe904be622920996bfcac8d116fabe3c7" dependencies = [ "bincode", - "bitflags 2.11.1", + "bitflags 2.12.1", "cfg_eval", "serde", "serde_derive", @@ -10566,7 +10566,7 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cca424247104946a59dacd27eaad296223b7feec3d168a6dd04585183091eb0b" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "byteorder", "enum-as-inner", "libc", @@ -11075,7 +11075,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ "async-compression", - "bitflags 2.11.1", + "bitflags 2.12.1", "bytes", "futures-core", "futures-util", @@ -11932,7 +11932,7 @@ version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", ] [[package]] From 35758a72550eaaf858304a7260977678b3015c04 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 19:35:25 +0800 Subject: [PATCH 279/576] chore(deps): bump log from 0.4.30 to 0.4.31 (#13104) * chore(deps): bump log from 0.4.30 to 0.4.31 Bumps [log](https://github.com/rust-lang/log) from 0.4.30 to 0.4.31. - [Release notes](https://github.com/rust-lang/log/releases) - [Changelog](https://github.com/rust-lang/log/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-lang/log/compare/0.4.30...0.4.31) --- updated-dependencies: - dependency-name: log dependency-version: 0.4.31 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Update all workspaces --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- ci/xtask/Cargo.lock | 4 ++-- dev-bins/Cargo.lock | 4 ++-- programs/sbf/Cargo.lock | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9079f633bac..6a7777af4ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4410,9 +4410,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.30" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" [[package]] name = "lru" diff --git a/Cargo.toml b/Cargo.toml index a5d9a268bc1..d3dc660685e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -284,7 +284,7 @@ libsecp256k1 = { version = "0.7.2", default-features = false, features = [ "std", "static-context", ] } -log = "0.4.30" +log = "0.4.31" lru = "0.18.0" lz4 = "1.28.1" memmap2 = "0.9.10" diff --git a/ci/xtask/Cargo.lock b/ci/xtask/Cargo.lock index 7358517b8c1..d75e2f15094 100644 --- a/ci/xtask/Cargo.lock +++ b/ci/xtask/Cargo.lock @@ -1090,9 +1090,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.30" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" +checksum = "113b30b4cd05f7c06868fdb2854f66a7b9fece9a48425351cd532e810d74024f" [[package]] name = "memchr" diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 9c168510cc5..004d5bbfc6c 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -3854,9 +3854,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.30" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" +checksum = "113b30b4cd05f7c06868fdb2854f66a7b9fece9a48425351cd532e810d74024f" [[package]] name = "lru" diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index b8edd6bd51a..59d367ef19f 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -3778,9 +3778,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.30" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" +checksum = "113b30b4cd05f7c06868fdb2854f66a7b9fece9a48425351cd532e810d74024f" [[package]] name = "lru" From a7b19d3acf8b86aee7b1c3dae984d0d20af6539c Mon Sep 17 00:00:00 2001 From: Yihau Chen Date: Wed, 10 Jun 2026 20:05:44 +0800 Subject: [PATCH 280/576] chore: remove systemstat dep (#13107) --- Cargo.lock | 21 --------------------- Cargo.toml | 1 - client-test/Cargo.toml | 1 - client-test/tests/client.rs | 3 +-- 4 files changed, 1 insertion(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6a7777af4ba..fc537fec4e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1631,12 +1631,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bytesize" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c58ec36aac5066d5ca17df51b3e70279f5670a72102f5752cb7e7c856adfc70" - [[package]] name = "bzip2" version = "0.6.1" @@ -7591,7 +7585,6 @@ dependencies = [ "solana-test-validator", "solana-transaction-status", "solana-version", - "systemstat", "tokio", "tungstenite", ] @@ -11974,20 +11967,6 @@ dependencies = [ "walkdir", ] -[[package]] -name = "systemstat" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6e89b75de097d0c52a1dc2114e19439d55f0e2e42d32168c6df44f139dfb66f" -dependencies = [ - "bytesize", - "lazy_static", - "libc", - "nom", - "time", - "winapi", -] - [[package]] name = "tap" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index d3dc660685e..5418815e872 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -541,7 +541,6 @@ strum_macros = "0.28.0" symlink = "0.1.0" sys-info = "0.9.1" sysctl = "0.7.1" -systemstat = "0.2.6" tar = "0.4.46" tarpc = "0.29.0" tempfile = "3.27.0" diff --git a/client-test/Cargo.toml b/client-test/Cargo.toml index b4f6aaadede..7a6f00e602a 100644 --- a/client-test/Cargo.toml +++ b/client-test/Cargo.toml @@ -37,7 +37,6 @@ solana-system-interface = { workspace = true } solana-system-transaction = { workspace = true } solana-transaction-status = { workspace = true } solana-version = { workspace = true } -systemstat = { workspace = true } tokio = { workspace = true, features = ["full"] } tungstenite = { workspace = true, features = ["rustls-tls-webpki-roots"] } diff --git a/client-test/tests/client.rs b/client-test/tests/client.rs index c58940c4dfd..8d0da4dcdc5 100644 --- a/client-test/tests/client.rs +++ b/client-test/tests/client.rs @@ -38,7 +38,7 @@ use { }, std::{ collections::HashSet, - net::{IpAddr, SocketAddr}, + net::{IpAddr, Ipv4Addr, SocketAddr}, sync::{ Arc, RwLock, atomic::{AtomicBool, AtomicU64, Ordering}, @@ -46,7 +46,6 @@ use { thread::sleep, time::{Duration, Instant}, }, - systemstat::Ipv4Addr, tungstenite::{client::IntoClientRequest, connect}, }; From 244368199dc8cc584fe9dceceb8bf33e423f95cb Mon Sep 17 00:00:00 2001 From: Yihau Chen Date: Wed, 10 Jun 2026 20:11:59 +0800 Subject: [PATCH 281/576] chore: bump solana-signer from 3.0.0 to 3.0.1 (#13108) * chore: bump solana-signer from 3.0.0 to 3.0.1 * fix solana-pubkey in builtins-default-costs --- Cargo.lock | 6 +++--- Cargo.toml | 2 +- builtins-default-costs/Cargo.toml | 1 + dev-bins/Cargo.lock | 6 +++--- dev-bins/Cargo.toml | 2 +- install/Cargo.toml | 2 +- keygen/Cargo.toml | 2 +- programs/sbf/Cargo.lock | 6 +++--- 8 files changed, 14 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fc537fec4e7..3d544a579f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10330,11 +10330,11 @@ dependencies = [ [[package]] name = "solana-signer" -version = "3.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bfea97951fee8bae0d6038f39a5efcb6230ecdfe33425ac75196d1a1e3e3235" +checksum = "520bd6021163ee517f4bdc7ae03ded904f97e11320001ba0b3355f45eb14f558" dependencies = [ - "solana-pubkey 3.0.0", + "solana-pubkey 4.2.0", "solana-signature", "solana-transaction-error", ] diff --git a/Cargo.toml b/Cargo.toml index 5418815e872..ab0d04547dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -479,7 +479,7 @@ solana-sha512-hasher = "1.0.1" solana-short-vec = "3.2.2" solana-shred-version = "3.0.1" solana-signature = { version = "3.4.1", default-features = false } -solana-signer = "3.0.0" +solana-signer = "3.0.1" solana-signer-store = "0.1.0" solana-slot-hashes = "3.0.2" solana-slot-history = "3.0.1" diff --git a/builtins-default-costs/Cargo.toml b/builtins-default-costs/Cargo.toml index 2c6e2aefb8d..6e083605b2f 100644 --- a/builtins-default-costs/Cargo.toml +++ b/builtins-default-costs/Cargo.toml @@ -29,6 +29,7 @@ solana-pubkey = { workspace = true } solana-sdk-ids = { workspace = true } [dev-dependencies] +solana-pubkey = { workspace = true, features = ["std"] } static_assertions = { workspace = true } [lints] diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 004d5bbfc6c..289830f1e11 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -8689,11 +8689,11 @@ dependencies = [ [[package]] name = "solana-signer" -version = "3.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bfea97951fee8bae0d6038f39a5efcb6230ecdfe33425ac75196d1a1e3e3235" +checksum = "520bd6021163ee517f4bdc7ae03ded904f97e11320001ba0b3355f45eb14f558" dependencies = [ - "solana-pubkey 3.0.0", + "solana-pubkey 4.2.0", "solana-signature", "solana-transaction-error", ] diff --git a/dev-bins/Cargo.toml b/dev-bins/Cargo.toml index 3106c2ca859..eb10ac9594e 100644 --- a/dev-bins/Cargo.toml +++ b/dev-bins/Cargo.toml @@ -121,7 +121,7 @@ solana-sbpf = { version = "=0.21.0", default-features = false } solana-sdk-ids = "3.1.0" solana-shred-version = "3.0.1" solana-signature = { version = "3.4.1", default-features = false } -solana-signer = "3.0.0" +solana-signer = "3.0.1" solana-stake-interface = "4.2.0" solana-storage-bigtable = { path = "../storage-bigtable", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-streamer = { path = "../streamer", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } diff --git a/install/Cargo.toml b/install/Cargo.toml index d40eb9a253d..37abefc753e 100644 --- a/install/Cargo.toml +++ b/install/Cargo.toml @@ -48,7 +48,7 @@ solana-pubkey = { version = "=4.2.0", default-features = false } solana-rpc-client = { workspace = true } solana-sha256-hasher = { workspace = true } solana-signature = { version = "=3.4.1", default-features = false } -solana-signer = "=3.0.0" +solana-signer = "=3.0.1" solana-transaction = { version = "=4.1.2", features = ["wincode"] } solana-version = { workspace = true } tar = { workspace = true } diff --git a/keygen/Cargo.toml b/keygen/Cargo.toml index e5dcc809808..fe7a46e8dff 100644 --- a/keygen/Cargo.toml +++ b/keygen/Cargo.toml @@ -40,7 +40,7 @@ solana-message = { version = "=4.2.1", features = ["wincode"] } solana-pubkey = { version = "=4.2.0", default-features = false } solana-remote-wallet = { workspace = true } solana-seed-derivable = { workspace = true } -solana-signer = "=3.0.0" +solana-signer = "=3.0.1" solana-version = { workspace = true } [dev-dependencies] diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index 59d367ef19f..09d985cd390 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -9224,11 +9224,11 @@ dependencies = [ [[package]] name = "solana-signer" -version = "3.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bfea97951fee8bae0d6038f39a5efcb6230ecdfe33425ac75196d1a1e3e3235" +checksum = "520bd6021163ee517f4bdc7ae03ded904f97e11320001ba0b3355f45eb14f558" dependencies = [ - "solana-pubkey 3.0.0", + "solana-pubkey 4.2.0", "solana-signature", "solana-transaction-error", ] From 935587762ec11d1884ad6d7e52e77ced54968b70 Mon Sep 17 00:00:00 2001 From: Edvard Fagerholm Date: Wed, 10 Jun 2026 15:57:35 +0300 Subject: [PATCH 282/576] test(runtime): bound previously-unbounded test-only channels (#13037) * test: bound previously-unbounded test-only channels in runtime Replace unbounded() with crossbeam_channel::bounded(1024) at all test-only channel call sites. Production channels are left unbounded. * test(runtime): make scan_finished channel send non-blocking Bounding the scan_finished channel meant the scan thread could block on a full send() if a caller never drained it (deadlocking against the bounded(1) bank_to_scan channel). Use try_send() and panic on Full (tolerating Disconnected at shutdown), and drain the channel in the two callers that previously ignored it. The drain is non-blocking and placed at end-of-loop to preserve the intentional overlap of clean/squash with the in-flight scan. --- runtime/src/accounts_background_service.rs | 8 +++--- runtime/src/bank/tests.rs | 32 +++++++++++++++++----- runtime/src/snapshot_controller.rs | 8 +++--- 3 files changed, 33 insertions(+), 15 deletions(-) diff --git a/runtime/src/accounts_background_service.rs b/runtime/src/accounts_background_service.rs index c350363f090..ebfd0732b6b 100644 --- a/runtime/src/accounts_background_service.rs +++ b/runtime/src/accounts_background_service.rs @@ -719,7 +719,7 @@ fn cmp_snapshot_request_kinds_by_priority( mod test { use { super::*, crate::genesis_utils::create_genesis_config, - agave_snapshots::snapshot_config::SnapshotConfig, crossbeam_channel::unbounded, + agave_snapshots::snapshot_config::SnapshotConfig, crossbeam_channel::bounded, solana_account::AccountSharedData, solana_epoch_schedule::EpochSchedule, solana_leader_schedule::SlotLeader, solana_pubkey::Pubkey, }; @@ -728,7 +728,7 @@ mod test { fn test_accounts_background_service_remove_dead_slots() { let genesis = create_genesis_config(10); let bank0 = Arc::new(Bank::new_for_tests(&genesis.genesis_config)); - let (pruned_banks_sender, pruned_banks_receiver) = unbounded(); + let (pruned_banks_sender, pruned_banks_receiver) = bounded(1024); let pruned_banks_request_handler = PrunedBanksRequestHandler { pruned_banks_receiver, }; @@ -768,7 +768,7 @@ mod test { let snapshot_config = SnapshotConfig::default(); let pending_snapshot_packages = Arc::new(Mutex::new(PendingSnapshotPackages::default())); - let (snapshot_request_sender, snapshot_request_receiver) = crossbeam_channel::unbounded(); + let (snapshot_request_sender, snapshot_request_receiver) = bounded(1024); let snapshot_controller = Arc::new(SnapshotController::new( snapshot_request_sender.clone(), snapshot_config, @@ -908,7 +908,7 @@ mod test { /// Ensure that we can prune banks with the same slot (if they were on different forks) #[test] fn test_pruned_banks_request_handler_handle_request() { - let (pruned_banks_sender, pruned_banks_receiver) = crossbeam_channel::unbounded(); + let (pruned_banks_sender, pruned_banks_receiver) = bounded(1024); let pruned_banks_request_handler = PrunedBanksRequestHandler { pruned_banks_receiver, }; diff --git a/runtime/src/bank/tests.rs b/runtime/src/bank/tests.rs index 956a6d7eff2..be429d32634 100644 --- a/runtime/src/bank/tests.rs +++ b/runtime/src/bank/tests.rs @@ -33,7 +33,7 @@ use { agave_reserved_account_keys::ReservedAccount, ahash::AHashMap, assert_matches::assert_matches, - crossbeam_channel::{bounded, unbounded}, + crossbeam_channel::{TrySendError, bounded}, itertools::Itertools, rand::Rng, rayon::{ThreadPool, ThreadPoolBuilder, iter::IntoParallelIterator}, @@ -7256,7 +7256,7 @@ fn test_store_scan_consistency( let (scan_finished_sender, scan_finished_receiver): ( crossbeam_channel::Sender, crossbeam_channel::Receiver, - ) = unbounded(); + ) = bounded(1024); let num_banks_scanned = Arc::new(AtomicU64::new(0)); let scan_thread = { let exit = exit.clone(); @@ -7275,7 +7275,15 @@ fn test_store_scan_consistency( { info!("scanning program accounts for slot {}", bank_to_scan.slot()); let accounts_result = bank_to_scan.get_program_accounts(&program_id); - let _ = scan_finished_sender.send(bank_to_scan.bank_id()); + match scan_finished_sender.try_send(bank_to_scan.bank_id()) { + // The receiver may have hung up during shutdown; that's fine. + Ok(()) | Err(TrySendError::Disconnected(_)) => {} + // A full channel means a caller isn't draining it, which would + // block this scan thread. Fail loudly instead of deadlocking. + Err(TrySendError::Full(_)) => { + panic!("scan_finished channel is full; caller must drain it") + } + } num_banks_scanned.fetch_add(1, Relaxed); match (&acceptable_scan_results, accounts_result.is_err()) { (AcceptableScanResults::DroppedSlotError, _) @@ -7365,14 +7373,14 @@ fn test_store_scan_consistency( #[test] fn test_store_scan_consistency_unrooted() { - let (pruned_banks_sender, pruned_banks_receiver) = unbounded(); + let (pruned_banks_sender, pruned_banks_receiver) = bounded(1024); let pruned_banks_request_handler = PrunedBanksRequestHandler { pruned_banks_receiver, }; test_store_scan_consistency( move |bank0, bank_to_scan_sender, - _scan_finished_receiver, + scan_finished_receiver, pubkeys_to_modify, program_id, starting_lamports| { @@ -7451,6 +7459,11 @@ fn test_store_scan_consistency_unrooted() { // Move purge here so that Bank::drop()->purge_slots() doesn't race // with clean. Simulates the call from AccountsBackgroundService pruned_banks_request_handler.handle_request(¤t_major_fork_bank); + + // Drain finished-scan notifications so the bounded channel can't fill + // up and block the scan thread. Non-blocking, to preserve the + // intentional overlap of clean/squash with the in-flight scan. + while scan_finished_receiver.try_recv().is_ok() {} } }, Some(Box::new(SendDroppedBankCallback::new(pruned_banks_sender))), @@ -7460,14 +7473,14 @@ fn test_store_scan_consistency_unrooted() { #[test] fn test_store_scan_consistency_root() { - let (pruned_banks_sender, pruned_banks_receiver) = unbounded(); + let (pruned_banks_sender, pruned_banks_receiver) = bounded(1024); let pruned_banks_request_handler = PrunedBanksRequestHandler { pruned_banks_receiver, }; test_store_scan_consistency( move |bank0, bank_to_scan_sender, - _scan_finished_receiver, + scan_finished_receiver, pubkeys_to_modify, program_id, starting_lamports| { @@ -7507,6 +7520,11 @@ fn test_store_scan_consistency_root() { // Move purge here so that Bank::drop()->purge_slots() doesn't race // with clean. Simulates the call from AccountsBackgroundService pruned_banks_request_handler.handle_request(¤t_bank); + + // Drain finished-scan notifications so the bounded channel can't fill + // up and block the scan thread. Non-blocking, to preserve the + // intentional overlap of clean/squash with the in-flight scan. + while scan_finished_receiver.try_recv().is_ok() {} } }, Some(Box::new(SendDroppedBankCallback::new(pruned_banks_sender))), diff --git a/runtime/src/snapshot_controller.rs b/runtime/src/snapshot_controller.rs index 671ffacab1c..88750d38663 100644 --- a/runtime/src/snapshot_controller.rs +++ b/runtime/src/snapshot_controller.rs @@ -172,7 +172,7 @@ impl SnapshotController { mod tests { use { super::*, crate::accounts_background_service::SnapshotRequestKind, - agave_snapshots::snapshot_config::SnapshotConfig, crossbeam_channel::unbounded, + agave_snapshots::snapshot_config::SnapshotConfig, crossbeam_channel::bounded, solana_genesis_config::create_genesis_config, solana_leader_schedule::SlotLeader, std::sync::Arc, test_case::test_case, }; @@ -227,7 +227,7 @@ mod tests { ..Default::default() }; - let (snapshot_request_sender, snapshot_request_receiver) = unbounded(); + let (snapshot_request_sender, snapshot_request_receiver) = bounded(1024); let snapshot_controller = SnapshotController::new(snapshot_request_sender, snapshot_config, 0); @@ -287,7 +287,7 @@ mod tests { incremental_snapshot_archive_interval, ..Default::default() }; - let (snapshot_request_sender, _snapshot_request_receiver) = unbounded(); + let (snapshot_request_sender, _snapshot_request_receiver) = bounded(1024); let snapshot_controller = SnapshotController::new(snapshot_request_sender, snapshot_config, 0); assert_eq!(snapshot_controller.is_generating_snapshots(), expected); @@ -318,7 +318,7 @@ mod tests { ..Default::default() }; - let (snapshot_request_sender, snapshot_request_receiver) = unbounded(); + let (snapshot_request_sender, snapshot_request_receiver) = bounded(1024); let snapshot_controller = SnapshotController::new(snapshot_request_sender, snapshot_config, 0); From 6be71ae8e3bd84f161222389d8aa72c2885aae5d Mon Sep 17 00:00:00 2001 From: steviez Date: Wed, 10 Jun 2026 08:54:31 -0500 Subject: [PATCH 283/576] core: Remove metrics counters from FetchStage (#13098) Replace the counters with a single datapoint that includes the 3 previous counters --- core/src/fetch_stage.rs | 72 +++++++++++++++++++++++++++++++++-------- 1 file changed, 58 insertions(+), 14 deletions(-) diff --git a/core/src/fetch_stage.rs b/core/src/fetch_stage.rs index bb02cef8c34..a686bbf78fd 100644 --- a/core/src/fetch_stage.rs +++ b/core/src/fetch_stage.rs @@ -4,7 +4,6 @@ use { crate::result::{Error, Result}, crossbeam_channel::{RecvTimeoutError, TrySendError, unbounded}, solana_clock::{DEFAULT_TICKS_PER_SLOT, HOLD_TRANSACTIONS_SLOT_OFFSET}, - solana_metrics::{inc_new_counter_debug, inc_new_counter_info}, solana_packet::PacketFlags, solana_perf::{ packet::{PacketBatch, PacketBatchRecycler, PacketRefMut}, @@ -22,10 +21,54 @@ use { atomic::{AtomicBool, Ordering}, }, thread::{self, Builder, JoinHandle, sleep}, - time::Duration, + time::{Duration, Instant}, }, }; +struct ForwardingStats { + last_report: Instant, + /// The number of packets sent along to the next stage + num_packets_sent: usize, + /// The number of packets discarded because we are not leader soon + num_packets_discarded: usize, + /// The number of packets dropped because the channel is already full + num_packets_dropped: usize, +} + +impl ForwardingStats { + const REPORT_INTERVAL: Duration = Duration::from_secs(2); + + fn new() -> Self { + Self { + last_report: Instant::now(), + num_packets_sent: 0, + num_packets_discarded: 0, + num_packets_dropped: 0, + } + } + + fn maybe_report_and_reset(&mut self) { + if self.last_report.elapsed() < Self::REPORT_INTERVAL { + return; + } + + if !self.is_empty() { + datapoint_info!( + "fetch_stage-forwards", + ("num_packets_sent", self.num_packets_sent, i64), + ("num_packets_discarded", self.num_packets_discarded, i64), + ("num_packets_dropped", self.num_packets_dropped, i64) + ); + } + + *self = Self::new(); + } + + fn is_empty(&self) -> bool { + (self.num_packets_sent + self.num_packets_discarded + self.num_packets_dropped) == 0 + } +} + pub struct FetchStage { thread_hdls: Vec>, } @@ -81,6 +124,7 @@ impl FetchStage { recvr: &PacketBatchReceiver, sendr: &PacketBatchSender, poh_recorder: &Arc>, + stats: &mut ForwardingStats, ) -> Result<()> { let mark_forwarded = |mut packet: PacketRefMut| { packet.meta_mut().flags |= PacketFlags::FORWARDED; @@ -105,26 +149,20 @@ impl FetchStage { .unwrap() .would_be_leader(HOLD_TRANSACTIONS_SLOT_OFFSET.saturating_mul(DEFAULT_TICKS_PER_SLOT)) { - let mut packets_sent = 0usize; - let mut packets_dropped = 0usize; for packet_batch in packet_batches { let packets_in_batch = packet_batch.len(); match sendr.try_send(packet_batch) { Ok(()) => { - packets_sent += packets_in_batch; + stats.num_packets_sent += packets_in_batch; } Err(TrySendError::Full(_)) => { - packets_dropped += packets_in_batch; + stats.num_packets_dropped += packets_in_batch; } Err(TrySendError::Disconnected(_)) => return Err(Error::Send), }; } - inc_new_counter_debug!("fetch_stage-honor_forwards", packets_sent); - if packets_dropped > 0 { - inc_new_counter_error!("fetch_stage-dropped_forwards", packets_dropped); - } } else { - inc_new_counter_info!("fetch_stage-discard_forwards", num_packets); + stats.num_packets_discarded += num_packets; } Ok(()) @@ -166,10 +204,15 @@ impl FetchStage { let fwd_thread_hdl = Builder::new() .name("solFetchStgFwRx".to_string()) .spawn(move || { + let mut stats = ForwardingStats::new(); + loop { - if let Err(e) = - Self::handle_forwarded_packets(&forward_receiver, &sender, &poh_recorder) - { + if let Err(e) = Self::handle_forwarded_packets( + &forward_receiver, + &sender, + &poh_recorder, + &mut stats, + ) { match e { Error::RecvTimeout(RecvTimeoutError::Disconnected) => break, Error::RecvTimeout(RecvTimeoutError::Timeout) => (), @@ -178,6 +221,7 @@ impl FetchStage { _ => error!("{e:?}"), } } + stats.maybe_report_and_reset(); } }) .unwrap(); From be8588816fd1c72b46a4059a39e1cc8969223174 Mon Sep 17 00:00:00 2001 From: Simonas Kazlauskas Date: Wed, 10 Jun 2026 16:58:00 +0300 Subject: [PATCH 284/576] runtime: optimize the memcmp syscall (#13078) * runtime: optimize the memcmp syscall We cannot use the off-shelf `libc::memcmp` because it does not guarantee any particular return value, only its return sign; and that is not suitable for use in a blockchain protocol. This implements a less naive non-scalar memcmp instead. Broad algorithm logic stays the same: find a mismatch, then determine what sort of mismatch this is. Using u128s for the type allows the compiler to use 128-bit SIMD which makes this reasonably fast already. It wouldn't be terribly difficult to extend this or change it to use 256-bit mid slices for a different tradeoff. Surprising to me is that this implementation is slightly better for slices that are less than 16 bytes long. I would have expected less speed due to the `align_to` overhead, but turns out this rewrite of the code makes the rest of the code easier-enough to optimize to the point where the the `align_to` no longer even registers. See the PR for nice violin plots. * add bigendian support For the heck of it I guess? * fix bigendian? --- syscalls/src/mem_ops.rs | 59 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 54 insertions(+), 5 deletions(-) diff --git a/syscalls/src/mem_ops.rs b/syscalls/src/mem_ops.rs index 6beb79d3980..40461bfef0b 100644 --- a/syscalls/src/mem_ops.rs +++ b/syscalls/src/mem_ops.rs @@ -159,13 +159,62 @@ fn memmove( // Marked unsafe since it assumes that the slices are at least `n` bytes long. unsafe fn memcmp(s1: &[u8], s2: &[u8], n: usize) -> i32 { - for i in 0..n { + let (s1pre, s1mid, s1end) = unsafe { + // SAFETY: Caller is required to guarantee both slices are at least n-long. + s1.get_unchecked(..n).align_to::() + }; + let mut s2ptr = s2.as_ptr(); + for s1pre_byte in s1pre.iter().copied() { unsafe { - let a = *s1.get_unchecked(i); - let b = *s2.get_unchecked(i); - if a != b { - return (a as i32).saturating_sub(b as i32); + // SAFETY: we are guaranteed to stay in bounds of a slice `s2` by virtue of both slices + // containing at least `n` bytes (caller precondition.) + let s2pre_byte = *s2ptr; + if s1pre_byte != s2pre_byte { + return i32::from(s1pre_byte).wrapping_sub(s2pre_byte.into()); + } + s2ptr = s2ptr.add(1); + } + } + for s1mid_value in s1mid.iter().copied() { + let s2mid_value = unsafe { + // SAFETY: Caller is required to guarantee both slices are at least n-long. + // SAFETY: Pointer is guaranteed to be dereferenceable by virtue of being derived from + // `s2` slice. + s2ptr.cast::().read_unaligned().to_le() + }; + if s1mid_value != s2mid_value { + // It would seem that we could work with u128s directly here and leave it to LLVM to + // figure out how to split up the operations to u64s, but it seems to produce notably + // worse code than splitting the u64s out manually (even _when_ these splits result in + // the values being re-read from "memory"). + let (s1_word, s2_word) = if s1mid_value as u64 != s2mid_value as u64 { + let w1 = s1mid_value as u64; + let w2 = s2mid_value as u64; + (w1, w2) + } else { + let w1 = (s1mid_value >> 64) as u64; + let w2 = (s2mid_value >> 64) as u64; + (w1, w2) }; + let shift = (s1_word ^ s2_word).trailing_zeros() & !7; + let b1 = (s1_word >> shift) as u8; + let b2 = (s2_word >> shift) as u8; + return i32::from(b1).wrapping_sub(b2.into()); + } + unsafe { + // SAFETY: we are guaranteed to stay in bounds of a slice `s2` by virtue of both slices + // containing at least `n` bytes (caller precondition.) + s2ptr = s2ptr.add(std::mem::size_of::()); + } + } + for s1end_byte in s1end.iter().copied() { + unsafe { + // This is the same as the `pre` slice loop above. + let s2end_byte = *s2ptr; + if s1end_byte != s2end_byte { + return i32::from(s1end_byte).wrapping_sub(s2end_byte.into()); + } + s2ptr = s2ptr.add(1); } } 0 From 3477fa2534a7848446ada6b0e126c525a57d735b Mon Sep 17 00:00:00 2001 From: Yihau Chen Date: Wed, 10 Jun 2026 22:14:38 +0800 Subject: [PATCH 285/576] fix: restore dcou gated formatting (#13110) * fix dcou enabling * enable solana-tpu-client-next/dev-context-only-utils in solana-core/dev-context-only-utils --- core/Cargo.toml | 1 + local-cluster/Cargo.toml | 12 ++++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/core/Cargo.toml b/core/Cargo.toml index bf171f15265..cef346f4b52 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -20,6 +20,7 @@ dev-context-only-utils = [ "solana-perf/dev-context-only-utils", "solana-runtime/dev-context-only-utils", "solana-streamer/dev-context-only-utils", + "solana-tpu-client-next/dev-context-only-utils", "agave-votor/dev-context-only-utils", ] frozen-abi = [ diff --git a/local-cluster/Cargo.toml b/local-cluster/Cargo.toml index 3d72c0a9957..95a892743b1 100644 --- a/local-cluster/Cargo.toml +++ b/local-cluster/Cargo.toml @@ -14,14 +14,18 @@ targets = ["x86_64-unknown-linux-gnu"] [features] agave-unstable-api = [] -dev-context-only-utils = [] +dev-context-only-utils = [ + "solana-core/dev-context-only-utils", + "agave-votor-messages/dev-context-only-utils", + "solana-tpu-client-next/dev-context-only-utils", +] [dependencies] agave-feature-set = { workspace = true } agave-logger = { workspace = true } agave-snapshots = { workspace = true } agave-votor = { workspace = true } -agave-votor-messages = { workspace = true, features = ["dev-context-only-utils"] } +agave-votor-messages = { workspace = true } crossbeam-channel = { workspace = true } itertools = { workspace = true } log = { workspace = true } @@ -34,7 +38,7 @@ solana-client-traits = { workspace = true } solana-clock = { workspace = true } solana-cluster-type = { workspace = true } solana-commitment-config = { workspace = true } -solana-core = { workspace = true, features = ["dev-context-only-utils"] } +solana-core = { workspace = true } solana-entry = { workspace = true } solana-epoch-schedule = { workspace = true } solana-feature-gate-interface = { workspace = true } @@ -68,7 +72,7 @@ solana-system-interface = { workspace = true } solana-system-transaction = { workspace = true } solana-time-utils = { workspace = true } solana-tpu-client = { workspace = true } -solana-tpu-client-next = { workspace = true, features = ["dev-context-only-utils"] } +solana-tpu-client-next = { workspace = true } solana-transaction = { workspace = true } solana-transaction-error = { workspace = true } solana-turbine = { workspace = true } From 4417fda243cdf28f9593565b90ff65283146574a Mon Sep 17 00:00:00 2001 From: kirill lykov Date: Wed, 10 Jun 2026 18:50:16 +0200 Subject: [PATCH 286/576] xdp: decouple XDP handoff from retransmit port (#13047) The previous XDP handoff passed a retransmit socket address from execute.rs into validator.rs. That tied the shared XDP transmitter setup to the retransmit port and made it harder to reuse for other protocols such as gossip or repair. Pass the source IP with the transmitter builder instead, and let validator.rs derive protocol-specific source ports from its node sockets. --- core/src/validator.rs | 33 +++++++++---- validator/src/commands/run/execute.rs | 67 +++++++++++++-------------- 2 files changed, 58 insertions(+), 42 deletions(-) diff --git a/core/src/validator.rs b/core/src/validator.rs index 160eb923341..e1366849e3c 100644 --- a/core/src/validator.rs +++ b/core/src/validator.rs @@ -152,7 +152,7 @@ use { borrow::Cow, cmp, collections::{HashMap, HashSet}, - net::{SocketAddr, SocketAddrV4}, + net::{Ipv4Addr, SocketAddr, SocketAddrV4}, num::{NonZeroU64, NonZeroUsize}, path::{Path, PathBuf}, str::FromStr, @@ -532,6 +532,11 @@ pub enum ValidatorStartProgress { Running, } +pub struct XdpTransmitSetup { + pub transmitter_builder: TransmitterBuilder, + pub src_ip: Ipv4Addr, +} + struct BlockstoreRootScan { thread: Option>>, } @@ -691,7 +696,7 @@ impl Validator { socket_addr_space: SocketAddrSpace, tpu_config: ValidatorTpuConfig, admin_rpc_service_post_init: Arc>>, - xdp_builder_with_src_addr: Option<(TransmitterBuilder, SocketAddrV4)>, + xdp_transmit_setup: Option, ) -> Result { let exit = Arc::new(AtomicBool::new(false)); Self::new_with_exit( @@ -707,7 +712,7 @@ impl Validator { socket_addr_space, tpu_config, admin_rpc_service_post_init, - xdp_builder_with_src_addr, + xdp_transmit_setup, exit, ) } @@ -726,7 +731,7 @@ impl Validator { socket_addr_space: SocketAddrSpace, tpu_config: ValidatorTpuConfig, admin_rpc_service_post_init: Arc>>, - xdp_builder_with_src_addr: Option<(TransmitterBuilder, SocketAddrV4)>, + xdp_transmit_setup: Option, exit: Arc, ) -> Result { #[cfg(debug_assertions)] @@ -1556,12 +1561,24 @@ impl Validator { let (votor_event_sender, votor_event_receiver) = bounded(1000); let (xdp_transmitter, turbine_xdp_sender, quic_xdp_sender) = - if let Some((xdp_transmit_builder, src_addr)) = xdp_builder_with_src_addr { - let (transmitter, sender) = xdp_transmit_builder.build(); + if let Some(XdpTransmitSetup { + transmitter_builder, + src_ip, + }) = xdp_transmit_setup + { + let turbine_src_port = node.sockets.retransmit_sockets[0] + .local_addr() + .expect("retransmit socket should have local address") + .port(); + + let (transmitter, sender) = transmitter_builder.build(); ( Some(transmitter), - Some(TurbineXdpSender::new(sender.clone(), src_addr)), - Some((sender, *src_addr.ip())), + Some(TurbineXdpSender::new( + sender.clone(), + SocketAddrV4::new(src_ip, turbine_src_port), + )), + Some((sender, src_ip)), ) } else { (None, None, None) diff --git a/validator/src/commands/run/execute.rs b/validator/src/commands/run/execute.rs index 4a43fa3a41c..f6ab90d6dde 100644 --- a/validator/src/commands/run/execute.rs +++ b/validator/src/commands/run/execute.rs @@ -165,26 +165,29 @@ pub fn execute( } } - let xdp_interface = matches - .value_of("xdp_interface") - .or_else(|| matches.value_of("experimental_retransmit_xdp_interface")); - let xdp_zero_copy = matches.is_present("xdp_zero_copy") - || matches.is_present("experimental_retransmit_xdp_zero_copy"); - let retransmit_xdp = matches + let xdp_transmit_config = if let Some(xdp_cpu_cores) = matches .value_of("xdp_cpu_cores") .or_else(|| matches.value_of("experimental_retransmit_xdp_cpu_cores")) - .map(|cpus| { - XdpConfig::new( - xdp_interface, - parse_cpu_ranges(cpus).unwrap(), - xdp_zero_copy, - ) - }); - if bind_addresses.len() > 1 && retransmit_xdp.is_some() { - Err(String::from( - "--xdp-cpu-cores cannot be used in a multihoming context", - ))?; - } + { + let xdp_interface = matches + .value_of("xdp_interface") + .or_else(|| matches.value_of("experimental_retransmit_xdp_interface")); + let xdp_zero_copy = matches.is_present("xdp_zero_copy") + || matches.is_present("experimental_retransmit_xdp_zero_copy"); + let config = XdpConfig::new( + xdp_interface, + parse_cpu_ranges(xdp_cpu_cores).unwrap(), + xdp_zero_copy, + ); + if bind_addresses.len() > 1 { + Err(String::from( + "--xdp-cpu-cores cannot be used in a multihoming context", + ))?; + } + Some(config) + } else { + None + }; let dynamic_port_range = solana_net_utils::parse_port_range(matches.value_of("dynamic_port_range").unwrap()) @@ -287,7 +290,7 @@ pub fn execute( let _ = config; #[cfg(target_os = "linux")] - let xdp_builder_with_src_addr = { + let xdp_transmit_setup = { use { agave_xdp::transmitter::TransmitterBuilder, caps::{ @@ -313,7 +316,7 @@ pub fn execute( required_caps.extend(primordial_caps.clone()); retained_caps.extend(primordial_caps.clone()); - if let Some(xdp_config) = retransmit_xdp.as_ref() { + if let Some(xdp_config) = xdp_transmit_config.as_ref() { required_caps.insert(CAP_NET_ADMIN); required_caps.insert(CAP_NET_RAW); if xdp_config.zero_copy { @@ -369,16 +372,12 @@ pub fn execute( // XDP _MUST_ be setup _BEFORE_ the app spawns any threads to ensure linux // capabilities do not leak, leaving the process in a state where it could // potentially be used as a privilege escalation gadget - let xdp_builder_with_src_addr = retransmit_xdp.clone().map(|xdp_config| { + let xdp_transmit_setup = xdp_transmit_config.clone().map(|xdp_config| { use { agave_xdp::{default_device_ipv4, interface_ipv4}, - std::net::SocketAddrV4, + solana_core::validator::XdpTransmitSetup, }; - let src_port = node.sockets.retransmit_sockets[0] - .local_addr() - .expect("failed to get local address") - .port(); let src_ip = match node.bind_ip_addrs.active() { IpAddr::V4(ip) if !ip.is_unspecified() => ip, IpAddr::V4(_unspecified) => { @@ -394,11 +393,11 @@ pub fn execute( } _ => panic!("IPv6 not supported"), }; - ( - TransmitterBuilder::new(xdp_config, exit.clone()) + XdpTransmitSetup { + transmitter_builder: TransmitterBuilder::new(xdp_config, exit.clone()) .expect("failed to create xdp transmitter"), - SocketAddrV4::new(src_ip, src_port), - ) + src_ip, + } }); // we're done with caps needed to init xdp now. remove them from our process @@ -407,13 +406,13 @@ pub fn execute( caps::set(None, CapSet::Permitted, &retained_caps) .expect("linux allows permitted capset to be set"); - xdp_builder_with_src_addr + xdp_transmit_setup }; #[cfg(not(target_os = "linux"))] - let xdp_builder_with_src_addr = None; + let xdp_transmit_setup = None; - let reserved = retransmit_xdp + let reserved = xdp_transmit_config .map(|xdp| xdp.cpus.clone()) .unwrap_or_default() .iter() @@ -1117,7 +1116,7 @@ pub fn execute( sigverify_threads: tpu_sigverify_threads, }, admin_service_post_init, - xdp_builder_with_src_addr, + xdp_transmit_setup, exit, ) .map_err(|err| format!("{err:?}"))?; From 3c0ba1a961bc2122026340e40fb8009f99ce351b Mon Sep 17 00:00:00 2001 From: Kamil Skalski Date: Wed, 10 Jun 2026 21:56:55 +0200 Subject: [PATCH 287/576] clippy: move allow of collapsible_if to Cargo.toml lint definitions (#12680) * Add allows to Cargo.toml * Remove allow from clippy script --- Cargo.toml | 2 ++ dev-bins/Cargo.toml | 2 ++ scripts/cargo-clippy-nightly.sh | 5 +---- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ab0d04547dd..e4e142bfc5b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -164,6 +164,8 @@ used_underscore_binding = "deny" # Allowed lints new_without_default = "allow" +## Due to recent switch to Rust 2024, should be removed after repo auto-fix before release cut +collapsible_if = "allow" [workspace.dependencies] Inflector = "0.11.4" diff --git a/dev-bins/Cargo.toml b/dev-bins/Cargo.toml index eb10ac9594e..c3941023f7e 100644 --- a/dev-bins/Cargo.toml +++ b/dev-bins/Cargo.toml @@ -29,6 +29,8 @@ used_underscore_binding = "deny" # Allowed lints new_without_default = "allow" +## Due to recent switch to Rust 2024, should be removed after repo auto-fix before release cut +collapsible_if = "allow" [workspace.dependencies] agave-feature-set = { path = "../feature-set", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } diff --git a/scripts/cargo-clippy-nightly.sh b/scripts/cargo-clippy-nightly.sh index 3413e7b3a53..7a63529cb92 100755 --- a/scripts/cargo-clippy-nightly.sh +++ b/scripts/cargo-clippy-nightly.sh @@ -22,8 +22,6 @@ source "$here/../ci/rust-version.sh" nightly # Similarly, nightly is desired to run clippy over all of bench files because # the bench itself isn't stabilized yet... # ref: https://github.com/rust-lang/rust/issues/66287 -# -# allow arguments are temporary, violations should be fixed after migration to Rust 2024 "$here/cargo-for-all-lock-files.sh" -- \ "+${rust_nightly}" clippy \ --workspace --all-targets --features dummy-for-ci-check,frozen-abi -- \ @@ -32,5 +30,4 @@ source "$here/../ci/rust-version.sh" nightly --deny=clippy::arithmetic_side_effects \ --deny=clippy::manual_let_else \ --deny=clippy::uninlined-format-args \ - --deny=clippy::used_underscore_binding \ - --allow=clippy::collapsible_if + --deny=clippy::used_underscore_binding From a9d371a68a9c904e733f228400b2a5fb14590993 Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Wed, 10 Jun 2026 16:24:04 -0400 Subject: [PATCH 288/576] startup replay: enforce bank hash mismatch check (#13118) --- ledger/src/blockstore_processor.rs | 51 ++++++++++++++++++------------ 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/ledger/src/blockstore_processor.rs b/ledger/src/blockstore_processor.rs index 4835261faa0..7a7e0ab14ea 100644 --- a/ledger/src/blockstore_processor.rs +++ b/ledger/src/blockstore_processor.rs @@ -2765,8 +2765,21 @@ pub fn check_chained_block_id( } } +fn mark_dead_if_primary_access(blockstore: &Blockstore, slot: Slot) { + if blockstore.is_primary_access() { + blockstore + .set_dead_slot(slot) + .expect("Failed to mark slot as dead in blockstore"); + } else { + info!("Failed slot {slot} won't be marked dead due to being read-only blockstore access"); + } +} + // Processes and replays the contents of a single slot, returns Error -// if failed to play the slot +// if failed to play the slot. +// +// For use during startup replay, enforces any pre and post replay checks +// that occur in ReplayStage. #[allow(clippy::too_many_arguments)] pub fn process_single_slot( blockstore: &Blockstore, @@ -2789,16 +2802,7 @@ pub fn process_single_slot( } ChainedBlockIdCheck::Mismatch => { // Mismatch, mark dead - if blockstore.is_primary_access() { - blockstore - .set_dead_slot(slot) - .expect("Failed to mark slot as dead in blockstore"); - } else { - info!( - "Failed slot {slot} won't be marked dead due to being read-only blockstore \ - access" - ); - } + mark_dead_if_primary_access(blockstore, slot); return Err(BlockstoreProcessorError::ChainedBlockIdFailure( slot, bank.parent_slot(), @@ -2822,15 +2826,7 @@ pub fn process_single_slot( ) .map_err(|err| { warn!("slot {slot} failed to verify: {err}"); - if blockstore.is_primary_access() { - blockstore - .set_dead_slot(slot) - .expect("Failed to mark slot as dead in blockstore"); - } else { - info!( - "Failed slot {slot} won't be marked dead due to being read-only blockstore access" - ); - } + mark_dead_if_primary_access(blockstore, slot); err })?; @@ -2839,7 +2835,20 @@ pub fn process_single_slot( .expect("Blockstore operations must succeed") .expect("Full block must have block id"); bank.set_block_id(Some(block_id)); - bank.freeze(); // all banks handled by this routine are created from complete slots + let verify_result = bank.freeze_and_verify_bank_hash(); // all banks handled by this routine are created from complete slots + + if let Err((expected_hash, computed_hash)) = verify_result { + warn!( + "slot {slot} failed to freeze, bank hash mismatch expected {expected_hash} computed \ + {computed_hash}" + ); + mark_dead_if_primary_access(blockstore, slot); + return Err(BlockstoreProcessorError::BankHashMismatch( + slot, + expected_hash, + computed_hash, + )); + } if let Some(slot_callback) = &opts.slot_callback { slot_callback(bank); From f0c9a8d0af97f830724bfde62a4abb4297e594ea Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Wed, 10 Jun 2026 16:42:10 -0400 Subject: [PATCH 289/576] retransmit: cleanup too many arguments (#13119) --- turbine/src/retransmit_stage.rs | 163 +++++++++++++++++--------------- 1 file changed, 88 insertions(+), 75 deletions(-) diff --git a/turbine/src/retransmit_stage.rs b/turbine/src/retransmit_stage.rs index 25c64edec34..3e2359dd0c6 100644 --- a/turbine/src/retransmit_stage.rs +++ b/turbine/src/retransmit_stage.rs @@ -119,6 +119,43 @@ struct RetransmitStats { unknown_shred_slot_leader: usize, } +struct RetransmitState { + stats: RetransmitStats, + addr_cache: AddrCache, + shred_buf: Vec>, +} + +struct RetransmitNotifiers { + rpc_subscriptions: Option>, + slot_status_notifier: Option, + migration_status: Arc, + votor_event_sender: Sender, +} + +struct RetransmitContext { + thread_pool: ThreadPool, + bank_forks: Arc>, + leader_schedule_cache: Arc, + cluster_info: Arc, + retransmit_receiver: Receiver>, + retransmit_sockets: Arc>, + xdp_sender: Option, + cluster_nodes_cache: ClusterNodesCache, + shred_deduper: ShredDeduper, + max_slots: Arc, + notifiers: RetransmitNotifiers, +} + +impl RetransmitState { + fn new(now: Instant) -> Self { + Self { + stats: RetransmitStats::new(now), + addr_cache: AddrCache::with_capacity(/*capacity:*/ 4), + shred_buf: Vec::with_capacity(RETRANSMIT_BATCH_SIZE), + } + } +} + impl RetransmitStats { fn maybe_submit( &mut self, @@ -279,26 +316,23 @@ const RETRANSMIT_BATCH_SIZE: usize = 4096; // pull the shreds from the shreds_receiver until empty, then retransmit them. // uses a thread_pool to parallelize work if there are enough shreds to justify that -#[allow(clippy::too_many_arguments)] -fn retransmit( - thread_pool: &ThreadPool, - bank_forks: &RwLock, - leader_schedule_cache: &LeaderScheduleCache, - cluster_info: &ClusterInfo, - retransmit_receiver: &Receiver>, - retransmit_sockets: &[UdpSocket], - xdp_sender: Option<&XdpSender>, - stats: &mut RetransmitStats, - cluster_nodes_cache: &ClusterNodesCache, - addr_cache: &mut AddrCache, - shred_deduper: &ShredDeduper, - max_slots: &MaxSlots, - rpc_subscriptions: Option<&RpcSubscriptions>, - slot_status_notifier: Option<&SlotStatusNotifier>, - shred_buf: &mut Vec>, - votor_event_sender: &Sender, - migration_status: &MigrationStatus, -) -> Result<(), ()> { +fn retransmit(context: &RetransmitContext, state: &mut RetransmitState) -> Result<(), ()> { + let thread_pool = &context.thread_pool; + let bank_forks = context.bank_forks.as_ref(); + let leader_schedule_cache = context.leader_schedule_cache.as_ref(); + let cluster_info = context.cluster_info.as_ref(); + let retransmit_receiver = &context.retransmit_receiver; + let retransmit_sockets = context.retransmit_sockets.as_slice(); + let xdp_sender = context.xdp_sender.as_ref(); + let cluster_nodes_cache = &context.cluster_nodes_cache; + let shred_deduper = &context.shred_deduper; + let max_slots = context.max_slots.as_ref(); + let RetransmitState { + stats, + addr_cache, + shred_buf, + } = state; + // Try to receive shreds from the channel without blocking. If the channel // is empty precompute turbine trees speculatively. If no cache updates are // made then block on the channel until some shreds are received. @@ -425,15 +459,7 @@ fn retransmit( }) }; - stats.upsert_slot_stats( - slot_stats, - root_bank.slot(), - addr_cache, - rpc_subscriptions, - slot_status_notifier, - migration_status, - votor_event_sender, - )?; + stats.upsert_slot_stats(slot_stats, root_bank.slot(), addr_cache, &context.notifiers)?; timer_start.stop(); stats.total_time += timer_start.as_us(); stats.maybe_submit( @@ -645,8 +671,6 @@ impl RetransmitStage { CLUSTER_NODES_CACHE_TTL, ); let mut rng = rand::rng(); - let mut stats = RetransmitStats::new(Instant::now()); - let mut addr_cache = AddrCache::with_capacity(/*capacity:*/ 4); let shred_deduper = ShredDeduper::new(&mut rng, DEDUPER_NUM_BITS); let thread_pool = { @@ -658,32 +682,31 @@ impl RetransmitStage { .unwrap() }; + let retransmit_context = RetransmitContext { + thread_pool, + bank_forks, + leader_schedule_cache, + cluster_info, + retransmit_receiver, + retransmit_sockets, + xdp_sender, + cluster_nodes_cache, + shred_deduper, + max_slots, + notifiers: RetransmitNotifiers { + rpc_subscriptions, + slot_status_notifier, + migration_status, + votor_event_sender, + }, + }; + let retransmit_thread_handle = Builder::new() .name("solRetransmittr".to_string()) .spawn({ move || { - let mut shred_buf = Vec::with_capacity(RETRANSMIT_BATCH_SIZE); - while retransmit( - &thread_pool, - &bank_forks, - &leader_schedule_cache, - &cluster_info, - &retransmit_receiver, - &retransmit_sockets, - xdp_sender.as_ref(), - &mut stats, - &cluster_nodes_cache, - &mut addr_cache, - &shred_deduper, - &max_slots, - rpc_subscriptions.as_deref(), - slot_status_notifier.as_ref(), - &mut shred_buf, - &votor_event_sender, - &migration_status, - ) - .is_ok() - {} + let mut retransmit_state = RetransmitState::new(Instant::now()); + while retransmit(&retransmit_context, &mut retransmit_state).is_ok() {} } }) .unwrap(); @@ -762,24 +785,14 @@ impl RetransmitStats { feed: impl IntoIterator, root: Slot, addr_cache: &mut AddrCache, - rpc_subscriptions: Option<&RpcSubscriptions>, - slot_status_notifier: Option<&SlotStatusNotifier>, - migration_status: &MigrationStatus, - votor_event_sender: &Sender, + notifiers: &RetransmitNotifiers, ) -> Result<(), ()> { for (slot, mut slot_stats) in feed { addr_cache.record(slot, &mut slot_stats); match self.slot_stats.get_mut(&slot) { None => { if slot > root { - notify_subscribers( - slot, - slot_stats.outset, - rpc_subscriptions, - slot_status_notifier, - migration_status, - votor_event_sender, - )?; + notify_subscribers(slot, slot_stats.outset, notifiers)?; } self.slot_stats.put(slot, slot_stats); } @@ -871,32 +884,32 @@ impl RetransmitSlotStats { fn notify_subscribers( slot: Slot, timestamp: u64, // When the first shred in the slot was received. - rpc_subscriptions: Option<&RpcSubscriptions>, - slot_status_notifier: Option<&SlotStatusNotifier>, - migration_status: &MigrationStatus, - votor_event_sender: &Sender, + notifiers: &RetransmitNotifiers, ) -> Result<(), ()> { - if let Some(rpc_subscriptions) = rpc_subscriptions { + if let Some(rpc_subscriptions) = notifiers.rpc_subscriptions.as_ref() { let slot_update = SlotUpdate::FirstShredReceived { slot, timestamp }; rpc_subscriptions.notify_slot_update(slot_update); datapoint_info!("retransmit-first-shred", ("slot", slot, i64)); } - if let Some(slot_status_notifier) = slot_status_notifier { + if let Some(slot_status_notifier) = notifiers.slot_status_notifier.as_ref() { slot_status_notifier .read() .unwrap() .notify_first_shred_received(slot); } - if migration_status.should_send_votor_event(slot) { - match votor_event_sender.try_send(VotorEvent::FirstShred(slot)) { + if notifiers.migration_status.should_send_votor_event(slot) { + match notifiers + .votor_event_sender + .try_send(VotorEvent::FirstShred(slot)) + { Ok(()) => (), Err(TrySendError::Full(event)) => { error!( "Votor event channel is backed up len {}, something is wrong, blocking", - votor_event_sender.len(), + notifiers.votor_event_sender.len(), ); - let _ = votor_event_sender.send(event); + let _ = notifiers.votor_event_sender.send(event); } Err(TrySendError::Disconnected(_)) => { info!("Votor event channel disconnectioned, we are shutting down") From 79a95d7961456c858b85d5609fb2830d36bb13dd Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Wed, 10 Jun 2026 18:35:41 -0400 Subject: [PATCH 290/576] bcp: enforce clock bounds on first alpenglow block (#13120) --- ledger/src/blockstore_processor.rs | 9 ++++- runtime/src/block_component_processor.rs | 45 ++++++++++++++++++++---- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/ledger/src/blockstore_processor.rs b/ledger/src/blockstore_processor.rs index 7a7e0ab14ea..8bbca503cbd 100644 --- a/ledger/src/blockstore_processor.rs +++ b/ledger/src/blockstore_processor.rs @@ -6127,9 +6127,16 @@ pub mod tests { }); let header_component = BlockComponent::new_block_marker(header); + let block_producer_time_nanos = u64::try_from( + genesis_config + .creation_time + .saturating_mul(1_000_000_000) + .saturating_add(1), + ) + .unwrap(); let footer = VersionedBlockMarker::from_block_footer(BlockFooterV1 { bank_hash: Hash::new_unique(), - block_producer_time_nanos: 1_000_000_000, + block_producer_time_nanos, block_user_agent: b"test".to_vec(), block_final_cert: None, skip_reward_cert: None, diff --git a/runtime/src/block_component_processor.rs b/runtime/src/block_component_processor.rs index 1245fdb77bc..0ff98f8dda6 100644 --- a/runtime/src/block_component_processor.rs +++ b/runtime/src/block_component_processor.rs @@ -454,12 +454,11 @@ impl BlockComponentProcessor { parent_bank: &Bank, footer: &BlockFooterV1, ) -> Result<(), BlockComponentProcessorError> { - // Get parent time from nanosecond clock account - // If nanosecond clock hasn't been populated, don't enforce the bounds; note that the - // nanosecond clock is populated as soon as Alpenglow migration is complete. - let Some(parent_time_nanos) = parent_bank.get_nanosecond_clock() else { - return Ok(()); - }; + // Get parent time from the nanosecond clock account, or from the Tower-based + // clock for the first Alpenglow block. + let parent_time_nanos = parent_bank + .get_nanosecond_clock() + .unwrap_or_else(|| bank.clock().unix_timestamp.saturating_mul(1_000_000_000)); let parent_slot = parent_bank.slot(); let current_time_nanos = @@ -1216,6 +1215,40 @@ mod tests { test_clock_bounds_helper(1, |parent_time, _, _| parent_time, false); } + #[test] + fn test_clock_bounds_without_parent_nanosecond_clock_rejects_out_of_bounds() { + let mut processor = BlockComponentProcessor { + has_header: true, + ..Default::default() + }; + + let (parent, bank_forks) = create_test_bank_alpenglow(); + assert_eq!(parent.get_nanosecond_clock(), None); + + let bank = create_child_bank(&bank_forks, &parent, 1); + let parent_time_nanos = bank.clock().unix_timestamp.saturating_mul(1_000_000_000); + let elapsed_slot_duration_nanos = + bank.slot_range_duration_nanos(parent.slot().saturating_add(1), bank.slot()); + let (_, upper_bound) = BlockComponentProcessor::nanosecond_time_bounds( + parent_time_nanos, + elapsed_slot_duration_nanos, + ); + + let footer = VersionedBlockFooter::V1(BlockFooterV1 { + bank_hash: Hash::new_unique(), + block_producer_time_nanos: u64::try_from(upper_bound.saturating_add(1)).unwrap(), + block_user_agent: vec![], + block_final_cert: None, + skip_reward_cert: None, + notar_reward_cert: None, + }); + + assert!(matches!( + processor.on_footer(bank, parent, footer, None), + Err(BlockComponentProcessorError::NanosecondClockOutOfBounds) + )); + } + #[test] fn test_clock_bounds_rejects_timestamp_above_i64() { let mut processor = BlockComponentProcessor { From 0355b6566140553465b66e5bad7589038ebd2508 Mon Sep 17 00:00:00 2001 From: Alex Pyattaev Date: Thu, 11 Jun 2026 02:13:18 +0300 Subject: [PATCH 291/576] connection cache: bump max connections to 2x vote accounts (#13075) connection cache: make max_connections configurable, set to 2x vote accounts for votor --- client/src/connection_cache.rs | 30 ++++++++++++-- connection-cache/src/connection_cache.rs | 51 ++++++++++++++---------- core/src/validator.rs | 6 ++- 3 files changed, 61 insertions(+), 26 deletions(-) diff --git a/client/src/connection_cache.rs b/client/src/connection_cache.rs index b86d794ddf1..75f5200104d 100644 --- a/client/src/connection_cache.rs +++ b/client/src/connection_cache.rs @@ -1,4 +1,4 @@ -pub use solana_connection_cache::connection_cache::Protocol; +pub use solana_connection_cache::connection_cache::{DEFAULT_MAX_CONNECTIONS, Protocol}; use { solana_connection_cache::{ client_connection::ClientConnection, @@ -92,6 +92,25 @@ impl ConnectionCache { client_socket: Option, cert_info: Option<(&Keypair, IpAddr)>, stake_info: Option<(&Arc>, &Pubkey)>, + ) -> Self { + Self::new_with_max_connections( + name, + connection_pool_size, + DEFAULT_MAX_CONNECTIONS, + client_socket, + cert_info, + stake_info, + ) + } + + /// Create a quic connection_cache with configurable max connections + pub fn new_with_max_connections( + name: &'static str, + connection_pool_size: usize, + max_connections: usize, + client_socket: Option, + cert_info: Option<(&Keypair, IpAddr)>, + stake_info: Option<(&Arc>, &Pubkey)>, ) -> Self { // The minimum pool size is 1. let connection_pool_size = 1.max(connection_pool_size); @@ -106,8 +125,13 @@ impl ConnectionCache { config.set_staked_nodes(stake_info.0, stake_info.1); } let connection_manager = QuicConnectionManager::new_with_connection_config(config); - let cache = - BackendConnectionCache::new(name, connection_manager, connection_pool_size).unwrap(); + let cache = BackendConnectionCache::new_with_max_connections( + name, + connection_manager, + connection_pool_size, + max_connections, + ) + .unwrap(); Self::Quic(Arc::new(cache)) } diff --git a/connection-cache/src/connection_cache.rs b/connection-cache/src/connection_cache.rs index 74dc32e7c8f..8a5f3f34182 100644 --- a/connection-cache/src/connection_cache.rs +++ b/connection-cache/src/connection_cache.rs @@ -19,8 +19,8 @@ use { thiserror::Error, }; -// Should be non-zero -const MAX_CONNECTIONS: usize = 1024; +/// Default maximum number of connections to keep +pub const DEFAULT_MAX_CONNECTIONS: usize = 1024; /// Default connection pool size per remote address pub const DEFAULT_CONNECTION_POOL_SIZE: usize = 2; @@ -53,6 +53,7 @@ pub struct ConnectionCache< stats: Arc, last_stats: AtomicInterval, connection_pool_size: usize, + max_connections: usize, connection_config: Arc, sender: Sender<(usize, SocketAddr)>, } @@ -68,26 +69,29 @@ where connection_manager: M, connection_pool_size: usize, ) -> Result { - let config = connection_manager.new_connection_config(); - Ok(Self::new_with_config( + Self::new_with_max_connections( name, - connection_pool_size, - config, connection_manager, - )) + connection_pool_size, + DEFAULT_MAX_CONNECTIONS, + ) } - pub fn new_with_config( + pub fn new_with_max_connections( name: &'static str, - connection_pool_size: usize, - connection_config: C, connection_manager: M, - ) -> Self { - info!("Creating ConnectionCache {name}, pool size: {connection_pool_size}"); + connection_pool_size: usize, + max_connections: usize, + ) -> Result { + let config = Arc::new(connection_manager.new_connection_config()); + let max_connections = 1.max(max_connections); // The minimum is 1. + info!( + "Creating ConnectionCache {name}, pool size: {connection_pool_size}, max connections: \ + {max_connections}" + ); let (sender, receiver) = crossbeam_channel::unbounded(); - let map = Arc::new(RwLock::new(IndexMap::with_capacity(MAX_CONNECTIONS))); - let config = Arc::new(connection_config); + let map = Arc::new(RwLock::new(IndexMap::with_capacity(max_connections))); let connection_manager = Arc::new(connection_manager); let connection_pool_size = 1.max(connection_pool_size); // The minimum pool size is 1. @@ -95,16 +99,17 @@ where let _async_connection_thread = Self::create_connection_async_thread(map.clone(), receiver, stats.clone()); - Self { + Ok(Self { name, map, stats, connection_manager, last_stats: AtomicInterval::default(), connection_pool_size, + max_connections, connection_config: config, sender, - } + }) } /// This actually triggers the connection creation by sending empty data @@ -174,6 +179,7 @@ where &mut map, addr, self.connection_pool_size, + self.max_connections, None, ) } else { @@ -189,6 +195,7 @@ where &mut map, addr, self.connection_pool_size, + self.max_connections, Some(&self.sender), ); } @@ -211,6 +218,7 @@ where map: &mut std::sync::RwLockWriteGuard<'_, IndexMap>, addr: &SocketAddr, connection_pool_size: usize, + max_connections: usize, async_connection_sender: Option<&Sender<(usize, SocketAddr)>>, ) -> (bool, u64, u64) { // evict a connection if the cache is reaching upper bounds @@ -218,9 +226,9 @@ where let mut get_connection_cache_eviction_measure = Measure::start("get_connection_cache_eviction_measure"); let existing_index = map.get_index_of(addr); - while map.len() >= MAX_CONNECTIONS { + while map.len() >= max_connections { let mut rng = rng(); - let n = rng.random_range(0..MAX_CONNECTIONS); + let n = rng.random_range(0..max_connections); if let Some(index) = existing_index { if n == index { continue; @@ -304,6 +312,7 @@ where &mut map, addr, self.connection_pool_size, + self.max_connections, Some(&self.sender), ); } @@ -709,7 +718,7 @@ mod tests { DEFAULT_CONNECTION_POOL_SIZE, ) .unwrap(); - let addrs = (0..MAX_CONNECTIONS) + let addrs = (0..DEFAULT_MAX_CONNECTIONS) .map(|_| { let addr = get_addr(&mut rng); connection_cache.get_connection(&addr); @@ -718,7 +727,7 @@ mod tests { .collect::>(); { let map = connection_cache.map.read().unwrap(); - assert!(map.len() == MAX_CONNECTIONS); + assert!(map.len() == DEFAULT_MAX_CONNECTIONS); addrs.iter().for_each(|addr| { let conn = &map.get(addr).expect("Address not found").get(0).unwrap(); let conn = conn.new_blocking_connection(*addr, connection_cache.stats.clone()); @@ -739,7 +748,7 @@ mod tests { let port = addr.port(); let addr_with_quic_port = SocketAddr::new(addr.ip(), port); let map = connection_cache.map.read().unwrap(); - assert!(map.len() == MAX_CONNECTIONS); + assert!(map.len() == DEFAULT_MAX_CONNECTIONS); let _conn = map.get(&addr_with_quic_port).expect("Address not found"); } diff --git a/core/src/validator.rs b/core/src/validator.rs index e1366849e3c..69c202da9f4 100644 --- a/core/src/validator.rs +++ b/core/src/validator.rs @@ -123,7 +123,7 @@ use { AbsRequestHandlers, AccountsBackgroundService, DroppedSlotsReceiver, PendingSnapshotPackages, PrunedBanksRequestHandler, SnapshotRequestHandler, }, - bank::Bank, + bank::{Bank, MAX_ALPENGLOW_VOTE_ACCOUNTS}, bank_forks::BankForks, bank_forks_controller::BankForksControllerHandle, commitment::BlockCommitmentCache, @@ -1192,11 +1192,13 @@ impl Validator { )) }; - let bls_connection_cache = Arc::new(ConnectionCache::new_with_client_options( + let bls_connection_cache = Arc::new(ConnectionCache::new_with_max_connections( "connection_cache_bls_quic", // BLS consensus messaging is extremely low throughput (5 PPS). Even during standstill operations // we wouldn't expect more than a 100 PPS. 1 connection is enough. 1, /* connection_pool_size */ + // Overprovision to account for epoch boundary validator set rotations + MAX_ALPENGLOW_VOTE_ACCOUNTS * 2, /* max_connections */ Some(node.sockets.quic_alpenglow_client), Some(( &identity_keypair, From 47ce041c08cb21465a3a204a4297c5200733017d Mon Sep 17 00:00:00 2001 From: Andrew Fitzgerald Date: Thu, 11 Jun 2026 09:25:14 +0800 Subject: [PATCH 292/576] Use bounded channels inside banking stage (#12779) --- core/src/banking_stage.rs | 9 ++++++--- .../transaction_scheduler/greedy_scheduler.rs | 12 +++++++----- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/core/src/banking_stage.rs b/core/src/banking_stage.rs index 7104cff1f90..c649b9a2890 100644 --- a/core/src/banking_stage.rs +++ b/core/src/banking_stage.rs @@ -19,7 +19,7 @@ use { validator::BlockProductionMethod, }, agave_banking_stage_ingress_types::{BankingPacketReceiver, SchedulerPriorityFloor}, - crossbeam_channel::{Receiver, Sender, unbounded}, + crossbeam_channel::{Receiver, Sender, bounded}, futures::{StreamExt, stream::FuturesUnordered}, histogram::Histogram, solana_gossip::{cluster_info::ClusterInfo, contact_info::ContactInfoQuery}, @@ -518,9 +518,12 @@ impl BankingStage { threads.push(self.spawn_vote_worker()); // Create channels for communication between scheduler and workers + const CHANNEL_CAPACITY: usize = 10_000; // unlikely we'll ever hit this given default configuration. + let (work_senders, work_receivers): (Vec>, Vec>) = - (0..num_workers).map(|_| unbounded()).unzip(); - let (finished_work_sender, finished_work_receiver) = unbounded(); + (0..num_workers).map(|_| bounded(CHANNEL_CAPACITY)).unzip(); + let (finished_work_sender, finished_work_receiver) = + bounded(num_workers.saturating_mul(CHANNEL_CAPACITY)); // Spawn the worker threads let decision_maker = DecisionMaker::from(self.poh_recorder.read().unwrap().deref()); diff --git a/core/src/banking_stage/transaction_scheduler/greedy_scheduler.rs b/core/src/banking_stage/transaction_scheduler/greedy_scheduler.rs index 3219032760f..e1e1f76c862 100644 --- a/core/src/banking_stage/transaction_scheduler/greedy_scheduler.rs +++ b/core/src/banking_stage/transaction_scheduler/greedy_scheduler.rs @@ -101,8 +101,9 @@ impl Scheduler for GreedyScheduler { let mut schedulable_threads = ThreadSet::any(num_threads); for thread_id in 0..num_threads { - if self.common.in_flight_tracker.cus_in_flight_per_thread()[thread_id] - >= target_cu_per_thread + if self.common.consume_work_senders[thread_id].is_full() + || self.common.in_flight_tracker.cus_in_flight_per_thread()[thread_id] + >= target_cu_per_thread { schedulable_threads.remove(thread_id); } @@ -203,9 +204,10 @@ impl Scheduler for GreedyScheduler { // if the thread is at target_cu_per_thread, remove it from the schedulable threads // if there are no more schedulable threads, stop scheduling. - if self.common.in_flight_tracker.cus_in_flight_per_thread()[thread_id] - + self.common.batches.total_cus()[thread_id] - >= target_cu_per_thread + if self.common.consume_work_senders[thread_id].is_full() + || self.common.in_flight_tracker.cus_in_flight_per_thread()[thread_id] + + self.common.batches.total_cus()[thread_id] + >= target_cu_per_thread { schedulable_threads.remove(thread_id); if schedulable_threads.is_empty() { From a64b6358a247b7f16426aa1f070cd2f0f21aba15 Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Wed, 10 Jun 2026 23:17:49 -0400 Subject: [PATCH 293/576] SIMD-0204: backout migrating slashing program to builtin address (#13096) --- builtins/src/lib.rs | 37 ++-------------------------------- feature-set/src/lib.rs | 16 --------------- program-test/tests/core_bpf.rs | 33 ------------------------------ 3 files changed, 2 insertions(+), 84 deletions(-) diff --git a/builtins/src/lib.rs b/builtins/src/lib.rs index 8b3182488f2..67605d116d0 100644 --- a/builtins/src/lib.rs +++ b/builtins/src/lib.rs @@ -14,10 +14,7 @@ pub mod core_bpf_migration; pub mod prototype; use { - crate::{ - core_bpf_migration::{CoreBpfMigrationConfig, CoreBpfMigrationTargetType}, - prototype::{BuiltinPrototype, StatelessBuiltinPrototype}, - }, + crate::prototype::{BuiltinPrototype, StatelessBuiltinPrototype}, agave_feature_set as feature_set, solana_program_runtime::solana_sbpf::program::BuiltinFunctionDefinition, solana_sdk_ids::{bpf_loader, bpf_loader_deprecated, bpf_loader_upgradeable}, @@ -111,37 +108,7 @@ pub static BUILTINS: &[BuiltinPrototype] = &[ }, ]; -pub static STATELESS_BUILTINS: &[StatelessBuiltinPrototype] = &[StatelessBuiltinPrototype { - core_bpf_migration_config: Some(CoreBpfMigrationConfig { - source_buffer_address: buffer_accounts::slashing_program::id(), - upgrade_authority_address: None, - feature_id: feature_set::enshrine_slashing_program::id(), - verified_build_hash: Some(buffer_accounts::slashing_program::VERIFIED_BUILD_HASH), - migration_target: CoreBpfMigrationTargetType::Stateless, - datapoint_name: "enshrine_slashing_program", - }), - program_id: buffer_accounts::slashing_program::PROGRAM_ID, - name: "solana_slashing_program", -}]; - -/// Live source buffer accounts for builtin migrations. -mod buffer_accounts { - pub mod slashing_program { - use {solana_hash::Hash, solana_pubkey::Pubkey}; - - solana_pubkey::declare_id!("S1asHs4je6wPb2kWiHqNNdpNRiDaBEDQyfyCThhsrgv"); - - pub(crate) const PROGRAM_ID: Pubkey = - Pubkey::from_str_const("S1ashing11111111111111111111111111111111111"); - // 192ed727334abe822d5accba8b886e25f88b03c76973c2e7290cfb55b9e1115f - const HASH_BYTES: [u8; 32] = [ - 0x19, 0x2e, 0xd7, 0x27, 0x33, 0x4a, 0xbe, 0x82, 0x2d, 0x5a, 0xcc, 0xba, 0x8b, 0x88, - 0x6e, 0x25, 0xf8, 0x8b, 0x03, 0xc7, 0x69, 0x73, 0xc2, 0xe7, 0x29, 0x0c, 0xfb, 0x55, - 0xb9, 0xe1, 0x11, 0x5f, - ]; - pub(crate) const VERIFIED_BUILD_HASH: Hash = Hash::new_from_array(HASH_BYTES); - } -} +pub static STATELESS_BUILTINS: &[StatelessBuiltinPrototype] = &[]; // This module contains a number of arbitrary addresses used for testing Core // BPF migrations. diff --git a/feature-set/src/lib.rs b/feature-set/src/lib.rs index 79beb9eec73..df5cfc8d6aa 100644 --- a/feature-set/src/lib.rs +++ b/feature-set/src/lib.rs @@ -1282,10 +1282,6 @@ pub mod relax_intrabatch_account_locks { solana_pubkey::declare_id!("4WeHX6QoXCCwqbSFgi6dxnB6QsPo6YApaNTH7P4MLQ99"); } -pub mod create_slashing_program { - solana_pubkey::declare_id!("sProgVaNWkYdP2eTRAy1CPrgb3b9p8yXCASrPEqo6VJ"); -} - pub mod disable_partitioned_rent_collection { solana_pubkey::declare_id!("2B2SBNbUcr438LtGXNcJNBP2GBSxjx81F945SdSkUSfC"); } @@ -1306,10 +1302,6 @@ pub mod mask_out_rent_epoch_in_vm_serialization { solana_pubkey::declare_id!("RENtePQcDLrAbxAsP3k8dwVcnNYQ466hi2uKvALjnXx"); } -pub mod enshrine_slashing_program { - solana_pubkey::declare_id!("sProgVaNWkYdP2eTRAy1CPrgb3b9p8yXCASrPEqo6VJ"); -} - pub mod enable_extend_program_checked { solana_pubkey::declare_id!("ExtendProgCheckedWi11BeDe1eted11111111111111"); } @@ -2405,10 +2397,6 @@ pub static FEATURE_NAMES: LazyLock> = LazyLock::n relax_intrabatch_account_locks::id(), "SIMD-0083: Allow batched transactions to read/write and write/write the same accounts", ), - ( - create_slashing_program::id(), - "SIMD-0204: creates an enshrined slashing program", - ), ( disable_partitioned_rent_collection::id(), "SIMD-0175: Disable partitioned rent collection #4562", @@ -2429,10 +2417,6 @@ pub static FEATURE_NAMES: LazyLock> = LazyLock::n mask_out_rent_epoch_in_vm_serialization::id(), "SIMD-0267: Sets rent_epoch to a constant in the VM", ), - ( - enshrine_slashing_program::id(), - "SIMD-0204: Slashable event verification", - ), ( enable_extend_program_checked::id(), "Enable ExtendProgramChecked instruction", diff --git a/program-test/tests/core_bpf.rs b/program-test/tests/core_bpf.rs index 5af93d36a56..d2748d93a31 100644 --- a/program-test/tests/core_bpf.rs +++ b/program-test/tests/core_bpf.rs @@ -1,10 +1,7 @@ use { - solana_instruction::Instruction, solana_program_test::{ProgramTest, ProgramTestContext}, solana_pubkey::Pubkey, solana_sdk_ids::bpf_loader_upgradeable, - solana_signer::Signer, - solana_transaction::Transaction, }; async fn assert_bpf_program(context: &ProgramTestContext, program_id: &Pubkey) { @@ -28,33 +25,3 @@ async fn test_vended_core_bpf_programs() { assert_bpf_program(&context, &solana_sdk_ids::feature::id()).await; assert_bpf_program(&context, &solana_sdk_ids::stake::id()).await; } - -// Note this test has to use an entry from `solana_builtins::BUILTINS` with a -// `core_bpf_migration_config` set. -#[tokio::test] -async fn test_add_core_bpf_program_manually() { - // Core BPF program: Slashing. - let program_id = Pubkey::from_str_const("S1ashing11111111111111111111111111111111111"); - - let mut program_test = ProgramTest::default(); - program_test.add_upgradeable_program_to_genesis("noop_program", &program_id); - - let context = program_test.start_with_context().await; - - // Assert the program is a BPF Loader Upgradeable program. - assert_bpf_program(&context, &program_id).await; - - // Invoke the program. - let instruction = Instruction::new_with_bytes(program_id, &[], Vec::new()); - let transaction = Transaction::new_signed_with_payer( - &[instruction], - Some(&context.payer.pubkey()), - &[&context.payer], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); -} From 8ff0d492711a99f0f1a24238663bb993864cc72f Mon Sep 17 00:00:00 2001 From: Andrew Fitzgerald Date: Thu, 11 Jun 2026 14:42:11 +0800 Subject: [PATCH 294/576] use solana-svm-transaction dependency (#13123) --- Cargo.lock | 12 +- Cargo.toml | 5 +- dev-bins/Cargo.lock | 8 +- dev-bins/Cargo.toml | 2 +- install/Cargo.toml | 2 +- programs/sbf/Cargo.lock | 8 +- programs/sbf/Cargo.toml | 2 +- scripts/patch-crates.sh | 2 - svm-transaction/Cargo.toml | 32 -- svm-transaction/src/instruction.rs | 24 -- svm-transaction/src/lib.rs | 7 - .../src/message_address_table_lookup.rs | 23 -- svm-transaction/src/svm_message.rs | 160 ---------- .../src/svm_message/sanitized_message.rs | 89 ------ .../src/svm_message/sanitized_transaction.rs | 79 ----- svm-transaction/src/svm_transaction.rs | 11 - .../svm_transaction/sanitized_transaction.rs | 14 - svm-transaction/src/tests.rs | 292 ------------------ 18 files changed, 20 insertions(+), 752 deletions(-) delete mode 100644 svm-transaction/Cargo.toml delete mode 100644 svm-transaction/src/instruction.rs delete mode 100644 svm-transaction/src/lib.rs delete mode 100644 svm-transaction/src/message_address_table_lookup.rs delete mode 100644 svm-transaction/src/svm_message.rs delete mode 100644 svm-transaction/src/svm_message/sanitized_message.rs delete mode 100644 svm-transaction/src/svm_message/sanitized_transaction.rs delete mode 100644 svm-transaction/src/svm_transaction.rs delete mode 100644 svm-transaction/src/svm_transaction/sanitized_transaction.rs delete mode 100644 svm-transaction/src/tests.rs diff --git a/Cargo.lock b/Cargo.lock index 3d544a579f5..d3b4b987978 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10668,18 +10668,16 @@ dependencies = [ [[package]] name = "solana-svm-transaction" -version = "4.2.0-alpha.0" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736d2cec944c6f8f298a7bd8ada5eb318cca9ab859281b1300842fa0e9a25afa" dependencies = [ "solana-hash 4.4.0", "solana-message", - "solana-nonce", "solana-pubkey 4.2.0", "solana-sdk-ids", "solana-signature", - "solana-system-interface 3.2.0", "solana-transaction", - "static_assertions", - "test-case", ] [[package]] @@ -11085,9 +11083,9 @@ dependencies = [ [[package]] name = "solana-transaction" -version = "4.1.2" +version = "4.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45eb9e481a70f582be316d0bf2f8f8704079664e636a4e5d0c0d077f1c9de8f3" +checksum = "47d105ecce084206697230226c6b2230401c220feb4dc63e1274d58b38969292" dependencies = [ "serde", "serde_derive", diff --git a/Cargo.toml b/Cargo.toml index e4e142bfc5b..20446810142 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -100,7 +100,6 @@ members = [ "svm-log-collector", "svm-measure", "svm-timings", - "svm-transaction", "svm-type-overrides", "syscalls", "syscalls/gen-syscall-list", @@ -496,7 +495,7 @@ solana-svm-feature-set = { path = "svm-feature-set", version = "=4.2.0-alpha.0", solana-svm-log-collector = { path = "svm-log-collector", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-svm-measure = { path = "svm-measure", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-svm-timings = { path = "svm-timings", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-svm-transaction = { path = "svm-transaction", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } +solana-svm-transaction = "4.2.0" solana-svm-type-overrides = { path = "svm-type-overrides", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-syscalls = { path = "syscalls", version = "=4.2.0-alpha.0", default-features = false, features = ["agave-unstable-api"] } solana-system-interface = { version = "3.2", features = ["alloc", "bincode", "serde", "wincode"] } @@ -509,7 +508,7 @@ solana-time-utils = "3.0.0" solana-tls-utils = { path = "tls-utils", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-tpu-client = { path = "tpu-client", version = "=4.2.0-alpha.0", default-features = false, features = ["agave-unstable-api"] } solana-tpu-client-next = { path = "tpu-client-next", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-transaction = "4.1.2" +solana-transaction = "4.1.3" solana-transaction-context = { path = "transaction-context", version = "=4.2.0-alpha.0", features = ["agave-unstable-api", "bincode"] } solana-transaction-error = "3.2.1" solana-transaction-status = { path = "transaction-status", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 289830f1e11..1ce083cef65 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -8946,7 +8946,9 @@ dependencies = [ [[package]] name = "solana-svm-transaction" -version = "4.2.0-alpha.0" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736d2cec944c6f8f298a7bd8ada5eb318cca9ab859281b1300842fa0e9a25afa" dependencies = [ "solana-hash 4.4.0", "solana-message", @@ -9273,9 +9275,9 @@ dependencies = [ [[package]] name = "solana-transaction" -version = "4.1.2" +version = "4.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45eb9e481a70f582be316d0bf2f8f8704079664e636a4e5d0c0d077f1c9de8f3" +checksum = "47d105ecce084206697230226c6b2230401c220feb4dc63e1274d58b38969292" dependencies = [ "serde", "serde_derive", diff --git a/dev-bins/Cargo.toml b/dev-bins/Cargo.toml index c3941023f7e..87af91e3e8a 100644 --- a/dev-bins/Cargo.toml +++ b/dev-bins/Cargo.toml @@ -132,7 +132,7 @@ solana-svm-callback = { path = "../svm-callback", version = "=4.2.0-alpha.0", fe solana-svm-feature-set = { path = "../svm-feature-set", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-svm-log-collector = { path = "../svm-log-collector", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-svm-timings = { path = "../svm-timings", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-svm-transaction = { path = "../svm-transaction", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } +solana-svm-transaction = "4.2.0" solana-syscalls = { path = "../syscalls", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-system-interface = "3.2" solana-system-program = { path = "../programs/system", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } diff --git a/install/Cargo.toml b/install/Cargo.toml index 37abefc753e..1f22a0a127e 100644 --- a/install/Cargo.toml +++ b/install/Cargo.toml @@ -49,7 +49,7 @@ solana-rpc-client = { workspace = true } solana-sha256-hasher = { workspace = true } solana-signature = { version = "=3.4.1", default-features = false } solana-signer = "=3.0.1" -solana-transaction = { version = "=4.1.2", features = ["wincode"] } +solana-transaction = { version = "=4.1.3", features = ["wincode"] } solana-version = { workspace = true } tar = { workspace = true } tempfile = { workspace = true } diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index 09d985cd390..d3273019379 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -9480,7 +9480,9 @@ dependencies = [ [[package]] name = "solana-svm-transaction" -version = "4.2.0-alpha.0" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736d2cec944c6f8f298a7bd8ada5eb318cca9ab859281b1300842fa0e9a25afa" dependencies = [ "solana-hash 4.4.0", "solana-message", @@ -9779,9 +9781,9 @@ dependencies = [ [[package]] name = "solana-transaction" -version = "4.1.2" +version = "4.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45eb9e481a70f582be316d0bf2f8f8704079664e636a4e5d0c0d077f1c9de8f3" +checksum = "47d105ecce084206697230226c6b2230401c220feb4dc63e1274d58b38969292" dependencies = [ "serde", "serde_derive", diff --git a/programs/sbf/Cargo.toml b/programs/sbf/Cargo.toml index a29a1d930af..9c109511a1c 100644 --- a/programs/sbf/Cargo.toml +++ b/programs/sbf/Cargo.toml @@ -144,7 +144,7 @@ solana-stake-interface = { version = "=4.2.0", features = ["bincode"] } solana-svm = { path = "../../svm", version = "=4.2.0-alpha.0" } solana-svm-feature-set = { path = "../../svm-feature-set", version = "=4.2.0-alpha.0" } solana-svm-timings = { path = "../../svm-timings", version = "=4.2.0-alpha.0" } -solana-svm-transaction = { path = "../../svm-transaction", version = "=4.2.0-alpha.0" } +solana-svm-transaction = "=4.2.0" solana-svm-type-overrides = { path = "../../svm-type-overrides", version = "=4.2.0-alpha.0" } solana-system-interface = { version = "=3.2", features = ["bincode"] } solana-sysvar = "=4.0.0" diff --git a/scripts/patch-crates.sh b/scripts/patch-crates.sh index 4047d37ff84..3868fadb34d 100644 --- a/scripts/patch-crates.sh +++ b/scripts/patch-crates.sh @@ -61,7 +61,6 @@ update_solana_dependencies() { solana-storage-bigtable solana-storage-proto solana-streamer - solana-svm-transaction solana-test-validator solana-tpu-client solana-transaction-status @@ -146,7 +145,6 @@ patch_crates_io_solana_no_header() { crates_map+=("solana-storage-bigtable storage-bigtable") crates_map+=("solana-storage-proto storage-proto") crates_map+=("solana-streamer streamer") - crates_map+=("solana-svm-transaction svm-transaction") crates_map+=("solana-test-validator test-validator") crates_map+=("solana-tpu-client tpu-client") crates_map+=("solana-transaction-status transaction-status") diff --git a/svm-transaction/Cargo.toml b/svm-transaction/Cargo.toml deleted file mode 100644 index ff4e99c99fb..00000000000 --- a/svm-transaction/Cargo.toml +++ /dev/null @@ -1,32 +0,0 @@ -[package] -name = "solana-svm-transaction" -description = "Solana SVM Transaction" -documentation = "https://docs.rs/solana-svm-transaction" -version = { workspace = true } -authors = { workspace = true } -repository = { workspace = true } -homepage = { workspace = true } -license = { workspace = true } -edition = { workspace = true } - -[features] -agave-unstable-api = [] - -[dependencies] -solana-hash = { workspace = true } -solana-message = { workspace = true } -solana-pubkey = { workspace = true } -solana-sdk-ids = { workspace = true } -solana-signature = { workspace = true } -solana-transaction = { workspace = true } - -[dev-dependencies] -solana-message = { workspace = true, features = ["wincode"] } -solana-nonce = { workspace = true } -solana-pubkey = { workspace = true, features = ["std"] } -solana-system-interface = { workspace = true, features = ["bincode"] } -static_assertions = { workspace = true } -test-case = { workspace = true } - -[lints] -workspace = true diff --git a/svm-transaction/src/instruction.rs b/svm-transaction/src/instruction.rs deleted file mode 100644 index acf1cb06502..00000000000 --- a/svm-transaction/src/instruction.rs +++ /dev/null @@ -1,24 +0,0 @@ -use solana_message::compiled_instruction::CompiledInstruction; - -/// A non-owning version of [`CompiledInstruction`] that references -/// slices of account indexes and data. -// `program_id_index` is still owned, as it is a simple u8. -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct SVMInstruction<'a> { - /// Index into the transaction keys array indicating the program account that executes this instruction. - pub program_id_index: u8, - /// Ordered indices into the transaction keys array indicating which accounts to pass to the program. - pub accounts: &'a [u8], - /// The program input data. - pub data: &'a [u8], -} - -impl<'a> From<&'a CompiledInstruction> for SVMInstruction<'a> { - fn from(ix: &'a CompiledInstruction) -> Self { - Self { - program_id_index: ix.program_id_index, - accounts: ix.accounts.as_slice(), - data: ix.data.as_slice(), - } - } -} diff --git a/svm-transaction/src/lib.rs b/svm-transaction/src/lib.rs deleted file mode 100644 index d49d5c57a98..00000000000 --- a/svm-transaction/src/lib.rs +++ /dev/null @@ -1,7 +0,0 @@ -#![cfg(feature = "agave-unstable-api")] -pub mod instruction; -pub mod message_address_table_lookup; -pub mod svm_message; -pub mod svm_transaction; - -mod tests; diff --git a/svm-transaction/src/message_address_table_lookup.rs b/svm-transaction/src/message_address_table_lookup.rs deleted file mode 100644 index 4bb013df36f..00000000000 --- a/svm-transaction/src/message_address_table_lookup.rs +++ /dev/null @@ -1,23 +0,0 @@ -use {solana_message::v0, solana_pubkey::Pubkey}; - -/// A non-owning version of [`v0::MessageAddressTableLookup`]. -/// This simply references the data in the original message. -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct SVMMessageAddressTableLookup<'a> { - /// Address lookup table account key - pub account_key: &'a Pubkey, - /// List of indexes used to load writable account addresses - pub writable_indexes: &'a [u8], - /// List of indexes used to load readonly account addresses - pub readonly_indexes: &'a [u8], -} - -impl<'a> From<&'a v0::MessageAddressTableLookup> for SVMMessageAddressTableLookup<'a> { - fn from(lookup: &'a v0::MessageAddressTableLookup) -> Self { - Self { - account_key: &lookup.account_key, - writable_indexes: &lookup.writable_indexes, - readonly_indexes: &lookup.readonly_indexes, - } - } -} diff --git a/svm-transaction/src/svm_message.rs b/svm-transaction/src/svm_message.rs deleted file mode 100644 index 4fb7aa34d60..00000000000 --- a/svm-transaction/src/svm_message.rs +++ /dev/null @@ -1,160 +0,0 @@ -use { - crate::{ - instruction::SVMInstruction, message_address_table_lookup::SVMMessageAddressTableLookup, - }, - core::fmt::Debug, - solana_hash::Hash, - solana_message::AccountKeys, - solana_pubkey::Pubkey, - solana_sdk_ids::{ed25519_program, secp256k1_program, secp256r1_program, system_program}, - solana_transaction::versioned::TransactionVersion, -}; - -mod sanitized_message; -mod sanitized_transaction; -// inlined to avoid solana-nonce dep -#[cfg(test)] -static_assertions::const_assert_eq!( - NONCED_TX_MARKER_IX_INDEX, - solana_nonce::NONCED_TX_MARKER_IX_INDEX -); -const NONCED_TX_MARKER_IX_INDEX: u8 = 0; - -pub trait SVMStaticMessage { - /// Get the transaction version. - fn version(&self) -> TransactionVersion; - - /// Return the number of transaction-level signatures in the message. - fn num_transaction_signatures(&self) -> u64; - /// Return the number of ed25519 precompile signatures in the message. - fn num_ed25519_signatures(&self) -> u64 { - default_precompile_signature_count(&ed25519_program::ID, self.program_instructions_iter()) - } - /// Return the number of secp256k1 precompile signatures in the message. - fn num_secp256k1_signatures(&self) -> u64 { - default_precompile_signature_count(&secp256k1_program::ID, self.program_instructions_iter()) - } - /// Return the number of secp256r1 precompile signatures in the message. - fn num_secp256r1_signatures(&self) -> u64 { - default_precompile_signature_count(&secp256r1_program::ID, self.program_instructions_iter()) - } - - /// Returns the number of requested write-locks in this message. - /// This does not consider if write-locks are demoted. - fn num_write_locks(&self) -> u64; - - /// Return the recent blockhash. - fn recent_blockhash(&self) -> &Hash; - - /// Return the number of instructions in the message. - fn num_instructions(&self) -> usize; - - /// Return an iterator over the instructions in the message. - fn instructions_iter(&self) -> impl Iterator>; - - /// Return an iterator over the instructions in the message, paired with - /// the pubkey of the program. - fn program_instructions_iter( - &self, - ) -> impl Iterator)> + Clone; - - /// Return the list of static account keys. - fn static_account_keys(&self) -> &[Pubkey]; - - /// Return the fee-payer - fn fee_payer(&self) -> &Pubkey; - - /// Get the number of lookup tables. - fn num_lookup_tables(&self) -> usize; - - /// Get message address table lookups used in the message - fn message_address_table_lookups( - &self, - ) -> impl Iterator>; -} - -// - Debug to support legacy logging -pub trait SVMMessage: Debug + SVMStaticMessage { - /// Return the account keys. - fn account_keys(&self) -> AccountKeys<'_>; - - /// Returns `true` if the account at `index` is writable. - fn is_writable(&self, index: usize) -> bool; - - /// Returns `true` if the account at `index` is signer. - fn is_signer(&self, index: usize) -> bool; - - /// Returns true if the account at the specified index is invoked as a - /// program in top-level instructions of this message. - fn is_invoked(&self, key_index: usize) -> bool; - - /// Returns true if the account at the specified index is an input to some - /// program instruction in this message. - fn is_instruction_account(&self, key_index: usize) -> bool { - if let Ok(key_index) = u8::try_from(key_index) { - self.instructions_iter() - .any(|ix| ix.accounts.contains(&key_index)) - } else { - false - } - } - - /// If the message uses a durable nonce, return the pubkey of the nonce account - fn get_durable_nonce(&self) -> Option<&Pubkey> { - let account_keys = self.account_keys(); - self.instructions_iter() - .nth(usize::from(NONCED_TX_MARKER_IX_INDEX)) - .filter( - |ix| match account_keys.get(usize::from(ix.program_id_index)) { - Some(program_id) => system_program::check_id(program_id), - _ => false, - }, - ) - .filter(|ix| { - /// Serialized value of [`SystemInstruction::AdvanceNonceAccount`]. - const SERIALIZED_ADVANCE_NONCE_ACCOUNT: [u8; 4] = 4u32.to_le_bytes(); - const SERIALIZED_SIZE: usize = SERIALIZED_ADVANCE_NONCE_ACCOUNT.len(); - - ix.data - .get(..SERIALIZED_SIZE) - .map(|data| data == SERIALIZED_ADVANCE_NONCE_ACCOUNT) - .unwrap_or(false) - }) - .and_then(|ix| { - ix.accounts.first().and_then(|idx| { - let index = usize::from(*idx); - if index >= self.static_account_keys().len() || !self.is_writable(index) { - None - } else { - account_keys.get(index) - } - }) - }) - } - - /// For the instruction at `index`, return an iterator over input accounts - /// that are signers. - fn get_ix_signers(&self, index: usize) -> impl Iterator { - self.instructions_iter() - .nth(index) - .into_iter() - .flat_map(|ix| { - ix.accounts - .iter() - .copied() - .map(usize::from) - .filter(|index| self.is_signer(*index)) - .filter_map(|signer_index| self.account_keys().get(signer_index)) - }) - } -} - -fn default_precompile_signature_count<'a>( - precompile: &Pubkey, - instructions: impl Iterator)>, -) -> u64 { - instructions - .filter(|(program_id, _)| *program_id == precompile) - .map(|(_, ix)| u64::from(ix.data.first().copied().unwrap_or(0))) - .sum() -} diff --git a/svm-transaction/src/svm_message/sanitized_message.rs b/svm-transaction/src/svm_message/sanitized_message.rs deleted file mode 100644 index e1e890f3fe7..00000000000 --- a/svm-transaction/src/svm_message/sanitized_message.rs +++ /dev/null @@ -1,89 +0,0 @@ -use { - crate::{ - instruction::SVMInstruction, - message_address_table_lookup::SVMMessageAddressTableLookup, - svm_message::{SVMMessage, SVMStaticMessage}, - }, - solana_hash::Hash, - solana_message::{AccountKeys, SanitizedMessage}, - solana_pubkey::Pubkey, - solana_transaction::versioned::TransactionVersion, -}; - -impl SVMStaticMessage for SanitizedMessage { - fn version(&self) -> TransactionVersion { - match self { - SanitizedMessage::Legacy(_) => TransactionVersion::LEGACY, - SanitizedMessage::V0(_) => TransactionVersion::Number(0), - SanitizedMessage::V1(_) => TransactionVersion::Number(1), - } - } - - fn num_transaction_signatures(&self) -> u64 { - u64::from(self.header().num_required_signatures) - } - - fn num_write_locks(&self) -> u64 { - SanitizedMessage::num_write_locks(self) - } - - fn recent_blockhash(&self) -> &Hash { - SanitizedMessage::recent_blockhash(self) - } - - fn num_instructions(&self) -> usize { - SanitizedMessage::instructions(self).len() - } - - fn instructions_iter(&self) -> impl Iterator> { - SanitizedMessage::instructions(self) - .iter() - .map(SVMInstruction::from) - } - - fn program_instructions_iter( - &self, - ) -> impl Iterator)> + Clone { - SanitizedMessage::program_instructions_iter(self) - .map(|(pubkey, ix)| (pubkey, SVMInstruction::from(ix))) - } - - fn static_account_keys(&self) -> &[Pubkey] { - SanitizedMessage::static_account_keys(self) - } - - fn fee_payer(&self) -> &Pubkey { - SanitizedMessage::fee_payer(self) - } - - fn num_lookup_tables(&self) -> usize { - SanitizedMessage::message_address_table_lookups(self).len() - } - - fn message_address_table_lookups( - &self, - ) -> impl Iterator> { - SanitizedMessage::message_address_table_lookups(self) - .iter() - .map(SVMMessageAddressTableLookup::from) - } -} - -// Implement for the "reference" `SanitizedMessage` type. -impl SVMMessage for SanitizedMessage { - fn account_keys(&self) -> AccountKeys<'_> { - SanitizedMessage::account_keys(self) - } - - fn is_writable(&self, index: usize) -> bool { - SanitizedMessage::is_writable(self, index) - } - - fn is_signer(&self, index: usize) -> bool { - SanitizedMessage::is_signer(self, index) - } - - fn is_invoked(&self, key_index: usize) -> bool { - SanitizedMessage::is_invoked(self, key_index) - } -} diff --git a/svm-transaction/src/svm_message/sanitized_transaction.rs b/svm-transaction/src/svm_message/sanitized_transaction.rs deleted file mode 100644 index e396a3976b5..00000000000 --- a/svm-transaction/src/svm_message/sanitized_transaction.rs +++ /dev/null @@ -1,79 +0,0 @@ -use { - crate::{ - instruction::SVMInstruction, - message_address_table_lookup::SVMMessageAddressTableLookup, - svm_message::{SVMMessage, SVMStaticMessage}, - }, - solana_hash::Hash, - solana_message::AccountKeys, - solana_pubkey::Pubkey, - solana_transaction::{sanitized::SanitizedTransaction, versioned::TransactionVersion}, -}; - -impl SVMStaticMessage for SanitizedTransaction { - fn version(&self) -> TransactionVersion { - SVMStaticMessage::version(SanitizedTransaction::message(self)) - } - - fn num_transaction_signatures(&self) -> u64 { - SVMStaticMessage::num_transaction_signatures(SanitizedTransaction::message(self)) - } - - fn num_write_locks(&self) -> u64 { - SVMStaticMessage::num_write_locks(SanitizedTransaction::message(self)) - } - - fn recent_blockhash(&self) -> &Hash { - SVMStaticMessage::recent_blockhash(SanitizedTransaction::message(self)) - } - - fn num_instructions(&self) -> usize { - SVMStaticMessage::num_instructions(SanitizedTransaction::message(self)) - } - - fn instructions_iter(&self) -> impl Iterator> { - SVMStaticMessage::instructions_iter(SanitizedTransaction::message(self)) - } - - fn program_instructions_iter( - &self, - ) -> impl Iterator)> + Clone { - SVMStaticMessage::program_instructions_iter(SanitizedTransaction::message(self)) - } - - fn static_account_keys(&self) -> &[Pubkey] { - SVMStaticMessage::static_account_keys(SanitizedTransaction::message(self)) - } - - fn fee_payer(&self) -> &Pubkey { - SVMStaticMessage::fee_payer(SanitizedTransaction::message(self)) - } - - fn num_lookup_tables(&self) -> usize { - SVMStaticMessage::num_lookup_tables(SanitizedTransaction::message(self)) - } - - fn message_address_table_lookups( - &self, - ) -> impl Iterator> { - SVMStaticMessage::message_address_table_lookups(SanitizedTransaction::message(self)) - } -} - -impl SVMMessage for SanitizedTransaction { - fn account_keys(&self) -> AccountKeys<'_> { - SVMMessage::account_keys(SanitizedTransaction::message(self)) - } - - fn is_writable(&self, index: usize) -> bool { - SVMMessage::is_writable(SanitizedTransaction::message(self), index) - } - - fn is_signer(&self, index: usize) -> bool { - SVMMessage::is_signer(SanitizedTransaction::message(self), index) - } - - fn is_invoked(&self, key_index: usize) -> bool { - SVMMessage::is_invoked(SanitizedTransaction::message(self), key_index) - } -} diff --git a/svm-transaction/src/svm_transaction.rs b/svm-transaction/src/svm_transaction.rs deleted file mode 100644 index a45063384f4..00000000000 --- a/svm-transaction/src/svm_transaction.rs +++ /dev/null @@ -1,11 +0,0 @@ -use {crate::svm_message::SVMMessage, solana_signature::Signature}; - -mod sanitized_transaction; - -pub trait SVMTransaction: SVMMessage { - /// Get the first signature of the message. - fn signature(&self) -> &Signature; - - /// Get all the signatures of the message. - fn signatures(&self) -> &[Signature]; -} diff --git a/svm-transaction/src/svm_transaction/sanitized_transaction.rs b/svm-transaction/src/svm_transaction/sanitized_transaction.rs deleted file mode 100644 index 19d102d65a6..00000000000 --- a/svm-transaction/src/svm_transaction/sanitized_transaction.rs +++ /dev/null @@ -1,14 +0,0 @@ -use { - crate::svm_transaction::SVMTransaction, solana_signature::Signature, - solana_transaction::sanitized::SanitizedTransaction, -}; - -impl SVMTransaction for SanitizedTransaction { - fn signature(&self) -> &Signature { - SanitizedTransaction::signature(self) - } - - fn signatures(&self) -> &[Signature] { - SanitizedTransaction::signatures(self) - } -} diff --git a/svm-transaction/src/tests.rs b/svm-transaction/src/tests.rs deleted file mode 100644 index dbca7e721ba..00000000000 --- a/svm-transaction/src/tests.rs +++ /dev/null @@ -1,292 +0,0 @@ -#![cfg(test)] -use { - crate::svm_message::SVMMessage, - solana_hash::Hash, - solana_message::{ - MessageHeader, SanitizedMessage, SanitizedVersionedMessage, SimpleAddressLoader, - VersionedMessage, - compiled_instruction::CompiledInstruction, - legacy, - v0::{self, LoadedAddresses, MessageAddressTableLookup}, - }, - solana_pubkey::Pubkey, - solana_sdk_ids::system_program, - solana_system_interface::instruction::SystemInstruction, - std::collections::HashSet, -}; - -#[test] -fn test_get_durable_nonce() { - fn create_message_for_test( - num_signers: u8, - num_writable: u8, - account_keys: Vec, - instructions: Vec, - loaded_addresses: Option, - ) -> SanitizedMessage { - let header = MessageHeader { - num_required_signatures: num_signers, - num_readonly_signed_accounts: 0, - num_readonly_unsigned_accounts: u8::try_from(account_keys.len()) - .unwrap() - .checked_sub(num_writable) - .unwrap(), - }; - let (versioned_message, loader) = match loaded_addresses { - None => ( - VersionedMessage::Legacy(legacy::Message { - header, - account_keys, - recent_blockhash: Hash::default(), - instructions, - }), - SimpleAddressLoader::Disabled, - ), - Some(loaded_addresses) => ( - VersionedMessage::V0(v0::Message { - header, - account_keys, - recent_blockhash: Hash::default(), - instructions, - address_table_lookups: vec![MessageAddressTableLookup { - account_key: Pubkey::new_unique(), - writable_indexes: (0..loaded_addresses.writable.len()) - .map(|x| x as u8) - .collect(), - readonly_indexes: (0..loaded_addresses.readonly.len()) - .map(|x| loaded_addresses.writable.len().saturating_add(x) as u8) - .collect(), - }], - }), - SimpleAddressLoader::Enabled(loaded_addresses), - ), - }; - SanitizedMessage::try_new( - SanitizedVersionedMessage::try_new(versioned_message).unwrap(), - loader, - &HashSet::new(), - ) - .unwrap() - } - - // No instructions - no nonce - { - let message = create_message_for_test(1, 1, vec![Pubkey::new_unique()], vec![], None); - assert!(SanitizedMessage::get_durable_nonce(&message).is_none()); - assert!(SVMMessage::get_durable_nonce(&message).is_none()); - } - - // system program id instruction - invalid - { - let message = create_message_for_test( - 1, - 1, - vec![Pubkey::new_unique(), system_program::id()], - vec![CompiledInstruction::new_from_raw_parts(1, vec![], vec![])], - None, - ); - assert!(SanitizedMessage::get_durable_nonce(&message).is_none()); - assert!(SVMMessage::get_durable_nonce(&message).is_none()); - } - - // system program id instruction - not nonce - { - let message = create_message_for_test( - 1, - 1, - vec![Pubkey::new_unique(), system_program::id()], - vec![CompiledInstruction::new( - 1, - &SystemInstruction::Transfer { lamports: 1 }, - vec![0, 0], - )], - None, - ); - assert!(SanitizedMessage::get_durable_nonce(&message).is_none()); - assert!(SVMMessage::get_durable_nonce(&message).is_none()); - } - - // system program id - nonce instruction (no accounts) - { - let message = create_message_for_test( - 1, - 1, - vec![Pubkey::new_unique(), system_program::id()], - vec![CompiledInstruction::new( - 1, - &SystemInstruction::AdvanceNonceAccount, - vec![], - )], - None, - ); - assert!(SanitizedMessage::get_durable_nonce(&message).is_none()); - assert!(SVMMessage::get_durable_nonce(&message).is_none()); - } - - // system program id - nonce instruction (non-fee-payer, non-writable) - { - let payer = Pubkey::new_unique(); - let nonce = Pubkey::new_unique(); - let message = create_message_for_test( - 1, - 1, - vec![payer, nonce, system_program::id()], - vec![CompiledInstruction::new( - 1, - &SystemInstruction::AdvanceNonceAccount, - vec![1], - )], - None, - ); - assert!(SanitizedMessage::get_durable_nonce(&message).is_none()); - assert!(SVMMessage::get_durable_nonce(&message).is_none()); - } - - // system program id - nonce instruction fee-payer - { - let payer_nonce = Pubkey::new_unique(); - let message = create_message_for_test( - 1, - 1, - vec![payer_nonce, system_program::id()], - vec![CompiledInstruction::new( - 1, - &SystemInstruction::AdvanceNonceAccount, - vec![0], - )], - None, - ); - assert_eq!( - SanitizedMessage::get_durable_nonce(&message), - Some(&payer_nonce) - ); - assert_eq!(SVMMessage::get_durable_nonce(&message), Some(&payer_nonce)); - } - - // system program id - nonce instruction w/ trailing bytes fee-payer - { - let payer_nonce = Pubkey::new_unique(); - let mut instruction_bytes = vec![4, 0, 0, 0]; - instruction_bytes.push(0); // add a trailing byte - let message = create_message_for_test( - 1, - 1, - vec![payer_nonce, system_program::id()], - vec![CompiledInstruction::new_from_raw_parts( - 1, - instruction_bytes, - vec![0], - )], - None, - ); - assert_eq!( - SanitizedMessage::get_durable_nonce(&message), - Some(&payer_nonce) - ); - assert_eq!(SVMMessage::get_durable_nonce(&message), Some(&payer_nonce)); - } - - // system program id - nonce instruction (non-fee-payer) - { - let payer = Pubkey::new_unique(); - let nonce = Pubkey::new_unique(); - let message = create_message_for_test( - 1, - 2, - vec![payer, nonce, system_program::id()], - vec![CompiledInstruction::new( - 2, - &SystemInstruction::AdvanceNonceAccount, - vec![1], - )], - None, - ); - assert_eq!(SanitizedMessage::get_durable_nonce(&message), Some(&nonce)); - assert_eq!(SVMMessage::get_durable_nonce(&message), Some(&nonce)); - } - - // system program id - nonce instruction (non-fee-payer, multiple accounts) - { - let payer = Pubkey::new_unique(); - let other = Pubkey::new_unique(); - let nonce = Pubkey::new_unique(); - let message = create_message_for_test( - 1, - 3, - vec![payer, other, nonce, system_program::id()], - vec![CompiledInstruction::new( - 3, - &SystemInstruction::AdvanceNonceAccount, - vec![2, 1, 0], - )], - None, - ); - assert_eq!(SanitizedMessage::get_durable_nonce(&message), Some(&nonce)); - assert_eq!(SVMMessage::get_durable_nonce(&message), Some(&nonce)); - } - - // system program id - nonce instruction (non-fee-payer, loaded account) - { - let payer = Pubkey::new_unique(); - let nonce = Pubkey::new_unique(); - let message = create_message_for_test( - 1, - 1, - vec![payer, system_program::id()], - vec![CompiledInstruction::new( - 1, - &SystemInstruction::AdvanceNonceAccount, - vec![2, 0, 1], - )], - Some(LoadedAddresses { - writable: vec![nonce], - readonly: vec![], - }), - ); - assert_eq!(SanitizedMessage::get_durable_nonce(&message), Some(&nonce)); - assert_eq!(SVMMessage::get_durable_nonce(&message), None); - } -} - -#[test] -fn test_get_ix_signers() { - let signer0 = Pubkey::new_unique(); - let signer1 = Pubkey::new_unique(); - let non_signer = Pubkey::new_unique(); - let loader_key = Pubkey::new_unique(); - let instructions = vec![ - CompiledInstruction::new(3, &(), vec![2, 0]), - CompiledInstruction::new(3, &(), vec![0, 1]), - CompiledInstruction::new(3, &(), vec![0, 0]), - ]; - - let message = SanitizedMessage::try_from_legacy_message( - legacy::Message::new_with_compiled_instructions( - 2, - 1, - 2, - vec![signer0, signer1, non_signer, loader_key], - Hash::default(), - instructions, - ), - &HashSet::default(), - ) - .unwrap(); - - assert_eq!( - SVMMessage::get_ix_signers(&message, 0).collect::>(), - HashSet::from_iter([&signer0]) - ); - assert_eq!( - SVMMessage::get_ix_signers(&message, 1).collect::>(), - HashSet::from_iter([&signer0, &signer1]) - ); - assert_eq!( - SVMMessage::get_ix_signers(&message, 2).collect::>(), - HashSet::from_iter([&signer0]) - ); - assert_eq!( - SVMMessage::get_ix_signers(&message, 3).collect::>(), - HashSet::default() - ); -} From 6d7908c2f250587b71c051960c5119c6799dd2a0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 19:27:45 +0800 Subject: [PATCH 295/576] chore(deps): bump octocrab from 0.51.0 to 0.53.0 in /ci/xtask (#13112) Bumps [octocrab](https://github.com/XAMPPRocky/octocrab) from 0.51.0 to 0.53.0. - [Release notes](https://github.com/XAMPPRocky/octocrab/releases) - [Changelog](https://github.com/XAMPPRocky/octocrab/blob/main/CHANGELOG.md) - [Commits](https://github.com/XAMPPRocky/octocrab/compare/v0.51.0...v0.53.0) --- updated-dependencies: - dependency-name: octocrab dependency-version: 0.53.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- ci/xtask/Cargo.lock | 15 +++++++++++++-- ci/xtask/Cargo.toml | 2 +- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/ci/xtask/Cargo.lock b/ci/xtask/Cargo.lock index d75e2f15094..23d413ac0fb 100644 --- a/ci/xtask/Cargo.lock +++ b/ci/xtask/Cargo.lock @@ -743,6 +743,16 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-serde" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f056c8559e3757392c8d091e796416e4649d8e49e88b8d76df6c002f05027fd" +dependencies = [ + "http", + "serde", +] + [[package]] name = "httparse" version = "1.10.1" @@ -1175,9 +1185,9 @@ dependencies = [ [[package]] name = "octocrab" -version = "0.51.0" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb2ad8abffe4e2b05f9cdc7e061de63d305a6dca0af81ca1064a7d98e0b78267" +checksum = "46a4184473c1d00a892dad5259e1d19421ab1f8e4b7d714113a6cfdb213d9980" dependencies = [ "arc-swap", "async-trait", @@ -1193,6 +1203,7 @@ dependencies = [ "http", "http-body", "http-body-util", + "http-serde", "hyper", "hyper-rustls", "hyper-timeout", diff --git a/ci/xtask/Cargo.toml b/ci/xtask/Cargo.toml index 9048205ac38..35f4e7ecf4c 100644 --- a/ci/xtask/Cargo.toml +++ b/ci/xtask/Cargo.toml @@ -24,7 +24,7 @@ clap = { version = "4.6.1", features = ["derive"] } env_logger = "0.11.10" futures-util = "0.3.31" log = "0.4.28" -octocrab = { version = "0.51.0", features = ["stream"] } +octocrab = { version = "0.53.0", features = ["stream"] } regex = "1.12.3" serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.145" From 9bec67b417d332e42a9e7cf164949b0bd06fd4f8 Mon Sep 17 00:00:00 2001 From: Yihau Chen Date: Thu, 11 Jun 2026 19:30:31 +0800 Subject: [PATCH 296/576] ci: run release dcou check in sanity (#13115) * add dcou check mode * add the check to sanity * Update scripts/cargo-install-all.sh Co-authored-by: Mircea Colonescu * check dcou check only flag first * Update ci/test-sanity.sh Co-authored-by: Mykola Dzham * Update scripts/cargo-install-all.sh Co-authored-by: Mykola Dzham * Update scripts/cargo-install-all.sh Co-authored-by: Mykola Dzham --------- Co-authored-by: Mircea Colonescu Co-authored-by: Mykola Dzham --- ci/test-sanity.sh | 2 ++ scripts/cargo-install-all.sh | 49 +++++++++++++++++++++++++++--------- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/ci/test-sanity.sh b/ci/test-sanity.sh index c1ef0a9af63..33ef66fec19 100755 --- a/ci/test-sanity.sh +++ b/ci/test-sanity.sh @@ -82,4 +82,6 @@ EOF fi ) +./scripts/cargo-install-all.sh --dcou-check-only + echo --- ok diff --git a/scripts/cargo-install-all.sh b/scripts/cargo-install-all.sh index e52475a8409..75e30b2bd94 100755 --- a/scripts/cargo-install-all.sh +++ b/scripts/cargo-install-all.sh @@ -33,6 +33,7 @@ usage: $0 [+] [options] + Build using instead of the version defined in rust-toolchain.toml. Options: + --dcou-check-only Only check that dcou feature activation is correct and exit (no build). --debug Build with debug profile instead of release profile. --release-with-debug Build with release-with-debug profile instead of release profile. --release-with-lto Build with release-with-lto profile instead of release profile. @@ -58,6 +59,7 @@ buildProfileArg='--profile release' buildProfile='release' # Build selection +dcouCheckOnly= noBuildDCOUBins= noBuildDeprecatedBins= noBuildDevBins= @@ -68,7 +70,10 @@ noSPLToken= while [[ -n $1 ]]; do if [[ ${1:0:1} = - ]]; then - if [[ $1 = --debug ]]; then + if [[ $1 = --dcou-check-only ]]; then + dcouCheckOnly=true + shift + elif [[ $1 = --debug ]]; then buildProfileArg= # the default cargo profile is 'debug' buildProfile='debug' shift @@ -124,15 +129,19 @@ while [[ -n $1 ]]; do fi done -if [[ -z "$installDir" ]]; then - usage "Install directory not specified" - exit 1 -fi -installDir="$(mkdir -p "$installDir"; cd "$installDir"; pwd)" -mkdir -p "$installDir/bin/deps" +if [[ -n "$dcouCheckOnly" ]]; then + echo "(dcou check mode: ignore installDir)" +else + if [[ -z "$installDir" ]]; then + usage "Install directory not specified" + fi -echo "Install location: $installDir ($buildProfile)" + installDir="$(mkdir -p "$installDir"; cd "$installDir"; pwd)" + mkdir -p "$installDir/bin/deps" + + echo "Install location: $installDir ($buildProfile)" +fi cd "$(dirname "$0")"/.. @@ -204,6 +213,21 @@ check_dcou() { exit 1 fi + # Likewise, make sure the dev tools really do activate dcou. Done before + # building so that `--dcou-check` can verify both expectations up front. + if [[ ${#dcouBinArgs[@]} -gt 0 ]]; then + if ! check_dcou --manifest-path "dev-bins/Cargo.toml" "${dcouBinArgs[@]}"; then + echo 'dcou feature activation is incorrectly deactivated!' + exit 1 + fi + fi + + # Stop here if we only want to check the dcou feature activation. + if [[ -n "$dcouCheckOnly" ]]; then + echo 'dcou feature activation check passed.' + exit 0 + fi + # Build our production binaries without dcou. if [[ ${#binArgs[@]} -gt 0 ]]; then cargo_build "${binArgs[@]}" --workspace @@ -211,10 +235,6 @@ check_dcou() { # Finally, build the remaining dev tools with dcou. if [[ ${#dcouBinArgs[@]} -gt 0 ]]; then - if ! check_dcou --manifest-path "dev-bins/Cargo.toml" "${dcouBinArgs[@]}"; then - echo 'dcou feature activation is incorrectly remain to be deactivated!' - exit 1 - fi cargo_build --manifest-path "dev-bins/Cargo.toml" "${dcouBinArgs[@]}" fi @@ -228,6 +248,11 @@ check_dcou() { fi ) +# The subshell above exits early in dcou-check mode; stop here before installing. +if [[ -n "$dcouCheckOnly" ]]; then + exit 0 +fi + for bin in "${BINS[@]}"; do cp -fv "target/$buildProfile/$bin" "$installDir"/bin done From 5e248f17b4319c171a6818e294c605c9cf6771c9 Mon Sep 17 00:00:00 2001 From: Tao Zhu <82401714+tao-stones@users.noreply.github.com> Date: Thu, 11 Jun 2026 09:30:05 -0500 Subject: [PATCH 297/576] Refactor: simplify vote batching process (#13092) * refactor: remove UNPROCESSED_BUFFER_STEP_SIZE, remove Itertools::chunk, simplify retry handling * refactor do_process_packets() to not take &mut parameter, instead return a small ProcessPacketsOutput containing both retryable_transaction_indexes and reached_end_of_slot * replace for loop with while loop to bulk reinsert at end of slot; move end-of-slot check one level up for more simplification * Removed do_process_packets(), inlining it into process_packets() made the single-vote flow clearer --- core/src/banking_stage/vote_worker.rs | 249 ++++---------------------- 1 file changed, 39 insertions(+), 210 deletions(-) diff --git a/core/src/banking_stage/vote_worker.rs b/core/src/banking_stage/vote_worker.rs index 02e3fa47fc5..b7c05af0a43 100644 --- a/core/src/banking_stage/vote_worker.rs +++ b/core/src/banking_stage/vote_worker.rs @@ -17,9 +17,7 @@ use { agave_transaction_view::{ transaction_version::TransactionVersion, transaction_view::SanitizedTransactionView, }, - arrayvec::ArrayVec, crossbeam_channel::RecvTimeoutError, - itertools::Itertools, solana_accounts_db::account_locks::validate_account_locks, solana_clock::FORWARD_TRANSACTIONS_TO_LEADER_AT_SLOT_OFFSET, solana_measure::{measure::Measure, measure_us}, @@ -50,11 +48,6 @@ mod transaction { pub use solana_transaction_error::TransactionResult as Result; } -// Process vote packets one at a time to avoid over reserving block CUs during packing, -// also keep each recorded vote batch as small as possible, which favors entry/FEC-set -// packing. -pub const UNPROCESSED_BUFFER_STEP_SIZE: usize = 1; - pub struct VoteWorker { exit: Arc, shutdown_signal: CancellationToken, @@ -244,109 +237,55 @@ impl VoteWorker { // Based on the stake distribution present in the supplied bank, drain the unprocessed votes // from each validator using a weighted random ordering. Votes from validators with // 0 stake are ignored. - let all_vote_packets = self.storage.drain_unprocessed(bank); - - let mut reached_end_of_slot = false; + let mut all_vote_packets = self.storage.drain_unprocessed(bank).into_iter(); let mut error_counters: TransactionErrorMetrics = TransactionErrorMetrics::default(); - let mut resolved_txs = ArrayVec::<_, UNPROCESSED_BUFFER_STEP_SIZE>::new(); - for chunk in Itertools::chunks(all_vote_packets.into_iter(), UNPROCESSED_BUFFER_STEP_SIZE) - .into_iter() - { - debug_assert!(resolved_txs.is_empty()); - - // Short circuit if we've reached the end of slot. - if reached_end_of_slot { - self.storage.reinsert_packets(chunk.into_iter()); - + // Process one vote at a time to avoid over-reserving block CUs during packing. + // This also keeps each recorded vote batch small, which favors entry/FEC-set packing. + while let Some(packet) = all_vote_packets.next() { + let Some(sanitized_transaction) = + consume_scan_should_process_packet(bank, packet, &mut error_counters) + else { continue; - } - - // Sanitize & resolve our chunk. - for packet in chunk.into_iter() { - if let Some(tx) = - consume_scan_should_process_packet(bank, packet, &mut error_counters) - { - resolved_txs.push(tx); - } - } - - if let Some(retryable_vote_indices) = self.do_process_packets( - bank, - &mut reached_end_of_slot, - &resolved_txs, - banking_stage_stats, - consumed_buffered_packets_count, - rebuffered_packet_count, - slot_metrics_tracker, - ) { - self.storage.reinsert_packets( - Self::extract_retryable(&mut resolved_txs, retryable_vote_indices) - .map(|tx| tx.into_inner_transaction().into_view()), - ); - } else { - self.storage.reinsert_packets( - resolved_txs - .drain(..) - .map(|tx| tx.into_inner_transaction().into_view()), - ); - } - } + }; - reached_end_of_slot - } - - fn do_process_packets( - &self, - bank: &Bank, - reached_end_of_slot: &mut bool, - sanitized_transactions: &[RuntimeTransactionView], - banking_stage_stats: &BankingStageStats, - consumed_buffered_packets_count: &mut usize, - rebuffered_packet_count: &mut usize, - slot_metrics_tracker: &mut LeaderSlotMetricsTracker, - ) -> Option> { - if *reached_end_of_slot { - return None; - } + let (process_transactions_summary, process_packets_transactions_us) = + measure_us!(self.process_packets_transactions( + bank, + std::slice::from_ref(&sanitized_transaction), + banking_stage_stats, + slot_metrics_tracker, + )); + slot_metrics_tracker + .increment_process_packets_transactions_us(process_packets_transactions_us); - let (process_transactions_summary, process_packets_transactions_us) = - measure_us!(self.process_packets_transactions( - bank, - sanitized_transactions, - banking_stage_stats, - slot_metrics_tracker, - )); + let ProcessTransactionsSummary { + reached_max_poh_height, + retryable_transaction_indexes: retryable_vote_indices, + .. + } = process_transactions_summary; - slot_metrics_tracker - .increment_process_packets_transactions_us(process_packets_transactions_us); + let retryable_vote_count = retryable_vote_indices.len(); + *consumed_buffered_packets_count += usize::from(retryable_vote_count == 0); + *rebuffered_packet_count += retryable_vote_count; - let ProcessTransactionsSummary { - reached_max_poh_height, - retryable_transaction_indexes, - .. - } = process_transactions_summary; + slot_metrics_tracker.increment_retryable_packets_count(retryable_vote_count as u64); - *reached_end_of_slot = has_reached_end_of_slot(reached_max_poh_height, bank); + if retryable_vote_count != 0 { + // vote is processed one at a time, so the only valid retryable index is 0 + assert_eq!(retryable_vote_indices.as_slice(), &[0]); - // The difference between all transactions passed to execution and the ones that - // are retryable were the ones that were either: - // 1) Committed into the block - // 2) Dropped without being committed because they had some fatal error (too old, - // duplicate signature, etc.) - // - // Note: This assumes that every packet deserializes into one transaction! - *consumed_buffered_packets_count += sanitized_transactions - .len() - .saturating_sub(retryable_transaction_indexes.len()); - - // Out of the buffered packets just retried, collect any still unprocessed - // transactions in this batch - *rebuffered_packet_count += retryable_transaction_indexes.len(); + self.storage.reinsert_packets(std::iter::once( + sanitized_transaction.into_inner_transaction().into_view(), + )); + } - slot_metrics_tracker - .increment_retryable_packets_count(retryable_transaction_indexes.len() as u64); + if has_reached_end_of_slot(reached_max_poh_height, bank) { + self.storage.reinsert_packets(all_vote_packets); + return true; + } + } - Some(retryable_transaction_indexes) + false } fn process_packets_transactions( @@ -500,25 +439,6 @@ impl VoteWorker { .filter_map(|(index, res)| res.as_ref().ok().map(|_| index)) .collect() } - - fn extract_retryable( - vote_packets: &mut ArrayVec, - retryable_vote_indices: Vec, - ) -> impl Iterator + '_ { - debug_assert!(retryable_vote_indices.is_sorted()); - let mut retryable_vote_indices = retryable_vote_indices.into_iter().peekable(); - - vote_packets - .drain(..) - .enumerate() - .filter_map(move |(i, packet)| { - (Some(&i) == retryable_vote_indices.peek()).then(|| { - retryable_vote_indices.next(); - - packet - }) - }) - } } fn consume_scan_should_process_packet( @@ -575,33 +495,14 @@ mod tests { crate::banking_stage::{ committer::Committer, tests::{create_slow_genesis_config, sanitize_transactions}, - vote_storage::tests::packet_from_slots, }, crossbeam_channel::bounded, solana_ledger::genesis_utils::GenesisConfigInfo, - solana_perf::packet::BytesPacket, solana_poh::record_channels::record_channels, - solana_runtime::genesis_utils::ValidatorVoteKeypairs, - solana_runtime_transaction::transaction_meta::TransactionMeta, solana_svm::account_loader::CheckedTransactionDetails, solana_system_transaction as system_transaction, - std::collections::HashSet, }; - fn to_runtime_transaction_view(packet: BytesPacket) -> RuntimeTransactionView { - let tx = - SanitizedTransactionView::try_new_sanitized(Arc::new(packet.buffer().to_vec()), true) - .unwrap(); - let tx = RuntimeTransaction::>::try_new( - tx, - MessageHash::Compute, - None, - ) - .unwrap(); - - RuntimeTransactionView::try_new(tx, None, &HashSet::default()).unwrap() - } - #[test] fn test_bank_prepare_filter_for_pending_transaction() { assert_eq!( @@ -656,78 +557,6 @@ mod tests { ); } - #[test] - fn extract_retryable_one_all_retryable() { - let keypair_a = ValidatorVoteKeypairs::new_rand(); - let mut packets: ArrayVec<_, 1> = ArrayVec::from_iter([to_runtime_transaction_view( - packet_from_slots(vec![(1, 1)], &keypair_a, None), - )]); - let retryable_indices = vec![0]; - - // Assert - Able to extract exactly one packet. - let expected = *packets[0].message_hash(); - let mut extracted = VoteWorker::extract_retryable(&mut packets, retryable_indices); - assert_eq!(extracted.next().unwrap().message_hash(), &expected); - assert!(extracted.next().is_none()); - } - - #[test] - fn extract_retryable_one_none_retryable() { - let keypair_a = ValidatorVoteKeypairs::new_rand(); - let mut packets: ArrayVec<_, 1> = ArrayVec::from_iter([to_runtime_transaction_view( - packet_from_slots(vec![(1, 1)], &keypair_a, None), - )]); - let retryable_indices = vec![]; - - // Assert - Able to extract exactly zero packets. - let mut extracted = VoteWorker::extract_retryable(&mut packets, retryable_indices); - assert!(extracted.next().is_none()); - } - - #[test] - fn extract_retryable_three_last_retryable() { - let keypair_a = ValidatorVoteKeypairs::new_rand(); - let mut packets: ArrayVec<_, 3> = ArrayVec::from_iter( - [ - packet_from_slots(vec![(5, 3)], &keypair_a, None), - packet_from_slots(vec![(6, 2)], &keypair_a, None), - packet_from_slots(vec![(7, 1)], &keypair_a, None), - ] - .into_iter() - .map(to_runtime_transaction_view), - ); - let retryable_indices = vec![2]; - - // Assert - Able to extract exactly one packet. - let expected = *packets[2].message_hash(); - let mut extracted = VoteWorker::extract_retryable(&mut packets, retryable_indices); - assert_eq!(extracted.next().unwrap().message_hash(), &expected); - assert!(extracted.next().is_none()); - } - - #[test] - fn extract_retryable_three_first_last_retryable() { - let keypair_a = ValidatorVoteKeypairs::new_rand(); - let mut packets: ArrayVec<_, 3> = ArrayVec::from_iter( - [ - packet_from_slots(vec![(5, 3)], &keypair_a, None), - packet_from_slots(vec![(6, 2)], &keypair_a, None), - packet_from_slots(vec![(7, 1)], &keypair_a, None), - ] - .into_iter() - .map(to_runtime_transaction_view), - ); - let retryable_indices = vec![0, 2]; - - // Assert - Able to extract exactly one packet. - let expected0 = *packets[0].message_hash(); - let expected1 = *packets[2].message_hash(); - let mut extracted = VoteWorker::extract_retryable(&mut packets, retryable_indices); - assert_eq!(extracted.next().unwrap().message_hash(), &expected0); - assert_eq!(extracted.next().unwrap().message_hash(), &expected1); - assert!(extracted.next().is_none()); - } - #[test] fn test_has_reached_end_of_slot() { let GenesisConfigInfo { genesis_config, .. } = create_slow_genesis_config(10_000); From 038013d928de79a20bcadf5c4a1ec3b92d4aa882 Mon Sep 17 00:00:00 2001 From: Alex Pyattaev Date: Thu, 11 Jun 2026 20:05:39 +0300 Subject: [PATCH 298/576] votor: rm unnecessary stake check (#13121) votor: rm unnecessary check --- votor/src/staked_validators_cache.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/votor/src/staked_validators_cache.rs b/votor/src/staked_validators_cache.rs index 0912b78690c..94fec023736 100644 --- a/votor/src/staked_validators_cache.rs +++ b/votor/src/staked_validators_cache.rs @@ -106,12 +106,7 @@ impl StakedValidatorsCache { let mut nodes: Vec<_> = epoch_staked_nodes .iter() - .filter(|(pubkey, stake)| { - let positive_stake = **stake > 0; - let not_self = pubkey != &&cluster_info.id(); - - positive_stake && (self.include_self || not_self) - }) + .filter(|(pubkey, _stake)| self.include_self || pubkey != &&cluster_info.id()) .filter_map(|(pubkey, stake)| { cluster_info.lookup_contact_info(pubkey, |node| { node.alpenglow().map(|alpenglow_socket| Node { From 32665e6c99389c010e1e65bf622db4ccaa3c95bc Mon Sep 17 00:00:00 2001 From: Alex Pyattaev Date: Thu, 11 Jun 2026 21:26:54 +0300 Subject: [PATCH 299/576] fix local cluster leader targeting (#13129) --- local-cluster/src/cluster_tests.rs | 198 ++++++++++++++++++----------- 1 file changed, 122 insertions(+), 76 deletions(-) diff --git a/local-cluster/src/cluster_tests.rs b/local-cluster/src/cluster_tests.rs index 4cade87eb80..956904a5ab0 100644 --- a/local-cluster/src/cluster_tests.rs +++ b/local-cluster/src/cluster_tests.rs @@ -19,7 +19,7 @@ use { solana_epoch_schedule::MINIMUM_SLOTS_PER_EPOCH, solana_gossip::{ cluster_info::{self, ClusterInfo}, - contact_info::{ContactInfo, Protocol}, + contact_info::ContactInfo, crds::Cursor, crds_data::{self, CrdsData}, crds_value::{CrdsValue, CrdsValueLabel}, @@ -145,6 +145,58 @@ impl TpuSender { } } +/// Resolve the current slot leader and its QUIC TPU address via RPC. +/// +/// Returns `None` when the leader cannot yet be mapped to a QUIC TPU address. +fn current_leader(rpc_client: &RpcClient) -> Option<(Pubkey, SocketAddr)> { + let leader_pubkey = rpc_client + .get_slot_leader() + .expect("current_leader: get_slot_leader"); + let tpu = rpc_client + .get_cluster_nodes() + .expect("current_leader: get_cluster_nodes") + .into_iter() + .find(|n| n.pubkey == leader_pubkey.to_string()) + .and_then(|n| n.tpu_quic)?; + Some((leader_pubkey, tpu)) +} + +/// Resolve the QUIC TPU address of the current slot leader via RPC. +/// +/// Returns `None` when the leader cannot yet be mapped to a QUIC TPU address +fn current_leader_tpu(rpc_client: &RpcClient) -> Option { + current_leader(rpc_client).map(|(_, tpu)| tpu) +} + +/// Poll until `transaction` is observed as processed by `rpc_client`, or `timeout` elapses. +/// +/// Returns `true` once the transaction is processed. Returns `false` if timeout expires. +fn poll_processed_until_timeout( + rpc_client: &RpcClient, + transaction: &Transaction, + timeout: Duration, +) -> bool { + let deadline = Instant::now() + timeout; + loop { + if let Ok(Some(status)) = rpc_client.get_signature_status_with_commitment( + &transaction.signatures[0], + CommitmentConfig::processed(), + ) { + // Surface a genuine execution error rather than silently retrying it as if the + // transaction had never arrived. + assert!( + status.is_ok(), + "poll_processed_within: transaction failed on-chain: {status:?}" + ); + return true; + } + if Instant::now() >= deadline { + return false; + } + sleep(Duration::from_millis(200)); + } +} + /// Spend and verify from every node in the network pub fn spend_and_verify_all_nodes( entry_point_info: &ContactInfo, @@ -181,19 +233,9 @@ pub fn spend_and_verify_all_nodes( .unwrap(); let mut transaction = system_transaction::transfer(funding_keypair, &random_keypair.pubkey(), 1, blockhash); - // Like send_many_transactions: use ingress_node for RPC but send to the - // current slot leader, matching old TpuClient behaviour. - let leader_pubkey = rpc_client - .get_slot_leader() - .expect("spend_and_verify_all_nodes: get_slot_leader"); - let tpu_addr = rpc_client - .get_cluster_nodes() - .expect("spend_and_verify_all_nodes: get_cluster_nodes") - .into_iter() - .find(|n| n.pubkey == leader_pubkey.to_string()) - .and_then(|n| n.tpu_quic) - .or_else(|| ingress_node.tpu(Protocol::QUIC)) - .expect("spend_and_verify_all_nodes: leader has no QUIC TPU address"); + // find target TPU address from RPC + let tpu_addr = current_leader_tpu(&rpc_client) + .expect("spend_and_verify_all_nodes: no current leader TPU"); tpu_sender.with_connection(tpu_addr, |sender| { tpu_sender .send_and_confirm_transaction( @@ -237,19 +279,9 @@ pub fn send_many_transactions( num_txs: u64, ) -> HashMap { let rpc_client = RpcClient::new(format!("http://{}", node.rpc().unwrap())); - // Replicate old TpuClient behaviour: send to the current slot leader, not to `node`. - // `node` is used only as the RPC endpoint; the leader is discovered via RPC + cluster nodes. - let leader_pubkey = rpc_client - .get_slot_leader() - .expect("send_many_transactions: get_slot_leader"); - let tpu_addr = rpc_client - .get_cluster_nodes() - .expect("send_many_transactions: get_cluster_nodes") - .into_iter() - .find(|n| n.pubkey == leader_pubkey.to_string()) - .and_then(|n| n.tpu_quic) - .or_else(|| node.tpu(Protocol::QUIC)) - .expect("send_many_transactions: leader has no QUIC TPU address"); + // Ask RPC who the leader is + let tpu_addr = + current_leader_tpu(&rpc_client).expect("send_many_transactions: no current leader TPU"); // One persistent QUIC connection for all transactions to avoid per-tx handshake overhead. tpu_sender.with_connection(tpu_addr, |sender| { let mut expected_balances = HashMap::new(); @@ -386,59 +418,73 @@ pub fn kill_entry_and_spend_and_verify_rest( .expect("balance in source"); assert_ne!(balance, 0); - let tpu_addr = ingress_node - .tpu(Protocol::QUIC) - .expect("ingress_node has no QUIC TPU address"); - - // Retry sending a transaction to the current ingress node until it is - // observed by the entire cluster or we exhaust all retries. - tpu_sender.with_connection(tpu_addr, |sender| { - let mut result = Ok(()); - let mut retries = 0; - loop { - retries += 1; - if retries > 5 { - result.unwrap(); - } - - // Send a simple transfer transaction to the current ingress node. - let random_keypair = Keypair::new(); - let (blockhash, _) = rpc_client - .get_latest_blockhash_with_commitment(CommitmentConfig::processed()) - .unwrap(); - let mut transaction = system_transaction::transfer( - funding_keypair, - &random_keypair.pubkey(), - 1, - blockhash, + // After the bootstrap leader is killed it stays in the (fixed) leader schedule for the + // rest of the epoch, so `get_slot_leader` points at the dead node for ~1/4 of slots. It + // accepts no QUIC connection, so targeting it wastes a whole poll window; skip it and wait + // one slot for rotation to a live leader instead. For live leaders, resend with a fresh + // blockhash each attempt and give the send a bounded window to land. + let killed_leader = *entry_point_info.pubkey(); + const MAX_SEND_ATTEMPTS: usize = 20; + // Generous enough for a live leader to land a transaction on a loaded CI runner, yet far + // below the blockhash lifetime so a wrong/stuck target is abandoned in seconds. + let poll_window = Duration::from_millis(slot_millis * 20).max(Duration::from_secs(8)); + let mut confirmed = false; + for attempt in 1..=MAX_SEND_ATTEMPTS { + let Some((leader_pubkey, tpu_addr)) = current_leader(&rpc_client) else { + // Leader not resolvable, back off a slot + sleep(Duration::from_millis(slot_millis)); + continue; + }; + if leader_pubkey == killed_leader { + // Scheduled leader is the dead bootstrap node; wait for rotation rather than + // burning a poll window on a node that will never accept the transaction. + sleep(Duration::from_millis(slot_millis)); + continue; + } + let random_keypair = Keypair::new(); + let (blockhash, _) = rpc_client + .get_latest_blockhash_with_commitment(CommitmentConfig::processed()) + .unwrap(); + let transaction = system_transaction::transfer( + funding_keypair, + &random_keypair.pubkey(), + 1, + blockhash, + ); + let wire_tx = wincode::serialize(&transaction) + .expect("kill_entry_and_spend_and_verify_rest: should serialize transaction"); + // Poll *inside* the connection: tpu-client-next only enqueues the transaction, so the + // QUIC client must stay alive long enough to flush it onto the wire (and retransmit) + // before we tear it down. + let landed = tpu_sender.with_connection(tpu_addr, |sender| { + tpu_sender.send_wire_transaction(sender, wire_tx); + poll_processed_until_timeout(&rpc_client, &transaction, poll_window) + }); + if !landed { + warn!( + "kill_entry_and_spend_and_verify_rest: attempt {attempt} to {tpu_addr} did \ + not land within {poll_window:?}, re-resolving leader" ); + continue; + } - if let Err(err) = tpu_sender.send_and_confirm_transaction( - sender, - &rpc_client, - &[funding_keypair], - &mut transaction, - 5, - ) { - result = Err(err); - continue; - } - - // Ensure all non-entry point nodes are able to confirm the - // transaction. - info!("poll_all_nodes_for_signature()"); - match poll_all_nodes_for_signature(entry_point_info, &cluster_nodes, &transaction) { - Err(e) => { - info!("poll_all_nodes_for_signature() failed {e:?}"); - result = Err(e); - } - Ok(()) => { - info!("poll_all_nodes_for_signature() succeeded, done."); - break; - } + // The transaction landed at the targeted node, ensure every other node + // observes it too. If it landed on a fork that loses, resend. + info!("poll_all_nodes_for_signature()"); + match poll_all_nodes_for_signature(entry_point_info, &cluster_nodes, &transaction) { + Ok(()) => { + info!("poll_all_nodes_for_signature() succeeded, done."); + confirmed = true; + break; } + Err(e) => info!("poll_all_nodes_for_signature() failed {e:?}, resending"), } - }); + } + assert!( + confirmed, + "kill_entry_and_spend_and_verify_rest: transaction never confirmed on all surviving \ + nodes after {MAX_SEND_ATTEMPTS} attempts" + ); } } From d50816520fbc66b4e24878a2cf0ef05b110999b7 Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Thu, 11 Jun 2026 14:27:34 -0400 Subject: [PATCH 300/576] nit: use the parent bank when fetching the timestamp for BCL (#13140) --- core/src/block_creation_loop.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/block_creation_loop.rs b/core/src/block_creation_loop.rs index 699163e3f11..e752ee135b9 100644 --- a/core/src/block_creation_loop.rs +++ b/core/src/block_creation_loop.rs @@ -517,7 +517,7 @@ fn produce_block_footer( if let Some(parent_bank) = bank.parent() { // Get parent time from alpenglow clock (nanoseconds) or fall back to clock sysvar (seconds -> nanoseconds) - let parent_time_nanos = bank + let parent_time_nanos = parent_bank .get_nanosecond_clock() .unwrap_or_else(|| bank.clock().unix_timestamp.saturating_mul(1_000_000_000)); let parent_slot = parent_bank.slot(); From 95864a000226345f4df71936fb59124c3a16a7bf Mon Sep 17 00:00:00 2001 From: Lucas Ste <38472950+LucasSte@users.noreply.github.com> Date: Thu, 11 Jun 2026 15:43:34 -0300 Subject: [PATCH 301/576] Unify `translate_signers_c` and `translate_signers_rust`. (#13095) * Delete SolSignerSeedC * Delete translate_signers_c * Rename function * Reviewer's suggestions --- program-runtime/src/cpi.rs | 117 ++++++---------------------- syscalls/src/cpi.rs | 29 ------- transaction-context/src/vm_slice.rs | 2 + 3 files changed, 27 insertions(+), 121 deletions(-) diff --git a/program-runtime/src/cpi.rs b/program-runtime/src/cpi.rs index d5cd9c2027b..5bfc8b18176 100644 --- a/program-runtime/src/cpi.rs +++ b/program-runtime/src/cpi.rs @@ -102,22 +102,6 @@ struct SolAccountInfo { pub executable: bool, } -/// Rust representation of C's SolSignerSeed -#[derive(Debug)] -#[repr(C)] -struct SolSignerSeedC { - pub addr: u64, - pub len: u64, -} - -/// Rust representation of C's SolSignerSeeds -#[derive(Debug)] -#[repr(C)] -struct SolSignerSeedsC { - pub addr: u64, - pub len: u64, -} - /// Maximum number of account info structs that can be used in a single CPI invocation const MAX_CPI_ACCOUNT_INFOS: usize = 255; @@ -549,12 +533,6 @@ pub trait SyscallInvokeSigned { account_infos_len: u64, invoke_context: &InvokeContext, ) -> Result>, Error>; - fn translate_signers( - program_id: &Pubkey, - signers_seeds_addr: u64, - signers_seeds_len: u64, - invoke_context: &InvokeContext, - ) -> Result, Error>; } pub fn translate_instruction_rust( @@ -650,7 +628,7 @@ pub fn translate_accounts_rust<'a>( )? } -pub fn translate_signers_rust( +pub fn translate_signers( program_id: &Pubkey, signers_seeds_addr: u64, signers_seeds_len: u64, @@ -658,7 +636,6 @@ pub fn translate_signers_rust( ) -> Result, Error> { let check_aligned = invoke_context.get_check_aligned(); let memory_mapping = invoke_context.memory_contexts.memory_mapping()?; - let mut signers = Vec::new(); if signers_seeds_len > 0 { let signers_seeds = translate_slice::>>( memory_mapping, @@ -669,27 +646,28 @@ pub fn translate_signers_rust( if signers_seeds.len() > MAX_SIGNERS { return Err(Box::new(CpiError::TooManySigners)); } - for signer_seeds in signers_seeds.iter() { - let untranslated_seeds = translate_slice::>( - memory_mapping, - signer_seeds.ptr(), - signer_seeds.len(), - check_aligned, - )?; - if untranslated_seeds.len() > MAX_SEEDS { - return Err(Box::new(InstructionError::MaxSeedLengthExceeded)); - } - let seeds = untranslated_seeds - .iter() - .map(|untranslated_seed| { - translate_vm_slice(untranslated_seed, memory_mapping, check_aligned) - }) - .collect::, Error>>()?; - let signer = - Pubkey::create_program_address(&seeds, program_id).map_err(CpiError::BadSeeds)?; - signers.push(signer); - } - Ok(signers) + Ok(signers_seeds + .iter() + .map(|signer_seeds| { + let untranslated_seeds = translate_slice::>( + memory_mapping, + signer_seeds.ptr(), + signer_seeds.len(), + check_aligned, + )?; + if untranslated_seeds.len() > MAX_SEEDS { + return Err(Box::new(InstructionError::MaxSeedLengthExceeded) as Error); + } + let seeds_bytes = untranslated_seeds + .iter() + .map(|untranslated_seed| { + translate_vm_slice(untranslated_seed, memory_mapping, check_aligned) + }) + .collect::, Error>>()?; + Pubkey::create_program_address(&seeds_bytes, program_id) + .map_err(|err| Box::new(CpiError::BadSeeds(err)) as Error) + }) + .collect::, Error>>()?) } else { Ok(vec![]) } @@ -791,51 +769,6 @@ pub fn translate_accounts_c<'a>( )? } -pub fn translate_signers_c( - program_id: &Pubkey, - signers_seeds_addr: u64, - signers_seeds_len: u64, - invoke_context: &InvokeContext, -) -> Result, Error> { - let check_aligned = invoke_context.get_check_aligned(); - let memory_mapping = invoke_context.memory_contexts.memory_mapping()?; - if signers_seeds_len > 0 { - let signers_seeds = translate_slice::( - memory_mapping, - signers_seeds_addr, - signers_seeds_len, - check_aligned, - )?; - if signers_seeds.len() > MAX_SIGNERS { - return Err(Box::new(CpiError::TooManySigners)); - } - Ok(signers_seeds - .iter() - .map(|signer_seeds| { - let seeds = translate_slice::( - memory_mapping, - signer_seeds.addr, - signer_seeds.len, - check_aligned, - )?; - if seeds.len() > MAX_SEEDS { - return Err(Box::new(InstructionError::MaxSeedLengthExceeded) as Error); - } - let seeds_bytes = seeds - .iter() - .map(|seed| { - translate_slice::(memory_mapping, seed.addr, seed.len, check_aligned) - }) - .collect::, Error>>()?; - Pubkey::create_program_address(&seeds_bytes, program_id) - .map_err(|err| Box::new(CpiError::BadSeeds(err)) as Error) - }) - .collect::, Error>>()?) - } else { - Ok(vec![]) - } -} - /// Call process instruction, common to both Rust and C pub fn cpi_common( invoke_context: &mut InvokeContext, @@ -865,7 +798,7 @@ pub fn cpi_common( .transaction_context .get_current_instruction_context()?; let caller_program_id = instruction_context.get_program_key()?; - let signers = S::translate_signers( + let signers = translate_signers( caller_program_id, signers_seeds_addr, signers_seeds_len, @@ -1935,7 +1868,7 @@ mod tests { )) .unwrap(); - let signers = translate_signers_rust(&program_id, vm_addr, 1, &invoke_context).unwrap(); + let signers = translate_signers(&program_id, vm_addr, 1, &invoke_context).unwrap(); assert_eq!(signers[0], derived_key); } diff --git a/syscalls/src/cpi.rs b/syscalls/src/cpi.rs index fe47d7812da..9f9383d865b 100644 --- a/syscalls/src/cpi.rs +++ b/syscalls/src/cpi.rs @@ -4,7 +4,6 @@ use { solana_program_runtime::cpi::{ SyscallInvokeSigned, TranslatedAccount, cpi_common, translate_accounts_c, translate_accounts_rust, translate_instruction_c, translate_instruction_rust, - translate_signers_c, translate_signers_rust, }, }; @@ -45,20 +44,6 @@ impl SyscallInvokeSigned for SyscallInvokeSignedRust { ) -> Result>, Error> { translate_accounts_rust(account_infos_addr, account_infos_len, invoke_context) } - - fn translate_signers( - program_id: &Pubkey, - signers_seeds_addr: u64, - signers_seeds_len: u64, - invoke_context: &InvokeContext, - ) -> Result, Error> { - translate_signers_rust( - program_id, - signers_seeds_addr, - signers_seeds_len, - invoke_context, - ) - } } declare_builtin_function!( @@ -98,18 +83,4 @@ impl SyscallInvokeSigned for SyscallInvokeSignedC { ) -> Result>, Error> { translate_accounts_c(account_infos_addr, account_infos_len, invoke_context) } - - fn translate_signers( - program_id: &Pubkey, - signers_seeds_addr: u64, - signers_seeds_len: u64, - invoke_context: &InvokeContext, - ) -> Result, Error> { - translate_signers_c( - program_id, - signers_seeds_addr, - signers_seeds_len, - invoke_context, - ) - } } diff --git a/transaction-context/src/vm_slice.rs b/transaction-context/src/vm_slice.rs index b973c5c2e73..a9445c9c336 100644 --- a/transaction-context/src/vm_slice.rs +++ b/transaction-context/src/vm_slice.rs @@ -11,6 +11,8 @@ use std::marker::PhantomData; +/// VmSlice serves as a stable layout for slices shared between the guest and the host. +/// It is also the layout for SolSignerSeedC and SolSignerSeedsC. #[repr(C)] #[derive(Copy, Clone, Debug, PartialEq)] pub struct VmSlice { From 6dd13d6fc2f45576fde219f20070997acf8a4131 Mon Sep 17 00:00:00 2001 From: Rory Harris Date: Thu, 11 Jun 2026 12:42:49 -0700 Subject: [PATCH 302/576] accounts-db: decouple flush dedup from iteration order (#13116) * Move de-duplication of accounts to loop prior to flush * address review: faster hashers + should_clean rename - select_pubkeys_to_flush returns an IntMap (BuildNoHashHasher), matching the slot-keyed maps used elsewhere (e.g. the write cache's DashMap). - the dedup set and each root's flush set use PubkeyHasherBuilder, matching the other pubkey-keyed collections in accounts-db. - rename the `clean` flag to `should_clean`. Co-Authored-By: Claude Opus 4.8 (1M context) * Updates from review - Move Enum Definition. - Add new Enum for FlushShouldClean - Use HashMapExt --------- Co-authored-by: Claude Opus 4.8 (1M context) --- accounts-db/src/accounts_db.rs | 173 ++++++++++++++++++--------- accounts-db/src/accounts_db/stats.rs | 2 + accounts-db/src/accounts_db/tests.rs | 38 ++++++ 3 files changed, 159 insertions(+), 54 deletions(-) diff --git a/accounts-db/src/accounts_db.rs b/accounts-db/src/accounts_db.rs index ae8e70c3a75..97dcbe17152 100644 --- a/accounts-db/src/accounts_db.rs +++ b/accounts-db/src/accounts_db.rs @@ -64,6 +64,7 @@ use { utils::{self, create_account_shared_data}, }, agave_fs::buffered_reader::RequiredLenBufFileRead, + ahash::HashMapExt, bv::BitVec, dashmap::{DashMap, DashSet}, log::*, @@ -4673,6 +4674,7 @@ impl AccountsDb { flush_stats.store_accounts_timing.handle_reclaims_elapsed, i64 ), + ("select_pubkeys_us", flush_stats.select_pubkeys_us.0, i64), ); } @@ -4688,20 +4690,9 @@ impl AccountsDb { // If there is a long running scan going on, this could prevent any cleaning // based on updates from slots > `max_clean_root`. let max_clean_root = self.max_clean_root(requested_flush_root); - - let mut written_accounts = HashSet::new(); - - let mut should_flush_f = { - Some(move |&pubkey: &Pubkey| { - // if not in hashset, then not flushed previously, so flush it - written_accounts.insert(pubkey) - }) - }; - self.flush_rooted_accounts_cache( requested_flush_root, - max_clean_root, - should_flush_f.as_mut(), + FlushShouldClean::Yes { max_clean_root }, ) } @@ -4709,15 +4700,19 @@ impl AccountsDb { // to storage but older versions of the accounts are still required. For example, the // older versions may be needed for snapshotting. fn flush_rooted_accounts_cache_without_clean(&self) -> (usize, usize, FlushStats) { - self.flush_rooted_accounts_cache(None, None, None::<&mut fn(&_) -> bool>) + self.flush_rooted_accounts_cache(None, FlushShouldClean::No) } /// Flush all rooted slots up to `requested_flush_root`. + /// + /// When `should_clean` is `Yes`, only the newest version of each account across the + /// flushed roots (at or below its `max_clean_root`) is written to storage; older + /// versions are purged from the index instead. When `No`, every account in + /// every flushed root is written. fn flush_rooted_accounts_cache( &self, requested_flush_root: Option, - max_clean_root: Option, - mut should_flush_f: Option<&mut impl FnMut(&Pubkey) -> bool>, + should_clean: FlushShouldClean, ) -> (usize, usize, FlushStats) { // Always flush up to `requested_flush_root`, which is necessary for things like snapshotting. let flushed_roots: BTreeSet = @@ -4725,14 +4720,30 @@ impl AccountsDb { let max_flush_root = flushed_roots.last().copied(); let num_new_roots = flushed_roots.len(); + // For each root being flushed, which of its cached accounts to write to storage. + let (pubkeys_to_flush, select_pubkeys_us) = match should_clean { + FlushShouldClean::Yes { max_clean_root } => { + measure_us!(self.select_pubkeys_to_flush(&flushed_roots, max_clean_root)) + } + // Not cleaning: every root writes all of its accounts. + FlushShouldClean::No => ( + flushed_roots + .iter() + .map(|&root| (root, PubkeysToFlush::All)) + .collect(), + 0, + ), + }; + // Iterate from highest to lowest so that we don't need to flush earlier // outdated updates in earlier roots let mut num_roots_flushed = 0; - let mut flush_stats = FlushStats::default(); + let mut flush_stats = FlushStats { + select_pubkeys_us: Saturating(select_pubkeys_us), + ..FlushStats::default() + }; for root in flushed_roots.into_iter().rev() { - if let Some(stats) = - self.flush_slot_cache(root, should_flush_f.as_mut(), max_clean_root) - { + if let Some(stats) = self.flush_slot_cache(root, &pubkeys_to_flush[&root]) { num_roots_flushed += 1; flush_stats.accumulate(&stats); } @@ -4746,37 +4757,69 @@ impl AccountsDb { (num_new_roots, num_roots_flushed, flush_stats) } + /// Determines which of each flushed root's accounts to write to storage when cleaning: each + /// root keeps `Only` the newest version of each account, deduped newest-first. A root above + /// `max_clean_root` instead flushes `All`, since an in-flight scan may still need those + /// versions; `None` means there is no bound and every flushed root is cleaned. + fn select_pubkeys_to_flush( + &self, + flushed_roots: &BTreeSet, + max_clean_root: Option, + ) -> IntMap { + let mut pubkeys_to_flush = IntMap::with_capacity(flushed_roots.len()); + + // Presize the dedup set from the newest root (flushed first), doubled to leave room + // for unique accounts contributed by older roots. + let dedup_capacity = flushed_roots + .last() + .and_then(|&root| self.accounts_cache.slot_cache(root)) + .map_or(0, |slot_cache| slot_cache.len() * 2); + let mut written_accounts = + HashSet::with_capacity_and_hasher(dedup_capacity, PubkeyHasherBuilder::default()); + + // Iterate from newest root to oldest root being flushed in this batch + for &root in flushed_roots.iter().rev() { + let cleaned = max_clean_root.is_none_or(|max_clean_root| root <= max_clean_root); + let to_flush = if !cleaned { + PubkeysToFlush::All + } else { + let mut flush_keys = HashSet::default(); + if let Some(slot_cache) = self.accounts_cache.slot_cache(root) { + for entry in slot_cache.iter() { + let pubkey = *entry.key(); + // If not seen in a newer root, this is the newest version, so flush it. + if written_accounts.insert(pubkey) { + flush_keys.insert(pubkey); + } + } + } + PubkeysToFlush::Only(flush_keys) + }; + pubkeys_to_flush.insert(root, to_flush); + } + pubkeys_to_flush + } + fn do_flush_slot_cache( &self, slot: Slot, slot_cache: &SlotCache, - mut should_flush_f: Option<&mut impl FnMut(&Pubkey) -> bool>, - max_clean_root: Option, + pubkeys_to_flush: &PubkeysToFlush, ) -> FlushStats { debug_assert!(self.accounts_index.is_alive_root(slot)); let mut flush_stats = FlushStats::default(); let iter_items: Vec<_> = slot_cache.iter().collect(); - let mut pubkeys: Vec = vec![]; - if should_flush_f.is_some() { - if let Some(max_clean_root) = max_clean_root { - if slot > max_clean_root { - // Only if the root is greater than the `max_clean_root` do we - // have to prevent cleaning, otherwise, just default to `should_flush_f` - // for any slots <= `max_clean_root` - should_flush_f = None; - } - } - } + let mut purged_pubkeys: Vec = vec![]; let accounts: Vec<(&Pubkey, &AccountSharedData)> = iter_items .iter() .filter_map(|iter_item| { let key = iter_item.key(); let account = &iter_item.value().account; - let should_flush = should_flush_f - .as_mut() - .map(|should_flush_f| should_flush_f(key)) - .unwrap_or(true); + let should_flush = match pubkeys_to_flush { + PubkeysToFlush::All => true, + PubkeysToFlush::Only(flush_keys) => flush_keys.contains(key), + }; if should_flush { flush_stats.num_bytes_flushed += AppendVec::calculate_stored_size(account.data().len()) as u64; @@ -4785,7 +4828,7 @@ impl AccountsDb { } else { // If we don't flush, we have to remove the entry from the // index, since it's equivalent to purging - pubkeys.push(*key); + purged_pubkeys.push(*key); flush_stats.num_bytes_purged += AppendVec::calculate_stored_size(account.data().len()) as u64; flush_stats.num_accounts_purged += 1; @@ -4797,17 +4840,16 @@ impl AccountsDb { let is_dead_slot = accounts.is_empty(); // Remove the account index entries from earlier roots that are outdated by later roots. // Safe because queries to the index will be reading updates from later roots. - self.purge_slot_cache_pubkeys(slot, pubkeys, is_dead_slot); + self.purge_slot_cache_pubkeys(slot, purged_pubkeys, is_dead_slot); - // Use ReclaimOldSlots to reclaim old slots if marking obsolete accounts and cleaning - // Cleaning is enabled if `should_flush_f` is Some. - // should_flush_f is set to None when + // Use ReclaimOldSlots to reclaim old slots if marking obsolete accounts and cleaning. + // Cleaning is enabled if pubkeys_to_flush is PubkeysToFlush::Only + // pubkeys_to_flush is PubkeysToFlush::All when // 1) There's an ongoing scan to avoid reclaiming accounts being scanned. // 2) The slot is > max_clean_root to prevent unrooted slots from reclaiming rooted versions. - let reclaim_method = if should_flush_f.is_some() { - UpsertReclaim::ReclaimOldSlots - } else { - UpsertReclaim::IgnoreReclaims + let reclaim_method = match pubkeys_to_flush { + PubkeysToFlush::Only(_) => UpsertReclaim::ReclaimOldSlots, + PubkeysToFlush::All => UpsertReclaim::IgnoreReclaims, }; if !is_dead_slot { @@ -4863,19 +4905,18 @@ impl AccountsDb { flush_stats } - /// `should_flush_f` is an optional closure that determines whether a given - /// account should be flushed. Passing `None` will by default flush all - /// accounts + /// `pubkeys_to_flush` selects which accounts are written to storage: `Only(set)` flushes + /// just the pubkeys in the set (purging the rest from the index), while `All` flushes + /// every account in the slot. fn flush_slot_cache( &self, slot: Slot, - should_flush_f: Option<&mut impl FnMut(&Pubkey) -> bool>, - max_clean_root: Option, + pubkeys_to_flush: &PubkeysToFlush, ) -> Option { // If a slot cache exists for this slot, flush it. - self.accounts_cache.slot_cache(slot).map(|slot_cache| { - self.do_flush_slot_cache(slot, &slot_cache, should_flush_f, max_clean_root) - }) + self.accounts_cache + .slot_cache(slot) + .map(|slot_cache| self.do_flush_slot_cache(slot, &slot_cache, pubkeys_to_flush)) } fn report_store_stats(&self) { @@ -6922,6 +6963,30 @@ impl AccountsDb { } } +/// Whether a rooted-cache flush should clean (dedup across roots, keeping only the newest +/// version of each account and reclaiming older ones) or write every account untouched. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum FlushShouldClean { + /// Write every account in every flushed root. Older versions must be preserved (e.g. for + /// snapshotting). + No, + /// Clean roots at or below `max_clean_root`; roots above it still flush all their accounts + /// (`None` means clean every flushed root). + Yes { max_clean_root: Option }, +} + +/// Which of a slot's cached accounts to write to storage when flushing it. +#[derive(Debug, PartialEq, Eq)] +enum PubkeysToFlush { + /// Flush every account in the slot, reclaiming nothing. Used for roots above + /// `max_clean_root` and when not cleaning, since an in-flight scan may still need + /// those versions. + All, + /// Flush only these pubkeys (the newest version of each, per `select_pubkeys_to_flush`), + /// purging the rest from the index and reclaiming older versions. + Only(HashSet), +} + /// Specify whether obsolete accounts should be marked or not during reclaims /// They should only be marked if they are also getting unreffed in the index /// Temporarily allow dead code until the feature is implemented @@ -7013,7 +7078,7 @@ impl AccountsDb { .contains(&slot), "slot: {slot}" ); - self.flush_slot_cache(slot, None::<&mut fn(&_) -> bool>, None); + self.flush_slot_cache(slot, &PubkeysToFlush::All); } /// useful to adapt tests written prior to introduction of the write cache diff --git a/accounts-db/src/accounts_db/stats.rs b/accounts-db/src/accounts_db/stats.rs index f9dd010abd3..fff8ed91995 100644 --- a/accounts-db/src/accounts_db/stats.rs +++ b/accounts-db/src/accounts_db/stats.rs @@ -353,6 +353,7 @@ pub struct FlushStats { pub num_bytes_purged: Saturating, pub store_accounts_timing: StoreAccountsTiming, pub store_accounts_total_us: Saturating, + pub select_pubkeys_us: Saturating, } impl FlushStats { @@ -364,6 +365,7 @@ impl FlushStats { self.store_accounts_timing .accumulate(&other.store_accounts_timing); self.store_accounts_total_us += other.store_accounts_total_us; + self.select_pubkeys_us += other.select_pubkeys_us; } } diff --git a/accounts-db/src/accounts_db/tests.rs b/accounts-db/src/accounts_db/tests.rs index bfb239a3c06..2db6769459d 100644 --- a/accounts-db/src/accounts_db/tests.rs +++ b/accounts-db/src/accounts_db/tests.rs @@ -3330,6 +3330,44 @@ fn test_load_with_read_only_accounts_cache() { assert_eq!(slot, 2); } +/// `select_pubkeys_to_flush` keeps only the newest version of each account across the +/// cleaned roots and flushes roots above `max_clean_root` in full. +#[test] +fn test_select_pubkeys_to_flush() { + let db = AccountsDb::new_single_for_tests(); + let account = AccountSharedData::new(1, 0, &Pubkey::default()); + + // The same account is written in all three roots. + let roots = BTreeSet::from([5, 10, 15]); + let shared = Pubkey::new_unique(); + for &slot in &roots { + db.accounts_cache.store(slot, &shared, account.clone()); + } + + let shared_only = PubkeysToFlush::Only([shared].into_iter().collect()); + let deduped = PubkeysToFlush::Only(HashSet::default()); + + // No bound: every flushed root is cleaned, so `shared` is written only at its newest root + // (15), deduped from 5 and 10. + let plans = db.select_pubkeys_to_flush(&roots, None); + assert_eq!(plans[&15], shared_only); + assert_eq!(plans[&10], deduped); + assert_eq!(plans[&5], deduped); + + // max_clean_root = 15 (== newest root): same result, every root is at or below the bound. + let plans = db.select_pubkeys_to_flush(&roots, Some(15)); + assert_eq!(plans[&15], shared_only); + assert_eq!(plans[&10], deduped); + assert_eq!(plans[&5], deduped); + + // max_clean_root = 10: root 15 is above the boundary and flushes `All` (no dedup), so + // `shared` is not dropped from root 10 — a scan at root 10 may still need that version. + let plans = db.select_pubkeys_to_flush(&roots, Some(10)); + assert_eq!(plans[&15], PubkeysToFlush::All); + assert_eq!(plans[&10], shared_only); + assert_eq!(plans[&5], deduped); +} + #[test] fn test_flush_cache_clean() { let db = Arc::new(AccountsDb::new_single_for_tests()); From 9424ed13952959c50e65311b9c6e4d7b9981f512 Mon Sep 17 00:00:00 2001 From: Rory Harris Date: Thu, 11 Jun 2026 14:03:20 -0700 Subject: [PATCH 303/576] Iterate roots in forwards direction (#13142) * Switch iterator in flush roots to forwards (rather than reverse) * Re-add into_iter * Revert "Re-add into_iter" This reverts commit 16e342d68710983f877d73ae98518a09cd025ef3. --- accounts-db/src/accounts_db.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/accounts-db/src/accounts_db.rs b/accounts-db/src/accounts_db.rs index 97dcbe17152..f7adafa281c 100644 --- a/accounts-db/src/accounts_db.rs +++ b/accounts-db/src/accounts_db.rs @@ -4735,14 +4735,12 @@ impl AccountsDb { ), }; - // Iterate from highest to lowest so that we don't need to flush earlier - // outdated updates in earlier roots let mut num_roots_flushed = 0; let mut flush_stats = FlushStats { select_pubkeys_us: Saturating(select_pubkeys_us), ..FlushStats::default() }; - for root in flushed_roots.into_iter().rev() { + for root in flushed_roots { if let Some(stats) = self.flush_slot_cache(root, &pubkeys_to_flush[&root]) { num_roots_flushed += 1; flush_stats.accumulate(&stats); From 2c6fe605e846333c701f35dfb3e2460e10ff062d Mon Sep 17 00:00:00 2001 From: Ilan Gitter <8359193+gitteri@users.noreply.github.com> Date: Thu, 11 Jun 2026 19:47:34 -0600 Subject: [PATCH 304/576] transaction-status: fix parsed account labels for confidential transfer `Deposit` and `Withdraw` (#13149) * Fix jsonParsed account labels for confidential-transfer Deposit and Withdraw The Deposit and Withdraw parsers mapped accounts as [0]=source, [1]=destination, [2]=mint. But these instructions move funds within a single token account (public <-> confidential) and have no destination; their real layout is [token_account, mint, (proof accounts), owner/signers]. As a result jsonParsed returned the owner pubkey in the `mint` field and the real mint under a nonexistent `destination` field, and single-owner Deposit (3 accounts) failed the account-count check and did not parse at all. Map account=[0], mint=[1], drop the phantom destination, start the Withdraw proof/signer offset at 2, and lower the account-count guards (Deposit 4->3, Withdraw 5->4). Transfer and TransferWithFee are unchanged: they genuinely have source/mint/destination. Adds value-asserting tests for Deposit and Withdraw; the existing tests only checked that parsing did not panic, which is why the mislabeling went unnoticed. * changelog: document breaking jsonParsed change for confidential transfer Deposit/Withdraw --------- Co-authored-by: Claude --- CHANGELOG.md | 1 + .../extension/confidential_transfer.rs | 97 +++++++++++++++++-- 2 files changed, 88 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e37e6476767..b8c9ec0b80a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Release channels have their own copy of this changelog: ## 4.2.0-Unreleased ### RPC #### Breaking +* The `jsonParsed` output for confidential transfer `Deposit` (`depositConfidentialTransfer`) and `Withdraw` (`withdrawConfidentialTransfer`) instructions has been corrected. These instructions operate on a single token account, so the mislabeled `source` and `destination` fields have been replaced by a single `account` field (the `mint` field is unchanged). #### Changes * Added `RpcClient::get_latest_blockhash_with_commitment_and_context`, which returns the `getLatestBlockhash` response together with its context (notably `context.slot`). diff --git a/transaction-status/src/parse_token/extension/confidential_transfer.rs b/transaction-status/src/parse_token/extension/confidential_transfer.rs index dd4d47c41b5..9c04f9ee349 100644 --- a/transaction-status/src/parse_token/extension/confidential_transfer.rs +++ b/transaction-status/src/parse_token/extension/confidential_transfer.rs @@ -167,16 +167,15 @@ pub(in crate::parse_token) fn parse_confidential_transfer_instruction( }) } ConfidentialTransferInstruction::Deposit => { - check_num_token_accounts(account_indexes, 4)?; + check_num_token_accounts(account_indexes, 3)?; let deposit_data: DepositInstructionData = *decode_instruction_data(instruction_data) .map_err(|_| { ParseInstructionError::InstructionNotParsable(ParsableProgram::SplToken) })?; let amount: u64 = deposit_data.amount.into(); let mut value = json!({ - "source": account_keys[account_indexes[0] as usize].to_string(), - "destination": account_keys[account_indexes[1] as usize].to_string(), - "mint": account_keys[account_indexes[2] as usize].to_string(), + "account": account_keys[account_indexes[0] as usize].to_string(), + "mint": account_keys[account_indexes[1] as usize].to_string(), "amount": amount, "decimals": deposit_data.decimals, @@ -184,7 +183,7 @@ pub(in crate::parse_token) fn parse_confidential_transfer_instruction( let map = value.as_object_mut().unwrap(); parse_signers( map, - 3, + 2, account_keys, account_indexes, "owner", @@ -196,16 +195,15 @@ pub(in crate::parse_token) fn parse_confidential_transfer_instruction( }) } ConfidentialTransferInstruction::Withdraw => { - check_num_token_accounts(account_indexes, 5)?; + check_num_token_accounts(account_indexes, 4)?; let withdrawal_data: WithdrawInstructionData = *decode_instruction_data(instruction_data).map_err(|_| { ParseInstructionError::InstructionNotParsable(ParsableProgram::SplToken) })?; let amount: u64 = withdrawal_data.amount.into(); let mut value = json!({ - "source": account_keys[account_indexes[0] as usize].to_string(), - "destination": account_keys[account_indexes[1] as usize].to_string(), - "mint": account_keys[account_indexes[2] as usize].to_string(), + "account": account_keys[account_indexes[0] as usize].to_string(), + "mint": account_keys[account_indexes[1] as usize].to_string(), "amount": amount, "decimals": withdrawal_data.decimals, "newDecryptableAvailableBalance": format!("{}", withdrawal_data.new_decryptable_available_balance), @@ -213,7 +211,7 @@ pub(in crate::parse_token) fn parse_confidential_transfer_instruction( "rangeProofInstructionOffset": withdrawal_data.range_proof_instruction_offset, }); - let mut offset = 3; + let mut offset = 2; let map = value.as_object_mut().unwrap(); if offset < account_indexes.len() - 1 && (withdrawal_data.equality_proof_instruction_offset != 0 @@ -716,6 +714,85 @@ mod test { } } + #[test] + fn test_deposit() { + let token_account = Pubkey::new_unique(); + let mint = Pubkey::new_unique(); + let owner = Pubkey::new_unique(); + let instruction = deposit( + &spl_token_2022_interface::id(), + &token_account, + &mint, + 42, + 9, + &owner, + &[], + ) + .unwrap(); + let message = Message::new(&[instruction], None); + let compiled_instruction = &message.instructions[0]; + assert_eq!( + parse_token( + compiled_instruction, + &AccountKeys::new(&message.account_keys, None) + ) + .unwrap(), + ParsedInstructionEnum { + instruction_type: "depositConfidentialTransfer".to_string(), + info: json!({ + "account": token_account.to_string(), + "mint": mint.to_string(), + "amount": 42, + "decimals": 9, + "owner": owner.to_string(), + }), + } + ); + } + + #[test] + fn test_withdraw_account_mapping() { + let token_account = Pubkey::new_unique(); + let mint = Pubkey::new_unique(); + let owner = Pubkey::new_unique(); + let equality_context = Pubkey::new_unique(); + let range_context = Pubkey::new_unique(); + let instruction = inner_withdraw( + &spl_token_2022_interface::id(), + &token_account, + &mint, + 1, + 2, + &PodAeCiphertext::default(), + &owner, + &[], + ProofLocation::ContextStateAccount(&equality_context), + ProofLocation::ContextStateAccount(&range_context), + ) + .unwrap(); + let message = Message::new(&[instruction], None); + let compiled_instruction = &message.instructions[0]; + let parsed = parse_token( + compiled_instruction, + &AccountKeys::new(&message.account_keys, None), + ) + .unwrap(); + assert_eq!(parsed.instruction_type, "withdrawConfidentialTransfer"); + assert_eq!(parsed.info["account"], json!(token_account.to_string())); + assert_eq!(parsed.info["mint"], json!(mint.to_string())); + assert_eq!(parsed.info["owner"], json!(owner.to_string())); + assert_eq!( + parsed.info["equalityProofContextStateAccount"], + json!(equality_context.to_string()) + ); + assert_eq!( + parsed.info["rangeProofContextStateAccount"], + json!(range_context.to_string()) + ); + assert!(parsed.info.get("destination").is_none()); + assert_ne!(parsed.info["mint"], parsed.info["owner"]); + } + #[test] fn test_withdraw() { for (equality_proof_location, range_proof_location) in [ From 777586b6476898c835362491406288096f50f89f Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Thu, 11 Jun 2026 23:37:10 -0400 Subject: [PATCH 305/576] startup_replay: clear all outdated tower bft banks on ag migration (#13139) --- ledger/src/blockstore_processor.rs | 140 ++++++++++++++++++++++++----- 1 file changed, 120 insertions(+), 20 deletions(-) diff --git a/ledger/src/blockstore_processor.rs b/ledger/src/blockstore_processor.rs index 8bbca503cbd..7eabdc77a15 100644 --- a/ledger/src/blockstore_processor.rs +++ b/ledger/src/blockstore_processor.rs @@ -19,7 +19,7 @@ use { solana_accounts_db::{ accounts_db::AccountsDbConfig, accounts_update_notifier_interface::AccountsUpdateNotifier, }, - solana_clock::Slot, + solana_clock::{BankId, Slot}, solana_cost_model::{cost_model::CostModel, transaction_cost::TransactionCost}, solana_entry::{ block_component::BlockComponent, @@ -2215,9 +2215,24 @@ fn process_bank_0( Ok(()) } -/// Clean up a failed slot and restart processing from the given genesis slot +fn cleanup_outdated_tower_bft_startup_banks( + root_bank: &Bank, + blockstore: &Blockstore, + slots_to_cleanup: &[(Slot, BankId)], +) { + root_bank.remove_unrooted_slots(slots_to_cleanup); + + for &(slot, _) in slots_to_cleanup { + root_bank.clear_slot_signatures(slot); + root_bank.prune_program_cache_by_deployment_slot(slot); + reset_dead_if_primary_access(blockstore, slot); + } +} + +/// Clean up failed startup slots and restart processing from the given genesis slot /// -/// `first_alpenglow_bank` is removed from runtime caches, and its dead status is reset +/// `first_alpenglow_bank` and any current `pending_slots` banks are removed from runtime caches, +/// and their dead statuses are reset. /// `pending_slots` is the current child blocks left to be processed. We clear and update /// this with the children of `genesis_slot` instead. fn cleanup_and_populate_pending_from_alpenglow_genesis( @@ -2230,24 +2245,18 @@ fn cleanup_and_populate_pending_from_alpenglow_genesis( opts: &ProcessOptions, migration_status: &MigrationStatus, ) -> result::Result<(), BlockstoreProcessorError> { - // `first_alpenglow_bank` was processed as a TowerBFT bank. Reset it. - let first_alpenglow_slot = first_alpenglow_bank.slot(); - let root_bank = bank_forks.read().unwrap().root_bank(); - root_bank - .remove_unrooted_slots(&[(first_alpenglow_bank.slot(), first_alpenglow_bank.bank_id())]); - root_bank.clear_slot_signatures(first_alpenglow_bank.slot()); - root_bank.prune_program_cache_by_deployment_slot(first_alpenglow_bank.slot()); - - if blockstore.is_dead(first_alpenglow_slot) { - if blockstore.is_primary_access() { - blockstore.remove_dead_slot(first_alpenglow_slot).unwrap(); - } else { - info!( - "First alpenglow slot {first_alpenglow_slot} won't be cleared from dead due to \ - being read-only blockstore access" + // The frontier is now out of date, as all banks were created as TowerBFT banks. + // Cleanup all the banks in the frontier and recreate them as Alpenglow banks. + let slots_to_cleanup = + std::iter::once((first_alpenglow_bank.slot(), first_alpenglow_bank.bank_id())) + .chain( + pending_slots + .iter() + .map(|(_, bank, _)| (bank.slot(), bank.bank_id())), ) - } - } + .collect::>(); + let root_bank = bank_forks.read().unwrap().root_bank(); + cleanup_outdated_tower_bft_startup_banks(&root_bank, blockstore, &slots_to_cleanup); let genesis_slot_meta = blockstore .meta(genesis_slot) @@ -2775,6 +2784,17 @@ fn mark_dead_if_primary_access(blockstore: &Blockstore, slot: Slot) { } } +fn reset_dead_if_primary_access(blockstore: &Blockstore, slot: Slot) { + if !blockstore.is_dead(slot) { + return; + } + if blockstore.is_primary_access() { + blockstore.remove_dead_slot(slot).unwrap(); + } else { + info!("slot {slot} won't be cleared from dead due to being read-only blockstore access"); + } +} + // Processes and replays the contents of a single slot, returns Error // if failed to play the slot. // @@ -6468,6 +6488,86 @@ pub mod tests { )); } + #[test] + fn test_cleanup_alpenglow_genesis_cleans_pending_slots() { + let ledger_path = get_tmp_ledger_path_auto_delete!(); + let blockstore = Blockstore::open(ledger_path.path()).unwrap(); + + let GenesisConfigInfo { genesis_config, .. } = create_genesis_config(10_000); + let bank_forks = BankForks::new_rw_arc(Bank::new_for_tests(&genesis_config)); + let bank0 = bank_forks.read().unwrap().get(0).unwrap(); + let leader_schedule_cache = LeaderScheduleCache::new_from_bank(&bank0); + + let mut genesis_meta = SlotMeta::new(0, None); + genesis_meta.next_slots = vec![1, 2]; + blockstore.put_meta(0, &genesis_meta).unwrap(); + + for slot in [1, 2] { + let mut meta = SlotMeta::new(slot, Some(0)); + meta.consumed = 1; + meta.received = 1; + meta.last_index = Some(0); + blockstore.put_meta(slot, &meta).unwrap(); + blockstore.set_dead_slot(slot).unwrap(); + } + + let owner = Pubkey::new_unique(); + let first_alpenglow_key = Pubkey::new_unique(); + let pending_key = Pubkey::new_unique(); + + let first_alpenglow_bank = Arc::new(Bank::new_from_parent( + bank0.clone(), + SlotLeader::default(), + 1, + )); + first_alpenglow_bank + .store_account(&first_alpenglow_key, &AccountSharedData::new(1, 0, &owner)); + assert!( + first_alpenglow_bank + .get_account(&first_alpenglow_key) + .is_some() + ); + let first_alpenglow_bank = + BankWithScheduler::new_without_scheduler(first_alpenglow_bank.clone()); + + let pending_bank = Bank::new_from_parent(bank0.clone(), SlotLeader::default(), 2); + pending_bank.store_account(&pending_key, &AccountSharedData::new(1, 0, &owner)); + assert!(pending_bank.get_account(&pending_key).is_some()); + + let pending_meta = blockstore.meta(2).unwrap().unwrap(); + let mut pending_slots = vec![(pending_meta, pending_bank, bank0.last_blockhash())]; + + cleanup_and_populate_pending_from_alpenglow_genesis( + &first_alpenglow_bank, + 0, + &bank_forks, + &blockstore, + &leader_schedule_cache, + &mut pending_slots, + &ProcessOptions::default(), + &MigrationStatus::post_migration_status(), + ) + .unwrap(); + + assert!(!blockstore.is_dead(1)); + assert!(!blockstore.is_dead(2)); + assert!( + first_alpenglow_bank + .get_account(&first_alpenglow_key) + .is_none() + ); + + let queued_slots: BTreeSet<_> = pending_slots + .iter() + .map(|(_, bank, _)| bank.slot()) + .collect(); + assert_eq!(queued_slots, BTreeSet::from([1, 2])); + for (_, bank, _) in &pending_slots { + assert!(bank.get_account(&first_alpenglow_key).is_none()); + assert!(bank.get_account(&pending_key).is_none()); + } + } + #[test] fn test_startup_parent_id_check() { let ledger_path = get_tmp_ledger_path_auto_delete!(); From 5bf97f75b4e56d1983fd6169e4ce1c3d5dc451ef Mon Sep 17 00:00:00 2001 From: Brooks Date: Fri, 12 Jun 2026 01:03:39 -0400 Subject: [PATCH 306/576] Updates accounts lt hash in a thread pool (#12474) --- Cargo.lock | 16 +- Cargo.toml | 2 + dev-bins/Cargo.lock | 11 + programs/sbf/Cargo.lock | 16 +- runtime/Cargo.toml | 2 + runtime/src/bank.rs | 61 +- runtime/src/bank/accounts_lt_hash.rs | 864 +++++++++++---------------- runtime/src/bank/metrics.rs | 14 - runtime/src/bank/tests.rs | 1 + 9 files changed, 417 insertions(+), 570 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d3b4b987978..981fe280c56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2125,14 +2125,20 @@ dependencies = [ ] [[package]] -name = "crossbeam-utils" -version = "0.8.18" +name = "crossbeam-queue" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3a430a770ebd84726f584a90ee7f020d28db52c6d02138900f22341f866d39c" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" dependencies = [ - "cfg-if 1.0.4", + "crossbeam-utils", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "crunchy" version = "0.2.2" @@ -9949,6 +9955,8 @@ dependencies = [ "bytemuck", "criterion", "crossbeam-channel", + "crossbeam-queue", + "crossbeam-utils", "dashmap", "imbl", "itertools 0.14.0", diff --git a/Cargo.toml b/Cargo.toml index 20446810142..5c9578d6fd0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -226,6 +226,8 @@ const_format = "0.2.36" core_affinity = "0.8.3" criterion = "0.8.2" crossbeam-channel = "0.5.15" +crossbeam-queue = "0.3.12" +crossbeam-utils = "0.8.21" csv = "1.4.0" ctrlc = "3.5.2" curve25519-dalek = { version = "4.1.3", features = ["digest", "rand_core"] } diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 1ce083cef65..ee0a81a94e9 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -1774,6 +1774,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -8346,6 +8355,8 @@ dependencies = [ "bitvec", "bytemuck", "crossbeam-channel", + "crossbeam-queue", + "crossbeam-utils", "dashmap", "imbl", "itertools 0.14.0", diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index d3273019379..5a28523886f 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -1709,14 +1709,20 @@ dependencies = [ ] [[package]] -name = "crossbeam-utils" -version = "0.8.18" +name = "crossbeam-queue" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3a430a770ebd84726f584a90ee7f020d28db52c6d02138900f22341f866d39c" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" dependencies = [ - "cfg-if 1.0.4", + "crossbeam-utils", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "crunchy" version = "0.2.2" @@ -8166,6 +8172,8 @@ dependencies = [ "bitvec", "bytemuck", "crossbeam-channel", + "crossbeam-queue", + "crossbeam-utils", "dashmap", "imbl", "itertools 0.14.0", diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index e3b87dd60a5..245ee54a1b1 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -68,6 +68,8 @@ bincode = { workspace = true } bitvec = { workspace = true } bytemuck = { workspace = true } crossbeam-channel = { workspace = true } +crossbeam-queue = { workspace = true } +crossbeam-utils = { workspace = true } dashmap = { workspace = true, features = ["rayon", "raw-api", "serde"] } imbl = { workspace = true, features = ["rayon", "serde"] } itertools = { workspace = true } diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 382b0b09cd7..28fdedeabd6 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -73,7 +73,7 @@ use { status_cache::{SlotDelta, StatusCache}, transaction_batch::{OwnedOrBorrowed, TransactionBatch}, }, - accounts_lt_hash::{CacheValue as AccountsLtHashCacheValue, Stats as AccountsLtHashStats}, + accounts_lt_hash::AccountsLtHashAsyncProgress, agave_bls_cert_verify::cert_verify::{self, Error as CertVerifyError}, agave_feature_set::{self as feature_set, FeatureSet}, agave_precompiles::{get_precompile, get_precompiles, is_precompile}, @@ -81,7 +81,6 @@ use { agave_snapshots::snapshot_hash::SnapshotHash, agave_votor_messages::{certificate::Certificate, migration::GENESIS_CERTIFICATE_ACCOUNT}, ahash::AHashSet, - dashmap::DashMap, log::*, partitioned_epoch_rewards::PartitionedRewardsCalculation, rayon::ThreadPool, @@ -649,8 +648,7 @@ impl PartialEq for Bank { compute_budget: _, transaction_account_lock_limit: _, fee_structure: _, - cache_for_accounts_lt_hash: _, - stats_for_accounts_lt_hash: _, + accounts_lt_hash_async_progress: _, block_id, expected_bank_hash: _, bank_hash_stats: _, @@ -985,17 +983,8 @@ pub struct Bank { /// The value is only meaningful after freezing. accounts_lt_hash: Mutex, - /// A cache of *the initial state* of accounts modified in this slot - /// - /// The accounts lt hash needs both the initial and final state of each - /// account that was modified in this slot. Cache the initial state here. - /// - /// Note: The initial state must be strictly from an ancestor, - /// and not an intermediate state within this slot. - cache_for_accounts_lt_hash: DashMap, - - /// Stats related to the accounts lt hash - stats_for_accounts_lt_hash: AccountsLtHashStats, + /// Track progress of the asynchronous accounts lt hashing for this Bank. + accounts_lt_hash_async_progress: AccountsLtHashAsyncProgress, /// The unique identifier for the corresponding block for this bank. /// None for banks that have not yet completed replay or for leader banks as we cannot populate block_id @@ -1225,8 +1214,7 @@ impl Bank { #[cfg(feature = "dev-context-only-utils")] hash_overrides: Arc::new(Mutex::new(HashOverrides::default())), accounts_lt_hash: Mutex::new(AccountsLtHash(LtHash::identity())), - cache_for_accounts_lt_hash: DashMap::default(), - stats_for_accounts_lt_hash: AccountsLtHashStats::default(), + accounts_lt_hash_async_progress: AccountsLtHashAsyncProgress::new(), block_id: RwLock::new(None), expected_bank_hash: RwLock::new(None), bank_hash_stats: AtomicBankHashStats::default(), @@ -1485,8 +1473,7 @@ impl Bank { #[cfg(feature = "dev-context-only-utils")] hash_overrides: parent.hash_overrides.clone(), accounts_lt_hash: Mutex::new(parent.accounts_lt_hash.lock().unwrap().clone()), - cache_for_accounts_lt_hash: DashMap::default(), - stats_for_accounts_lt_hash: AccountsLtHashStats::default(), + accounts_lt_hash_async_progress: AccountsLtHashAsyncProgress::new(), block_id: RwLock::new(None), expected_bank_hash: RwLock::new(None), bank_hash_stats: AtomicBankHashStats::default(), @@ -1516,7 +1503,6 @@ impl Bank { slot, parent.slot(), new.block_height, - prepare_timings.num_accounts_modified_this_slot, NewBankTimings { bank_rc_creation_time_us, total_elapsed_time_us: time.as_us(), @@ -1536,8 +1522,6 @@ impl Bank { cache_preparation_time_us: prepare_timings.cache_preparation_time_us, update_sysvars_time_us: prepare_timings.update_sysvars_time_us, fill_sysvar_cache_time_us: prepare_timings.fill_sysvar_cache_time_us, - populate_cache_for_accounts_lt_hash_us: prepare_timings - .populate_cache_for_accounts_lt_hash_us, }, ); @@ -1984,34 +1968,12 @@ impl Bank { .fill_missing_sysvar_cache_entries(self) ); - let (num_accounts_modified_this_slot, populate_cache_for_accounts_lt_hash_us) = - measure_us!({ - // The cache for accounts lt hash needs to be made aware of accounts modified - // before transaction processing begins. Otherwise we may calculate the wrong - // accounts lt hash due to having the wrong initial state of the account. The - // lt hash cache's initial state must always be from an ancestor, and cannot be - // an intermediate state within this Bank's slot. If the lt hash cache has the - // wrong initial account state, we'll mix out the wrong lt hash value, and thus - // have the wrong overall accounts lt hash, and diverge. - let accounts_modified_this_slot = - self.rc.accounts.accounts_db.get_pubkeys_for_slot(slot); - let num_accounts_modified_this_slot = accounts_modified_this_slot.len(); - for pubkey in accounts_modified_this_slot { - self.cache_for_accounts_lt_hash - .entry(pubkey) - .or_insert(AccountsLtHashCacheValue::BankNew); - } - num_accounts_modified_this_slot - }); - PrepareBlockExecutionStats { update_epoch_time_us, distribute_rewards_time_us, cache_preparation_time_us, update_sysvars_time_us, fill_sysvar_cache_time_us, - num_accounts_modified_this_slot, - populate_cache_for_accounts_lt_hash_us, } } @@ -2170,8 +2132,7 @@ impl Bank { #[cfg(feature = "dev-context-only-utils")] hash_overrides: Arc::new(Mutex::new(HashOverrides::default())), accounts_lt_hash: Mutex::new(fields.accounts_lt_hash), - cache_for_accounts_lt_hash: DashMap::default(), - stats_for_accounts_lt_hash: AccountsLtHashStats::default(), + accounts_lt_hash_async_progress: AccountsLtHashAsyncProgress::new(), block_id: RwLock::new(fields.block_id), bank_hash_stats: AtomicBankHashStats::new(&fields.bank_hash_stats), epoch_rewards_calculation_cache: Arc::new(Mutex::new(HashMap::default())), @@ -3022,7 +2983,7 @@ impl Bank { self.freeze_started.store(true, Relaxed); // updating the accounts lt hash must happen *outside* of hash_internal_state() so // that rehash() can be called and *not* modify self.accounts_lt_hash. - self.update_accounts_lt_hash(); + self.finish_accounts_lt_hash_updates(); *hash = self.hash_internal_state(); self.rc.accounts.accounts_db.mark_slot_frozen(self.slot()); } @@ -4294,6 +4255,7 @@ impl Bank { let to_store = (self.slot(), accounts_to_store.as_slice()); self.update_bank_hash_stats(&to_store); + self.enqueue_accounts_lt_hash_updates(&to_store); // See https://github.com/solana-labs/solana/pull/31455 for discussion // on *not* updating the index within a threadpool. self.rc @@ -4689,6 +4651,7 @@ impl Bank { fn store_accounts_without_stakes_cache<'a>(&self, accounts: impl StorableAccounts<'a>) { assert!(!self.freeze_started()); self.update_bank_hash_stats(&accounts); + self.enqueue_accounts_lt_hash_updates(&accounts); self.rc .accounts .store_accounts_par(accounts, None, &self.ancestors); @@ -6679,8 +6642,8 @@ impl TransactionProcessingCallback for Bank { .load_with_fixed_root(&self.ancestors, pubkey) } - fn inspect_account(&self, address: &Pubkey, account_state: AccountState, is_writable: bool) { - self.inspect_account_for_accounts_lt_hash(address, &account_state, is_writable); + fn inspect_account(&self, _address: &Pubkey, _account_state: AccountState, _is_writable: bool) { + // nothing to do here } } diff --git a/runtime/src/bank/accounts_lt_hash.rs b/runtime/src/bank/accounts_lt_hash.rs index 6388b322f2f..ca46f8f7acb 100644 --- a/runtime/src/bank/accounts_lt_hash.rs +++ b/runtime/src/bank/accounts_lt_hash.rs @@ -1,388 +1,361 @@ use { super::Bank, - rayon::prelude::*, - solana_account::{AccountSharedData, accounts_equal}, - solana_accounts_db::accounts_db::AccountsDb, - solana_hash::Hash, + crossbeam_queue::ArrayQueue, + crossbeam_utils::CachePadded, + rayon::{ThreadPool, ThreadPoolBuilder}, + solana_account::{AccountSharedData, ReadableAccount}, + solana_accounts_db::{accounts_db::AccountsDb, storable_accounts::StorableAccounts}, solana_lattice_hash::lt_hash::LtHash, - solana_measure::{meas_dur, measure::Measure}, solana_pubkey::Pubkey, - solana_svm_callback::AccountState, std::{ - ops::AddAssign, - sync::atomic::{AtomicU64, Ordering}, - time::Duration, + array, hint, + mem::size_of, + sync::{ + Arc, LazyLock, Mutex, + atomic::{AtomicU64, AtomicUsize, Ordering}, + }, + time::Instant, }, }; -impl Bank { - /// Updates the accounts lt hash - /// - /// When freezing a bank, we compute and update the accounts lt hash. - /// For each account modified in this bank, we: - /// - mix out its previous state, and - /// - mix in its current state - /// - /// Since this function is non-idempotent, it should only be called once per bank. - pub fn update_accounts_lt_hash(&self) { - let delta_lt_hash = self.calculate_delta_lt_hash(); - let mut accounts_lt_hash = self.accounts_lt_hash.lock().unwrap(); - accounts_lt_hash.0.mix_in(&delta_lt_hash); - } +/// Number of threads for the async accounts hasher thread pool. +const NUM_ACCOUNTS_HASHER_THREADS: usize = 4; - /// Calculates the lt hash *of only this slot* - /// - /// This can be thought of as akin to the accounts delta hash. - /// - /// For each account modified in this bank, we: - /// - mix out its previous state, and - /// - mix in its current state - /// - /// This function is idempotent, and may be called more than once. - fn calculate_delta_lt_hash(&self) -> LtHash { - let measure_total = Measure::start(""); - let slot = self.slot(); - - // If we don't find the account in the cache, we need to go load it. - // We want the version of the account *before* it was written in this slot. - // Bank::ancestors *includes* this slot, so we need to remove it before loading. - let strictly_ancestors = { - let mut ancestors = self.ancestors.clone(); - ancestors.remove(&self.slot()); - ancestors - }; +// Maximum size, in bytes, for the seen-accounts freelist. +const MAX_BYTES_SEEN_ACCOUNTS_FREELIST: usize = 10_000_000; - if slot == 0 { - // Slot 0 is special when calculating the accounts lt hash. - // Primordial accounts (those in genesis) that are modified by transaction processing - // in slot 0 will have Alive entries in the accounts lt hash cache. - // When calculating the accounts lt hash, if an account was initially alive, we mix - // *out* its previous lt hash value. In slot 0, we haven't stored any previous lt hash - // values (since it is in the first slot), yet we'd still mix out these accounts! - // This produces the incorrect accounts lt hash. - // From the perspective of the accounts lt hash, in slot 0 we cannot have any accounts - // as previously alive. So to work around this issue, we clear the cache. - // And since `strictly_ancestors` is empty, loading the previous version of the account - // from accounts db will return `None` (aka Dead), which is the correct behavior. - assert!(strictly_ancestors.is_empty()); - self.cache_for_accounts_lt_hash.clear(); +impl Bank { + /// Enqueues the accounts lt hash updates for `accounts` to the accounts hasher thread pool. + pub fn enqueue_accounts_lt_hash_updates<'a>(&self, accounts: &impl StorableAccounts<'a>) { + if accounts.is_empty() { + return; } - // Get all the accounts stored in this slot. - // Since this bank is in the middle of being frozen, it hasn't been rooted. - // That means the accounts should all be in the write cache, and loading will be fast. - let (accounts_curr, time_loading_accounts_curr) = meas_dur!({ - self.rc + let seen_accounts_freelist = seen_accounts_freelist(); + let mut seen_accounts = seen_accounts_freelist.try_pop().unwrap_or_default(); + let async_progress = &self.accounts_lt_hash_async_progress; + let thread_pool = accounts_hasher_thread_pool(); + + // process accounts in reverse because we must only count the latest version of each account + for index in (0..accounts.len()).rev() { + let address = accounts.pubkey(index); + if !seen_accounts.insert(*address) { + // we've already enqueued a newer update for the same account; skip this one + continue; + } + let prev_account = self + .rc .accounts - .accounts_db - .get_pubkey_account_for_slot(slot) - }); - let num_accounts_total = accounts_curr.len(); - - #[derive(Debug, Default)] - struct Stats { - num_cache_misses: usize, - num_accounts_unmodified: usize, - time_loading_accounts_prev: Duration, - time_comparing_accounts: Duration, - time_computing_hashes: Duration, - time_mixing_hashes: Duration, - } - impl AddAssign for Stats { - fn add_assign(&mut self, other: Self) { - self.num_cache_misses += other.num_cache_misses; - self.num_accounts_unmodified += other.num_accounts_unmodified; - self.time_loading_accounts_prev += other.time_loading_accounts_prev; - self.time_comparing_accounts += other.time_comparing_accounts; - self.time_computing_hashes += other.time_computing_hashes; - self.time_mixing_hashes += other.time_mixing_hashes; + .load_with_fixed_root_do_not_populate_read_cache(&self.ancestors, address) + .map(|(account, _slot)| account); + let curr_account = accounts.account(index, |account| { + (account.lamports() != 0).then(|| account.take_account()) + }); + if prev_account.is_none() && curr_account.is_none() { + // the account was ephemeral; skip it + } else { + // the account was modified; enqueue this update + async_progress.spawn( + thread_pool, + AccountsLtHashUpdate { + address: *address, + prev_account, + curr_account, + }, + ); } } - let do_calculate_delta_lt_hash = || { - // Work on chunks of 128 pubkeys, which is 4 KiB. - // And 4 KiB is likely the smallest a real page size will be. - // And a single page is likely the smallest size a disk read will actually read. - // This can be tuned larger, but likely not smaller. - const CHUNK_SIZE: usize = 128; - accounts_curr - .par_iter() - .fold_chunks( - CHUNK_SIZE, - || (LtHash::identity(), Stats::default()), - |mut accum, (pubkey, curr_account)| { - // load the initial state of the account - let (initial_state_of_account, measure_load) = meas_dur!({ - let cache_value = self - .cache_for_accounts_lt_hash - .get(pubkey) - .map(|entry| entry.value().clone()); - match cache_value { - Some(CacheValue::InspectAccount(initial_state_of_account)) => { - initial_state_of_account - } - Some(CacheValue::BankNew) | None => { - accum.1.num_cache_misses += 1; - // If the initial state of the account is not in the accounts - // lt hash cache, or is explicitly unknown, then it is likely - // this account was stored *outside* of transaction processing - // (e.g. creating a new bank). - // Do not populate the read cache, as this account likely will - // not be accessed again soon. - let account_slot = self - .rc - .accounts - .load_with_fixed_root_do_not_populate_read_cache( - &strictly_ancestors, - pubkey, - ); - match account_slot { - Some((account, _slot)) => { - InitialStateOfAccount::Alive(account) - } - None => InitialStateOfAccount::Dead, - } - } - } - }); - accum.1.time_loading_accounts_prev += measure_load; - - // mix out the previous version of the account - match initial_state_of_account { - InitialStateOfAccount::Dead => { - // nothing to do here - } - InitialStateOfAccount::Alive(prev_account) => { - let (are_accounts_equal, measure_is_equal) = - meas_dur!(accounts_equal(curr_account, &prev_account)); - accum.1.time_comparing_accounts += measure_is_equal; - if are_accounts_equal { - // this account didn't actually change, so skip it for lt hashing - accum.1.num_accounts_unmodified += 1; - return accum; - } - let (prev_lt_hash, measure_hashing) = - meas_dur!(AccountsDb::lt_hash_account(&prev_account, pubkey)); - let (_, measure_mixing) = - meas_dur!(accum.0.mix_out(&prev_lt_hash.0)); - accum.1.time_computing_hashes += measure_hashing; - accum.1.time_mixing_hashes += measure_mixing; - } - } - - // mix in the new version of the account - let (curr_lt_hash, measure_hashing) = - meas_dur!(AccountsDb::lt_hash_account(curr_account, pubkey)); - let (_, measure_mixing) = meas_dur!(accum.0.mix_in(&curr_lt_hash.0)); - accum.1.time_computing_hashes += measure_hashing; - accum.1.time_mixing_hashes += measure_mixing; - - accum - }, - ) - .reduce( - || (LtHash::identity(), Stats::default()), - |mut accum, elem| { - accum.0.mix_in(&elem.0); - accum.1 += elem.1; - accum - }, - ) + // reclaim the seen accounts hashset + seen_accounts_freelist.try_push(seen_accounts); + } + + /// Updates the accounts lt hash. + /// + /// When freezing a bank, we compute and update the accounts lt hash. + /// For each account modified in this bank, we: + /// - mix out its previous state, and + /// - mix in its current state + /// + /// This function waits for any in-flight jobs on the accounts hasher threads, + /// computes their combined delta lt hash, then mixes it into the bank. + pub fn finish_accounts_lt_hash_updates(&self) { + let timer = Instant::now(); + let num_jobs_total = { + let mut accounts_lt_hash = self.accounts_lt_hash.lock().unwrap(); + self.accounts_lt_hash_async_progress + .finish(&mut accounts_lt_hash.0) }; - let (delta_lt_hash, stats) = self - .rc - .accounts - .accounts_db - .thread_pool_foreground - .install(do_calculate_delta_lt_hash); + let finish_time = timer.elapsed(); - let total_time = measure_total.end_as_duration(); - let num_accounts_modified = - num_accounts_total.saturating_sub(stats.num_accounts_unmodified); + let seen_accounts_freelist_stats = seen_accounts_freelist().stats(); datapoint_info!( "bank-accounts_lt_hash", - ("slot", slot, i64), - ("num_accounts_total", num_accounts_total, i64), - ("num_accounts_modified", num_accounts_modified, i64), - ( - "num_accounts_unmodified", - stats.num_accounts_unmodified, - i64 - ), - ("num_cache_misses", stats.num_cache_misses, i64), - ("total_us", total_time.as_micros(), i64), + ("slot", self.slot(), i64), + ("num_jobs", num_jobs_total, i64), + ("finish_us", finish_time.as_micros(), i64), ( - "loading_accounts_curr_us", - time_loading_accounts_curr.as_micros(), + "seen_accounts_freelist_num_containers", + seen_accounts_freelist_stats.num_containers, i64 ), ( - "par_loading_accounts_prev_us", - stats.time_loading_accounts_prev.as_micros(), + "seen_accounts_freelist_capacity_elems", + seen_accounts_freelist_stats.capacity_elems, i64 ), ( - "par_comparing_accounts_us", - stats.time_comparing_accounts.as_micros(), - i64 - ), - ( - "par_computing_hashes_us", - stats.time_computing_hashes.as_micros(), - i64 - ), - ( - "par_mixing_hashes_us", - stats.time_mixing_hashes.as_micros(), - i64 - ), - ( - "num_inspect_account_hits", - self.stats_for_accounts_lt_hash - .num_inspect_account_hits - .load(Ordering::Relaxed), - i64 - ), - ( - "num_inspect_account_misses", - self.stats_for_accounts_lt_hash - .num_inspect_account_misses - .load(Ordering::Relaxed), - i64 - ), - ( - "num_inspect_account_after_frozen", - self.stats_for_accounts_lt_hash - .num_inspect_account_after_frozen - .load(Ordering::Relaxed), - i64 - ), - ( - "inspect_account_lookup_ns", - self.stats_for_accounts_lt_hash - .inspect_account_lookup_time_ns - .load(Ordering::Relaxed), - i64 - ), - ( - "inspect_account_insert_ns", - self.stats_for_accounts_lt_hash - .inspect_account_insert_time_ns - .load(Ordering::Relaxed), + "seen_accounts_freelist_capacity_bytes", + seen_accounts_freelist_stats.capacity_bytes, i64 ), ); + } +} + +/// Struct for tracking progress of the asynchronous accounts lt hashing for a Bank. +pub struct AccountsLtHashAsyncProgress { + // Note: use [Mutex>] and *not* [CachePadded>]. + // - In both ways each mutex is on its own separate cache line. + // - In both ways the size used for each element, including padding, is the same. + // - Only this way ensures that each LtHash is placed for aligned SIMD/AVX access. + // + // Here's the layout of [Mutex>; 2] + // + // │element 0 │element 1 + // │ │ + // ▼───────┬─────────┬────────────────▼───────┬─────────┬────────────────┐ + // │ Mutex │ padding │ LtHash │ Mutex │ padding │ LtHash │ + // ├───────┼─────────┼────────────────┼───────┼─────────┼────────────────┤ + // │ │ │ │ │ │ │ + // │0 │6 │128 <-- aligned │2176 │2182 │2304 │4352 + // + // + // And here's the layout of [CachePadded>; 2] + // + // │element 0 │element 1 + // │ │ + // ▼───────┬────────────────┬─────────▼───────┬────────────────┬─────────┐ + // │ Mutex │ LtHash │ padding │ Mutex │ LtHash │ padding │ + // ├───────┼────────────────┼─────────┼───────┼────────────────┼─────────┤ + // │ │ │ │ │ │ │ + // │0 │6 <-- unaligned │2054 │2176 │2182 │4230 │4352 + // + accumulators: Arc<[Mutex>; NUM_ACCOUNTS_HASHER_THREADS]>, + num_jobs_pending: Arc, + num_jobs_total: AtomicU64, +} - delta_lt_hash +impl AccountsLtHashAsyncProgress { + /// Creates a new AccountsLtHashAsyncProgress variable, which is suitable for a new Bank. + pub fn new() -> Self { + Self { + accumulators: Arc::new(array::from_fn(|_| { + Mutex::new(CachePadded::new(LtHash::identity())) + })), + num_jobs_pending: Arc::new(AtomicUsize::new(0)), + num_jobs_total: AtomicU64::new(0), + } } - /// Caches initial state of writeable accounts + /// Enqueues `update` into `thread_pool` for asynchronous processing. + fn spawn(&self, thread_pool: &'static ThreadPool, update: AccountsLtHashUpdate) { + self.num_jobs_pending.fetch_add(1, Ordering::Relaxed); + self.num_jobs_total.fetch_add(1, Ordering::Relaxed); + thread_pool.spawn({ + let accumulators = Arc::clone(&self.accumulators); + let num_jobs_pending = Arc::clone(&self.num_jobs_pending); + move || { + // SAFETY: We always call from the same/correct Rayon thread pool. + let worker_index = thread_pool.current_thread_index().unwrap(); + + // SAFETY: There are num_threads accumulators, and each + // thread's index shall always be in range 0..num_threads. + debug_assert!(worker_index < accumulators.len()); + let accumulator = unsafe { accumulators.get_unchecked(worker_index) }; + + Self::process(&mut accumulator.lock().unwrap(), update); + + // Decrementing the number of pending jobs MUST happen *after* + // accumulating the result. This ensures `finish()` cannot + // observe zero pending jobs until all workers are done. + num_jobs_pending.fetch_sub(1, Ordering::Relaxed); + } + }); + } + + /// Waits for all pending jobs to complete, then mixes the results into `lt_hash`. /// - /// If a transaction account is writeable, cache its initial account state. - /// The initial state is needed when computing the accounts lt hash for the slot, and caching - /// the initial state saves us from having to look it up on disk later. - pub fn inspect_account_for_accounts_lt_hash( - &self, - address: &Pubkey, - account_state: &AccountState, - is_writable: bool, - ) { - if !is_writable { - // if the account is not writable, then it cannot be modified; nothing to do here - return; + /// Returns the number of asynchronous jobs completed. + /// + /// Note: Since an LtHash is large, `lt_hash` is passed as an in-out parameter. + /// This it to avoid Rust compiler bug that fails to perform return value optimization. + fn finish(&self, lt_hash: &mut LtHash) -> u64 { + while self.num_jobs_pending.load(Ordering::Relaxed) > 0 { + // Spin, do not yield! This is called by Bank::freeze() and we want to be fast. + hint::spin_loop(); } - // Only insert the account the *first* time we see it. - // We want to capture the value of the account *before* any modifications during this slot. - let (is_in_cache, lookup_time) = - meas_dur!(self.cache_for_accounts_lt_hash.contains_key(address)); - if !is_in_cache { - // We need to check if the bank is frozen. In order to do that safely, we - // must hold a read lock on Bank::hash to read the frozen state. - let freeze_guard = self.freeze_lock(); - let is_frozen = *freeze_guard != Hash::default(); - if is_frozen { - // If the bank is frozen, do not add this account to the cache. - // It is possible for the leader to be executing transactions after freeze has - // started, i.e. while any deferred changes to account state is finishing up. - // This means the transaction could load an account *after* it was modified by the - // deferred changes, which would be the wrong initial state of the account. - // Inserting the wrong initial state of an account into the cache will end up - // producing the wrong accounts lt hash. - self.stats_for_accounts_lt_hash - .num_inspect_account_after_frozen - .fetch_add(1, Ordering::Relaxed); - return; - } - let (_, insert_time) = meas_dur!({ - self.cache_for_accounts_lt_hash - .entry(*address) - .or_insert_with(|| { - let initial_state_of_account = match account_state { - AccountState::Dead => InitialStateOfAccount::Dead, - AccountState::Alive(account) => { - InitialStateOfAccount::Alive((*account).clone()) - } - }; - CacheValue::InspectAccount(initial_state_of_account) - }); - }); - drop(freeze_guard); - - self.stats_for_accounts_lt_hash - .num_inspect_account_misses - .fetch_add(1, Ordering::Relaxed); - self.stats_for_accounts_lt_hash - .inspect_account_insert_time_ns - // N.B. this needs to be nanoseconds because it can be so fast - .fetch_add(insert_time.as_nanos() as u64, Ordering::Relaxed); - } else { - // The account is already in the cache, so nothing to do here other than update stats. - self.stats_for_accounts_lt_hash - .num_inspect_account_hits - .fetch_add(1, Ordering::Relaxed); + for thread_accumulator in self.accumulators.iter() { + lt_hash.mix_in(&thread_accumulator.lock().unwrap()); } + self.num_jobs_total.load(Ordering::Relaxed) + } - self.stats_for_accounts_lt_hash - .inspect_account_lookup_time_ns - // N.B. this needs to be nanoseconds because it can be so fast - .fetch_add(lookup_time.as_nanos() as u64, Ordering::Relaxed); + /// Processes `update` and mixes the result into `accum_lt_hash`. + /// + /// Note: Since an LtHash is large, `accum_lt_hash` is passed as an in-out parameter. + /// This it to avoid Rust compiler bug that fails to perform return value optimization. + fn process(accum_lt_hash: &mut LtHash, update: AccountsLtHashUpdate) { + let AccountsLtHashUpdate { + address, + prev_account, + curr_account, + } = update; + if let Some(prev_account) = prev_account { + let prev_lt_hash = AccountsDb::lt_hash_account(&prev_account, &address); + accum_lt_hash.mix_out(&prev_lt_hash.0); + } + if let Some(curr_account) = curr_account { + let curr_lt_hash = AccountsDb::lt_hash_account(&curr_account, &address); + accum_lt_hash.mix_in(&curr_lt_hash.0); + } } } -/// Stats related to accounts lt hash -#[derive(Debug, Default)] -pub struct Stats { - /// the number of times the cache already contained the account being inspected - num_inspect_account_hits: AtomicU64, - /// the number of times the cache *did not* already contain the account being inspected - num_inspect_account_misses: AtomicU64, - /// the number of times an account was inspected after the bank was frozen - num_inspect_account_after_frozen: AtomicU64, - /// time spent checking if accounts are in the cache - inspect_account_lookup_time_ns: AtomicU64, - /// time spent inserting accounts into the cache - inspect_account_insert_time_ns: AtomicU64, +/// A single accounts lt hash update to process. +#[derive(Debug)] +struct AccountsLtHashUpdate { + address: Pubkey, + prev_account: Option, + curr_account: Option, +} + +/// Get the freelist of hashsets to use for seen accounts. +fn seen_accounts_freelist() -> &'static HashSetFreelist { + // Derived empirically while observing an unstaked node on mnb. + // Should end up being the same number as replay threads. + const MAX_CONTAINERS: usize = 50; + static FREELIST: LazyLock> = LazyLock::new(|| { + HashSetFreelist::new(MAX_CONTAINERS, Some(MAX_BYTES_SEEN_ACCOUNTS_FREELIST)) + }); + &FREELIST +} + +/// Freelist of containers, to avoid repeat allocations/deallocations. +#[derive(Debug)] +struct HashSetFreelist { + list: ArrayQueue>, + + /// the maximum capacity, in elements, this freelist will hold + max_capacity: Option, + + // stats + num_containers: AtomicUsize, + total_capacity: AtomicUsize, +} + +impl HashSetFreelist { + /// Creates a new, empty, freelist. + /// + /// max_containers: + /// * The maximum number of containers this freelist can hold. + /// + /// max_bytes: + /// * The maximum number of bytes this freelist can hold. + /// * This value corresponds to the total capacity across all the containers in the freelist. + /// * If `None`, there is no maximum. + fn new(max_containers: usize, max_bytes: Option) -> Self { + let max_capacity = max_bytes.map(|max_bytes| max_bytes / size_of::()); + Self { + list: ArrayQueue::new(max_containers), + max_capacity, + num_containers: AtomicUsize::new(0), + total_capacity: AtomicUsize::new(0), + } + } + + /// Pushes `container` on to the freelist (IFF its capacity is greater than zero). + fn try_push(&self, mut container: ahash::HashSet) { + // If the capacity is zero, then the container never allocated. + // In that case, don't waste time putting it back into the freelist, + // since there's nothing of value to reuse. + // + // Else, check if pushing the container would exceed the max capacity of the freelist. + // If so, also do not put it back into the freelist. + let capacity = container.capacity(); + if capacity != 0 + && self.max_capacity.is_none_or(|max_capacity| { + // only check the `total_capacity` atomic if a max capacity is set + if let Some(new_total_capacity) = self + .total_capacity + .load(Ordering::Relaxed) + .checked_add(capacity) + { + new_total_capacity <= max_capacity + } else { + // if the total capacity would overflow, do not push + false + } + }) + { + container.clear(); + let result = self.list.push(container); + if result.is_ok() { + // pushing the container may fail if the freelist is full, + // so only update the stats once we know push succeeded + self.num_containers.fetch_add(1, Ordering::Relaxed); + self.total_capacity.fetch_add(capacity, Ordering::Relaxed); + } + } + } + + /// Pops a container off the freelist and returns it. + /// + /// The returned container will always be empty. + fn try_pop(&self) -> Option> { + let container = self.list.pop()?; + assert!(container.is_empty()); + self.num_containers.fetch_sub(1, Ordering::Relaxed); + self.total_capacity + .fetch_sub(container.capacity(), Ordering::Relaxed); + Some(container) + } + + /// Returns a snapshot of the freelist's stats. + fn stats(&self) -> FreelistStats { + let capacity_elems = self.total_capacity.load(Ordering::Relaxed); + FreelistStats { + num_containers: self.num_containers.load(Ordering::Relaxed), + capacity_elems, + capacity_bytes: capacity_elems * size_of::(), + } + } } -/// The initial state of an account prior to being modified in this slot/transaction -#[derive(Debug, Clone, PartialEq)] -pub enum InitialStateOfAccount { - /// The account was initially dead - Dead, - /// The account was initially alive - Alive(AccountSharedData), +/// A snapshot of a freelist's stats. +#[derive(Debug, Eq, PartialEq)] +struct FreelistStats { + /// the number of containers held by the freelist + num_containers: usize, + /// the capacity, in elements, across all the containers in the freelist + capacity_elems: usize, + /// the capacity, in bytes, across all the containers in the freelist + capacity_bytes: usize, } -/// The value type for the accounts lt hash cache -#[derive(Debug, Clone, PartialEq)] -pub enum CacheValue { - /// The value was inserted by `inspect_account()`. - /// This means we will have the initial state of the account. - InspectAccount(InitialStateOfAccount), - /// The value was inserted by `Bank::new()`. - /// This means we will *not* have the initial state of the account. - BankNew, +/// Returns the thread pool for asynchronous accounts hashing. +/// +/// Note, the thread pool will be created on first call. +fn accounts_hasher_thread_pool() -> &'static ThreadPool { + static THREAD_POOL: LazyLock = LazyLock::new(|| { + ThreadPoolBuilder::new() + .num_threads(NUM_ACCOUNTS_HASHER_THREADS) + .thread_name(|i| format!("solAcctsHashr{i:02}")) + .build() + .expect("new accounts hasher rayon threadpool") + }); + &THREAD_POOL } #[cfg(test)] @@ -395,7 +368,6 @@ mod tests { }, agave_feature_set::FeatureSet, agave_snapshots::snapshot_config::SnapshotConfig, - solana_account::{ReadableAccount as _, WritableAccount as _}, solana_accounts_db::{ accounts_db::{ACCOUNTS_DB_CONFIG_FOR_TESTING, AccountsDbConfig}, accounts_index::{ACCOUNTS_INDEX_CONFIG_FOR_TESTING, AccountsIndexConfig, IndexLimit}, @@ -403,6 +375,7 @@ mod tests { solana_cluster_type::ClusterType, solana_fee_calculator::FeeRateGovernor, solana_genesis_config::{self, GenesisConfig}, + solana_hash::Hash, solana_keypair::Keypair, solana_leader_schedule::SlotLeader, solana_native_token::LAMPORTS_PER_SOL, @@ -495,7 +468,7 @@ mod tests { bank.transfer(amount, &mint_keypair, &keypair5.pubkey()) .unwrap(); - // manually freeze the bank to trigger update_accounts_lt_hash() to run + // manually freeze the bank to trigger updating the accounts lt hash bank.freeze(); let prev_accounts_lt_hash = bank.accounts_lt_hash.lock().unwrap().clone(); @@ -550,10 +523,9 @@ mod tests { // store account 5 into this new bank, unchanged bank.store_account(&keypair5.pubkey(), prev_account5.as_ref().unwrap()); - // freeze the bank to trigger update_accounts_lt_hash() to run + // freeze the bank to trigger updating the accounts lt hash bank.freeze(); - let actual_delta_lt_hash = bank.calculate_delta_lt_hash(); let post_accounts_lt_hash = bank.accounts_lt_hash.lock().unwrap().clone(); let post_mint = bank.get_account_with_fixed_root(&mint_keypair.pubkey()); let post_account1 = bank.get_account_with_fixed_root(&keypair1.pubkey()); @@ -574,21 +546,18 @@ mod tests { .map(|address| bank.get_account_with_fixed_root(address)) .collect(); - let mut expected_delta_lt_hash = LtHash::identity(); let mut expected_accounts_lt_hash = prev_accounts_lt_hash; let mut updater = |address: &Pubkey, prev: Option, post: Option| { // if there was an alive account, mix out if let Some(prev) = prev { let prev_lt_hash = AccountsDb::lt_hash_account(&prev, address); - expected_delta_lt_hash.mix_out(&prev_lt_hash.0); expected_accounts_lt_hash.0.mix_out(&prev_lt_hash.0); } // mix in the new one let post = post.unwrap_or_default(); let post_lt_hash = AccountsDb::lt_hash_account(&post, address); - expected_delta_lt_hash.mix_in(&post_lt_hash.0); expected_accounts_lt_hash.0.mix_in(&post_lt_hash.0); }; updater(&mint_keypair.pubkey(), prev_mint, post_mint); @@ -605,15 +574,7 @@ mod tests { ); } - // now make sure the delta lt hashes match - let expected = expected_delta_lt_hash.checksum(); - let actual = actual_delta_lt_hash.checksum(); - assert_eq!( - expected, actual, - "delta_lt_hash, expected: {expected}, actual: {actual}", - ); - - // ...and the accounts lt hashes match too + // now make sure the accounts lt hashes match let expected = expected_accounts_lt_hash.0.checksum(); let actual = post_accounts_lt_hash.0.checksum(); assert_eq!( @@ -626,7 +587,7 @@ mod tests { /// /// This test does a simple transfer in slot 0 so that a primordial account is modified. /// - /// See the comments in calculate_delta_lt_hash() for more information. + /// Slot 0 is special because primordial accounts have no previous accounts lt hash entry. #[test_case(Features::None; "no features")] #[test_case(Features::All; "all features")] fn test_slot0_accounts_lt_hash(features: Features) { @@ -640,7 +601,7 @@ mod tests { bank.transfer(LAMPORTS_PER_SOL, &mint_keypair, &Pubkey::new_unique()) .unwrap(); - // manually freeze the bank to trigger update_accounts_lt_hash() to run + // manually freeze the bank to trigger updating the accounts lt hash bank.freeze(); let actual_accounts_lt_hash = bank.accounts_lt_hash.lock().unwrap().clone(); @@ -653,114 +614,6 @@ mod tests { assert_eq!(actual_accounts_lt_hash, calculated_accounts_lt_hash); } - #[test_case(Features::None; "no features")] - #[test_case(Features::All; "all features")] - fn test_inspect_account_for_accounts_lt_hash(features: Features) { - let (genesis_config, _mint_keypair) = genesis_config_with(features); - let (bank, _bank_forks) = Bank::new_with_bank_forks_for_tests(&genesis_config); - - // the cache should start off empty - assert_eq!(bank.cache_for_accounts_lt_hash.len(), 0); - - // ensure non-writable accounts are *not* added to the cache - bank.inspect_account_for_accounts_lt_hash( - &Pubkey::new_unique(), - &AccountState::Dead, - false, - ); - bank.inspect_account_for_accounts_lt_hash( - &Pubkey::new_unique(), - &AccountState::Alive(&AccountSharedData::default()), - false, - ); - assert_eq!(bank.cache_for_accounts_lt_hash.len(), 0); - - // ensure *new* accounts are added to the cache - let address = Pubkey::new_unique(); - bank.inspect_account_for_accounts_lt_hash(&address, &AccountState::Dead, true); - assert_eq!(bank.cache_for_accounts_lt_hash.len(), 1); - assert!(bank.cache_for_accounts_lt_hash.contains_key(&address)); - - // ensure *existing* accounts are added to the cache - let address = Pubkey::new_unique(); - let initial_lamports = 123; - let mut account = AccountSharedData::new(initial_lamports, 0, &Pubkey::default()); - bank.inspect_account_for_accounts_lt_hash(&address, &AccountState::Alive(&account), true); - assert_eq!(bank.cache_for_accounts_lt_hash.len(), 2); - if let CacheValue::InspectAccount(InitialStateOfAccount::Alive(cached_account)) = bank - .cache_for_accounts_lt_hash - .get(&address) - .unwrap() - .value() - { - assert_eq!(*cached_account, account); - } else { - panic!("wrong initial state for account"); - }; - - // ensure if an account is modified multiple times that we only cache the *first* one - let updated_lamports = account.lamports() + 1; - account.set_lamports(updated_lamports); - bank.inspect_account_for_accounts_lt_hash(&address, &AccountState::Alive(&account), true); - assert_eq!(bank.cache_for_accounts_lt_hash.len(), 2); - if let CacheValue::InspectAccount(InitialStateOfAccount::Alive(cached_account)) = bank - .cache_for_accounts_lt_hash - .get(&address) - .unwrap() - .value() - { - assert_eq!(cached_account.lamports(), initial_lamports); - } else { - panic!("wrong initial state for account"); - }; - - // and ensure multiple updates are handled correctly when the account is initially dead - { - let address = Pubkey::new_unique(); - bank.inspect_account_for_accounts_lt_hash(&address, &AccountState::Dead, true); - assert_eq!(bank.cache_for_accounts_lt_hash.len(), 3); - match bank - .cache_for_accounts_lt_hash - .get(&address) - .unwrap() - .value() - { - CacheValue::InspectAccount(InitialStateOfAccount::Dead) => { - // this is expected, nothing to do here - } - _ => panic!("wrong initial state for account"), - }; - - bank.inspect_account_for_accounts_lt_hash( - &address, - &AccountState::Alive(&AccountSharedData::default()), - true, - ); - assert_eq!(bank.cache_for_accounts_lt_hash.len(), 3); - match bank - .cache_for_accounts_lt_hash - .get(&address) - .unwrap() - .value() - { - CacheValue::InspectAccount(InitialStateOfAccount::Dead) => { - // this is expected, nothing to do here - } - _ => panic!("wrong initial state for account"), - }; - } - - // ensure accounts are *not* added to the cache if the bank is frozen - // N.B. this test should remain *last*, as Bank::freeze() is not meant to be undone - bank.freeze(); - let address = Pubkey::new_unique(); - let num_cache_entries_prev = bank.cache_for_accounts_lt_hash.len(); - bank.inspect_account_for_accounts_lt_hash(&address, &AccountState::Dead, true); - let num_cache_entries_curr = bank.cache_for_accounts_lt_hash.len(); - assert_eq!(num_cache_entries_curr, num_cache_entries_prev); - assert!(!bank.cache_for_accounts_lt_hash.contains_key(&address)); - } - #[test_case(Features::None; "no features")] #[test_case(Features::All; "all features")] fn test_calculate_accounts_lt_hash_at_startup_from_index(features: Features) { @@ -932,39 +785,6 @@ mod tests { assert_eq!(roundtrip_bank, *bank); } - /// Ensure that accounts written in Bank::new() are added to the accounts lt hash cache. - #[test_case(Features::None; "no features")] - #[test_case(Features::All; "all features")] - fn test_accounts_lt_hash_cache_values_from_bank_new(features: Features) { - let (genesis_config, _mint_keypair) = genesis_config_with(features); - let (mut bank, bank_forks) = Bank::new_with_bank_forks_for_tests(&genesis_config); - - let slot = bank.slot() + 1; - bank = - Bank::new_from_parent_with_bank_forks(&bank_forks, bank, SlotLeader::default(), slot); - - // These are the two accounts *currently* added to the bank during Bank::new(). - // More accounts could be added later, so if the test fails, inspect the actual cache - // accounts and update the expected cache accounts as necessary. - let expected_cache = &[ - ( - Pubkey::from_str_const("SysvarC1ock11111111111111111111111111111111"), - CacheValue::BankNew, - ), - ( - Pubkey::from_str_const("SysvarS1otHashes111111111111111111111111111"), - CacheValue::BankNew, - ), - ]; - let mut actual_cache: Vec<_> = bank - .cache_for_accounts_lt_hash - .iter() - .map(|entry| (*entry.key(), entry.value().clone())) - .collect(); - actual_cache.sort_unstable_by_key(|a| a.0); - assert_eq!(expected_cache, actual_cache.as_slice()); - } - /// Ensure that the snapshot hash is correct #[test_case(Features::None; "no features")] #[test_case(Features::All; "all features")] @@ -1024,4 +844,50 @@ mod tests { assert_eq!(roundtrip_bank, *bank); } + + /// Ensure freelist respects max size. + #[test] + fn test_freelist_max_capacity() { + use ahash::HashSetExt as _; + type Container = ahash::HashSet; + + // This test uses a hashbrown container, which has some special power-of-two sizing plus + // a buffer. So create the container first, and use that to derive the max capacity. + let container = Container::with_capacity(77); + + let max_capacity = container.capacity(); + let max_bytes = max_capacity * size_of::(); + let mut freelist = HashSetFreelist::new(10, Some(max_bytes)); + + // pushing a container that is too big will not actually push + freelist.try_push(Container::with_capacity(max_capacity + 1)); + let stats0 = freelist.stats(); + assert_eq!(stats0.num_containers, 0); + assert_eq!(stats0.capacity_elems, 0); + assert_eq!(stats0.capacity_bytes, 0); + + // pushing a container that is not too big will actually push + freelist.try_push(container); + let stats1 = freelist.stats(); + assert_eq!(stats1.num_containers, 1); + assert_eq!(stats1.capacity_elems, max_capacity); + assert_eq!(stats1.capacity_bytes, max_bytes); + + // pushing a container that would exceed capacity will not push + freelist.try_push(Container::with_capacity(1)); + assert_eq!(freelist.stats(), stats1); + + // ...but, if we remove the limit, push should work again + freelist.max_capacity = None; + let container = Container::with_capacity(1); + let container_capacity = container.capacity(); + freelist.try_push(container); + let stats2 = freelist.stats(); + assert_eq!(stats2.num_containers, 2); + assert_eq!(stats2.capacity_elems, max_capacity + container_capacity); + assert_eq!( + stats2.capacity_bytes, + max_bytes + container_capacity * size_of::(), + ); + } } diff --git a/runtime/src/bank/metrics.rs b/runtime/src/bank/metrics.rs index 712253282a9..a1ba882e253 100644 --- a/runtime/src/bank/metrics.rs +++ b/runtime/src/bank/metrics.rs @@ -44,7 +44,6 @@ pub(crate) struct NewBankTimings { pub(crate) cache_preparation_time_us: u64, pub(crate) update_sysvars_time_us: u64, pub(crate) fill_sysvar_cache_time_us: u64, - pub(crate) populate_cache_for_accounts_lt_hash_us: u64, } pub(crate) struct PrepareBlockExecutionStats { @@ -53,8 +52,6 @@ pub(crate) struct PrepareBlockExecutionStats { pub(crate) cache_preparation_time_us: u64, pub(crate) update_sysvars_time_us: u64, pub(crate) fill_sysvar_cache_time_us: u64, - pub(crate) num_accounts_modified_this_slot: usize, - pub(crate) populate_cache_for_accounts_lt_hash_us: u64, } pub(crate) fn report_new_epoch_metrics( @@ -122,7 +119,6 @@ pub(crate) fn report_new_bank_metrics( slot: Slot, parent_slot: Slot, block_height: u64, - num_accounts_modified_this_slot: usize, timings: NewBankTimings, ) { datapoint_info!( @@ -172,16 +168,6 @@ pub(crate) fn report_new_bank_metrics( timings.fill_sysvar_cache_time_us, i64 ), - ( - "num_accounts_modified_this_slot", - num_accounts_modified_this_slot, - i64 - ), - ( - "populate_cache_for_accounts_lt_hash_us", - timings.populate_cache_for_accounts_lt_hash_us, - i64 - ), ); } diff --git a/runtime/src/bank/tests.rs b/runtime/src/bank/tests.rs index be429d32634..84d577f4536 100644 --- a/runtime/src/bank/tests.rs +++ b/runtime/src/bank/tests.rs @@ -34,6 +34,7 @@ use { ahash::AHashMap, assert_matches::assert_matches, crossbeam_channel::{TrySendError, bounded}, + dashmap::DashMap, itertools::Itertools, rand::Rng, rayon::{ThreadPool, ThreadPoolBuilder, iter::IntoParallelIterator}, From 5fb575b301dca7c15f5542805eee06aa5b23b58f Mon Sep 17 00:00:00 2001 From: Andrew Fitzgerald Date: Fri, 12 Jun 2026 13:27:26 +0800 Subject: [PATCH 307/576] fix: get_fee_for_message for txv1 (#13125) * RPC: failing get_fee_for_message test * fix get_fee_for_message --- rpc/src/rpc.rs | 27 ++++++++ .../runtime_transaction/sdk_transactions.rs | 33 +++------- runtime-transaction/src/transaction_meta.rs | 61 ++++++++++++++++++- runtime/src/bank.rs | 25 +++----- runtime/src/bank/tests.rs | 1 + 5 files changed, 103 insertions(+), 44 deletions(-) diff --git a/rpc/src/rpc.rs b/rpc/src/rpc.rs index a520366e99b..1d504ef492a 100644 --- a/rpc/src/rpc.rs +++ b/rpc/src/rpc.rs @@ -4610,6 +4610,7 @@ pub mod tests { solana_message::{ Message, MessageHeader, SimpleAddressLoader, VersionedMessage, v0::{self, MessageAddressTableLookup}, + v1, }, solana_nonce::{self as nonce, state::DurableNonce}, solana_program_option::COption, @@ -9448,6 +9449,32 @@ pub mod tests { let response: RpcResponse = parse_success_result(rpc.handle_request_sync(request)); assert_eq!(response.value, TEST_SIGNATURE_FEE); } + + { + const PRIORITY_FEE: u64 = 42; + let v1_msg = VersionedMessage::V1(v1::Message::new( + MessageHeader { + num_required_signatures: 1, + ..MessageHeader::default() + }, + v1::TransactionConfig { + priority_fee: Some(PRIORITY_FEE), + ..v1::TransactionConfig::empty() + }, + recent_blockhash, + vec![Pubkey::new_unique()], + vec![], + )); + + let request = create_test_request( + "getFeeForMessage", + Some(json!([ + BASE64_STANDARD.encode(wincode::serialize(&v1_msg).unwrap()) + ])), + ); + let response: RpcResponse = parse_success_result(rpc.handle_request_sync(request)); + assert_eq!(response.value, TEST_SIGNATURE_FEE + PRIORITY_FEE); + } } #[test] diff --git a/runtime-transaction/src/runtime_transaction/sdk_transactions.rs b/runtime-transaction/src/runtime_transaction/sdk_transactions.rs index 611d6986a55..3288fd25f80 100644 --- a/runtime-transaction/src/runtime_transaction/sdk_transactions.rs +++ b/runtime-transaction/src/runtime_transaction/sdk_transactions.rs @@ -1,15 +1,13 @@ use { - super::{ComputeBudgetInstructionDetails, RuntimeTransaction}, + super::RuntimeTransaction, crate::{ instruction_meta::InstructionMeta, transaction_meta::{ - CachedTransactionMeta, TransactionConfiguration, TransactionMeta, - VersionedTransactionConfiguration, + CachedTransactionMeta, TransactionMeta, VersionedTransactionConfiguration, }, transaction_with_meta::TransactionWithMeta, }, - solana_message::{AddressLoader, TransactionSignatureDetails, VersionedMessage}, - solana_program_entrypoint::HEAP_LENGTH, + solana_message::{AddressLoader, TransactionSignatureDetails}, solana_pubkey::Pubkey, solana_svm_transaction::instruction::SVMInstruction, solana_transaction::{ @@ -56,27 +54,10 @@ impl RuntimeTransaction { precompile_signature_details.num_secp256r1_instruction_signatures, ); - let versioned_transaction_config = match &sanitized_versioned_tx.get_message().message { - VersionedMessage::V1(msg) => { - VersionedTransactionConfiguration::V1(TransactionConfiguration { - priority_fee_lamports: msg.config.priority_fee.unwrap_or(0), - compute_unit_limit: msg.config.compute_unit_limit.unwrap_or(0), - loaded_accounts_data_size_limit: msg - .config - .loaded_accounts_data_size_limit - .unwrap_or(0), - updated_heap_bytes: msg.config.heap_size.unwrap_or(HEAP_LENGTH as u32), - }) - } - _ => VersionedTransactionConfiguration::LegacyAndV0( - ComputeBudgetInstructionDetails::try_from( - sanitized_versioned_tx - .get_message() - .program_instructions_iter() - .map(|(program_id, ix)| (program_id, SVMInstruction::from(ix))), - )?, - ), - }; + let versioned_transaction_config = + VersionedTransactionConfiguration::try_from_sanitized_versioned_message( + sanitized_versioned_tx.get_message(), + )?; Ok(Self { transaction: sanitized_versioned_tx, diff --git a/runtime-transaction/src/transaction_meta.rs b/runtime-transaction/src/transaction_meta.rs index 56bb3fe1b70..d638a16b8b3 100644 --- a/runtime-transaction/src/transaction_meta.rs +++ b/runtime-transaction/src/transaction_meta.rs @@ -19,7 +19,13 @@ use { }, solana_compute_budget_instruction::compute_budget_instruction_details::ComputeBudgetInstructionDetails, solana_hash::Hash, - solana_message::TransactionSignatureDetails, + solana_message::{ + SanitizedMessage, SanitizedVersionedMessage, TransactionSignatureDetails, VersionedMessage, + v1::TransactionConfig, + }, + solana_program_entrypoint::HEAP_LENGTH, + solana_pubkey::Pubkey, + solana_svm_transaction::{instruction::SVMInstruction, svm_message::SVMStaticMessage}, solana_transaction::TransactionError, }; @@ -54,6 +60,14 @@ pub struct TransactionConfiguration { } impl TransactionConfiguration { + pub fn try_from_sanitized_message( + message: &SanitizedMessage, + feature_set: &FeatureSet, + ) -> Result { + VersionedTransactionConfiguration::try_from_sanitized_message(message)? + .try_into_config(feature_set) + } + /// Compute the compute unit price in micro-lamports per compute unit. /// /// Note: that this will return an effective price according to the actual @@ -77,6 +91,51 @@ pub(crate) enum VersionedTransactionConfiguration { } impl VersionedTransactionConfiguration { + pub(crate) fn try_from_sanitized_message( + message: &SanitizedMessage, + ) -> Result { + match message { + SanitizedMessage::V1(message) => Ok(Self::from_v1_config(&message.message.config)), + SanitizedMessage::Legacy(_) | SanitizedMessage::V0(_) => { + Self::try_from_legacy_and_v0_instructions( + SVMStaticMessage::program_instructions_iter(message), + ) + } + } + } + + pub(crate) fn try_from_sanitized_versioned_message( + message: &SanitizedVersionedMessage, + ) -> Result { + match &message.message { + VersionedMessage::V1(message) => Ok(Self::from_v1_config(&message.config)), + VersionedMessage::Legacy(_) | VersionedMessage::V0(_) => { + Self::try_from_legacy_and_v0_instructions( + message + .program_instructions_iter() + .map(|(program_id, ix)| (program_id, SVMInstruction::from(ix))), + ) + } + } + } + + fn from_v1_config(config: &TransactionConfig) -> Self { + Self::V1(TransactionConfiguration { + priority_fee_lamports: config.priority_fee.unwrap_or(0), + compute_unit_limit: config.compute_unit_limit.unwrap_or(0), + loaded_accounts_data_size_limit: config.loaded_accounts_data_size_limit.unwrap_or(0), + updated_heap_bytes: config.heap_size.unwrap_or(HEAP_LENGTH as u32), + }) + } + + fn try_from_legacy_and_v0_instructions<'a>( + instructions: impl Iterator)> + Clone, + ) -> Result { + Ok(Self::LegacyAndV0( + ComputeBudgetInstructionDetails::try_from(instructions)?, + )) + } + pub(crate) fn try_into_config( &self, feature_set: &FeatureSet, diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 28fdedeabd6..ce16ab11356 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -110,7 +110,6 @@ use { }, solana_cluster_type::ClusterType, solana_compute_budget::compute_budget::ComputeBudget, - solana_compute_budget_instruction::instructions_processor::process_compute_budget_instructions, solana_cost_model::cost_tracker::CostTracker, solana_epoch_info::EpochInfo, solana_epoch_schedule::EpochSchedule, @@ -138,7 +137,8 @@ use { solana_pubkey::Pubkey, solana_rent::Rent, solana_runtime_transaction::{ - runtime_transaction::RuntimeTransaction, transaction_with_meta::TransactionWithMeta, + runtime_transaction::RuntimeTransaction, transaction_meta::TransactionConfiguration, + transaction_with_meta::TransactionWithMeta, }, solana_sdk_ids::{ bpf_loader_upgradeable, incinerator, native_loader, system_program, sysvar as sysvar_id, @@ -3254,25 +3254,16 @@ impl Bank { self.load_message_nonce_data(message) .map(|(_nonce_address, nonce_data)| nonce_data.get_lamports_per_signature()) })?; - Some(self.get_fee_for_message_with_lamports_per_signature(message)) - } - pub fn get_fee_for_message_with_lamports_per_signature( - &self, - message: &impl SVMMessage, - ) -> u64 { - let prioritization_fee = process_compute_budget_instructions( - message.program_instructions_iter(), - &self.feature_set, - ) - .unwrap_or_default() - .get_prioritization_fee(); - solana_fee::calculate_fee( + let transaction_configuration = + TransactionConfiguration::try_from_sanitized_message(message, &self.feature_set) + .ok()?; + Some(solana_fee::calculate_fee( message, self.fee_structure().lamports_per_signature, - prioritization_fee, + transaction_configuration.priority_fee_lamports, FeeFeatures::from(self.feature_set.as_ref()), - ) + )) } pub fn get_blockhash_last_valid_block_height(&self, blockhash: &Hash) -> Option { diff --git a/runtime/src/bank/tests.rs b/runtime/src/bank/tests.rs index 84d577f4536..b2407d2f5ef 100644 --- a/runtime/src/bank/tests.rs +++ b/runtime/src/bank/tests.rs @@ -64,6 +64,7 @@ use { solana_compute_budget::{ compute_budget::ComputeBudget, compute_budget_limits::ComputeBudgetLimits, }, + solana_compute_budget_instruction::instructions_processor::process_compute_budget_instructions, solana_compute_budget_interface::ComputeBudgetInstruction, solana_cost_model::{ block_cost_limits::{ From d4adf6d7eb9e4bfb97aac8e523111faed6861cbc Mon Sep 17 00:00:00 2001 From: ax <142979932+ax-x2@users.noreply.github.com> Date: Fri, 12 Jun 2026 10:17:29 +0200 Subject: [PATCH 308/576] add cpu-utils (#8944) cpu-utils: add cpu-utils --- Cargo.lock | 24 ++--- Cargo.toml | 3 +- core/src/validator.rs | 2 +- cpu-utils/Cargo.toml | 15 +++ cpu-utils/src/affinity.rs | 145 ++++++++++++++++++++++++++ cpu-utils/src/lib.rs | 26 +++++ cpu-utils/tests/integration_test.rs | 77 ++++++++++++++ dev-bins/Cargo.lock | 22 ++-- poh/Cargo.toml | 2 +- poh/src/poh_service.rs | 28 +++-- programs/sbf/Cargo.lock | 24 ++--- validator/Cargo.toml | 2 +- validator/src/commands/run/args.rs | 15 +-- validator/src/commands/run/execute.rs | 33 ++---- xdp/Cargo.toml | 2 +- xdp/src/lib.rs | 45 -------- xdp/src/transmitter.rs | 49 ++++----- xdp/src/tx_loop.rs | 4 +- 18 files changed, 360 insertions(+), 158 deletions(-) create mode 100644 cpu-utils/Cargo.toml create mode 100644 cpu-utils/src/affinity.rs create mode 100644 cpu-utils/src/lib.rs create mode 100644 cpu-utils/tests/integration_test.rs diff --git a/Cargo.lock b/Cargo.lock index 981fe280c56..eb206b72983 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -80,6 +80,13 @@ dependencies = [ "wincode", ] +[[package]] +name = "agave-cpu-utils" +version = "4.2.0-alpha.0" +dependencies = [ + "libc", +] + [[package]] name = "agave-feature-set" version = "4.2.0-alpha.0" @@ -339,6 +346,7 @@ dependencies = [ name = "agave-validator" version = "4.2.0-alpha.0" dependencies = [ + "agave-cpu-utils", "agave-logger", "agave-snapshots", "agave-votor", @@ -349,7 +357,6 @@ dependencies = [ "chrono", "clap 2.33.3", "console 0.16.3", - "core_affinity", "crossbeam-channel", "fd-lock", "indicatif", @@ -531,13 +538,13 @@ dependencies = [ name = "agave-xdp" version = "4.2.0-alpha.0" dependencies = [ + "agave-cpu-utils", "agave-xdp-ebpf", "arc-swap", "arrayvec", "aya", "bytes", "caps", - "core_affinity", "crossbeam-channel", "libc", "log", @@ -2019,17 +2026,6 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" -[[package]] -name = "core_affinity" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a034b3a7b624016c6e13f5df875747cc25f884156aad2abd12b6c46797971342" -dependencies = [ - "libc", - "num_cpus", - "winapi", -] - [[package]] name = "cpufeatures" version = "0.2.17" @@ -9213,12 +9209,12 @@ dependencies = [ name = "solana-poh" version = "4.2.0-alpha.0" dependencies = [ + "agave-cpu-utils", "agave-logger", "agave-votor-messages", "arc-swap", "assert_matches", "bincode", - "core_affinity", "criterion", "crossbeam-channel", "log", diff --git a/Cargo.toml b/Cargo.toml index 5c9578d6fd0..e10b0677125 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ members = [ "connection-cache", "core", "cost-model", + "cpu-utils", "download-utils", "entry", "faucet", @@ -170,6 +171,7 @@ collapsible_if = "allow" Inflector = "0.11.4" agave-banking-stage-ingress-types = { path = "banking-stage-ingress-types", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } agave-bls-cert-verify = { path = "bls-cert-verify", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } +agave-cpu-utils = { path = "cpu-utils", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } agave-feature-set = { path = "feature-set", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } agave-fs = { path = "fs", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } agave-geyser-plugin-interface = { path = "geyser-plugin-interface", version = "=4.2.0-alpha.0" } @@ -223,7 +225,6 @@ chrono-humanize = "0.2.3" clap = { version = "2.33.1", default-features = false, features = ["suggestions"] } console = "0.16.3" const_format = "0.2.36" -core_affinity = "0.8.3" criterion = "0.8.2" crossbeam-channel = "0.5.15" crossbeam-queue = "0.3.12" diff --git a/core/src/validator.rs b/core/src/validator.rs index 69c202da9f4..16bd361c3e7 100644 --- a/core/src/validator.rs +++ b/core/src/validator.rs @@ -363,7 +363,7 @@ pub struct ValidatorConfig { pub no_os_cpu_stats_reporting: bool, pub no_os_disk_stats_reporting: bool, pub enforce_ulimit_nofile: bool, - pub poh_pinned_cpu_core: usize, + pub poh_pinned_cpu_core: Option, pub poh_hashes_per_batch: u64, pub process_ledger_before_services: bool, pub accounts_db_config: AccountsDbConfig, diff --git a/cpu-utils/Cargo.toml b/cpu-utils/Cargo.toml new file mode 100644 index 00000000000..3958efbf076 --- /dev/null +++ b/cpu-utils/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "agave-cpu-utils" +description = "Agave CPU Utils" +version = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +edition = { workspace = true } +publish = true + +[features] +agave-unstable-api = [] + +[dependencies] +libc = { workspace = true } diff --git a/cpu-utils/src/affinity.rs b/cpu-utils/src/affinity.rs new file mode 100644 index 00000000000..bac514ede83 --- /dev/null +++ b/cpu-utils/src/affinity.rs @@ -0,0 +1,145 @@ +//! Core CPU affinity operations. + +use std::{io, mem, ops::Deref}; + +const CPU_SETSIZE: usize = libc::CPU_SETSIZE as usize; + +/// Identifies a logical CPU (hardware thread) by its kernel-assigned ID. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct CpuId(usize); + +impl CpuId { + pub fn new(cpu: usize) -> io::Result { + if cpu < CPU_SETSIZE { + Ok(Self(cpu)) + } else { + Err(io::Error::from_raw_os_error(libc::EINVAL)) + } + } +} + +impl Deref for CpuId { + type Target = usize; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +/// Set CPU affinity for a thread. +/// +/// Restricts the thread to run only on the specified CPUs. +/// +/// # Arguments +/// * `thread_id` - Thread ID to set affinity for. `None` means the calling thread. +/// * `cpus` - CPU IDs to bind the thread to. Can be any iterable collection. +/// +/// # Examples +/// +/// ```no_run +/// # use agave_cpu_utils::*; +/// # fn main() -> std::io::Result<()> { +/// // Pin current thread to CPU 0 +/// set_cpu_affinity(None, [CpuId::new(0)?])?; +/// +/// // Pin current thread to multiple CPUs +/// set_cpu_affinity(None, [CpuId::new(0)?, CpuId::new(1)?, CpuId::new(2)?])?; +/// # Ok(()) +/// # } +/// ``` +/// +/// # Errors +/// +/// Returns [`io::ErrorKind::InvalidInput`] if any CPU ID is too large for `cpu_set_t`. +/// Returns [`io::Error`] if the system call fails, including offline CPUs or CPUs +/// disallowed by the task's hard cpuset/cgroup. +pub fn set_cpu_affinity( + thread_id: Option, + cpus: impl IntoIterator, +) -> io::Result<()> { + // safety: cpu_set_t is a POD type, zero-initialization is standard + let mut cpu_set: libc::cpu_set_t = unsafe { mem::zeroed() }; + + for cpu in cpus { + // safety: CpuId values are constructed only after validating cpu < CPU_SETSIZE. + unsafe { libc::CPU_SET(*cpu, &mut cpu_set) }; + } + + let tid = thread_id.unwrap_or(0); + + // safety: sched_setaffinity is safe with valid parameters + let result = + unsafe { libc::sched_setaffinity(tid, mem::size_of::(), &cpu_set) }; + + if result != 0 { + return Err(io::Error::last_os_error()); + } + + Ok(()) +} + +/// Get the CPU affinity mask for a thread. +/// +/// Returns a sorted vector of CPU IDs that the thread is allowed to run on. +/// +/// # Arguments +/// * `thread_id` - Thread ID to query. `None` means the calling thread. +/// +/// # Examples +/// +/// ```no_run +/// # use agave_cpu_utils::*; +/// # fn main() -> std::io::Result<()> { +/// let cpus = cpu_affinity(None)?; +/// println!("Thread can run on CPUs: {:?}", cpus); +/// # Ok(()) +/// # } +/// ``` +/// +/// # Errors +/// +/// Returns [`io::Error`] if the system call fails. +pub fn cpu_affinity(thread_id: Option) -> io::Result> { + // safety: cpu_set_t is a POD type, zero-initialization is standard + let mut cpu_set: libc::cpu_set_t = unsafe { mem::zeroed() }; + + let tid = thread_id.unwrap_or(0); + + // safety: sched_getaffinity is safe with valid parameters + let result = + unsafe { libc::sched_getaffinity(tid, mem::size_of::(), &mut cpu_set) }; + + if result != 0 { + return Err(io::Error::last_os_error()); + } + + let mut cpus = Vec::new(); + for cpu in 0..CPU_SETSIZE { + // safety: cpu < CPU_SETSIZE by construction + if unsafe { libc::CPU_ISSET(cpu, &cpu_set) } { + cpus.push(CpuId::new(cpu)?); + } + } + + Ok(cpus) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_cpu_id_validation() { + let result = CpuId::new(CPU_SETSIZE); + assert_eq!(result.unwrap_err().raw_os_error(), Some(libc::EINVAL)); + } + + #[test] + fn test_cpu_affinity_returns_sorted() { + let cpus = cpu_affinity(None).expect("failed to query current CPU affinity"); + assert!( + cpus.windows(2).all(|window| *window[0] <= *window[1]), + "cpu_affinity should return sorted CPU list" + ); + } +} diff --git a/cpu-utils/src/lib.rs b/cpu-utils/src/lib.rs new file mode 100644 index 00000000000..7806376f1a6 --- /dev/null +++ b/cpu-utils/src/lib.rs @@ -0,0 +1,26 @@ +#![cfg(all(feature = "agave-unstable-api", target_os = "linux"))] + +//! CPU affinity utilities for Linux systems. +//! +//! This crate provides safe Rust bindings for setting CPU affinity and querying +//! the current task affinity mask. Useful for performance-critical applications +//! that need precise control over thread placement. +//! +//! # Examples +//! +//! ```no_run +//! use agave_cpu_utils::*; +//! +//! # fn main() -> std::io::Result<()> { +//! let allowed = cpu_affinity(None)?; +//! if let Some(&cpu) = allowed.first() { +//! set_cpu_affinity(None, [cpu])?; +//! } +//! # Ok(()) +//! # } +//! ``` +//! + +mod affinity; + +pub use affinity::{CpuId, cpu_affinity, set_cpu_affinity}; diff --git a/cpu-utils/tests/integration_test.rs b/cpu-utils/tests/integration_test.rs new file mode 100644 index 00000000000..12c81cd3fcc --- /dev/null +++ b/cpu-utils/tests/integration_test.rs @@ -0,0 +1,77 @@ +#![cfg(all(feature = "agave-unstable-api", target_os = "linux"))] + +//! Integration tests for agave-cpu-utils +//! +//! These tests require a Linux system. +//! Tests that modify CPU affinity spawn dedicated threads to avoid affecting the +//! test harness. + +use { + agave_cpu_utils::{CpuId, cpu_affinity, set_cpu_affinity}, + std::{io, thread}, +}; + +fn current_affinity() -> Vec { + cpu_affinity(None).expect("failed to query current CPU affinity") +} + +fn handle_affinity_result(result: io::Result<()>, test_name: &str) { + match result { + Ok(()) => {} + Err(e) => panic!("{test_name}: unexpected error: {e:?}"), + } +} + +#[test] +fn test_set_and_get_affinity() { + let affinity = current_affinity(); + let cpu = affinity + .first() + .copied() + .expect("current CPU affinity mask should not be empty"); + + let result = thread::spawn(move || { + set_cpu_affinity(None, [cpu])?; + let affinity = cpu_affinity(None)?; + assert_eq!(affinity, vec![cpu]); + Ok::<(), io::Error>(()) + }) + .join() + .expect("Thread panicked"); + + handle_affinity_result(result, "test_set_and_get_affinity"); +} + +#[test] +fn test_affinity_with_multiple_cpus() { + let available = current_affinity(); + if available.len() < 2 { + eprintln!( + "Skipping test_affinity_with_multiple_cpus: current CPU affinity mask has fewer than \ + 2 CPUs" + ); + return; + } + let (cpu0, cpu1) = (available[0], available[1]); + + let result = thread::spawn(move || { + set_cpu_affinity(None, [cpu0, cpu1])?; + let affinity = cpu_affinity(None)?; + assert_eq!(affinity, vec![cpu0, cpu1]); + Ok::<(), io::Error>(()) + }) + .join() + .expect("Thread panicked"); + + handle_affinity_result(result, "test_affinity_with_multiple_cpus"); +} + +#[test] +fn test_invalid_cpu_index_rejected() { + assert_eq!( + CpuId::new(libc::CPU_SETSIZE as usize) + .unwrap_err() + .raw_os_error(), + Some(libc::EINVAL) + ); +} diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index ee0a81a94e9..bc229946b8b 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -76,6 +76,13 @@ dependencies = [ "wincode", ] +[[package]] +name = "agave-cpu-utils" +version = "4.2.0-alpha.0" +dependencies = [ + "libc", +] + [[package]] name = "agave-feature-set" version = "4.2.0-alpha.0" @@ -411,13 +418,13 @@ dependencies = [ name = "agave-xdp" version = "4.2.0-alpha.0" dependencies = [ + "agave-cpu-utils", "agave-xdp-ebpf", "arc-swap", "arrayvec", "aya", "bytes", "caps", - "core_affinity", "crossbeam-channel", "libc", "log", @@ -1708,17 +1715,6 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" -[[package]] -name = "core_affinity" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a034b3a7b624016c6e13f5df875747cc25f884156aad2abd12b6c46797971342" -dependencies = [ - "libc", - "num_cpus", - "winapi", -] - [[package]] name = "cpufeatures" version = "0.2.17" @@ -7807,9 +7803,9 @@ dependencies = [ name = "solana-poh" version = "4.2.0-alpha.0" dependencies = [ + "agave-cpu-utils", "agave-votor-messages", "arc-swap", - "core_affinity", "crossbeam-channel", "log", "qualifier_attr", diff --git a/poh/Cargo.toml b/poh/Cargo.toml index 48f136daae3..d85b28b428e 100644 --- a/poh/Cargo.toml +++ b/poh/Cargo.toml @@ -22,9 +22,9 @@ dev-context-only-utils = [] shuttle-test = ["dep:shuttle"] [dependencies] +agave-cpu-utils = { workspace = true } agave-votor-messages = { workspace = true } arc-swap = { workspace = true } -core_affinity = { workspace = true } crossbeam-channel = { workspace = true } log = { workspace = true } qualifier_attr = { workspace = true } diff --git a/poh/src/poh_service.rs b/poh/src/poh_service.rs index 80b9719e746..c8dbc60ef95 100644 --- a/poh/src/poh_service.rs +++ b/poh/src/poh_service.rs @@ -1,5 +1,7 @@ //! The `poh_service` module implements a service that records the passing of //! "ticks", a measure of time in the PoH stream +#[cfg(target_os = "linux")] +use agave_cpu_utils::{CpuId, set_cpu_affinity}; use { crate::{ poh_controller::{PohServiceMessage, PohServiceMessageGuard, PohServiceMessageReceiver}, @@ -37,7 +39,10 @@ const TARGET_HASH_BATCH_TIME_US: u64 = 50; pub const DEFAULT_HASHES_PER_BATCH: u64 = TARGET_HASH_BATCH_TIME_US * DEFAULT_HASHES_PER_SECOND / 1_000_000; -pub const DEFAULT_PINNED_CPU_CORE: usize = 0; +#[cfg(target_os = "linux")] +pub const DEFAULT_PINNED_CPU_CORE: Option = Some(0); +#[cfg(not(target_os = "linux"))] +pub const DEFAULT_PINNED_CPU_CORE: Option = None; const TARGET_SLOT_ADJUSTMENT_NS: u64 = 50_000_000; @@ -100,7 +105,7 @@ impl PohService { poh_config: &PohConfig, poh_exit: Arc, ticks_per_slot: u64, - pinned_cpu_core: usize, + pinned_cpu_core: Option, hashes_per_batch: u64, mut record_receiver: RecordReceiver, poh_service_receiver: PohServiceMessageReceiver, @@ -109,6 +114,8 @@ impl PohService { ) -> Self { migration_status.set_poh_service_started(); let poh_config = poh_config.clone(); + #[cfg(not(target_os = "linux"))] + let _ = pinned_cpu_core; let tick_producer = Builder::new() .name("solPohTickProd".to_string()) .spawn(move || { @@ -144,11 +151,18 @@ impl PohService { ) } } else { - // PoH service runs in a tight loop, generating hashes as fast as possible. - // Let's dedicate one of the CPU cores to this thread so that it can gain - // from cache performance. - if let Some(cores) = core_affinity::get_core_ids() { - core_affinity::set_for_current(cores[pinned_cpu_core]); + #[cfg(target_os = "linux")] + if let Some(pinned_cpu_core) = pinned_cpu_core { + // PoH service runs in a tight loop, generating hashes as fast as possible. + // Let's dedicate one of the CPU cores to this thread so that it can gain + // from cache performance. + let pinned_cpu = CpuId::new(pinned_cpu_core).unwrap(); + set_cpu_affinity(None, [pinned_cpu]).unwrap_or_else(|e| { + panic!( + "Failed to set CPU affinity for POH service to CPU \ + {pinned_cpu_core}: {e:?}. This is critical for performance." + ) + }); } Self::tick_producer( poh_recorder, diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index 5a28523886f..53b554b7c4d 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -76,6 +76,13 @@ dependencies = [ "wincode", ] +[[package]] +name = "agave-cpu-utils" +version = "4.2.0-alpha.0" +dependencies = [ + "libc", +] + [[package]] name = "agave-feature-set" version = "4.2.0-alpha.0" @@ -253,6 +260,7 @@ dependencies = [ name = "agave-validator" version = "4.2.0-alpha.0" dependencies = [ + "agave-cpu-utils", "agave-logger", "agave-snapshots", "agave-votor", @@ -261,7 +269,6 @@ dependencies = [ "chrono", "clap", "console 0.16.3", - "core_affinity", "crossbeam-channel", "fd-lock", "indicatif", @@ -399,13 +406,13 @@ dependencies = [ name = "agave-xdp" version = "4.2.0-alpha.0" dependencies = [ + "agave-cpu-utils", "agave-xdp-ebpf", "arc-swap", "arrayvec", "aya", "bytes", "caps", - "core_affinity", "crossbeam-channel", "libc", "log", @@ -1637,17 +1644,6 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" -[[package]] -name = "core_affinity" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a034b3a7b624016c6e13f5df875747cc25f884156aad2abd12b6c46797971342" -dependencies = [ - "libc", - "num_cpus", - "winapi", -] - [[package]] name = "cpufeatures" version = "0.2.17" @@ -7575,9 +7571,9 @@ dependencies = [ name = "solana-poh" version = "4.2.0-alpha.0" dependencies = [ + "agave-cpu-utils", "agave-votor-messages", "arc-swap", - "core_affinity", "crossbeam-channel", "log", "qualifier_attr", diff --git a/validator/Cargo.toml b/validator/Cargo.toml index f843577fa3d..cd7b91d4459 100644 --- a/validator/Cargo.toml +++ b/validator/Cargo.toml @@ -18,6 +18,7 @@ targets = ["x86_64-unknown-linux-gnu"] agave-unstable-api = [] [dependencies] +agave-cpu-utils = { workspace = true } agave-logger = { workspace = true } agave-snapshots = { workspace = true } agave-votor = { workspace = true } @@ -25,7 +26,6 @@ agave-xdp = { workspace = true } chrono = { workspace = true, features = ["default", "serde"] } clap = { workspace = true } console = { workspace = true } -core_affinity = { workspace = true } crossbeam-channel = { workspace = true } fd-lock = { workspace = true } indicatif = { workspace = true } diff --git a/validator/src/commands/run/args.rs b/validator/src/commands/run/args.rs index 71292bc14ff..6ac31cda870 100644 --- a/validator/src/commands/run/args.rs +++ b/validator/src/commands/run/args.rs @@ -867,18 +867,9 @@ pub fn add_args<'a>(app: App<'a, 'a>, default_args: &'a DefaultArgs) -> App<'a, .hidden(hidden_unless_forced()) .long("experimental-poh-pinned-cpu-core") .takes_value(true) - .value_name("CPU_CORE_INDEX") - .validator(|s| { - let core_index = usize::from_str(&s).map_err(|e| e.to_string())?; - let max_index = core_affinity::get_core_ids() - .map(|cids| cids.len() - 1) - .unwrap_or(0); - if core_index > max_index { - return Err(format!("core index must be in the range [0, {max_index}]")); - } - Ok(()) - }) - .help("EXPERIMENTAL: Specify which CPU core PoH is pinned to"), + .value_name("CPU_ID") + .validator(|s| usize::from_str(&s).map(|_| ()).map_err(|e| e.to_string())) + .help("Specify which CPU core PoH is pinned to"), ) .arg( Arg::with_name("poh_hashes_per_batch") diff --git a/validator/src/commands/run/execute.rs b/validator/src/commands/run/execute.rs index f6ab90d6dde..c8462da273a 100644 --- a/validator/src/commands/run/execute.rs +++ b/validator/src/commands/run/execute.rs @@ -12,7 +12,6 @@ use { snapshot_config::{SnapshotConfig, SnapshotUsage}, }, agave_votor::vote_history_storage, - agave_xdp::{set_cpu_affinity, transmitter::XdpConfig}, clap::{ArgMatches, crate_name, value_t, value_t_or_exit, values_t, values_t_or_exit}, crossbeam_channel::unbounded, log::*, @@ -29,9 +28,7 @@ use { create_and_canonicalize_directory, }, }, - solana_clap_utils::input_parsers::{ - keypair_of, keypairs_of, parse_cpu_ranges, pubkey_of, value_of, values_of, - }, + solana_clap_utils::input_parsers::{keypair_of, keypairs_of, pubkey_of, value_of, values_of}, solana_clock::{DEFAULT_SLOTS_PER_EPOCH, Slot}, solana_core::{ banking_stage::transaction_scheduler::scheduler_controller::SchedulerConfig, @@ -84,6 +81,8 @@ use { sync::{Arc, RwLock, atomic::AtomicBool}, }, }; +#[cfg(target_os = "linux")] +use {agave_xdp::transmitter::XdpConfig, solana_clap_utils::input_parsers::parse_cpu_ranges}; #[derive(Debug, PartialEq, Eq)] pub enum Operation { @@ -164,7 +163,7 @@ pub fn execute( Err(format!("invalid entrypoint address: {addr}"))?; } } - + #[cfg(target_os = "linux")] let xdp_transmit_config = if let Some(xdp_cpu_cores) = matches .value_of("xdp_cpu_cores") .or_else(|| matches.value_of("experimental_retransmit_xdp_cpu_cores")) @@ -412,21 +411,12 @@ pub fn execute( #[cfg(not(target_os = "linux"))] let xdp_transmit_setup = None; - let reserved = xdp_transmit_config - .map(|xdp| xdp.cpus.clone()) - .unwrap_or_default() - .iter() - .cloned() - .collect::>(); - if !reserved.is_empty() { - let available = core_affinity::get_core_ids() - .unwrap_or_default() - .into_iter() - .map(|core_id| core_id.id) - .collect::>(); - let available = available.difference(&reserved); - set_cpu_affinity(available.into_iter().copied()).unwrap(); - } + #[cfg(target_os = "linux")] + let poh_pinned_cpu_core = + value_of(matches, "poh_pinned_cpu_core").or(poh_service::DEFAULT_PINNED_CPU_CORE); + + #[cfg(not(target_os = "linux"))] + let poh_pinned_cpu_core = None; solana_core::validator::report_target_features(); @@ -804,8 +794,7 @@ pub fn execute( // The validator needs to open many files, check that the process has // permission to do so in order to fail quickly and give a direct error enforce_ulimit_nofile: true, - poh_pinned_cpu_core: value_of(matches, "poh_pinned_cpu_core") - .unwrap_or(poh_service::DEFAULT_PINNED_CPU_CORE), + poh_pinned_cpu_core, poh_hashes_per_batch: value_of(matches, "poh_hashes_per_batch") .unwrap_or(poh_service::DEFAULT_HASHES_PER_BATCH), process_ledger_before_services: matches.is_present("process_ledger_before_services"), diff --git a/xdp/Cargo.toml b/xdp/Cargo.toml index d4437e904e5..cd916284dbc 100644 --- a/xdp/Cargo.toml +++ b/xdp/Cargo.toml @@ -12,6 +12,7 @@ publish = true agave-unstable-api = [] [dependencies] +agave-cpu-utils = { workspace = true } arc-swap = { workspace = true } bytes = { workspace = true } crossbeam-channel = { workspace = true } @@ -24,7 +25,6 @@ agave-xdp-ebpf = { workspace = true } arrayvec = { workspace = true } aya = { workspace = true } caps = { workspace = true } -core_affinity = { workspace = true } [lints] workspace = true diff --git a/xdp/src/lib.rs b/xdp/src/lib.rs index 6f149efa5af..daf78473219 100644 --- a/xdp/src/lib.rs +++ b/xdp/src/lib.rs @@ -31,51 +31,6 @@ pub mod transmitter; pub use program::load_xdp_program; use std::{io, net::Ipv4Addr}; -#[cfg(target_os = "linux")] -pub fn set_cpu_affinity(cpus: impl IntoIterator) -> Result<(), io::Error> { - unsafe { - let mut cpu_set = std::mem::zeroed(); - - for cpu in cpus { - libc::CPU_SET(cpu, &mut cpu_set); - } - - let result = libc::sched_setaffinity( - 0, - std::mem::size_of::(), - &cpu_set as *const libc::cpu_set_t, - ); - if result != 0 { - Err(io::Error::last_os_error()) - } else { - Ok(()) - } - } -} - -#[cfg(not(target_os = "linux"))] -pub fn set_cpu_affinity(_cpus: impl IntoIterator) -> Result<(), io::Error> { - unimplemented!() -} - -#[cfg(target_os = "linux")] -pub fn get_cpu() -> Result { - unsafe { - let result = libc::sched_getcpu(); - if result < 0 { - assert_eq!(result, -1); - Err(io::Error::last_os_error()) - } else { - Ok(result as usize) - } - } -} - -#[cfg(not(target_os = "linux"))] -pub fn get_cpu() -> Result { - unimplemented!() -} - /// Returns the IPv4 address of the specified network interface. /// /// If the interface is part of a bonded interface, returns the master's IPv4 address. diff --git a/xdp/src/transmitter.rs b/xdp/src/transmitter.rs index e72b6ad793d..09bc99a7ba0 100644 --- a/xdp/src/transmitter.rs +++ b/xdp/src/transmitter.rs @@ -16,10 +16,10 @@ use { load_xdp_program, route::{RouteTable, Router, RoutingTables}, route_monitor::RouteMonitor, - set_cpu_affinity, tx_loop::{TxLoop, TxLoopBuilder, TxLoopConfigBuilder, TxPacket}, umem::{OwnedUmem, PageAlignedMemory}, }, + agave_cpu_utils::{CpuId, cpu_affinity, set_cpu_affinity}, arc_swap::ArcSwap, arrayvec::ArrayVec, aya::Ebpf, @@ -251,32 +251,33 @@ impl TransmitterBuilder { tx_loop_config_builder.zero_copy(zero_copy); let tx_loop_config = tx_loop_config_builder.build_with_src_device(&dev); - let reserved_cores = cpus.iter().cloned().collect::>(); - let available_cores = core_affinity::get_core_ids() - .expect("linux provide affine cores") + let reserved_cores = cpus + .iter() + .copied() + .map(CpuId::new) + .collect::>>()?; + let unreserved_cores = cpu_affinity(None)? .into_iter() - .map(|core_affinity::CoreId { id }| id) - .collect::>(); - let unreserved_cores = available_cores - .difference(&reserved_cores) - .cloned() + .filter(|core| !reserved_cores.contains(core)) .collect::>(); - let tx_loop_builders = cpus - .into_iter() - .zip(std::iter::repeat_with(|| tx_loop_config.clone())) - .enumerate() - .map(|(i, (cpu_id, config))| { - // since we aren't necessarily allocating from the thread that we intend to run on, - // temporarily switch to the target cpu for each TxLoop to ensure that the Umem region - // is allocated to the correct numa node - set_cpu_affinity([cpu_id]).unwrap(); - let tx_loop_builder = TxLoopBuilder::new(cpu_id, QueueId(i as u64), config, &dev); - // migrate main thread back off of the last xdp reserved cpu - set_cpu_affinity(unreserved_cores.clone()).unwrap(); - tx_loop_builder - }) - .collect::>(); + if unreserved_cores.is_empty() { + return Err("all CPUs are reserved; no CPU available for the main thread".into()); + } + + let mut tx_loop_builders = Vec::with_capacity(cpus.len()); + for (i, cpu_id) in cpus.into_iter().enumerate() { + // since we aren't necessarily allocating from the thread that we intend to run on, + // temporarily switch to the target cpu for each TxLoop to ensure that the Umem region + // is allocated to the correct numa node + let cpu = CpuId::new(cpu_id)?; + set_cpu_affinity(None, [cpu])?; + let tx_loop_builder = + TxLoopBuilder::new(cpu_id, QueueId(i as u64), tx_loop_config.clone(), &dev); + // migrate main thread back off of the last xdp reserved cpu + set_cpu_affinity(None, unreserved_cores.iter().copied())?; + tx_loop_builders.push(tx_loop_builder); + } // switch to higher caps while we setup XDP. We assume that an error in // this function is irrecoverable so we don't try to drop on errors. diff --git a/xdp/src/tx_loop.rs b/xdp/src/tx_loop.rs index 00630cdd7d6..80f383628c6 100644 --- a/xdp/src/tx_loop.rs +++ b/xdp/src/tx_loop.rs @@ -14,10 +14,10 @@ use { write_ip_header_for_udp, write_udp_header, }, route::NextHop, - set_cpu_affinity, socket::{Socket, Tx, TxRing}, umem::{Frame, OwnedUmem, PageAlignedMemory, Umem}, }, + agave_cpu_utils::set_cpu_affinity, crossbeam_channel::{Receiver, Sender, TryRecvError}, libc::{_SC_PAGESIZE, sysconf}, std::{ @@ -240,7 +240,7 @@ impl TxLoop { } = self; // each queue is bound to its own CPU core - set_cpu_affinity([cpu_id]).unwrap(); + set_cpu_affinity(None, [agave_cpu_utils::CpuId::new(cpu_id).unwrap()]).unwrap(); let umem = socket.umem(); let umem_tx_capacity = umem.available(); From 73713f985c2fbab11db33a42cb6ee87f883b101a Mon Sep 17 00:00:00 2001 From: Edvard Fagerholm Date: Fri, 12 Jun 2026 11:18:49 +0300 Subject: [PATCH 309/576] gossip: Improve pull bloom filter accounting (#13143) --- bloom/src/bloom.rs | 25 ++++++----- gossip/src/cluster_info.rs | 70 ++++++++++++++++++++++++++---- gossip/src/cluster_info_metrics.rs | 6 +++ gossip/src/crds.rs | 2 +- gossip/src/crds_gossip.rs | 2 + gossip/src/crds_gossip_pull.rs | 41 +++++++++++++---- gossip/tests/crds_gossip.rs | 3 +- 7 files changed, 119 insertions(+), 30 deletions(-) diff --git a/bloom/src/bloom.rs b/bloom/src/bloom.rs index 80003035eb7..fea24bda834 100644 --- a/bloom/src/bloom.rs +++ b/bloom/src/bloom.rs @@ -5,7 +5,6 @@ use { fnv::FnvHasher, rand::{self, Rng}, serde::{Deserialize, Serialize}, - solana_sanitize::{Sanitize, SanitizeError}, solana_time_utils::AtomicInterval, std::{ cmp, fmt, @@ -56,17 +55,6 @@ impl fmt::Debug for Bloom { } } -impl Sanitize for Bloom { - fn sanitize(&self) -> Result<(), SanitizeError> { - // Avoid division by zero in self.pos(...). - if self.bits.is_empty() { - Err(SanitizeError::InvalidValue) - } else { - Ok(()) - } - } -} - impl Bloom { #[cfg(feature = "dev-context-only-utils")] pub fn num_bits_set(&self) -> u64 { @@ -120,6 +108,9 @@ impl Bloom { self.num_bits_set = 0; } pub fn add(&mut self, key: &T) { + if self.bits.is_empty() { + return; + } for k in &self.keys { let pos = self.pos(key, *k); if !self.bits.get(pos) { @@ -129,6 +120,9 @@ impl Bloom { } } pub fn contains(&self, key: &T) -> bool { + if self.keys.is_empty() || self.bits.is_empty() { + return false; + } for k in &self.keys { let pos = self.pos(key, *k); if !self.bits.get(pos) { @@ -195,6 +189,9 @@ impl ConcurrentBloom { /// Adds an item to the bloom filter and returns true if the item /// was not in the filter before. pub fn add(&self, key: &T) -> bool { + if self.bits.is_empty() { + return false; + } let mut added = false; for k in &self.keys { let (index, mask) = self.pos(key, *k); @@ -205,6 +202,9 @@ impl ConcurrentBloom { } pub fn contains(&self, key: &T) -> bool { + if self.keys.is_empty() || self.bits.is_empty() { + return false; + } self.keys.iter().all(|k| { let (index, mask) = self.pos(key, *k); let bit = self.bits[index].load(Ordering::Relaxed) & mask; @@ -309,6 +309,7 @@ mod test { bloom.add(&key); assert!(bloom.contains(&key)); } + #[test] fn test_random() { let mut b1: Bloom = Bloom::random(10, 0.1, 100); diff --git a/gossip/src/cluster_info.rs b/gossip/src/cluster_info.rs index d33483482d5..6737726ee2b 100644 --- a/gossip/src/cluster_info.rs +++ b/gossip/src/cluster_info.rs @@ -17,13 +17,13 @@ use { crate::{ cluster_info_metrics::{Counter, GossipStats, ScopedTimer, TimedGuard}, contact_info::{self, ContactInfo, ContactInfoQuery, Error as ContactInfoError}, - crds::{Crds, Cursor, GossipRoute}, + crds::{CRDS_SHARDS_BITS, Crds, Cursor, GossipRoute}, crds_data::{self, CrdsData, EpochSlotsIndex, LowestSlot, MAX_VOTES, SnapshotHashes, Vote}, crds_filter::{GossipFilterDirection, should_retain_crds_value}, crds_gossip::CrdsGossip, crds_gossip_error::CrdsGossipError, crds_gossip_pull::{ - CRDS_GOSSIP_PULL_CRDS_TIMEOUT_MS, CrdsFilter, CrdsTimeouts, ProcessPullStats, + self, CRDS_GOSSIP_PULL_CRDS_TIMEOUT_MS, CrdsFilter, CrdsTimeouts, ProcessPullStats, PullRequest, get_max_bloom_filter_bytes, }, crds_value::{CrdsValue, CrdsValueLabel}, @@ -53,6 +53,7 @@ use { PortRange, SocketAddrSpace, VALIDATOR_PORT_RANGE, bind_in_range, multihomed_sockets::BindIpAddrs, sockets::{bind_gossip_port_in_range, bind_to_localhost_unique}, + token_bucket::{KeyedRateLimiter, TokenBucket}, }, solana_perf::{ data_budget::DataBudget, @@ -121,6 +122,27 @@ pub(crate) const GOSSIP_CHANNEL_CAPACITY: usize = 4096; // 2^12 const GOSSIP_PING_CACHE_CAPACITY: usize = 126976; const GOSSIP_PING_CACHE_TTL: Duration = Duration::from_secs(1280); const GOSSIP_PING_CACHE_RATE_LIMIT_DELAY: Duration = Duration::from_secs(1280 / 64); +// Per-IP scan budget for incoming pull requests; validators are assumed not +// to share IPs. Mirrors the ping-pong cache capacity. +const GOSSIP_PULL_SCAN_BUDGET_CACHE_CAPACITY: usize = GOSSIP_PING_CACHE_CAPACITY; +const GOSSIP_PULL_SCAN_BUDGET_CAPACITY: u64 = 16 * crds_gossip_pull::MIN_NUM_BLOOM_ITEMS as u64; +const GOSSIP_PULL_SCAN_BUDGET_REFILL_PER_SEC: u64 = + 4 * crds_gossip_pull::MIN_NUM_BLOOM_ITEMS as u64; +const GOSSIP_PULL_SCAN_BUDGET_SHARD_COUNT: usize = 64; + +/// Estimated CRDS shard scan work for a pull request filter. +#[inline] +fn pull_request_scan_cost(crds_len: usize, mask_bits: u32, bloom_hash_count: usize) -> u64 { + // Extra mask bits still require one full shard scan. + let shift = mask_bits.min(CRDS_SHARDS_BITS); + let scan_entries = (crds_len >> shift).max(1) as u64; + + let bloom_hash_count = u64::try_from(bloom_hash_count) + .unwrap_or(u64::MAX) + .max(crds_gossip_pull::KEYS as u64); + scan_entries.saturating_mul(bloom_hash_count) +} + pub const DEFAULT_CONTACT_DEBUG_INTERVAL_MILLIS: u64 = 10_000; pub const DEFAULT_CONTACT_SAVE_INTERVAL_MILLIS: u64 = 60_000; // Limit number of unique pubkeys in the crds table. @@ -163,6 +185,7 @@ pub struct ClusterInfo { outbound_budget: DataBudget, my_contact_info: RwLock, ping_cache: Mutex, + pull_request_budget: KeyedRateLimiter, pub(crate) stats: GossipStats, local_message_pending_push_queue: Mutex>, contact_debug_interval: u64, // milliseconds, 0 = disabled @@ -191,6 +214,15 @@ impl ClusterInfo { GOSSIP_PING_CACHE_RATE_LIMIT_DELAY, GOSSIP_PING_CACHE_CAPACITY, )), + pull_request_budget: KeyedRateLimiter::new( + GOSSIP_PULL_SCAN_BUDGET_CACHE_CAPACITY, + TokenBucket::new( + GOSSIP_PULL_SCAN_BUDGET_CAPACITY, + GOSSIP_PULL_SCAN_BUDGET_CAPACITY, + GOSSIP_PULL_SCAN_BUDGET_REFILL_PER_SEC as f64, + ), + GOSSIP_PULL_SCAN_BUDGET_SHARD_COUNT, + ), stats: GossipStats::default(), local_message_pending_push_queue: Mutex::default(), contact_debug_interval: DEFAULT_CONTACT_DEBUG_INTERVAL_MILLIS, @@ -1638,6 +1670,24 @@ impl ClusterInfo { } } + fn try_consume_pull_request_scan_budget(&self, request: &PullRequest, crds_len: usize) -> bool { + let cost = pull_request_scan_cost( + crds_len, + request.filter.get_mask_bits(), + request.filter.bloom_hash_count(), + ); + if self + .pull_request_budget + .consume_tokens(request.addr.ip(), cost) + .is_ok() + { + true + } else { + self.stats.pull_request_scan_budget_exhausted.add_relaxed(1); + false + } + } + // Pull requests take an incoming bloom filter of contained entries from a node // and tries to send back to them the values it detects are missing. fn handle_pull_requests( @@ -1673,6 +1723,7 @@ impl ClusterInfo { GossipFilterDirection::EgressPullResponse, ) }, + |request, crds_len| self.try_consume_pull_request_scan_budget(request, crds_len), &self.stats, ) }; @@ -3279,13 +3330,16 @@ mod tests { tower.clear(); tower.extend(0..=slot); let vote = new_vote_transaction(vec![slot]); + // `KeyedRateLimiter` is not unwind-safe; this test only checks the panic. assert!( - panic::catch_unwind(|| cluster_info.push_vote(&tower, vote)) - .err() - .and_then(|a| a - .downcast_ref::() - .map(|s| { s.starts_with("Submitting old vote") })) - .unwrap_or_default() + panic::catch_unwind(panic::AssertUnwindSafe(|| { + cluster_info.push_vote(&tower, vote) + })) + .err() + .and_then(|a| a + .downcast_ref::() + .map(|s| { s.starts_with("Submitting old vote") })) + .unwrap_or_default() ); } diff --git a/gossip/src/cluster_info_metrics.rs b/gossip/src/cluster_info_metrics.rs index 752dc6336c9..afa3b7f5805 100644 --- a/gossip/src/cluster_info_metrics.rs +++ b/gossip/src/cluster_info_metrics.rs @@ -148,6 +148,7 @@ pub struct GossipStats { pub(crate) prune_received_cache: Counter, pub(crate) pull_from_entrypoint_count: Counter, pub(crate) pull_request_ping_pong_check_failed_count: Counter, + pub(crate) pull_request_scan_budget_exhausted: Counter, pub(crate) purge: Counter, pub(crate) purge_count: Counter, pub(crate) push_fanout_num_entries: Counter, @@ -356,6 +357,11 @@ pub(crate) fn submit_gossip_stats( stats.pull_request_ping_pong_check_failed_count.clear(), i64 ), + ( + "pull_request_scan_budget_exhausted", + stats.pull_request_scan_budget_exhausted.clear(), + i64 + ), ( "generate_pull_responses", stats.generate_pull_responses.clear(), diff --git a/gossip/src/crds.rs b/gossip/src/crds.rs index a4f0cc9a9fd..32b062661d6 100644 --- a/gossip/src/crds.rs +++ b/gossip/src/crds.rs @@ -55,7 +55,7 @@ use { }, }; -const CRDS_SHARDS_BITS: u32 = 12; +pub(crate) const CRDS_SHARDS_BITS: u32 = 12; // Number of vote slots to track in an lru-cache for metrics. const VOTE_SLOTS_METRICS_CAP: usize = 100; // Required number of leading zero bits for crds signature to get reported to influx diff --git a/gossip/src/crds_gossip.rs b/gossip/src/crds_gossip.rs index 73b1e5fb9d7..f023ba694bf 100644 --- a/gossip/src/crds_gossip.rs +++ b/gossip/src/crds_gossip.rs @@ -236,6 +236,7 @@ impl CrdsGossip { output_size_limit: usize, // Limit number of crds values returned. now: u64, should_retain_crds_value: impl Fn(&CrdsValue) -> bool + Sync, + try_consume_scan_budget: impl Fn(&PullRequest, usize) -> bool + Sync, stats: &GossipStats, ) -> Vec> { CrdsGossipPull::generate_pull_responses( @@ -245,6 +246,7 @@ impl CrdsGossip { output_size_limit, now, should_retain_crds_value, + try_consume_scan_budget, stats, ) } diff --git a/gossip/src/crds_gossip_pull.rs b/gossip/src/crds_gossip_pull.rs index 282e606e65e..ecc3a831b60 100644 --- a/gossip/src/crds_gossip_pull.rs +++ b/gossip/src/crds_gossip_pull.rs @@ -65,6 +65,19 @@ pub struct CrdsFilter { mask_bits: u32, } +#[cfg(debug_assertions)] +pub(crate) const MIN_NUM_BLOOM_ITEMS: usize = 512; +#[cfg(not(debug_assertions))] +pub(crate) const MIN_NUM_BLOOM_ITEMS: usize = 65_536; + +// Loosest mask_bits floor accepted for incoming pull requests. +// `PACKET_DATA_SIZE` avoids rejecting honest smaller bloom filters. +static MIN_PULL_REQUEST_MASK_BITS: LazyLock = LazyLock::new(|| { + let max_bits = (PACKET_DATA_SIZE * 8) as f64; + let max_items = CrdsFilter::max_items(max_bits, FALSE_RATE, KEYS); + CrdsFilter::mask_bits(MIN_NUM_BLOOM_ITEMS as f64, max_items) +}); + // Incoming gossip pull request from a remote node. pub struct PullRequest { pub pubkey: Pubkey, // remote node's pubkey @@ -85,7 +98,9 @@ impl Default for CrdsFilter { impl solana_sanitize::Sanitize for CrdsFilter { fn sanitize(&self) -> std::result::Result<(), solana_sanitize::SanitizeError> { - self.filter.sanitize()?; + if self.mask_bits < *MIN_PULL_REQUEST_MASK_BITS { + return Err(solana_sanitize::SanitizeError::InvalidValue); + } Ok(()) } } @@ -97,11 +112,14 @@ impl CrdsFilter { self.mask } - #[cfg(any(test, feature = "conformance"))] - pub(crate) fn get_mask_bits(&self) -> u32 { + pub fn get_mask_bits(&self) -> u32 { self.mask_bits } + pub fn bloom_hash_count(&self) -> usize { + self.filter.keys.len() + } + #[cfg(any(test, feature = "dev-context-only-utils"))] pub fn new_rand(num_items: usize, max_bytes: usize) -> Self { let max_bits = (max_bytes * 8) as f64; @@ -330,6 +348,7 @@ impl CrdsGossipPull { output_size_limit: usize, // Limit number of crds values returned. now: u64, should_retain_crds_value: impl Fn(&CrdsValue) -> bool + Sync, + try_consume_scan_budget: impl Fn(&PullRequest, usize) -> bool + Sync, stats: &GossipStats, ) -> Vec> { Self::filter_crds_values( @@ -339,6 +358,7 @@ impl CrdsGossipPull { output_size_limit, now, should_retain_crds_value, + try_consume_scan_budget, stats, ) } @@ -450,10 +470,6 @@ impl CrdsGossipPull { bloom_size: usize, ) -> Vec { const PAR_MIN_LENGTH: usize = 512; - #[cfg(debug_assertions)] - const MIN_NUM_BLOOM_ITEMS: usize = 512; - #[cfg(not(debug_assertions))] - const MIN_NUM_BLOOM_ITEMS: usize = 65_536; let failed_inserts = self.failed_inserts.read().unwrap(); // crds should be locked last after self.failed_inserts. let crds = crds.read().unwrap(); @@ -487,6 +503,9 @@ impl CrdsGossipPull { now: u64, // Predicate returning false if the CRDS value should be discarded. should_retain_crds_value: impl Fn(&CrdsValue) -> bool + Sync, + // False drops the request before scanning CRDS. + // Implementations may consume caller-owned scan budget. + try_consume_scan_budget: impl Fn(&PullRequest, usize) -> bool + Sync, stats: &GossipStats, ) -> Vec> { let msg_timeout = CRDS_GOSSIP_PULL_CRDS_TIMEOUT_MS; @@ -499,6 +518,7 @@ impl CrdsGossipPull { let output_size_limit = output_size_limit.try_into().unwrap_or(i64::MAX); let output_size_limit = AtomicI64::new(output_size_limit); let crds = crds.read().unwrap(); + let crds_len = crds.len(); let apply_filter = |request: &PullRequest| { if output_size_limit.load(Ordering::Relaxed) <= 0 { return Vec::default(); @@ -509,6 +529,10 @@ impl CrdsGossipPull { dropped_requests.fetch_add(1, Ordering::Relaxed); return Vec::default(); } + // Charge only requests that passed cheaper pre-scan checks. + if !try_consume_scan_budget(request, crds_len) { + return Vec::default(); + } let caller_wallclock = caller_wallclock.checked_add(jitter).unwrap_or(0); let pred = |entry: &&VersionedCrdsValue| { debug_assert!(filter.test_mask(entry.value.hash())); @@ -1119,7 +1143,8 @@ pub(crate) mod tests { &requests, usize::MAX, new_wallclock, - |_| true, + |_| true, // should_retain_crds_value + |_, _| true, // try_consume_scan_budget &GossipStats::default(), ); assert_eq!(rsp.len(), 2); diff --git a/gossip/tests/crds_gossip.rs b/gossip/tests/crds_gossip.rs index f2c98a88bf9..28978a7ebff 100644 --- a/gossip/tests/crds_gossip.rs +++ b/gossip/tests/crds_gossip.rs @@ -595,7 +595,8 @@ fn network_run_pull( &requests, usize::MAX, // output_size_limit now, - |_| true, // should_retain_crds_value + |_| true, // should_retain_crds_value + |_, _| true, // try_consume_scan_budget &GossipStats::default(), ) .into_iter() From d52dcf082ba1c0a78e8020280bc4999b27ec5270 Mon Sep 17 00:00:00 2001 From: Rory Harris Date: Fri, 12 Jun 2026 08:52:38 -0700 Subject: [PATCH 310/576] accounts-db: remove SlotStatus; trust write-cache hits (#13146) Remove SlotStatus checks, as write cache will always be newer than storage now --- accounts-db/src/accounts_cache.rs | 112 +++++++-------------------- accounts-db/src/accounts_db.rs | 82 +++++--------------- accounts-db/src/accounts_db/tests.rs | 45 ++++------- 3 files changed, 63 insertions(+), 176 deletions(-) diff --git a/accounts-db/src/accounts_cache.rs b/accounts-db/src/accounts_cache.rs index 50fbdeefac7..f2bf7736e45 100644 --- a/accounts-db/src/accounts_cache.rs +++ b/accounts-db/src/accounts_cache.rs @@ -36,26 +36,6 @@ impl MaxFlushedRoot { } } -/// Indicates whether a slot is an ancestor, an unflushed root, or a root being flushed -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum SlotStatus { - /// Slot is in the ancestors list and not a root being flushed. Slot is guaranteed to be - /// the newest version of the account on this fork so the cached copy can be used without - /// checking storage - Ancestor, - /// Slot is in the ancestors list but is also a root being flushed. The version found in the - /// accounts cache may not be the newest version of the account, and the storage should be - /// checked for newer versions - AncestorBeingFlushed, - /// Slot is a root that has not yet been claimed for flushing. Slot is guaranteed to be the - /// newest version of the account so the cached copy can be used without checking storage - UnflushedRoot, - /// Slot is a root that is currently being flushed. The version found in the accounts cache - /// may not be the newest version of the account, and the storage should be checked for newer - /// versions - RootBeingFlushed, -} - #[derive(Debug)] pub struct SlotCache { cache: DashMap, PubkeyHasherBuilder>, @@ -347,40 +327,28 @@ impl AccountsCache { result } - /// Finds the best write-cache entry for `pubkey` visible from `ancestors`. Searches - /// ancestors first (highest to lowest), then unflushed roots, then roots being flushed. - /// Ancestors are checked exhaustively before roots, so a lower-slot ancestor wins over a - /// higher-slot root. Returns the account, slot, and status, or `None` if not found. + /// Finds the newest write-cache entry for `pubkey` visible from `ancestors`. Searches + /// ancestors first (highest to lowest), then roots (highest to lowest). Ancestors are + /// checked exhaustively before roots, so a lower-slot ancestor wins over a higher-slot + /// root. Returns the account and its slot, or `None` if not found. pub fn load_latest( &self, pubkey: &Pubkey, ancestors: &Ancestors, - ) -> Option<(Arc, Slot, SlotStatus)> { + ) -> Option<(Arc, Slot)> { // Exit early if the pubkey isn't in the cache let index_max_slot = self.index.max_slot_for_pubkey(pubkey)?; + // Ancestors take priority over roots regardless of slot. Iterate every slot in the + // range in descending order and return the first (highest) ancestor that has it. if let Some(ancestors_min_slot) = ancestors.min_slot() { - // Iterate every slot in the range in descending order - // Grab a read lock on flushing roots once before the loop to avoid locking/unlocking - // on every iteration. This lock ensures that the load call in the loop correctly - // identifies slots with status AncestorBeingFlushed. - let r_roots_being_flushed = self.roots_being_flushed.read().unwrap(); for slot in (ancestors_min_slot..=index_max_slot).rev() { if ancestors.contains_key(&slot) { if let Some(account) = self.load(slot, pubkey) { - // Need to check flush status of the slot even for ancestors, because - // there could be newer version of the account that has already been - // flushed - let slot_status = if r_roots_being_flushed.contains(&slot) { - SlotStatus::AncestorBeingFlushed - } else { - SlotStatus::Ancestor - }; - return Some((account, slot, slot_status)); + return Some((account, slot)); } } } - drop(r_roots_being_flushed); } // If the slot is not found in the ancestors fall back to searching roots. @@ -396,7 +364,7 @@ impl AccountsCache { let r_maybe_unflushed_roots = self.maybe_unflushed_roots.read().unwrap(); for &slot in r_maybe_unflushed_roots.range(..=max_root_slot).rev() { if let Some(account) = self.load(slot, pubkey) { - return Some((account, slot, SlotStatus::UnflushedRoot)); + return Some((account, slot)); } } drop(r_maybe_unflushed_roots); @@ -404,7 +372,7 @@ impl AccountsCache { let r_roots_being_flushed = self.roots_being_flushed.read().unwrap(); for &slot in r_roots_being_flushed.range(..=max_root_slot).rev() { if let Some(account) = self.load(slot, pubkey) { - return Some((account, slot, SlotStatus::RootBeingFlushed)); + return Some((account, slot)); } } drop(r_roots_being_flushed); @@ -692,7 +660,7 @@ mod tests { assert!(cache.index.max_slot_for_pubkey(&pk1).is_none()); } - /// Tests that `load_latest` returns the correct slot, status, and account value + /// Tests that `load_latest` returns the correct slot and account value /// given various combinations of ancestor slots, root slots, and flushing state. /// /// Ancestors always take priority over roots regardless of slot @@ -701,25 +669,25 @@ mod tests { // data stored in the cache. This lets us test root bounding by min_slot // without the ancestor path short-circuiting the lookup. #[test_case(&[], &[], &[], &[], None; "not ancestor not root")] - #[test_case(&[10], &[], &[], &[], Some((10, SlotStatus::Ancestor)); "ancestor only")] - #[test_case(&[5, 10, 15], &[], &[], &[], Some((15, SlotStatus::Ancestor)); "highest ancestor returned")] - #[test_case(&[], &[10, 20], &[], &[], Some((20, SlotStatus::UnflushedRoot)); "rooted, with no ancestors")] - #[test_case(&[], &[], &[10], &[], Some((10, SlotStatus::RootBeingFlushed)); "root being flushed")] - #[test_case(&[10], &[], &[10], &[], Some((10, SlotStatus::AncestorBeingFlushed)); "ancestor being flushed")] - #[test_case(&[5], &[20], &[], &[], Some((5, SlotStatus::Ancestor)); "ancestor wins over higher root")] - #[test_case(&[], &[20], &[10], &[], Some((20, SlotStatus::UnflushedRoot));"unflushed root over flushing root")] - #[test_case(&[5], &[20], &[10], &[], Some((5, SlotStatus::Ancestor));"ancestor over unflushed and flushing roots")] + #[test_case(&[10], &[], &[], &[], Some(10); "ancestor only")] + #[test_case(&[5, 10, 15], &[], &[], &[], Some(15); "highest ancestor returned")] + #[test_case(&[], &[10, 20], &[], &[], Some(20); "rooted, with no ancestors")] + #[test_case(&[], &[], &[10], &[], Some(10); "root being flushed")] + #[test_case(&[10], &[], &[10], &[], Some(10); "ancestor being flushed")] + #[test_case(&[5], &[20], &[], &[], Some(5); "ancestor wins over higher root")] + #[test_case(&[], &[20], &[10], &[], Some(20);"unflushed root over flushing root")] + #[test_case(&[5], &[20], &[10], &[], Some(5);"ancestor over unflushed and flushing roots")] // Root beyond ancestors.min_slot() is excluded; older root still found - #[test_case(&[], &[5, 11], &[], &[10], Some((5, SlotStatus::UnflushedRoot)); "unflushed root beyond min ancestor excluded")] - #[test_case(&[], &[], &[5, 11], &[10], Some((5, SlotStatus::RootBeingFlushed)); "flushing root beyond min ancestor excluded")] + #[test_case(&[], &[5, 11], &[], &[10], Some(5); "unflushed root beyond min ancestor excluded")] + #[test_case(&[], &[], &[5, 11], &[10], Some(5); "flushing root beyond min ancestor excluded")] // Root within min_slot bound is still returned - #[test_case(&[], &[10], &[], &[15], Some((10, SlotStatus::UnflushedRoot)); "unflushed root below min ancestor returned")] + #[test_case(&[], &[10], &[], &[15], Some(10); "unflushed root below min ancestor returned")] fn test_load_latest_slot_priority( ancestor_slots: &[Slot], unflushed_root_slots: &[Slot], flushing_root_slots: &[Slot], uncached_ancestors: &[Slot], - expected: Option<(Slot, SlotStatus)>, + expected: Option, ) { let cache = AccountsCache::default(); let pk = Pubkey::new_unique(); @@ -746,12 +714,10 @@ mod tests { let mut all_ancestors: Vec = ancestor_slots.to_vec(); all_ancestors.extend_from_slice(uncached_ancestors); let ancestors = Ancestors::from(all_ancestors); - let result = cache - .load_latest(&pk, &ancestors) - .map(|(account, slot, status)| { - assert_eq!(account.account.lamports(), slot); - (slot, status) - }); + let result = cache.load_latest(&pk, &ancestors).map(|(account, slot)| { + assert_eq!(account.account.lamports(), slot); + slot + }); assert_eq!(result, expected); } @@ -862,28 +828,4 @@ mod tests { assert_eq!(taken, BTreeSet::from([3])); cache.end_flush_roots(); } - - #[test] - fn test_load_latest_multiple_ancestors_one_being_flushed() { - let cache = AccountsCache::default(); - let pk = Pubkey::new_unique(); - - // Store at slots 5 and 10, both will be ancestors - cache.store(5, &pk, AccountSharedData::new(5, 0, &Pubkey::default())); - cache.store(10, &pk, AccountSharedData::new(10, 0, &Pubkey::default())); - - // Slot 10 is also a root that gets claimed for flushing - cache.add_root(10); - cache.begin_flush_roots(None); - - let ancestors = Ancestors::from(vec![5, 10]); - let (account, slot, status) = cache.load_latest(&pk, &ancestors).unwrap(); - - // Slot 10 is highest ancestor but is being flushed, so should return AncestorBeingFlushed - assert_eq!(slot, 10); - assert_eq!(status, SlotStatus::AncestorBeingFlushed); - assert_eq!(account.account.lamports(), 10); - - cache.end_flush_roots(); - } } diff --git a/accounts-db/src/accounts_db.rs b/accounts-db/src/accounts_db.rs index f7adafa281c..d21c3a12b3c 100644 --- a/accounts-db/src/accounts_db.rs +++ b/accounts-db/src/accounts_db.rs @@ -36,7 +36,7 @@ use { stored_account_info::{StoredAccountInfo, StoredAccountInfoWithoutData}, }, account_storage_entry::AccountStorageEntry, - accounts_cache::{AccountsCache, CachedAccount, SlotCache, SlotStatus}, + accounts_cache::{AccountsCache, CachedAccount, SlotCache}, accounts_db::stats::{ AccountsStats, CleanAccountsStats, FlushStats, LoadAccountsStats, ObsoleteAccountsStats, PurgeStats, ShrinkAncientStats, ShrinkStats, ShrinkStatsSub, @@ -3471,7 +3471,7 @@ impl AccountsDb { break; } - if let Some((cached_account, slot, _)) = + if let Some((cached_account, slot)) = self.accounts_cache.load_latest(&pubkey, ancestors) { cached_versions.insert(pubkey, (cached_account, slot)); @@ -4060,39 +4060,21 @@ impl AccountsDb { ) -> Option<(AccountSharedData, Slot)> { let starting_max_root = self.accounts_index.max_root_inclusive(); - // First check the write cache - let cache_result = self.accounts_cache.load_latest(pubkey, ancestors); - - if let Some((cached_account, cached_slot, slot_status)) = &cache_result { - // If the slot is an ancestor or an unflushed root, it hasn't been flushed to - // storage yet, so the write cache is authoritative — return it directly. - if matches!( - slot_status, - SlotStatus::Ancestor | SlotStatus::UnflushedRoot - ) { - self.load_account_stats - .num_loaded_from_write_cache - .fetch_add(1, Ordering::Relaxed); - return Some((cached_account.account.clone(), *cached_slot)); - } + // Check the write cache first; a hit is the freshest version visible on this fork, + // so return it + if let Some((cached_account, cached_slot)) = + self.accounts_cache.load_latest(pubkey, ancestors) + { + self.load_account_stats + .num_loaded_from_write_cache + .fetch_add(1, Ordering::Relaxed); + return Some((cached_account.account.clone(), cached_slot)); } let (slot, storage_location, _maybe_account_accessor) = self.read_index_for_accessor_or_load_slow(ancestors, pubkey, false)?; // Notice the subtle `?` at previous line, we bail out pretty early if missing. - if let Some((cached_account, cached_slot, _)) = &cache_result { - if *cached_slot >= slot { - // The write cache holds a version at a slot at least as new as what the - // index returned, so it represents the freshest visible value and can be - // returned directly without going through the storage accessor. - self.load_account_stats - .num_loaded_from_write_cache - .fetch_add(1, Ordering::Relaxed); - return Some((cached_account.account.clone(), *cached_slot)); - } - } - let in_write_cache = storage_location.is_cached(); if !in_write_cache { let result = self.read_only_accounts_cache.load(*pubkey, slot); @@ -4115,17 +4097,12 @@ impl AccountsDb { // since the cache could be flushed in between the 2 calls. let in_write_cache = matches!(account_accessor, LoadedAccountAccessor::Cached(_)); if in_write_cache { - // While the account wasn't loaded directly from the write cache, the write cache - // provided the correct slot, so count this as a load from the write cache - if cache_result.is_some_and(|(_, cached_slot, _)| cached_slot == slot) { - self.load_account_stats - .num_loaded_from_write_cache - .fetch_add(1, Ordering::Relaxed); - } else { - self.load_account_stats - .num_loaded_from_index_cache - .fetch_add(1, Ordering::Relaxed); - } + // `load_latest` missed above (otherwise we'd have returned early), but the account + // was written to the cache between then and the index lookup, so the index pointed + // us back at the cache. + self.load_account_stats + .num_loaded_from_index_cache + .fetch_add(1, Ordering::Relaxed); } else { self.load_account_stats .num_loaded_from_index_storage @@ -5912,29 +5889,12 @@ impl AccountsDb { /// Returns whether the latest version of pubkey from ancestors is zero-lamport /// Returns `None` if the account doesn't exist fn is_ancestor_zero_lamport(&self, pubkey: &Pubkey, ancestors: &Ancestors) -> Option { - if let Some((cached_account, cache_slot, status)) = + if let Some((cached_account, _cache_slot)) = self.accounts_cache.load_latest(pubkey, ancestors) { - let cache_is_zero_lamport = cached_account.account.lamports() == 0; - // When the slot isn't being flushed, the cache is definitely the newest - if matches!(status, SlotStatus::Ancestor | SlotStatus::UnflushedRoot) { - return Some(cache_is_zero_lamport); - } - - // Slot is being flushed; a newer version may already exist in storage. - if let Some((index_slot, index_is_zero_lamport)) = self - .accounts_index - .get_with_and_then(pubkey, ancestors, true, |(slot, account)| { - (slot, account.is_zero_lamport()) - }) - { - // Return the zero lamport status of the newest version between the cache and index - // If slots are the same it doesn't matter which is returned (They are identical) - if index_slot > cache_slot { - return Some(index_is_zero_lamport); - } - } - Some(cache_is_zero_lamport) + // Check the write cache first; a hit is the freshest version visible on this fork, + // so return it + Some(cached_account.account.lamports() == 0) } else { self.accounts_index .get_with_and_then(pubkey, ancestors, true, |(_, account)| { diff --git a/accounts-db/src/accounts_db/tests.rs b/accounts-db/src/accounts_db/tests.rs index 2db6769459d..5c5aab07dd1 100644 --- a/accounts-db/src/accounts_db/tests.rs +++ b/accounts-db/src/accounts_db/tests.rs @@ -6833,42 +6833,27 @@ fn test_is_ancestor_zero_lamport_index_only() { assert_eq!(db.is_ancestor_zero_lamport(&pubkey, &ancestors), Some(true)); } -/// Verifies that when the cache entry is in a `*BeingFlushed` state, is_ancestor_zero_lamport -/// also consults the index and returns the zero lamport status of whichever version sits on the -/// higher slot, regardless of whether that version is in the cache or the index. -#[test_case(10, 5, true; "newer_cache_wins")] -#[test_case(5, 10, false; "newer_index_wins")] -fn test_is_ancestor_zero_lamport_being_flushed_picks_higher_slot( - cache_slot: Slot, - index_slot: Slot, - expected: bool, -) { +/// Verifies that when a pubkey is in both storage and the write cache, is_ancestor_zero_lamport +/// returns the cached version's zero lamport status. +#[test] +fn test_is_ancestor_zero_lamport_cache_over_storage() { let db = AccountsDb::new_single_for_tests(); let pubkey = Pubkey::new_unique(); + let storage_slot = 5; + let cache_slot = 10; - // Non-zero lamport entry in the index. Set up first: flushing goes through the cache, - // so doing it after the cache setup would conflict with `begin_flush_roots`. - let account = AccountSharedData::new(100, 0, &Pubkey::default()); - db.store_for_tests((index_slot, [(&pubkey, &account)].as_slice())); - db.add_root_and_flush_write_cache(index_slot); + // Non-zero lamport entry in storage at the older slot. + let nonzero_account = AccountSharedData::new(100, 0, &Pubkey::default()); + db.store_for_tests((storage_slot, [(&pubkey, &nonzero_account)].as_slice())); + db.add_root_and_flush_write_cache(storage_slot); assert!(!db.accounts_cache.contains_pubkey(&pubkey)); - // Zero lamport entry in the cache. Note: this entry may be older than the slot that was - // flushed above which is normally not allowed to be added to the cache. However, - // this state is a valid intermediate state when flushing the cache so needs - // test coverage - let account = AccountSharedData::new(0, 0, &Pubkey::default()); - db.accounts_cache.store(cache_slot, &pubkey, account); - db.accounts_cache.add_root(cache_slot); - db.accounts_cache.begin_flush_roots(Some(cache_slot)); - - let ancestors = Ancestors::from(vec![cache_slot, index_slot]); - assert_eq!( - db.is_ancestor_zero_lamport(&pubkey, &ancestors), - Some(expected) - ); + // Zero lamport entry in the cache at the newer slot. + let zero_account = AccountSharedData::new(0, 0, &Pubkey::default()); + db.accounts_cache.store(cache_slot, &pubkey, zero_account); - db.accounts_cache.end_flush_roots(); + let ancestors = Ancestors::from(vec![cache_slot, storage_slot]); + assert_eq!(db.is_ancestor_zero_lamport(&pubkey, &ancestors), Some(true)); } /// loading an account through an older bank must not return From 67915564a9cbeb1db36b7b8f4053ea96a3df3ebd Mon Sep 17 00:00:00 2001 From: Brooks Date: Fri, 12 Jun 2026 12:20:20 -0400 Subject: [PATCH 311/576] Do not read account data from disk in update_index_for_shrink() (#13171) --- accounts-db/src/accounts_db.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/accounts-db/src/accounts_db.rs b/accounts-db/src/accounts_db.rs index d21c3a12b3c..7475b53644e 100644 --- a/accounts-db/src/accounts_db.rs +++ b/accounts-db/src/accounts_db.rs @@ -5297,11 +5297,10 @@ impl AccountsDb { (start..end).for_each(|i| { let info: AccountInfo = infos[i]; debug_assert!(!info.is_cached()); - accounts.account(i, |account| { - let old_slot = accounts.slot(i); - self.accounts_index - .replace(target_slot, old_slot, account.pubkey(), info); - }); + let old_slot = accounts.slot(i); + let pubkey = accounts.pubkey(i); + self.accounts_index + .replace(target_slot, old_slot, pubkey, info); }); }; From 6ca7e37ad41bfce9231ecf11eac8adcbdab2fb0b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 Jun 2026 19:19:23 +0200 Subject: [PATCH 312/576] chore(deps): bump solana-frozen-abi-macro from 3.5.0 to 3.6.0 (#13155) * chore(deps): bump solana-frozen-abi-macro from 3.5.0 to 3.6.0 Bumps [solana-frozen-abi-macro](https://github.com/anza-xyz/solana-sdk) from 3.5.0 to 3.6.0. - [Release notes](https://github.com/anza-xyz/solana-sdk/releases) - [Commits](https://github.com/anza-xyz/solana-sdk/commits) --- updated-dependencies: - dependency-name: solana-frozen-abi-macro dependency-version: 3.6.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * Update all workspaces --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eb206b72983..771188089fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8311,9 +8311,9 @@ dependencies = [ [[package]] name = "solana-frozen-abi-macro" -version = "3.5.0" +version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9385c5dc5555c010a96504d80f47be914ec4637a320d99b2e26d964ecd32691a" +checksum = "3ce2a1f17252391f44f620d4e47d1e3e0bec173f760a86ee7de2426db9ea7496" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index e10b0677125..e1d0cfda676 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -402,7 +402,7 @@ solana-fee-calculator = "3.2.1" solana-fee-structure = "3.0.0" solana-file-download = "3.1.4" solana-frozen-abi = "3.5.0" -solana-frozen-abi-macro = "3.5.0" +solana-frozen-abi-macro = "3.6.0" solana-genesis = { path = "genesis", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-genesis-config = "4.0.0" solana-genesis-utils = { path = "genesis-utils", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } From 05bb45687aa7597eebf430e44b9f38079de92b7d Mon Sep 17 00:00:00 2001 From: Brooks Date: Fri, 12 Jun 2026 13:21:36 -0400 Subject: [PATCH 313/576] Avoid calling StorableAccounts::account() in update_index_stored_accounts() (#13176) --- accounts-db/src/accounts_db.rs | 38 ++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/accounts-db/src/accounts_db.rs b/accounts-db/src/accounts_db.rs index 7475b53644e..2ec9fbfbabb 100644 --- a/accounts-db/src/accounts_db.rs +++ b/accounts-db/src/accounts_db.rs @@ -5227,22 +5227,28 @@ impl AccountsDb { (start..end).for_each(|i| { let info: AccountInfo = infos[i]; debug_assert!(!info.is_cached()); - accounts.account(i, |account| { - let old_slot = accounts.slot(i); - self.accounts_index.upsert( - target_slot, - old_slot, - account.pubkey(), - info, - &mut reclaims, - reclaim, - ); - self.accounts_index.update_secondary_indexes( - account.pubkey(), - &account, - &self.account_indexes, - ); - }); + let old_slot = accounts.slot(i); + let pubkey = accounts.pubkey(i); + self.accounts_index.upsert( + target_slot, + old_slot, + pubkey, + info, + &mut reclaims, + reclaim, + ); + + if !self.account_indexes.is_empty() { + // Since StorableAccounts::account() may read the account from disk, + // avoid calling it unless secondary indexes are enabled. + accounts.account(i, |account| { + self.accounts_index.update_secondary_indexes( + pubkey, + &account, + &self.account_indexes, + ); + }); + } }); reclaims }; From 100c4c3e31a783dee51aa31357fbcb8de943913f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 13 Jun 2026 02:04:07 +0800 Subject: [PATCH 314/576] chore(deps): bump solana-frozen-abi from 3.5.0 to 3.6.0 (#13157) * chore(deps): bump solana-frozen-abi from 3.5.0 to 3.6.0 Bumps [solana-frozen-abi](https://github.com/anza-xyz/solana-sdk) from 3.5.0 to 3.6.0. - [Release notes](https://github.com/anza-xyz/solana-sdk/releases) - [Commits](https://github.com/anza-xyz/solana-sdk/commits) --- updated-dependencies: - dependency-name: solana-frozen-abi dependency-version: 3.6.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * Update all workspaces --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 6 +++--- Cargo.toml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 771188089fa..ba4b1e63fbc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1340,7 +1340,7 @@ checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" dependencies = [ "bitcoin_hashes", "rand 0.8.6", - "rand_core 0.6.4", + "rand_core 0.5.1", "serde", "unicode-normalization", ] @@ -8284,9 +8284,9 @@ dependencies = [ [[package]] name = "solana-frozen-abi" -version = "3.5.0" +version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6fc5ab0701253517fdc7de93f965988cce05e52ac494f9c11e1f46742b2c346" +checksum = "f47b47faa2099702dc4c764bd70806ac07166a2c81fd0c5154ed04c6e57e2b93" dependencies = [ "bincode", "boxcar", diff --git a/Cargo.toml b/Cargo.toml index e1d0cfda676..c2b328e3b65 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -401,7 +401,7 @@ solana-fee = { path = "fee", version = "=4.2.0-alpha.0", features = ["agave-unst solana-fee-calculator = "3.2.1" solana-fee-structure = "3.0.0" solana-file-download = "3.1.4" -solana-frozen-abi = "3.5.0" +solana-frozen-abi = "3.6.0" solana-frozen-abi-macro = "3.6.0" solana-genesis = { path = "genesis", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } solana-genesis-config = "4.0.0" From 30aea66412c8e291ce2a55fe828508290dff2a99 Mon Sep 17 00:00:00 2001 From: Akhilesh Singhania Date: Fri, 12 Jun 2026 20:05:09 +0200 Subject: [PATCH 315/576] Moves bls sigverifier into its own crate (#13130) --- Cargo.lock | 41 +++++ Cargo.toml | 2 + bls-sigverify/Cargo.toml | 62 +++++++ .../benches/bls_vote_sigverify.rs | 14 +- .../src}/bls_cert_sigverify.rs | 3 +- .../src}/bls_sigverifier.rs | 121 +++++++------- .../src}/bls_vote_sigverify.rs | 24 +-- .../src}/errors.rs | 0 .../src/generated_cert_types.rs | 7 +- .../mod.rs => bls-sigverify/src/lib.rs | 4 + bls-sigverify/src/rewards.rs | 39 +++++ .../src}/stats.rs | 157 ++++++++++-------- .../src}/utils.rs | 21 ++- core/Cargo.toml | 5 +- core/src/block_creation_loop.rs | 5 +- .../rewards/certs_builder.rs | 35 +--- .../block_creation_loop/rewards/msg_types.rs | 14 +- .../rewards/reward_certs_service.rs | 5 +- core/src/cluster_info_vote_listener.rs | 7 +- core/src/lib.rs | 1 - core/src/repair/repair_service.rs | 3 +- core/src/tpu.rs | 3 +- core/src/tvu.rs | 17 +- dev-bins/Cargo.lock | 30 ++++ programs/sbf/Cargo.lock | 30 ++++ votor-messages/Cargo.toml | 1 + votor-messages/src/lib.rs | 13 ++ votor-messages/src/metric_types.rs | 54 ++++++ votor-messages/src/reward_certificate.rs | 8 + votor/Cargo.toml | 1 + votor/src/consensus_metrics.rs | 47 +----- votor/src/consensus_pool.rs | 2 +- votor/src/consensus_pool_service.rs | 2 +- votor/src/event_handler.rs | 8 +- votor/src/lib.rs | 1 - votor/src/voting_utils.rs | 2 +- votor/src/votor.rs | 11 +- 37 files changed, 508 insertions(+), 292 deletions(-) create mode 100644 bls-sigverify/Cargo.toml rename {core => bls-sigverify}/benches/bls_vote_sigverify.rs (99%) rename {core/src/bls_sigverify => bls-sigverify/src}/bls_cert_sigverify.rs (98%) rename {core/src/bls_sigverify => bls-sigverify/src}/bls_sigverifier.rs (95%) rename {core/src/bls_sigverify => bls-sigverify/src}/bls_vote_sigverify.rs (95%) rename {core/src/bls_sigverify => bls-sigverify/src}/errors.rs (100%) rename {votor => bls-sigverify}/src/generated_cert_types.rs (76%) rename core/src/bls_sigverify/mod.rs => bls-sigverify/src/lib.rs (57%) create mode 100644 bls-sigverify/src/rewards.rs rename {core/src/bls_sigverify => bls-sigverify/src}/stats.rs (75%) rename {core/src/bls_sigverify => bls-sigverify/src}/utils.rs (87%) create mode 100644 votor-messages/src/metric_types.rs diff --git a/Cargo.lock b/Cargo.lock index ba4b1e63fbc..b045f4955c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -80,6 +80,44 @@ dependencies = [ "wincode", ] +[[package]] +name = "agave-bls-sigverify" +version = "4.2.0-alpha.0" +dependencies = [ + "agave-bls-cert-verify", + "agave-bls-sigverify", + "agave-logger", + "agave-math-utils", + "agave-votor", + "agave-votor-messages", + "bincode", + "bitvec", + "criterion", + "crossbeam-channel", + "log", + "parking_lot 0.12.3", + "qualifier_attr", + "rayon", + "solana-bls-signatures", + "solana-clock", + "solana-epoch-schedule", + "solana-gossip", + "solana-hash 4.4.0", + "solana-keypair", + "solana-ledger", + "solana-measure", + "solana-metrics", + "solana-net-utils", + "solana-perf", + "solana-pubkey 4.2.0", + "solana-runtime", + "solana-signer", + "solana-signer-store", + "solana-streamer", + "thiserror 2.0.18", + "wincode", +] + [[package]] name = "agave-cpu-utils" version = "4.2.0-alpha.0" @@ -438,6 +476,7 @@ dependencies = [ name = "agave-votor" version = "4.2.0-alpha.0" dependencies = [ + "agave-bls-sigverify", "agave-logger", "agave-math-utils", "agave-votor", @@ -496,6 +535,7 @@ version = "4.2.0-alpha.0" dependencies = [ "agave-feature-set", "agave-votor-messages", + "crossbeam-channel", "log", "serde", "solana-address 2.6.1", @@ -7757,6 +7797,7 @@ version = "4.2.0-alpha.0" dependencies = [ "agave-banking-stage-ingress-types", "agave-bls-cert-verify", + "agave-bls-sigverify", "agave-feature-set", "agave-fs", "agave-logger", diff --git a/Cargo.toml b/Cargo.toml index c2b328e3b65..d9a4fea087f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ members = [ "banks-server", "bloom", "bls-cert-verify", + "bls-sigverify", "bucket_map", "builtins", "builtins-default-costs", @@ -171,6 +172,7 @@ collapsible_if = "allow" Inflector = "0.11.4" agave-banking-stage-ingress-types = { path = "banking-stage-ingress-types", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } agave-bls-cert-verify = { path = "bls-cert-verify", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } +agave-bls-sigverify = { path = "bls-sigverify", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } agave-cpu-utils = { path = "cpu-utils", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } agave-feature-set = { path = "feature-set", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } agave-fs = { path = "fs", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } diff --git a/bls-sigverify/Cargo.toml b/bls-sigverify/Cargo.toml new file mode 100644 index 00000000000..438f13fe289 --- /dev/null +++ b/bls-sigverify/Cargo.toml @@ -0,0 +1,62 @@ +[package] +name = "agave-bls-sigverify" +description = "Logic to process incoming alpenglow consensus messages" +readme = "../README.md" +version = { workspace = true } +authors = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +edition = { workspace = true } + +[features] +agave-unstable-api = [] +dev-context-only-utils = [ + "solana-runtime/dev-context-only-utils", + "agave-votor-messages/dev-context-only-utils", +] + +[dependencies] +agave-bls-cert-verify = { workspace = true } +agave-math-utils = { workspace = true } +agave-votor-messages = { workspace = true } +bitvec = { workspace = true } +crossbeam-channel = { workspace = true } +log = { workspace = true } +parking_lot = { workspace = true } +qualifier_attr = { workspace = true } +rayon = { workspace = true } +solana-bls-signatures = { workspace = true, features = ["parallel"] } +solana-clock = { workspace = true } +solana-gossip = { workspace = true } +solana-ledger = { workspace = true } +solana-measure = { workspace = true } +solana-metrics = { workspace = true } +solana-pubkey = { workspace = true } +solana-runtime = { workspace = true } +solana-signer-store = { workspace = true } +solana-streamer = { workspace = true } +thiserror = { workspace = true } +wincode = { workspace = true } + +[dev-dependencies] +# See order-crates-for-publishing.py for using this unusual `path = "."` +agave-bls-sigverify = { path = ".", features = ["agave-unstable-api", "dev-context-only-utils"] } +agave-logger = { workspace = true } +# See order-crates-for-publishing.py for using this unusual `path` +agave-votor = { path = "../votor", features = ["agave-unstable-api"] } +bincode = { workspace = true } +criterion = { workspace = true } +solana-epoch-schedule = { workspace = true } +solana-hash = { workspace = true } +solana-keypair = { workspace = true } +solana-net-utils = { workspace = true } +solana-perf = { workspace = true } +solana-signer = { workspace = true } + +[[bench]] +name = "bls_vote_sigverify" +harness = false + +[lints] +workspace = true diff --git a/core/benches/bls_vote_sigverify.rs b/bls-sigverify/benches/bls_vote_sigverify.rs similarity index 99% rename from core/benches/bls_vote_sigverify.rs rename to bls-sigverify/benches/bls_vote_sigverify.rs index b2e90eaf9c7..7160d5a65c9 100644 --- a/core/benches/bls_vote_sigverify.rs +++ b/bls-sigverify/benches/bls_vote_sigverify.rs @@ -4,6 +4,13 @@ */ use { + agave_bls_sigverify::{ + bls_vote_sigverify::{ + VotePayload, aggregate_pubkeys_by_payload, aggregate_signatures, + verify_individual_votes, verify_votes_optimistic, + }, + stats::SigVerifyVoteStats, + }, agave_votor_messages::{ consensus_message::{Block, VoteMessage}, vote::Vote, @@ -11,13 +18,6 @@ use { criterion::{BatchSize, Criterion, criterion_group, criterion_main}, rayon::{ThreadPool, ThreadPoolBuilder}, solana_bls_signatures::{Keypair as BLSKeypair, PreparedHashedMessage, VerifySignature}, - solana_core::bls_sigverify::{ - bls_vote_sigverify::{ - VotePayload, aggregate_pubkeys_by_payload, aggregate_signatures, - verify_individual_votes, verify_votes_optimistic, - }, - stats::SigVerifyVoteStats, - }, solana_hash::Hash, solana_keypair::Keypair, solana_signer::Signer, diff --git a/core/src/bls_sigverify/bls_cert_sigverify.rs b/bls-sigverify/src/bls_cert_sigverify.rs similarity index 98% rename from core/src/bls_sigverify/bls_cert_sigverify.rs rename to bls-sigverify/src/bls_cert_sigverify.rs index 63aa9554024..1826eca4e63 100644 --- a/core/src/bls_sigverify/bls_cert_sigverify.rs +++ b/bls-sigverify/src/bls_cert_sigverify.rs @@ -1,6 +1,6 @@ use { super::{bls_sigverifier::BAN_TIMEOUT, errors::SigVerifyCertError, stats::SigVerifyCertStats}, - crate::bls_sigverify::{bls_sigverifier::NUM_SLOTS_FOR_VERIFY, utils::send_certs_to_pool}, + crate::{bls_sigverifier::NUM_SLOTS_FOR_VERIFY, utils::send_certs_to_pool}, agave_bls_cert_verify::cert_verify::Error as BlsCertVerifyError, agave_votor_messages::{ certificate::{Certificate, CertificateType}, @@ -8,6 +8,7 @@ use { fraction::Fraction, }, crossbeam_channel::Sender, + log::info, rayon::{ ThreadPool, iter::{IntoParallelIterator, ParallelIterator}, diff --git a/core/src/bls_sigverify/bls_sigverifier.rs b/bls-sigverify/src/bls_sigverifier.rs similarity index 95% rename from core/src/bls_sigverify/bls_sigverifier.rs rename to bls-sigverify/src/bls_sigverifier.rs index f07cdc96a25..0cd0fa02203 100644 --- a/core/src/bls_sigverify/bls_sigverifier.rs +++ b/bls-sigverify/src/bls_sigverifier.rs @@ -1,25 +1,24 @@ //! The BLS signature verifier. use { - super::{ + crate::{ bls_cert_sigverify::{CertPayload, verify_and_send_certificates}, bls_vote_sigverify::{VotePayload, verify_and_send_votes}, errors::SigVerifyError, + generated_cert_types::GeneratedCertTypes, + rewards::rewards_wants_vote, stats::SigVerifierStats, }, - crate::{ - block_creation_loop::rewards::{certs_builder::wants_vote, msg_types::AddVoteMessage}, - cluster_info_vote_listener::VerifiedVoterSlotsSender, - }, - agave_votor::{ - consensus_metrics::ConsensusMetricsEventSender, generated_cert_types::GeneratedCertTypes, - }, agave_votor_messages::{ + VerifiedVoterSlotsSender, certificate::CertificateType, consensus_message::{ConsensusMessage, SigVerifiedBatch, VoteMessage}, + metric_types::ConsensusMetricsEventSender, migration::MigrationStatus, + reward_certificate::AddVoteMessage, }, crossbeam_channel::{Receiver, RecvTimeoutError, Sender, TryRecvError}, + log::error, rayon::{ThreadPool, ThreadPoolBuilder}, solana_bls_signatures::pubkey::{PopVerified, PubkeyAffine as BlsPubkeyAffine}, solana_clock::Slot, @@ -50,26 +49,26 @@ pub(super) const NUM_SLOTS_FOR_VERIFY: Slot = 90_000; /// We ban the sender for 2 days which roughly corresponds to an epoch pub(super) const BAN_TIMEOUT: Duration = Duration::from_hours(48); -pub(crate) struct SigVerifierContext { - pub(crate) migration_status: Arc, - pub(crate) banlist: Arc, - pub(crate) sharable_banks: SharableBanks, - pub(crate) cluster_info: Arc, - pub(crate) leader_schedule: Arc, - pub(crate) num_threads: usize, - pub(crate) generated_cert_types: Arc, +pub struct SigVerifierContext { + pub migration_status: Arc, + pub banlist: Arc, + pub sharable_banks: SharableBanks, + pub cluster_info: Arc, + pub leader_schedule: Arc, + pub num_threads: usize, + pub generated_cert_types: Arc, } -pub(crate) struct SigVerifierChannels { - pub(crate) packet_receiver: Receiver, - pub(crate) channel_to_repair: VerifiedVoterSlotsSender, - pub(crate) channel_to_reward: Sender, - pub(crate) channel_to_pool: Sender, - pub(crate) channel_to_metrics: ConsensusMetricsEventSender, +pub struct SigVerifierChannels { + pub packet_receiver: Receiver, + pub channel_to_repair: VerifiedVoterSlotsSender, + pub channel_to_reward: Sender, + pub channel_to_pool: Sender, + pub channel_to_metrics: ConsensusMetricsEventSender, } /// Starts the BLS sigverifier service in its own dedicated thread. -pub(crate) fn spawn_service( +pub fn spawn_service( exit: Arc, context: SigVerifierContext, channels: SigVerifierChannels, @@ -286,7 +285,7 @@ impl SigVerifier { if vote.vote.slot() > root_slot { return ret; } - if wants_vote(&self.cluster_info, &self.leader_schedule, root_slot, vote) { + if rewards_wants_vote(&self.cluster_info, &self.leader_schedule, root_slot, vote) { return ret; } self.stats.num_old_votes_received += 1; @@ -332,14 +331,12 @@ fn recv_batches( mod tests { use { super::*, - crate::cluster_info_vote_listener::VerifiedVoterSlotsReceiver, - agave_votor::{ - consensus_metrics::ConsensusMetricsEventReceiver, - consensus_pool::certificate_builder::CertificateBuilder, - }, + agave_votor::consensus_pool::certificate_builder::CertificateBuilder, agave_votor_messages::{ + VerifiedVoterSlotsReceiver, certificate::{Certificate, CertificateType}, consensus_message::{Block, ConsensusMessage, VoteMessage}, + metric_types::ConsensusMetricsEventReceiver, vote::Vote, }, bitvec::prelude::{BitVec, Lsb0}, @@ -396,7 +393,7 @@ mod tests { .map(|_| ValidatorVoteKeypairs::new_rand()) .collect::>(); let stakes_vec = (0..validator_keypairs.len()) - .map(|i| 1_000 - i as u64) + .map(|i| 1_000u64.saturating_sub(i as u64)) .collect::>(); let mut genesis = create_genesis_config_with_alpenglow_vote_accounts( 1_000_000_000, @@ -529,8 +526,8 @@ mod tests { .verify_and_send_batches(messages_to_batches(&messages1)) .unwrap(); assert_eq!(ctx.pool_receiver.try_iter().count(), 2); - assert_eq!(ctx.verifier.stats.vote_stats.pool_sent, 1); - assert_eq!(ctx.verifier.stats.cert_stats.pool_sent, 1); + assert_eq!(ctx.verifier.stats.vote_stats.pool_sent.0, 1); + assert_eq!(ctx.verifier.stats.cert_stats.pool_sent.0, 1); let received_verified_votes1 = ctx.repair_receiver.try_recv().unwrap(); assert_eq!( received_verified_votes1, @@ -556,8 +553,8 @@ mod tests { .unwrap(); assert_eq!(ctx.pool_receiver.try_iter().count(), 1); - assert_eq!(ctx.verifier.stats.vote_stats.pool_sent, 1); - assert_eq!(ctx.verifier.stats.cert_stats.pool_sent, 0); + assert_eq!(ctx.verifier.stats.vote_stats.pool_sent.0, 1); + assert_eq!(ctx.verifier.stats.cert_stats.pool_sent.0, 0); let received_verified_votes2 = ctx.repair_receiver.try_recv().unwrap(); assert_eq!( received_verified_votes2, @@ -582,8 +579,8 @@ mod tests { .verify_and_send_batches(messages_to_batches(&messages3)) .unwrap(); assert_eq!(ctx.pool_receiver.try_iter().count(), 1); - assert_eq!(ctx.verifier.stats.vote_stats.pool_sent, 1); - assert_eq!(ctx.verifier.stats.cert_stats.pool_sent, 0); + assert_eq!(ctx.verifier.stats.vote_stats.pool_sent.0, 1); + assert_eq!(ctx.verifier.stats.cert_stats.pool_sent.0, 0); let received_verified_votes3 = ctx.repair_receiver.try_recv().unwrap(); assert_eq!( received_verified_votes3, @@ -603,9 +600,9 @@ mod tests { ctx.verifier .verify_and_send_batches(packet_batches) .unwrap(); - assert_eq!(ctx.verifier.stats.vote_stats.pool_sent, 0); - assert_eq!(ctx.verifier.stats.cert_stats.pool_sent, 0); - assert_eq!(ctx.verifier.stats.num_malformed_pkts, 1); + assert_eq!(ctx.verifier.stats.vote_stats.pool_sent.0, 0); + assert_eq!(ctx.verifier.stats.cert_stats.pool_sent.0, 0); + assert_eq!(ctx.verifier.stats.num_malformed_pkts.0, 1); // Expect no messages since the packet was malformed expect_no_receive(&ctx.pool_receiver); @@ -622,7 +619,7 @@ mod tests { .verify_and_send_batches(messages_to_batches(&messages_no_stakes)) .unwrap(); - assert_eq!(ctx.verifier.stats.discard_vote_no_epoch_stakes, 1); + assert_eq!(ctx.verifier.stats.discard_vote_no_epoch_stakes.0, 1); // Expect no messages since the packet was malformed expect_no_receive(&ctx.pool_receiver); @@ -636,7 +633,7 @@ mod tests { ctx.verifier .verify_and_send_batches(messages_to_batches(&messages_invalid_rank)) .unwrap(); - assert_eq!(ctx.verifier.stats.discard_vote_invalid_rank, 1); + assert_eq!(ctx.verifier.stats.discard_vote_invalid_rank.0, 1); // Expect no messages since the packet was malformed expect_no_receive(&ctx.pool_receiver); @@ -691,7 +688,7 @@ mod tests { assert_eq!(m2_recv, SigVerifiedBatch::Votes(vec![msg2])); // pool_sent counts every message that made it onto the channel, // whether via try_send or the blocking fallback. - assert_eq!(ctx.verifier.stats.vote_stats.pool_sent, 2); + assert_eq!(ctx.verifier.stats.vote_stats.pool_sent.0, 2); } #[test] @@ -732,8 +729,8 @@ mod tests { .verify_and_send_batches(packet_batches) .unwrap(); expect_no_receive(&ctx.pool_receiver); - assert_eq!(ctx.verifier.stats.vote_stats.pool_sent, 0); - assert_eq!(ctx.verifier.stats.num_discarded_pkts, 1); + assert_eq!(ctx.verifier.stats.vote_stats.pool_sent.0, 0); + assert_eq!(ctx.verifier.stats.num_discarded_pkts.0, 1); } #[test] @@ -1062,7 +1059,7 @@ mod tests { 0, "This certificate should be invalid" ); - assert_eq!(ctx.verifier.stats.cert_stats.stake_verification_failed, 1); + assert_eq!(ctx.verifier.stats.cert_stats.stake_verification_failed.0, 1); } #[test] @@ -1201,7 +1198,7 @@ mod tests { 0, "This certificate should be invalid" ); - assert_eq!(ctx.verifier.stats.cert_stats.stake_verification_failed, 1); + assert_eq!(ctx.verifier.stats.cert_stats.stake_verification_failed.0, 1); } #[test] @@ -1236,7 +1233,11 @@ mod tests { .unwrap(); expect_no_receive(&ctx.pool_receiver); assert_eq!( - ctx.verifier.stats.cert_stats.signature_verification_failed, + ctx.verifier + .stats + .cert_stats + .signature_verification_failed + .0, 1 ); } @@ -1320,8 +1321,8 @@ mod tests { assert_eq!(certs.len(), 1); } } - assert_eq!(ctx.verifier.stats.vote_stats.pool_sent, num_votes as u64); - assert_eq!(ctx.verifier.stats.cert_stats.pool_sent, 1); + assert_eq!(ctx.verifier.stats.vote_stats.pool_sent.0, num_votes as u64); + assert_eq!(ctx.verifier.stats.cert_stats.pool_sent.0, 1); } #[test] @@ -1345,7 +1346,7 @@ mod tests { .verify_and_send_batches(packet_batches) .unwrap(); expect_no_receive(&ctx.pool_receiver); - assert_eq!(ctx.verifier.stats.discard_vote_invalid_rank, 1); + assert_eq!(ctx.verifier.stats.discard_vote_invalid_rank.0, 1); } #[test] @@ -1416,7 +1417,7 @@ mod tests { .verify_and_send_batches(packet_batches_vote) .unwrap(); expect_no_receive(&message_receiver); - assert_eq!(sig_verifier.stats.num_old_votes_received, 1); + assert_eq!(sig_verifier.stats.num_old_votes_received.0, 1); let cert = create_signed_certificate_message( &validator_keypairs, @@ -1430,8 +1431,8 @@ mod tests { .verify_and_send_batches(packet_batches_cert) .unwrap(); expect_no_receive(&message_receiver); - assert_eq!(sig_verifier.stats.num_old_certs_received, 1); - assert_eq!(sig_verifier.stats.num_old_votes_received, 1); + assert_eq!(sig_verifier.stats.num_old_certs_received.0, 1); + assert_eq!(sig_verifier.stats.num_old_votes_received.0, 1); } #[test] @@ -1473,8 +1474,8 @@ mod tests { .unwrap(); assert_eq!(ctx.pool_receiver.try_iter().count(), 1); - assert_eq!(ctx.verifier.stats.num_verified_certs_received, 0); - assert_eq!(ctx.verifier.stats.cert_stats.certs_to_sig_verify, 1); + assert_eq!(ctx.verifier.stats.num_verified_certs_received.0, 0); + assert_eq!(ctx.verifier.stats.cert_stats.certs_to_sig_verify.0, 1); vote_messages.pop(); // Remove one signature let mut builder2 = CertificateBuilder::new(cert_type); @@ -1490,8 +1491,8 @@ mod tests { .verify_and_send_batches(packet_batches2) .unwrap(); expect_no_receive(&ctx.pool_receiver); - assert_eq!(ctx.verifier.stats.num_verified_certs_received, 1); - assert_eq!(ctx.verifier.stats.cert_stats.certs_to_sig_verify, 0); + assert_eq!(ctx.verifier.stats.num_verified_certs_received.0, 1); + assert_eq!(ctx.verifier.stats.cert_stats.certs_to_sig_verify.0, 0); } #[test] @@ -1648,7 +1649,7 @@ mod tests { ctx.verifier .verify_and_send_batches(packet_batches) .unwrap(); - assert_eq!(ctx.verifier.stats.num_generated_certs_received, 1); + assert_eq!(ctx.verifier.stats.num_generated_certs_received.0, 1); } #[test] @@ -1671,8 +1672,8 @@ mod tests { ctx.verifier .verify_and_send_batches(packet_batches) .unwrap(); - assert_eq!(ctx.verifier.stats.cert_stats.too_far_in_future, 1); - assert_eq!(ctx.verifier.stats.vote_stats.too_far_in_future, 1); + assert_eq!(ctx.verifier.stats.cert_stats.too_far_in_future.0, 1); + assert_eq!(ctx.verifier.stats.vote_stats.too_far_in_future.0, 1); } fn messages_to_batches(messages: &[ConsensusMessage]) -> Vec { diff --git a/core/src/bls_sigverify/bls_vote_sigverify.rs b/bls-sigverify/src/bls_vote_sigverify.rs similarity index 95% rename from core/src/bls_sigverify/bls_vote_sigverify.rs rename to bls-sigverify/src/bls_vote_sigverify.rs index 6974e7867cf..3da3ea3f707 100644 --- a/core/src/bls_sigverify/bls_vote_sigverify.rs +++ b/bls-sigverify/src/bls_vote_sigverify.rs @@ -1,22 +1,22 @@ #[cfg(feature = "dev-context-only-utils")] use qualifier_attr::qualifiers; use { - super::{errors::SigVerifyVoteError, stats::SigVerifyVoteStats}, crate::{ - block_creation_loop::rewards::msg_types::AddVoteMessage, - bls_sigverify::{ - bls_sigverifier::{BAN_TIMEOUT, NUM_SLOTS_FOR_VERIFY, SigVerifierChannels}, - utils::{ - send_votes_to_metrics, send_votes_to_pool, send_votes_to_repair, - send_votes_to_rewards, - }, + bls_sigverifier::{BAN_TIMEOUT, NUM_SLOTS_FOR_VERIFY, SigVerifierChannels}, + errors::SigVerifyVoteError, + rewards::rewards_wants_vote, + stats::SigVerifyVoteStats, + utils::{ + send_votes_to_metrics, send_votes_to_pool, send_votes_to_repair, send_votes_to_rewards, }, }, - agave_votor::consensus_metrics::ConsensusMetricsEvent, agave_votor_messages::{ consensus_message::{SigVerifiedBatch, VoteMessage}, + metric_types::ConsensusMetricsEvent, + reward_certificate::AddVoteMessage, vote::Vote, }, + log::info, rayon::{ ThreadPool, current_thread_index, iter::{Either, IntoParallelIterator, IntoParallelRefIterator, ParallelIterator}, @@ -139,7 +139,7 @@ fn process_verified_votes( let mut votes_for_pool = Vec::with_capacity(verified_votes.len()); let mut votes_for_metrics = Vec::with_capacity(verified_votes.len()); for payload in verified_votes { - if crate::block_creation_loop::rewards::certs_builder::wants_vote( + if rewards_wants_vote( cluster_info, leader_schedule, root_bank.slot(), @@ -191,8 +191,8 @@ fn verify_votes( v.vote_message.vote.slot() <= root_bank.slot().saturating_add(NUM_SLOTS_FOR_VERIFY) }) .collect::>(); - let num_discarded = len_before - votes_to_verify.len(); - stats.too_far_in_future = stats.too_far_in_future.saturating_add(num_discarded as u64); + let num_discarded = len_before.saturating_sub(votes_to_verify.len()); + stats.too_far_in_future += num_discarded as u64; // Try optimistic verification - fast to verify, but cannot identify invalid votes let (optimistic_result, distinct_votes, distinct_payloads) = diff --git a/core/src/bls_sigverify/errors.rs b/bls-sigverify/src/errors.rs similarity index 100% rename from core/src/bls_sigverify/errors.rs rename to bls-sigverify/src/errors.rs diff --git a/votor/src/generated_cert_types.rs b/bls-sigverify/src/generated_cert_types.rs similarity index 76% rename from votor/src/generated_cert_types.rs rename to bls-sigverify/src/generated_cert_types.rs index 639076c6d9f..666e5ef6577 100644 --- a/votor/src/generated_cert_types.rs +++ b/bls-sigverify/src/generated_cert_types.rs @@ -1,5 +1,3 @@ -#[cfg(feature = "dev-context-only-utils")] -use qualifier_attr::qualifiers; use { agave_votor_messages::certificate::CertificateType, parking_lot::RwLock, solana_clock::Slot, std::collections::HashSet, @@ -17,13 +15,12 @@ impl GeneratedCertTypes { } /// Inserts the `cert_type` into the container. - #[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))] - pub(crate) fn insert_cert(&self, cert_type: CertificateType) { + pub fn insert_cert(&self, cert_type: CertificateType) { self.0.write().insert(cert_type); } /// Prunes the container, it drops all certs older than `root_slot` as they are no longer needed. - pub(crate) fn prune(&self, root_slot: Slot) { + pub fn prune(&self, root_slot: Slot) { self.0.write().retain(|c| c.slot() >= root_slot); } } diff --git a/core/src/bls_sigverify/mod.rs b/bls-sigverify/src/lib.rs similarity index 57% rename from core/src/bls_sigverify/mod.rs rename to bls-sigverify/src/lib.rs index d214c96dd67..bcf4d8fd13b 100644 --- a/core/src/bls_sigverify/mod.rs +++ b/bls-sigverify/src/lib.rs @@ -1,6 +1,10 @@ +#![cfg(feature = "agave-unstable-api")] + pub mod bls_cert_sigverify; pub mod bls_sigverifier; pub mod bls_vote_sigverify; mod errors; +pub mod generated_cert_types; +pub mod rewards; pub mod stats; mod utils; diff --git a/bls-sigverify/src/rewards.rs b/bls-sigverify/src/rewards.rs new file mode 100644 index 00000000000..47ee5c2a2c2 --- /dev/null +++ b/bls-sigverify/src/rewards.rs @@ -0,0 +1,39 @@ +use { + agave_votor_messages::{ + consensus_message::VoteMessage, reward_certificate::NUM_SLOTS_FOR_REWARD, vote::Vote, + }, + solana_clock::Slot, + solana_gossip::cluster_info::ClusterInfo, + solana_ledger::leader_schedule_cache::LeaderScheduleCache, +}; + +/// Returns [`false`] if the rewards container is not interested in the [`VoteMessage`]. +/// Returns [`true`] if the rewards container might be interested in the [`VoteMessage`]. +pub fn rewards_wants_vote( + cluster_info: &ClusterInfo, + leader_schedule: &LeaderScheduleCache, + root_slot: Slot, + vote: &VoteMessage, +) -> bool { + match vote.vote { + Vote::Notarize(_) | Vote::Skip(_) => (), + Vote::Finalize(_) + | Vote::NotarizeFallback(_) + | Vote::SkipFallback(_) + | Vote::Genesis(_) => return false, + } + let vote_slot = vote.vote.slot(); + if vote_slot.saturating_add(NUM_SLOTS_FOR_REWARD) <= root_slot { + return false; + } + let my_pubkey = cluster_info.id(); + let Some(leader) = + leader_schedule.slot_leader_at(vote_slot.saturating_add(NUM_SLOTS_FOR_REWARD), None) + else { + return false; + }; + if leader.id != my_pubkey { + return false; + } + true +} diff --git a/core/src/bls_sigverify/stats.rs b/bls-sigverify/src/stats.rs similarity index 75% rename from core/src/bls_sigverify/stats.rs rename to bls-sigverify/src/stats.rs index 9a787f448a1..03328df4a55 100644 --- a/core/src/bls_sigverify/stats.rs +++ b/bls-sigverify/src/stats.rs @@ -3,7 +3,11 @@ use qualifier_attr::qualifiers; use { agave_math_utils::welford_stats::WelfordStats, solana_clock::Slot, - std::time::{Duration, Instant}, + solana_metrics::datapoint_info, + std::{ + num::Saturating, + time::{Duration, Instant}, + }, }; /// Max number of root slots to wait before triggering reporting of stats. @@ -30,7 +34,8 @@ impl Reporting { /// Returns `true` if reporting should be done else `false`. fn should_report(&self, root_slot: Slot) -> bool { - root_slot >= self.slot + SLOTS_INTERVAL || self.time.elapsed() > DURATION_INTERVAL + root_slot >= self.slot.saturating_add(SLOTS_INTERVAL) + || self.time.elapsed() > DURATION_INTERVAL } } @@ -48,21 +53,21 @@ pub(super) struct SigVerifierStats { /// Number of packets received. pub(super) num_pkts: WelfordStats, /// Number of discarded packets received from the streamer. - pub(super) num_discarded_pkts: u64, + pub(super) num_discarded_pkts: Saturating, /// Number of times we failed to deserialize a packet. - pub(super) num_malformed_pkts: u64, + pub(super) num_malformed_pkts: Saturating, /// Number of votes discarded due to an invalid rank. - pub(super) discard_vote_invalid_rank: u64, + pub(super) discard_vote_invalid_rank: Saturating, /// Number of votes discarded due to no epoch stakes. - pub(super) discard_vote_no_epoch_stakes: u64, + pub(super) discard_vote_no_epoch_stakes: Saturating, /// Number of outdated votes received. - pub(super) num_old_votes_received: u64, + pub(super) num_old_votes_received: Saturating, /// Number of outdated certs received. - pub(super) num_old_certs_received: u64, + pub(super) num_old_certs_received: Saturating, /// Number of already verified certs received. - pub(super) num_verified_certs_received: u64, + pub(super) num_verified_certs_received: Saturating, /// Number of certs received that the node has already generated. - pub(super) num_generated_certs_received: u64, + pub(super) num_generated_certs_received: Saturating, /// Last time the stats were reported. pub(super) last_report: Reporting, } @@ -74,14 +79,14 @@ impl SigVerifierStats { cert_stats: SigVerifyCertStats::default(), extract_filter_msgs_us: WelfordStats::default(), num_pkts: WelfordStats::default(), - discard_vote_invalid_rank: 0, - num_discarded_pkts: 0, - num_malformed_pkts: 0, - discard_vote_no_epoch_stakes: 0, - num_old_votes_received: 0, - num_old_certs_received: 0, - num_verified_certs_received: 0, - num_generated_certs_received: 0, + discard_vote_invalid_rank: Saturating(0), + num_discarded_pkts: Saturating(0), + num_malformed_pkts: Saturating(0), + discard_vote_no_epoch_stakes: Saturating(0), + num_old_votes_received: Saturating(0), + num_old_certs_received: Saturating(0), + num_verified_certs_received: Saturating(0), + num_generated_certs_received: Saturating(0), verify_and_send_batch_us: WelfordStats::default(), last_report: Reporting::new(root_slot), } @@ -131,26 +136,30 @@ impl SigVerifierStats { extract_filter_msgs_us.mean().unwrap_or(0), i64 ), - ("discard_vote_invalid_rank", *discard_vote_invalid_rank, i64), - ("num_discarded_pkts", *num_discarded_pkts, i64), - ("num_old_votes_received", *num_old_votes_received, i64), + ( + "discard_vote_invalid_rank", + discard_vote_invalid_rank.0, + i64 + ), + ("num_discarded_pkts", num_discarded_pkts.0, i64), + ("num_old_votes_received", num_old_votes_received.0, i64), ( "num_verified_certs_received", - *num_verified_certs_received, + num_verified_certs_received.0, i64 ), ( "num_generated_certs_received", - *num_generated_certs_received, + num_generated_certs_received.0, i64 ), ( "discard_vote_no_epoch_stakes", - *discard_vote_no_epoch_stakes, + discard_vote_no_epoch_stakes.0, i64 ), - ("num_malformed_pkts", *num_malformed_pkts, i64), - ("num_old_certs_received", *num_old_certs_received, i64), + ("num_malformed_pkts", num_malformed_pkts.0, i64), + ("num_old_certs_received", num_old_certs_received.0, i64), ( "verify_and_send_batch_us_max", verify_and_send_batch_us.maximum().unwrap_or(0), @@ -177,28 +186,28 @@ impl SigVerifierStats { #[derive(Default, Debug)] pub(super) struct SigVerifyCertStats { /// Number of certs [`verify_and_send_certificates`] was requested to verify the signature of. - pub(super) certs_to_sig_verify: u64, + pub(super) certs_to_sig_verify: Saturating, /// Number of certs [`verify_and_send_certificates`] successfully verified the signature of. - pub(super) sig_verified_certs: u64, + pub(super) sig_verified_certs: Saturating, /// Number of certs that were verified unnecessarily because another cert of the same /// `CertificateType` was already verified. - pub(super) unnecessary_certs_verified: u64, + pub(super) unnecessary_certs_verified: Saturating, /// Number of times we are banning a validator that was already banned. - pub(super) already_banned: u64, + pub(super) already_banned: Saturating, /// Number of times stake verification failed on a cert. - pub(super) stake_verification_failed: u64, + pub(super) stake_verification_failed: Saturating, /// Number of times signature verification failed on a cert. - pub(super) signature_verification_failed: u64, + pub(super) signature_verification_failed: Saturating, /// Number of times the cert was too far in the future and discarded. - pub(super) too_far_in_future: u64, + pub(super) too_far_in_future: Saturating, /// How many messages are outstanding on the channel. - pub(super) pool_outstanding_msgs: u64, + pub(super) pool_outstanding_msgs: Saturating, /// Number of votes sent successfully over the channel to consensus pool. - pub(super) pool_sent: u64, + pub(super) pool_sent: Saturating, /// Number of times the channel to consensus pool was full and we resorted to blocking send. - pub(super) pool_channel_full: u64, + pub(super) pool_channel_full: Saturating, /// Stats for [`verify_and_send_certificates`]. pub(super) fn_verify_and_send_certs_stats: WelfordStats, @@ -250,24 +259,28 @@ impl SigVerifyCertStats { datapoint_info!( "bls_cert_sigverify_stats", - ("certs_to_sig_verify", *certs_to_sig_verify, i64), - ("sig_verified_certs", *sig_verified_certs, i64), + ("certs_to_sig_verify", certs_to_sig_verify.0, i64), + ("sig_verified_certs", sig_verified_certs.0, i64), ( "unnecessary_certs_verified", - *unnecessary_certs_verified, + unnecessary_certs_verified.0, + i64 + ), + ("already_banned", already_banned.0, i64), + ( + "stake_verification_failed", + stake_verification_failed.0, i64 ), - ("already_banned", *already_banned, i64), - ("stake_verification_failed", *stake_verification_failed, i64), ( "signature_verification_failed", - *signature_verification_failed, + signature_verification_failed.0, i64 ), - ("too_far_in_future", *too_far_in_future, i64), - ("pool_outstanding_msgs", *pool_outstanding_msgs, i64), - ("pool_sent", *pool_sent, i64), - ("pool_channel_full", *pool_channel_full, i64), + ("too_far_in_future", too_far_in_future.0, i64), + ("pool_outstanding_msgs", pool_outstanding_msgs.0, i64), + ("pool_sent", pool_sent.0, i64), + ("pool_channel_full", pool_channel_full.0, i64), ( "fn_verify_and_send_certs_count", fn_verify_and_send_certs_stats.count(), @@ -287,33 +300,33 @@ impl SigVerifyCertStats { #[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))] pub(super) struct SigVerifyVoteStats { /// Number of votes [`verify_and_send_votes`] was requested to verify the signature of. - pub(super) votes_to_sig_verify: u64, + pub(super) votes_to_sig_verify: Saturating, /// Number of votes [`verify_and_send_votes`] successfully verified the signature of. - pub(super) sig_verified_votes: u64, + pub(super) sig_verified_votes: Saturating, /// Number of times the cert was too far in the future and discarded. - pub(super) too_far_in_future: u64, + pub(super) too_far_in_future: Saturating, /// Number of times we are banning a validator that was already banned. - pub(super) already_banned: u64, + pub(super) already_banned: Saturating, /// Number of votes sent successfully over the channel to metrics. - pub(super) metrics_sent: u64, + pub(super) metrics_sent: Saturating, /// Number of times the channel to metrics was full. - pub(super) metrics_channel_full: u64, + pub(super) metrics_channel_full: Saturating, /// Number of votes sent successfully over the channel to rewards. - pub(super) rewards_sent: u64, + pub(super) rewards_sent: Saturating, /// Number of times the channel to rewards was full. - pub(super) rewards_channel_full: u64, + pub(super) rewards_channel_full: Saturating, /// How many messages are outstanding on the channel. - pub(super) pool_outstanding_msgs: u64, + pub(super) pool_outstanding_msgs: Saturating, /// Number of votes sent successfully over the channel to consensus pool. - pub(super) pool_sent: u64, + pub(super) pool_sent: Saturating, /// Number of times the channel to consensus pool was full and we resorted to blocking send. - pub(super) pool_channel_full: u64, + pub(super) pool_channel_full: Saturating, /// Number of votes sent successfully over the channel to repair. - pub(super) repair_sent: u64, + pub(super) repair_sent: Saturating, /// Number of times the channel to repair was full. - pub(super) repair_channel_full: u64, + pub(super) repair_channel_full: Saturating, /// Stats for [`verify_and_send_votes`]. pub(super) fn_verify_and_send_votes_stats: WelfordStats, @@ -391,19 +404,19 @@ impl SigVerifyVoteStats { } = self; datapoint_info!( "bls_vote_sigverify_stats", - ("votes_to_sig_verify", *votes_to_sig_verify, i64), - ("sig_verified_votes", *sig_verified_votes, i64), - ("too_far_in_future", *too_far_in_future, i64), - ("already_banned", *already_banned, i64), - ("metrics_sent", *metrics_sent, i64), - ("metrics_channel_full", *metrics_channel_full, i64), - ("rewards_sent", *rewards_sent, i64), - ("rewards_channel_full", *rewards_channel_full, i64), - ("repair_sent", *repair_sent, i64), - ("repair_channel_full", *repair_channel_full, i64), - ("pool_outstanding_msgs", *pool_outstanding_msgs, i64), - ("pool_sent", *pool_sent, i64), - ("pool_channel_full", *pool_channel_full, i64), + ("votes_to_sig_verify", votes_to_sig_verify.0, i64), + ("sig_verified_votes", sig_verified_votes.0, i64), + ("too_far_in_future", too_far_in_future.0, i64), + ("already_banned", already_banned.0, i64), + ("metrics_sent", metrics_sent.0, i64), + ("metrics_channel_full", metrics_channel_full.0, i64), + ("rewards_sent", rewards_sent.0, i64), + ("rewards_channel_full", rewards_channel_full.0, i64), + ("repair_sent", repair_sent.0, i64), + ("repair_channel_full", repair_channel_full.0, i64), + ("pool_outstanding_msgs", pool_outstanding_msgs.0, i64), + ("pool_sent", pool_sent.0, i64), + ("pool_channel_full", pool_channel_full.0, i64), ( "fn_verify_and_send_votes_count", fn_verify_and_send_votes_stats.count(), diff --git a/core/src/bls_sigverify/utils.rs b/bls-sigverify/src/utils.rs similarity index 87% rename from core/src/bls_sigverify/utils.rs rename to bls-sigverify/src/utils.rs index ef88b89fc4d..565f8fc9b1e 100644 --- a/core/src/bls_sigverify/utils.rs +++ b/bls-sigverify/src/utils.rs @@ -1,16 +1,19 @@ use { - super::{errors::SigVerifyVoteError, stats::SigVerifyVoteStats}, crate::{ - block_creation_loop::rewards::msg_types::AddVoteMessage, - bls_sigverify::{errors::SigVerifyCertError, stats::SigVerifyCertStats}, - cluster_info_vote_listener::VerifiedVoterSlotsSender, + errors::{SigVerifyCertError, SigVerifyVoteError}, + stats::{SigVerifyCertStats, SigVerifyVoteStats}, + }, + agave_votor_messages::{ + VerifiedVoterSlotsSender, + consensus_message::SigVerifiedBatch, + metric_types::{ConsensusMetricsEvent, ConsensusMetricsEventSender}, + reward_certificate::AddVoteMessage, }, - agave_votor::consensus_metrics::{ConsensusMetricsEvent, ConsensusMetricsEventSender}, - agave_votor_messages::consensus_message::SigVerifiedBatch, crossbeam_channel::{Sender, TrySendError}, + log::{error, info}, solana_clock::Slot, solana_pubkey::Pubkey, - std::{collections::HashMap, time::Instant}, + std::{collections::HashMap, num::Saturating, time::Instant}, }; pub(super) fn send_votes_to_metrics( @@ -66,7 +69,7 @@ pub(super) fn send_votes_to_pool( match channel.try_send(batch) { Ok(()) => { stats.pool_sent += len as u64; - stats.pool_outstanding_msgs = channel.len() as u64; + stats.pool_outstanding_msgs = Saturating(channel.len() as u64); Ok(()) } Err(TrySendError::Full(msgs)) => { @@ -122,7 +125,7 @@ pub(super) fn send_certs_to_pool( match channel_to_pool.try_send(batch) { Ok(()) => { stats.pool_sent += len as u64; - stats.pool_outstanding_msgs = channel_to_pool.len() as u64; + stats.pool_outstanding_msgs = Saturating(channel_to_pool.len() as u64); Ok(()) } Err(TrySendError::Full(msgs)) => { diff --git a/core/Cargo.toml b/core/Cargo.toml index cef346f4b52..3a8b2524d70 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -43,6 +43,7 @@ frozen-abi = [ [dependencies] agave-banking-stage-ingress-types = { workspace = true } agave-bls-cert-verify = { workspace = true } +agave-bls-sigverify = { workspace = true } agave-feature-set = { workspace = true } agave-fs = { workspace = true } agave-logger = { workspace = true } @@ -224,10 +225,6 @@ jemallocator = { workspace = true } name = "sigverify_stage" harness = false -[[bench]] -name = "bls_vote_sigverify" -harness = false - [[bench]] name = "shredder" harness = false diff --git a/core/src/block_creation_loop.rs b/core/src/block_creation_loop.rs index e752ee135b9..7793b319843 100644 --- a/core/src/block_creation_loop.rs +++ b/core/src/block_creation_loop.rs @@ -8,8 +8,7 @@ use { crate::{ banking_trace::{BankingPacketSender, BankingTracer}, block_creation_loop::rewards::{ - certs_requestor::CertsRequestor, - msg_types::{AddVoteMessage, RewardRespSucc}, + certs_requestor::CertsRequestor, msg_types::RewardRespSucc, reward_certs_service::RewardCertsService, }, replay_stage::{Finalizer, ReplayStage}, @@ -17,7 +16,7 @@ use { agave_votor::event::LeaderWindowInfo, agave_votor_messages::{ consensus_message::Block, - reward_certificate::{NotarRewardCertificate, SkipRewardCertificate}, + reward_certificate::{AddVoteMessage, NotarRewardCertificate, SkipRewardCertificate}, }, crossbeam_channel::{Receiver, Sender, select_biased}, solana_clock::Slot, diff --git a/core/src/block_creation_loop/rewards/certs_builder.rs b/core/src/block_creation_loop/rewards/certs_builder.rs index 4c3b88db754..5187d0f453c 100644 --- a/core/src/block_creation_loop/rewards/certs_builder.rs +++ b/core/src/block_creation_loop/rewards/certs_builder.rs @@ -2,10 +2,10 @@ use { crate::block_creation_loop::rewards::msg_types::{ RewardRequest, RewardRespSucc, RewardResponse, }, + agave_bls_sigverify::rewards::rewards_wants_vote, agave_votor_messages::{ consensus_message::VoteMessage, reward_certificate::{BuildRewardCertsRespError, NUM_SLOTS_FOR_REWARD}, - vote::Vote, }, crossbeam_channel::RecvError, entry::Entry, @@ -18,37 +18,6 @@ use { mod entry; -/// Returns [`false`] if the rewards container is not interested in the [`VoteMessage`]. -/// Returns [`true`] if the rewards container might be interested in the [`VoteMessage`]. -pub fn wants_vote( - cluster_info: &ClusterInfo, - leader_schedule: &LeaderScheduleCache, - root_slot: Slot, - vote: &VoteMessage, -) -> bool { - match vote.vote { - Vote::Notarize(_) | Vote::Skip(_) => (), - Vote::Finalize(_) - | Vote::NotarizeFallback(_) - | Vote::SkipFallback(_) - | Vote::Genesis(_) => return false, - } - let vote_slot = vote.vote.slot(); - if vote_slot.saturating_add(NUM_SLOTS_FOR_REWARD) <= root_slot { - return false; - } - let my_pubkey = cluster_info.id(); - let Some(leader) = - leader_schedule.slot_leader_at(vote_slot.saturating_add(NUM_SLOTS_FOR_REWARD), None) - else { - return false; - }; - if leader.id != my_pubkey { - return false; - } - true -} - /// Container to store state needed to generate reward certificates. pub(super) struct CertsBuilder { /// Per [`Slot`], stores skip and notar votes. @@ -118,7 +87,7 @@ impl CertsBuilder { /// Returns [`true`] if the rewards container is interested in this vote else [`false`]. fn wants_vote(&self, root_slot: Slot, vote: &VoteMessage) -> bool { - if !wants_vote(&self.cluster_info, &self.leader_schedule, root_slot, vote) { + if !rewards_wants_vote(&self.cluster_info, &self.leader_schedule, root_slot, vote) { return false; } let Some(entry) = self.votes.get(&vote.vote.slot()) else { diff --git a/core/src/block_creation_loop/rewards/msg_types.rs b/core/src/block_creation_loop/rewards/msg_types.rs index e57776bd311..901e4ebb600 100644 --- a/core/src/block_creation_loop/rewards/msg_types.rs +++ b/core/src/block_creation_loop/rewards/msg_types.rs @@ -1,9 +1,6 @@ use { - agave_votor_messages::{ - consensus_message::VoteMessage, - reward_certificate::{ - BuildRewardCertsRespError, NotarRewardCertificate, SkipRewardCertificate, - }, + agave_votor_messages::reward_certificate::{ + BuildRewardCertsRespError, NotarRewardCertificate, SkipRewardCertificate, }, crossbeam_channel::{Receiver, Sender}, solana_clock::Slot, @@ -36,13 +33,6 @@ pub(crate) struct RewardResponse { pub(crate) result: Result, } -/// Message to add votes to the rewards container. -#[derive(Debug)] -pub struct AddVoteMessage { - /// List of [`VoteMessage`]s. - pub(crate) votes: Vec, -} - /// The request token to be used to receive responses on previously sent requests to build reward certs. pub(crate) struct RewardRequestToken { pub(super) reply_receiver: Receiver, diff --git a/core/src/block_creation_loop/rewards/reward_certs_service.rs b/core/src/block_creation_loop/rewards/reward_certs_service.rs index c642cdd0661..23d028a365b 100644 --- a/core/src/block_creation_loop/rewards/reward_certs_service.rs +++ b/core/src/block_creation_loop/rewards/reward_certs_service.rs @@ -1,12 +1,11 @@ use { crate::{ block_creation_loop::rewards::{ - certs_builder::CertsBuilder, - certs_requestor::CertsRequestor, - msg_types::{AddVoteMessage, RewardRequest}, + certs_builder::CertsBuilder, certs_requestor::CertsRequestor, msg_types::RewardRequest, }, tvu::MAX_ALPENGLOW_PACKET_NUM, }, + agave_votor_messages::reward_certificate::AddVoteMessage, crossbeam_channel::{Receiver, Sender, bounded, select_biased}, solana_gossip::cluster_info::ClusterInfo, solana_ledger::leader_schedule_cache::LeaderScheduleCache, diff --git a/core/src/cluster_info_vote_listener.rs b/core/src/cluster_info_vote_listener.rs index 1b41eb3ac48..924a4f96752 100644 --- a/core/src/cluster_info_vote_listener.rs +++ b/core/src/cluster_info_vote_listener.rs @@ -8,7 +8,7 @@ use { sigverify_stage::GossipSigVerifyHandle, }, agave_banking_stage_ingress_types::BankingPacketBatch, - agave_votor_messages::migration::MigrationStatus, + agave_votor_messages::{VerifiedVoterSlotsSender, migration::MigrationStatus}, crossbeam_channel::{Receiver, RecvTimeoutError, Select, Sender, unbounded}, log::*, solana_clock::{BankId, Slot}, @@ -56,11 +56,6 @@ use { pub type ThresholdConfirmedSlots = Vec<(Slot, Hash)>; pub type VerifiedVoteTransactionsSender = Sender>; pub type VerifiedVoteTransactionsReceiver = Receiver>; -// Send side of verified voter channel. -// Each message contains the Pubkey of the voter and the slots in last verified vote. -pub type VerifiedVoterSlotsSender = Sender<(Pubkey, Vec)>; -// Receive side of verified voter channel. -pub type VerifiedVoterSlotsReceiver = Receiver<(Pubkey, Vec)>; pub type GossipVerifiedVoteHashSender = Sender<(Pubkey, Slot, Hash)>; pub type GossipVerifiedVoteHashReceiver = Receiver<(Pubkey, Slot, Hash)>; pub type DuplicateConfirmedSlotsSender = Sender; diff --git a/core/src/lib.rs b/core/src/lib.rs index b3f6a506a2b..51801573b35 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -13,7 +13,6 @@ pub mod banking_simulation; pub mod banking_stage; pub mod banking_trace; pub(crate) mod block_creation_loop; -pub mod bls_sigverify; pub mod cluster_info_vote_listener; pub mod cluster_slots_service; pub mod commitment_service; diff --git a/core/src/repair/repair_service.rs b/core/src/repair/repair_service.rs index d3e1c037871..cfc915f9655 100644 --- a/core/src/repair/repair_service.rs +++ b/core/src/repair/repair_service.rs @@ -3,7 +3,6 @@ use { super::standard_repair_handler::StandardRepairHandler, crate::{ - cluster_info_vote_listener::VerifiedVoterSlotsReceiver, cluster_slots_service::cluster_slots::ClusterSlots, repair::{ ancestor_hashes_service::{ @@ -18,7 +17,7 @@ use { }, }, }, - agave_votor_messages::migration::MigrationStatus, + agave_votor_messages::{VerifiedVoterSlotsReceiver, migration::MigrationStatus}, crossbeam_channel::{Receiver as CrossbeamReceiver, Sender as CrossbeamSender}, lazy_lru::LruCache, rand::prelude::IndexedRandom as _, diff --git a/core/src/tpu.rs b/core/src/tpu.rs index 9400d1009a0..303ded32851 100644 --- a/core/src/tpu.rs +++ b/core/src/tpu.rs @@ -11,7 +11,7 @@ use { banking_trace::{Channels, TracerThread}, cluster_info_vote_listener::{ ClusterInfoVoteListener, DuplicateConfirmedSlotsSender, GossipVerifiedVoteHashSender, - VerifiedVoterSlotsSender, VoteTracker, + VoteTracker, }, fetch_stage::FetchStage, forwarding_stage::{ @@ -25,6 +25,7 @@ use { }, agave_banking_stage_ingress_types::SchedulerPriorityFloor, agave_votor::event::VotorEventSender, + agave_votor_messages::VerifiedVoterSlotsSender, agave_xdp::transmitter::XdpSender, crossbeam_channel::{Receiver, bounded, unbounded}, solana_clock::Slot, diff --git a/core/src/tvu.rs b/core/src/tvu.rs index 604cf319f53..b3a49b6fb8c 100644 --- a/core/src/tvu.rs +++ b/core/src/tvu.rs @@ -5,11 +5,9 @@ use { crate::{ admin_rpc_post_init::{KeyUpdaterType, KeyUpdaters}, banking_trace::BankingTracer, - block_creation_loop::{ReplayHighestFrozen, rewards::msg_types::AddVoteMessage}, - bls_sigverify::bls_sigverifier::{self, SigVerifierChannels, SigVerifierContext}, + block_creation_loop::ReplayHighestFrozen, cluster_info_vote_listener::{ - DuplicateConfirmedSlotsReceiver, GossipVerifiedVoteHashReceiver, - VerifiedVoterSlotsReceiver, VerifiedVoterSlotsSender, VoteTracker, + DuplicateConfirmedSlotsReceiver, GossipVerifiedVoteHashReceiver, VoteTracker, }, cluster_slots_service::{ClusterSlotsService, cluster_slots::ClusterSlots}, commitment_service::AggregateCommitmentService, @@ -28,16 +26,21 @@ use { warm_quic_cache_service::WarmQuicCacheService, window_service::{WindowService, WindowServiceChannels}, }, + agave_bls_sigverify::{ + bls_sigverifier::{self, SigVerifierChannels, SigVerifierContext}, + generated_cert_types::GeneratedCertTypes, + }, agave_votor::{ - consensus_metrics::MAX_IN_FLIGHT_CONSENSUS_EVENTS, event::{LatestSwitchRequest, LeaderWindowInfo, VotorEventReceiver, VotorEventSender}, - generated_cert_types::GeneratedCertTypes, vote_history::VoteHistory, vote_history_storage::VoteHistoryStorage, voting_service::{VotingService as BLSVotingService, VotingServiceOverride}, votor::{Votor, VotorConfig}, }, - agave_votor_messages::consensus_message::Block, + agave_votor_messages::{ + VerifiedVoterSlotsReceiver, VerifiedVoterSlotsSender, consensus_message::Block, + metric_types::MAX_IN_FLIGHT_CONSENSUS_EVENTS, reward_certificate::AddVoteMessage, + }, crossbeam_channel::{Receiver, Sender, bounded, unbounded}, solana_client::connection_cache::ConnectionCache, solana_clock::Slot, diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index bc229946b8b..34bd0e5c8f5 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -76,6 +76,33 @@ dependencies = [ "wincode", ] +[[package]] +name = "agave-bls-sigverify" +version = "4.2.0-alpha.0" +dependencies = [ + "agave-bls-cert-verify", + "agave-math-utils", + "agave-votor-messages", + "bitvec", + "crossbeam-channel", + "log", + "parking_lot 0.12.5", + "qualifier_attr", + "rayon", + "solana-bls-signatures", + "solana-clock", + "solana-gossip", + "solana-ledger", + "solana-measure", + "solana-metrics", + "solana-pubkey 4.2.0", + "solana-runtime", + "solana-signer-store", + "solana-streamer", + "thiserror 2.0.18", + "wincode", +] + [[package]] name = "agave-cpu-utils" version = "4.2.0-alpha.0" @@ -353,6 +380,7 @@ dependencies = [ name = "agave-votor" version = "4.2.0-alpha.0" dependencies = [ + "agave-bls-sigverify", "agave-logger", "agave-math-utils", "agave-votor-messages", @@ -400,6 +428,7 @@ name = "agave-votor-messages" version = "4.2.0-alpha.0" dependencies = [ "agave-feature-set", + "crossbeam-channel", "log", "serde", "solana-address 2.6.1", @@ -6637,6 +6666,7 @@ version = "4.2.0-alpha.0" dependencies = [ "agave-banking-stage-ingress-types", "agave-bls-cert-verify", + "agave-bls-sigverify", "agave-feature-set", "agave-fs", "agave-logger", diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index 53b554b7c4d..7a9389e9515 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -76,6 +76,33 @@ dependencies = [ "wincode", ] +[[package]] +name = "agave-bls-sigverify" +version = "4.2.0-alpha.0" +dependencies = [ + "agave-bls-cert-verify", + "agave-math-utils", + "agave-votor-messages", + "bitvec", + "crossbeam-channel", + "log", + "parking_lot 0.12.2", + "qualifier_attr", + "rayon", + "solana-bls-signatures", + "solana-clock", + "solana-gossip", + "solana-ledger", + "solana-measure", + "solana-metrics", + "solana-pubkey 4.2.0", + "solana-runtime", + "solana-signer-store", + "solana-streamer", + "thiserror 2.0.18", + "wincode", +] + [[package]] name = "agave-cpu-utils" version = "4.2.0-alpha.0" @@ -341,6 +368,7 @@ dependencies = [ name = "agave-votor" version = "4.2.0-alpha.0" dependencies = [ + "agave-bls-sigverify", "agave-logger", "agave-math-utils", "agave-votor-messages", @@ -388,6 +416,7 @@ name = "agave-votor-messages" version = "4.2.0-alpha.0" dependencies = [ "agave-feature-set", + "crossbeam-channel", "log", "serde", "solana-address 2.6.1", @@ -6497,6 +6526,7 @@ version = "4.2.0-alpha.0" dependencies = [ "agave-banking-stage-ingress-types", "agave-bls-cert-verify", + "agave-bls-sigverify", "agave-feature-set", "agave-fs", "agave-logger", diff --git a/votor-messages/Cargo.toml b/votor-messages/Cargo.toml index be52ed32ade..aaaefd2b38c 100644 --- a/votor-messages/Cargo.toml +++ b/votor-messages/Cargo.toml @@ -21,6 +21,7 @@ frozen-abi = [ [dependencies] agave-feature-set = { workspace = true } +crossbeam-channel = { workspace = true } log = { workspace = true } serde = { workspace = true } solana-address = { workspace = true, features = ["curve25519"] } diff --git a/votor-messages/src/lib.rs b/votor-messages/src/lib.rs index c3d78d143a4..4bce443f571 100644 --- a/votor-messages/src/lib.rs +++ b/votor-messages/src/lib.rs @@ -3,9 +3,16 @@ #![cfg_attr(feature = "frozen-abi", feature(min_specialization))] #![deny(missing_docs)] +use { + crossbeam_channel::{Receiver, Sender}, + solana_clock::Slot, + solana_pubkey::Pubkey, +}; + pub mod certificate; pub mod consensus_message; pub mod fraction; +pub mod metric_types; pub mod migration; pub mod reward_certificate; pub mod vote; @@ -13,3 +20,9 @@ pub mod vote; #[cfg_attr(feature = "frozen-abi", macro_use)] #[cfg(feature = "frozen-abi")] extern crate solana_frozen_abi_macro; + +/// Send side of verified voter channel. +/// Each message contains the Pubkey of the voter and the slots in last verified vote. +pub type VerifiedVoterSlotsSender = Sender<(Pubkey, Vec)>; +/// Receive side of verified voter channel. +pub type VerifiedVoterSlotsReceiver = Receiver<(Pubkey, Vec)>; diff --git a/votor-messages/src/metric_types.rs b/votor-messages/src/metric_types.rs new file mode 100644 index 00000000000..487c70327c7 --- /dev/null +++ b/votor-messages/src/metric_types.rs @@ -0,0 +1,54 @@ +//! Definitions related to consensus metrics collection. + +use { + crate::vote::Vote, + crossbeam_channel::{Receiver, Sender}, + solana_clock::Slot, + solana_pubkey::Pubkey, + std::time::Instant, +}; + +/// Different types of events to notify the metrics container of. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ConsensusMetricsEvent { + /// A vote was received from the node with `id`. + Vote { + /// The validator that voted. + id: Pubkey, + /// The type of vote. + vote: Vote, + }, + /// A block hash was seen for `slot` and the `leader` is responsible for producing it. + BlockHashSeen { + /// The leader that produced the block. + leader: Pubkey, + /// The slot the block was produced for. + slot: Slot, + }, + /// Start of slot. + StartOfSlot { + /// The slot that just started. + slot: Slot, + }, + /// A slot was finalized. + SlotFinalized { + /// The slot that was finalized. + slot: Slot, + }, +} + +/// Send side of the channel to send metrics events on. +pub type ConsensusMetricsEventSender = Sender<(Instant, Vec)>; +/// Receive side of the channel to receive metrics events on. +pub type ConsensusMetricsEventReceiver = Receiver<(Instant, Vec)>; + +/// Even at 10 events per slot, this supports 1000 slots in flight +/// With 2000 active validators, we can't have more than: +/// - 1 Notarize vote +/// - 3 Notarize-fallback votes +/// - 1 Skip-fallback vote +/// - 1 Finalize vote +/// +/// Per validator, resulting in 12k vote events. +/// We overprovision this channel at 15k total events. +pub const MAX_IN_FLIGHT_CONSENSUS_EVENTS: usize = 15_000; diff --git a/votor-messages/src/reward_certificate.rs b/votor-messages/src/reward_certificate.rs index 7bee333eafd..71f5604ab27 100644 --- a/votor-messages/src/reward_certificate.rs +++ b/votor-messages/src/reward_certificate.rs @@ -1,6 +1,7 @@ //! Defines aggregates used for vote rewards. use { + crate::consensus_message::VoteMessage, solana_bls_signatures::SignatureCompressed as BLSSignatureCompressed, solana_clock::Slot, solana_hash::Hash, @@ -125,3 +126,10 @@ pub enum BuildRewardCertsRespError { #[error("encode error {0:?}")] Encode(EncodeError), } + +/// Message to add votes to the rewards container. +#[derive(Debug)] +pub struct AddVoteMessage { + /// List of [`VoteMessage`]s. + pub votes: Vec, +} diff --git a/votor/Cargo.toml b/votor/Cargo.toml index 50794e2696d..0eff869732e 100644 --- a/votor/Cargo.toml +++ b/votor/Cargo.toml @@ -29,6 +29,7 @@ frozen-abi = [ ] [dependencies] +agave-bls-sigverify = { workspace = true } agave-logger = { workspace = true } agave-math-utils = { workspace = true } agave-votor-messages = { workspace = true } diff --git a/votor/src/consensus_metrics.rs b/votor/src/consensus_metrics.rs index 2b4310d41a3..9153357f8dc 100644 --- a/votor/src/consensus_metrics.rs +++ b/votor/src/consensus_metrics.rs @@ -1,7 +1,10 @@ use { agave_math_utils::welford_stats::WelfordStats, - agave_votor_messages::vote::Vote, - crossbeam_channel::{Receiver, RecvTimeoutError, Sender}, + agave_votor_messages::{ + metric_types::{ConsensusMetricsEvent, ConsensusMetricsEventReceiver}, + vote::Vote, + }, + crossbeam_channel::RecvTimeoutError, solana_clock::{Epoch, Slot}, solana_epoch_schedule::EpochSchedule, solana_metrics::datapoint_info, @@ -17,35 +20,9 @@ use { }, }; -/// Even at 10 events per slot, this supports 1000 slots in flight -/// With 2000 active validators, we can't have more than: -/// - 1 Notarize vote -/// - 3 Notarize-fallback votes -/// - 1 Skip-fallback vote -/// - 1 Finalize vote -/// -/// Per validator, resulting in 12k vote events. -/// We overprovision this channel at 15k total events. -pub const MAX_IN_FLIGHT_CONSENSUS_EVENTS: usize = 15_000; - /// Number of epochs to retain metrics for (current + previous). const EPOCHS_TO_RETAIN: u64 = 2; -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ConsensusMetricsEvent { - /// A vote was received from the node with `id`. - Vote { id: Pubkey, vote: Vote }, - /// A block hash was seen for `slot` and the `leader` is responsible for producing it. - BlockHashSeen { leader: Pubkey, slot: Slot }, - /// Start of slot - StartOfSlot { slot: Slot }, - /// A slot was finalized - SlotFinalized { slot: Slot }, -} - -pub type ConsensusMetricsEventSender = Sender<(Instant, Vec)>; -pub type ConsensusMetricsEventReceiver = Receiver<(Instant, Vec)>; - /// Tracks all [`Vote`] metrics for a given node. #[derive(Debug, Default)] struct NodeVoteMetrics { @@ -81,20 +58,6 @@ impl NodeVoteMetrics { } } -/// Errors returned from [`ConsensusMetrics::record_vote`]. -#[derive(Debug)] -pub enum RecordVoteError { - /// Could not find start of slot entry. - SlotNotFound, -} - -/// Errors returned from [`ConsensusMetrics::record_block_hash_seen`]. -#[derive(Debug)] -pub enum RecordBlockHashError { - /// Could not find start of slot entry. - SlotNotFound, -} - /// Per-epoch metrics container. #[derive(Debug, Default)] struct EpochMetrics { diff --git a/votor/src/consensus_pool.rs b/votor/src/consensus_pool.rs index 411dd0326d5..e2d0ecc4b9c 100644 --- a/votor/src/consensus_pool.rs +++ b/votor/src/consensus_pool.rs @@ -12,8 +12,8 @@ use { vote_pool::{DuplicateBlockVotePool, SimpleVotePool, VotePool}, }, event::VotorEvent, - generated_cert_types::GeneratedCertTypes, }, + agave_bls_sigverify::generated_cert_types::GeneratedCertTypes, agave_votor_messages::{ certificate::{Certificate, CertificateType}, consensus_message::{Block, ConsensusMessage, VoteMessage}, diff --git a/votor/src/consensus_pool_service.rs b/votor/src/consensus_pool_service.rs index 2068a763c30..18034967477 100644 --- a/votor/src/consensus_pool_service.rs +++ b/votor/src/consensus_pool_service.rs @@ -15,9 +15,9 @@ use { parent_ready_tracker::{BlockProductionParent, ParentReady}, }, event::{LeaderWindowInfo, RepairEvent, RepairEventSender, VotorEvent, VotorEventSender}, - generated_cert_types::GeneratedCertTypes, voting_service::BLSOp, }, + agave_bls_sigverify::generated_cert_types::GeneratedCertTypes, agave_votor_messages::{ certificate::Certificate, consensus_message::{Block, ConsensusMessage, SigVerifiedBatch}, diff --git a/votor/src/event_handler.rs b/votor/src/event_handler.rs index 2f87b338a3f..dacc9a3aa5f 100644 --- a/votor/src/event_handler.rs +++ b/votor/src/event_handler.rs @@ -4,7 +4,6 @@ use { crate::{ commitment::{CommitmentType, update_commitment_cache}, - consensus_metrics::ConsensusMetricsEvent, event::{ CompletedBlock, LatestSwitchRequest, RepairEvent, RepairEventSender, SwitchBankEvent, VotorEvent, VotorEventReceiver, @@ -20,7 +19,10 @@ use { }, votor::SharedContext, }, - agave_votor_messages::{consensus_message::Block, migration::MigrationStatus, vote::Vote}, + agave_votor_messages::{ + consensus_message::Block, metric_types::ConsensusMetricsEvent, migration::MigrationStatus, + vote::Vote, + }, crossbeam_channel::{RecvError, SendError, TrySendError, select}, parking_lot::RwLock, solana_clock::Slot, @@ -951,7 +953,6 @@ mod tests { super::*, crate::{ commitment::CommitmentAggregationData, - consensus_metrics::ConsensusMetricsEventReceiver, event::{LeaderWindowInfo, RepairEventReceiver}, vote_history_storage::{ FileVoteHistoryStorage, SavedVoteHistory, SavedVoteHistoryVersions, @@ -961,6 +962,7 @@ mod tests { }, agave_votor_messages::{ consensus_message::{BLS_KEYPAIR_DERIVE_SEED, SigVerifiedBatch, VoteMessage}, + metric_types::ConsensusMetricsEventReceiver, vote::Vote, }, crossbeam_channel::{Receiver, Sender, TryRecvError, bounded}, diff --git a/votor/src/lib.rs b/votor/src/lib.rs index e3cf0a52fa9..7a1161e4bb4 100644 --- a/votor/src/lib.rs +++ b/votor/src/lib.rs @@ -11,7 +11,6 @@ pub mod consensus_pool; mod consensus_pool_service; pub mod event; mod event_handler; -pub mod generated_cert_types; pub mod root_utils; mod staked_validators_cache; mod timer_manager; diff --git a/votor/src/voting_utils.rs b/votor/src/voting_utils.rs index 35f4cf29133..048739c96ea 100644 --- a/votor/src/voting_utils.rs +++ b/votor/src/voting_utils.rs @@ -1,13 +1,13 @@ use { crate::{ commitment::{CommitmentAggregationData, CommitmentError}, - consensus_metrics::ConsensusMetricsEventSender, vote_history::{VoteHistory, VoteHistoryError}, vote_history_storage::{SavedVoteHistory, SavedVoteHistoryVersions}, voting_service::BLSOp, }, agave_votor_messages::{ consensus_message::{BLS_KEYPAIR_DERIVE_SEED, SigVerifiedBatch, VoteMessage}, + metric_types::ConsensusMetricsEventSender, vote::Vote, }, crossbeam_channel::{SendError, Sender}, diff --git a/votor/src/votor.rs b/votor/src/votor.rs index 7786d8d0d1a..ca07aed9593 100644 --- a/votor/src/votor.rs +++ b/votor/src/votor.rs @@ -46,16 +46,13 @@ use { crate::{ commitment::CommitmentAggregationData, - consensus_metrics::{ - ConsensusMetrics, ConsensusMetricsEventReceiver, ConsensusMetricsEventSender, - }, + consensus_metrics::ConsensusMetrics, consensus_pool_service::{ConsensusPoolContext, ConsensusPoolService}, event::{ LatestSwitchRequest, LeaderWindowInfo, RepairEventSender, VotorEventReceiver, VotorEventSender, }, event_handler::{EventHandler, EventHandlerContext}, - generated_cert_types::GeneratedCertTypes, root_utils::RootContext, timer_manager::TimerManager, vote_history::VoteHistory, @@ -63,7 +60,11 @@ use { voting_service::BLSOp, voting_utils::VotingContext, }, - agave_votor_messages::consensus_message::{Block, SigVerifiedBatch}, + agave_bls_sigverify::generated_cert_types::GeneratedCertTypes, + agave_votor_messages::{ + consensus_message::{Block, SigVerifiedBatch}, + metric_types::{ConsensusMetricsEventReceiver, ConsensusMetricsEventSender}, + }, crossbeam_channel::{Receiver, Sender}, parking_lot::RwLock as PlRwLock, solana_clock::Slot, From 1ef531edc6c520ad42bc13d483a86003a792ba66 Mon Sep 17 00:00:00 2001 From: Brooks Date: Fri, 12 Jun 2026 14:16:25 -0400 Subject: [PATCH 316/576] Renames fn to update_index_for_flush() (#13177) --- accounts-db/src/accounts_db.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/accounts-db/src/accounts_db.rs b/accounts-db/src/accounts_db.rs index 2ec9fbfbabb..71856fad964 100644 --- a/accounts-db/src/accounts_db.rs +++ b/accounts-db/src/accounts_db.rs @@ -5199,10 +5199,10 @@ impl AccountsDb { } /// Updates the accounts index with the given `infos` and `accounts`. - /// Used when storing accounts to storage. + /// Used when storing accounts to storage for flush. /// Returns a vector of `SlotList` containing the reclaims for each batch processed. /// The element of the returned vector is guaranteed to be non-empty. - fn update_index_stored_accounts<'a>( + fn update_index_for_flush<'a>( &self, infos: Vec, accounts: &impl StorableAccounts<'a>, @@ -5822,7 +5822,7 @@ impl AccountsDb { let mark_zero_lamport_us = mark_zero_lamport_time.end_as_us(); let update_index_time = Measure::start("update_index"); - let reclaims = self.update_index_stored_accounts( + let reclaims = self.update_index_for_flush( infos, &accounts, reclaim_handling, From da634f1572909f6eea130430244a82c2bc784f56 Mon Sep 17 00:00:00 2001 From: Giga <60071892+ataridev@users.noreply.github.com> Date: Fri, 12 Jun 2026 22:21:22 +0400 Subject: [PATCH 317/576] bls-cert-verify: add certificate-corruption robustness tests (#13101) Adversarial tests for verify_certificate -- the verifiable core of "a single bad validator corrupting a BLS certificate" (anza-xyz/alpenglow#474): - tampered bitmap (adding an unsigned validator / removing a real signer) fails at signature verification (VerifySig(VerificationFailed)) -> stake cannot be inflated - altered cert_type payload fails verification - empty bitmap -> VerifySig(EmptyAggregation); base3 bitmap on a base2 cert -> WrongEncoding; unknown rank -> MissingRank; oversized bitmap -> Decode(CorruptDataPayload) - arbitrary garbage bitmaps never panic the verifier - base3 (split-vote) tampering: swapping vote types or adding an unsigned fallback signer fails verification --- bls-cert-verify/src/cert_verify.rs | 286 ++++++++++++++++++++++++++++- 1 file changed, 285 insertions(+), 1 deletion(-) diff --git a/bls-cert-verify/src/cert_verify.rs b/bls-cert-verify/src/cert_verify.rs index ede586b2c04..3e537482e04 100644 --- a/bls-cert-verify/src/cert_verify.rs +++ b/bls-cert-verify/src/cert_verify.rs @@ -280,7 +280,7 @@ mod test { keypair::Keypair as BLSKeypair, signature::Signature as BLSSignature, }, solana_hash::Hash, - solana_signer_store::encode_base2, + solana_signer_store::{encode_base2, encode_base3}, }; fn create_bls_keypairs(num_signers: usize) -> Vec { @@ -465,4 +465,288 @@ mod test { Err(Error::WrongEncoding) ); } + + // ---------------------------------------------------------------------------- + // Adversarial / corruption robustness. + // + // These tests cover the unit-level core of "a single bad validator corrupting a + // BLS certificate" (anza-xyz/alpenglow#474): a tampered certificate must be + // rejected and must never report stake it cannot prove a signature for. The + // network-level blacklisting test described in that issue builds on top of this + // verification contract. + // ---------------------------------------------------------------------------- + + const STAKE_PER_VALIDATOR: u64 = 100; + + /// A `Block` for `slot` with a fresh, unique block id. + fn fresh_block(slot: u64) -> Block { + Block { + slot, + block_id: Hash::new_unique(), + } + } + + /// Encode a Base2 rank bitmap with the given `ranks` set out of `num_bits`. + fn encoded_base2_bitmap(ranks: &[usize], num_bits: usize) -> Vec { + let mut bitmap = BitVec::new(); + bitmap.resize(num_bits, false); + for &rank in ranks { + bitmap.set(rank, true); + } + encode_base2(&bitmap).unwrap() + } + + /// Encode a Base3 rank bitmap where `primary` ranks signed the primary vote and + /// `fallback` ranks signed the fallback vote. The two sets must be disjoint. + fn encoded_base3_bitmap(primary: &[usize], fallback: &[usize], num_bits: usize) -> Vec { + let mut base = BitVec::new(); + base.resize(num_bits, false); + for &rank in primary { + base.set(rank, true); + } + let mut fallback_bits = BitVec::new(); + fallback_bits.resize(num_bits, false); + for &rank in fallback { + fallback_bits.set(rank, true); + } + encode_base3(&base, &fallback_bits).unwrap() + } + + /// Uniform `rank_map` over `keypairs`, each with `STAKE_PER_VALIDATOR` stake. + fn rank_map( + keypairs: &[BLSKeypair], + ) -> impl FnMut(usize) -> Option<(u64, PopVerified)> + '_ { + move |rank| { + keypairs + .get(rank) + .map(|kp| (STAKE_PER_VALIDATOR, kp.public)) + } + } + + /// A certificate with the given bitmap and a placeholder (all-zero) signature, + /// for tests whose rejection is independent of the signature contents. + fn unsigned_cert(cert_type: CertificateType, bitmap: Vec) -> Certificate { + Certificate { + cert_type, + signature: BLSSignature([0; 192]), + bitmap, + } + } + + /// Build a valid Base3 `NotarizeFallback` certificate where `primary_ranks` signed + /// the notarization vote and `fallback_ranks` signed the notarization fallback vote. + fn signed_notarize_fallback_cert( + keypairs: &[BLSKeypair], + block: Block, + primary_ranks: &[usize], + fallback_ranks: &[usize], + ) -> Certificate { + let notarize = Vote::new_notarization_vote(block); + let fallback = Vote::new_notarization_fallback_vote(block); + let mut messages = Vec::new(); + for &rank in primary_ranks { + messages.push(create_signed_vote_message(keypairs, notarize, rank)); + } + for &rank in fallback_ranks { + messages.push(create_signed_vote_message(keypairs, fallback, rank)); + } + let mut builder = CertificateBuilder::new(CertificateType::NotarizeFallback(block)); + builder.aggregate(&messages).unwrap(); + builder.build().unwrap() + } + + /// Adding a validator to the bitmap who did not actually sign must fail + /// verification (the aggregate no longer matches the signature) - a bad actor + /// cannot inflate the signing stake of a certificate. + #[test] + fn tampered_bitmap_adding_unsigned_validator_fails() { + let keypairs = create_bls_keypairs(10); + let cert_type = CertificateType::Notarize(fresh_block(10)); + let mut cert = create_signed_certificate_message(&keypairs, cert_type, &[0, 1, 2, 3, 4, 5]); + + // Claim rank 6 also signed, even though it never did. + cert.bitmap = encoded_base2_bitmap(&[0, 1, 2, 3, 4, 5, 6], 7); + + // Must fail at the signature stage (not via a decode / missing-rank path), + // proving the extra validator cannot be smuggled in. + assert_eq!( + verify_certificate(&cert, 10, rank_map(&keypairs)).unwrap_err(), + Error::VerifySig(BlsError::VerificationFailed) + ); + } + + /// Removing a genuine signer from the bitmap must also fail: the aggregate + /// signature still carries that signer's contribution, so the reduced key set + /// will not verify. + #[test] + fn tampered_bitmap_removing_signer_fails() { + let keypairs = create_bls_keypairs(10); + let cert_type = CertificateType::Notarize(fresh_block(10)); + let mut cert = create_signed_certificate_message(&keypairs, cert_type, &[0, 1, 2, 3, 4, 5]); + + // Drop rank 5 from the bitmap while keeping its signature in the aggregate. + cert.bitmap = encoded_base2_bitmap(&[0, 1, 2, 3, 4], 6); + + assert_eq!( + verify_certificate(&cert, 10, rank_map(&keypairs)).unwrap_err(), + Error::VerifySig(BlsError::VerificationFailed) + ); + } + + /// A certificate whose `cert_type` (here the block id) is altered after the votes + /// were signed must fail: the reconstructed payload no longer matches what the + /// validators signed. + #[test] + fn tampered_cert_type_payload_fails() { + let keypairs = create_bls_keypairs(10); + let cert = create_signed_certificate_message( + &keypairs, + CertificateType::Notarize(fresh_block(10)), + &[0, 1, 2, 3, 4, 5], + ); + + let forged = Certificate { + cert_type: CertificateType::Notarize(fresh_block(10)), + signature: cert.signature, + bitmap: cert.bitmap.clone(), + }; + + assert_eq!( + verify_certificate(&forged, 10, rank_map(&keypairs)).unwrap_err(), + Error::VerifySig(BlsError::VerificationFailed) + ); + } + + /// A certificate with no signers (empty bitmap) must NOT verify. Accepting it + /// would let an attacker present a "certificate" backed by zero stake. + #[test] + fn empty_bitmap_certificate_does_not_verify() { + let keypairs = create_bls_keypairs(10); + let cert = unsigned_cert( + CertificateType::Notarize(fresh_block(10)), + encoded_base2_bitmap(&[], 0), + ); + + // An empty signer set cannot produce a valid aggregate signature. + assert_eq!( + verify_certificate(&cert, 10, rank_map(&keypairs)).unwrap_err(), + Error::VerifySig(BlsError::EmptyAggregation) + ); + } + + /// A Base3-encoded bitmap supplied for a certificate type that is verified as + /// Base2 (e.g. `Notarize`) must be rejected with `WrongEncoding` rather than + /// silently mis-decoded. + #[test] + fn base3_bitmap_rejected_for_base2_certificate() { + let keypairs = create_bls_keypairs(10); + let bitmap = encoded_base3_bitmap(&[0, 1], &[2], 6); + let cert = unsigned_cert(CertificateType::Notarize(fresh_block(10)), bitmap); + + assert_eq!( + verify_certificate(&cert, 10, rank_map(&keypairs)).unwrap_err(), + Error::WrongEncoding + ); + } + + /// If a rank present in the bitmap is unknown to the `rank_map` (e.g. not in the + /// epoch's stake table), verification fails with `MissingRank` rather than + /// silently skipping that validator. + #[test] + fn unknown_rank_in_bitmap_fails() { + let keypairs = create_bls_keypairs(10); + let cert_type = CertificateType::Notarize(fresh_block(10)); + let cert = create_signed_certificate_message(&keypairs, cert_type, &[0, 1, 2, 3, 4, 5]); + + // rank_map returns None for rank 3, simulating an unknown/unstaked validator. + let result = verify_certificate(&cert, 10, |rank| { + if rank == 3 { + None + } else { + keypairs + .get(rank) + .map(|kp| (STAKE_PER_VALIDATOR, kp.public)) + } + }); + + assert_eq!(result.unwrap_err(), Error::MissingRank); + } + + /// A bitmap declaring more bits than `max_validators` must be rejected by the + /// decoder (no panic, no out-of-bounds rank lookups). + #[test] + fn oversized_bitmap_is_rejected() { + let keypairs = create_bls_keypairs(10); + // 20 bits declared, but only 10 validators allowed. + let cert = unsigned_cert( + CertificateType::Notarize(fresh_block(10)), + encoded_base2_bitmap(&[0, 1], 20), + ); + + assert_eq!( + verify_certificate(&cert, 10, rank_map(&keypairs)).unwrap_err(), + Error::Decode(DecodeError::CorruptDataPayload) + ); + } + + /// Arbitrary attacker-controlled bitmap bytes must never panic the verifier and + /// must never verify (these certificates carry no valid aggregate signature). + #[test] + fn garbage_bitmap_never_panics_and_never_verifies() { + let keypairs = create_bls_keypairs(10); + for seed in 0u64..512 { + let len = (seed % 40) as usize; + let bitmap: Vec = (0..len) + .map(|i| seed.wrapping_mul(2_654_435_761).wrapping_add(i as u64 * 7) as u8) + .collect(); + let cert = unsigned_cert(CertificateType::Notarize(fresh_block(10)), bitmap); + // Must return (Ok/Err) without panicking; a zero signature can never + // produce a valid certificate, so the result must be an error. + assert!(verify_certificate(&cert, 10, rank_map(&keypairs)).is_err()); + } + } + + /// In a Base3 (split-vote) certificate, swapping which payload each validator is + /// claimed to have signed must fail: every validator's signature is bound to a + /// specific vote payload, so misattributing it breaks the aggregate check. + #[test] + fn base3_tampered_swapping_vote_types_fails() { + let keypairs = create_bls_keypairs(10); + let block = fresh_block(20); + // 0,1 signed the primary (notarize) vote; 2,3 signed the fallback vote. + let cert = signed_notarize_fallback_cert(&keypairs, block, &[0, 1], &[2, 3]); + + // Forge a bitmap claiming the opposite: 2,3 as primary and 0,1 as fallback. + let forged = Certificate { + cert_type: cert.cert_type, + signature: cert.signature, + bitmap: encoded_base3_bitmap(&[2, 3], &[0, 1], 4), + }; + + assert_eq!( + verify_certificate(&forged, 10, rank_map(&keypairs)).unwrap_err(), + Error::VerifySig(BlsError::VerificationFailed) + ); + } + + /// Adding an unsigned validator to the fallback set of a Base3 certificate must + /// fail verification - the split-vote path is no easier to inflate than Base2. + #[test] + fn base3_tampered_adding_unsigned_validator_fails() { + let keypairs = create_bls_keypairs(10); + let block = fresh_block(20); + let cert = signed_notarize_fallback_cert(&keypairs, block, &[0, 1], &[2, 3]); + + // Claim rank 4 also cast a fallback vote, though it never signed. + let forged = Certificate { + cert_type: cert.cert_type, + signature: cert.signature, + bitmap: encoded_base3_bitmap(&[0, 1], &[2, 3, 4], 5), + }; + + assert_eq!( + verify_certificate(&forged, 10, rank_map(&keypairs)).unwrap_err(), + Error::VerifySig(BlsError::VerificationFailed) + ); + } } From 3811bccb938540ab519ea65d7a404659d25593c3 Mon Sep 17 00:00:00 2001 From: Rory Harris Date: Fri, 12 Jun 2026 12:04:32 -0700 Subject: [PATCH 318/576] Collapse unflushed roots (#13174) accounts-db: collapse unflushed-root tracking into one set --- accounts-db/src/accounts_cache.rs | 213 +++++++++++---------------- accounts-db/src/accounts_db.rs | 11 +- accounts-db/src/accounts_db/tests.rs | 26 ++++ 3 files changed, 122 insertions(+), 128 deletions(-) diff --git a/accounts-db/src/accounts_cache.rs b/accounts-db/src/accounts_cache.rs index f2bf7736e45..bd24e3907d2 100644 --- a/accounts-db/src/accounts_cache.rs +++ b/accounts-db/src/accounts_cache.rs @@ -234,12 +234,10 @@ pub struct AccountsCache { cache: DashMap, BuildNoHashHasher>, // Index to find which slots a pubkey is stored in, to speed up lookups in load_latest index: AccountsCacheIndex, - // Queue of potentially unflushed roots. Random eviction + cache too large - // could have triggered a flush of this slot already - maybe_unflushed_roots: RwLock>, - // Roots that have been claimed for flushing by `begin_flush_roots` but not yet - // cleared by `end_flush_roots` - roots_being_flushed: RwLock>, + // Rooted slots that may still have accounts in the write cache. A slot enters via `add_root` + // and is dropped either by `remove_slot` when its cache is flushed, or by + // `remove_unflushed_root` when a flush finds it had no cache. + unflushed_roots: RwLock>, max_flushed_root: MaxFlushedRoot, /// The size of account data stored in the whole AccountsCache, in bytes total_size: Arc, @@ -267,11 +265,7 @@ impl AccountsCache { pub fn report_size(&self) { datapoint_info!( "accounts_cache_size", - ( - "num_roots", - self.maybe_unflushed_roots.read().unwrap().len(), - i64 - ), + ("num_roots", self.unflushed_roots.read().unwrap().len(), i64), ("num_slots", self.cache.len(), i64), ("total_size", self.size(), i64), ( @@ -324,6 +318,8 @@ impl AccountsCache { if let Some(slot_cache) = &result { self.index.remove(slot_cache.iter().map(|item| *item.key())); } + // If this slot was a root, it has now left the cache, so stop tracking it as unflushed. + self.unflushed_roots.write().unwrap().remove(&slot); result } @@ -361,21 +357,13 @@ impl AccountsCache { .unwrap_or(index_max_slot) .min(index_max_slot); - let r_maybe_unflushed_roots = self.maybe_unflushed_roots.read().unwrap(); - for &slot in r_maybe_unflushed_roots.range(..=max_root_slot).rev() { - if let Some(account) = self.load(slot, pubkey) { - return Some((account, slot)); - } - } - drop(r_maybe_unflushed_roots); - - let r_roots_being_flushed = self.roots_being_flushed.read().unwrap(); - for &slot in r_roots_being_flushed.range(..=max_root_slot).rev() { + let r_unflushed_roots = self.unflushed_roots.read().unwrap(); + for &slot in r_unflushed_roots.range(..=max_root_slot).rev() { if let Some(account) = self.load(slot, pubkey) { return Some((account, slot)); } } - drop(r_roots_being_flushed); + drop(r_unflushed_roots); // Found nothing, the version of the account in the cache must be on a different fork None @@ -386,42 +374,25 @@ impl AccountsCache { } pub fn add_root(&self, root: Slot) { - self.maybe_unflushed_roots.write().unwrap().insert(root); + self.unflushed_roots.write().unwrap().insert(root); } pub fn num_unflushed_roots(&self) -> usize { - self.maybe_unflushed_roots.read().unwrap().len() - } - - /// Atomically moves roots up to and including `max_root` (or all roots if `None`) out of - /// `maybe_unflushed_roots` and into `roots_being_flushed`, then returns them - /// - /// Panics if there are already roots being flushed (i.e. `end_flush_roots` was not called after - /// a previous `begin_flush_roots`) - pub fn begin_flush_roots(&self, max_root: Option) -> BTreeSet { - let mut w_maybe_unflushed_roots = self.maybe_unflushed_roots.write().unwrap(); - let mut w_roots_being_flushed = self.roots_being_flushed.write().unwrap(); - - assert!( - w_roots_being_flushed.is_empty(), - "begin_flush_roots called while roots are already being flushed; end_flush_roots must \ - be called first" - ); - - let roots_to_flush = if let Some(max_root) = max_root { - let remaining = w_maybe_unflushed_roots.split_off(&(max_root + 1)); - std::mem::replace(&mut *w_maybe_unflushed_roots, remaining) - } else { - std::mem::take(&mut *w_maybe_unflushed_roots) - }; - - *w_roots_being_flushed = roots_to_flush.clone(); - roots_to_flush - } - - /// Signals that flushing is complete. Clears the roots that were staged by `begin_flush_roots` - pub fn end_flush_roots(&self) { - self.roots_being_flushed.write().unwrap().clear(); + self.unflushed_roots.read().unwrap().len() + } + + /// Returns the unflushed roots up to and including `max_root` (or all roots if `None`). The + /// returned roots remain tracked as unflushed until `remove_slot` drops each one when its cache + /// is flushed. + pub fn roots_to_flush(&self, max_root: Option) -> BTreeSet { + // `None` means no upper bound, i.e. every root. + let max_root = max_root.unwrap_or(Slot::MAX); + self.unflushed_roots + .read() + .unwrap() + .range(..=max_root) + .copied() + .collect() } pub fn cached_frozen_slots(&self) -> Vec { @@ -461,8 +432,27 @@ impl AccountsCache { self.max_flushed_root.get() } + /// Stop tracking `slot` as an unflushed root without touching its cache. Used when a flushed + /// root had no cache for `remove_slot` to drop (e.g. genesis), so it would otherwise linger. + pub fn remove_unflushed_root(&self, slot: Slot) { + self.unflushed_roots.write().unwrap().remove(&slot); + } + pub fn set_max_flush_root(&self, root: Slot) { self.max_flushed_root.fetch_max(root); + // Every flushed root has left the cache via `remove_slot`, and any flushed root that had + // no cache was dropped by `remove_unflushed_root`. So once max_flushed_root advances, every + // remaining unflushed root must be > root; one at or below would be older than its + // just-flushed storage version yet still win in `load_latest`. + debug_assert!( + self.unflushed_roots + .read() + .unwrap() + .first() + .is_none_or(|&min_unflushed_root| min_unflushed_root > root), + "unflushed root {:?} is at or below the max flushed root {root}", + self.unflushed_roots.read().unwrap().first(), + ); } } @@ -661,31 +651,25 @@ mod tests { } /// Tests that `load_latest` returns the correct slot and account value - /// given various combinations of ancestor slots, root slots, and flushing state. + /// given various combinations of ancestor slots and root slots. /// /// Ancestors always take priority over roots regardless of slot // None case // `uncached_ancestors` are slots added to the Ancestors set but with no account // data stored in the cache. This lets us test root bounding by min_slot // without the ancestor path short-circuiting the lookup. - #[test_case(&[], &[], &[], &[], None; "not ancestor not root")] - #[test_case(&[10], &[], &[], &[], Some(10); "ancestor only")] - #[test_case(&[5, 10, 15], &[], &[], &[], Some(15); "highest ancestor returned")] - #[test_case(&[], &[10, 20], &[], &[], Some(20); "rooted, with no ancestors")] - #[test_case(&[], &[], &[10], &[], Some(10); "root being flushed")] - #[test_case(&[10], &[], &[10], &[], Some(10); "ancestor being flushed")] - #[test_case(&[5], &[20], &[], &[], Some(5); "ancestor wins over higher root")] - #[test_case(&[], &[20], &[10], &[], Some(20);"unflushed root over flushing root")] - #[test_case(&[5], &[20], &[10], &[], Some(5);"ancestor over unflushed and flushing roots")] + #[test_case(&[], &[], &[], None; "not ancestor not root")] + #[test_case(&[10], &[], &[], Some(10); "ancestor only")] + #[test_case(&[5, 10, 15], &[], &[], Some(15); "highest ancestor returned")] + #[test_case(&[], &[10, 20], &[], Some(20); "rooted, with no ancestors")] + #[test_case(&[5], &[20], &[], Some(5); "ancestor wins over higher root")] // Root beyond ancestors.min_slot() is excluded; older root still found - #[test_case(&[], &[5, 11], &[], &[10], Some(5); "unflushed root beyond min ancestor excluded")] - #[test_case(&[], &[], &[5, 11], &[10], Some(5); "flushing root beyond min ancestor excluded")] + #[test_case(&[], &[5, 11], &[10], Some(5); "root beyond min ancestor excluded")] // Root within min_slot bound is still returned - #[test_case(&[], &[10], &[], &[15], Some(10); "unflushed root below min ancestor returned")] + #[test_case(&[], &[10], &[15], Some(10); "root below min ancestor returned")] fn test_load_latest_slot_priority( ancestor_slots: &[Slot], - unflushed_root_slots: &[Slot], - flushing_root_slots: &[Slot], + root_slots: &[Slot], uncached_ancestors: &[Slot], expected: Option, ) { @@ -699,7 +683,7 @@ mod tests { AccountSharedData::new(slot, 0, &Pubkey::default()), ); } - for &slot in unflushed_root_slots.iter().chain(flushing_root_slots) { + for &slot in root_slots { cache.store( slot, &pk, @@ -707,9 +691,6 @@ mod tests { ); cache.add_root(slot); } - if let Some(&max) = flushing_root_slots.iter().max() { - cache.begin_flush_roots(Some(max)); - } let mut all_ancestors: Vec = ancestor_slots.to_vec(); all_ancestors.extend_from_slice(uncached_ancestors); @@ -741,91 +722,75 @@ mod tests { cache.store(10, &pk, AccountSharedData::new(100, 0, &Pubkey::default())); cache.add_root(10); - cache.begin_flush_roots(None); - cache.end_flush_roots(); + // A flush finishes a slot with `remove_slot`, which drops both its cache and its + // unflushed-root tracking; call it directly here to stand in for that flush. + cache.remove_slot(10); - // After clearing flushed roots, slot is no longer visible + // With the slot gone from the cache and untracked as a root, it is not visible. let empty = Ancestors::default(); assert!(cache.load_latest(&pk, &empty).is_none()); + assert!(cache.unflushed_roots.read().unwrap().is_empty()); } #[test] - fn test_begin_flush_roots_with_max_root() { + fn test_roots_to_flush_with_max_root() { let cache = AccountsCache::default(); cache.add_root(1); cache.add_root(3); cache.add_root(5); cache.add_root(7); - // begin_flush_roots(Some(5)) should return {1, 3, 5} and leave {7} - let taken = cache.begin_flush_roots(Some(5)); - assert_eq!(taken, BTreeSet::from([1, 3, 5])); - - // Remaining unflushed roots should only contain 7 - assert_eq!(cache.maybe_unflushed_roots.read().unwrap().len(), 1); - assert!(cache.maybe_unflushed_roots.read().unwrap().contains(&7)); - - // Taken roots should now be in roots_being_flushed - assert!(cache.roots_being_flushed.read().unwrap().contains(&1)); - assert!(cache.roots_being_flushed.read().unwrap().contains(&3)); - assert!(cache.roots_being_flushed.read().unwrap().contains(&5)); - assert!(!cache.roots_being_flushed.read().unwrap().contains(&7)); + // roots_to_flush(Some(5)) returns {1, 3, 5}, excluding 7. + let to_flush = cache.roots_to_flush(Some(5)); + assert_eq!(to_flush, BTreeSet::from([1, 3, 5])); - cache.end_flush_roots(); + // roots_to_flush only reads: the tracked roots are unchanged. + assert_eq!( + *cache.unflushed_roots.read().unwrap(), + BTreeSet::from([1, 3, 5, 7]) + ); } #[test] - fn test_begin_flush_roots_none_takes_all() { + fn test_roots_to_flush_none_takes_all() { let cache = AccountsCache::default(); cache.add_root(2); cache.add_root(4); cache.add_root(6); - let taken = cache.begin_flush_roots(None); - assert_eq!(taken, BTreeSet::from([2, 4, 6])); - - // All unflushed roots should be drained - assert!(cache.maybe_unflushed_roots.read().unwrap().is_empty()); + let to_flush = cache.roots_to_flush(None); + assert_eq!(to_flush, BTreeSet::from([2, 4, 6])); - // All should be in roots_being_flushed + // roots_to_flush only reads: the tracked roots are unchanged. assert_eq!( - *cache.roots_being_flushed.read().unwrap(), + *cache.unflushed_roots.read().unwrap(), BTreeSet::from([2, 4, 6]) ); - - cache.end_flush_roots(); } #[test] - #[should_panic(expected = "begin_flush_roots called while roots are already being flushed")] - fn test_begin_flush_roots_panics_if_already_flushing() { + fn test_remove_slot_drops_unflushed_root() { let cache = AccountsCache::default(); + let pk = Pubkey::new_unique(); + cache.store(1, &pk, AccountSharedData::new(1, 0, &Pubkey::default())); cache.add_root(1); - cache.begin_flush_roots(None); - // Calling again without end_flush_roots should panic + cache.store(2, &pk, AccountSharedData::new(2, 0, &Pubkey::default())); cache.add_root(2); - cache.begin_flush_roots(None); + + // remove_slot drops slot 1 from both the cache and the tracked roots, leaving slot 2. + cache.remove_slot(1); + assert_eq!(*cache.unflushed_roots.read().unwrap(), BTreeSet::from([2])); } + /// Advancing `max_flushed_root` past a still-tracked unflushed root is forbidden: that root + /// would be older than its just-flushed storage version yet still win in `load_latest`. #[test] - fn test_end_flush_roots_allows_next_flush() { + #[should_panic(expected = "unflushed root Some(3) is at or below the max flushed root 5")] + fn test_set_max_flush_root_below_unflushed_root_panics() { let cache = AccountsCache::default(); - cache.add_root(1); - cache.add_root(2); - - // First cycle - let taken = cache.begin_flush_roots(None); - assert_eq!(taken, BTreeSet::from([1, 2])); - - cache.end_flush_roots(); - - // After clear, roots_being_flushed is empty - assert!(cache.roots_being_flushed.read().unwrap().is_empty()); - - // Second cycle works without panic cache.add_root(3); - let taken = cache.begin_flush_roots(None); - assert_eq!(taken, BTreeSet::from([3])); - cache.end_flush_roots(); + cache.add_root(7); + // Root 3 is still tracked as unflushed; moving max_flushed_root to 5 leaves it stranded below. + cache.set_max_flush_root(5); } } diff --git a/accounts-db/src/accounts_db.rs b/accounts-db/src/accounts_db.rs index 71856fad964..cbf33faa41d 100644 --- a/accounts-db/src/accounts_db.rs +++ b/accounts-db/src/accounts_db.rs @@ -4693,7 +4693,7 @@ impl AccountsDb { ) -> (usize, usize, FlushStats) { // Always flush up to `requested_flush_root`, which is necessary for things like snapshotting. let flushed_roots: BTreeSet = - self.accounts_cache.begin_flush_roots(requested_flush_root); + self.accounts_cache.roots_to_flush(requested_flush_root); let max_flush_root = flushed_roots.last().copied(); let num_new_roots = flushed_roots.len(); @@ -4721,14 +4721,17 @@ impl AccountsDb { if let Some(stats) = self.flush_slot_cache(root, &pubkeys_to_flush[&root]) { num_roots_flushed += 1; flush_stats.accumulate(&stats); + } else { + // A root with no cache to flush (e.g. genesis, whose accounts load straight to + // storage) was still tracked by `add_root`. `flush_slot_cache` couldn't drop it + // via `remove_slot`, so untrack it here to keep it from lingering as an unflushed + // root at or below max_flushed_root. + self.accounts_cache.remove_unflushed_root(root); } } max_flush_root.inspect(|&root| self.accounts_cache.set_max_flush_root(root)); - // Now that all the rooted slots are flushed, we can clear the roots from the cache - self.accounts_cache.end_flush_roots(); - (num_new_roots, num_roots_flushed, flush_stats) } diff --git a/accounts-db/src/accounts_db/tests.rs b/accounts-db/src/accounts_db/tests.rs index 5c5aab07dd1..221cf462aa0 100644 --- a/accounts-db/src/accounts_db/tests.rs +++ b/accounts-db/src/accounts_db/tests.rs @@ -4203,6 +4203,32 @@ fn test_flush_rooted_accounts_cache_with_clean() { fn test_flush_rooted_accounts_cache_without_clean() { run_flush_rooted_accounts_cache(false); } + +/// A rooted slot with no write-cache entry (e.g. genesis, whose accounts load straight to storage) +/// is still tracked by `add_root`. Flushing must untrack it rather than leave it stranded in +/// `unflushed_roots` at or below `max_flushed_root`. +#[test] +fn test_flush_untracks_cacheless_root() { + let db = AccountsDb::new_single_for_tests(); + + // Slot 0: rooted but never written to the write cache. + db.accounts_cache.add_root(0); + + // Slot 10: a normal rooted slot with a cached account. + let pubkey = Pubkey::new_unique(); + db.accounts_cache.store( + 10, + &pubkey, + AccountSharedData::new(10, 0, &Pubkey::default()), + ); + db.add_root(10); + + // Flushing through slot 10 must drop the cacheless root 0 instead of stranding it below + // max_flushed_root (which would otherwise trip the unflushed-root invariant). + db.flush_accounts_cache(true, Some(10)); + + assert_eq!(db.accounts_cache.num_unflushed_roots(), 0); +} #[test] fn test_shrink_unref() { let db = AccountsDb::new_single_for_tests(); From 0b4214aeb436dadf4c27f1b94b0b556cfcb08db9 Mon Sep 17 00:00:00 2001 From: Brooks Date: Fri, 12 Jun 2026 15:09:47 -0400 Subject: [PATCH 319/576] Sets a max loop frequency for AccountsBackgroundService (#13168) --- runtime/src/accounts_background_service.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/runtime/src/accounts_background_service.rs b/runtime/src/accounts_background_service.rs index ebfd0732b6b..656497f0d22 100644 --- a/runtime/src/accounts_background_service.rs +++ b/runtime/src/accounts_background_service.rs @@ -34,7 +34,12 @@ use { }, }; -const INTERVAL_MS: u64 = 100; +/// Limit the maximum frequency that the ABS main loop can run. +/// If the loop ran for less than this duration, sleep the remainder. +/// E.g. with a min interval of 100 millis, the loop will run a maximum +/// of 10 times per second. Lower frequency is allowed, and occurs +/// when longer-running tasks are triggered. +const MIN_LOOP_INTERVAL: Duration = Duration::from_millis(100); // Set the clean interval duration to be approximately how long before the next incremental // snapshot request is received, plus some buffer. The default incremental snapshot interval is // 100 slots, which ends up being 40 seconds plus buffer. @@ -565,8 +570,12 @@ impl AccountsBackgroundService { previous_shrink_time = Instant::now(); } } - stats.record_and_maybe_submit(start_time.elapsed()); - sleep(Duration::from_millis(INTERVAL_MS)); + + let loop_dur = start_time.elapsed(); + stats.record_and_maybe_submit(loop_dur); + if let Some(sleep_dur) = MIN_LOOP_INTERVAL.checked_sub(loop_dur) { + sleep(sleep_dur); + } } info!("AccountsBackgroundService has stopped"); is_running.store(false, Ordering::Relaxed); From 9a3682e9dc8ada1ed25454ac487c304efc1e552e Mon Sep 17 00:00:00 2001 From: Zach Brown Date: Fri, 12 Jun 2026 13:26:05 -0700 Subject: [PATCH 320/576] Remove needless collect in `GenericTraversal` (#13182) --- core/src/repair/repair_generic_traversal.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/core/src/repair/repair_generic_traversal.rs b/core/src/repair/repair_generic_traversal.rs index b7090c04faa..1a41c6ecbe0 100644 --- a/core/src/repair/repair_generic_traversal.rs +++ b/core/src/repair/repair_generic_traversal.rs @@ -31,12 +31,11 @@ impl Iterator for GenericTraversal<'_> { fn next(&mut self) -> Option { let next = self.pending.pop(); if let Some(slot) = next { - let children: Vec<_> = self + let children = self .tree .children(&(slot, Hash::default())) .unwrap() - .map(|(child_slot, _)| *child_slot) - .collect(); + .map(|(child_slot, _)| *child_slot); self.pending.extend(children); } next From b7020259dc33f3ff56f98bbb8d452c1e0551bd1f Mon Sep 17 00:00:00 2001 From: Alex Pyattaev Date: Sat, 13 Jun 2026 00:17:10 +0300 Subject: [PATCH 321/576] refactor: alpenglow: simplify socket wiring (#13145) alpenglow: simplify socket wiring --- core/src/tvu.rs | 2 +- core/src/validator.rs | 4 ++-- gossip/src/cluster_info.rs | 6 ++---- gossip/src/node.rs | 2 +- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/core/src/tvu.rs b/core/src/tvu.rs index b3a49b6fb8c..077e605b2c9 100644 --- a/core/src/tvu.rs +++ b/core/src/tvu.rs @@ -847,7 +847,7 @@ pub mod tests { retransmit: target1.sockets.retransmit_sockets, fetch: target1.sockets.tvu, ancestor_hashes_requests: target1.sockets.ancestor_hashes_requests, - alpenglow: target1.sockets.alpenglow, + alpenglow: Some(target1.sockets.alpenglow), block_id_repair: target1.sockets.block_id_repair, }, blockstore, diff --git a/core/src/validator.rs b/core/src/validator.rs index 16bd361c3e7..32c4af5b76f 100644 --- a/core/src/validator.rs +++ b/core/src/validator.rs @@ -1586,11 +1586,11 @@ impl Validator { (None, None, None) }; - // disable all2all tests if not allowed for a given cluster type + // disable Alpenglow votor networking if not allowed for cluster type let alpenglow_socket = if genesis_config.cluster_type == ClusterType::Testnet || genesis_config.cluster_type == ClusterType::Development { - node.sockets.alpenglow + Some(node.sockets.alpenglow) } else { None }; diff --git a/gossip/src/cluster_info.rs b/gossip/src/cluster_info.rs index 6737726ee2b..f5c7f08cacf 100644 --- a/gossip/src/cluster_info.rs +++ b/gossip/src/cluster_info.rs @@ -2401,7 +2401,7 @@ pub struct Sockets { /// Client-side socket for ForwardingStage non-vote transactions pub tpu_transaction_forwarding_clients: Box<[UdpSocket]>, // quic write only /// Socket for alpenglow consensus logic - pub alpenglow: Option, // udp read/write + pub alpenglow: UdpSocket, // quic read/write /// Connection cache endpoint for QUIC-based Vote pub quic_vote_client: UdpSocket, // quic write only /// Connection cache endpoint for QUIC-based Alpenglow messages @@ -2946,9 +2946,7 @@ mod tests { fn check_node_sockets(node: &Node, ip: IpAddr, range: (u16, u16)) { check_socket(&node.sockets.repair, ip, range); - if let Some(alpenglow_port) = &node.sockets.alpenglow { - check_socket(alpenglow_port, ip, range); - } + check_socket(&node.sockets.alpenglow, ip, range); check_sockets(&node.sockets.gossip, ip, range); check_sockets(&node.sockets.tvu, ip, range); check_sockets(&node.sockets.tpu_quic, ip, range); diff --git a/gossip/src/node.rs b/gossip/src/node.rs index 9dbe1c9a73d..4f5e462ebfa 100644 --- a/gossip/src/node.rs +++ b/gossip/src/node.rs @@ -401,7 +401,7 @@ impl Node { trace!("new ContactInfo: {info:?}"); let sockets = Sockets { - alpenglow: Some(alpenglow), + alpenglow, gossip: gossip_sockets.into_iter().collect(), tvu: tvu_sockets, tpu_vote: tpu_vote_sockets, From 283b0febc8cd8c2c2c68d3136ca8d7ce87629367 Mon Sep 17 00:00:00 2001 From: Brooks Date: Fri, 12 Jun 2026 19:41:27 -0400 Subject: [PATCH 322/576] Wakes up read cache evictor every 10 millis (#13179) * Wakes up read cache evictor every 10 millis * Removes evictor wakeup count * Rename field to evict_run_count --- accounts-db/src/accounts_db.rs | 9 ++---- accounts-db/src/read_only_accounts_cache.rs | 32 ++++++--------------- 2 files changed, 10 insertions(+), 31 deletions(-) diff --git a/accounts-db/src/accounts_db.rs b/accounts-db/src/accounts_db.rs index cbf33faa41d..2b25609a493 100644 --- a/accounts-db/src/accounts_db.rs +++ b/accounts-db/src/accounts_db.rs @@ -6131,13 +6131,8 @@ impl AccountsDb { i64 ), ( - "read_only_accounts_cache_evictor_wakeup_count_all", - read_cache_stats.evictor_wakeup_count_all, - i64 - ), - ( - "read_only_accounts_cache_evictor_wakeup_count_productive", - read_cache_stats.evictor_wakeup_count_productive, + "read_only_accounts_cache_evict_run_count", + read_cache_stats.evict_run_count, i64 ), ( diff --git a/accounts-db/src/read_only_accounts_cache.rs b/accounts-db/src/read_only_accounts_cache.rs index 36fab4f62af..abe1bff2802 100644 --- a/accounts-db/src/read_only_accounts_cache.rs +++ b/accounts-db/src/read_only_accounts_cache.rs @@ -57,8 +57,7 @@ pub struct ReadOnlyCacheStats { pub load_us: u64, pub store_us: u64, pub evict_us: u64, - pub evictor_wakeup_count_all: u64, - pub evictor_wakeup_count_productive: u64, + pub evict_run_count: u64, } #[derive(Default, Debug)] @@ -69,8 +68,7 @@ struct AtomicReadOnlyCacheStats { load_us: AtomicU64, store_us: AtomicU64, evict_us: AtomicU64, - evictor_wakeup_count_all: AtomicU64, - evictor_wakeup_count_productive: AtomicU64, + evict_run_count: AtomicU64, } /// Shared state between the cache and its evictor thread, used to signal @@ -274,14 +272,7 @@ impl ReadOnlyAccountsCache { let load_us = self.stats.load_us.swap(0, Ordering::Relaxed); let store_us = self.stats.store_us.swap(0, Ordering::Relaxed); let evict_us = self.stats.evict_us.swap(0, Ordering::Relaxed); - let evictor_wakeup_count_all = self - .stats - .evictor_wakeup_count_all - .swap(0, Ordering::Relaxed); - let evictor_wakeup_count_productive = self - .stats - .evictor_wakeup_count_productive - .swap(0, Ordering::Relaxed); + let evict_run_count = self.stats.evict_run_count.swap(0, Ordering::Relaxed); ReadOnlyCacheStats { hits, @@ -290,8 +281,7 @@ impl ReadOnlyAccountsCache { load_us, store_us, evict_us, - evictor_wakeup_count_all, - evictor_wakeup_count_productive, + evict_run_count, } } @@ -312,28 +302,22 @@ impl ReadOnlyAccountsCache { info!("AccountsReadCacheEvictor has started"); let mut rng = SmallRng::from_os_rng(); loop { - // Wait up to 100 ms, or until the exit flag is set. - // 100 ms is already four times per slot, which should be plenty. + // Wait up to 10 ms, or until the exit flag is set. + // Ensure this timeout stays many times smaller than the slot time. let exit_flag = control.exit.lock().unwrap(); let (exit_flag, _) = control .wake - .wait_timeout_while(exit_flag, Duration::from_millis(100), |exit| !*exit) + .wait_timeout_while(exit_flag, Duration::from_millis(10), |exit| !*exit) .unwrap(); if *exit_flag { break; } drop(exit_flag); - stats - .evictor_wakeup_count_all - .fetch_add(1, Ordering::Relaxed); - if data_size.load(Ordering::Relaxed) <= max_data_size_hi { continue; } - stats - .evictor_wakeup_count_productive - .fetch_add(1, Ordering::Relaxed); + stats.evict_run_count.fetch_add(1, Ordering::Relaxed); #[cfg(not(feature = "dev-context-only-utils"))] let (num_evicts, evict_us) = measure_us!(Self::evict( From dab939cfc237176f71654be8c526f7c8b8b61168 Mon Sep 17 00:00:00 2001 From: Zach Brown Date: Sat, 13 Jun 2026 10:03:44 -0700 Subject: [PATCH 323/576] Add trimmed version of `SlotMeta` for repairs to avoid bitvec alloc (#13181) --- core/src/repair/repair_generic_traversal.rs | 14 +- core/src/repair/repair_service.rs | 36 ++--- core/src/repair/repair_weight.rs | 9 +- core/src/repair/repair_weighted_traversal.rs | 6 +- ledger/src/blockstore.rs | 9 ++ ledger/src/blockstore_meta.rs | 139 +++++++++++++++---- 6 files changed, 159 insertions(+), 54 deletions(-) diff --git a/core/src/repair/repair_generic_traversal.rs b/core/src/repair/repair_generic_traversal.rs index 1a41c6ecbe0..c4f291436ce 100644 --- a/core/src/repair/repair_generic_traversal.rs +++ b/core/src/repair/repair_generic_traversal.rs @@ -8,7 +8,7 @@ use { }, solana_clock::Slot, solana_hash::Hash, - solana_ledger::{blockstore::Blockstore, blockstore_meta::SlotMeta}, + solana_ledger::{blockstore::Blockstore, blockstore_meta::SlotMetaRepair}, std::collections::{HashMap, HashSet}, }; @@ -51,7 +51,7 @@ impl Iterator for GenericTraversal<'_> { pub fn get_unknown_last_index( tree: &HeaviestSubtreeForkChoice, blockstore: &Blockstore, - slot_meta_cache: &mut HashMap>, + slot_meta_cache: &mut HashMap>, processed_slots: &mut HashSet, limit: usize, outstanding_repairs: &mut HashMap, @@ -64,7 +64,7 @@ pub fn get_unknown_last_index( } let slot_meta = slot_meta_cache .entry(slot) - .or_insert_with(|| blockstore.meta(slot).unwrap()); + .or_insert_with(|| blockstore.meta_repair(slot).unwrap()); if let Some(slot_meta) = slot_meta { if slot_meta.last_index.is_none() { let shred_index = blockstore.get_index(slot).unwrap(); @@ -97,7 +97,7 @@ pub fn get_unknown_last_index( fn get_unrepaired_path( start_slot: Slot, blockstore: &Blockstore, - slot_meta_cache: &mut HashMap>, + slot_meta_cache: &mut HashMap>, visited: &mut HashSet, ) -> Vec { let mut path = Vec::new(); @@ -105,7 +105,7 @@ fn get_unrepaired_path( while visited.insert(slot) { let slot_meta = slot_meta_cache .entry(slot) - .or_insert_with(|| blockstore.meta(slot).unwrap()); + .or_insert_with(|| blockstore.meta_repair(slot).unwrap()); if let Some(slot_meta) = slot_meta { if !slot_meta.is_full() { path.push(slot); @@ -126,7 +126,7 @@ pub fn get_closest_completion( tree: &HeaviestSubtreeForkChoice, blockstore: &Blockstore, root_slot: Slot, - slot_meta_cache: &mut HashMap>, + slot_meta_cache: &mut HashMap>, processed_slots: &mut HashSet, limit: usize, repair_eligibility: &mut RepairEligibility, @@ -140,7 +140,7 @@ pub fn get_closest_completion( } let slot_meta = slot_meta_cache .entry(slot) - .or_insert_with(|| blockstore.meta(slot).unwrap()); + .or_insert_with(|| blockstore.meta_repair(slot).unwrap()); if let Some(slot_meta) = slot_meta { if slot_meta.is_full() { continue; diff --git a/core/src/repair/repair_service.rs b/core/src/repair/repair_service.rs index cfc915f9655..47540fca322 100644 --- a/core/src/repair/repair_service.rs +++ b/core/src/repair/repair_service.rs @@ -28,8 +28,8 @@ use { solana_hash::Hash, solana_keypair::Signer, solana_ledger::{ - blockstore::{Blockstore, SlotMeta}, - blockstore_meta::BlockLocation, + blockstore::Blockstore, + blockstore_meta::{BlockLocation, SlotMetaRepair}, shred, }, solana_measure::measure::Measure, @@ -153,7 +153,7 @@ impl RepairEligibility { /// highest received shred falls in an older FEC, that FEC was already /// timestamped; if it falls in a future FEC, resizing the vector backfills /// any skipped FECs with the same timestamp. - fn observe_slot(&mut self, slot: Slot, slot_meta: &SlotMeta) { + fn observe_slot(&mut self, slot: Slot, slot_meta: &SlotMetaRepair) { let Some(last_received_index) = slot_meta.received.checked_sub(1) else { // Have not observed any shreds for this slot yet. return; @@ -207,7 +207,7 @@ impl RepairEligibility { pub(crate) fn is_highest_shred_eligible( &self, slot: Slot, - slot_meta: &SlotMeta, + slot_meta: &SlotMetaRepair, now_ms: u64, ) -> bool { if slot_meta.last_index.is_some() { @@ -230,7 +230,11 @@ impl RepairEligibility { /// This keeps traversal tests focused on ordering and duplicate suppression /// without sleeping in every setup path. #[cfg(test)] - pub(crate) fn observe_slot_as_elapsed_for_tests(&mut self, slot: Slot, slot_meta: &SlotMeta) { + pub(crate) fn observe_slot_as_elapsed_for_tests( + &mut self, + slot: Slot, + slot_meta: &SlotMetaRepair, + ) { self.observe_slot(slot, slot_meta); if let Some(slot_repair) = self.slots.get_mut(&slot) { let elapsed_time = timestamp().saturating_sub(FEC_REPAIR_DELAY.as_millis() as u64); @@ -250,7 +254,7 @@ impl RepairEligibility { ) -> Self { let mut repair_eligibility = Self::default(); for slot in slots { - if let Some(slot_meta) = blockstore.meta(slot).unwrap() { + if let Some(slot_meta) = blockstore.meta_repair(slot).unwrap() { repair_eligibility.observe_slot_as_elapsed_for_tests(slot, &slot_meta); } } @@ -966,7 +970,7 @@ impl RepairService { pub(crate) fn generate_repairs_for_slot( blockstore: &Blockstore, slot: Slot, - slot_meta: &SlotMeta, + slot_meta: &SlotMetaRepair, repair_eligibility: &mut RepairEligibility, max_repairs: usize, outstanding_repairs: &mut HashMap, @@ -1022,7 +1026,7 @@ impl RepairService { let mut pending_slots = vec![slot]; while repairs.len() < max_repairs && !pending_slots.is_empty() { let slot = pending_slots.pop().unwrap(); - if let Some(slot_meta) = blockstore.meta(slot).unwrap() { + if let Some(slot_meta) = blockstore.meta_repair(slot).unwrap() { let new_repairs = Self::generate_repairs_for_slot( blockstore, slot, @@ -1202,11 +1206,11 @@ impl RepairService { } let meta = blockstore - .meta(slot) + .meta_repair(slot) .expect("Unable to lookup slot meta") - .unwrap_or(SlotMeta { + .unwrap_or(SlotMetaRepair { slot, - ..SlotMeta::default() + ..SlotMetaRepair::default() }); repair_eligibility.observe_slot_as_elapsed_for_tests(slot, &meta); @@ -1229,7 +1233,7 @@ impl RepairService { blockstore: &Blockstore, slot: Slot, ) -> Option> { - if let Some(slot_meta) = blockstore.meta(slot).unwrap() { + if let Some(slot_meta) = blockstore.meta_repair(slot).unwrap() { if slot_meta.is_full() { // If the slot is full, no further need to repair this slot None @@ -1647,7 +1651,7 @@ mod test { false, ) .unwrap(); - let slot_meta = blockstore.meta(slot).unwrap().unwrap(); + let slot_meta = blockstore.meta_repair(slot).unwrap().unwrap(); let mut repair_eligibility = RepairEligibility::default(); assert_eq!( RepairService::generate_repairs_for_slot( @@ -1686,7 +1690,7 @@ mod test { .insert_shreds(shreds_to_insert, None, false) .unwrap(); - let slot_meta = blockstore.meta(slot).unwrap().unwrap(); + let slot_meta = blockstore.meta_repair(slot).unwrap().unwrap(); let mut repair_eligibility = RepairEligibility::default(); assert_eq!( RepairService::generate_repairs_for_slot( @@ -1788,7 +1792,7 @@ mod test { .insert_shreds(vec![shreds_by_index.get(&0).unwrap().clone()], None, false) .unwrap(); let mut repair_eligibility = RepairEligibility::default(); - let slot_meta = blockstore.meta(0).unwrap().unwrap(); + let slot_meta = blockstore.meta_repair(0).unwrap().unwrap(); assert_eq!( RepairService::generate_repairs_for_slot( &blockstore, @@ -1804,7 +1808,7 @@ mod test { blockstore .insert_shreds(vec![shreds_by_index.get(&1).unwrap().clone()], None, false) .unwrap(); - let slot_meta = blockstore.meta(0).unwrap().unwrap(); + let slot_meta = blockstore.meta_repair(0).unwrap().unwrap(); assert_eq!( RepairService::generate_repairs_for_slot( &blockstore, diff --git a/core/src/repair/repair_weight.rs b/core/src/repair/repair_weight.rs index 635071f7852..ba92a931bc9 100644 --- a/core/src/repair/repair_weight.rs +++ b/core/src/repair/repair_weight.rs @@ -13,7 +13,8 @@ use { solana_epoch_schedule::EpochSchedule, solana_hash::Hash, solana_ledger::{ - ancestor_iterator::AncestorIterator, blockstore::Blockstore, blockstore_meta::SlotMeta, + ancestor_iterator::AncestorIterator, blockstore::Blockstore, + blockstore_meta::SlotMetaRepair, }, solana_measure::measure::Measure, solana_pubkey::Pubkey, @@ -502,7 +503,7 @@ impl RepairWeight { fn get_best_shreds( &mut self, blockstore: &Blockstore, - slot_meta_cache: &mut HashMap>, + slot_meta_cache: &mut HashMap>, repairs: &mut Vec, max_new_shreds: usize, repair_eligibility: &mut RepairEligibility, @@ -600,7 +601,7 @@ impl RepairWeight { fn get_best_unknown_last_index( &mut self, blockstore: &Blockstore, - slot_meta_cache: &mut HashMap>, + slot_meta_cache: &mut HashMap>, processed_slots: &mut HashSet, max_new_repairs: usize, outstanding_repairs: &mut HashMap, @@ -630,7 +631,7 @@ impl RepairWeight { fn get_best_closest_completion( &mut self, blockstore: &Blockstore, - slot_meta_cache: &mut HashMap>, + slot_meta_cache: &mut HashMap>, processed_slots: &mut HashSet, max_new_repairs: usize, repair_eligibility: &mut RepairEligibility, diff --git a/core/src/repair/repair_weighted_traversal.rs b/core/src/repair/repair_weighted_traversal.rs index 4c6a7234a53..9f4b19806cc 100644 --- a/core/src/repair/repair_weighted_traversal.rs +++ b/core/src/repair/repair_weighted_traversal.rs @@ -8,7 +8,7 @@ use { }, solana_clock::Slot, solana_hash::Hash, - solana_ledger::{blockstore::Blockstore, blockstore_meta::SlotMeta}, + solana_ledger::{blockstore::Blockstore, blockstore_meta::SlotMetaRepair}, std::collections::{HashMap, HashSet}, }; @@ -79,7 +79,7 @@ impl Iterator for RepairWeightTraversal<'_> { pub fn get_best_repair_shreds( tree: &HeaviestSubtreeForkChoice, blockstore: &Blockstore, - slot_meta_cache: &mut HashMap>, + slot_meta_cache: &mut HashMap>, repairs: &mut Vec, max_new_shreds: usize, repair_eligibility: &mut RepairEligibility, @@ -99,7 +99,7 @@ pub fn get_best_repair_shreds( let slot_meta = slot_meta_cache .entry(next.slot()) - .or_insert_with(|| blockstore.meta(next.slot()).unwrap()); + .or_insert_with(|| blockstore.meta_repair(next.slot()).unwrap()); // May not exist if blockstore purged the SlotMeta due to something // like duplicate slots. TODO: Account for duplicate slot may be in orphans, especially diff --git a/ledger/src/blockstore.rs b/ledger/src/blockstore.rs index 5dc397828d0..d805482414b 100644 --- a/ledger/src/blockstore.rs +++ b/ledger/src/blockstore.rs @@ -780,6 +780,15 @@ impl Blockstore { self.meta_cf.get(slot) } + /// Returns the [`SlotMetaRepair`] of the specified slot. + pub fn meta_repair(&self, slot: Slot) -> Result> { + self.meta_cf + .get_slice(slot)? + .as_deref() + .map(|bytes| Ok(wincode::deserialize::(bytes)?)) + .transpose() + } + /// Returns the SlotMeta of the specified slot from the specified location pub fn meta_from_location( &self, diff --git a/ledger/src/blockstore_meta.rs b/ledger/src/blockstore_meta.rs index 2868667f0fc..8d30a6e0d2d 100644 --- a/ledger/src/blockstore_meta.rs +++ b/ledger/src/blockstore_meta.rs @@ -185,6 +185,24 @@ pub struct SlotMetaV3 { pub type SlotMeta = SlotMetaV3; +/// Lighter-weight version of [`SlotMeta`] containing just the set +/// of fields needed for repair. +/// +/// wincode deserializes in field declaration order, so [`SlotMetaRepair`] +/// can always be deserialized from [`SlotMeta`]. +#[derive(Clone, Debug, Default, SchemaRead, Eq, PartialEq)] +pub struct SlotMetaRepair { + pub slot: Slot, + pub consumed: u64, + pub received: u64, + pub first_shred_timestamp: u64, + #[wincode(with = "wincode_compat::OptionCompat")] + pub last_index: Option, + #[wincode(with = "wincode_compat::OptionCompat")] + pub parent_slot: Option, + pub next_slots: Vec, +} + // Wincode implementation of serialize and deserialize for Option // where None is represented as u64::MAX; for backward compatibility. mod wincode_compat { @@ -517,32 +535,33 @@ impl FromIterator for ShredIndex { } } +fn slot_meta_is_full(last_index: Option, consumed: u64) -> bool { + // last_index is None when it has no information about how + // many shreds will fill this slot. + // Note: A full slot with zero shreds is not possible. + // Should never happen + if last_index.map(|ix| consumed > ix + 1).unwrap_or_default() { + datapoint_error!( + "blockstore_error", + ( + "error", + format!( + "Observed a slot meta with consumed: {} > meta.last_index + 1: {:?}", + consumed, + last_index.map(|ix| ix + 1), + ), + String + ) + ); + } + + Some(consumed) == last_index.map(|ix| ix + 1) +} + impl SlotMeta { + #[inline] pub fn is_full(&self) -> bool { - // last_index is None when it has no information about how - // many shreds will fill this slot. - // Note: A full slot with zero shreds is not possible. - // Should never happen - if self - .last_index - .map(|ix| self.consumed > ix + 1) - .unwrap_or_default() - { - datapoint_error!( - "blockstore_error", - ( - "error", - format!( - "Observed a slot meta with consumed: {} > meta.last_index + 1: {:?}", - self.consumed, - self.last_index.map(|ix| ix + 1), - ), - String - ) - ); - } - - Some(self.consumed) == self.last_index.map(|ix| ix + 1) + slot_meta_is_full(self.last_index, self.consumed) } /// Returns a boolean indicating whether this meta's parent slot is known. @@ -645,6 +664,13 @@ impl SlotMeta { } } +impl SlotMetaRepair { + #[inline] + pub fn is_full(&self) -> bool { + slot_meta_is_full(self.last_index, self.consumed) + } +} + impl ErasureMeta { pub(crate) fn from_coding_shred(shred: &Shred) -> Option { match shred.shred_type() { @@ -983,6 +1009,71 @@ mod test { } } + fn arb_slot_meta() -> impl Strategy { + ( + any::(), + any::(), + any::(), + any::(), + proptest::option::of(any::()), + proptest::option::of(any::()), + proptest::collection::vec(any::(), 0..32), + any::(), + proptest::collection::vec(0..MAX_DATA_SHREDS_PER_SLOT as u32, 0..64), + any::<[u8; HASH_BYTES]>(), + any::(), + ) + .prop_map( + |( + slot, + consumed, + received, + first_shred_timestamp, + last_index, + parent_slot, + next_slots, + connected_flags, + completed_data_indexes, + parent_block_id, + replay_fec_set_index, + )| SlotMeta { + slot, + consumed, + received, + first_shred_timestamp, + last_index, + parent_slot, + next_slots, + connected_flags: ConnectedFlags::from_bits_truncate(connected_flags), + completed_data_indexes: completed_data_indexes.into_iter().collect(), + parent_block_id: Hash::new_from_array(parent_block_id), + replay_fec_set_index, + }, + ) + } + + proptest! { + // Property: `SlotMetaRepair` is always deserializable from serialized `SlotMeta`. + #[test] + fn test_slot_meta_repair_deserialization( + slot_meta in arb_slot_meta(), + ) { + let serialized = wincode::serialize(&slot_meta).unwrap(); + let deserialized: SlotMetaRepair = wincode::deserialize(&serialized).unwrap(); + + prop_assert_eq!(deserialized.slot, slot_meta.slot); + prop_assert_eq!(deserialized.consumed, slot_meta.consumed); + prop_assert_eq!(deserialized.received, slot_meta.received); + prop_assert_eq!( + deserialized.first_shred_timestamp, + slot_meta.first_shred_timestamp + ); + prop_assert_eq!(deserialized.last_index, slot_meta.last_index); + prop_assert_eq!(deserialized.parent_slot, slot_meta.parent_slot); + prop_assert_eq!(deserialized.next_slots, slot_meta.next_slots); + } + } + #[test] fn test_shred_index_range_bounds() { let mut index = ShredIndex::default(); From 20b1191af605b5f8a04aa5fe6e94e774ec6ce21e Mon Sep 17 00:00:00 2001 From: Rocker Zhang Date: Sun, 14 Jun 2026 01:04:22 +0800 Subject: [PATCH 324/576] local-cluster: time-bound wait_for_duplicate_fork_frozen (#12998) The nested helper in test_duplicate_shreds_switch_failure has no timeout. If the duplicate fork validator never freezes dup_slot the helper loops forever and the test just hangs. Add a 60s assert mirroring the sibling helpers (:992 / :1585 / :1686 / :1724 / :2832). --- local-cluster/tests/local_cluster.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/local-cluster/tests/local_cluster.rs b/local-cluster/tests/local_cluster.rs index 687f63edd3e..c2d1b63cb4a 100644 --- a/local-cluster/tests/local_cluster.rs +++ b/local-cluster/tests/local_cluster.rs @@ -5225,11 +5225,16 @@ fn test_duplicate_shreds_switch_failure() { // Ensure all the slots <= dup_slot are also full so we know we can replay up to dup_slot // on restart info!("Waiting to receive and replay entire duplicate fork with tip {dup_slot}"); + let start = Instant::now(); loop { let duplicate_fork_validator_blockstore = open_blockstore(ledger_path); if let Some(frozen_hash) = duplicate_fork_validator_blockstore.get_bank_hash(dup_slot) { return frozen_hash; } + assert!( + start.elapsed() < Duration::from_secs(60), + "Timed out waiting to receive and replay duplicate fork with tip {dup_slot}", + ); sleep(Duration::from_millis(1000)); } } From a70a9d41902e58a28cf27e1f6c86ae5a6c4e3160 Mon Sep 17 00:00:00 2001 From: Andrew Fitzgerald Date: Sun, 14 Jun 2026 13:58:08 +0800 Subject: [PATCH 325/576] Remove special casing in calculate_reward_and_burn_fee_details (#13153) --- runtime/src/bank/fee_distribution.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/runtime/src/bank/fee_distribution.rs b/runtime/src/bank/fee_distribution.rs index 7c9815fac4e..1a9972d4294 100644 --- a/runtime/src/bank/fee_distribution.rs +++ b/runtime/src/bank/fee_distribution.rs @@ -69,10 +69,6 @@ impl Bank { // form of transaction fees as well. pub(super) fn distribute_transaction_fee_details(&self) { let fee_details = self.collector_fee_details.read().unwrap(); - if fee_details.total_transaction_fee() == 0 { - // nothing to distribute, exit early - return; - } let FeeDistribution { deposit, burn } = self.calculate_reward_and_burn_fee_details(&fee_details); @@ -103,10 +99,6 @@ impl Bank { &self, fee_details: &CollectorFeeDetails, ) -> FeeDistribution { - if fee_details.transaction_fee == 0 { - return FeeDistribution::default(); - } - let burn = fee_details.transaction_fee * self.burn_percent() / 100; let deposit = fee_details .priority_fee From 32f420fd3413c3480c97218b4da92183be8acfca Mon Sep 17 00:00:00 2001 From: steviez Date: Sun, 14 Jun 2026 02:58:34 -0500 Subject: [PATCH 326/576] Revert "Collapse unflushed roots (#13174)" (#13189) This reverts commit 3811bccb938540ab519ea65d7a404659d25593c3; one of the mnb canaries hit a debug assertion that this commit added --- accounts-db/src/accounts_cache.rs | 213 ++++++++++++++++----------- accounts-db/src/accounts_db.rs | 11 +- accounts-db/src/accounts_db/tests.rs | 26 ---- 3 files changed, 128 insertions(+), 122 deletions(-) diff --git a/accounts-db/src/accounts_cache.rs b/accounts-db/src/accounts_cache.rs index bd24e3907d2..f2bf7736e45 100644 --- a/accounts-db/src/accounts_cache.rs +++ b/accounts-db/src/accounts_cache.rs @@ -234,10 +234,12 @@ pub struct AccountsCache { cache: DashMap, BuildNoHashHasher>, // Index to find which slots a pubkey is stored in, to speed up lookups in load_latest index: AccountsCacheIndex, - // Rooted slots that may still have accounts in the write cache. A slot enters via `add_root` - // and is dropped either by `remove_slot` when its cache is flushed, or by - // `remove_unflushed_root` when a flush finds it had no cache. - unflushed_roots: RwLock>, + // Queue of potentially unflushed roots. Random eviction + cache too large + // could have triggered a flush of this slot already + maybe_unflushed_roots: RwLock>, + // Roots that have been claimed for flushing by `begin_flush_roots` but not yet + // cleared by `end_flush_roots` + roots_being_flushed: RwLock>, max_flushed_root: MaxFlushedRoot, /// The size of account data stored in the whole AccountsCache, in bytes total_size: Arc, @@ -265,7 +267,11 @@ impl AccountsCache { pub fn report_size(&self) { datapoint_info!( "accounts_cache_size", - ("num_roots", self.unflushed_roots.read().unwrap().len(), i64), + ( + "num_roots", + self.maybe_unflushed_roots.read().unwrap().len(), + i64 + ), ("num_slots", self.cache.len(), i64), ("total_size", self.size(), i64), ( @@ -318,8 +324,6 @@ impl AccountsCache { if let Some(slot_cache) = &result { self.index.remove(slot_cache.iter().map(|item| *item.key())); } - // If this slot was a root, it has now left the cache, so stop tracking it as unflushed. - self.unflushed_roots.write().unwrap().remove(&slot); result } @@ -357,13 +361,21 @@ impl AccountsCache { .unwrap_or(index_max_slot) .min(index_max_slot); - let r_unflushed_roots = self.unflushed_roots.read().unwrap(); - for &slot in r_unflushed_roots.range(..=max_root_slot).rev() { + let r_maybe_unflushed_roots = self.maybe_unflushed_roots.read().unwrap(); + for &slot in r_maybe_unflushed_roots.range(..=max_root_slot).rev() { + if let Some(account) = self.load(slot, pubkey) { + return Some((account, slot)); + } + } + drop(r_maybe_unflushed_roots); + + let r_roots_being_flushed = self.roots_being_flushed.read().unwrap(); + for &slot in r_roots_being_flushed.range(..=max_root_slot).rev() { if let Some(account) = self.load(slot, pubkey) { return Some((account, slot)); } } - drop(r_unflushed_roots); + drop(r_roots_being_flushed); // Found nothing, the version of the account in the cache must be on a different fork None @@ -374,25 +386,42 @@ impl AccountsCache { } pub fn add_root(&self, root: Slot) { - self.unflushed_roots.write().unwrap().insert(root); + self.maybe_unflushed_roots.write().unwrap().insert(root); } pub fn num_unflushed_roots(&self) -> usize { - self.unflushed_roots.read().unwrap().len() - } - - /// Returns the unflushed roots up to and including `max_root` (or all roots if `None`). The - /// returned roots remain tracked as unflushed until `remove_slot` drops each one when its cache - /// is flushed. - pub fn roots_to_flush(&self, max_root: Option) -> BTreeSet { - // `None` means no upper bound, i.e. every root. - let max_root = max_root.unwrap_or(Slot::MAX); - self.unflushed_roots - .read() - .unwrap() - .range(..=max_root) - .copied() - .collect() + self.maybe_unflushed_roots.read().unwrap().len() + } + + /// Atomically moves roots up to and including `max_root` (or all roots if `None`) out of + /// `maybe_unflushed_roots` and into `roots_being_flushed`, then returns them + /// + /// Panics if there are already roots being flushed (i.e. `end_flush_roots` was not called after + /// a previous `begin_flush_roots`) + pub fn begin_flush_roots(&self, max_root: Option) -> BTreeSet { + let mut w_maybe_unflushed_roots = self.maybe_unflushed_roots.write().unwrap(); + let mut w_roots_being_flushed = self.roots_being_flushed.write().unwrap(); + + assert!( + w_roots_being_flushed.is_empty(), + "begin_flush_roots called while roots are already being flushed; end_flush_roots must \ + be called first" + ); + + let roots_to_flush = if let Some(max_root) = max_root { + let remaining = w_maybe_unflushed_roots.split_off(&(max_root + 1)); + std::mem::replace(&mut *w_maybe_unflushed_roots, remaining) + } else { + std::mem::take(&mut *w_maybe_unflushed_roots) + }; + + *w_roots_being_flushed = roots_to_flush.clone(); + roots_to_flush + } + + /// Signals that flushing is complete. Clears the roots that were staged by `begin_flush_roots` + pub fn end_flush_roots(&self) { + self.roots_being_flushed.write().unwrap().clear(); } pub fn cached_frozen_slots(&self) -> Vec { @@ -432,27 +461,8 @@ impl AccountsCache { self.max_flushed_root.get() } - /// Stop tracking `slot` as an unflushed root without touching its cache. Used when a flushed - /// root had no cache for `remove_slot` to drop (e.g. genesis), so it would otherwise linger. - pub fn remove_unflushed_root(&self, slot: Slot) { - self.unflushed_roots.write().unwrap().remove(&slot); - } - pub fn set_max_flush_root(&self, root: Slot) { self.max_flushed_root.fetch_max(root); - // Every flushed root has left the cache via `remove_slot`, and any flushed root that had - // no cache was dropped by `remove_unflushed_root`. So once max_flushed_root advances, every - // remaining unflushed root must be > root; one at or below would be older than its - // just-flushed storage version yet still win in `load_latest`. - debug_assert!( - self.unflushed_roots - .read() - .unwrap() - .first() - .is_none_or(|&min_unflushed_root| min_unflushed_root > root), - "unflushed root {:?} is at or below the max flushed root {root}", - self.unflushed_roots.read().unwrap().first(), - ); } } @@ -651,25 +661,31 @@ mod tests { } /// Tests that `load_latest` returns the correct slot and account value - /// given various combinations of ancestor slots and root slots. + /// given various combinations of ancestor slots, root slots, and flushing state. /// /// Ancestors always take priority over roots regardless of slot // None case // `uncached_ancestors` are slots added to the Ancestors set but with no account // data stored in the cache. This lets us test root bounding by min_slot // without the ancestor path short-circuiting the lookup. - #[test_case(&[], &[], &[], None; "not ancestor not root")] - #[test_case(&[10], &[], &[], Some(10); "ancestor only")] - #[test_case(&[5, 10, 15], &[], &[], Some(15); "highest ancestor returned")] - #[test_case(&[], &[10, 20], &[], Some(20); "rooted, with no ancestors")] - #[test_case(&[5], &[20], &[], Some(5); "ancestor wins over higher root")] + #[test_case(&[], &[], &[], &[], None; "not ancestor not root")] + #[test_case(&[10], &[], &[], &[], Some(10); "ancestor only")] + #[test_case(&[5, 10, 15], &[], &[], &[], Some(15); "highest ancestor returned")] + #[test_case(&[], &[10, 20], &[], &[], Some(20); "rooted, with no ancestors")] + #[test_case(&[], &[], &[10], &[], Some(10); "root being flushed")] + #[test_case(&[10], &[], &[10], &[], Some(10); "ancestor being flushed")] + #[test_case(&[5], &[20], &[], &[], Some(5); "ancestor wins over higher root")] + #[test_case(&[], &[20], &[10], &[], Some(20);"unflushed root over flushing root")] + #[test_case(&[5], &[20], &[10], &[], Some(5);"ancestor over unflushed and flushing roots")] // Root beyond ancestors.min_slot() is excluded; older root still found - #[test_case(&[], &[5, 11], &[10], Some(5); "root beyond min ancestor excluded")] + #[test_case(&[], &[5, 11], &[], &[10], Some(5); "unflushed root beyond min ancestor excluded")] + #[test_case(&[], &[], &[5, 11], &[10], Some(5); "flushing root beyond min ancestor excluded")] // Root within min_slot bound is still returned - #[test_case(&[], &[10], &[15], Some(10); "root below min ancestor returned")] + #[test_case(&[], &[10], &[], &[15], Some(10); "unflushed root below min ancestor returned")] fn test_load_latest_slot_priority( ancestor_slots: &[Slot], - root_slots: &[Slot], + unflushed_root_slots: &[Slot], + flushing_root_slots: &[Slot], uncached_ancestors: &[Slot], expected: Option, ) { @@ -683,7 +699,7 @@ mod tests { AccountSharedData::new(slot, 0, &Pubkey::default()), ); } - for &slot in root_slots { + for &slot in unflushed_root_slots.iter().chain(flushing_root_slots) { cache.store( slot, &pk, @@ -691,6 +707,9 @@ mod tests { ); cache.add_root(slot); } + if let Some(&max) = flushing_root_slots.iter().max() { + cache.begin_flush_roots(Some(max)); + } let mut all_ancestors: Vec = ancestor_slots.to_vec(); all_ancestors.extend_from_slice(uncached_ancestors); @@ -722,75 +741,91 @@ mod tests { cache.store(10, &pk, AccountSharedData::new(100, 0, &Pubkey::default())); cache.add_root(10); - // A flush finishes a slot with `remove_slot`, which drops both its cache and its - // unflushed-root tracking; call it directly here to stand in for that flush. - cache.remove_slot(10); + cache.begin_flush_roots(None); + cache.end_flush_roots(); - // With the slot gone from the cache and untracked as a root, it is not visible. + // After clearing flushed roots, slot is no longer visible let empty = Ancestors::default(); assert!(cache.load_latest(&pk, &empty).is_none()); - assert!(cache.unflushed_roots.read().unwrap().is_empty()); } #[test] - fn test_roots_to_flush_with_max_root() { + fn test_begin_flush_roots_with_max_root() { let cache = AccountsCache::default(); cache.add_root(1); cache.add_root(3); cache.add_root(5); cache.add_root(7); - // roots_to_flush(Some(5)) returns {1, 3, 5}, excluding 7. - let to_flush = cache.roots_to_flush(Some(5)); - assert_eq!(to_flush, BTreeSet::from([1, 3, 5])); + // begin_flush_roots(Some(5)) should return {1, 3, 5} and leave {7} + let taken = cache.begin_flush_roots(Some(5)); + assert_eq!(taken, BTreeSet::from([1, 3, 5])); - // roots_to_flush only reads: the tracked roots are unchanged. - assert_eq!( - *cache.unflushed_roots.read().unwrap(), - BTreeSet::from([1, 3, 5, 7]) - ); + // Remaining unflushed roots should only contain 7 + assert_eq!(cache.maybe_unflushed_roots.read().unwrap().len(), 1); + assert!(cache.maybe_unflushed_roots.read().unwrap().contains(&7)); + + // Taken roots should now be in roots_being_flushed + assert!(cache.roots_being_flushed.read().unwrap().contains(&1)); + assert!(cache.roots_being_flushed.read().unwrap().contains(&3)); + assert!(cache.roots_being_flushed.read().unwrap().contains(&5)); + assert!(!cache.roots_being_flushed.read().unwrap().contains(&7)); + + cache.end_flush_roots(); } #[test] - fn test_roots_to_flush_none_takes_all() { + fn test_begin_flush_roots_none_takes_all() { let cache = AccountsCache::default(); cache.add_root(2); cache.add_root(4); cache.add_root(6); - let to_flush = cache.roots_to_flush(None); - assert_eq!(to_flush, BTreeSet::from([2, 4, 6])); + let taken = cache.begin_flush_roots(None); + assert_eq!(taken, BTreeSet::from([2, 4, 6])); + + // All unflushed roots should be drained + assert!(cache.maybe_unflushed_roots.read().unwrap().is_empty()); - // roots_to_flush only reads: the tracked roots are unchanged. + // All should be in roots_being_flushed assert_eq!( - *cache.unflushed_roots.read().unwrap(), + *cache.roots_being_flushed.read().unwrap(), BTreeSet::from([2, 4, 6]) ); + + cache.end_flush_roots(); } #[test] - fn test_remove_slot_drops_unflushed_root() { + #[should_panic(expected = "begin_flush_roots called while roots are already being flushed")] + fn test_begin_flush_roots_panics_if_already_flushing() { let cache = AccountsCache::default(); - let pk = Pubkey::new_unique(); - cache.store(1, &pk, AccountSharedData::new(1, 0, &Pubkey::default())); cache.add_root(1); - cache.store(2, &pk, AccountSharedData::new(2, 0, &Pubkey::default())); + cache.begin_flush_roots(None); + // Calling again without end_flush_roots should panic cache.add_root(2); - - // remove_slot drops slot 1 from both the cache and the tracked roots, leaving slot 2. - cache.remove_slot(1); - assert_eq!(*cache.unflushed_roots.read().unwrap(), BTreeSet::from([2])); + cache.begin_flush_roots(None); } - /// Advancing `max_flushed_root` past a still-tracked unflushed root is forbidden: that root - /// would be older than its just-flushed storage version yet still win in `load_latest`. #[test] - #[should_panic(expected = "unflushed root Some(3) is at or below the max flushed root 5")] - fn test_set_max_flush_root_below_unflushed_root_panics() { + fn test_end_flush_roots_allows_next_flush() { let cache = AccountsCache::default(); + cache.add_root(1); + cache.add_root(2); + + // First cycle + let taken = cache.begin_flush_roots(None); + assert_eq!(taken, BTreeSet::from([1, 2])); + + cache.end_flush_roots(); + + // After clear, roots_being_flushed is empty + assert!(cache.roots_being_flushed.read().unwrap().is_empty()); + + // Second cycle works without panic cache.add_root(3); - cache.add_root(7); - // Root 3 is still tracked as unflushed; moving max_flushed_root to 5 leaves it stranded below. - cache.set_max_flush_root(5); + let taken = cache.begin_flush_roots(None); + assert_eq!(taken, BTreeSet::from([3])); + cache.end_flush_roots(); } } diff --git a/accounts-db/src/accounts_db.rs b/accounts-db/src/accounts_db.rs index 2b25609a493..118aced28b2 100644 --- a/accounts-db/src/accounts_db.rs +++ b/accounts-db/src/accounts_db.rs @@ -4693,7 +4693,7 @@ impl AccountsDb { ) -> (usize, usize, FlushStats) { // Always flush up to `requested_flush_root`, which is necessary for things like snapshotting. let flushed_roots: BTreeSet = - self.accounts_cache.roots_to_flush(requested_flush_root); + self.accounts_cache.begin_flush_roots(requested_flush_root); let max_flush_root = flushed_roots.last().copied(); let num_new_roots = flushed_roots.len(); @@ -4721,17 +4721,14 @@ impl AccountsDb { if let Some(stats) = self.flush_slot_cache(root, &pubkeys_to_flush[&root]) { num_roots_flushed += 1; flush_stats.accumulate(&stats); - } else { - // A root with no cache to flush (e.g. genesis, whose accounts load straight to - // storage) was still tracked by `add_root`. `flush_slot_cache` couldn't drop it - // via `remove_slot`, so untrack it here to keep it from lingering as an unflushed - // root at or below max_flushed_root. - self.accounts_cache.remove_unflushed_root(root); } } max_flush_root.inspect(|&root| self.accounts_cache.set_max_flush_root(root)); + // Now that all the rooted slots are flushed, we can clear the roots from the cache + self.accounts_cache.end_flush_roots(); + (num_new_roots, num_roots_flushed, flush_stats) } diff --git a/accounts-db/src/accounts_db/tests.rs b/accounts-db/src/accounts_db/tests.rs index 221cf462aa0..5c5aab07dd1 100644 --- a/accounts-db/src/accounts_db/tests.rs +++ b/accounts-db/src/accounts_db/tests.rs @@ -4203,32 +4203,6 @@ fn test_flush_rooted_accounts_cache_with_clean() { fn test_flush_rooted_accounts_cache_without_clean() { run_flush_rooted_accounts_cache(false); } - -/// A rooted slot with no write-cache entry (e.g. genesis, whose accounts load straight to storage) -/// is still tracked by `add_root`. Flushing must untrack it rather than leave it stranded in -/// `unflushed_roots` at or below `max_flushed_root`. -#[test] -fn test_flush_untracks_cacheless_root() { - let db = AccountsDb::new_single_for_tests(); - - // Slot 0: rooted but never written to the write cache. - db.accounts_cache.add_root(0); - - // Slot 10: a normal rooted slot with a cached account. - let pubkey = Pubkey::new_unique(); - db.accounts_cache.store( - 10, - &pubkey, - AccountSharedData::new(10, 0, &Pubkey::default()), - ); - db.add_root(10); - - // Flushing through slot 10 must drop the cacheless root 0 instead of stranding it below - // max_flushed_root (which would otherwise trip the unflushed-root invariant). - db.flush_accounts_cache(true, Some(10)); - - assert_eq!(db.accounts_cache.num_unflushed_roots(), 0); -} #[test] fn test_shrink_unref() { let db = AccountsDb::new_single_for_tests(); From b5e2612c8501ce13a587ca6bdb236ed382b576be Mon Sep 17 00:00:00 2001 From: kirill lykov Date: Sun, 14 Jun 2026 11:22:11 +0200 Subject: [PATCH 327/576] xdp: add kernel, driver, zc, nic vendor/model to metrics (#13067) Problem is that we don't have information about kernel, driver, NIC vendor/model and ZC configuration in the metrics. This PR introduces these metrics reported hourly. This PR introduces such metrics. --- core/src/system_monitor_service.rs | 334 ++++++++++++++++++++++++- core/src/validator.rs | 6 +- ledger-tool/src/main.rs | 2 + local-cluster/src/validator_configs.rs | 1 + validator/src/commands/run/execute.rs | 75 +++--- 5 files changed, 386 insertions(+), 32 deletions(-) diff --git a/core/src/system_monitor_service.rs b/core/src/system_monitor_service.rs index c1a3c6dede2..c1c0ab71ed7 100644 --- a/core/src/system_monitor_service.rs +++ b/core/src/system_monitor_service.rs @@ -5,7 +5,13 @@ use core::arch::x86_64::{__cpuid, __cpuid_count, __get_cpuid_max, CpuidResult}; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] use num_enum::{IntoPrimitive, TryFromPrimitive}; #[cfg(target_os = "linux")] -use std::{fs::File, io::BufReader}; +use { + agave_xdp::device::NetworkDevice, + std::{ + fs::{self, File}, + io::{self, BufReader, ErrorKind}, + }, +}; use { solana_time_utils::AtomicInterval, std::{ @@ -26,6 +32,7 @@ const MS_PER_M: u64 = MS_PER_S * 60; const MS_PER_H: u64 = MS_PER_M * 60; const SAMPLE_INTERVAL_UDP_MS: u64 = 2 * MS_PER_S; const SAMPLE_INTERVAL_OS_NETWORK_LIMITS_MS: u64 = MS_PER_H; +const SAMPLE_INTERVAL_XDP_NETWORK_CONFIG_MS: u64 = MS_PER_H; const SAMPLE_INTERVAL_MEM_MS: u64 = 5 * MS_PER_S; const SAMPLE_INTERVAL_CPU_MS: u64 = 10 * MS_PER_S; const SAMPLE_INTERVAL_CPU_ID_MS: u64 = MS_PER_H; @@ -38,6 +45,22 @@ const PROC_NET_SNMP_PATH: &str = "/proc/net/snmp"; const PROC_NET_DEV_PATH: &str = "/proc/net/dev"; #[cfg(target_os = "linux")] const SYS_BLOCK_PATH: &str = "/sys/block"; +#[cfg(target_os = "linux")] +const PCI_IDS_PATHS: &[&str] = &["/usr/share/hwdata/pci.ids", "/usr/share/misc/pci.ids"]; + +#[derive(Clone, Debug)] +pub struct XdpNetworkConfigReport { + pub zero_copy: bool, + pub interface: String, +} + +#[cfg_attr(not(target_os = "linux"), allow(dead_code))] +struct XdpNetworkConfigMetrics { + kernel_version: String, + driver: String, + vendor: String, + model: String, +} pub struct SystemMonitorService { thread_hdl: JoinHandle<()>, @@ -297,6 +320,157 @@ fn parse_net_dev_stats(reader_dev: &mut impl BufRead) -> Result Result<(String, String), String> { + let class = match read_network_device_sysfs_value(interface, "class") { + Ok(class) => class, + Err(err) => { + return Err(err.to_string()); + } + }; + if !normalize_pci_id(&class).is_some_and(|class| class.starts_with("02")) { + return Err(format!( + "xdp network config device for interface {interface} has non-network PCI class {class}" + )); + } + + let (Some(vendor_id), Some(model_id)) = + (normalize_pci_id(vendor_id), normalize_pci_id(model_id)) + else { + return Err(format!( + "failed to normalize xdp network config PCI ids for interface {interface}: \ + vendor_id={vendor_id} model_id={model_id}" + )); + }; + + match read_pci_database_device_names(&vendor_id, &model_id) { + Ok(Some((vendor, model))) => Ok((vendor, model)), + Ok(None) => Err(format!( + "failed to find xdp network config PCI names for vendor_id={vendor_id} \ + model_id={model_id}" + )), + Err(err) => Err(format!( + "failed to read PCI database for xdp network config: {err}" + )), + } +} + +#[cfg(target_os = "linux")] +fn read_network_device_sysfs_value(interface: &str, file_name: &str) -> io::Result { + let path = format!("/sys/class/net/{interface}/device/{file_name}"); + let value = fs::read_to_string(&path) + .map_err(|e| { + io::Error::new( + e.kind(), + format!("Failed to read {file_name} for interface {interface}: {e}"), + ) + })? + .trim() + .to_string(); + + if value.is_empty() { + return Err(io::Error::new( + ErrorKind::InvalidData, + format!("Empty {file_name} for interface {interface}"), + )); + } + + Ok(value) +} + +#[cfg(target_os = "linux")] +fn normalize_pci_id(value: &str) -> Option { + let value = value + .strip_prefix("0x") + .or_else(|| value.strip_prefix("0X")) + .unwrap_or(value); + + (!value.is_empty() && value.chars().all(|char| char.is_ascii_hexdigit())) + .then(|| value.to_ascii_lowercase()) +} + +#[cfg(target_os = "linux")] +fn read_pci_database_device_names( + vendor_id: &str, + model_id: &str, +) -> io::Result> { + for path in PCI_IDS_PATHS { + let contents = match fs::read_to_string(path) { + Ok(contents) => contents, + Err(err) if err.kind() == ErrorKind::NotFound => continue, + Err(err) => { + return Err(io::Error::new( + err.kind(), + format!("Failed to read {path}: {err}"), + )); + } + }; + return Ok(parse_pci_database_device_names( + &contents, vendor_id, model_id, + )); + } + + Err(io::Error::new( + ErrorKind::NotFound, + format!("PCI database not found in {}", PCI_IDS_PATHS.join(", ")), + )) +} + +#[cfg_attr(not(target_os = "linux"), allow(dead_code))] +fn parse_pci_database_device_names( + contents: &str, + vendor_id: &str, + model_id: &str, +) -> Option<(String, String)> { + let mut matching_vendor_name: Option = None; + + for line in contents.lines() { + if line.is_empty() || line.starts_with('#') || line.starts_with("\t\t") { + continue; + } + // PCI class-code section starts, done with the vendor/device section. + if line.starts_with("C ") { + break; + } + + if let Some(device_line) = line.strip_prefix('\t') { + let Some(vendor_name) = matching_vendor_name.as_ref() else { + continue; + }; + + let Some((device_line_id, device_name)) = parse_pci_ids_line(device_line) else { + continue; + }; + if device_line_id.eq_ignore_ascii_case(model_id) { + return Some((vendor_name.clone(), device_name.to_string())); + } + continue; + } + + let Some((vendor_line_id, vendor_name)) = parse_pci_ids_line(line) else { + continue; + }; + matching_vendor_name = vendor_line_id + .eq_ignore_ascii_case(vendor_id) + .then(|| vendor_name.to_string()); + } + + None +} + +#[cfg_attr(not(target_os = "linux"), allow(dead_code))] +fn parse_pci_ids_line(line: &str) -> Option<(&str, &str)> { + let line = line.trim(); + let id_end = line.find(char::is_whitespace)?; + let id = &line[..id_end]; + let name = line[id_end..].trim(); + (!id.is_empty() && !name.is_empty()).then_some((id, name)) +} + #[cfg(target_os = "linux")] pub fn verify_net_stats_access() -> Result<(), String> { read_net_stats()?; @@ -386,6 +560,7 @@ fn parse_disk_stats(reader_diskstats: &mut impl BufRead) -> Result, pub report_os_cpu_stats: bool, pub report_os_disk_stats: bool, } @@ -989,11 +1164,13 @@ impl SystemMonitorService { let mut udp_stats = None; let mut disk_stats = None; let network_limits_timer = AtomicInterval::default(); + let xdp_network_config_timer = AtomicInterval::default(); let udp_timer = AtomicInterval::default(); let mem_timer = AtomicInterval::default(); let cpu_timer = AtomicInterval::default(); let cpuid_timer = AtomicInterval::default(); let disk_timer = AtomicInterval::default(); + let mut xdp_network_config_metrics = None; loop { if exit.load(Ordering::Relaxed) { @@ -1007,6 +1184,16 @@ impl SystemMonitorService { Self::process_net_stats(&mut udp_stats); } } + if let Some(xdp_network_config_report) = &config.xdp_network_config_report { + if xdp_network_config_timer + .should_update_ext(SAMPLE_INTERVAL_XDP_NETWORK_CONFIG_MS, false) + { + let metrics = xdp_network_config_metrics.get_or_insert_with(|| { + Self::load_xdp_network_config_metrics(xdp_network_config_report) + }); + Self::report_xdp_network_config(xdp_network_config_report, metrics); + } + } if config.report_os_memory_stats && mem_timer.should_update(SAMPLE_INTERVAL_MEM_MS) { Self::report_mem_stats(); #[cfg(not(any(target_env = "msvc", target_os = "freebsd")))] @@ -1028,6 +1215,114 @@ impl SystemMonitorService { } } + #[cfg(not(target_os = "linux"))] + fn report_xdp_network_config( + _config: &XdpNetworkConfigReport, + _metrics: &XdpNetworkConfigMetrics, + ) { + } + + #[cfg(target_os = "linux")] + fn load_xdp_network_config_metrics(config: &XdpNetworkConfigReport) -> XdpNetworkConfigMetrics { + let Ok(device) = NetworkDevice::new(&config.interface) else { + warn!( + "failed to get xdp network config device for interface {}", + config.interface + ); + return XdpNetworkConfigMetrics { + kernel_version: Self::kernel_version(), + driver: "unknown".to_string(), + vendor: "unknown".to_string(), + model: "unknown".to_string(), + }; + }; + let driver = device.driver().unwrap_or_else(|err| { + warn!( + "failed to get xdp network config driver for interface {}: {err}", + config.interface + ); + "unknown".to_string() + }); + let vendor_id = read_network_device_sysfs_value(&config.interface, "vendor") + .unwrap_or_else(|err| { + warn!( + "failed to get xdp network config vendor id for interface {}: {err}", + config.interface + ); + "unknown".to_string() + }); + let model_id = + read_network_device_sysfs_value(&config.interface, "device").unwrap_or_else(|err| { + warn!( + "failed to get xdp network config model id for interface {}: {err}", + config.interface + ); + "unknown".to_string() + }); + let (vendor, model) = + match try_resolve_network_device_pci_names(&config.interface, &vendor_id, &model_id) { + Ok((vendor, model)) => (vendor, model), + Err(err) => { + warn!( + "failed to resolve xdp network config PCI names for interface {}: {}", + config.interface, err + ); + // Fall back to reporting raw PCI IDs if name resolution fails + let fallback_vendor = + normalize_pci_id(&vendor_id).unwrap_or_else(|| vendor_id.clone()); + let fallback_model = + normalize_pci_id(&model_id).unwrap_or_else(|| model_id.clone()); + (fallback_vendor, fallback_model) + } + }; + + XdpNetworkConfigMetrics { + kernel_version: Self::kernel_version(), + driver, + vendor, + model, + } + } + + #[cfg(not(target_os = "linux"))] + fn load_xdp_network_config_metrics( + _config: &XdpNetworkConfigReport, + ) -> XdpNetworkConfigMetrics { + XdpNetworkConfigMetrics { + kernel_version: "unknown".to_string(), + driver: "unknown".to_string(), + vendor: "unknown".to_string(), + model: "unknown".to_string(), + } + } + + #[cfg(target_os = "linux")] + fn report_xdp_network_config( + config: &XdpNetworkConfigReport, + metrics: &XdpNetworkConfigMetrics, + ) { + solana_metrics::datapoint_info!( + "xdp-network-config", + "driver" => metrics.driver.clone(), + "zero_copy" => config.zero_copy.to_string(), + ("kernel_version", metrics.kernel_version.clone(), String), + ("vendor", metrics.vendor.clone(), String), + ("model", metrics.model.clone(), String), + ); + } + + #[cfg(target_os = "linux")] + fn kernel_version() -> String { + let mut utsname = unsafe { std::mem::zeroed::() }; + if unsafe { libc::uname(&mut utsname) } != 0 { + return format!("unknown: {}", std::io::Error::last_os_error()); + } + + unsafe { std::ffi::CStr::from_ptr(utsname.release.as_ptr()) } + .to_string_lossy() + .into_owned() + } + pub fn join(self) -> thread::Result<()> { self.thread_hdl.join() } @@ -1037,6 +1332,43 @@ impl SystemMonitorService { mod tests { use super::*; + #[test] + fn test_parse_pci_database_device_names() { + let pci_ids = "\ +8086 Intel Corporation +\t1593 Ethernet Controller E810-C for QSFP +\t\t8086 0001 Ethernet Network Adapter E810-C-Q1 +10ec Realtek Semiconductor Co., Ltd. +\t8136 RTL810xE PCI Express Fast Ethernet controller +C 02 Network controller +\t00 Ethernet controller +"; + + assert_eq!( + parse_pci_database_device_names(pci_ids, "8086", "1593"), + Some(( + "Intel Corporation".to_string(), + "Ethernet Controller E810-C for QSFP".to_string() + )) + ); + assert_eq!( + parse_pci_database_device_names(pci_ids, "10EC", "8136"), + Some(( + "Realtek Semiconductor Co., Ltd.".to_string(), + "RTL810xE PCI Express Fast Ethernet controller".to_string() + )) + ); + assert_eq!( + parse_pci_database_device_names(pci_ids, "8086", "0001"), + None + ); + assert_eq!( + parse_pci_database_device_names(pci_ids, "0000", "1593"), + None + ); + assert_eq!(parse_pci_database_device_names(pci_ids, "C", "02"), None); + } + #[test] fn test_parse_udp_stats() { const MOCK_SNMP: &[u8] = diff --git a/core/src/validator.rs b/core/src/validator.rs index 32c4af5b76f..1128292bead 100644 --- a/core/src/validator.rs +++ b/core/src/validator.rs @@ -24,7 +24,8 @@ use { snapshot_packager_service::SnapshotPackagerService, stats_reporter_service::StatsReporterService, system_monitor_service::{ - SystemMonitorService, SystemMonitorStatsReportConfig, verify_net_stats_access, + SystemMonitorService, SystemMonitorStatsReportConfig, XdpNetworkConfigReport, + verify_net_stats_access, }, tpu::{Tpu, TpuSockets}, tvu::{AlpenglowInitializationState, Tvu, TvuConfig, TvuSockets}, @@ -360,6 +361,7 @@ pub struct ValidatorConfig { pub no_poh_speed_test: bool, pub no_os_memory_stats_reporting: bool, pub no_os_network_stats_reporting: bool, + pub xdp_network_config_report: Option, pub no_os_cpu_stats_reporting: bool, pub no_os_disk_stats_reporting: bool, pub enforce_ulimit_nofile: bool, @@ -441,6 +443,7 @@ impl ValidatorConfig { no_poh_speed_test: true, no_os_memory_stats_reporting: true, no_os_network_stats_reporting: true, + xdp_network_config_report: None, no_os_cpu_stats_reporting: true, no_os_disk_stats_reporting: true, // No need to enforce nofile limit in tests @@ -893,6 +896,7 @@ impl Validator { SystemMonitorStatsReportConfig { report_os_memory_stats: !config.no_os_memory_stats_reporting, report_os_network_stats: !config.no_os_network_stats_reporting, + xdp_network_config_report: config.xdp_network_config_report.clone(), report_os_cpu_stats: !config.no_os_cpu_stats_reporting, report_os_disk_stats: !config.no_os_disk_stats_reporting, }, diff --git a/ledger-tool/src/main.rs b/ledger-tool/src/main.rs index be6b2a2a048..945ab2efa46 100644 --- a/ledger-tool/src/main.rs +++ b/ledger-tool/src/main.rs @@ -1852,6 +1852,7 @@ fn main() { SystemMonitorStatsReportConfig { report_os_memory_stats, report_os_network_stats: false, + xdp_network_config_report: None, report_os_cpu_stats: false, report_os_disk_stats: false, }, @@ -1990,6 +1991,7 @@ fn main() { SystemMonitorStatsReportConfig { report_os_memory_stats: true, report_os_network_stats: false, + xdp_network_config_report: None, report_os_cpu_stats: false, report_os_disk_stats: false, }, diff --git a/local-cluster/src/validator_configs.rs b/local-cluster/src/validator_configs.rs index ebd76009044..0c9eec39aef 100644 --- a/local-cluster/src/validator_configs.rs +++ b/local-cluster/src/validator_configs.rs @@ -45,6 +45,7 @@ pub fn safe_clone_config(config: &ValidatorConfig) -> ValidatorConfig { no_poh_speed_test: config.no_poh_speed_test, no_os_memory_stats_reporting: config.no_os_memory_stats_reporting, no_os_network_stats_reporting: config.no_os_network_stats_reporting, + xdp_network_config_report: config.xdp_network_config_report.clone(), no_os_cpu_stats_reporting: config.no_os_cpu_stats_reporting, no_os_disk_stats_reporting: config.no_os_disk_stats_reporting, enforce_ulimit_nofile: config.enforce_ulimit_nofile, diff --git a/validator/src/commands/run/execute.rs b/validator/src/commands/run/execute.rs index c8462da273a..37b441b9836 100644 --- a/validator/src/commands/run/execute.rs +++ b/validator/src/commands/run/execute.rs @@ -289,13 +289,14 @@ pub fn execute( let _ = config; #[cfg(target_os = "linux")] - let xdp_transmit_setup = { + let (xdp_transmit_setup, xdp_network_config_report) = { use { agave_xdp::transmitter::TransmitterBuilder, caps::{ CapSet, Capability::{CAP_BPF, CAP_NET_ADMIN, CAP_NET_RAW, CAP_PERFMON, CAP_SYS_NICE}, }, + solana_core::system_monitor_service::XdpNetworkConfigReport, }; let super::Config { primordial_caps } = config; @@ -371,33 +372,46 @@ pub fn execute( // XDP _MUST_ be setup _BEFORE_ the app spawns any threads to ensure linux // capabilities do not leak, leaving the process in a state where it could // potentially be used as a privilege escalation gadget - let xdp_transmit_setup = xdp_transmit_config.clone().map(|xdp_config| { - use { - agave_xdp::{default_device_ipv4, interface_ipv4}, - solana_core::validator::XdpTransmitSetup, - }; - - let src_ip = match node.bind_ip_addrs.active() { - IpAddr::V4(ip) if !ip.is_unspecified() => ip, - IpAddr::V4(_unspecified) => { - if let Some(interface) = xdp_config.interface.as_ref() { - interface_ipv4(interface).expect( - "configured interface should exist and have an IPv4 address assigned", - ) - } else { - default_device_ipv4().expect( - "default route device should exist and have an IPv4 address assigned", - ) - } - } - _ => panic!("IPv6 not supported"), - }; - XdpTransmitSetup { - transmitter_builder: TransmitterBuilder::new(xdp_config, exit.clone()) - .expect("failed to create xdp transmitter"), - src_ip, - } - }); + let (xdp_transmit_setup, report) = xdp_transmit_config + .clone() + .map(|mut xdp_config| { + use { + agave_xdp::{device::NetworkDevice, interface_ipv4}, + solana_core::validator::XdpTransmitSetup, + }; + + let device = if let Some(interface) = xdp_config.interface.as_ref() { + NetworkDevice::new(interface).expect("configured interface should exist") + } else { + NetworkDevice::new_from_default_route() + .expect("default route device should exist") + }; + + let xdp_interface = device.name().to_string(); + // Keep the transmitter and metrics on the selected XDP device. Source IP lookup + // uses the same interface name, with bond-master fallback. + xdp_config.interface = Some(xdp_interface.clone()); + let zero_copy = xdp_config.zero_copy; + let src_ip = match node.bind_ip_addrs.active() { + IpAddr::V4(ip) if !ip.is_unspecified() => ip, + IpAddr::V4(_unspecified) => interface_ipv4(&xdp_interface).expect( + "selected interface should exist and have an IPv4 address assigned", + ), + _ => panic!("IPv6 not supported"), + }; + ( + XdpTransmitSetup { + transmitter_builder: TransmitterBuilder::new(xdp_config, exit.clone()) + .expect("failed to create xdp transmitter"), + src_ip, + }, + XdpNetworkConfigReport { + zero_copy, + interface: xdp_interface, + }, + ) + }) + .map_or((None, None), |(setup, report)| (Some(setup), Some(report))); // we're done with caps needed to init xdp now. remove them from our process caps::set(None, CapSet::Effective, &retained_caps) @@ -405,11 +419,11 @@ pub fn execute( caps::set(None, CapSet::Permitted, &retained_caps) .expect("linux allows permitted capset to be set"); - xdp_transmit_setup + (xdp_transmit_setup, report) }; #[cfg(not(target_os = "linux"))] - let xdp_transmit_setup = None; + let (xdp_transmit_setup, xdp_network_config_report) = (None, None); #[cfg(target_os = "linux")] let poh_pinned_cpu_core = @@ -789,6 +803,7 @@ pub fn execute( no_poh_speed_test: matches.is_present("no_poh_speed_test"), no_os_memory_stats_reporting: matches.is_present("no_os_memory_stats_reporting"), no_os_network_stats_reporting: matches.is_present("no_os_network_stats_reporting"), + xdp_network_config_report, no_os_cpu_stats_reporting: matches.is_present("no_os_cpu_stats_reporting"), no_os_disk_stats_reporting: matches.is_present("no_os_disk_stats_reporting"), // The validator needs to open many files, check that the process has From 6ae276d23684e077bbf5052beda7bfac333f90c5 Mon Sep 17 00:00:00 2001 From: Akhilesh Singhania Date: Sun, 14 Jun 2026 22:04:35 +0200 Subject: [PATCH 328/576] Adds consensus team as owner of bls-sigverify crate (#13194) --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1524ca26c66..9b06b97eba2 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,8 +1,8 @@ # Please keep this file sorted /bloom/ @anza-xyz/networking /bls-cert-verify/ @anza-xyz/consensus +/bls-sigverify/ @anza-xyz/consensus /compute-budget-instruction/ @anza-xyz/fees -/core/src/bls_sigverify/ @anza-xyz/consensus /core/src/consensus.rs @anza-xyz/consensus /core/src/consensus/ @anza-xyz/consensus /core/src/repair/ @anza-xyz/networking @anza-xyz/consensus From f9996bea3e4ddfc820cf98da274df1ddad0eecec Mon Sep 17 00:00:00 2001 From: Akhilesh Singhania Date: Sun, 14 Jun 2026 22:26:55 +0200 Subject: [PATCH 329/576] Improves naming of fields in `VotePayload` and `CertPayload` (#13193) Improves naming of fields in VotePayload --- bls-sigverify/benches/bls_vote_sigverify.rs | 6 ++-- bls-sigverify/src/bls_cert_sigverify.rs | 6 ++-- bls-sigverify/src/bls_sigverifier.rs | 14 ++++++---- bls-sigverify/src/bls_vote_sigverify.rs | 31 +++++++++++---------- 4 files changed, 31 insertions(+), 26 deletions(-) diff --git a/bls-sigverify/benches/bls_vote_sigverify.rs b/bls-sigverify/benches/bls_vote_sigverify.rs index 7160d5a65c9..40bb8c21457 100644 --- a/bls-sigverify/benches/bls_vote_sigverify.rs +++ b/bls-sigverify/benches/bls_vote_sigverify.rs @@ -83,9 +83,9 @@ fn generate_test_data(num_distinct_messages: usize, batch_size: usize) -> Vec { - if banlist.ban(cert_payload.remote_pubkey, BAN_TIMEOUT) { + if banlist.ban(cert_payload.sender_identity_pubkey, BAN_TIMEOUT) { stats.already_banned += 1; } else { info!( "bls_cert_sigverify: banned sender={} due to error {e}", - cert_payload.remote_pubkey + cert_payload.sender_identity_pubkey ); } } diff --git a/bls-sigverify/src/bls_sigverifier.rs b/bls-sigverify/src/bls_sigverifier.rs index 0cd0fa02203..670bba519ed 100644 --- a/bls-sigverify/src/bls_sigverifier.rs +++ b/bls-sigverify/src/bls_sigverifier.rs @@ -223,19 +223,21 @@ impl SigVerifier { self.stats.num_malformed_pkts += 1; continue; }; - let Some(remote_pubkey) = packet.meta().remote_pubkey() else { + let Some(sender_identity_pubkey) = packet.meta().remote_pubkey() else { debug_assert!(false, "BLS packet missing remote pubkey"); self.stats.num_malformed_pkts += 1; continue; }; match msg { ConsensusMessage::Vote(vote) => { - if let Some((pubkey, bls_pubkey)) = self.keep_vote(&vote, root_bank) { + if let Some((sender_vote_account_pubkey, sender_bls_pubkey)) = + self.keep_vote(&vote, root_bank) + { votes.push(VotePayload { vote_message: vote, - bls_pubkey, - pubkey, - remote_pubkey, + sender_bls_pubkey, + sender_vote_account_pubkey, + sender_identity_pubkey, prepared_payload: None, }); } @@ -255,7 +257,7 @@ impl SigVerifier { } certs.push(CertPayload { cert, - remote_pubkey, + sender_identity_pubkey, }); } } diff --git a/bls-sigverify/src/bls_vote_sigverify.rs b/bls-sigverify/src/bls_vote_sigverify.rs index 3da3ea3f707..979581c7dbd 100644 --- a/bls-sigverify/src/bls_vote_sigverify.rs +++ b/bls-sigverify/src/bls_vote_sigverify.rs @@ -45,21 +45,21 @@ const PREPARED_PAYLOAD_CACHE_DISTINCT_VOTE_THRESHOLD_PERCENT: usize = 90; #[derive(Clone, Debug)] pub(super) struct VotePayload { pub vote_message: VoteMessage, - pub bls_pubkey: PopVerified, - pub pubkey: Pubkey, - pub remote_pubkey: Pubkey, + pub sender_bls_pubkey: PopVerified, + pub sender_vote_account_pubkey: Pubkey, + pub sender_identity_pubkey: Pubkey, pub prepared_payload: Option>, } impl VotePayload { fn verify(self) -> Option { let is_verified = if let Some(prepared_payload) = self.prepared_payload.as_deref() { - self.bls_pubkey + self.sender_bls_pubkey .verify_signature_prepared(&self.vote_message.signature, prepared_payload) .is_ok() } else { let payload = wincode::serialize(&self.vote_message.vote).ok()?; - self.bls_pubkey + self.sender_bls_pubkey .verify_signature(&self.vote_message.signature, &payload) .is_ok() }; @@ -111,7 +111,7 @@ fn inspect_for_repair(vote: &VotePayload, msgs_for_repair: &mut HashMap { msgs_for_repair - .entry(vote.pubkey) + .entry(vote.sender_vote_account_pubkey) .or_default() .push(vote_slot); } @@ -151,7 +151,7 @@ fn process_verified_votes( inspect_for_repair(&payload, &mut msgs_for_repair); votes_for_metrics.push(ConsensusMetricsEvent::Vote { - id: payload.pubkey, + id: payload.sender_vote_account_pubkey, vote: payload.vote_message.vote, }); votes_for_pool.push(payload.vote_message); @@ -208,11 +208,14 @@ fn verify_votes( distinct_payloads, thread_pool, )); - for remote_pubkey in invalid_remote_pubkeys { - if banlist.ban(remote_pubkey, BAN_TIMEOUT) { + for sender_identity_pubkey in invalid_remote_pubkeys { + if banlist.ban(sender_identity_pubkey, BAN_TIMEOUT) { stats.already_banned += 1; } else { - info!("bls_vote_sigverify: banned sender={remote_pubkey} due to failed verification"); + info!( + "bls_vote_sigverify: banned sender={sender_identity_pubkey} due to failed \ + verification" + ); } } stats.fn_verify_individual_votes_stats.add_sample(time_us); @@ -351,7 +354,7 @@ fn aggregate_pubkeys_by_payload( grouped_votes .entry(&v.vote_message.vote) .or_default() - .push(&v.bls_pubkey); + .push(&v.sender_bls_pubkey); } stats @@ -389,7 +392,7 @@ fn aggregate_pubkeys_by_payload( /// /// Returns: /// - `Vec`: votes that passed verification. -/// - `Vec`: remote pubkeys for votes that failed verification. +/// - `Vec`: senders' identity pubkeys for votes that failed verification. #[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))] fn verify_individual_votes( mut votes_to_verify: Vec, @@ -410,10 +413,10 @@ fn verify_individual_votes( thread_pool.install(|| { votes_to_verify.into_par_iter().partition_map(|vote| { - let remote_pubkey = vote.remote_pubkey; + let sender_identity_pubkey = vote.sender_identity_pubkey; match vote.verify() { Some(vote) => Either::Left(vote), - None => Either::Right(remote_pubkey), + None => Either::Right(sender_identity_pubkey), } }) }) From ee0f18a703803ac54518015a94ba6cd2bcf7ba01 Mon Sep 17 00:00:00 2001 From: cavemanloverboy <93507302+cavemanloverboy@users.noreply.github.com> Date: Sun, 14 Jun 2026 19:07:39 -0400 Subject: [PATCH 330/576] default delay leader block for pending fork (#13186) --- core/src/validator.rs | 2 +- validator/src/commands/run/args.rs | 16 ++++++++-------- validator/src/commands/run/execute.rs | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/core/src/validator.rs b/core/src/validator.rs index 1128292bead..6b800ca7f55 100644 --- a/core/src/validator.rs +++ b/core/src/validator.rs @@ -480,7 +480,7 @@ impl ValidatorConfig { replay_transactions_threads: NonZeroUsize::new(2).expect("2 is non-zero"), tvu_shred_sigverify_threads: NonZeroUsize::new(2).expect("2 is non-zero"), tvu_bls_sigverify_threads: NonZeroUsize::new(2).expect("2 is non-zero"), - delay_leader_block_for_pending_fork: false, + delay_leader_block_for_pending_fork: true, voting_service_test_override: None, repair_handler_type: RepairHandlerType::default(), snapshot_packager_niceness_adj: 0, diff --git a/validator/src/commands/run/args.rs b/validator/src/commands/run/args.rs index 6ac31cda870..2c125869d9f 100644 --- a/validator/src/commands/run/args.rs +++ b/validator/src/commands/run/args.rs @@ -1139,17 +1139,17 @@ pub fn add_args<'a>(app: App<'a, 'a>, default_args: &'a DefaultArgs) -> App<'a, .help("Disables the banking trace"), ) .arg( - Arg::with_name("delay_leader_block_for_pending_fork") + Arg::with_name("no_delay_leader_block_for_pending_fork") .hidden(hidden_unless_forced()) - .long("delay-leader-block-for-pending-fork") + .long("no-delay-leader-block-for-pending-fork") .takes_value(false) .help( - "Delay leader block creation while replaying a block which descends from the \ - current fork and has a lower slot than our next leader slot. If we don't delay \ - here, our new leader block will be on a different fork from the block we are \ - replaying and there is a high chance that the cluster will confirm that block's \ - fork rather than our leader block's fork because it was created before we \ - started creating ours.", + "Disable delaying leader block creation while replaying a block which descends \ + from the current fork and has a lower slot than our next leader slot. If we \ + don't delay here, our new leader block will be on a different fork from the \ + block we are replaying and there is a high chance that the cluster will confirm \ + that block's fork rather than our leader block's fork because it was created \ + before we started creating ours.", ), ) .arg( diff --git a/validator/src/commands/run/execute.rs b/validator/src/commands/run/execute.rs index 37b441b9836..5d1d7a92c85 100644 --- a/validator/src/commands/run/execute.rs +++ b/validator/src/commands/run/execute.rs @@ -833,8 +833,8 @@ pub fn execute( replay_transactions_threads, tvu_shred_sigverify_threads: tvu_sigverify_threads, tvu_bls_sigverify_threads, - delay_leader_block_for_pending_fork: matches - .is_present("delay_leader_block_for_pending_fork"), + delay_leader_block_for_pending_fork: !matches + .is_present("no_delay_leader_block_for_pending_fork"), turbine_mode: TurbineMode::default(), broadcast_stage_type: BroadcastStageType::Standard, block_verification_method: value_t_or_exit!( From aa61dbb5b9797417467be7540dce5c03068a2543 Mon Sep 17 00:00:00 2001 From: Akhilesh Singhania Date: Mon, 15 Jun 2026 02:14:49 +0200 Subject: [PATCH 331/576] nit: asserts total_stakes != 0 earlier (#13093) --- bls-sigverify/src/bls_cert_sigverify.rs | 7 +++---- runtime/src/bank.rs | 6 ++++-- runtime/src/block_component_processor.rs | 4 ++-- runtime/src/validated_block_finalization.rs | 13 +++++-------- 4 files changed, 14 insertions(+), 16 deletions(-) diff --git a/bls-sigverify/src/bls_cert_sigverify.rs b/bls-sigverify/src/bls_cert_sigverify.rs index 870656724d5..e3e821bbc14 100644 --- a/bls-sigverify/src/bls_cert_sigverify.rs +++ b/bls-sigverify/src/bls_cert_sigverify.rs @@ -18,7 +18,7 @@ use { solana_pubkey::Pubkey, solana_runtime::bank::Bank, solana_streamer::nonblocking::simple_qos::SimpleQosBanlist, - std::{collections::HashSet, num::NonZeroU64}, + std::{collections::HashSet, num::NonZero}, thiserror::Error, }; @@ -163,17 +163,16 @@ fn verify_cert(cert: &Certificate, root_bank: &Bank) -> Result<(), CertVerifyErr }); } let (aggregate_stake, total_stake) = root_bank.verify_certificate(cert)?; - debug_assert!(aggregate_stake <= total_stake); + debug_assert!(aggregate_stake <= total_stake.get()); verify_stake(cert, aggregate_stake, total_stake) } fn verify_stake( cert: &Certificate, aggregate_stake: u64, - total_stake: u64, + total_stake: NonZero, ) -> Result<(), CertVerifyError> { let (required_fraction, _) = cert.cert_type.limits_and_vote_types(); - let total_stake = NonZeroU64::new(total_stake).expect("Total stake cannot be zero"); let cert_fraction = Fraction::new(aggregate_stake, total_stake); if cert_fraction >= required_fraction { Ok(()) diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index ce16ab11356..6d8c20c2b52 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -196,6 +196,7 @@ use { cmp::Ordering, collections::{HashMap, HashSet}, fmt, + num::NonZero, ops::AddAssign, path::PathBuf, slice, @@ -5700,13 +5701,14 @@ impl Bank { pub fn verify_certificate( &self, cert: &Certificate, - ) -> std::result::Result<(u64, u64), CertVerifyError> { + ) -> std::result::Result<(u64, NonZero), CertVerifyError> { let slot = cert.cert_type.slot(); let epoch_stakes = self .epoch_stakes_from_slot(slot) .ok_or(CertVerifyError::MissingRankMap)?; let key_to_rank_map = epoch_stakes.bls_pubkey_to_rank_map(); - let total_stake = key_to_rank_map.total_stake(); + let total_stake = + NonZero::new(key_to_rank_map.total_stake()).expect("total stake cannot be 0"); let stake = cert_verify::verify_certificate(cert, key_to_rank_map.len(), |rank| { key_to_rank_map diff --git a/runtime/src/block_component_processor.rs b/runtime/src/block_component_processor.rs index 0ff98f8dda6..9321313b10f 100644 --- a/runtime/src/block_component_processor.rs +++ b/runtime/src/block_component_processor.rs @@ -25,7 +25,7 @@ use { }, solana_hash::Hash, solana_pubkey::Pubkey, - std::{collections::HashSet, num::NonZeroU64, sync::Arc}, + std::{collections::HashSet, sync::Arc}, thiserror::Error, }; @@ -296,7 +296,7 @@ impl BlockComponentProcessor { BlockComponentProcessorError::GenesisCertificateFailedVerification })?; - let genesis_percent = Fraction::new(genesis_stake, NonZeroU64::new(total_stake).unwrap()); + let genesis_percent = Fraction::new(genesis_stake, total_stake); if genesis_percent < GENESIS_VOTE_THRESHOLD { warn!( "Received a genesis certificate for slot {cert_slot} in bank slot {} with \ diff --git a/runtime/src/validated_block_finalization.rs b/runtime/src/validated_block_finalization.rs index 8ad47918032..ef339cda36b 100644 --- a/runtime/src/validated_block_finalization.rs +++ b/runtime/src/validated_block_finalization.rs @@ -17,7 +17,7 @@ use { solana_entry::block_component::{BlockFinalizationCert, VotesAggregate}, solana_pubkey::Pubkey, solana_signer_store::{DecodeError, Decoded, decode}, - std::{collections::HashSet, num::NonZeroU64}, + std::{collections::HashSet, num::NonZero}, thiserror::Error, }; @@ -125,10 +125,8 @@ impl ValidatedBlockFinalizationCert { let (notarize_stake, total_stake) = Self::verify_certificate(bank, ¬arize_cert)?; let (finalize_stake, _) = Self::verify_certificate(bank, &finalize_cert)?; - let notarize_percent = - Fraction::new(notarize_stake, NonZeroU64::new(total_stake).unwrap()); - let finalize_percent = - Fraction::new(finalize_stake, NonZeroU64::new(total_stake).unwrap()); + let notarize_percent = Fraction::new(notarize_stake, total_stake); + let finalize_percent = Fraction::new(finalize_stake, total_stake); let notarize_threshold = notarize_cert.cert_type.limits_and_vote_types().0; let finalize_threshold = finalize_cert.cert_type.limits_and_vote_types().0; @@ -186,8 +184,7 @@ impl ValidatedBlockFinalizationCert { let (finalize_stake, total_stake) = Self::verify_certificate(bank, &fast_finalize_cert)?; - let finalize_percent = - Fraction::new(finalize_stake, NonZeroU64::new(total_stake).unwrap()); + let finalize_percent = Fraction::new(finalize_stake, total_stake); let finalize_threshold = fast_finalize_cert.cert_type.limits_and_vote_types().0; if finalize_percent < finalize_threshold { @@ -362,7 +359,7 @@ impl ValidatedBlockFinalizationCert { fn verify_certificate( bank: &Bank, cert: &Certificate, - ) -> Result<(u64, u64), BlockFinalizationCertError> { + ) -> Result<(u64, NonZero), BlockFinalizationCertError> { bank.verify_certificate(cert) .map_err(|_| BlockFinalizationCertError::SignatureVerificationFailed(cert.cert_type)) } From 7f70cf81ebb62590bfcd6c0064cafc303e668d4a Mon Sep 17 00:00:00 2001 From: Kamil Skalski Date: Mon, 15 Jun 2026 09:58:53 +0200 Subject: [PATCH 332/576] perf: skip writing untouched accounts (#13079) * perf: skip writing untouched accounts * Simplify by passing through touched_flags * Replace Box<[Cell]> with Box<[bool]> once owned in LoadedTransaction * Use safe, but in-place into_iter collect * Move Box of Cell conversion to fn from() function * Include fee payer in execute_timings.details.touched_account_count --- runtime/src/account_saver.rs | 95 +++++++++++++++++++++++++- svm/src/account_loader.rs | 68 ++++++++++++++++++ svm/src/transaction_processor.rs | 18 ++++- transaction-context/src/transaction.rs | 20 ++++-- 4 files changed, 191 insertions(+), 10 deletions(-) diff --git a/runtime/src/account_saver.rs b/runtime/src/account_saver.rs index 6e554ef1810..78eb22a1630 100644 --- a/runtime/src/account_saver.rs +++ b/runtime/src/account_saver.rs @@ -80,6 +80,7 @@ pub fn collect_accounts_to_store<'a, T: SVMMessage>( transaction, transaction_ref, &executed_tx.loaded_transaction.accounts, + &executed_tx.loaded_transaction.touched_flags, ); } else { collect_accounts_for_failed_tx( @@ -109,12 +110,18 @@ fn collect_accounts_for_successful_tx<'a, T: SVMMessage>( transaction: &'a T, transaction_ref: Option<&'a SanitizedTransaction>, transaction_accounts: &'a [KeyedAccountSharedData], + touched_flags: &[bool], ) { for (i, (address, account)) in (0..transaction.account_keys().len()).zip(transaction_accounts) { if !transaction.is_writable(i) { continue; } + // Skip write-locked accounts the transaction left unmodified. + if !touched_flags[i] { + continue; + } + // Accounts that are invoked and also not passed as an instruction // account to a program don't need to be stored because it's assumed // to be impossible for a committable transaction to modify an @@ -174,9 +181,15 @@ mod tests { solana_system_interface::{instruction as system_instruction, program as system_program}, solana_transaction::{Transaction, sanitized::SanitizedTransaction}, solana_transaction_error::{TransactionError, TransactionResult as Result}, - std::collections::HashMap, + std::collections::{HashMap, HashSet}, }; + /// Builds touched flags for `num_total` accounts with the first + /// `num_touched` of them marked as touched. + fn touched_flags_for_test(num_touched: usize, num_total: usize) -> Box<[bool]> { + (0..num_total).map(|index| index < num_touched).collect() + } + fn new_sanitized_tx( from_keypairs: &T, message: Message, @@ -252,16 +265,22 @@ mod tests { ]; let tx1 = new_sanitized_tx(&[&keypair1], message, Hash::default()); + let touched0 = + touched_flags_for_test(transaction_accounts0.len(), transaction_accounts0.len()); let loaded0 = LoadedTransaction { accounts: transaction_accounts0, + touched_flags: touched0, fee_details: FeeDetails::default(), rollback_accounts: RollbackAccounts::default(), compute_budget: SVMTransactionExecutionBudget::default(), loaded_accounts_data_size: 0, }; + let touched1 = + touched_flags_for_test(transaction_accounts1.len(), transaction_accounts1.len()); let loaded1 = LoadedTransaction { accounts: transaction_accounts1, + touched_flags: touched1, fee_details: FeeDetails::default(), rollback_accounts: RollbackAccounts::default(), compute_budget: SVMTransactionExecutionBudget::default(), @@ -303,6 +322,65 @@ mod tests { } } + #[test] + fn test_collect_accounts_to_store_skips_untouched_accounts() { + let fee_payer = Keypair::new(); + let (touched_key, untouched_key) = (solana_pubkey::new_rand(), solana_pubkey::new_rand()); + + let fee_payer_account = AccountSharedData::new(100, 0, &Pubkey::default()); + let touched_account = AccountSharedData::new(1, 0, &Pubkey::default()); + let untouched_account = AccountSharedData::new(2, 0, &Pubkey::default()); + let program_account = AccountSharedData::new(3, 0, &Pubkey::default()); + + // Writable accounts at indices 0, 1, 2, plus a readonly program at index 3. + let instructions = vec![CompiledInstruction::new(3, &(), vec![1, 2])]; + let message = Message::new_with_compiled_instructions( + 1, // num_required_signatures + 0, // num_readonly_signed_accounts + 1, // num_readonly_unsigned_accounts -> only the program is readonly + vec![ + fee_payer.pubkey(), + touched_key, + untouched_key, + native_loader::id(), + ], + Hash::default(), + instructions, + ); + let transaction_accounts = vec![ + (message.account_keys[0], fee_payer_account), + (message.account_keys[1], touched_account), + (message.account_keys[2], untouched_account), + (message.account_keys[3], program_account), + ]; + let tx = new_sanitized_tx(&[&fee_payer], message, Hash::default()); + + // The processor marks the fee payer (index 0) and every account the VM + // modified. Here only index 1 was modified; the writable account at + // index 2 was left untouched and must not be written back. + let loaded = LoadedTransaction { + touched_flags: touched_flags_for_test(2, transaction_accounts.len()), + accounts: transaction_accounts, + ..Default::default() + }; + + let txs = vec![tx]; + let processing_results = vec![new_executed_processing_result(Ok(()), loaded)]; + + let transaction_refs: Option> = None; + let (collected_accounts, _transactions) = + collect_accounts_to_store(&txs, &transaction_refs, &processing_results); + + // Only the fee payer and the touched writable account are stored; the + // untouched writable account and the readonly program are skipped. + let collected_keys = + HashSet::from_iter(collected_accounts.into_iter().map(|(pubkey, _act)| *pubkey)); + assert_eq!( + collected_keys, + HashSet::from([fee_payer.pubkey(), touched_key]), + ); + } + #[test] fn test_collect_accounts_for_failed_tx_rollback_fee_payer_only() { let from = keypair_from_seed(&[1; 32]).unwrap(); @@ -322,8 +400,13 @@ mod tests { let from_account_pre = AccountSharedData::new(4242, 0, &Pubkey::default()); + let touched_flags = + touched_flags_for_test(transaction_accounts.len(), transaction_accounts.len()); let loaded = LoadedTransaction { accounts: transaction_accounts, + // Worst case: every writable account appears modified, yet a failed + // tx must still persist only its rollback accounts. + touched_flags, fee_details: FeeDetails::default(), rollback_accounts: RollbackAccounts::FeePayerOnly { fee_payer: (from_address, from_account_pre.clone()), @@ -412,8 +495,13 @@ mod tests { AccountSharedData::new_data(42, &nonce_state, &system_program::id()).unwrap(); let from_account_pre = AccountSharedData::new(4242, 0, &Pubkey::default()); + let touched_flags = + touched_flags_for_test(transaction_accounts.len(), transaction_accounts.len()); let loaded = LoadedTransaction { accounts: transaction_accounts, + // Worst case: every writable account appears modified, yet a failed + // tx must still persist only its rollback accounts. + touched_flags, fee_details: FeeDetails::default(), rollback_accounts: RollbackAccounts::SeparateNonceAndFeePayer { nonce: (nonce_address, nonce_account_pre.clone()), @@ -519,8 +607,13 @@ mod tests { let nonce_account_pre = AccountSharedData::new_data(42, &nonce_state, &system_program::id()).unwrap(); + let touched_flags = + touched_flags_for_test(transaction_accounts.len(), transaction_accounts.len()); let loaded = LoadedTransaction { accounts: transaction_accounts, + // Worst case: every writable account appears modified, yet a failed + // tx must still persist only its rollback accounts. + touched_flags, fee_details: FeeDetails::default(), rollback_accounts: RollbackAccounts::SameNonceAndFeePayer { nonce: (nonce_address, nonce_account_pre.clone()), diff --git a/svm/src/account_loader.rs b/svm/src/account_loader.rs index fa81bf6cda3..5092b10cb4f 100644 --- a/svm/src/account_loader.rs +++ b/svm/src/account_loader.rs @@ -142,6 +142,9 @@ pub(crate) struct LoadedTransactionAccount { )] pub struct LoadedTransaction { pub accounts: Vec, + /// Parallel to `accounts`: whether each account must be written back. Empty + /// until execution. + pub touched_flags: Box<[bool]>, pub fee_details: FeeDetails, pub rollback_accounts: RollbackAccounts, pub(crate) compute_budget: SVMTransactionExecutionBudget, @@ -290,6 +293,7 @@ impl<'a, CB: TransactionProcessingCallback> AccountLoader<'a, CB> { &mut self, message: &impl SVMMessage, transaction_accounts: &[KeyedAccountSharedData], + touched_flags: &[bool], current_slot: Slot, ) { for (i, (address, account)) in (0..message.account_keys().len()).zip(transaction_accounts) { @@ -297,6 +301,11 @@ impl<'a, CB: TransactionProcessingCallback> AccountLoader<'a, CB> { continue; } + // Skip write-locked accounts the transaction left unmodified. + if !touched_flags[i] { + continue; + } + // Accounts that are invoked and also not passed as an instruction // account to a program don't need to be stored because it's assumed // to be impossible for a committable transaction to modify an @@ -419,6 +428,8 @@ pub(crate) fn load_transaction( match load_result { Ok(accounts) => TransactionLoadResult::Loaded(LoadedTransaction { accounts, + // Populated after execution by execute_loaded_transaction. + touched_flags: Box::default(), fee_details: tx_details.fee_details, rollback_accounts: tx_details.rollback_accounts, compute_budget: tx_details.compute_budget, @@ -796,6 +807,62 @@ mod tests { SanitizedMessage::Legacy(LegacyMessage::new(message, &HashSet::new())) } + #[test] + fn test_update_accounts_for_successful_tx_skips_untouched() { + let fee_payer = Keypair::new(); + let (touched_key, untouched_key) = (Pubkey::new_unique(), Pubkey::new_unique()); + let program = Pubkey::new_unique(); + + // Writable accounts at indices 0, 1, 2; readonly program at index 3. + let instructions = vec![CompiledInstruction::new(3, &(), vec![1, 2])]; + let message = new_unchecked_sanitized_message(Message::new_with_compiled_instructions( + 1, // num_required_signatures + 0, // num_readonly_signed_accounts + 1, // num_readonly_unsigned_accounts -> only the program is readonly + vec![fee_payer.pubkey(), touched_key, untouched_key, program], + Hash::default(), + instructions, + )); + let transaction_accounts = [ + ( + fee_payer.pubkey(), + AccountSharedData::new(100, 0, &Pubkey::default()), + ), + ( + touched_key, + AccountSharedData::new(1, 0, &Pubkey::default()), + ), + ( + untouched_key, + AccountSharedData::new(2, 0, &Pubkey::default()), + ), + (program, AccountSharedData::new(3, 0, &Pubkey::default())), + ]; + + // Fee payer (index 0) and the VM-modified account (index 1) are marked; + // the writable account at index 2 is left untouched. + let touched_flags: Box<[bool]> = [true, true, false, false].into(); + + let callbacks = TestCallbacks::default(); + let mut account_loader: AccountLoader = (&callbacks).into(); + account_loader.update_accounts_for_successful_tx( + &message, + &transaction_accounts, + &touched_flags, + 5, + ); + + // Only touched writable accounts propagate to the in-batch loader cache. + assert!( + account_loader + .loaded_accounts + .contains_key(&fee_payer.pubkey()) + ); + assert!(account_loader.loaded_accounts.contains_key(&touched_key)); + assert!(!account_loader.loaded_accounts.contains_key(&untouched_key)); + assert!(!account_loader.loaded_accounts.contains_key(&program)); + } + #[test] fn test_load_accounts_unknown_program_id() { let mut accounts: Vec = Vec::new(); @@ -2139,6 +2206,7 @@ mod tests { ), (key3.pubkey(), account_data), ], + touched_flags: Box::default(), fee_details: FeeDetails::default(), rollback_accounts: RollbackAccounts::default(), compute_budget: SVMTransactionExecutionBudget::default(), diff --git a/svm/src/transaction_processor.rs b/svm/src/transaction_processor.rs index 1cb1f381e01..222b4a72a93 100644 --- a/svm/src/transaction_processor.rs +++ b/svm/src/transaction_processor.rs @@ -569,6 +569,7 @@ impl TransactionBatchProcessor { account_loader.update_accounts_for_successful_tx( tx, &executed_tx.loaded_transaction.accounts, + &executed_tx.loaded_transaction.touched_flags, self.slot, ); // Also update local program cache with modifications made by the @@ -1036,10 +1037,20 @@ impl TransactionBatchProcessor { let ExecutionRecord { accounts, return_data, - touched_account_count, + mut touched_flags, accounts_resize_delta, } = execution_record; + // The fee payer (account index 0) is debited during loading, outside the + // VM, so it carries no VM touch flag but must still be written back. + if let Some(fee_payer_touched) = touched_flags.first_mut() { + *fee_payer_touched = true; + } + + // changed_account_count reflects every account that will be written back, + // including the fee payer marked above. + let touched_account_count = touched_flags.iter().filter(|touched| **touched).count(); + if post_account_state_info_result.is_ok() && transaction_accounts_lamports_sum(&accounts) .filter(|lamports_after_tx| lamports_before_tx == *lamports_after_tx) @@ -1065,8 +1076,9 @@ impl TransactionBatchProcessor { .unwrap_or_else(|err| (Err(err), None)); loaded_transaction.accounts = accounts; + loaded_transaction.touched_flags = touched_flags; execute_timings.details.total_account_count += loaded_transaction.accounts.len() as u64; - execute_timings.details.changed_account_count += touched_account_count; + execute_timings.details.changed_account_count += touched_account_count as u64; let return_data = if config.recording_config.enable_return_data_recording && !return_data.data.is_empty() @@ -1583,6 +1595,7 @@ mod tests { let loaded_transaction = LoadedTransaction { accounts: vec![(Pubkey::new_unique(), AccountSharedData::default())], + touched_flags: Box::default(), fee_details: FeeDetails::default(), rollback_accounts: RollbackAccounts::default(), compute_budget: SVMTransactionExecutionBudget::default(), @@ -1677,6 +1690,7 @@ mod tests { (key1, AccountSharedData::default()), (key2, AccountSharedData::default()), ], + touched_flags: Box::default(), fee_details: FeeDetails::default(), rollback_accounts: RollbackAccounts::default(), compute_budget: SVMTransactionExecutionBudget::default(), diff --git a/transaction-context/src/transaction.rs b/transaction-context/src/transaction.rs index 0ea8f81efe2..0e253561efd 100644 --- a/transaction-context/src/transaction.rs +++ b/transaction-context/src/transaction.rs @@ -612,7 +612,8 @@ pub struct TransactionReturnData { pub struct ExecutionRecord { pub accounts: Vec, pub return_data: TransactionReturnData, - pub touched_account_count: u64, + /// Parallel to `accounts`: whether each account was modified by the VM. + pub touched_flags: Box<[bool]>, pub accounts_resize_delta: i64, } @@ -623,11 +624,16 @@ impl From> for ExecutionRecord { let (accounts, touched_flags, resize_delta) = Rc::try_unwrap(context.accounts) .expect("transaction_context.accounts has unexpected outstanding refs") .take(); - let touched_account_count = touched_flags - .iter() - .fold(0usize, |accumulator, was_touched| { - accumulator.saturating_add(was_touched.get() as usize) - }) as u64; + + // The flags only needed interior mutability while the VM was running. + // Now that we own them, unwrap the per-element `Cell`s into a plain + // `Box<[bool]>`. `Vec::from` reuses the box's allocation and the mapped + // collect reuses that same buffer in place (`Cell` and `bool` have + // identical layout), so no reallocation occurs. + let touched_flags: Box<[bool]> = Vec::from(touched_flags) + .into_iter() + .map(|flag| flag.into_inner()) + .collect(); let return_data = TransactionReturnData { program_id: context.transaction_frame.return_data_pubkey, @@ -637,7 +643,7 @@ impl From> for ExecutionRecord { Self { accounts, return_data, - touched_account_count, + touched_flags, accounts_resize_delta: Cell::into_inner(resize_delta), } } From 9b7cf58ae62f120605fa87374f8cf774cf3297a1 Mon Sep 17 00:00:00 2001 From: Greg Cusack Date: Mon, 15 Jun 2026 12:39:46 +0200 Subject: [PATCH 333/576] Promote PoH pinned CPU core flag (#13191) --- CHANGELOG.md | 1 + validator/src/cli.rs | 11 +++++++++++ validator/src/commands/run/args.rs | 9 ++++----- validator/src/commands/run/execute.rs | 5 +++-- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8c9ec0b80a..af5470b0fe5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ Release channels have their own copy of this changelog: * `--accounts-db-access-storages-method` is now deprecated and a no-op (the `mmap` value was deprecated in v4.0.0; mmap mode has now been removed entirely). The flag is still accepted for backward compatibility, but account storages are always accessed via file I/O. +* `--experimental-poh-pinned-cpu-core` is now deprecated. Use `--poh-pinned-cpu-core` instead. #### Changes * Turbine shred ingestion now rejects shreds more than half an epoch in the future (previously up to 2 full epochs ahead was accepted). ### CLI diff --git a/validator/src/cli.rs b/validator/src/cli.rs index 7c5484ac007..4011ed283b8 100644 --- a/validator/src/cli.rs +++ b/validator/src/cli.rs @@ -158,6 +158,17 @@ fn deprecated_arguments() -> Vec { .conflicts_with("accounts_index_limit"), replaced_by: "accounts-index-limit", ); + add_arg!( + // deprecated in v4.2.0 + Arg::with_name("experimental_poh_pinned_cpu_core") + .long("experimental-poh-pinned-cpu-core") + .takes_value(true) + .value_name("CPU_ID") + .conflicts_with("poh_pinned_cpu_core") + .validator(is_parsable::) + .help("Specify which CPU core PoH is pinned to. Use --poh-pinned-cpu-core instead"), + replaced_by: "poh-pinned-cpu-core", + ); add_arg!( // deprecated in v4.1.0 Arg::with_name("experimental_retransmit_xdp_cpu_cores") diff --git a/validator/src/commands/run/args.rs b/validator/src/commands/run/args.rs index 2c125869d9f..a75b95a71a8 100644 --- a/validator/src/commands/run/args.rs +++ b/validator/src/commands/run/args.rs @@ -30,7 +30,7 @@ use { solana_send_transaction_service::send_transaction_service::Config as SendTransactionServiceConfig, solana_signer::Signer, solana_unified_scheduler_pool::DefaultSchedulerPool, - std::{collections::HashSet, net::SocketAddr, path::PathBuf, str::FromStr}, + std::{collections::HashSet, net::SocketAddr, path::PathBuf}, }; const EXCLUDE_KEY: &str = "account-index-exclude-key"; @@ -864,12 +864,11 @@ pub fn add_args<'a>(app: App<'a, 'a>, default_args: &'a DefaultArgs) -> App<'a, ) .arg( Arg::with_name("poh_pinned_cpu_core") - .hidden(hidden_unless_forced()) - .long("experimental-poh-pinned-cpu-core") + .long("poh-pinned-cpu-core") .takes_value(true) .value_name("CPU_ID") - .validator(|s| usize::from_str(&s).map(|_| ()).map_err(|e| e.to_string())) - .help("Specify which CPU core PoH is pinned to"), + .validator(is_parsable::) + .help("Specify which CPU core PoH is pinned to. Defaults to CPU 0 on Linux"), ) .arg( Arg::with_name("poh_hashes_per_batch") diff --git a/validator/src/commands/run/execute.rs b/validator/src/commands/run/execute.rs index 5d1d7a92c85..b61f39555be 100644 --- a/validator/src/commands/run/execute.rs +++ b/validator/src/commands/run/execute.rs @@ -426,8 +426,9 @@ pub fn execute( let (xdp_transmit_setup, xdp_network_config_report) = (None, None); #[cfg(target_os = "linux")] - let poh_pinned_cpu_core = - value_of(matches, "poh_pinned_cpu_core").or(poh_service::DEFAULT_PINNED_CPU_CORE); + let poh_pinned_cpu_core = value_of(matches, "poh_pinned_cpu_core") + .or_else(|| value_of(matches, "experimental_poh_pinned_cpu_core")) + .or(poh_service::DEFAULT_PINNED_CPU_CORE); #[cfg(not(target_os = "linux"))] let poh_pinned_cpu_core = None; From 3665026beacb3c4241755f288f249233ea23409f Mon Sep 17 00:00:00 2001 From: kirill lykov Date: Mon, 15 Jun 2026 13:44:55 +0200 Subject: [PATCH 334/576] bench-tps: remove it, use transaction-bench (#13109) * Remove bench-tps bench-tps has been eclipsed by solana-transaction-bench tool * Remove bench-tps from multinode scripts and documentation --- CONTRIBUTING.md | 2 +- bench-tps/.gitignore | 4 - bench-tps/Cargo.toml | 89 - bench-tps/src/bench.rs | 1511 ----------------- bench-tps/src/cli.rs | 734 -------- bench-tps/src/keypairs.rs | 82 - bench-tps/src/lib.rs | 9 - bench-tps/src/log_transaction_service.rs | 496 ------ bench-tps/src/main.rs | 277 --- bench-tps/src/perf_utils.rs | 86 - bench-tps/src/rpc_with_retry_utils.rs | 60 - bench-tps/src/send_batch.rs | 519 ------ bench-tps/tests/bench_tps.rs | 169 -- .../tests/fixtures/spl_instruction_padding.so | Bin 16800 -> 0 bytes ci/coverage/part-3.sh | 2 +- dev-bins/.config/nextest.toml | 4 - dev-bins/Cargo.lock | 601 +------ dev-bins/Cargo.toml | 2 +- docs/deploy.sh | 1 - docs/src/clusters/benchmark.md | 6 +- docs/src/clusters/metrics.md | 4 +- docs/src/clusters/testnet.md | 10 +- .../dashboards/cluster-monitor.json | 370 ---- multinode-demo/bench-tps.sh | 45 - multinode-demo/common.sh | 3 +- multinode-demo/txs-bench.sh | 1 - net/net.sh | 46 +- net/remote/remote-client.sh | 65 +- net/remote/remote-node.sh | 45 +- scripts/agave-build-lists.sh | 2 - 30 files changed, 45 insertions(+), 5200 deletions(-) delete mode 100644 bench-tps/.gitignore delete mode 100644 bench-tps/Cargo.toml delete mode 100644 bench-tps/src/bench.rs delete mode 100644 bench-tps/src/cli.rs delete mode 100644 bench-tps/src/keypairs.rs delete mode 100644 bench-tps/src/lib.rs delete mode 100644 bench-tps/src/log_transaction_service.rs delete mode 100644 bench-tps/src/main.rs delete mode 100644 bench-tps/src/perf_utils.rs delete mode 100644 bench-tps/src/rpc_with_retry_utils.rs delete mode 100644 bench-tps/src/send_batch.rs delete mode 100644 bench-tps/tests/bench_tps.rs delete mode 100755 bench-tps/tests/fixtures/spl_instruction_padding.so delete mode 100755 multinode-demo/bench-tps.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index badc9364006..cb6aa1ddff8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -71,7 +71,7 @@ are not present, then write them. All changes should be benchmarked and evidence posted to the PR. Microbenchmark results along with mainnet/testnet validator timings or profiles, -bench-tps or other relevant integration benchmarks. Any code that adds complexity +and relevant integration benchmarks. Any code that adds complexity should be justified by a comisurate improvement in speed. All changes should be reviewed by subject matter experts. diff --git a/bench-tps/.gitignore b/bench-tps/.gitignore deleted file mode 100644 index 252411f34dc..00000000000 --- a/bench-tps/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -/target/ -/config/ -/config-local/ -/farf/ diff --git a/bench-tps/Cargo.toml b/bench-tps/Cargo.toml deleted file mode 100644 index 3e4247af455..00000000000 --- a/bench-tps/Cargo.toml +++ /dev/null @@ -1,89 +0,0 @@ -[package] -name = "solana-bench-tps" -publish = false -version = { workspace = true } -authors = { workspace = true } -repository = { workspace = true } -homepage = { workspace = true } -license = { workspace = true } -edition = { workspace = true } -workspace = "../dev-bins" - -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] - -[features] -default = ["agave-unstable-api"] -agave-unstable-api = [] -dev-context-only-utils = [] -dummy-for-ci-check = [] -frozen-abi = [] - -[dependencies] -agave-logger = { workspace = true } -chrono = { workspace = true } -clap = { workspace = true } -crossbeam-channel = { workspace = true } -csv = { workspace = true } -log = { workspace = true } -rand = { workspace = true } -rayon = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -serde_yaml = { workspace = true } -solana-account = { workspace = true } -solana-clap-utils = { workspace = true } -solana-cli-config = { workspace = true } -solana-client = { workspace = true } -solana-clock = { workspace = true } -solana-commitment-config = { workspace = true } -solana-compute-budget-interface = { workspace = true } -solana-connection-cache = { workspace = true } -solana-core = { workspace = true, features = ["dev-context-only-utils"] } -solana-fee-calculator = { workspace = true } -solana-genesis = { workspace = true } -solana-genesis-config = { workspace = true } -solana-hash = { workspace = true } -solana-instruction = { workspace = true } -solana-keypair = { workspace = true } -solana-measure = { workspace = true } -solana-message = { workspace = true } -solana-metrics = { workspace = true } -solana-native-token = { workspace = true } -solana-net-utils = { workspace = true } -solana-nonce = { workspace = true } -solana-pubkey = { workspace = true } -solana-quic-client = { workspace = true } -solana-rpc = { workspace = true } -solana-rpc-client = { workspace = true } -solana-rpc-client-api = { workspace = true } -solana-runtime = { workspace = true, features = ["dev-context-only-utils"] } -solana-signature = { workspace = true } -solana-signer = { workspace = true } -solana-streamer = { workspace = true } -solana-system-interface = { workspace = true } -solana-time-utils = { workspace = true } -solana-tps-client = { workspace = true } -solana-tpu-client = { workspace = true } -solana-transaction = { workspace = true } -solana-transaction-status = { workspace = true } -solana-version = { workspace = true } -spl-instruction-padding-interface = { version = "=1.0.0" } - -[target.'cfg(not(any(target_env = "msvc", target_os = "freebsd")))'.dependencies] -jemallocator = { workspace = true } - -[dev-dependencies] -agave-feature-set = { workspace = true } -serial_test = { workspace = true } -solana-faucet = { workspace = true, features = ["dev-context-only-utils"] } -solana-local-cluster = { workspace = true } -solana-rent = { workspace = true } -solana-runtime = { workspace = true, features = ["dev-context-only-utils"] } -solana-sdk-ids = { workspace = true } -solana-test-validator = { workspace = true, features = ["agave-unstable-api", "dev-context-only-utils"] } -solana-tps-client = { workspace = true, features = ["bank-client"] } -tempfile = { workspace = true } - -[lints] -workspace = true diff --git a/bench-tps/src/bench.rs b/bench-tps/src/bench.rs deleted file mode 100644 index cc9e0b1b621..00000000000 --- a/bench-tps/src/bench.rs +++ /dev/null @@ -1,1511 +0,0 @@ -use { - crate::{ - cli::{ComputeUnitPrice, Config, InstructionPaddingConfig}, - log_transaction_service::{ - SignatureBatchSender, TransactionInfoBatch, create_log_transactions_service_and_sender, - }, - perf_utils::{SampleStats, sample_txs}, - send_batch::*, - }, - chrono::Utc, - log::*, - rand::distr::{Distribution, Uniform}, - rayon::prelude::*, - solana_account::Account, - solana_client::nonce_utils, - solana_clock::{DEFAULT_MS_PER_SLOT, DEFAULT_S_PER_SLOT, MAX_PROCESSING_AGE}, - solana_compute_budget_interface::ComputeBudgetInstruction, - solana_hash::Hash, - solana_instruction::{AccountMeta, Instruction}, - solana_keypair::Keypair, - solana_message::{ - Message, VersionedMessage, - v1::{Message as V1Message, TransactionConfig}, - }, - solana_metrics::{self, datapoint_info}, - solana_native_token::Sol, - solana_pubkey::Pubkey, - solana_rpc_client_api::request::MAX_MULTIPLE_ACCOUNTS, - solana_signer::Signer, - solana_system_interface::instruction as system_instruction, - solana_time_utils::timestamp, - solana_tps_client::*, - solana_transaction::{Transaction, versioned::VersionedTransaction}, - spl_instruction_padding_interface::instruction::wrap_instruction, - std::{ - collections::{HashSet, VecDeque}, - process::exit, - sync::{ - Arc, RwLock, - atomic::{AtomicBool, AtomicIsize, AtomicUsize, Ordering}, - }, - thread::{Builder, JoinHandle, sleep}, - time::{Duration, Instant}, - }, -}; - -// The point at which transactions become "too old", in seconds. -const MAX_TX_QUEUE_AGE: u64 = (MAX_PROCESSING_AGE as f64 * DEFAULT_S_PER_SLOT) as u64; - -// Add prioritization fee to transfer transactions, if `compute_unit_price` is set. -// If `Random` the compute-unit-price is determined by generating a random number in the range -// 0..MAX_RANDOM_COMPUTE_UNIT_PRICE then multiplying by COMPUTE_UNIT_PRICE_MULTIPLIER. -// If `Fixed` the compute-unit-price is the value of the `compute-unit-price` parameter. -// It also sets transaction's compute-unit to TRANSFER_TRANSACTION_COMPUTE_UNIT. Therefore the -// max additional cost is: -// `TRANSFER_TRANSACTION_COMPUTE_UNIT * MAX_COMPUTE_UNIT_PRICE * COMPUTE_UNIT_PRICE_MULTIPLIER / 1_000_000` -const MAX_RANDOM_COMPUTE_UNIT_PRICE: u64 = 50; -const COMPUTE_UNIT_PRICE_MULTIPLIER: u64 = 1_000; -const TRANSFER_TRANSACTION_COMPUTE_UNIT: u32 = 600; // 1 transfer is plus 3 compute_budget ixs -const PADDED_TRANSFER_COMPUTE_UNIT: u32 = 3_000; // padding program execution requires consumes this amount - -/// calculate maximum possible prioritization fee, if `use-randomized-compute-unit-price` is -/// enabled, round to nearest lamports. -pub fn max_lamports_for_prioritization(compute_unit_price: &Option) -> u64 { - let Some(compute_unit_price) = compute_unit_price else { - return 0; - }; - - let compute_unit_price = match compute_unit_price { - ComputeUnitPrice::Random => (MAX_RANDOM_COMPUTE_UNIT_PRICE as u128) - .saturating_mul(COMPUTE_UNIT_PRICE_MULTIPLIER as u128), - ComputeUnitPrice::Fixed(compute_unit_price) => *compute_unit_price as u128, - }; - - const MICRO_LAMPORTS_PER_LAMPORT: u64 = 1_000_000; - let micro_lamport_fee: u128 = - compute_unit_price.saturating_mul(TRANSFER_TRANSACTION_COMPUTE_UNIT as u128); - let fee = micro_lamport_fee - .saturating_add(MICRO_LAMPORTS_PER_LAMPORT.saturating_sub(1) as u128) - .saturating_div(MICRO_LAMPORTS_PER_LAMPORT as u128); - u64::try_from(fee).unwrap_or(u64::MAX) -} - -// In case of plain transfer transaction, set loaded account data size to 30K. -// It is large enough yet smaller than 32K page size, so it'd cost 0 extra CU. -const TRANSFER_TRANSACTION_LOADED_ACCOUNTS_DATA_SIZE: u32 = 30 * 1024; -// In case of padding program usage, we need to take into account program size -const PADDING_PROGRAM_ACCOUNT_DATA_SIZE: u32 = 28 * 1024; -fn get_transaction_loaded_accounts_data_size(enable_padding: bool) -> u32 { - if enable_padding { - TRANSFER_TRANSACTION_LOADED_ACCOUNTS_DATA_SIZE + PADDING_PROGRAM_ACCOUNT_DATA_SIZE - } else { - TRANSFER_TRANSACTION_LOADED_ACCOUNTS_DATA_SIZE - } -} - -#[derive(Debug, PartialEq, Default, Eq, Clone)] -pub(crate) struct TimestampedTransaction { - transaction: VersionedTransaction, - timestamp: Option, - compute_unit_price: Option, -} - -pub(crate) type SharedTransactions = Arc>>>; - -/// Keypairs split into source and destination -/// used for transfer transactions -struct KeypairChunks<'a> { - source: Vec>, - dest: Vec>, -} - -impl<'a> KeypairChunks<'a> { - /// Split input slice of keypairs into two sets of chunks of given size - fn new(keypairs: &'a [Keypair], chunk_size: usize) -> Self { - // Use `chunk_size` as the number of conflict groups per chunk so that each destination key is unique - Self::new_with_conflict_groups(keypairs, chunk_size, chunk_size) - } - - /// Split input slice of keypairs into two sets of chunks of given size. Each chunk - /// has a set of source keys and a set of destination keys. There will be - /// `num_conflict_groups_per_chunk` unique destination keys per chunk, so that the - /// destination keys may conflict with each other. - fn new_with_conflict_groups( - keypairs: &'a [Keypair], - chunk_size: usize, - num_conflict_groups_per_chunk: usize, - ) -> Self { - let mut source_keypair_chunks: Vec> = Vec::new(); - let mut dest_keypair_chunks: Vec> = Vec::new(); - for chunk in keypairs.chunks_exact(2 * chunk_size) { - source_keypair_chunks.push(chunk[..chunk_size].iter().collect()); - dest_keypair_chunks.push( - std::iter::repeat(&chunk[chunk_size..chunk_size + num_conflict_groups_per_chunk]) - .flatten() - .take(chunk_size) - .collect(), - ); - } - KeypairChunks { - source: source_keypair_chunks, - dest: dest_keypair_chunks, - } - } -} - -struct TransactionChunkGenerator<'a, 'b, T: ?Sized> { - client: Arc, - account_chunks: KeypairChunks<'a>, - nonce_chunks: Option>, - chunk_index: usize, - reclaim_lamports_back_to_source_account: bool, - compute_unit_price: Option, - instruction_padding_config: Option, - skip_tx_account_data_size: bool, - use_txv1: bool, -} - -impl<'a, 'b, T> TransactionChunkGenerator<'a, 'b, T> -where - T: 'static + TpsClient + Send + Sync + ?Sized, -{ - fn new( - client: Arc, - gen_keypairs: &'a [Keypair], - nonce_keypairs: Option<&'b Vec>, - chunk_size: usize, - compute_unit_price: Option, - instruction_padding_config: Option, - num_conflict_groups: Option, - skip_tx_account_data_size: bool, - use_txv1: bool, - ) -> Self { - let account_chunks = if let Some(num_conflict_groups) = num_conflict_groups { - KeypairChunks::new_with_conflict_groups(gen_keypairs, chunk_size, num_conflict_groups) - } else { - KeypairChunks::new(gen_keypairs, chunk_size) - }; - let nonce_chunks = - nonce_keypairs.map(|nonce_keypairs| KeypairChunks::new(nonce_keypairs, chunk_size)); - - TransactionChunkGenerator { - client, - account_chunks, - nonce_chunks, - chunk_index: 0, - reclaim_lamports_back_to_source_account: false, - compute_unit_price, - instruction_padding_config, - skip_tx_account_data_size, - use_txv1, - } - } - - /// generate transactions to transfer lamports from source to destination accounts - /// if durable nonce is used, blockhash is None - fn generate(&mut self, blockhash: Option<&Hash>) -> Vec { - let tx_count = self.account_chunks.source.len(); - info!( - "Signing transactions... {} (reclaim={}, blockhash={:?})", - tx_count, self.reclaim_lamports_back_to_source_account, blockhash - ); - let signing_start = Instant::now(); - - let source_chunk = &self.account_chunks.source[self.chunk_index]; - let dest_chunk = &self.account_chunks.dest[self.chunk_index]; - let transactions = if let Some(nonce_chunks) = &self.nonce_chunks { - let source_nonce_chunk = &nonce_chunks.source[self.chunk_index]; - let dest_nonce_chunk: &VecDeque<&Keypair> = &nonce_chunks.dest[self.chunk_index]; - generate_nonced_system_txs( - self.client.clone(), - source_chunk, - dest_chunk, - source_nonce_chunk, - dest_nonce_chunk, - self.reclaim_lamports_back_to_source_account, - self.skip_tx_account_data_size, - &self.instruction_padding_config, - ) - } else { - assert!(blockhash.is_some()); - generate_system_txs( - source_chunk, - dest_chunk, - self.reclaim_lamports_back_to_source_account, - blockhash.unwrap(), - &self.instruction_padding_config, - &self.compute_unit_price, - self.skip_tx_account_data_size, - self.use_txv1, - ) - }; - - let duration = signing_start.elapsed(); - let ns = duration.as_secs() * 1_000_000_000 + u64::from(duration.subsec_nanos()); - let bsps = (tx_count) as f64 / ns as f64; - let nsps = ns as f64 / (tx_count) as f64; - info!( - "Done. {:.2} thousand signatures per second, {:.2} us per signature, {} ms total \ - time, {:?}", - bsps * 1_000_000_f64, - nsps / 1_000_f64, - duration.as_millis(), - blockhash, - ); - datapoint_info!( - "bench-tps-generate_txs", - ("duration", duration.as_micros() as i64, i64) - ); - - transactions - } - - fn advance(&mut self) { - // Rotate destination keypairs so that the next round of transactions will have different - // transaction signatures even when blockhash is reused. - self.account_chunks.dest[self.chunk_index].rotate_left(1); - if let Some(nonce_chunks) = &mut self.nonce_chunks { - nonce_chunks.dest[self.chunk_index].rotate_left(1); - } - // Move on to next chunk - self.chunk_index = (self.chunk_index + 1) % self.account_chunks.source.len(); - - // Switch directions after transferring for each "chunk" - if self.chunk_index == 0 { - self.reclaim_lamports_back_to_source_account = - !self.reclaim_lamports_back_to_source_account; - } - } -} - -fn wait_for_target_slots_per_epoch(target_slots_per_epoch: u64, client: &Arc) -where - T: 'static + TpsClient + Send + Sync + ?Sized, -{ - if target_slots_per_epoch != 0 { - info!("Waiting until epochs are {target_slots_per_epoch} slots long..."); - loop { - if let Ok(epoch_info) = client.get_epoch_info() { - if epoch_info.slots_in_epoch >= target_slots_per_epoch { - info!("Done epoch_info: {epoch_info:?}"); - break; - } - info!( - "Waiting for epoch: {} now: {}", - target_slots_per_epoch, epoch_info.slots_in_epoch - ); - } - sleep(Duration::from_secs(3)); - } - } -} - -fn create_sampler_thread( - client: &Arc, - exit_signal: Arc, - sample_period: u64, - maxes: &Arc>>, -) -> JoinHandle<()> -where - T: 'static + TpsClient + Send + Sync + ?Sized, -{ - info!("Sampling TPS every {sample_period} second..."); - let maxes = maxes.clone(); - let client = client.clone(); - Builder::new() - .name("solana-client-sample".to_string()) - .spawn(move || { - sample_txs(exit_signal, &maxes, sample_period, &client); - }) - .unwrap() -} - -fn generate_chunked_transfers( - recent_blockhash: Arc>, - shared_txs: &SharedTransactions, - shared_tx_active_thread_count: Arc, - mut chunk_generator: TransactionChunkGenerator<'_, '_, T>, - threads: usize, - duration: Duration, - sustained: bool, - use_durable_nonce: bool, -) { - // generate and send transactions for the specified duration - let start = Instant::now(); - let mut last_generate_txs_time = Instant::now(); - - while start.elapsed() < duration { - generate_txs( - shared_txs, - &recent_blockhash, - &mut chunk_generator, - threads, - use_durable_nonce, - ); - - datapoint_info!( - "blockhash_stats", - ( - "time_elapsed_since_last_generate_txs", - last_generate_txs_time.elapsed().as_millis(), - i64 - ) - ); - - last_generate_txs_time = Instant::now(); - - // In sustained mode, overlap the transfers with generation. This has higher average - // performance but lower peak performance in tested environments. - if sustained { - // Ensure that we don't generate more transactions than we can handle. - while shared_txs.read().unwrap().len() > 2 * threads { - sleep(Duration::from_millis(1)); - } - } else { - while !shared_txs.read().unwrap().is_empty() - || shared_tx_active_thread_count.load(Ordering::Relaxed) > 0 - { - sleep(Duration::from_millis(1)); - } - } - chunk_generator.advance(); - } -} - -fn create_sender_threads( - client: &Arc, - shared_txs: &SharedTransactions, - thread_batch_sleep_ms: usize, - total_tx_sent_count: &Arc, - threads: usize, - exit_signal: Arc, - shared_tx_active_thread_count: &Arc, - signatures_sender: Option, -) -> Vec> -where - T: 'static + TpsClient + Send + Sync + ?Sized, -{ - (0..threads) - .map(|_| { - let exit_signal = exit_signal.clone(); - let shared_txs = shared_txs.clone(); - let shared_tx_active_thread_count = shared_tx_active_thread_count.clone(); - let total_tx_sent_count = total_tx_sent_count.clone(); - let client = client.clone(); - let signatures_sender = signatures_sender.clone(); - Builder::new() - .name("solana-client-sender".to_string()) - .spawn(move || { - do_tx_transfers( - &exit_signal, - &shared_txs, - &shared_tx_active_thread_count, - &total_tx_sent_count, - thread_batch_sleep_ms, - &client, - signatures_sender, - ); - }) - .unwrap() - }) - .collect() -} - -pub fn do_bench_tps( - client: Arc, - config: Config, - gen_keypairs: Vec, - nonce_keypairs: Option>, -) -> u64 -where - T: 'static + TpsClient + Send + Sync + ?Sized, -{ - let Config { - id, - threads, - thread_batch_sleep_ms, - duration, - tx_count, - sustained, - target_slots_per_epoch, - compute_unit_price, - skip_tx_account_data_size, - use_durable_nonce, - instruction_padding_config, - num_conflict_groups, - block_data_file, - transaction_data_file, - use_txv1, - .. - } = config; - - assert!(gen_keypairs.len() >= 2 * tx_count); - let chunk_generator = TransactionChunkGenerator::new( - client.clone(), - &gen_keypairs, - nonce_keypairs.as_ref(), - tx_count, - compute_unit_price, - instruction_padding_config, - num_conflict_groups, - skip_tx_account_data_size, - use_txv1, - ); - - let first_tx_count = loop { - match client.get_transaction_count() { - Ok(count) => break count, - Err(err) => { - info!("Couldn't get transaction count: {err:?}"); - sleep(Duration::from_secs(1)); - } - } - }; - info!("Initial transaction count {first_tx_count}"); - - let exit_signal = Arc::new(AtomicBool::new(false)); - - // Setup a thread per validator to sample every period - // collect the max transaction rate and total tx count seen - let maxes = Arc::new(RwLock::new(Vec::new())); - let sample_period = 1; // in seconds - let sample_thread = create_sampler_thread(&client, exit_signal.clone(), sample_period, &maxes); - - let shared_txs: SharedTransactions = Arc::new(RwLock::new(VecDeque::new())); - - let blockhash = Arc::new(RwLock::new(get_latest_blockhash(client.as_ref()))); - let shared_tx_active_thread_count = Arc::new(AtomicIsize::new(0)); - let total_tx_sent_count = Arc::new(AtomicUsize::new(0)); - - // if we use durable nonce, we don't need blockhash thread - let blockhash_thread = if !use_durable_nonce { - let exit_signal = exit_signal.clone(); - let blockhash = blockhash.clone(); - let client = client.clone(); - let id = id.pubkey(); - Some( - Builder::new() - .name("solana-blockhash-poller".to_string()) - .spawn(move || { - poll_blockhash(&exit_signal, &blockhash, &client, &id); - }) - .unwrap(), - ) - } else { - None - }; - - let (log_transaction_service, signatures_sender) = create_log_transactions_service_and_sender( - &client, - block_data_file.as_deref(), - transaction_data_file.as_deref(), - ); - - let sender_threads = create_sender_threads( - &client, - &shared_txs, - thread_batch_sleep_ms, - &total_tx_sent_count, - threads, - exit_signal.clone(), - &shared_tx_active_thread_count, - signatures_sender, - ); - - wait_for_target_slots_per_epoch(target_slots_per_epoch, &client); - - let start = Instant::now(); - - generate_chunked_transfers( - blockhash, - &shared_txs, - shared_tx_active_thread_count, - chunk_generator, - threads, - duration, - sustained, - use_durable_nonce, - ); - - // Stop the sampling threads so it will collect the stats - exit_signal.store(true, Ordering::Relaxed); - - info!("Waiting for sampler threads..."); - if let Err(err) = sample_thread.join() { - info!(" join() failed with: {err:?}"); - } - - // join the tx send threads - info!("Waiting for transmit threads..."); - for t in sender_threads { - if let Err(err) = t.join() { - info!(" join() failed with: {err:?}"); - } - } - - if let Some(blockhash_thread) = blockhash_thread { - info!("Waiting for blockhash thread..."); - if let Err(err) = blockhash_thread.join() { - info!(" join() failed with: {err:?}"); - } - } - - if let Some(log_transaction_service) = log_transaction_service { - info!("Waiting for log_transaction_service thread..."); - if let Err(err) = log_transaction_service.join() { - info!(" join() failed with: {err:?}"); - } - } - - if let Some(nonce_keypairs) = nonce_keypairs { - withdraw_durable_nonce_accounts(client.clone(), &gen_keypairs, &nonce_keypairs); - } - - let balance = client.get_balance(&id.pubkey()).unwrap_or(0); - metrics_submit_lamport_balance(balance); - - compute_and_report_stats( - &maxes, - sample_period, - &start.elapsed(), - total_tx_sent_count.load(Ordering::Relaxed), - ); - - let r_maxes = maxes.read().unwrap(); - r_maxes.first().unwrap().1.txs -} - -fn metrics_submit_lamport_balance(lamport_balance: u64) { - info!("Token balance: {lamport_balance}"); - datapoint_info!( - "bench-tps-lamport_balance", - ("balance", lamport_balance, i64) - ); -} - -fn generate_system_txs( - source: &[&Keypair], - dest: &VecDeque<&Keypair>, - reclaim: bool, - blockhash: &Hash, - instruction_padding_config: &Option, - compute_unit_price: &Option, - skip_tx_account_data_size: bool, - use_txv1: bool, -) -> Vec { - let pairs: Vec<_> = if !reclaim { - source.iter().zip(dest.iter()).collect() - } else { - dest.iter().zip(source.iter()).collect() - }; - - if let Some(compute_unit_price) = compute_unit_price { - let compute_unit_prices = match compute_unit_price { - ComputeUnitPrice::Random => { - let mut rng = rand::rng(); - let range = Uniform::try_from(0..MAX_RANDOM_COMPUTE_UNIT_PRICE) - .expect("ok for non-empty range"); - (0..pairs.len()) - .map(|_| { - range - .sample(&mut rng) - .saturating_mul(COMPUTE_UNIT_PRICE_MULTIPLIER) - }) - .collect() - } - ComputeUnitPrice::Fixed(compute_unit_price) => vec![*compute_unit_price; pairs.len()], - }; - - let pairs_with_compute_unit_prices: Vec<_> = - pairs.iter().zip(compute_unit_prices.iter()).collect(); - - pairs_with_compute_unit_prices - .par_iter() - .map(|((from, to), compute_unit_price)| { - let compute_unit_price = Some(**compute_unit_price); - TimestampedTransaction { - transaction: transfer_with_compute_unit_price_and_padding( - from, - &to.pubkey(), - 1, - *blockhash, - instruction_padding_config, - compute_unit_price, - skip_tx_account_data_size, - use_txv1, - ), - timestamp: Some(timestamp()), - compute_unit_price, - } - }) - .collect() - } else { - pairs - .par_iter() - .map(|(from, to)| TimestampedTransaction { - transaction: transfer_with_compute_unit_price_and_padding( - from, - &to.pubkey(), - 1, - *blockhash, - instruction_padding_config, - None, - skip_tx_account_data_size, - use_txv1, - ), - timestamp: Some(timestamp()), - compute_unit_price: None, - }) - .collect() - } -} - -fn transfer_with_compute_unit_price_and_padding( - from_keypair: &Keypair, - to: &Pubkey, - lamports: u64, - recent_blockhash: Hash, - instruction_padding_config: &Option, - compute_unit_price: Option, - skip_tx_account_data_size: bool, - use_txv1: bool, -) -> VersionedTransaction { - if use_txv1 { - transfer_with_compute_unit_price_and_padding_v1( - from_keypair, - to, - lamports, - recent_blockhash, - instruction_padding_config, - compute_unit_price, - skip_tx_account_data_size, - ) - } else { - transfer_with_compute_unit_price_and_padding_legacy( - from_keypair, - to, - lamports, - recent_blockhash, - instruction_padding_config, - compute_unit_price, - skip_tx_account_data_size, - ) - } -} - -fn transfer_with_compute_unit_price_and_padding_legacy( - from_keypair: &Keypair, - to: &Pubkey, - lamports: u64, - recent_blockhash: Hash, - instruction_padding_config: &Option, - compute_unit_price: Option, - skip_tx_account_data_size: bool, -) -> VersionedTransaction { - let from_pubkey = from_keypair.pubkey(); - let transfer_instruction = system_instruction::transfer(&from_pubkey, to, lamports); - let instruction = if let Some(instruction_padding_config) = instruction_padding_config { - wrap_instruction( - instruction_padding_config.program_id, - transfer_instruction, - vec![], - instruction_padding_config.data_size, - ) - .expect("Could not create padded instruction") - } else { - transfer_instruction - }; - let mut instructions = vec![]; - if !skip_tx_account_data_size { - instructions.push( - ComputeBudgetInstruction::set_loaded_accounts_data_size_limit( - get_transaction_loaded_accounts_data_size(instruction_padding_config.is_some()), - ), - ) - } - instructions.push(instruction); - if instruction_padding_config.is_some() { - // By default, CU budget is DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT which is much larger than needed - instructions.push(ComputeBudgetInstruction::set_compute_unit_limit( - PADDED_TRANSFER_COMPUTE_UNIT, - )); - } - - if let Some(compute_unit_price) = compute_unit_price { - instructions.extend_from_slice(&[ - ComputeBudgetInstruction::set_compute_unit_limit(TRANSFER_TRANSACTION_COMPUTE_UNIT), - ComputeBudgetInstruction::set_compute_unit_price(compute_unit_price), - ]) - } - let message = Message::new(&instructions, Some(&from_pubkey)); - Transaction::new(&[from_keypair], message, recent_blockhash).into() -} - -fn transfer_with_compute_unit_price_and_padding_v1( - from_keypair: &Keypair, - to: &Pubkey, - lamports: u64, - recent_blockhash: Hash, - instruction_padding_config: &Option, - compute_unit_price: Option, - skip_tx_account_data_size: bool, -) -> VersionedTransaction { - let from_pubkey = from_keypair.pubkey(); - let transfer_instruction = system_instruction::transfer(&from_pubkey, to, lamports); - let instruction = if let Some(instruction_padding_config) = instruction_padding_config { - wrap_instruction( - instruction_padding_config.program_id, - transfer_instruction, - vec![], - instruction_padding_config.data_size, - ) - .expect("Could not create padded instruction") - } else { - transfer_instruction - }; - let instructions = vec![instruction]; - let mut config = TransactionConfig::empty(); - if !skip_tx_account_data_size { - config.loaded_accounts_data_size_limit = Some(get_transaction_loaded_accounts_data_size( - instruction_padding_config.is_some(), - )); - } - if instruction_padding_config.is_some() { - config.compute_unit_limit = Some(PADDED_TRANSFER_COMPUTE_UNIT); - } - - if let Some(compute_unit_price) = compute_unit_price { - config.compute_unit_limit = Some(TRANSFER_TRANSACTION_COMPUTE_UNIT); - config.priority_fee = Some(compute_unit_price); - } - let message = - V1Message::try_compile_with_config(&from_pubkey, &instructions, recent_blockhash, config) - .unwrap(); - let versioned_message = VersionedMessage::V1(message); - VersionedTransaction::try_new(versioned_message, &[from_keypair]).unwrap() -} - -fn get_nonce_accounts( - client: &Arc, - nonce_pubkeys: &[Pubkey], -) -> Vec> { - // get_multiple_accounts supports maximum MAX_MULTIPLE_ACCOUNTS pubkeys in request - assert!(nonce_pubkeys.len() <= MAX_MULTIPLE_ACCOUNTS); - loop { - match client.get_multiple_accounts(nonce_pubkeys) { - Ok(nonce_accounts) => { - return nonce_accounts; - } - Err(err) => { - info!("Couldn't get durable nonce account: {err:?}"); - sleep(Duration::from_secs(1)); - } - } - } -} - -fn get_nonce_blockhashes( - client: &Arc, - nonce_pubkeys: &[Pubkey], -) -> Vec { - let num_accounts = nonce_pubkeys.len(); - let mut blockhashes = vec![Hash::default(); num_accounts]; - let mut unprocessed = (0..num_accounts).collect::>(); - - let mut request_pubkeys = Vec::::with_capacity(num_accounts); - let mut request_indexes = Vec::::with_capacity(num_accounts); - - while !unprocessed.is_empty() { - for i in &unprocessed { - request_pubkeys.push(nonce_pubkeys[*i]); - request_indexes.push(*i); - } - - let num_unprocessed_before = unprocessed.len(); - let accounts: Vec> = nonce_pubkeys - .chunks(MAX_MULTIPLE_ACCOUNTS) - .flat_map(|pubkeys| get_nonce_accounts(client, pubkeys)) - .collect(); - - for (account, index) in accounts.iter().zip(request_indexes.iter()) { - if let Some(nonce_account) = account { - let nonce_data = nonce_utils::data_from_account(nonce_account).unwrap(); - blockhashes[*index] = nonce_data.blockhash(); - unprocessed.remove(index); - } - } - let num_unprocessed_after = unprocessed.len(); - debug!( - "Received {} durable nonce accounts", - num_unprocessed_before - num_unprocessed_after - ); - request_pubkeys.clear(); - request_indexes.clear(); - } - blockhashes -} - -fn nonced_transfer_with_padding( - from_keypair: &Keypair, - to: &Pubkey, - lamports: u64, - nonce_account: &Pubkey, - nonce_authority: &Keypair, - nonce_hash: Hash, - skip_tx_account_data_size: bool, - instruction_padding_config: &Option, -) -> VersionedTransaction { - let from_pubkey = from_keypair.pubkey(); - let transfer_instruction = system_instruction::transfer(&from_pubkey, to, lamports); - let instruction = if let Some(instruction_padding_config) = instruction_padding_config { - wrap_instruction( - instruction_padding_config.program_id, - transfer_instruction, - vec![], - instruction_padding_config.data_size, - ) - .expect("Could not create padded instruction") - } else { - transfer_instruction - }; - let mut instructions = vec![]; - if !skip_tx_account_data_size { - instructions.push( - ComputeBudgetInstruction::set_loaded_accounts_data_size_limit( - get_transaction_loaded_accounts_data_size(instruction_padding_config.is_some()), - ), - ) - } - instructions.push(instruction); - let message = Message::new_with_nonce( - instructions, - Some(&from_pubkey), - nonce_account, - &nonce_authority.pubkey(), - ); - Transaction::new(&[from_keypair, nonce_authority], message, nonce_hash).into() -} - -fn generate_nonced_system_txs( - client: Arc, - source: &[&Keypair], - dest: &VecDeque<&Keypair>, - source_nonce: &[&Keypair], - dest_nonce: &VecDeque<&Keypair>, - reclaim: bool, - skip_tx_account_data_size: bool, - instruction_padding_config: &Option, -) -> Vec { - let length = source.len(); - let mut transactions: Vec = Vec::with_capacity(length); - if !reclaim { - let pubkeys: Vec = source_nonce - .iter() - .map(|keypair| keypair.pubkey()) - .collect(); - - let blockhashes: Vec = get_nonce_blockhashes(&client, &pubkeys); - for i in 0..length { - transactions.push(TimestampedTransaction { - transaction: nonced_transfer_with_padding( - source[i], - &dest[i].pubkey(), - 1, - &source_nonce[i].pubkey(), - source[i], - blockhashes[i], - skip_tx_account_data_size, - instruction_padding_config, - ), - timestamp: None, - compute_unit_price: None, - }); - } - } else { - let pubkeys: Vec = dest_nonce.iter().map(|keypair| keypair.pubkey()).collect(); - let blockhashes: Vec = get_nonce_blockhashes(&client, &pubkeys); - - for i in 0..length { - transactions.push(TimestampedTransaction { - transaction: nonced_transfer_with_padding( - dest[i], - &source[i].pubkey(), - 1, - &dest_nonce[i].pubkey(), - dest[i], - blockhashes[i], - skip_tx_account_data_size, - instruction_padding_config, - ), - timestamp: None, - compute_unit_price: None, - }); - } - } - transactions -} - -fn generate_txs( - shared_txs: &SharedTransactions, - blockhash: &Arc>, - chunk_generator: &mut TransactionChunkGenerator<'_, '_, T>, - threads: usize, - use_durable_nonce: bool, -) { - let transactions = if use_durable_nonce { - chunk_generator.generate(None) - } else { - let blockhash = blockhash.read().map(|x| *x).ok(); - chunk_generator.generate(blockhash.as_ref()) - }; - - let sz = transactions.len() / threads; - let chunks: Vec<_> = transactions.chunks(sz).collect(); - { - let mut shared_txs_wl = shared_txs.write().unwrap(); - for chunk in chunks { - shared_txs_wl.push_back(chunk.to_vec()); - } - } -} - -fn get_new_latest_blockhash( - client: &Arc, - blockhash: &Hash, -) -> Option { - let start = Instant::now(); - while start.elapsed().as_secs() < 5 { - if let Ok(new_blockhash) = client.get_latest_blockhash() { - if new_blockhash != *blockhash { - return Some(new_blockhash); - } - } - debug!("Got same blockhash ({blockhash:?}), will retry..."); - - // Retry ~twice during a slot - sleep(Duration::from_millis(DEFAULT_MS_PER_SLOT / 2)); - } - None -} - -fn poll_blockhash( - exit_signal: &AtomicBool, - blockhash: &Arc>, - client: &Arc, - id: &Pubkey, -) { - let mut blockhash_last_updated = Instant::now(); - let mut last_error_log = Instant::now(); - loop { - let blockhash_updated = { - let old_blockhash = *blockhash.read().unwrap(); - if let Some(new_blockhash) = get_new_latest_blockhash(client, &old_blockhash) { - *blockhash.write().unwrap() = new_blockhash; - blockhash_last_updated = Instant::now(); - true - } else { - if blockhash_last_updated.elapsed().as_secs() > 120 { - eprintln!("Blockhash is stuck"); - exit(1) - } else if blockhash_last_updated.elapsed().as_secs() > 30 - && last_error_log.elapsed().as_secs() >= 1 - { - last_error_log = Instant::now(); - error!("Blockhash is not updating"); - } - false - } - }; - - if blockhash_updated { - let balance = client.get_balance(id).unwrap_or(0); - metrics_submit_lamport_balance(balance); - datapoint_info!( - "blockhash_stats", - ( - "time_elapsed_since_last_blockhash_update", - blockhash_last_updated.elapsed().as_millis(), - i64 - ) - ) - } - - if exit_signal.load(Ordering::Relaxed) { - break; - } - - sleep(Duration::from_millis(50)); - } -} - -fn do_tx_transfers( - exit_signal: &AtomicBool, - shared_txs: &SharedTransactions, - shared_tx_thread_count: &Arc, - total_tx_sent_count: &Arc, - thread_batch_sleep_ms: usize, - client: &Arc, - signatures_sender: Option, -) { - let mut last_sent_time = timestamp(); - 'thread_loop: loop { - if thread_batch_sleep_ms > 0 { - sleep(Duration::from_millis(thread_batch_sleep_ms as u64)); - } - let txs = { - let mut shared_txs_wl = shared_txs.write().expect("write lock in do_tx_transfers"); - shared_txs_wl.pop_front() - }; - if let Some(txs) = txs { - shared_tx_thread_count.fetch_add(1, Ordering::Relaxed); - let num_txs = txs.len(); - info!("Transferring 1 unit {num_txs} times..."); - let transfer_start = Instant::now(); - let mut old_transactions = false; - let mut min_timestamp = u64::MAX; - let mut transactions = Vec::<_>::with_capacity(num_txs); - let mut signatures = Vec::<_>::with_capacity(num_txs); - let mut compute_unit_prices = Vec::<_>::with_capacity(num_txs); - for tx in txs { - let now = timestamp(); - // Transactions without durable nonce that are too old will be rejected by the cluster Don't bother - // sending them. - if let Some(tx_timestamp) = tx.timestamp { - if tx_timestamp < min_timestamp { - min_timestamp = tx_timestamp; - } - if now > tx_timestamp && now - tx_timestamp > 1000 * MAX_TX_QUEUE_AGE { - old_transactions = true; - continue; - } - } - signatures.push(tx.transaction.signatures[0]); - transactions.push(tx.transaction); - compute_unit_prices.push(tx.compute_unit_price); - } - - if min_timestamp != u64::MAX { - datapoint_info!( - "bench-tps-do_tx_transfers", - ("oldest-blockhash-age", timestamp() - min_timestamp, i64), - ); - } - - if let Some(signatures_sender) = &signatures_sender { - if let Err(error) = signatures_sender.send(TransactionInfoBatch { - signatures, - sent_at: Utc::now(), - compute_unit_prices, - }) { - error!( - "Receiver has been dropped with error `{error}`, stop sending \ - transactions." - ); - break 'thread_loop; - } - } - - if let Err(error) = client.send_batch(transactions) { - warn!("send_batch_sync in do_tx_transfers failed: {error}"); - } - - datapoint_info!( - "bench-tps-do_tx_transfers", - ( - "time-elapsed-since-last-send", - timestamp() - last_sent_time, - i64 - ), - ); - - last_sent_time = timestamp(); - - if old_transactions { - let mut shared_txs_wl = shared_txs.write().expect("write lock in do_tx_transfers"); - shared_txs_wl.clear(); - } - shared_tx_thread_count.fetch_add(-1, Ordering::Relaxed); - total_tx_sent_count.fetch_add(num_txs, Ordering::Relaxed); - info!( - "Tx send done. {} ms {} tps", - transfer_start.elapsed().as_millis(), - num_txs as f32 / transfer_start.elapsed().as_secs_f32(), - ); - datapoint_info!( - "bench-tps-do_tx_transfers", - ("duration", transfer_start.elapsed().as_micros() as i64, i64), - ("count", num_txs, i64) - ); - } - if exit_signal.load(Ordering::Relaxed) { - break; - } - } -} - -fn compute_and_report_stats( - maxes: &Arc>>, - sample_period: u64, - tx_send_elapsed: &Duration, - total_tx_send_count: usize, -) { - // Compute/report stats - let mut max_of_maxes = 0.0; - let mut max_tx_count = 0; - let mut nodes_with_zero_tps = 0; - let mut total_maxes = 0.0; - info!(" Node address | Max TPS | Total Transactions"); - info!("---------------------+---------------+--------------------"); - - for (sock, stats) in maxes.read().unwrap().iter() { - let maybe_flag = match stats.txs { - 0 => "!!!!!", - _ => "", - }; - - info!( - "{:20} | {:13.2} | {} {}", - sock, stats.tps, stats.txs, maybe_flag - ); - - if stats.tps == 0.0 { - nodes_with_zero_tps += 1; - } - total_maxes += stats.tps; - - if stats.tps > max_of_maxes { - max_of_maxes = stats.tps; - } - if stats.txs > max_tx_count { - max_tx_count = stats.txs; - } - } - - if total_maxes > 0.0 { - let num_nodes_with_tps = maxes.read().unwrap().len() - nodes_with_zero_tps; - let average_max = total_maxes / num_nodes_with_tps as f32; - info!("\nAverage max TPS: {average_max:.2}, {nodes_with_zero_tps} nodes had 0 TPS"); - } - - let total_tx_send_count = total_tx_send_count as u64; - let drop_rate = if total_tx_send_count > max_tx_count { - (total_tx_send_count - max_tx_count) as f64 / total_tx_send_count as f64 - } else { - 0.0 - }; - info!( - "\nHighest TPS: {:.2} sampling period {}s max transactions: {} clients: {} drop rate: \ - {:.2}", - max_of_maxes, - sample_period, - max_tx_count, - maxes.read().unwrap().len(), - drop_rate, - ); - info!( - "\tAverage TPS: {}", - max_tx_count as f32 / tx_send_elapsed.as_secs_f32() - ); -} - -pub fn generate_and_fund_keypairs( - client: Arc, - funding_key: &Keypair, - keypair_count: usize, - lamports_per_account: u64, - skip_tx_account_data_size: bool, - enable_padding: bool, -) -> TpsClientResult> { - let rent = client.get_minimum_balance_for_rent_exemption(0)?; - let lamports_per_account = lamports_per_account + rent; - - info!("Creating {keypair_count} keypairs..."); - let (mut keypairs, extra) = generate_keypairs(funding_key, keypair_count as u64); - fund_keypairs( - client, - funding_key, - &keypairs, - extra, - lamports_per_account, - skip_tx_account_data_size, - enable_padding, - )?; - - // 'generate_keypairs' generates extra keys to be able to have size-aligned funding batches for fund_keys. - keypairs.truncate(keypair_count); - - Ok(keypairs) -} - -pub fn fund_keypairs( - client: Arc, - funding_key: &Keypair, - keypairs: &[Keypair], - extra: u64, - lamports_per_account: u64, - skip_tx_account_data_size: bool, - enable_padding: bool, -) -> TpsClientResult<()> { - let rent = client.get_minimum_balance_for_rent_exemption(0)?; - info!("Get lamports..."); - - // Sample the first keypair, to prevent lamport loss on repeated solana-bench-tps executions - let first_key = keypairs[0].pubkey(); - let first_keypair_balance = client.get_balance(&first_key).unwrap_or(0); - - // Sample the last keypair, to check if funding was already completed - let last_key = keypairs[keypairs.len() - 1].pubkey(); - let last_keypair_balance = client.get_balance(&last_key).unwrap_or(0); - - // Repeated runs will eat up keypair balances from transaction fees. In order to quickly - // start another bench-tps run without re-funding all of the keypairs, check if the - // keypairs still have at least 80% of the expected funds. That should be enough to - // pay for the transaction fees in a new run. - let enough_lamports = 8 * lamports_per_account / 10; - if first_keypair_balance < enough_lamports || last_keypair_balance < enough_lamports { - let single_sig_message = Message::new_with_blockhash( - &[Instruction::new_with_bytes( - Pubkey::new_unique(), - &[], - vec![AccountMeta::new(Pubkey::new_unique(), true)], - )], - None, - &client.get_latest_blockhash().unwrap(), - ); - let max_fee = client.get_fee_for_message(&single_sig_message).unwrap(); - let extra_fees = extra * max_fee; - let total_keypairs = keypairs.len() as u64 + 1; // Add one for funding keypair - let total = lamports_per_account * total_keypairs + extra_fees; - - let funding_key_balance = client.get_balance(&funding_key.pubkey()).unwrap_or(0); - info!( - "Funding keypair balance: {funding_key_balance} max_fee: {max_fee} \ - lamports_per_account: {lamports_per_account} extra: {extra} total: {total}" - ); - - if funding_key_balance < total + rent { - error!( - "funder has {}, needed {}", - Sol(funding_key_balance), - Sol(total) - ); - let latest_blockhash = get_latest_blockhash(client.as_ref()); - if client - .request_airdrop_with_blockhash( - &funding_key.pubkey(), - total + rent - funding_key_balance, - &latest_blockhash, - ) - .is_err() - { - return Err(TpsClientError::AirdropFailure); - } - } - let data_size_limit = (!skip_tx_account_data_size) - .then(|| get_transaction_loaded_accounts_data_size(enable_padding)); - - fund_keys( - client, - funding_key, - keypairs, - total, - max_fee, - lamports_per_account, - data_size_limit, - ); - } - Ok(()) -} - -#[cfg(test)] -mod tests { - use { - super::*, - agave_feature_set::FeatureSet, - solana_commitment_config::CommitmentConfig, - solana_fee_calculator::FeeRateGovernor, - solana_genesis_config::{GenesisConfig, create_genesis_config}, - solana_native_token::LAMPORTS_PER_SOL, - solana_nonce::state::State, - solana_runtime::{bank::Bank, bank_client::BankClient, bank_forks::BankForks}, - }; - - fn bank_with_all_features( - genesis_config: &GenesisConfig, - ) -> (Arc, Arc>) { - let mut bank = Bank::new_for_tests(genesis_config); - bank.feature_set = Arc::new(FeatureSet::all_enabled()); - bank.wrap_with_bank_forks_for_tests() - } - - fn run_bench_tps_bank_client(mut config: Config) { - let (genesis_config, id) = create_genesis_config(10_000 * LAMPORTS_PER_SOL); - config.id = id; - config.tx_count = 10; - config.duration = Duration::from_secs(5); - let (bank, _bank_forks) = bank_with_all_features(&genesis_config); - let client = Arc::new(BankClient::new_shared(bank)); - - let keypair_count = config.tx_count * config.keypair_multiplier; - let keypairs = generate_and_fund_keypairs( - client.clone(), - &config.id, - keypair_count, - 1_000_000_000, - false, - false, - ) - .unwrap(); - let nonce_keypairs = if config.use_durable_nonce { - Some(generate_durable_nonce_accounts(client.clone(), &keypairs)) - } else { - None - }; - - do_bench_tps(client, config, keypairs, nonce_keypairs); - } - - #[test] - fn test_bench_tps_bank_client() { - run_bench_tps_bank_client(Config::default()); - } - - #[test] - fn test_bench_tps_bank_client_nonce() { - let config = Config { - use_durable_nonce: true, - ..Config::default() - }; - run_bench_tps_bank_client(config); - } - - #[test] - fn test_bench_tps_bank_client_with_padding() { - let config = Config { - instruction_padding_config: Some(InstructionPaddingConfig { - program_id: spl_instruction_padding_interface::ID, - data_size: 0, - }), - ..Config::default() - }; - run_bench_tps_bank_client(config); - } - - #[test] - fn test_bench_tps_fund_keys() { - let (genesis_config, id) = create_genesis_config(10_000 * LAMPORTS_PER_SOL); - let (bank, _bank_forks) = bank_with_all_features(&genesis_config); - let client = Arc::new(BankClient::new_shared(bank)); - let keypair_count = 20; - let lamports = 20; - let rent = client.get_minimum_balance_for_rent_exemption(0).unwrap(); - - let keypairs = - generate_and_fund_keypairs(client.clone(), &id, keypair_count, lamports, false, false) - .unwrap(); - - for kp in &keypairs { - assert_eq!( - client - .get_balance_with_commitment(&kp.pubkey(), CommitmentConfig::processed()) - .unwrap(), - lamports + rent - ); - } - } - - #[test] - fn test_bench_tps_fund_keys_with_fees() { - let (mut genesis_config, id) = create_genesis_config(10_000 * LAMPORTS_PER_SOL); - let fee_rate_governor = FeeRateGovernor::new(11, 0); - genesis_config.fee_rate_governor = fee_rate_governor; - let (bank, _bank_forks) = bank_with_all_features(&genesis_config); - let client = Arc::new(BankClient::new_shared(bank)); - let keypair_count = 20; - let lamports = 20; - let rent = client.get_minimum_balance_for_rent_exemption(0).unwrap(); - - let keypairs = - generate_and_fund_keypairs(client.clone(), &id, keypair_count, lamports, false, false) - .unwrap(); - - for kp in &keypairs { - assert_eq!(client.get_balance(&kp.pubkey()).unwrap(), lamports + rent); - } - } - - #[test] - fn test_bench_tps_create_durable_nonce() { - let (genesis_config, id) = create_genesis_config(10_000 * LAMPORTS_PER_SOL); - let (bank, _bank_forks) = bank_with_all_features(&genesis_config); - let client = Arc::new(BankClient::new_shared(bank)); - let keypair_count = 10; - let lamports = 10_000_000; - - let authority_keypairs = - generate_and_fund_keypairs(client.clone(), &id, keypair_count, lamports, false, false) - .unwrap(); - - let nonce_keypairs = generate_durable_nonce_accounts(client.clone(), &authority_keypairs); - - let rent = client - .get_minimum_balance_for_rent_exemption(State::size()) - .unwrap(); - for kp in &nonce_keypairs { - assert_eq!( - client - .get_balance_with_commitment(&kp.pubkey(), CommitmentConfig::processed()) - .unwrap(), - rent - ); - } - withdraw_durable_nonce_accounts(client, &authority_keypairs, &nonce_keypairs) - } - - #[test] - fn test_bench_tps_key_chunks_new() { - let num_keypairs = 16; - let chunk_size = 4; - let keypairs = std::iter::repeat_with(Keypair::new) - .take(num_keypairs) - .collect::>(); - - let chunks = KeypairChunks::new(&keypairs, chunk_size); - assert_eq!( - chunks.source[0], - &[&keypairs[0], &keypairs[1], &keypairs[2], &keypairs[3]] - ); - assert_eq!( - chunks.dest[0], - &[&keypairs[4], &keypairs[5], &keypairs[6], &keypairs[7]] - ); - assert_eq!( - chunks.source[1], - &[&keypairs[8], &keypairs[9], &keypairs[10], &keypairs[11]] - ); - assert_eq!( - chunks.dest[1], - &[&keypairs[12], &keypairs[13], &keypairs[14], &keypairs[15]] - ); - } - - #[test] - fn test_bench_tps_key_chunks_new_with_conflict_groups() { - let num_keypairs = 16; - let chunk_size = 4; - let num_conflict_groups = 2; - let keypairs = std::iter::repeat_with(Keypair::new) - .take(num_keypairs) - .collect::>(); - - let chunks = - KeypairChunks::new_with_conflict_groups(&keypairs, chunk_size, num_conflict_groups); - assert_eq!( - chunks.source[0], - &[&keypairs[0], &keypairs[1], &keypairs[2], &keypairs[3]] - ); - assert_eq!( - chunks.dest[0], - &[&keypairs[4], &keypairs[5], &keypairs[4], &keypairs[5]] - ); - assert_eq!( - chunks.source[1], - &[&keypairs[8], &keypairs[9], &keypairs[10], &keypairs[11]] - ); - assert_eq!( - chunks.dest[1], - &[&keypairs[12], &keypairs[13], &keypairs[12], &keypairs[13]] - ); - } -} diff --git a/bench-tps/src/cli.rs b/bench-tps/src/cli.rs deleted file mode 100644 index 0211fbc982c..00000000000 --- a/bench-tps/src/cli.rs +++ /dev/null @@ -1,734 +0,0 @@ -use { - clap::{App, Arg, ArgMatches, crate_description, crate_name, value_t_or_exit}, - solana_clap_utils::{ - hidden_unless_forced, - input_validators::{is_keypair, is_url, is_url_or_moniker, is_within_range}, - }, - solana_cli_config::{CONFIG_FILE, ConfigInput}, - solana_commitment_config::CommitmentConfig, - solana_fee_calculator::FeeRateGovernor, - solana_keypair::{Keypair, read_keypair_file}, - solana_pubkey::Pubkey, - solana_streamer::quic::DEFAULT_MAX_CONNECTIONS_PER_IPADDR_PER_MINUTE, - solana_tpu_client::tpu_client::DEFAULT_TPU_CONNECTION_POOL_SIZE, - std::{ - net::{IpAddr, Ipv4Addr}, - time::Duration, - }, -}; - -const NUM_LAMPORTS_PER_ACCOUNT_DEFAULT: u64 = solana_native_token::LAMPORTS_PER_SOL; - -#[derive(Eq, PartialEq, Debug, Default)] -pub enum ExternalClientType { - // Submits transactions to an Rpc node using an RpcClient - RpcClient, - // Submits transactions directly to leaders using a TpuClient, broadcasting to upcoming leaders - // via TpuClient default configuration - #[default] - TpuClient, -} - -#[derive(Eq, PartialEq, Debug)] -pub struct InstructionPaddingConfig { - pub program_id: Pubkey, - pub data_size: u32, -} - -#[derive(Debug, PartialEq)] -pub enum ComputeUnitPrice { - Fixed(u64), - Random, -} - -/// Holds the configuration for a single run of the benchmark -#[derive(PartialEq, Debug)] -pub struct Config { - pub json_rpc_url: String, - pub websocket_url: String, - pub id: Keypair, - pub threads: usize, - pub duration: Duration, - pub tx_count: usize, - pub keypair_multiplier: usize, - pub thread_batch_sleep_ms: usize, - pub sustained: bool, - pub client_ids_and_stake_file: String, - pub write_to_client_file: bool, - pub read_from_client_file: bool, - pub target_lamports_per_signature: u64, - pub num_lamports_per_account: u64, - pub target_slots_per_epoch: u64, - pub external_client_type: ExternalClientType, - pub tpu_connection_pool_size: usize, - pub tpu_max_connections_per_ipaddr_per_minute: u64, - pub compute_unit_price: Option, - pub skip_tx_account_data_size: bool, - pub use_durable_nonce: bool, - pub instruction_padding_config: Option, - pub num_conflict_groups: Option, - pub bind_address: IpAddr, - pub client_node_id: Option, - pub commitment_config: CommitmentConfig, - pub block_data_file: Option, - pub transaction_data_file: Option, - pub use_txv1: bool, -} - -impl Eq for Config {} - -impl Default for Config { - fn default() -> Config { - Config { - json_rpc_url: ConfigInput::default().json_rpc_url, - websocket_url: ConfigInput::default().websocket_url, - id: Keypair::new(), - threads: 4, - duration: Duration::new(u64::MAX, 0), - tx_count: 50_000, - keypair_multiplier: 8, - thread_batch_sleep_ms: 1000, - sustained: false, - client_ids_and_stake_file: String::new(), - write_to_client_file: false, - read_from_client_file: false, - target_lamports_per_signature: FeeRateGovernor::default().target_lamports_per_signature, - num_lamports_per_account: NUM_LAMPORTS_PER_ACCOUNT_DEFAULT, - target_slots_per_epoch: 0, - external_client_type: ExternalClientType::default(), - tpu_connection_pool_size: DEFAULT_TPU_CONNECTION_POOL_SIZE, - tpu_max_connections_per_ipaddr_per_minute: - DEFAULT_MAX_CONNECTIONS_PER_IPADDR_PER_MINUTE, - compute_unit_price: None, - skip_tx_account_data_size: false, - use_durable_nonce: false, - instruction_padding_config: None, - num_conflict_groups: None, - bind_address: IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), - client_node_id: None, - commitment_config: CommitmentConfig::confirmed(), - block_data_file: None, - transaction_data_file: None, - use_txv1: false, - } - } -} - -/// Defines and builds the CLI args for a run of the benchmark -pub fn build_args<'a>(version: &'_ str) -> App<'a, '_> { - App::new(crate_name!()) - .about(crate_description!()) - .version(version) - .arg({ - let arg = Arg::with_name("config_file") - .short("C") - .long("config") - .value_name("FILEPATH") - .takes_value(true) - .global(true) - .help("Configuration file to use"); - if let Some(ref config_file) = *CONFIG_FILE { - arg.default_value(config_file) - } else { - arg - } - }) - .arg( - Arg::with_name("json_rpc_url") - .short("u") - .long("url") - .value_name("URL_OR_MONIKER") - .takes_value(true) - .global(true) - .validator(is_url_or_moniker) - .help( - "URL for Solana's JSON RPC or moniker (or their first letter): [mainnet-beta, \ - testnet, devnet, localhost]", - ), - ) - .arg( - Arg::with_name("websocket_url") - .long("ws") - .value_name("URL") - .takes_value(true) - .global(true) - .validator(is_url) - .help("WebSocket URL for the solana cluster"), - ) - .arg( - Arg::with_name("faucet") - .short("d") - .long("faucet") - .value_name("HOST:PORT") - .takes_value(true) - .hidden(hidden_unless_forced()) - .help("Deprecated. BenchTps no longer queries the faucet directly"), - ) - .arg( - Arg::with_name("identity") - .short("i") - .long("identity") - .value_name("PATH") - .takes_value(true) - .hidden(hidden_unless_forced()) - .help("Deprecated. Use --authority instead"), - ) - .arg( - Arg::with_name("authority") - .short("a") - .long("authority") - .value_name("PATH") - .takes_value(true) - .help( - "File containing a client authority (keypair) to fund participating accounts", - ), - ) - .arg( - Arg::with_name("threads") - .short("t") - .long("threads") - .value_name("NUM") - .takes_value(true) - .help("Number of threads"), - ) - .arg( - Arg::with_name("duration") - .long("duration") - .value_name("SECS") - .takes_value(true) - .help("Seconds to run benchmark, then exit; default is forever"), - ) - .arg(Arg::with_name("sustained").long("sustained").help( - "Use sustained performance mode vs. peak mode. This overlaps the tx generation with \ - transfers.", - )) - .arg( - Arg::with_name("tx_count") - .long("tx-count") - .alias("tx_count") - .value_name("NUM") - .takes_value(true) - .help("Number of transactions to send per batch"), - ) - .arg( - Arg::with_name("keypair_multiplier") - .long("keypair-multiplier") - .value_name("NUM") - .takes_value(true) - .help("Multiply by transaction count to determine number of keypairs to create"), - ) - .arg( - Arg::with_name("thread-batch-sleep-ms") - .short("z") - .long("thread-batch-sleep-ms") - .value_name("NUM") - .takes_value(true) - .help("Per-thread-per-iteration sleep in ms"), - ) - .arg( - Arg::with_name("write-client-keys") - .long("write-client-keys") - .value_name("FILENAME") - .takes_value(true) - .help("Generate client keys and stakes and write the list to YAML file"), - ) - .arg( - Arg::with_name("read-client-keys") - .long("read-client-keys") - .value_name("FILENAME") - .takes_value(true) - .help("Read client keys and stakes from the YAML file"), - ) - .arg( - Arg::with_name("target_lamports_per_signature") - .long("target-lamports-per-signature") - .value_name("LAMPORTS") - .takes_value(true) - .help( - "The cost in lamports that the cluster will charge for signature verification \ - when the cluster is operating at target-signatures-per-slot", - ), - ) - .arg( - Arg::with_name("num_lamports_per_account") - .long("num-lamports-per-account") - .value_name("LAMPORTS") - .takes_value(true) - .help("Number of lamports per account."), - ) - .arg( - Arg::with_name("target_slots_per_epoch") - .long("target-slots-per-epoch") - .value_name("SLOTS") - .takes_value(true) - .help("Wait until epochs are this many slots long."), - ) - .arg( - Arg::with_name("rpc_client") - .long("use-rpc-client") - .conflicts_with("tpu_client") - .takes_value(false) - .help("Submit transactions with a RpcClient"), - ) - .arg( - Arg::with_name("tpu_client") - .long("use-tpu-client") - .conflicts_with("rpc_client") - .takes_value(false) - .help("Submit transactions with a TpuClient"), - ) - .arg( - Arg::with_name("tpu_connection_pool_size") - .long("tpu-connection-pool-size") - .takes_value(true) - .help( - "Controls the connection pool size per remote address; only affects TpuClient \ - (default) sends", - ), - ) - .arg( - Arg::with_name("compute_unit_price") - .long("compute-unit-price") - .takes_value(true) - .validator(|s| is_within_range(s, 0..)) - .help("Sets constant compute-unit-price to transfer transactions"), - ) - .arg( - Arg::with_name("use_randomized_compute_unit_price") - .long("use-randomized-compute-unit-price") - .takes_value(false) - .conflicts_with("compute_unit_price") - .help("Sets random compute-unit-price in range [0..100] to transfer transactions"), - ) - .arg( - Arg::with_name("skip_tx_account_data_size") - .long("skip-tx-account-data-size") - .takes_value(false) - .conflicts_with("instruction_padding_data_size") - .help("Skips setting the account data size for each transaction"), - ) - .arg( - Arg::with_name("use_durable_nonce") - .long("use-durable-nonce") - .help("Use durable transaction nonce instead of recent blockhash"), - ) - .arg( - Arg::with_name("instruction_padding_program_id") - .long("instruction-padding-program-id") - .requires("instruction_padding_data_size") - .takes_value(true) - .value_name("PUBKEY") - .help( - "If instruction data is padded, optionally specify the padding program id to \ - target", - ), - ) - .arg( - Arg::with_name("instruction_padding_data_size") - .long("instruction-padding-data-size") - .takes_value(true) - .help( - "If set, wraps all instructions in the instruction padding program, with the \ - given amount of padding bytes in instruction data.", - ), - ) - .arg( - Arg::with_name("num_conflict_groups") - .long("num-conflict-groups") - .takes_value(true) - .validator(|arg| is_within_range(arg, 1..)) - .help( - "The number of unique destination accounts per transactions 'chunk'. Lower \ - values will result in more transaction conflicts.", - ), - ) - .arg( - Arg::with_name("bind_address") - .long("bind-address") - .value_name("HOST") - .takes_value(true) - .validator(solana_net_utils::is_host) - .requires("client_node_id") - .help("IP address to use with connection cache"), - ) - .arg( - Arg::with_name("client_node_id") - .long("client-node-id") - .value_name("PATH") - .takes_value(true) - .requires("json_rpc_url") - .validator(is_keypair) - .help( - "File containing the node identity (keypair) of a validator with active \ - stake. This allows communicating with network using staked connection", - ), - ) - .arg( - Arg::with_name("commitment_config") - .long("commitment-config") - .takes_value(true) - .possible_values(&["processed", "confirmed", "finalized"]) - .default_value("confirmed") - .help("Block commitment config for getting latest blockhash"), - ) - .arg( - Arg::with_name("block_data_file") - .long("block-data-file") - .value_name("FILENAME") - .takes_value(true) - .help("File to save block statistics relevant to the submitted transactions."), - ) - .arg( - Arg::with_name("transaction_data_file") - .long("transaction-data-file") - .value_name("FILENAME") - .takes_value(true) - .help( - "File to save details about all the submitted transactions.This option is \ - useful for debug purposes.", - ), - ) - .arg( - Arg::with_name("use_txv1") - .long("use-txv1") - .takes_value(false) - .help("Generate and send Transfer transaction in V1 format"), - ) -} - -/// Parses a clap `ArgMatches` structure into a `Config` -pub fn parse_args(matches: &ArgMatches) -> Result { - let mut args = Config::default(); - - let config = if let Some(config_file) = matches.value_of("config_file") { - solana_cli_config::Config::load(config_file).unwrap_or_default() - } else { - solana_cli_config::Config::default() - }; - let (_, json_rpc_url) = ConfigInput::compute_json_rpc_url_setting( - matches.value_of("json_rpc_url").unwrap_or(""), - &config.json_rpc_url, - ); - args.json_rpc_url = json_rpc_url; - - let (_, websocket_url) = ConfigInput::compute_websocket_url_setting( - matches.value_of("websocket_url").unwrap_or(""), - &config.websocket_url, - matches.value_of("json_rpc_url").unwrap_or(""), - &config.json_rpc_url, - ); - args.websocket_url = websocket_url; - - if matches.is_present("identity") { - eprintln!("Warning: --identity is deprecated. Please use --authority"); - } - - let (_, id_path) = ConfigInput::compute_keypair_path_setting( - matches - .value_of("authority") - .or(matches.value_of("identity")) - .unwrap_or(""), - &config.keypair_path, - ); - if let Ok(id) = read_keypair_file(id_path) { - args.id = id; - } else if matches.is_present("identity") || matches.is_present("authority") { - return Err("could not parse authority path"); - } - - if matches.is_present("rpc_client") { - args.external_client_type = ExternalClientType::RpcClient; - } - - if let Some(v) = matches.value_of("tpu_connection_pool_size") { - args.tpu_connection_pool_size = v - .to_string() - .parse::() - .map_err(|_| "can't parse tpu-connection-pool-size")?; - } - - if let Some(t) = matches.value_of("threads") { - args.threads = t.to_string().parse().map_err(|_| "can't parse threads")?; - } - - if let Some(duration) = matches.value_of("duration") { - let seconds = duration - .to_string() - .parse() - .map_err(|_| "can't parse duration")?; - args.duration = Duration::new(seconds, 0); - } - - if let Some(s) = matches.value_of("tx_count") { - args.tx_count = s.to_string().parse().map_err(|_| "can't parse tx_count")?; - } - - if let Some(s) = matches.value_of("keypair_multiplier") { - args.keypair_multiplier = s - .to_string() - .parse() - .map_err(|_| "can't parse keypair-multiplier")?; - if args.keypair_multiplier < 2 { - return Err("args.keypair_multiplier must be greater than or equal to 2"); - } - } - - if let Some(t) = matches.value_of("thread-batch-sleep-ms") { - args.thread_batch_sleep_ms = t - .to_string() - .parse() - .map_err(|_| "can't parse thread-batch-sleep-ms")?; - } - - args.sustained = matches.is_present("sustained"); - - if let Some(s) = matches.value_of("write-client-keys") { - args.write_to_client_file = true; - args.client_ids_and_stake_file = s.to_string(); - } - - if let Some(s) = matches.value_of("read-client-keys") { - assert!(!args.write_to_client_file); - args.read_from_client_file = true; - args.client_ids_and_stake_file = s.to_string(); - } - - if let Some(v) = matches.value_of("target_lamports_per_signature") { - args.target_lamports_per_signature = v - .to_string() - .parse() - .map_err(|_| "can't parse target-lamports-per-signature")?; - } - - if let Some(v) = matches.value_of("num_lamports_per_account") { - args.num_lamports_per_account = v - .to_string() - .parse() - .map_err(|_| "can't parse num-lamports-per-account")?; - } - - if let Some(t) = matches.value_of("target_slots_per_epoch") { - args.target_slots_per_epoch = t - .to_string() - .parse() - .map_err(|_| "can't parse target-slots-per-epoch")?; - } - - if let Some(str) = matches.value_of("compute_unit_price") { - args.compute_unit_price = Some(ComputeUnitPrice::Fixed( - str.parse().map_err(|_| "can't parse compute-unit-price")?, - )); - } - - if matches.is_present("use_randomized_compute_unit_price") { - args.compute_unit_price = Some(ComputeUnitPrice::Random); - } - - if matches.is_present("skip_tx_account_data_size") { - args.skip_tx_account_data_size = true; - } - - if matches.is_present("use_durable_nonce") { - args.use_durable_nonce = true; - } - - if let Some(data_size) = matches.value_of("instruction_padding_data_size") { - let program_id = matches - .value_of("instruction_padding_program_id") - .map(|target_str| target_str.parse().unwrap()) - .unwrap_or_else(|| spl_instruction_padding_interface::ID); - let data_size = data_size - .parse() - .map_err(|_| "Can't parse padded instruction data size")?; - args.instruction_padding_config = Some(InstructionPaddingConfig { - program_id, - data_size, - }); - } - - if let Some(num_conflict_groups) = matches.value_of("num_conflict_groups") { - let parsed_num_conflict_groups = num_conflict_groups - .parse() - .map_err(|_| "Can't parse num-conflict-groups")?; - args.num_conflict_groups = Some(parsed_num_conflict_groups); - } - - if let Some(addr) = matches.value_of("bind_address") { - args.bind_address = - solana_net_utils::parse_host(addr).map_err(|_| "Failed to parse bind-address")?; - } - - if let Some(client_node_id_filename) = matches.value_of("client_node_id") { - // error is checked by arg validator - let client_node_id = read_keypair_file(client_node_id_filename).map_err(|_| "")?; - args.client_node_id = Some(client_node_id); - } - - args.commitment_config = value_t_or_exit!(matches, "commitment_config", CommitmentConfig); - args.block_data_file = matches.value_of("block_data_file").map(|s| s.to_string()); - args.transaction_data_file = matches - .value_of("transaction_data_file") - .map(|s| s.to_string()); - - args.use_txv1 = matches.is_present("use_txv1"); - - Ok(args) -} - -#[cfg(test)] -mod tests { - use { - super::*, - solana_keypair::{Keypair, read_keypair_file, write_keypair_file}, - solana_signer::Signer, - std::{ - net::{IpAddr, Ipv4Addr}, - time::Duration, - }, - tempfile::{TempDir, tempdir}, - }; - - /// create a keypair and write it to json file in temporary directory - /// return both generated keypair and full file name - fn write_tmp_keypair(out_dir: &TempDir) -> (Keypair, String) { - let keypair = Keypair::new(); - let file_path = out_dir - .path() - .join(format!("keypair_file-{}", keypair.pubkey())); - let keypair_file_name = file_path.into_os_string().into_string().unwrap(); - write_keypair_file(&keypair, &keypair_file_name).unwrap(); - (keypair, keypair_file_name) - } - - #[test] - #[allow(clippy::cognitive_complexity)] - fn test_cli_parse() { - // create a directory inside of std::env::temp_dir(), removed when out_dir goes out of scope - let out_dir = tempdir().unwrap(); - let (keypair, keypair_file_name) = write_tmp_keypair(&out_dir); - - // parse provided rpc address, check that default ws address is correct - // always specify authority in these tests because otherwise a random one will be used - let matches = build_args("1.0.0").get_matches_from(vec![ - "solana-bench-tps", - "--authority", - &keypair_file_name, - "-u", - "http://123.4.5.6:8899", - ]); - let actual = parse_args(&matches).unwrap(); - assert_eq!( - actual, - Config { - json_rpc_url: "http://123.4.5.6:8899".to_string(), - websocket_url: "ws://123.4.5.6:8900/".to_string(), - id: keypair, - ..Config::default() - } - ); - - // check if --identity is working - let keypair = read_keypair_file(&keypair_file_name).unwrap(); - let matches = build_args("1.0.0").get_matches_from(vec![ - "solana-bench-tps", - "--identity", - &keypair_file_name, - "-u", - "http://123.4.5.6:8899", - ]); - let actual = parse_args(&matches).unwrap(); - assert_eq!( - actual, - Config { - json_rpc_url: "http://123.4.5.6:8899".to_string(), - websocket_url: "ws://123.4.5.6:8900/".to_string(), - id: keypair, - ..Config::default() - } - ); - - // parse cli args typical for private cluster tests - let keypair = read_keypair_file(&keypair_file_name).unwrap(); - let matches = build_args("1.0.0").get_matches_from(vec![ - "solana-bench-tps", - "--authority", - &keypair_file_name, - "-u", - "http://123.4.5.6:8899", - "--duration", - "1000", - "--sustained", - "--threads", - "4", - "--read-client-keys", - "./client-accounts.yml", - ]); - let actual = parse_args(&matches).unwrap(); - assert_eq!( - actual, - Config { - json_rpc_url: "http://123.4.5.6:8899".to_string(), - websocket_url: "ws://123.4.5.6:8900/".to_string(), - id: keypair, - duration: Duration::new(1000, 0), - sustained: true, - threads: 4, - read_from_client_file: true, - client_ids_and_stake_file: "./client-accounts.yml".to_string(), - ..Config::default() - } - ); - - // select different client type and CommitmentConfig - let keypair = read_keypair_file(&keypair_file_name).unwrap(); - let matches = build_args("1.0.0").get_matches_from(vec![ - "solana-bench-tps", - "--authority", - &keypair_file_name, - "-u", - "http://123.4.5.6:8899", - "--use-rpc-client", - "--commitment-config", - "finalized", - ]); - let actual = parse_args(&matches).unwrap(); - assert_eq!( - actual, - Config { - json_rpc_url: "http://123.4.5.6:8899".to_string(), - websocket_url: "ws://123.4.5.6:8900/".to_string(), - id: keypair, - external_client_type: ExternalClientType::RpcClient, - commitment_config: CommitmentConfig::finalized(), - ..Config::default() - } - ); - - // with client node id - let keypair = read_keypair_file(&keypair_file_name).unwrap(); - let (client_id, client_id_file_name) = write_tmp_keypair(&out_dir); - let matches = build_args("1.0.0").get_matches_from(vec![ - "solana-bench-tps", - "--authority", - &keypair_file_name, - "-u", - "http://192.0.0.1:8899", - "--bind-address", - "192.9.8.7", - "--client-node-id", - &client_id_file_name, - ]); - let actual = parse_args(&matches).unwrap(); - assert_eq!( - actual, - Config { - json_rpc_url: "http://192.0.0.1:8899".to_string(), - websocket_url: "ws://192.0.0.1:8900/".to_string(), - id: keypair, - bind_address: IpAddr::V4(Ipv4Addr::new(192, 9, 8, 7)), - client_node_id: Some(client_id), - ..Config::default() - } - ); - } -} diff --git a/bench-tps/src/keypairs.rs b/bench-tps/src/keypairs.rs deleted file mode 100644 index b278bc0e619..00000000000 --- a/bench-tps/src/keypairs.rs +++ /dev/null @@ -1,82 +0,0 @@ -use { - crate::bench::{fund_keypairs, generate_and_fund_keypairs}, - log::*, - solana_genesis::Base64Account, - solana_keypair::Keypair, - solana_signer::Signer, - solana_tps_client::TpsClient, - std::{collections::HashMap, fs::File, path::Path, process::exit, sync::Arc}, -}; - -pub fn get_keypairs( - client: Arc, - id: &Keypair, - keypair_count: usize, - num_lamports_per_account: u64, - client_ids_and_stake_file: &str, - read_from_client_file: bool, - skip_tx_account_data_size: bool, - enable_padding: bool, -) -> Vec -where - T: 'static + TpsClient + Send + Sync + ?Sized, -{ - if read_from_client_file { - let path = Path::new(client_ids_and_stake_file); - let file = File::open(path).unwrap(); - - info!("Reading {client_ids_and_stake_file}"); - let accounts: HashMap = serde_yaml::from_reader(file).unwrap(); - let mut keypairs = vec![]; - let mut last_balance = 0; - - accounts - .into_iter() - .for_each(|(keypair, primordial_account)| { - let bytes: Vec = serde_json::from_str(keypair.as_str()).unwrap(); - keypairs.push(Keypair::try_from(bytes.as_ref()).unwrap()); - last_balance = primordial_account.balance; - }); - - if keypairs.len() < keypair_count { - eprintln!( - "Expected {} accounts in {}, only received {} (--tx_count mismatch?)", - keypair_count, - client_ids_and_stake_file, - keypairs.len(), - ); - exit(1); - } - // Sort keypairs so that do_bench_tps() uses the same subset of accounts for each run. - // This prevents the amount of storage needed for bench-tps accounts from creeping up - // across multiple runs. - keypairs.sort_by_key(|x| x.pubkey().to_string()); - fund_keypairs( - client, - id, - &keypairs, - keypairs.len().saturating_sub(keypair_count) as u64, - last_balance, - skip_tx_account_data_size, - enable_padding, - ) - .unwrap_or_else(|e| { - eprintln!("Error could not fund keys: {e:?}"); - exit(1); - }); - keypairs - } else { - generate_and_fund_keypairs( - client, - id, - keypair_count, - num_lamports_per_account, - skip_tx_account_data_size, - enable_padding, - ) - .unwrap_or_else(|e| { - eprintln!("Error could not fund keys: {e:?}"); - exit(1); - }) - } -} diff --git a/bench-tps/src/lib.rs b/bench-tps/src/lib.rs deleted file mode 100644 index 395f684a6a4..00000000000 --- a/bench-tps/src/lib.rs +++ /dev/null @@ -1,9 +0,0 @@ -#![cfg(feature = "agave-unstable-api")] -#![allow(clippy::arithmetic_side_effects)] -pub mod bench; -pub mod cli; -pub mod keypairs; -mod log_transaction_service; -mod perf_utils; -mod rpc_with_retry_utils; -pub mod send_batch; diff --git a/bench-tps/src/log_transaction_service.rs b/bench-tps/src/log_transaction_service.rs deleted file mode 100644 index 3b8a1f45b07..00000000000 --- a/bench-tps/src/log_transaction_service.rs +++ /dev/null @@ -1,496 +0,0 @@ -//! `LogTransactionService` requests confirmed blocks, analyses transactions submitted by bench-tps, -//! and saves log files in csv format. - -use { - crate::rpc_with_retry_utils::{get_blocks_with_retry, get_slot_with_retry}, - chrono::{DateTime, TimeZone, Utc}, - crossbeam_channel::{Receiver, Sender, select, tick, unbounded}, - log::*, - serde::Serialize, - solana_clock::{DEFAULT_MS_PER_SLOT, MAX_PROCESSING_AGE, Slot}, - solana_commitment_config::{CommitmentConfig, CommitmentLevel}, - solana_measure::measure::Measure, - solana_rpc_client_api::config::RpcBlockConfig, - solana_signature::Signature, - solana_tps_client::TpsClient, - solana_transaction_status::{ - EncodedTransactionWithStatusMeta, RewardType, TransactionDetails, UiConfirmedBlock, - UiTransactionEncoding, UiTransactionStatusMeta, option_serializer::OptionSerializer, - }, - std::{ - collections::HashMap, - fs::File, - sync::Arc, - thread::{self, Builder, JoinHandle}, - time::Duration, - }, -}; - -// Data to establish communication between sender thread and -// LogTransactionService. -#[derive(Clone)] -pub(crate) struct TransactionInfoBatch { - pub signatures: Vec, - pub sent_at: DateTime, - pub compute_unit_prices: Vec>, -} - -pub(crate) type SignatureBatchSender = Sender; - -pub(crate) struct LogTransactionService { - thread_handler: JoinHandle<()>, -} - -pub(crate) fn create_log_transactions_service_and_sender( - client: &Arc, - block_data_file: Option<&str>, - transaction_data_file: Option<&str>, -) -> (Option, Option) -where - Client: 'static + TpsClient + Send + Sync + ?Sized, -{ - if data_file_provided(block_data_file, transaction_data_file) { - let (sender, receiver) = unbounded(); - let log_tx_service = - LogTransactionService::new(client, receiver, block_data_file, transaction_data_file); - (Some(log_tx_service), Some(sender)) - } else { - (None, None) - } -} - -// How many blocks to process during one iteration. -// The time to process blocks is dominated by get_block calls. -// Each call takes slightly less time than slot. -const NUM_SLOTS_PER_ITERATION: u64 = 16; -// How often process blocks. -const PROCESS_BLOCKS_EVERY_MS: u64 = NUM_SLOTS_PER_ITERATION * DEFAULT_MS_PER_SLOT; -// Max age for transaction in the transaction map, older transactions are cleaned up and marked as timeout. -const REMOVE_TIMEOUT_TX_EVERY_MS: i64 = MAX_PROCESSING_AGE as i64 * DEFAULT_MS_PER_SLOT as i64; - -// Map used to filter submitted transactions. -#[derive(Clone)] -struct TransactionSendInfo { - pub sent_at: DateTime, - pub compute_unit_price: Option, -} -type MapSignatureToTxInfo = HashMap; - -type SignatureBatchReceiver = Receiver; - -impl LogTransactionService { - fn new( - client: &Arc, - signature_receiver: SignatureBatchReceiver, - block_data_file: Option<&str>, - transaction_data_file: Option<&str>, - ) -> Self - where - Client: 'static + TpsClient + Send + Sync + ?Sized, - { - if !data_file_provided(block_data_file, transaction_data_file) { - panic!( - "Expect block-data-file or transaction-data-file is specified, must have been \ - verified by callee." - ); - } - - let client = client.clone(); - let tx_log_writer = TransactionLogWriter::new(transaction_data_file); - let block_log_writer = BlockLogWriter::new(block_data_file); - - let thread_handler = Builder::new() - .name("LogTransactionService".to_string()) - .spawn(move || { - Self::run(client, signature_receiver, tx_log_writer, block_log_writer); - }) - .expect("LogTransactionService should have started successfully."); - Self { thread_handler } - } - - pub fn join(self) -> thread::Result<()> { - self.thread_handler.join() - } - - fn run( - client: Arc, - signature_receiver: SignatureBatchReceiver, - mut tx_log_writer: TransactionLogWriter, - mut block_log_writer: BlockLogWriter, - ) where - Client: 'static + TpsClient + Send + Sync + ?Sized, - { - // used to request blocks data and only confirmed makes sense in this context. - let commitment: CommitmentConfig = CommitmentConfig { - commitment: CommitmentLevel::Confirmed, - }; - let block_processing_timer_receiver = tick(Duration::from_millis(PROCESS_BLOCKS_EVERY_MS)); - - let mut start_slot = get_slot_with_retry(&client, commitment).expect( - "get_slot_with_retry should have succeed, cannot proceed without having slot. Must be \ - a problem with RPC.", - ); - - let mut sender_stopped = false; - let mut signature_to_tx_info = MapSignatureToTxInfo::new(); - loop { - select! { - recv(signature_receiver) -> msg => { - match msg { - Ok(TransactionInfoBatch { - signatures, - sent_at, - compute_unit_prices - }) => { - signatures.iter().zip(compute_unit_prices).for_each( |(sign, compute_unit_price)| {signature_to_tx_info.insert(*sign, TransactionSendInfo { - sent_at, - compute_unit_price - });}); - } - Err(_) => { - sender_stopped = true; - } - } - }, - recv(block_processing_timer_receiver) -> _ => { - info!("sign_receiver queue len: {}", signature_receiver.len()); - if !signature_receiver.is_empty() { - continue; - } - let mut measure_get_blocks = Measure::start("measure_get_blocks"); - let block_slots = get_blocks_with_retry(&client, start_slot, Some(start_slot + NUM_SLOTS_PER_ITERATION - 1), commitment); - measure_get_blocks.stop(); - let time_get_blocks_us = measure_get_blocks.as_us(); - info!("Time to get_blocks : {time_get_blocks_us}us."); - let Ok(block_slots) = block_slots else { - error!("Failed to get blocks, stop LogWriterService."); - break; - }; - if block_slots.is_empty() { - continue; - } - let last_block_time = Self::process_blocks( - &client, - block_slots, - &mut signature_to_tx_info, - &mut tx_log_writer, - &mut block_log_writer, - commitment, - ); - Self::clean_transaction_map(&mut tx_log_writer, &mut signature_to_tx_info, last_block_time); - - start_slot = start_slot.saturating_add(NUM_SLOTS_PER_ITERATION); - tx_log_writer.flush(); - block_log_writer.flush(); - if sender_stopped && signature_to_tx_info.is_empty() { - info!("Stop LogTransactionService"); - break; - } - }, - } - } - } - - /// Download and process the blocks. - /// Returns the time when the last processed block has been confirmed or now(). - fn process_blocks( - client: &Arc, - block_slots: Vec, - signature_to_tx_info: &mut MapSignatureToTxInfo, - tx_log_writer: &mut TransactionLogWriter, - block_log_writer: &mut BlockLogWriter, - commitment: CommitmentConfig, - ) -> DateTime - where - Client: 'static + TpsClient + Send + Sync + ?Sized, - { - let rpc_block_config = RpcBlockConfig { - encoding: Some(UiTransactionEncoding::Base64), - transaction_details: Some(TransactionDetails::Full), - rewards: Some(true), - commitment: Some(commitment), - max_supported_transaction_version: Some(0), - }; - let mut measure_process_blocks = Measure::start("measure_process_blocks"); - let blocks = block_slots - .iter() - .map(|slot| client.get_block_with_config(*slot, rpc_block_config)); - let num_blocks = blocks.len(); - let mut last_block_time = None; - for (block, slot) in blocks.zip(&block_slots) { - let Ok(block) = block else { - continue; - }; - let block_time = Self::process_block( - block, - signature_to_tx_info, - *slot, - tx_log_writer, - block_log_writer, - ); - // if last_time is some, it means that there is at least one valid block - if block_time.is_some() { - last_block_time = block_time; - } - } - measure_process_blocks.stop(); - let time_process_blocks_us = measure_process_blocks.as_us(); - info!("Time to process {num_blocks} blocks: {time_process_blocks_us}us."); - last_block_time.unwrap_or_else(Utc::now) - } - - fn process_block( - block: UiConfirmedBlock, - signature_to_tx_info: &mut MapSignatureToTxInfo, - slot: u64, - tx_log_writer: &mut TransactionLogWriter, - block_log_writer: &mut BlockLogWriter, - ) -> Option> { - let rewards = block - .rewards - .as_ref() - .expect("Rewards should be part of the block information."); - let slot_leader = rewards - .iter() - .find(|r| r.reward_type == Some(RewardType::Fee)) - .map_or("".to_string(), |x| x.pubkey.clone()); - - let Some(transactions) = &block.transactions else { - warn!("Empty block: {slot}"); - return None; - }; - - let mut num_bench_tps_transactions: usize = 0; - let mut total_cu_consumed: u64 = 0; - let mut bench_tps_cu_consumed: u64 = 0; - for EncodedTransactionWithStatusMeta { - transaction, meta, .. - } in transactions - { - let Some(transaction) = transaction.decode() else { - continue; - }; - let cu_consumed = meta - .as_ref() - .map_or(0, |meta| match meta.compute_units_consumed { - OptionSerializer::Some(cu_consumed) => cu_consumed, - _ => 0, - }); - let signature = &transaction.signatures[0]; - - total_cu_consumed = total_cu_consumed.saturating_add(cu_consumed); - if let Some(TransactionSendInfo { - sent_at, - compute_unit_price, - }) = signature_to_tx_info.remove(signature) - { - num_bench_tps_transactions = num_bench_tps_transactions.saturating_add(1); - bench_tps_cu_consumed = bench_tps_cu_consumed.saturating_add(cu_consumed); - - tx_log_writer.write( - Some(block.blockhash.clone()), - Some(slot_leader.clone()), - signature, - sent_at, - Some(slot), - block.block_time, - meta.as_ref(), - false, - compute_unit_price, - ); - } - } - block_log_writer.write( - block.blockhash.clone(), - slot_leader, - slot, - block.block_time, - num_bench_tps_transactions, - transactions.len(), - bench_tps_cu_consumed, - total_cu_consumed, - ); - - block.block_time.map(|time| { - Utc.timestamp_opt(time, 0) - .latest() - .expect("valid timestamp") - }) - } - - /// Remove from map all the signatures which we haven't processed before and they are - /// older than the timestamp of the last processed block plus max blockhash age. - fn clean_transaction_map( - tx_log_writer: &mut TransactionLogWriter, - signature_to_tx_info: &mut MapSignatureToTxInfo, - last_block_time: DateTime, - ) { - signature_to_tx_info.retain(|signature, tx_info| { - let duration_since_sent = last_block_time.signed_duration_since(tx_info.sent_at); - let is_timeout_tx = duration_since_sent.num_milliseconds() > REMOVE_TIMEOUT_TX_EVERY_MS; - if is_timeout_tx { - tx_log_writer.write( - None, - None, - signature, - tx_info.sent_at, - None, - None, - None, - true, - tx_info.compute_unit_price, - ); - } - !is_timeout_tx - }); - } -} - -fn data_file_provided(block_data_file: Option<&str>, transaction_data_file: Option<&str>) -> bool { - block_data_file.is_some() || transaction_data_file.is_some() -} - -type CsvFileWriter = csv::Writer; - -#[derive(Clone, Serialize)] -struct BlockData { - pub blockhash: String, - pub block_slot: Slot, - pub slot_leader: String, - pub block_time: Option>, - pub total_num_transactions: usize, - pub num_bench_tps_transactions: usize, - pub total_cu_consumed: u64, - pub bench_tps_cu_consumed: u64, -} - -struct BlockLogWriter { - log_writer: Option, -} - -impl BlockLogWriter { - fn new(block_data_file: Option<&str>) -> Self { - let block_log_writer = block_data_file.map(|block_data_file| { - CsvFileWriter::from_writer( - File::create(block_data_file) - .expect("Application should be able to create a file."), - ) - }); - Self { - log_writer: block_log_writer, - } - } - - #[allow(clippy::too_many_arguments)] - fn write( - &mut self, - blockhash: String, - slot_leader: String, - slot: Slot, - block_time: Option, - num_bench_tps_transactions: usize, - total_num_transactions: usize, - bench_tps_cu_consumed: u64, - total_cu_consumed: u64, - ) { - let Some(block_log_writer) = &mut self.log_writer else { - return; - }; - let block_data = BlockData { - blockhash, - slot_leader, - block_slot: slot, - block_time: block_time.map(|time| { - Utc.timestamp_opt(time, 0) - .latest() - .expect("timestamp should be valid") - }), - num_bench_tps_transactions, - total_num_transactions, - bench_tps_cu_consumed, - total_cu_consumed, - }; - let _ = block_log_writer.serialize(block_data); - } - - fn flush(&mut self) { - if let Some(block_log_writer) = &mut self.log_writer { - let _ = block_log_writer.flush(); - } - } -} - -#[derive(Clone, Serialize)] -struct TransactionData { - pub blockhash: Option, - pub slot_leader: Option, - pub signature: String, - pub sent_at: Option>, - pub confirmed_slot: Option, - pub block_time: Option>, - pub successful: bool, - pub error: Option, - pub timed_out: bool, - pub compute_unit_price: u64, -} - -struct TransactionLogWriter { - log_writer: Option, -} - -impl TransactionLogWriter { - fn new(transaction_data_file: Option<&str>) -> Self { - let transaction_log_writer = transaction_data_file.map(|transaction_data_file| { - CsvFileWriter::from_writer( - File::create(transaction_data_file) - .expect("Application should be able to create a file."), - ) - }); - Self { - log_writer: transaction_log_writer, - } - } - - #[allow(clippy::too_many_arguments)] - fn write( - &mut self, - blockhash: Option, - slot_leader: Option, - signature: &Signature, - sent_at: DateTime, - confirmed_slot: Option, - block_time: Option, - meta: Option<&UiTransactionStatusMeta>, - timed_out: bool, - compute_unit_price: Option, - ) { - let Some(transaction_log_writer) = &mut self.log_writer else { - return; - }; - let tx_data = TransactionData { - blockhash, - slot_leader, - signature: signature.to_string(), - sent_at: Some(sent_at), - confirmed_slot, - block_time: block_time.map(|time| { - Utc.timestamp_opt(time, 0) - .latest() - .expect("valid timestamp") - }), - successful: meta.as_ref().is_some_and(|m| m.status.is_ok()), - error: meta - .as_ref() - .and_then(|m| m.err.as_ref().map(|x| x.to_string())), - timed_out, - compute_unit_price: compute_unit_price.unwrap_or(0), - }; - let _ = transaction_log_writer.serialize(tx_data); - } - - fn flush(&mut self) { - if let Some(transaction_log_writer) = &mut self.log_writer { - let _ = transaction_log_writer.flush(); - } - } -} diff --git a/bench-tps/src/main.rs b/bench-tps/src/main.rs deleted file mode 100644 index 76ad2367335..00000000000 --- a/bench-tps/src/main.rs +++ /dev/null @@ -1,277 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -use { - log::*, - solana_bench_tps::{ - bench::{do_bench_tps, max_lamports_for_prioritization}, - cli::{self, ExternalClientType}, - keypairs::get_keypairs, - send_batch::{generate_durable_nonce_accounts, generate_keypairs}, - }, - solana_client::connection_cache::ConnectionCache, - solana_commitment_config::CommitmentConfig, - solana_fee_calculator::FeeRateGovernor, - solana_genesis::Base64Account, - solana_keypair::Keypair, - solana_pubkey::Pubkey, - solana_rpc_client::rpc_client::RpcClient, - solana_signer::Signer, - solana_streamer::streamer::StakedNodes, - solana_system_interface::program as system_program, - solana_tps_client::TpsClient, - solana_tpu_client::tpu_client::{TpuClient, TpuClientConfig}, - std::{ - collections::HashMap, - fs::File, - io::prelude::*, - net::IpAddr, - path::Path, - process::exit, - sync::{Arc, RwLock}, - }, -}; - -#[cfg(not(any(target_env = "msvc", target_os = "freebsd")))] -#[global_allocator] -static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc; - -/// Number of signatures for all transactions in ~1 week at ~100K TPS -pub const NUM_SIGNATURES_FOR_TXS: u64 = 100_000 * 60 * 60 * 24 * 7; - -/// Request information about node's stake -/// If fail to get requested information, return error -/// Otherwise return stake of the node -/// along with total activated stake of the network -fn find_node_activated_stake( - rpc_client: Arc, - node_id: Pubkey, -) -> Result<(u64, u64), ()> { - let vote_accounts = rpc_client.get_vote_accounts(); - if let Err(error) = vote_accounts { - error!("Failed to get vote accounts, error: {error}"); - return Err(()); - } - - let vote_accounts = vote_accounts.unwrap(); - - let total_active_stake: u64 = vote_accounts - .current - .iter() - .map(|vote_account| vote_account.activated_stake) - .sum(); - - let node_id_as_str = node_id.to_string(); - let find_result = vote_accounts - .current - .iter() - .find(|&vote_account| vote_account.node_pubkey == node_id_as_str); - match find_result { - Some(value) => Ok((value.activated_stake, total_active_stake)), - None => { - error!("Failed to find stake for requested node"); - Err(()) - } - } -} - -fn create_connection_cache( - json_rpc_url: &str, - tpu_connection_pool_size: usize, - bind_address: IpAddr, - client_node_id: Option<&Keypair>, - commitment_config: CommitmentConfig, -) -> ConnectionCache { - if client_node_id.is_none() { - return ConnectionCache::new_quic( - "bench-tps-connection_cache_quic", - tpu_connection_pool_size, - ); - } - - let rpc_client = Arc::new(RpcClient::new_with_commitment( - json_rpc_url.to_string(), - commitment_config, - )); - - let client_node_id = client_node_id.unwrap(); - let (stake, total_stake) = - find_node_activated_stake(rpc_client, client_node_id.pubkey()).unwrap_or_default(); - info!("Stake for specified client_node_id: {stake}, total stake: {total_stake}"); - let stakes = HashMap::from([ - (client_node_id.pubkey(), stake), - (Pubkey::new_unique(), total_stake - stake), - ]); - let staked_nodes = Arc::new(RwLock::new(StakedNodes::new( - Arc::new(stakes), - HashMap::::default(), // overrides - ))); - ConnectionCache::new_with_client_options( - "bench-tps-connection_cache_quic", - tpu_connection_pool_size, - None, - Some((client_node_id, bind_address)), - Some((&staked_nodes, &client_node_id.pubkey())), - ) -} - -#[allow(clippy::too_many_arguments)] -fn create_client( - external_client_type: &ExternalClientType, - json_rpc_url: &str, - websocket_url: &str, - connection_cache: ConnectionCache, - commitment_config: CommitmentConfig, -) -> Arc { - match external_client_type { - ExternalClientType::RpcClient => Arc::new(RpcClient::new_with_commitment( - json_rpc_url.to_string(), - commitment_config, - )), - ExternalClientType::TpuClient => { - let rpc_client = Arc::new(RpcClient::new_with_commitment( - json_rpc_url.to_string(), - commitment_config, - )); - match connection_cache { - ConnectionCache::Udp(cache) => Arc::new( - TpuClient::new_with_connection_cache( - rpc_client, - websocket_url, - TpuClientConfig::default(), - cache, - ) - .unwrap_or_else(|err| { - eprintln!("Could not create TpuClient {err:?}"); - exit(1); - }), - ), - ConnectionCache::Quic(cache) => Arc::new( - TpuClient::new_with_connection_cache( - rpc_client, - websocket_url, - TpuClientConfig::default(), - cache, - ) - .unwrap_or_else(|err| { - eprintln!("Could not create TpuClient {err:?}"); - exit(1); - }), - ), - } - } - } -} - -fn main() { - agave_logger::setup_with_default_filter(); - solana_metrics::set_panic_hook("bench-tps", /*version:*/ None); - - let matches = cli::build_args(solana_version::version!()).get_matches(); - let cli_config = match cli::parse_args(&matches) { - Ok(config) => config, - Err(error) => { - eprintln!("{error}"); - exit(1); - } - }; - - let cli::Config { - json_rpc_url, - websocket_url, - id, - tx_count, - keypair_multiplier, - client_ids_and_stake_file, - write_to_client_file, - read_from_client_file, - target_lamports_per_signature, - num_lamports_per_account, - external_client_type, - tpu_connection_pool_size, - skip_tx_account_data_size, - compute_unit_price, - use_durable_nonce, - instruction_padding_config, - bind_address, - client_node_id, - commitment_config, - .. - } = &cli_config; - - let keypair_count = *tx_count * keypair_multiplier; - if *write_to_client_file { - info!("Generating {keypair_count} keypairs"); - let (keypairs, _) = generate_keypairs(id, keypair_count as u64); - let num_accounts = keypairs.len() as u64; - let max_fee = FeeRateGovernor::new(*target_lamports_per_signature, 0) - .max_lamports_per_signature - .saturating_add(max_lamports_for_prioritization(compute_unit_price)); - let num_lamports_per_account = - (NUM_SIGNATURES_FOR_TXS * max_fee).div_ceil(num_accounts) + num_lamports_per_account; - let mut accounts = HashMap::new(); - keypairs.iter().for_each(|keypair| { - accounts.insert( - serde_json::to_string(&keypair.to_bytes().to_vec()).unwrap(), - Base64Account { - balance: num_lamports_per_account, - executable: false, - owner: system_program::id().to_string(), - data: String::new(), - }, - ); - }); - - info!("Writing {client_ids_and_stake_file}"); - let serialized = serde_yaml::to_string(&accounts).unwrap(); - let path = Path::new(&client_ids_and_stake_file); - let mut file = File::create(path).unwrap(); - file.write_all(b"---\n").unwrap(); - file.write_all(&serialized.into_bytes()).unwrap(); - return; - } - - let connection_cache = create_connection_cache( - json_rpc_url, - *tpu_connection_pool_size, - *bind_address, - client_node_id.as_ref(), - *commitment_config, - ); - let client = create_client( - external_client_type, - json_rpc_url, - websocket_url, - connection_cache, - *commitment_config, - ); - if let Some(instruction_padding_config) = instruction_padding_config { - info!( - "Checking for existence of instruction padding program: {}", - instruction_padding_config.program_id - ); - client - .get_account(&instruction_padding_config.program_id) - .expect( - "Instruction padding program must be deployed to this cluster. Deploy the program \ - using `solana program deploy \ - ./bench-tps/tests/fixtures/spl_instruction_padding.so` and pass the resulting \ - program id with `--instruction-padding-program-id`", - ); - } - let keypairs = get_keypairs( - client.clone(), - id, - keypair_count, - *num_lamports_per_account, - client_ids_and_stake_file, - *read_from_client_file, - *skip_tx_account_data_size, - instruction_padding_config.is_some(), - ); - - let nonce_keypairs = if *use_durable_nonce { - Some(generate_durable_nonce_accounts(client.clone(), &keypairs)) - } else { - None - }; - do_bench_tps(client, cli_config, keypairs, nonce_keypairs); -} diff --git a/bench-tps/src/perf_utils.rs b/bench-tps/src/perf_utils.rs deleted file mode 100644 index 0f33ff84963..00000000000 --- a/bench-tps/src/perf_utils.rs +++ /dev/null @@ -1,86 +0,0 @@ -use { - log::*, - solana_commitment_config::CommitmentConfig, - solana_tps_client::TpsClient, - std::{ - sync::{ - Arc, RwLock, - atomic::{AtomicBool, Ordering}, - }, - thread::sleep, - time::{Duration, Instant}, - }, -}; - -#[derive(Default)] -pub struct SampleStats { - /// Maximum TPS reported by this node - pub tps: f32, - /// Total transactions reported by this node - pub txs: u64, -} - -pub fn sample_txs( - exit_signal: Arc, - sample_stats: &Arc>>, - sample_period: u64, - client: &Arc, -) where - T: TpsClient + ?Sized, -{ - let mut max_tps = 0.0; - let mut total_elapsed; - let mut total_txs; - let mut now = Instant::now(); - let start_time = now; - let initial_txs = client - .get_transaction_count_with_commitment(CommitmentConfig::processed()) - .expect("transaction count"); - let mut last_txs = initial_txs; - - loop { - total_elapsed = start_time.elapsed(); - let elapsed = now.elapsed(); - now = Instant::now(); - let mut txs = - match client.get_transaction_count_with_commitment(CommitmentConfig::processed()) { - Err(e) => { - info!("Couldn't get transaction count {e:?}"); - sleep(Duration::from_secs(sample_period)); - continue; - } - Ok(tx_count) => tx_count, - }; - - if txs < last_txs { - info!("Expected txs({txs}) >= last_txs({last_txs})"); - txs = last_txs; - } - total_txs = txs - initial_txs; - let sample_txs = txs - last_txs; - last_txs = txs; - - let tps = sample_txs as f32 / elapsed.as_secs_f32(); - if tps > max_tps { - max_tps = tps; - } - - info!( - "Sampler {:9.2} TPS, Transactions: {:6}, Total transactions: {} over {} s", - tps, - sample_txs, - total_txs, - total_elapsed.as_secs(), - ); - - if exit_signal.load(Ordering::Relaxed) { - let stats = SampleStats { - tps: max_tps, - txs: total_txs, - }; - sample_stats.write().unwrap().push((client.addr(), stats)); - return; - } - sleep(Duration::from_secs(sample_period)); - } -} diff --git a/bench-tps/src/rpc_with_retry_utils.rs b/bench-tps/src/rpc_with_retry_utils.rs deleted file mode 100644 index 89f2aaa35be..00000000000 --- a/bench-tps/src/rpc_with_retry_utils.rs +++ /dev/null @@ -1,60 +0,0 @@ -use { - log::*, - solana_clock::{DEFAULT_MS_PER_SLOT, Slot}, - solana_commitment_config::CommitmentConfig, - solana_tps_client::{TpsClient, TpsClientResult}, - std::{sync::Arc, thread::sleep, time::Duration}, -}; - -const NUM_RETRY: u64 = 5; -const RETRY_EVERY_MS: u64 = 4 * DEFAULT_MS_PER_SLOT; - -fn call_rpc_with_retry(f: Func, retry_warning: &str) -> TpsClientResult -where - Func: Fn() -> TpsClientResult, -{ - let mut iretry = 0; - loop { - match f() { - Ok(slot) => { - return Ok(slot); - } - Err(error) => { - if iretry == NUM_RETRY { - return Err(error); - } - warn!("{retry_warning}: {error}, retry."); - sleep(Duration::from_millis(RETRY_EVERY_MS)); - } - } - iretry += 1; - } -} - -pub(crate) fn get_slot_with_retry( - client: &Arc, - commitment: CommitmentConfig, -) -> TpsClientResult -where - Client: 'static + TpsClient + Send + Sync + ?Sized, -{ - call_rpc_with_retry( - || client.get_slot_with_commitment(commitment), - "Failed to get slot", - ) -} - -pub(crate) fn get_blocks_with_retry( - client: &Arc, - start_slot: Slot, - end_slot: Option, - commitment: CommitmentConfig, -) -> TpsClientResult> -where - Client: 'static + TpsClient + Send + Sync + ?Sized, -{ - call_rpc_with_retry( - || client.get_blocks_with_commitment(start_slot, end_slot, commitment), - "Failed to download blocks", - ) -} diff --git a/bench-tps/src/send_batch.rs b/bench-tps/src/send_batch.rs deleted file mode 100644 index 6ecef8dccc6..00000000000 --- a/bench-tps/src/send_batch.rs +++ /dev/null @@ -1,519 +0,0 @@ -use { - log::*, - rayon::prelude::*, - solana_commitment_config::CommitmentConfig, - solana_compute_budget_interface::ComputeBudgetInstruction, - solana_core::gen_keys::GenKeys, - solana_hash::Hash, - solana_keypair::Keypair, - solana_measure::measure::Measure, - solana_message::Message, - solana_nonce::state::State, - solana_pubkey::Pubkey, - solana_signer::{Signer, signers::Signers}, - solana_system_interface::instruction as system_instruction, - solana_tps_client::*, - solana_transaction::Transaction, - std::{ - collections::HashSet, - marker::Send, - sync::{ - Arc, Mutex, - atomic::{AtomicBool, AtomicUsize, Ordering}, - }, - thread::sleep, - time::{Duration, Instant}, - }, -}; - -pub fn get_latest_blockhash(client: &T) -> Hash { - loop { - match client.get_latest_blockhash() { - Ok(blockhash) => return blockhash, - Err(err) => { - info!("Couldn't get last blockhash: {err:?}"); - sleep(Duration::from_secs(1)); - } - }; - } -} - -pub fn generate_keypairs(seed_keypair: &Keypair, count: u64) -> (Vec, u64) { - let mut seed = [0u8; 32]; - seed.copy_from_slice(&seed_keypair.to_bytes()[..32]); - let mut rnd = GenKeys::new(seed); - - let mut total_keys = 0; - let mut extra = 0; // This variable tracks the number of keypairs needing extra transaction fees funded - let mut delta = 1; - while total_keys < count { - extra += delta; - delta *= MAX_SPENDS_PER_TX; - total_keys += delta; - } - (rnd.gen_n_keypairs(total_keys), extra) -} - -/// fund the dests keys by spending all of the source keys into MAX_SPENDS_PER_TX -/// on every iteration. This allows us to replay the transfers because the source is either empty, -/// or full -pub fn fund_keys( - client: Arc, - source: &Keypair, - dests: &[Keypair], - total: u64, - max_fee: u64, - lamports_per_account: u64, - data_size_limit: Option, -) { - let mut funded: Vec<&Keypair> = vec![source]; - let mut funded_funds = total; - let mut not_funded: Vec<&Keypair> = dests.iter().collect(); - while !not_funded.is_empty() { - // Build to fund list and prepare funding sources for next iteration - let mut new_funded: Vec<&Keypair> = vec![]; - let mut to_fund: Vec<(&Keypair, Vec<(Pubkey, u64)>)> = vec![]; - let to_lamports = (funded_funds - lamports_per_account - max_fee) / MAX_SPENDS_PER_TX; - for f in funded { - let start = not_funded.len() - MAX_SPENDS_PER_TX as usize; - let dests: Vec<_> = not_funded.drain(start..).collect(); - let spends: Vec<_> = dests.iter().map(|k| (k.pubkey(), to_lamports)).collect(); - to_fund.push((f, spends)); - new_funded.extend(dests); - } - - to_fund.chunks(FUND_CHUNK_LEN).for_each(|chunk| { - Vec::<(&Keypair, Transaction)>::with_capacity(chunk.len()).fund( - &client, - chunk, - to_lamports, - data_size_limit, - ); - }); - - info!("funded: {} left: {}", new_funded.len(), not_funded.len()); - funded = new_funded; - funded_funds = to_lamports; - } -} - -pub fn generate_durable_nonce_accounts( - client: Arc, - authority_keypairs: &[Keypair], -) -> Vec { - let nonce_rent = client - .get_minimum_balance_for_rent_exemption(State::size()) - .unwrap(); - - let seed_keypair = &authority_keypairs[0]; - let count = authority_keypairs.len(); - let (mut nonce_keypairs, _extra) = generate_keypairs(seed_keypair, count as u64); - nonce_keypairs.truncate(count); - - info!("Creating {count} nonce accounts..."); - let to_fund: Vec = authority_keypairs - .iter() - .zip(nonce_keypairs.iter()) - .map(|x| NonceCreateSigners(x.0, x.1)) - .collect(); - - to_fund.chunks(FUND_CHUNK_LEN).for_each(|chunk| { - NonceCreateContainer::with_capacity(chunk.len()) - .create_accounts(&client, chunk, nonce_rent); - }); - nonce_keypairs -} - -pub fn withdraw_durable_nonce_accounts( - client: Arc, - authority_keypairs: &[Keypair], - nonce_keypairs: &[Keypair], -) { - let to_withdraw: Vec = authority_keypairs - .iter() - .zip(nonce_keypairs.iter()) - .map(|x| NonceWithdrawSigners(x.0, x.1.pubkey())) - .collect(); - - to_withdraw.chunks(FUND_CHUNK_LEN).for_each(|chunk| { - NonceWithdrawContainer::with_capacity(chunk.len()).withdraw_accounts(&client, chunk); - }); -} - -const MAX_SPENDS_PER_TX: u64 = 4; - -// Size of the chunk of transactions -// try to transfer a "few" at a time with recent blockhash -// assume 4MB network buffers, and 512 byte packets -const FUND_CHUNK_LEN: usize = 4 * 1024 * 1024 / 512; - -fn verify_funding_transfer( - client: &Arc, - tx: &Transaction, - amount: u64, -) -> bool { - for a in &tx.message().account_keys[1..] { - match client.get_balance_with_commitment(a, CommitmentConfig::processed()) { - Ok(balance) => return balance >= amount, - Err(err) => error!("failed to get balance {err:?}"), - } - } - false -} - -/// Helper trait to encapsulate common logic for sending transactions batch -/// -trait SendBatchTransactions<'a, T: Sliceable + Send + Sync> { - fn make (T, Transaction) + Send + Sync>( - &mut self, - chunk: &[V], - create_transaction: F, - ); - fn send_transactions(&mut self, client: &Arc, to_lamports: u64, log_progress: F) - where - C: 'static + TpsClient + Send + Sync + ?Sized, - F: Fn(usize, usize); - fn sign(&mut self, blockhash: Hash); - fn send(&self, client: &Arc); - fn verify( - &mut self, - client: &Arc, - to_lamports: u64, - ); -} - -/// This trait allows reuse SendBatchTransactions to send -/// transactions which require more than one signature -trait Sliceable { - type Slice; - fn as_slice(&self) -> Self::Slice; - // Pubkey used as unique id to identify verified transactions - fn get_pubkey(&self) -> Pubkey; -} - -impl SendBatchTransactions<'_, T> for Vec<(T, Transaction)> -where - ::Slice: Signers, -{ - fn make (T, Transaction) + Send + Sync>( - &mut self, - chunk: &[V], - create_transaction: F, - ) { - let mut make_txs = Measure::start("make_txs"); - let txs: Vec<(T, Transaction)> = chunk.par_iter().map(create_transaction).collect(); - make_txs.stop(); - debug!("make {} unsigned txs: {}us", txs.len(), make_txs.as_us()); - self.extend(txs); - } - - fn send_transactions(&mut self, client: &Arc, to_lamports: u64, log_progress: F) - where - C: 'static + TpsClient + Send + Sync + ?Sized, - F: Fn(usize, usize), - { - let mut tries: usize = 0; - while !self.is_empty() { - log_progress(tries, self.len()); - let blockhash = get_latest_blockhash(client.as_ref()); - - // re-sign retained to_fund_txes with updated blockhash - self.sign(blockhash); - self.send(client); - - // Sleep a few slots to allow transactions to process - sleep(Duration::from_secs(1)); - - self.verify(client, to_lamports); - - // retry anything that seems to have dropped through cracks - // again since these txs are all or nothing, they're fine to - // retry - tries += 1; - } - info!("transactions sent in {tries} tries"); - } - - fn sign(&mut self, blockhash: Hash) { - let mut sign_txs = Measure::start("sign_txs"); - self.par_iter_mut().for_each(|(k, tx)| { - tx.sign(&k.as_slice(), blockhash); - }); - sign_txs.stop(); - debug!("sign {} txs: {}us", self.len(), sign_txs.as_us()); - } - - fn send(&self, client: &Arc) { - let mut send_txs = Measure::start("send_and_clone_txs"); - let batch: Vec<_> = self - .iter() - .map(|(_keypair, tx)| tx.clone().into()) - .collect(); - let result = client.send_batch(batch); - send_txs.stop(); - if result.is_err() { - debug!("Failed to send batch {result:?}"); - } else { - debug!("send {} {}", self.len(), send_txs); - } - } - - fn verify( - &mut self, - client: &Arc, - to_lamports: u64, - ) { - let starting_txs = self.len(); - let verified_txs = Arc::new(AtomicUsize::new(0)); - let too_many_failures = Arc::new(AtomicBool::new(false)); - let loops = if starting_txs < 1000 { 3 } else { 1 }; - // Only loop multiple times for small (quick) transaction batches - let time = Arc::new(Mutex::new(Instant::now())); - for _ in 0..loops { - let time = time.clone(); - let failed_verify = Arc::new(AtomicUsize::new(0)); - let client = client.clone(); - let verified_txs = &verified_txs; - let failed_verify = &failed_verify; - let too_many_failures = &too_many_failures; - let verified_set: HashSet = self - .par_iter() - .filter_map(move |(k, tx)| { - let pubkey = k.get_pubkey(); - if too_many_failures.load(Ordering::Relaxed) { - return None; - } - - let verified = if verify_funding_transfer(&client, tx, to_lamports) { - verified_txs.fetch_add(1, Ordering::Relaxed); - Some(pubkey) - } else { - failed_verify.fetch_add(1, Ordering::Relaxed); - None - }; - - let verified_txs = verified_txs.load(Ordering::Relaxed); - let failed_verify = failed_verify.load(Ordering::Relaxed); - let remaining_count = starting_txs.saturating_sub(verified_txs + failed_verify); - if failed_verify > 100 && failed_verify > verified_txs { - too_many_failures.store(true, Ordering::Relaxed); - warn!( - "Too many failed transfers... {remaining_count} remaining, \ - {verified_txs} verified, {failed_verify} failures" - ); - } - if remaining_count > 0 { - let mut time_l = time.lock().unwrap(); - if time_l.elapsed().as_secs() > 2 { - info!( - "Verifying transfers... {remaining_count} remaining, \ - {verified_txs} verified, {failed_verify} failures" - ); - *time_l = Instant::now(); - } - } - - verified - }) - .collect(); - - self.retain(|(k, _)| !verified_set.contains(&k.get_pubkey())); - if self.is_empty() { - break; - } - info!("Looping verification"); - - let verified_txs = verified_txs.load(Ordering::Relaxed); - let failed_verify = failed_verify.load(Ordering::Relaxed); - let remaining_count = starting_txs.saturating_sub(verified_txs + failed_verify); - info!( - "Verifying transfers... {remaining_count} remaining, {verified_txs} verified, \ - {failed_verify} failures" - ); - sleep(Duration::from_millis(100)); - } - } -} - -type FundingSigners<'a> = &'a Keypair; -type FundingChunk<'a> = [(FundingSigners<'a>, Vec<(Pubkey, u64)>)]; -type FundingContainer<'a> = Vec<(FundingSigners<'a>, Transaction)>; - -impl<'a> Sliceable for FundingSigners<'a> { - type Slice = [FundingSigners<'a>; 1]; - fn as_slice(&self) -> Self::Slice { - [self] - } - fn get_pubkey(&self) -> Pubkey { - self.pubkey() - } -} - -trait FundingTransactions<'a>: SendBatchTransactions<'a, FundingSigners<'a>> { - fn fund( - &mut self, - client: &Arc, - to_fund: &FundingChunk<'a>, - to_lamports: u64, - data_size_limit: Option, - ); -} - -impl<'a> FundingTransactions<'a> for FundingContainer<'a> { - fn fund( - &mut self, - client: &Arc, - to_fund: &FundingChunk<'a>, - to_lamports: u64, - data_size_limit: Option, - ) { - self.make(to_fund, |(k, t)| -> (FundingSigners<'a>, Transaction) { - let mut instructions = system_instruction::transfer_many(&k.pubkey(), t); - if let Some(data_size_limit) = data_size_limit { - instructions.push( - ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(data_size_limit), - ); - } - let message = Message::new(&instructions, Some(&k.pubkey())); - (*k, Transaction::new_unsigned(message)) - }); - - let log_progress = |tries: usize, batch_len: usize| { - info!( - "{} {} each to {} accounts in {} txs", - if tries == 0 { - "transferring" - } else { - " retrying" - }, - to_lamports, - batch_len * MAX_SPENDS_PER_TX as usize, - batch_len, - ); - }; - self.send_transactions(client, to_lamports, log_progress); - } -} - -// Introduce a new structure to specify Sliceable implementations -// which uses both Keypairs to sign the transaction -struct NonceCreateSigners<'a>(&'a Keypair, &'a Keypair); -type NonceCreateChunk<'a> = [NonceCreateSigners<'a>]; -type NonceCreateContainer<'a> = Vec<(NonceCreateSigners<'a>, Transaction)>; - -impl<'a> Sliceable for NonceCreateSigners<'a> { - type Slice = [&'a Keypair; 2]; - fn as_slice(&self) -> Self::Slice { - [self.0, self.1] - } - fn get_pubkey(&self) -> Pubkey { - self.0.pubkey() - } -} - -trait NonceTransactions<'a>: SendBatchTransactions<'a, NonceCreateSigners<'a>> { - fn create_accounts( - &mut self, - client: &Arc, - to_fund: &'a NonceCreateChunk<'a>, - nonce_rent: u64, - ); -} - -impl<'a> NonceTransactions<'a> for NonceCreateContainer<'a> { - fn create_accounts( - &mut self, - client: &Arc, - to_fund: &'a NonceCreateChunk<'a>, - nonce_rent: u64, - ) { - self.make(to_fund, |kp| -> (NonceCreateSigners<'a>, Transaction) { - let authority = kp.0; - let nonce: &Keypair = kp.1; - let instructions = system_instruction::create_nonce_account( - &authority.pubkey(), - &nonce.pubkey(), - &authority.pubkey(), - nonce_rent, - ); - ( - NonceCreateSigners(authority, nonce), - Transaction::new_with_payer(&instructions, Some(&authority.pubkey())), - ) - }); - - let log_progress = |tries: usize, batch_len: usize| { - info!( - "@ {} {} accounts", - if tries == 0 { "creating" } else { " retrying" }, - batch_len, - ); - }; - self.send_transactions(client, nonce_rent, log_progress); - } -} - -// Only Pubkey is required for nonce because it doesn't sign withdraw account transaction -struct NonceWithdrawSigners<'a>(&'a Keypair, Pubkey); -type NonceWithdrawChunk<'a> = [NonceWithdrawSigners<'a>]; -type NonceWithdrawContainer<'a> = Vec<(NonceWithdrawSigners<'a>, Transaction)>; - -impl<'a> Sliceable for NonceWithdrawSigners<'a> { - type Slice = [&'a Keypair; 1]; - fn as_slice(&self) -> Self::Slice { - [self.0] - } - fn get_pubkey(&self) -> Pubkey { - self.0.pubkey() - } -} - -trait NonceWithdrawTransactions<'a>: SendBatchTransactions<'a, NonceWithdrawSigners<'a>> { - fn withdraw_accounts( - &mut self, - client: &Arc, - to_withdraw: &'a NonceWithdrawChunk<'a>, - ); -} -impl<'a> NonceWithdrawTransactions<'a> for NonceWithdrawContainer<'a> { - fn withdraw_accounts( - &mut self, - client: &Arc, - to_withdraw: &'a NonceWithdrawChunk<'a>, - ) { - self.make( - to_withdraw, - |kp| -> (NonceWithdrawSigners<'a>, Transaction) { - let authority = kp.0; - let nonce_pubkey: Pubkey = kp.1; - let nonce_balance = client.get_balance(&nonce_pubkey).unwrap(); - let instructions = vec![ - system_instruction::withdraw_nonce_account( - &nonce_pubkey, - &authority.pubkey(), - &authority.pubkey(), - nonce_balance, - ); - 1 - ]; - ( - NonceWithdrawSigners(authority, nonce_pubkey), - Transaction::new_with_payer(&instructions, Some(&authority.pubkey())), - ) - }, - ); - - let log_progress = |tries: usize, batch_len: usize| { - info!( - "@ {} {} accounts", - if tries == 0 { - "withdrawing" - } else { - " retrying" - }, - batch_len, - ); - }; - self.send_transactions(client, 0, log_progress); - } -} diff --git a/bench-tps/tests/bench_tps.rs b/bench-tps/tests/bench_tps.rs deleted file mode 100644 index d5dfb8ebc47..00000000000 --- a/bench-tps/tests/bench_tps.rs +++ /dev/null @@ -1,169 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] - -use { - serial_test::serial, - solana_bench_tps::{ - bench::{do_bench_tps, generate_and_fund_keypairs}, - cli::Config, - send_batch::generate_durable_nonce_accounts, - }, - solana_commitment_config::CommitmentConfig, - solana_connection_cache::connection_cache::NewConnectionConfig, - solana_core::validator::ValidatorConfig, - solana_faucet::faucet::run_local_faucet_for_tests, - solana_fee_calculator::FeeRateGovernor, - solana_keypair::Keypair, - solana_local_cluster::{ - cluster::Cluster, - local_cluster::{ClusterConfig, LocalCluster}, - validator_configs::make_identical_validator_configs, - }, - solana_net_utils::SocketAddrSpace, - solana_quic_client::{QuicConfig, QuicConnectionManager, QuicPool}, - solana_rent::Rent, - solana_rpc::rpc::JsonRpcConfig, - solana_rpc_client::rpc_client::RpcClient, - solana_signer::Signer, - solana_test_validator::{TestValidator, TestValidatorGenesis}, - solana_tpu_client::tpu_client::{TpuClient, TpuClientConfig}, - std::{sync::Arc, time::Duration}, -}; - -fn create_local_cluster_and_client() -> ( - LocalCluster, - Arc>, -) { - let faucet_keypair = Keypair::new(); - let faucet_pubkey = faucet_keypair.pubkey(); - let faucet_addr = run_local_faucet_for_tests( - faucet_keypair, - None, /* per_time_cap */ - 0, /* port */ - ); - - const NUM_NODES: usize = 1; - let cluster = LocalCluster::new( - &mut ClusterConfig { - node_stakes: vec![999_990; NUM_NODES], - mint_lamports: 400_000_000_000, - validator_configs: make_identical_validator_configs( - &ValidatorConfig { - rpc_config: JsonRpcConfig { - faucet_addr: Some(faucet_addr), - ..JsonRpcConfig::default_for_test() - }, - ..ValidatorConfig::default_for_test() - }, - NUM_NODES, - ), - ..ClusterConfig::default() - }, - SocketAddrSpace::Unspecified, - ); - - cluster.transfer(&cluster.funding_keypair, &faucet_pubkey, 100_000_000_000); - - let client = Arc::new( - cluster - .build_validator_tpu_quic_client(cluster.entry_point_info.pubkey()) - .unwrap_or_else(|err| { - panic!("Could not create TpuClient with Quic Cache {err:?}"); - }), - ); - - (cluster, client) -} - -fn create_test_validator_and_client() -> ( - TestValidator, - Arc>, -) { - let mint_keypair = Keypair::new(); - let mint_pubkey = mint_keypair.pubkey(); - let faucet_addr = run_local_faucet_for_tests( - mint_keypair, - None, /* per_time_cap */ - 0, /* port */ - ); - - let test_validator = TestValidatorGenesis::default_for_tests() - .fee_rate_governor(FeeRateGovernor::new(0, 0)) - .rent(Rent { - lamports_per_byte: 1, - ..Rent::default() - }) - .faucet_addr(Some(faucet_addr)) - .add_program( - "spl_instruction_padding", - spl_instruction_padding_interface::ID, - ) - .start_with_mint_address(mint_pubkey, SocketAddrSpace::Unspecified) - .expect("validator start failed"); - - let rpc_client = Arc::new(RpcClient::new_with_commitment( - test_validator.rpc_url(), - CommitmentConfig::processed(), - )); - let websocket_url = test_validator.rpc_pubsub_url(); - - ( - test_validator, - Arc::new( - TpuClient::new( - "tpu_client_quic_bench_tps", - rpc_client, - &websocket_url, - TpuClientConfig::default(), - QuicConnectionManager::new_with_connection_config(QuicConfig::new().unwrap()), - ) - .expect("Should build Quic Tpu Client."), - ), - ) -} - -fn run_bench_tps(client: Arc>) { - let config = Config { - tx_count: 100, - duration: Duration::from_secs(5), - ..Config::default() - }; - let keypair_count = config.tx_count * config.keypair_multiplier; - let lamports_per_account = 100_000; - let keypairs = generate_and_fund_keypairs( - client.clone(), - &config.id, - keypair_count, - lamports_per_account, - false, - false, - ) - .unwrap(); - let nonce_keypairs = if config.use_durable_nonce { - Some(generate_durable_nonce_accounts(client.clone(), &keypairs)) - } else { - None - }; - - let tps = do_bench_tps(client, config, keypairs, nonce_keypairs); - assert!(tps > 100, "TPS less than expected {tps}"); -} - -/// Verifies the bench-tps QUIC client, faucet plumbing, and funding flow work against a -/// real `LocalCluster`, and does not cover the lighter `TestValidator` path. -#[test] -#[serial] -fn test_bench_tps_local_cluster() { - agave_logger::setup(); - let (local_cluster, client) = create_local_cluster_and_client(); - run_bench_tps(client); - drop(local_cluster); -} - -#[test] -#[serial] -fn test_bench_tps_test_validator() { - agave_logger::setup(); - let (test_validator, client) = create_test_validator_and_client(); - run_bench_tps(client); - drop(test_validator) -} diff --git a/bench-tps/tests/fixtures/spl_instruction_padding.so b/bench-tps/tests/fixtures/spl_instruction_padding.so deleted file mode 100755 index a00aabeb93bc9159a120a0d69fb9c55d807923f2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16800 zcmb_kYiwM{b)F@8MT$+blydJ5eyU+Dbbdj5Oqxbl~N=bLPyM^O`e{d*!Dd_?-vq5(#Ie-u={}w*5C8!`<3&MIY*^ zzT4$pJ^gKK;nL&9p<$*NJ6)bEogJDS8$YcyU8Cct zhPz5(O zV?)C>^#3E@QgQ0&lfy$@r^Zhhcjta>{zKy>wFw!hYqC@vn=E&Y(Q|Qfg#q84o8ED+ zQ)4GmznMBYd|G=xP%4d=cBW1ZpBgXu6dSn+x1}C0j-4Dn@sSicA~pWF5;!+hJX0JR zoAjyiC&}L@$ETFf*y$6)=ZyIH_}SAZ$~#k&qr(^~HC9fgjuRi$g^)1PF_X!3XL>Td znQSJP>C5zI@|l6|Om}y8Pj_#3wma9|*WKTp?;hyM^mO<1^z`;*dvZN}J^elTo`K#> zZ+CA`Z*On5H`m+O+uxh-9mr;~-PxXOZ#J9FW&5)I*?e{&m&tYKdUCzFY%Z7U%k}5- zxq-e+Uw2y`gqEtnj`%=ho44tJXIu{1Cwp+yb8_Ts!?*baEf3 zXOtf_I=7af&1*_GT>hEpz`GXzeZp_P4SvHd@ZTeP;1AIc<)8rfHNt<8o@?=6Bs}DN ziy#VQL&*!^K6v~s`rSvr6xjgkCjo}A&3~lQSp03(=d@j)%p8d z7>4z*DA_{g!83BN*8y9RH)rTa1^7^NcpNgy#Rmkoe_G(8@>!%R9(NoN1nMY@s&GNG zaD1=eMRwncx6Z%@YqZb4pD}093Y@ryidce^`dNgfgLbUhM zn6-Q=`eV=;;kiQpy95{a2wbH1!fDfp|}FH*bB z$8e4C?|N1^_*v1@uTwui(Zb_KZH}SI9kn%aJ)V_*(7zS>U(-5_Om&MV$ThyayzFON zxnGzFME}_o*ZrJ~8|i$H*30-7Uw&@8(7i77ze>MJ=&$n+`30fruZsL(y*OgxQIRuT zBXShhsQn4uM!&$JC)o*|sRZuJ45NM5S&pdlvcR@03>%3pI5d(kNX_MHyEMTr2e>=? z|9eE^UlaR!iGVHt)#zrLh}4VTIhhc0y(1fQj6MV#4fL#V>sP>f#HtJqfB7)H>r8p@>~2F z=?^`RP)H`9SAMTZe?KR7zj6FIZitK0j%k{qM}JlPVSZ1lJU%USetj!HM4e|?9>1Oj zq7rqEYF*>K^O)4vakGos4sktP^B5x_Uzx^y*M2xId^^Z~_t0aUm3sRYff98TcN$MS zvR&MOI*OkaJ;MOI0Fs0JMavi$_~FKGprQOq6U$6zezEx#HT;zE(WM^>JpX-xliy)jKEZ69?MKR4;RAVK zAIOmbB>BngZsGWUa6j|6zb1MFP7-yY;dO40)$g_OyDNR2+bxg9Iv@eRW|*Q2ktE%=gq|X z98`Ie>;X#*Cpz6mf`!Cy^SP?JAMkK4bYQ7jcaDT0In;eb?1=9a+ zg4J};FK}N|x)gi5kl4xl)M`4|F>rsbbRXilC?xW79d`_d{r^a-5g-QsL^iK;G^<<=FNUR^~G`v>moDm?5Q zxGSshn8|_rgH?De(Sdtq6@D$_zpx6At$yGxt-`}TfqP*U9x4Rx_gCQ&7lHe|Re0Gq zURZ_43=P~ftMG`Mz{otH;OW$7Qu%~*odei z`=~#T_HmW>fj0Hfv}*&h82l%OU&N%LVtfjXvLCYj&Yk!zD8E`gBb9$)3#28#Z(-nU zeY5k1#R2m5k7y#r$Ar%8=rulkk4#vf6>Y+XHEf4MgTzUpPVNItZreZEelj49Q3`cp zFr)M8XRqfuJFh%Lv^O3*n=*N=y|@M0gx2-V5=cJHRp;*!xollS|F67sY!rI^5yhc(9fe=h(-@?RBwV4n#xx!oI7<4+Ub{Db=d z?l&Rv)Q??4U*VraOkaCn<*)B&{2=)Q@li-f+^_Nv zUmV}`gT)8-$xVTrH_y92S`XUKk#A9$Mw$U|n|P1GJbS!e8=XzPvf zi<%`a?EEs_Ea#Mh-mlSqgU7SDzWY@Xnp01+$axYtAvEQi21659;HT?13hCqB!-B_9wt@rb}M(d{rv ztU^gLnnX?x!t&{Wd0ZQH#K^zRYyKLlAb( zxf7pA^Ius1NzuPq>^PAYzeH`~r-`)q#m?;$Y4J(_uyTd|J z8wpIM8h?oKjHY}mez$!L)_q`bAG8k z^tYL33j;6ddQB6Mish$Te--M!!E`n+YW)`0{jKs5n0$!Y-Mj~w+q(pezj~cvWcx9@ zr{668vGqFIEd8Uq$XHZxPXUUkoI=$lbv{L9i3eNPV4pGaSM@#I8#R8pfOg#vP0RLr zh5z|I1GyRFfge0xoRxf5$#a?)hBio`*!a<5nTOH7R?%~3N?_$4%xPWx9(8W#x>tLI z_41*}&Gmc$>QekEovz~@q|-fwxBHi*?u%(22)}DGeg}z{LI0~F2i_;NP~G;6epcu| zxbY)|C4OfZI4@z&cIR>S3O*KbsJ|E$c^mwZx4(&XvPsTW{$;t}@fz1h!k=?|cs&RC zI#`$P=GV*l#TOwJ$yq1x$a;By=Zl)(>I5EH|6juA^Ljt^sL)R&(%e7P_gmNx;)GFH zBl*@Znb-PappIy2>B98z=VahN% zEb$&S?_UCkz{!2Dy$`$9y&m#TWnZTL^6E!FFL8x<#L>7o`yS?7oNr?2=Q0e34q!C8 zr(C}>E}@?l=;7x@KJ(-1c9HvND7_w1ds=>-B7P}q3TU!`B07AA$B*`%6S#ARq3s{- zJmX6(AYU=x4w2q=p0)QUmS?cPLi|U_!3 zfK;M`0)0;8j1CI)z1-}gtU#=Lu(O?q3w8g=ewDYGWN&JN9_A2?O2X+H~sw*56^!2Hu=a; zsNFO^eOc`^DRh2bxIqV5lUDoB6(QX(;Ovq z+&dZ__CA<7$baV$09tH6UDS2Z@(Avy+qNB>v@H;`^!Nh`vv8_LPI^@FHEa0#}iGg zU!Y$|6}JdKtdrQEE!>Zcj{76wKeR_C;7~USN+sGOabo+)C@pj*2mG;JSPH#<_b}Zo zaWdW9v;%xAM#*|=&R4)_Yyu9 zH_*N5RAQapLFv*p~RV;!!6D#=grsM68C>j=KFLb4NPU4HZbf58V3;a%&0tU zFS482Ya`)9etSgrpVfKXk^&nST~xlj3n0GKi}_L;psoMq4SF8eeO-Bj?o;&Mrn~{8 zf?o5+2H_j&`#HPU^ZK4Ivi}cRDY?ANtw) zZuc_4|47HBW*6H!nnsQ4Jpw_uhZKrBB_Ej_PtZJs{uQt5BF{+@3h`OO*d~}A{S6T$uq2`PZ52! z-rt2kNFNk`UFJntCld{R#MhujjW^pT4ZP zA)~kZo|SxOC{L)qonk+{zeYj4qL`iHFGzi?PV#Z|S;;R89pc#dF=?-SU!wWL)-}5) zv~!WaCV9ip%lPo~9})}fcQACpy}}Ird6B2W=S3aos*L|RjR9Mi11B4eXs7VcHMEfR zh_A{MHV8jEhs7TgegV%yI_LIF9s4uLVR=2QTgMF(qP`oUar1=vxyXb-#r9Me@4OsXwv5g^Hn*b9CSyV0yC)?#-!-v-%+Q`z7(8 zB8YxwuR)6Q<>hwVld=64MQ!JCu&3h1O)lCg{*Exbrph)y~fa{W9Lx-FA%= zv&SGA6Z)CmYW2497t+v5DyQW)}m&N}DM155EB*C&;A zwvrdkp9o4Si}Zt@1zmSST%=H`g^pYeJvgBYbQ?AS{ABS8h+00aK&>So?cK7tnVyS_(?rZwMo z2p)a}KYQQPu6B$cly-bqw9IdW1Ri}>@KdB0?0fXQ)RQNap5vayA-xl>OYxwG>&;&A zZlNzU?&XJA(z&xZ3+u-O-z{|W^xe!?=@I)CK--_iAyuf1s9kWcg)(3Non<^$`~5A_ z)z*V#n#o)x|83LysK6;wj#P24(I0a6@)SYp?jC+t+K;|0F!`WBzXjGH{-Tbn^d}*> zKSAz)Ug(cr5@>ew+gcc5@9!)9|Ig!VK7NnTP2eKYpr~&KL#r5 zPjQBVc}OC=*XYOoCCL8OJaJUq>5?gGpaS_3A$I>^?@tT$avzWJs^^)t+kR?_`l5d= zUzU?C2m8@{({Vwcpe^hV#ezf`=(~5J>`lF!#5xRFYIVyQM#m#g+(Y*A4)aUEAy0?jk*MAQJd8j+x zuPdGQv-^vF>ekXh{(+IGbP(jamwNm20%M)GSf8=q=qGrBnrz;oAL>RwqH#J%5c|6k zqK96{i)S=;z&Uvte+f@?EAV52w|!mmQK?T{0vGx>3J*dGS`RcIlpn+>R6lR+0w>M` z3wnWA&T&t$wedU4PrZDZ{?*Gy_K%&9R_Qk+{W{2wtRI4d$_V{1!XW)HGQRIvAwSho z{A;?7>by=K6#PU+EB_)QB-W*#QaX7 zPVt7)W&1+i2PRd1KeU*Z`{eKJW|4(3zwf!j! UbVW4^fit@Ws(UMq;9Xt*7nRdsFaQ7m diff --git a/ci/coverage/part-3.sh b/ci/coverage/part-3.sh index a43c1708351..c3410806e7f 100755 --- a/ci/coverage/part-3.sh +++ b/ci/coverage/part-3.sh @@ -31,7 +31,7 @@ echo "--- coverage: dev-bins" --features dev-context-only-utils \ --manifest-path "$git_root"/dev-bins/Cargo.toml \ --workspace \ - --lib + --bins # Clean up cargo clean diff --git a/dev-bins/.config/nextest.toml b/dev-bins/.config/nextest.toml index 0e6a1ad1a65..bb5ee619063 100644 --- a/dev-bins/.config/nextest.toml +++ b/dev-bins/.config/nextest.toml @@ -11,7 +11,3 @@ retries = { backoff = "fixed", count = 3, delay = "1s" } [profile.ci.junit] path = "../junit.xml" - -[[profile.ci.overrides]] -filter = "package(solana-bench-tps)" -threads-required = "num-cpus" diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 34bd0e5c8f5..19e401d33e6 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -666,7 +666,7 @@ dependencies = [ "ark-poly 0.5.0", "ark-serialize 0.5.0", "ark-std 0.5.0", - "educe 0.6.0", + "educe", "fnv", "hashbrown 0.15.5", "itertools 0.13.0", @@ -708,7 +708,7 @@ dependencies = [ "ark-std 0.5.0", "arrayvec", "digest 0.10.7", - "educe 0.6.0", + "educe", "itertools 0.13.0", "num-bigint 0.4.6", "num-traits", @@ -785,7 +785,7 @@ dependencies = [ "ark-ff 0.5.0", "ark-serialize 0.5.0", "ark-std 0.5.0", - "educe 0.6.0", + "educe", "fnv", "hashbrown 0.15.5", ] @@ -2263,25 +2263,13 @@ dependencies = [ "sha2 0.10.9", ] -[[package]] -name = "educe" -version = "0.4.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f0042ff8246a363dbe77d2ceedb073339e85a804b9a47636c6e016a9a32c05f" -dependencies = [ - "enum-ordinalize 3.1.15", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "educe" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" dependencies = [ - "enum-ordinalize 4.3.2", + "enum-ordinalize", "proc-macro2", "quote", "syn 2.0.117", @@ -2350,19 +2338,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "enum-ordinalize" -version = "3.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf1fa3f06bbff1ea5b1a9c7b14aa992a39657db60a2759457328d7e058f49ee" -dependencies = [ - "num-bigint 0.4.6", - "num-traits", - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "enum-ordinalize" version = "4.3.2" @@ -4368,25 +4343,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "opentelemetry" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6105e89802af13fdf48c49d7646d3b533a70e536d818aae7e78ba0433d01acb8" -dependencies = [ - "async-trait", - "crossbeam-channel", - "futures-channel", - "futures-executor", - "futures-util", - "js-sys", - "lazy_static", - "percent-encoding 2.3.2", - "pin-project", - "rand 0.8.6", - "thiserror 1.0.69", -] - [[package]] name = "pairing" version = "0.23.0" @@ -5650,31 +5606,6 @@ dependencies = [ "unsafe-libyaml", ] -[[package]] -name = "serial_test" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "699f4197115b8a7e7ff19c9a315a4bd6fffec26cc4626ef45ecaea389e081c6d" -dependencies = [ - "futures-executor", - "futures-util", - "log", - "once_cell", - "parking_lot 0.12.5", - "serial_test_derive", -] - -[[package]] -name = "serial_test_derive" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94e153fc76e1c6a068703d6d29c508a0b15c061c4b7e43da59cc097bc342673c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "sha1" version = "0.10.6" @@ -5736,15 +5667,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - [[package]] name = "shell-words" version = "1.1.1" @@ -6102,142 +6024,6 @@ dependencies = [ "parking_lot 0.12.5", ] -[[package]] -name = "solana-banks-client" -version = "4.2.0-alpha.0" -dependencies = [ - "borsh", - "futures 0.3.32", - "solana-account 4.3.0", - "solana-banks-interface", - "solana-clock", - "solana-commitment-config", - "solana-hash 4.4.0", - "solana-message", - "solana-program-pack", - "solana-pubkey 4.2.0", - "solana-rent", - "solana-signature", - "solana-sysvar", - "solana-transaction", - "solana-transaction-context", - "solana-transaction-error", - "tarpc", - "thiserror 2.0.18", - "tokio", - "tokio-serde", -] - -[[package]] -name = "solana-banks-interface" -version = "4.2.0-alpha.0" -dependencies = [ - "serde", - "solana-account 4.3.0", - "solana-clock", - "solana-commitment-config", - "solana-hash 4.4.0", - "solana-message", - "solana-pubkey 4.2.0", - "solana-signature", - "solana-transaction", - "solana-transaction-context", - "solana-transaction-error", - "tarpc", -] - -[[package]] -name = "solana-banks-server" -version = "4.2.0-alpha.0" -dependencies = [ - "agave-feature-set", - "bincode", - "crossbeam-channel", - "futures 0.3.32", - "solana-account 4.3.0", - "solana-banks-interface", - "solana-clock", - "solana-commitment-config", - "solana-hash 4.4.0", - "solana-message", - "solana-net-utils", - "solana-pubkey 4.2.0", - "solana-runtime", - "solana-runtime-transaction", - "solana-send-transaction-service", - "solana-signature", - "solana-transaction", - "solana-transaction-error", - "tarpc", - "tokio", - "tokio-serde", - "tokio-util 0.7.18", -] - -[[package]] -name = "solana-bench-tps" -version = "4.2.0-alpha.0" -dependencies = [ - "agave-feature-set", - "agave-logger", - "chrono", - "clap", - "crossbeam-channel", - "csv", - "log", - "rand 0.9.4", - "rayon", - "serde", - "serde_json", - "serde_yaml", - "serial_test", - "solana-account 4.3.0", - "solana-clap-utils", - "solana-cli-config", - "solana-client", - "solana-clock", - "solana-commitment-config", - "solana-compute-budget-interface", - "solana-connection-cache", - "solana-core", - "solana-faucet", - "solana-fee-calculator", - "solana-genesis", - "solana-genesis-config", - "solana-hash 4.4.0", - "solana-instruction", - "solana-keypair", - "solana-local-cluster", - "solana-measure", - "solana-message", - "solana-metrics", - "solana-native-token", - "solana-net-utils", - "solana-nonce", - "solana-pubkey 4.2.0", - "solana-quic-client", - "solana-rent", - "solana-rpc", - "solana-rpc-client", - "solana-rpc-client-api", - "solana-runtime", - "solana-sdk-ids", - "solana-signature", - "solana-signer", - "solana-streamer", - "solana-system-interface 3.2.0", - "solana-test-validator", - "solana-time-utils", - "solana-tps-client", - "solana-tpu-client", - "solana-transaction", - "solana-transaction-status", - "solana-version", - "spl-instruction-padding-interface", - "tempfile", - "tikv-jemallocator", -] - [[package]] name = "solana-bincode" version = "3.1.0" @@ -7079,53 +6865,6 @@ dependencies = [ "reqwest 0.13.2", ] -[[package]] -name = "solana-genesis" -version = "4.2.0-alpha.0" -dependencies = [ - "agave-feature-set", - "agave-logger", - "agave-snapshots", - "base64 0.22.1", - "bincode", - "clap", - "itertools 0.14.0", - "serde", - "serde_json", - "serde_yaml", - "solana-account 4.3.0", - "solana-bls-signatures", - "solana-clap-utils", - "solana-cli-config", - "solana-clock", - "solana-cluster-type", - "solana-commitment-config", - "solana-entry", - "solana-epoch-schedule", - "solana-feature-gate-interface", - "solana-fee-calculator", - "solana-genesis-config", - "solana-genesis-utils", - "solana-inflation", - "solana-keypair", - "solana-ledger", - "solana-loader-v3-interface", - "solana-native-token", - "solana-poh-config", - "solana-pubkey 4.2.0", - "solana-rent", - "solana-rpc-client", - "solana-rpc-client-api", - "solana-runtime", - "solana-sdk-ids", - "solana-signer", - "solana-stake-interface", - "solana-time-utils", - "solana-version", - "solana-vote-program", - "tempfile", -] - [[package]] name = "solana-genesis-config" version = "4.0.0" @@ -7315,7 +7054,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37ebb0ffd19263051bc3f683fcc086134b8ff23af894dcb63f7563c7137b42f1" dependencies = [ "bincode", - "borsh", "serde", "serde_derive", "solana-define-syscall 5.1.0", @@ -7556,82 +7294,11 @@ dependencies = [ ] [[package]] -name = "solana-local-cluster" -version = "4.2.0-alpha.0" -dependencies = [ - "agave-feature-set", - "agave-logger", - "agave-snapshots", - "agave-votor", - "agave-votor-messages", - "crossbeam-channel", - "itertools 0.14.0", - "log", - "rand 0.9.4", - "rayon", - "solana-account 4.3.0", - "solana-accounts-db", - "solana-client", - "solana-client-traits", - "solana-clock", - "solana-cluster-type", - "solana-commitment-config", - "solana-core", - "solana-entry", - "solana-epoch-schedule", - "solana-feature-gate-interface", - "solana-fee-structure", - "solana-genesis-config", - "solana-gossip", - "solana-hard-forks", - "solana-hash 4.4.0", - "solana-keypair", - "solana-leader-schedule", - "solana-ledger", - "solana-message", - "solana-native-token", - "solana-net-utils", - "solana-poh-config", - "solana-program-binaries", - "solana-pubkey 4.2.0", - "solana-pubsub-client", - "solana-quic-client", - "solana-rent", - "solana-rpc-client", - "solana-rpc-client-api", - "solana-runtime", - "solana-sdk-ids", - "solana-shred-version", - "solana-signer", - "solana-slot-hashes", - "solana-stake-interface", - "solana-streamer", - "solana-system-interface 3.2.0", - "solana-system-transaction", - "solana-time-utils", - "solana-tpu-client", - "solana-tpu-client-next", - "solana-transaction", - "solana-transaction-error", - "solana-turbine", - "solana-validator-exit", - "solana-vote", - "solana-vote-interface", - "solana-vote-program", - "static_assertions", - "strum", - "tempfile", - "tokio", - "tokio-util 0.7.18", - "wincode", -] - -[[package]] -name = "solana-measure" -version = "4.2.0-alpha.0" - -[[package]] -name = "solana-merkle-tree" +name = "solana-measure" +version = "4.2.0-alpha.0" + +[[package]] +name = "solana-merkle-tree" version = "4.2.0-alpha.0" dependencies = [ "fast-math", @@ -8001,68 +7668,6 @@ dependencies = [ "wincode", ] -[[package]] -name = "solana-program-test" -version = "4.2.0-alpha.0" -dependencies = [ - "agave-feature-set", - "agave-logger", - "assert_matches", - "async-trait", - "base64 0.22.1", - "bincode", - "chrono-humanize", - "log", - "serde", - "solana-account 4.3.0", - "solana-account-info", - "solana-accounts-db", - "solana-address 2.6.1", - "solana-banks-client", - "solana-banks-interface", - "solana-banks-server", - "solana-clock", - "solana-cluster-type", - "solana-commitment-config", - "solana-compute-budget", - "solana-epoch-rewards", - "solana-epoch-schedule", - "solana-fee-calculator", - "solana-genesis-config", - "solana-hash 4.4.0", - "solana-instruction", - "solana-keypair", - "solana-loader-v3-interface", - "solana-message", - "solana-msg", - "solana-native-token", - "solana-poh-config", - "solana-program-binaries", - "solana-program-entrypoint", - "solana-program-error", - "solana-program-runtime", - "solana-pubkey 4.2.0", - "solana-rent", - "solana-runtime", - "solana-sbpf", - "solana-sdk-ids", - "solana-signer", - "solana-stake-interface", - "solana-svm-log-collector", - "solana-svm-timings", - "solana-system-interface 3.2.0", - "solana-sysvar", - "solana-sysvar-id", - "solana-transaction", - "solana-transaction-context", - "solana-transaction-error", - "solana-vote-program", - "spl-generic-token", - "spl-memo-interface", - "thiserror 2.0.18", - "tokio", -] - [[package]] name = "solana-pubkey" version = "3.0.0" @@ -8790,7 +8395,6 @@ version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f34562cda7df74d85fcb1f7063d8cdec3e14f3d402aa3062605c116d8b8115" dependencies = [ - "borsh", "num-traits", "serde", "serde_derive", @@ -9152,59 +8756,6 @@ dependencies = [ "solana-sdk-ids", ] -[[package]] -name = "solana-test-validator" -version = "4.2.0-alpha.0" -dependencies = [ - "agave-feature-set", - "agave-snapshots", - "arc-swap", - "base64 0.22.1", - "bincode", - "crossbeam-channel", - "log", - "serde_json", - "solana-account 4.3.0", - "solana-accounts-db", - "solana-cli-output", - "solana-clock", - "solana-cluster-type", - "solana-commitment-config", - "solana-compute-budget", - "solana-core", - "solana-epoch-schedule", - "solana-feature-gate-interface", - "solana-fee-calculator", - "solana-genesis-utils", - "solana-geyser-plugin-manager", - "solana-gossip", - "solana-inflation", - "solana-instruction", - "solana-keypair", - "solana-ledger", - "solana-loader-v3-interface", - "solana-message", - "solana-native-token", - "solana-net-utils", - "solana-program-binaries", - "solana-program-runtime", - "solana-program-test", - "solana-pubkey 4.2.0", - "solana-rent", - "solana-rpc", - "solana-rpc-client", - "solana-rpc-client-api", - "solana-runtime", - "solana-sbpf", - "solana-sdk-ids", - "solana-signer", - "solana-streamer", - "solana-syscalls", - "solana-transaction", - "solana-validator-exit", - "tokio", -] - [[package]] name = "solana-time-utils" version = "3.0.0" @@ -9223,36 +8774,6 @@ dependencies = [ "x509-parser", ] -[[package]] -name = "solana-tps-client" -version = "4.2.0-alpha.0" -dependencies = [ - "log", - "solana-account 4.3.0", - "solana-client", - "solana-client-traits", - "solana-clock", - "solana-commitment-config", - "solana-connection-cache", - "solana-epoch-info", - "solana-hash 4.4.0", - "solana-keypair", - "solana-message", - "solana-net-utils", - "solana-pubkey 4.2.0", - "solana-rpc-client", - "solana-rpc-client-api", - "solana-runtime", - "solana-signature", - "solana-signer", - "solana-streamer", - "solana-tpu-client", - "solana-transaction", - "solana-transaction-error", - "solana-transaction-status", - "thiserror 2.0.18", -] - [[package]] name = "solana-tpu-client" version = "4.2.0-alpha.0" @@ -9808,18 +9329,6 @@ dependencies = [ "solana-pubkey 3.0.0", ] -[[package]] -name = "spl-instruction-padding-interface" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3a77c0c9b83b111ee29bc6aa6eaab54b82e1ed5db40ba9786527b80283c3ef" -dependencies = [ - "num_enum", - "solana-instruction", - "solana-program-error", - "solana-pubkey 3.0.0", -] - [[package]] name = "spl-memo-interface" version = "2.1.0" @@ -10136,41 +9645,6 @@ dependencies = [ "xattr", ] -[[package]] -name = "tarpc" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c38a012bed6fb9681d3bf71ffaa4f88f3b4b9ed3198cda6e4c8462d24d4bb80" -dependencies = [ - "anyhow", - "fnv", - "futures 0.3.32", - "humantime", - "opentelemetry", - "pin-project", - "rand 0.8.6", - "serde", - "static_assertions", - "tarpc-plugins", - "thiserror 1.0.69", - "tokio", - "tokio-serde", - "tokio-util 0.6.10", - "tracing", - "tracing-opentelemetry", -] - -[[package]] -name = "tarpc-plugins" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ee42b4e559f17bce0385ebf511a7beb67d5cc33c12c96b7f4e9789919d9c10f" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "tempfile" version = "3.27.0" @@ -10239,15 +9713,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if 1.0.4", -] - [[package]] name = "threadpool" version = "1.8.1" @@ -10392,22 +9857,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-serde" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "911a61637386b789af998ee23f50aa30d5fd7edcec8d6d3dedae5e5815205466" -dependencies = [ - "bincode", - "bytes", - "educe 0.4.23", - "futures-core", - "futures-sink", - "pin-project", - "serde", - "serde_json", -] - [[package]] name = "tokio-stream" version = "0.1.18" @@ -10446,7 +9895,6 @@ dependencies = [ "futures-sink", "log", "pin-project-lite", - "slab", "tokio", ] @@ -10657,31 +10105,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", - "valuable", -] - -[[package]] -name = "tracing-opentelemetry" -version = "0.17.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbbe89715c1dbbb790059e2565353978564924ee85017b5fff365c872ff6721f" -dependencies = [ - "once_cell", - "opentelemetry", - "tracing", - "tracing-core", - "tracing-subscriber", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" -dependencies = [ - "sharded-slab", - "thread_local", - "tracing-core", ] [[package]] @@ -10887,12 +10310,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - [[package]] name = "vcpkg" version = "0.2.15" diff --git a/dev-bins/Cargo.toml b/dev-bins/Cargo.toml index 87af91e3e8a..b06df62f0b8 100644 --- a/dev-bins/Cargo.toml +++ b/dev-bins/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["../accounts-db/store-tool", "../bench-tps", "../ledger-tool"] +members = ["../accounts-db/store-tool", "../ledger-tool"] resolver = "2" diff --git a/docs/deploy.sh b/docs/deploy.sh index b0bd9e28b90..0e090fb7be2 100755 --- a/docs/deploy.sh +++ b/docs/deploy.sh @@ -99,7 +99,6 @@ for PROJECT_NAME in "${PROJECT_NAMES[@]}"; do { "source": "/wallet-guide/hardware-wallet", "destination": "/cli/wallets/hardware-wallet" }, { "source": "/wallet-guide/hardware-wallet/ledger", "destination": "/cli/wallets/hardware-wallet/ledger" }, { "source": "/cluster/overview", "destination": "/clusters" }, - { "source": "/cluster/bench-tps", "destination": "/clusters/benchmark" }, { "source": "/cluster/performance-metrics", "destination": "/clusters/metrics" }, { "source": "/running-validator", "destination": "/operations" }, { "source": "/validator/get-started/setup-a-validator", "destination": "/operations/setup-a-validator" }, diff --git a/docs/src/clusters/benchmark.md b/docs/src/clusters/benchmark.md index dbb7113cdf6..722fd2bde8d 100644 --- a/docs/src/clusters/benchmark.md +++ b/docs/src/clusters/benchmark.md @@ -89,10 +89,10 @@ Now that your singlenode or multinode testnet is up and running let's send it so In a separate shell start the client: ```bash -./multinode-demo/bench-tps.sh # runs against localhost by default +./multinode-demo/txs-bench.sh # runs against localhost by default ``` -What just happened? The client demo spins up several threads to send 500,000 transactions to the testnet as quickly as it can. The client then pings the testnet periodically to see how many transactions it processed in that time. Take note that the demo intentionally floods the network with UDP packets, such that the network will almost certainly drop a bunch of them. This ensures the testnet has an opportunity to reach 710k TPS. The client demo completes after it has convinced itself the testnet won't process any additional transactions. You should see several TPS measurements printed to the screen. In the multinode variation, you'll see TPS measurements for each validator node as well. +What just happened? The client demo starts `solana-transaction-bench`, creates funded payer accounts from the configured authority, and sends transfer transactions to the testnet. You should see TPS measurements printed to the screen. In the multinode variation, you'll see TPS measurements for each validator node as well. ### Testnet Debugging @@ -130,7 +130,7 @@ This will dump all the threads stack traces into gdb.txt In this example the client connects to our public testnet. To run validators on the testnet you would need to open udp ports `8000-10000`. ```bash -./multinode-demo/bench-tps.sh --entrypoint entrypoint.devnet.solana.com:8001 --faucet api.devnet.solana.com:9900 --duration 60 --tx_count 50 +./multinode-demo/txs-bench.sh --url https://api.devnet.solana.com --authority /path/to/funded-keypair.json --duration 60 --target-tps 50 ws-leader-tracker ``` You can observe the effects of your client's transactions on our [metrics dashboard](https://metrics.solana.com:3000/d/monitor/cluster-telemetry?var-testnet=devnet) diff --git a/docs/src/clusters/metrics.md b/docs/src/clusters/metrics.md index 3405fab4001..7a442375257 100644 --- a/docs/src/clusters/metrics.md +++ b/docs/src/clusters/metrics.md @@ -22,6 +22,6 @@ The node assigns a timestamp to every new fork, and computes the time it took to The validator software is deployed to GCP n1-standard-16 instances with 1TB pd-ssd disk, and 2x Nvidia V100 GPUs. These are deployed in the us-west-1 region. -solana-bench-tps is started after the network converges from a client machine with n1-standard-16 CPU-only instance with the following arguments: `--tx\_count=50000 --thread-batch-sleep 1000` +Load generation is started after the network converges from a client machine with n1-standard-16 CPU-only instance. -TPS and confirmation metrics are captured from the dashboard numbers over a 5-minute average of when the bench-tps transfer stage begins. +TPS and confirmation metrics are captured from the dashboard numbers over a 5-minute average while the transaction workload is running. diff --git a/docs/src/clusters/testnet.md b/docs/src/clusters/testnet.md index 9a44a6ada1d..aa315c28036 100644 --- a/docs/src/clusters/testnet.md +++ b/docs/src/clusters/testnet.md @@ -97,7 +97,7 @@ cd net/ # Configure the metrics database and validate credentials using environment variable `SOLANA_METRICS_CONFIG` (skip this if you are not using metrics) ./init-metrics.sh -c testnet-dev-${USER} ${USER} -# Deploy the network from the local workspace and start processes on all nodes including bench-tps on the client node +# Deploy the network from the local workspace and start processes on all nodes including transaction-bench on the client node RUST_LOG=info ./net.sh start # Show a help to ssh into any testnet node to access logs/etc @@ -125,7 +125,7 @@ RUST_LOG=info ./net.sh start * `./init-metrics.sh -c testnet-dev-${user} ${user}` to create a new metrics database from scratch * Manually set `SOLANA_METRICS_CONFIG` in `./net/config/config` (which is exactly what `init-metrics.sh` does for you) * `./init-metrics.sh -e` to load metrics config from `SOLANA_METRICS_CONFIG` into the testnet config file or -* `./net.sh` controls the payload on the testnet nodes, i.e. bootstrapping, the validators and bench-tps. In principle, you can run everything by hand, but `./net.sh` makes it easier. +* `./net.sh` controls the payload on the testnet nodes, i.e. bootstrapping, the validators and transaction-bench clients. In principle, you can run everything by hand, but `./net.sh` makes it easier. * `./net.sh start` to actually run the test network. * This will actually upload your current sources to the bootstrap host, build them there and upload the result to all the nodes * The script will take 5-10 of minutes to run, in the end it should print something like @@ -135,8 +135,8 @@ RUST_LOG=info ./net.sh start ``` * You can also make sure it logs successful test transfers: ```✅ 1 lamport(s) transferred: seq=0 time= 402ms signature=33uJtPJM6ekBGrWCgWHKw1TTQJVrLxYMe3sp2PUmSRVb21LyXn3nDbQmzsgQyihE7VP2zD2iR66Du8aDUnSSd6pb``` - * `./net.sh start bench-tps=2="--tx_count 2500"` will start 2 clients with bench-tps workload sending 2500 transactions per batch. - * --tx_count argument is passed to the bench-tps program, see its manual for more options + * `./net.sh start -c transaction-bench=2="--target-tps 5000 ws-leader-tracker"` will start 2 clients with transaction-bench workloads targeting 5000 TPS. + * Arguments are passed to `solana-transaction-bench`; see its manual for more options. * `./net.sh sanity` to test the deployment, it is also run by start command * `./net.sh stop` to stop the validators and client. This does not kill the machines, so you can study the logs etc. * `./net.sh start --nobuild` will skip the source compilation, you will generally want that if you are only changing configuration files rather than code, or just want to re-run the last test. @@ -174,7 +174,7 @@ You will want to have a script like this pretty much immediately to avoid making # Patch metrics config from env into config file ./init-metrics.sh -e # Enable metrics and start the network (this will also build software) -RUST_LOG=info ./net.sh start -c bench-tps=2="--tx_count 25000" +RUST_LOG=info ./net.sh start -c transaction-bench=2="--target-tps 5000 ws-leader-tracker" ``` ### Inscrutable "nothing works everything times out state" diff --git a/metrics/scripts/grafana-provisioning/dashboards/cluster-monitor.json b/metrics/scripts/grafana-provisioning/dashboards/cluster-monitor.json index c33ece2f2eb..aa11ad38752 100644 --- a/metrics/scripts/grafana-provisioning/dashboards/cluster-monitor.json +++ b/metrics/scripts/grafana-provisioning/dashboards/cluster-monitor.json @@ -13015,376 +13015,6 @@ "alignLevel": null } }, - { - "collapsed": false, - "datasource": null, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 125 - }, - "id": 75, - "panels": [], - "title": "Bench TPS", - "type": "row" - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "fieldConfig": { - "defaults": { - "custom": {} - }, - "overrides": [] - }, - "fill": 1, - "gridPos": { - "h": 5, - "w": 7, - "x": 0, - "y": 126 - }, - "id": 76, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": false, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pluginVersion": "7.4.3", - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "measurement": "cluster_info-vote-count", - "orderByTime": "ASC", - "policy": "autogen", - "query": "SELECT balance FROM \"$testnet\".\"autogen\".\"bench-tps-lamport_balance\" WHERE $timeFilter fill(null)\n\n\n\n", - "rawQuery": true, - "refId": "A", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "count" - ], - "type": "field" - }, - { - "params": [], - "type": "sum" - } - ] - ], - "tags": [] - } - ], - "thresholds": [], - "timeFrom": null, - "timeShift": null, - "title": "Bench TPS Token Balance", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": "0", - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": false - } - ], - "yaxis": { - "align": true, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "fieldConfig": { - "defaults": { - "custom": {} - }, - "overrides": [] - }, - "fill": 1, - "gridPos": { - "h": 5, - "w": 7, - "x": 7, - "y": 126 - }, - "id": 77, - "legend": { - "alignAsTable": false, - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pluginVersion": "7.4.3", - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "measurement": "cluster_info-vote-count", - "orderByTime": "ASC", - "policy": "autogen", - "query": "SELECT mean(\"duration\") as \"Generation Time\" FROM \"$testnet\".\"autogen\".\"bench-tps-generate_txs\" WHERE $timeFilter GROUP BY time(1s)\n\n\n\n\n", - "rawQuery": true, - "refId": "A", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "count" - ], - "type": "field" - }, - { - "params": [], - "type": "sum" - } - ] - ], - "tags": [] - }, - { - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "orderByTime": "ASC", - "policy": "default", - "query": "SELECT mean(\"duration\") as \"Transmit Time\" FROM \"$testnet\".\"autogen\".\"bench-tps-do_tx_transfers\" WHERE $timeFilter GROUP BY time(1s)", - "rawQuery": true, - "refId": "B", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ] - ], - "tags": [] - }, - { - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "orderByTime": "ASC", - "policy": "default", - "query": "SELECT mean(\"duration\") as \"Get Blockhash\" FROM \"$testnet\".\"autogen\".\"bench-tps-get_blockhash\" WHERE $timeFilter GROUP BY time(1s)", - "rawQuery": true, - "refId": "C", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ] - ], - "tags": [] - }, - { - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "orderByTime": "ASC", - "policy": "default", - "query": "SELECT mean(\"duration\") as \"Get Balance\" FROM \"$testnet\".\"autogen\".\"bench-tps-get_balance\" WHERE $timeFilter GROUP BY time(1s)", - "rawQuery": true, - "refId": "D", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ] - ], - "tags": [] - } - ], - "thresholds": [], - "timeFrom": null, - "timeShift": null, - "title": "Bench TPS Transaction Activity", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "µs", - "label": null, - "logBase": 1, - "max": null, - "min": "0", - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": false - } - ], - "yaxis": { - "align": true, - "alignLevel": null - } - }, { "columns": [], "datasource": "$datasource", diff --git a/multinode-demo/bench-tps.sh b/multinode-demo/bench-tps.sh deleted file mode 100755 index 0a7d3e1a2ac..00000000000 --- a/multinode-demo/bench-tps.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env bash -set -e - -here=$(dirname "$0") -# shellcheck source=multinode-demo/common.sh -source "$here"/common.sh - -deprecation_notice() { - echo "WARNING: bench-tps is deprecated and will be removed in a future release." - echo "Please use multinode-demo/txs-bench.sh / solana-transaction-bench instead." -} - -usage() { - declare message=${1:-} - if [[ -n $message ]]; then - echo "$message" - echo - fi - echo "usage: $0 [extra args]" - echo - echo " Run bench-tps " - echo - deprecation_notice - echo - echo " extra args: additional arguments are passed along to solana-bench-tps" - echo - exit 1 -} - -if [[ ${1:-} = "-h" || ${1:-} = "--help" ]]; then - usage "" -fi - -deprecation_notice >&2 - -args=("$@") -default_arg --url "http://127.0.0.1:8899" -default_arg --faucet "127.0.0.1:9900" -default_arg --duration 90 -default_arg --tx-count 50000 -default_arg --thread-batch-sleep-ms 0 -default_arg --bind-address "127.0.0.1" -default_arg --client-node-id "${SOLANA_CONFIG_DIR}/bootstrap-validator/identity.json" - -$solana_bench_tps "${args[@]}" diff --git a/multinode-demo/common.sh b/multinode-demo/common.sh index e29755262b7..5abc5700cb4 100644 --- a/multinode-demo/common.sh +++ b/multinode-demo/common.sh @@ -36,7 +36,7 @@ else declare program="$1" declare crate="$program" declare manifest_path - if [[ $program == "bench-tps" || $program == "ledger-tool" ]]; then + if [[ $program == "ledger-tool" ]]; then manifest_path="--manifest-path $here/dev-bins/Cargo.toml" fi if [[ -z $program ]]; then @@ -65,7 +65,6 @@ else } fi -solana_bench_tps=$(solana_program bench-tps) solana_transaction_bench=${SOLANA_TRANSACTION_BENCH:-solana-transaction-bench} solana_faucet=$(solana_program faucet) agave_validator=$(solana_program validator) diff --git a/multinode-demo/txs-bench.sh b/multinode-demo/txs-bench.sh index 6cbb1c9c55d..3a2b51008ae 100755 --- a/multinode-demo/txs-bench.sh +++ b/multinode-demo/txs-bench.sh @@ -19,7 +19,6 @@ usage: $0 [extra args] Run solana-transaction-bench against the local multinode demo. extra args: additional arguments are passed along to solana-transaction-bench. - Use solana-transaction-bench argument names, not bench-tps names. Defaults: --url http://127.0.0.1:8899 diff --git a/net/net.sh b/net/net.sh index 28f7acdbe83..0b3ae2d08f3 100755 --- a/net/net.sh +++ b/net/net.sh @@ -20,11 +20,9 @@ usage() { Valid client types are: idle transaction-bench - bench-tps (deprecated) User can optionally provide extraArgs that are transparently supplied to the client program as command line parameters. - extraArgs use the argument names of the selected client program - (solana-transaction-bench or, for bench-tps, solana-bench-tps). + extraArgs use solana-transaction-bench argument names. For example, -c transaction-bench=2="--target-tps 5000 ws-leader-tracker" This will start 2 solana-transaction-bench clients, and supply @@ -116,10 +114,6 @@ Operate a configured testnet --tpu-enable-udp - Enable UDP for tpu transactions - --client-type - - Specify backend client type for the deprecated bench-tps. Valid options are (rpc-client|tpu-client), tpu-client is default. - Ignored by solana-transaction-bench, which always sends over QUIC to the TPU. - sanity/start-specific options: -F - Discard validator nodes that didn't bootup successfully -o rejectExtraNodes - Require the exact number of nodes @@ -327,7 +321,6 @@ startBootstrapLeader() { \"$internalNodesStakeLamports\" \ \"$internalNodesLamports\" \ $nodeIndex \ - $numBenchTpsClients \"$benchTpsExtraArgs\" \ \"$genesisOptions\" \ \"$maybeNoSnapshot $maybeSkipLedgerVerify $maybeLimitLedgerSize $maybeWaitForSupermajority $maybeAccountsDbSkipShrink $maybeSkipRequireTower\" \ \"$maybeWarpSlot\" \ @@ -402,7 +395,6 @@ startNode() { \"$internalNodesStakeLamports\" \ \"$internalNodesLamports\" \ $nodeIndex \ - $numBenchTpsClients \"$benchTpsExtraArgs\" \ \"$genesisOptions\" \ \"$maybeNoSnapshot $maybeSkipLedgerVerify $maybeLimitLedgerSize $maybeWaitForSupermajority $maybeAccountsDbSkipShrink $maybeSkipRequireTower\" \ \"$maybeWarpSlot\" \ @@ -423,13 +415,9 @@ startNode() { startClient() { declare ipAddress=$1 declare clientToRun="$2" - declare clientIndex="$3" # Forward the extra args that belong to the selected client program. - declare clientExtraArgs=$benchTpsExtraArgs - if [[ $clientToRun = solana-transaction-bench ]]; then - clientExtraArgs=$transactionBenchExtraArgs - fi + declare clientExtraArgs=$transactionBenchExtraArgs initLogDir declare logFile="$netLogDir/client-$clientToRun-$ipAddress.log" @@ -441,7 +429,7 @@ startClient() { startCommon "$ipAddress" ssh "${sshOptions[@]}" -f "$ipAddress" \ "./solana/net/remote/remote-client.sh $deployMethod $entrypointIp \ - $clientToRun \"$RUST_LOG\" \"$clientExtraArgs\" $clientIndex $clientType \ + $clientToRun \"$RUST_LOG\" \"$clientExtraArgs\" \ $maybeUseUnstakedConnection" ) >> "$logFile" 2>&1 || { cat "$logFile" @@ -451,15 +439,11 @@ startClient() { } startClients() { - benchTpsIndex=0 for ((i=0; i < numClients && i < numClientsRequested; i++)) do if [[ $i -lt $numTransactionBenchClients ]]; then - startClient "${clientIpList[$i]}" "solana-transaction-bench" "$i" - elif [[ $i -lt $((numTransactionBenchClients + numBenchTpsClients)) ]]; then - startClient "${clientIpList[$i]}" "solana-bench-tps" "$benchTpsIndex" - ((benchTpsIndex++)) + startClient "${clientIpList[$i]}" "solana-transaction-bench" else - startClient "${clientIpList[$i]}" "idle" "$i" + startClient "${clientIpList[$i]}" "idle" fi done } @@ -824,8 +808,6 @@ sanityExtraArgs= skipSetup=false nodeAddress= numIdleClients=0 -numBenchTpsClients=0 -benchTpsExtraArgs= numTransactionBenchClients=0 transactionBenchExtraArgs= failOnValidatorBootupFailure=true @@ -855,7 +837,6 @@ waitForNodeInit=true extraPrimordialStakes=0 disableQuic=false enableUdp=false -clientType=tpu-client maybeUseUnstakedConnection="" alpenglow=false @@ -972,17 +953,6 @@ while [[ -n $1 ]]; do elif [[ $1 = --skip-require-tower ]]; then maybeSkipRequireTower="$1" shift 1 - elif [[ $1 = --client-type ]]; then - clientType=$2 - case "$clientType" in - tpu-client|rpc-client) - ;; - *) - echo "Unexpected client type: \"$clientType\"" - exit 1 - ;; - esac - shift 2 elif [[ $1 = --use-unstaked-connection ]]; then maybeUseUnstakedConnection="$1" shift 1 @@ -1060,10 +1030,6 @@ while getopts "h?T:t:o:f:rc:Fn:i:d" opt "${shortArgs[@]}"; do numTransactionBenchClients=$numClients transactionBenchExtraArgs=$extraArgs ;; - bench-tps) - numBenchTpsClients=$numClients - benchTpsExtraArgs=$extraArgs - ;; *) echo "Unknown client type: $clientType" exit 1 @@ -1096,7 +1062,7 @@ if [[ -n $numValidatorsRequested ]]; then fi numClients=${#clientIpList[@]} -numClientsRequested=$((numTransactionBenchClients + numBenchTpsClients + numIdleClients)) +numClientsRequested=$((numTransactionBenchClients + numIdleClients)) if [[ "$numClientsRequested" -eq 0 ]]; then # Default to solana-transaction-bench on every available client node. numTransactionBenchClients=$numClients diff --git a/net/remote/remote-client.sh b/net/remote/remote-client.sh index ea145a49e33..9783364f81d 100755 --- a/net/remote/remote-client.sh +++ b/net/remote/remote-client.sh @@ -9,10 +9,8 @@ clientToRun="$3" if [[ -n $4 ]]; then export RUST_LOG="$4" fi -benchTpsExtraArgs="$5" -clientIndex="$6" -clientType="${7:-tpu-client}" -maybeUseUnstakedConnection="$8" +clientExtraArgs="$5" +maybeUseUnstakedConnection="$6" missing() { echo "Error: $1 not specified" @@ -25,11 +23,6 @@ missing() { source net/common.sh loadConfigFile -threadCount=$(nproc) -if [[ $threadCount -gt 4 ]]; then - threadCount=4 -fi - case $deployMethod in local|tar) PATH="$HOME"/.cargo/bin:"$PATH" @@ -43,20 +36,6 @@ skip) exit 1 esac -RPC_CLIENT=false -case "$clientType" in - tpu-client) - RPC_CLIENT=false - ;; - rpc-client) - RPC_CLIENT=true - ;; - *) - echo "Unexpected clientType: \"$clientType\"" - exit 1 - ;; -esac - # Does "$@" (beyond the first arg, which is the name to look for) contain $name, # either as a bare flag or as --flag=value? contains_arg() { @@ -110,9 +89,8 @@ solana-transaction-bench) ensureTransactionBench # transaction-bench creates and funds ephemeral payer accounts from the - # faucet (the genesis mint) at runtime, so unlike bench-tps it needs no - # pre-generated client account keys. We only need the faucet keypair as the - # funding authority and, for a staked QUIC connection, a staked validator + # faucet (the genesis mint) at runtime. We only need the faucet keypair as + # the funding authority and, for a staked QUIC connection, a staked validator # identity. net/scripts/rsync-retry.sh -vPrc \ "$entrypointIp":~/solana/config/faucet.json ./faucet.json @@ -120,10 +98,6 @@ solana-transaction-bench) net/scripts/rsync-retry.sh -vPrc \ "$entrypointIp":~/solana/config/validator-identity-1.json ./validator-identity.json - if [[ $clientType = rpc-client ]]; then - echo "Note: clientType=rpc-client is ignored by solana-transaction-bench; it always sends over QUIC to the TPU." - fi - # solana-transaction-bench expects its arguments in positional buckets: # run # Split the operator-supplied extra args into those buckets so that, e.g., @@ -133,7 +107,7 @@ solana-transaction-bench) leaderTrackerArgs=() # shellcheck disable=SC2206 # Intentional word-splitting of the extra args string - extraArgs=($benchTpsExtraArgs) + extraArgs=($clientExtraArgs) argIndex=0 while [[ $argIndex -lt ${#extraArgs[@]} ]]; do arg="${extraArgs[$argIndex]}" @@ -197,35 +171,6 @@ solana-transaction-bench) ${leaderTrackerArgs[*]} \ " ;; -solana-bench-tps) - net/scripts/rsync-retry.sh -vPrc \ - "$entrypointIp":~/solana/config/bench-tps"$clientIndex".yml ./client-accounts.yml - - net/scripts/rsync-retry.sh -vPrc \ - "$entrypointIp":~/solana/config/validator-identity-1.json ./validator-identity.json - - args=() - - if ${RPC_CLIENT}; then - args+=(--use-rpc-client) - fi - - if [[ -z "$maybeUseUnstakedConnection" ]]; then - args+=(--bind-address "$entrypointIp") - args+=(--client-node-id ./validator-identity.json) - fi - - clientCommand="\ - solana-bench-tps \ - --duration 7500 \ - --sustained \ - --threads $threadCount \ - $benchTpsExtraArgs \ - --read-client-keys ./client-accounts.yml \ - --url "http://$entrypointIp:8899" \ - ${args[*]} \ - " - ;; idle) # Add the faucet keypair to idle clients for convenience net/scripts/rsync-retry.sh -vPrc \ diff --git a/net/remote/remote-node.sh b/net/remote/remote-node.sh index 0ed7417c12f..6d95cbdf486 100755 --- a/net/remote/remote-node.sh +++ b/net/remote/remote-node.sh @@ -18,18 +18,16 @@ maybeDisableAirdrops="$9" internalNodesStakeLamports="${10}" internalNodesLamports="${11}" nodeIndex="${12}" -numBenchTpsClients="${13}" -benchTpsExtraArgs="${14}" -genesisOptions="${15}" -extraNodeArgs="${16}" -maybeWarpSlot="${17}" -maybeFullRpc="${18}" -waitForNodeInit="${19}" -extraPrimordialStakes="${20:=0}" -tmpfsAccounts="${21:false}" -disableQuic="${22}" -enableUdp="${23}" -alpenglow="${24:-false}" +genesisOptions="${13}" +extraNodeArgs="${14}" +maybeWarpSlot="${15}" +maybeFullRpc="${16}" +waitForNodeInit="${17}" +extraPrimordialStakes="${18:=0}" +tmpfsAccounts="${19:false}" +disableQuic="${20}" +enableUdp="${21}" +alpenglow="${22:-false}" set +x @@ -144,33 +142,12 @@ EOF done setupValidatorKeypair blockstreamer-identity - lamports_per_signature="42" - # shellcheck disable=SC2206 # Do not want to quote $genesisOptions - genesis_args=($genesisOptions) - for i in "${!genesis_args[@]}"; do - if [[ "${genesis_args[$i]}" = --target-lamports-per-signature ]]; then - lamports_per_signature="${genesis_args[$((i+1))]}" - break - fi - done - - for i in $(seq 0 $((numBenchTpsClients-1))); do - # shellcheck disable=SC2086 # Do not want to quote $benchTpsExtraArgs - solana-bench-tps --write-client-keys config/bench-tps"$i".yml \ - --target-lamports-per-signature "$lamports_per_signature" $benchTpsExtraArgs - # Skip first line, as it contains header - tail -n +2 -q config/bench-tps"$i".yml >> config/client-accounts.yml - echo "" >> config/client-accounts.yml - done if [[ -f $externalPrimordialAccountsFile ]]; then cat "$externalPrimordialAccountsFile" >> config/validator-balances.yml fi if [[ -f config/validator-balances.yml ]]; then genesisOptions+=" --primordial-accounts-file config/validator-balances.yml" fi - if [[ -f config/client-accounts.yml ]]; then - genesisOptions+=" --primordial-accounts-file config/client-accounts.yml" - fi if [[ -n $internalNodesStakeLamports ]]; then args+=(--bootstrap-validator-stake-lamports "$internalNodesStakeLamports") @@ -361,7 +338,7 @@ EOF set -x # Add the faucet keypair to validators for convenient access from tools - # like bench-tps and add to blocktreamers to run a faucet + # and add it to blockstreamers to run a faucet. scp "$entrypointIp":~/solana/config/faucet.json "$SOLANA_CONFIG_DIR"/ if [[ $nodeType = blockstreamer ]]; then # Run another faucet with the same keypair on the blockstreamer node. diff --git a/scripts/agave-build-lists.sh b/scripts/agave-build-lists.sh index fd72133bc06..17c7d76f0ee 100755 --- a/scripts/agave-build-lists.sh +++ b/scripts/agave-build-lists.sh @@ -31,7 +31,6 @@ AGAVE_BINS_VAL_OP=( AGAVE_BINS_DCOU=( agave-ledger-tool - solana-bench-tps ) # These bins are deprecated and will be removed in a future release @@ -47,7 +46,6 @@ DCOU_TAINTED_PACKAGES=( agave-store-tool solana-accounts-cluster-bench solana-banking-bench - solana-bench-tps solana-local-cluster solana-svm-test-harness solana-svm-test-harness-fixture From 4a538391c11598f15907e081021ce61f238d5d52 Mon Sep 17 00:00:00 2001 From: Mykola Dzham Date: Mon, 15 Jun 2026 14:32:47 +0200 Subject: [PATCH 335/576] Give write permissions to release jobs (#13178) * Give write permissions to release jobs Release jobs use default GITHUB_TOKEN for several jobs: to create version bump PRs and to create releases. These operations require the token to have `content: write` * Also give write access for pull requests The workflow creates version bump PRs. Co-authored-by: Yihau Chen --------- Co-authored-by: Yihau Chen --- .github/workflows/bump-version.yml | 3 +++ .github/workflows/publish-windows-tarball.yml | 2 ++ .github/workflows/release.yml | 2 ++ 3 files changed, 7 insertions(+) diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml index 07217572aed..64b32f54e00 100644 --- a/.github/workflows/bump-version.yml +++ b/.github/workflows/bump-version.yml @@ -19,6 +19,9 @@ jobs: version-bump: if: github.repository_owner == 'anza-xyz' runs-on: ubuntu-24.04 + permissions: + contents: write + pull-requests: write steps: - name: Checkout code uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 diff --git a/.github/workflows/publish-windows-tarball.yml b/.github/workflows/publish-windows-tarball.yml index ddabe7dd085..ef6c3cf2c40 100644 --- a/.github/workflows/publish-windows-tarball.yml +++ b/.github/workflows/publish-windows-tarball.yml @@ -89,6 +89,8 @@ jobs: if: ${{ needs.windows-build.outputs.tag != '' }} needs: [windows-build] runs-on: ubuntu-22.04 + permissions: + contents: write steps: - name: Download uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f815d0bc745..6f9741a7cba 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,6 +23,8 @@ jobs: draft-release: if: github.repository_owner == 'anza-xyz' runs-on: ubuntu-24.04 + permissions: + contents: write steps: - name: Create Release uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 From ab842fdae9336ae0ab8625c80d01bd786c0335de Mon Sep 17 00:00:00 2001 From: Leafaar <45247054+leafaar@users.noreply.github.com> Date: Mon, 15 Jun 2026 09:33:36 -0300 Subject: [PATCH 336/576] xdp: add 802.1Q vlan encap to the tx loop (#12920) xdp: add 802.1Q VLAN encapsulation for the tx loop AF_XDP transmits raw frames directly to the bound NIC and bypasses the kernel VLAN driver, so traffic the kernel would normally tag (via a `vlanN` sub-interface) leaves untagged when XDP is enabled. This adds 802.1Q encapsulation in userspace, modeled on the existing GRE path: when a route resolves through a VLAN sub-interface the tx loop prepends the 4-byte tag. The wire layout matches what the kernel VLAN driver would have produced: [dst|src MAC] [TPID 0x8100] [TCI {pcp, dei=0, vid}] [EtherType] [IPv4] [UDP] [payload]. VLAN sub-interfaces are discovered from netlink link attributes (IFLA_LINKINFO kind "vlan", IFLA_VLAN_ID), the same way GRE tunnel info is parsed, so no configuration is needed and the route monitor picks up link changes automatically. Only 802.1Q is handled; 802.1ad (QinQ) sub-interfaces are skipped. PCP is emitted as 0, matching the kernel's default egress QoS mapping. Since the XDP sender is shared, this tags both retransmit and broadcast traffic that egresses through a VLAN. --- xdp/src/netlink.rs | 39 +++++++ xdp/src/packet.rs | 283 +++++++++++++++++++++++++++++++++++++++++++++ xdp/src/route.rs | 197 ++++++++++++++++++++++++++++++- xdp/src/tx_loop.rs | 103 +++++++++++++---- 4 files changed, 598 insertions(+), 24 deletions(-) diff --git a/xdp/src/netlink.rs b/xdp/src/netlink.rs index 450acaf0020..e2d4b576df5 100644 --- a/xdp/src/netlink.rs +++ b/xdp/src/netlink.rs @@ -34,6 +34,10 @@ const IFLA_GRE_TTL: u16 = 8; const IFLA_GRE_TOS: u16 = 9; const IFLA_GRE_PMTUDISC: u16 = 10; +// VLAN nested attributes (from include/uapi/linux/if_link.h) +const IFLA_VLAN_ID: u16 = 1; +const IFLA_VLAN_PROTOCOL: u16 = 5; + #[repr(C)] #[allow(non_camel_case_types)] struct ifinfomsg { @@ -386,11 +390,19 @@ pub struct GreTunnelInfo { pub pmtudisc: u8, } +/// 802.1Q VLAN sub-interface information from netlink (IFLA_LINKINFO kind "vlan"). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct VlanLinkInfo { + /// VLAN ID (IFLA_VLAN_ID), 1-4094 in practice. + pub vid: u16, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct InterfaceInfo { pub if_index: u32, pub mtu: u32, pub gre_tunnel: Option, + pub vlan_link: Option, } impl InterfaceInfo { @@ -453,13 +465,35 @@ pub(crate) fn parse_rtm_ifinfomsg(msg: &NetlinkMessage) -> Option // Parse GRE tunnel information if this is a GRE interface let gre_tunnel = parse_gre_tunnel_info_from_linkinfo(&attrs); + // Parse VLAN information if this is an 802.1Q VLAN sub-interface + let vlan_link = parse_vlan_link_info_from_linkinfo(&attrs); Some(InterfaceInfo { if_index: ifi.ifi_index, mtu, gre_tunnel, + vlan_link, }) } +// Parse 802.1Q VLAN information from netlink +fn parse_vlan_link_info_from_linkinfo(attrs: &HashMap) -> Option { + let vlan = parse_linkinfo_data_for_kind(attrs, b"vlan")?; + + // Only 802.1Q is supported; skip 802.1ad (QinQ) sub-interfaces. The protocol attribute is a + // big-endian u16; kernels predating 802.1ad support omit it, which implies 802.1Q. + if let Some(proto) = vlan.get(&IFLA_VLAN_PROTOCOL) { + let proto = u16::from_be_bytes(proto.data.get(..2)?.try_into().ok()?); + if proto != libc::ETH_P_8021Q as u16 { + return None; + } + } + + let vid = vlan + .get(&IFLA_VLAN_ID) + .and_then(|a| u16_from_ne_bytes(a.data))?; + Some(VlanLinkInfo { vid }) +} + // Parse GRE tunnel information from netlink fn parse_gre_tunnel_info_from_linkinfo(attrs: &HashMap) -> Option { let gre = parse_linkinfo_data_for_kind(attrs, b"gre")?; @@ -783,3 +817,8 @@ fn u32_from_ne_bytes(data: &[u8]) -> Option { let bytes: [u8; 4] = data.get(..4)?.try_into().ok()?; Some(u32::from_ne_bytes(bytes)) } + +fn u16_from_ne_bytes(data: &[u8]) -> Option { + let bytes: [u8; 2] = data.get(..2)?.try_into().ok()?; + Some(u16::from_ne_bytes(bytes)) +} diff --git a/xdp/src/packet.rs b/xdp/src/packet.rs index dd6a419817a..13c2c2a951c 100644 --- a/xdp/src/packet.rs +++ b/xdp/src/packet.rs @@ -7,9 +7,17 @@ use { }; pub const ETH_HEADER_SIZE: usize = 14; +/// Plain Ethernet header (14B) + 802.1Q tag (4B). +pub const VLAN_ETH_HEADER_SIZE: usize = ETH_HEADER_SIZE + 4; pub const IP_HEADER_SIZE: usize = 20; pub const UDP_HEADER_SIZE: usize = 8; +/// Total header size of an untagged IPv4 UDP frame: Ethernet + IP + UDP. +pub const PACKET_HEADER_SIZE: usize = ETH_HEADER_SIZE + IP_HEADER_SIZE + UDP_HEADER_SIZE; +/// Total header size of an 802.1Q tagged IPv4 UDP frame: tagged Ethernet + IP + UDP. +pub const VLAN_PACKET_HEADER_SIZE: usize = VLAN_ETH_HEADER_SIZE + IP_HEADER_SIZE + UDP_HEADER_SIZE; const IP_DONT_FRAGMENT: u16 = 0x4000; +/// EtherType identifying an 802.1Q tagged frame. +const ETH_P_8021Q: u16 = 0x8100; pub fn write_eth_header(packet: &mut [u8], src_mac: &[u8; 6], dst_mac: &[u8; 6]) { packet[0..6].copy_from_slice(dst_mac); @@ -17,6 +25,116 @@ pub fn write_eth_header(packet: &mut [u8], src_mac: &[u8; 6], dst_mac: &[u8; 6]) packet[12..14].copy_from_slice(&(ETH_P_IP as u16).to_be_bytes()); } +/// Write an 18-byte Ethernet header carrying an 802.1Q VLAN tag. +/// +/// Layout: `dst MAC (6) | src MAC (6) | TPID=0x8100 (2) | TCI (2) | inner EtherType (2)`. +/// The TCI packs `pcp` into the high 3 bits, DEI=0, and `vid` into the low 12 bits. +#[inline] +pub fn write_vlan_eth_header( + packet: &mut [u8], + src_mac: &[u8; 6], + dst_mac: &[u8; 6], + vid: u16, + pcp: u8, +) { + packet[0..6].copy_from_slice(dst_mac); + packet[6..12].copy_from_slice(src_mac); + packet[12..14].copy_from_slice(Ð_P_8021Q.to_be_bytes()); + // TCI: PCP in the top 3 bits, then DEI=0 (frames are not marked drop-eligible, matching what + // the kernel VLAN driver emits by default), then the VID in the low 12 bits. + let tci = (((pcp as u16) & 0x7) << 13) | (vid & 0x0FFF); + packet[14..16].copy_from_slice(&tci.to_be_bytes()); + packet[16..18].copy_from_slice(&(ETH_P_IP as u16).to_be_bytes()); +} + +/// Construct a complete untagged IPv4 UDP frame in `packet`. +/// +/// Layout: `[Eth] [IPv4] [UDP] [payload]`. The caller is responsible for sizing `packet` to at +/// least `PACKET_HEADER_SIZE + payload.len()`; this function returns `false` and writes nothing +/// if the buffer is too small. +#[allow(clippy::too_many_arguments)] +pub fn construct_packet( + packet: &mut [u8], + src_mac: &[u8; 6], + dst_mac: &[u8; 6], + src_ip: &Ipv4Addr, + dst_ip: &Ipv4Addr, + src_port: u16, + dst_port: u16, + payload: &[u8], + ecn: Option, +) -> bool { + let payload_len = payload.len(); + if packet.len() < PACKET_HEADER_SIZE + payload_len { + return false; + } + // write the payload first as it's needed for checksum calculation (if enabled) + packet[PACKET_HEADER_SIZE..PACKET_HEADER_SIZE + payload_len].copy_from_slice(payload); + write_eth_header(packet, src_mac, dst_mac); + write_ip_header_for_udp( + &mut packet[ETH_HEADER_SIZE..], + src_ip, + dst_ip, + ecn, + (UDP_HEADER_SIZE + payload_len) as u16, + ); + write_udp_header( + &mut packet[ETH_HEADER_SIZE + IP_HEADER_SIZE..], + src_ip, + src_port, + dst_ip, + dst_port, + payload_len as u16, + false, + ); + true +} + +/// Construct a complete VLAN-tagged IPv4 UDP frame in `packet`. +/// +/// Layout: `[Eth + 802.1Q] [IPv4] [UDP] [payload]`. The caller is responsible for sizing `packet` +/// to at least `VLAN_PACKET_HEADER_SIZE + payload.len()`; this function returns `false` and +/// writes nothing if the buffer is too small. +#[allow(clippy::too_many_arguments)] +pub fn construct_vlan_packet( + packet: &mut [u8], + src_mac: &[u8; 6], + dst_mac: &[u8; 6], + src_ip: &Ipv4Addr, + dst_ip: &Ipv4Addr, + src_port: u16, + dst_port: u16, + vid: u16, + pcp: u8, + payload: &[u8], + ecn: Option, +) -> bool { + let payload_len = payload.len(); + if packet.len() < VLAN_PACKET_HEADER_SIZE + payload_len { + return false; + } + // write the payload first as it's needed for checksum calculation (if enabled) + packet[VLAN_PACKET_HEADER_SIZE..VLAN_PACKET_HEADER_SIZE + payload_len].copy_from_slice(payload); + write_vlan_eth_header(packet, src_mac, dst_mac, vid, pcp); + write_ip_header_for_udp( + &mut packet[VLAN_ETH_HEADER_SIZE..], + src_ip, + dst_ip, + ecn, + (UDP_HEADER_SIZE + payload_len) as u16, + ); + write_udp_header( + &mut packet[VLAN_ETH_HEADER_SIZE + IP_HEADER_SIZE..], + src_ip, + src_port, + dst_ip, + dst_port, + payload_len as u16, + false, + ); + true +} + pub(crate) fn write_ip_header( packet: &mut [u8], src_ip: &Ipv4Addr, @@ -148,3 +266,168 @@ fn calculate_ip_checksum(header: &[u8]) -> u16 { !(sum as u16) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_write_vlan_eth_header_layout() { + // dst, src, TPID=0x8100, TCI (pcp=0, dei=0, vid=900 -> 0x0384), inner EtherType=0x0800 + let src = [0x02, 0x00, 0x00, 0x00, 0x00, 0x01]; + let dst = [0x01, 0x00, 0x5e, 0x00, 0x00, 0x03]; + let mut buf = [0u8; VLAN_ETH_HEADER_SIZE]; + write_vlan_eth_header(&mut buf, &src, &dst, 900, 0); + assert_eq!(&buf[0..6], &dst); + assert_eq!(&buf[6..12], &src); + assert_eq!(&buf[12..14], &[0x81, 0x00]); + assert_eq!(&buf[14..16], &[0x03, 0x84]); + assert_eq!(&buf[16..18], &[0x08, 0x00]); + } + + #[test] + fn test_write_vlan_eth_header_packs_pcp_and_vid() { + // pcp=5, vid=0x0FFF (max 12-bit value). TCI = (5 << 13) | 0x0FFF = 0xAFFF. + let mut buf = [0u8; VLAN_ETH_HEADER_SIZE]; + write_vlan_eth_header(&mut buf, &[0u8; 6], &[0u8; 6], 0x0FFF, 5); + assert_eq!(&buf[14..16], &[0xAF, 0xFF]); + } + + #[test] + fn test_write_vlan_eth_header_masks_pcp_and_vid_inputs() { + // High bits in pcp and vid must not leak into adjacent fields. + let mut buf = [0u8; VLAN_ETH_HEADER_SIZE]; + write_vlan_eth_header(&mut buf, &[0u8; 6], &[0u8; 6], 0xFFFF, 0xFF); + // pcp masked to 0x7, then shifted into bits 13..16; vid masked to 0x0FFF. + // Expected TCI = (0x7 << 13) | 0x0FFF = 0xEFFF. + assert_eq!(&buf[14..16], &[0xEF, 0xFF]); + } + + #[test] + fn test_construct_packet_layout() { + let src_mac = [0x02, 0x00, 0x00, 0x00, 0x00, 0x01]; + let dst_mac = [0x02, 0x00, 0x00, 0x00, 0x00, 0x02]; + let src_ip = Ipv4Addr::new(10, 228, 0, 5); + let dst_ip = Ipv4Addr::new(10, 228, 0, 9); + let payload = b"hello-world"; + let mut buf = vec![0u8; PACKET_HEADER_SIZE + payload.len()]; + + assert!(construct_packet( + &mut buf, &src_mac, &dst_mac, &src_ip, &dst_ip, 7000, 7733, payload, None, + )); + + // Untagged ethernet header + assert_eq!(&buf[0..6], &dst_mac); + assert_eq!(&buf[6..12], &src_mac); + assert_eq!(&buf[12..14], &[0x08, 0x00]); // ethertype IPv4 + + // IPv4 header sanity: version+IHL byte, protocol=UDP, src/dst. + assert_eq!(buf[ETH_HEADER_SIZE], 0x45); + assert_eq!(buf[ETH_HEADER_SIZE + 9], IPPROTO_UDP as u8); + assert_eq!( + &buf[ETH_HEADER_SIZE + 12..ETH_HEADER_SIZE + 16], + &src_ip.octets() + ); + assert_eq!( + &buf[ETH_HEADER_SIZE + 16..ETH_HEADER_SIZE + 20], + &dst_ip.octets() + ); + + // UDP header sanity: src/dst ports, length. + let udp_off = ETH_HEADER_SIZE + IP_HEADER_SIZE; + assert_eq!(&buf[udp_off..udp_off + 2], &7000u16.to_be_bytes()); + assert_eq!(&buf[udp_off + 2..udp_off + 4], &7733u16.to_be_bytes()); + assert_eq!( + &buf[udp_off + 4..udp_off + 6], + &((UDP_HEADER_SIZE + payload.len()) as u16).to_be_bytes() + ); + + // Payload tail. + assert_eq!(&buf[udp_off + UDP_HEADER_SIZE..], payload.as_slice()); + } + + #[test] + fn test_construct_packet_rejects_undersized_buffer() { + let mut tiny = [0u8; 10]; + let ok = construct_packet( + &mut tiny, + &[0; 6], + &[0; 6], + &Ipv4Addr::UNSPECIFIED, + &Ipv4Addr::UNSPECIFIED, + 0, + 0, + b"too big", + None, + ); + assert!(!ok); + // Buffer must not be partially written. + assert!(tiny.iter().all(|&b| b == 0)); + } + + #[test] + fn test_construct_vlan_packet_layout() { + let src_mac = [0x02, 0x00, 0x00, 0x00, 0x00, 0x01]; + let dst_mac = [0x01, 0x00, 0x5e, 0x00, 0x00, 0x03]; + let src_ip = Ipv4Addr::new(10, 228, 0, 5); + let dst_ip = Ipv4Addr::new(239, 0, 0, 3); + let payload = b"hello-world"; + let mut buf = vec![0u8; VLAN_PACKET_HEADER_SIZE + payload.len()]; + + assert!(construct_vlan_packet( + &mut buf, &src_mac, &dst_mac, &src_ip, &dst_ip, 7000, 7733, 900, 0, payload, None, + )); + + // VLAN-tagged ethernet header + assert_eq!(&buf[0..6], &dst_mac); + assert_eq!(&buf[6..12], &src_mac); + assert_eq!(&buf[12..14], &[0x81, 0x00]); + assert_eq!(&buf[14..16], &[0x03, 0x84]); // vid=900, pcp=0 + assert_eq!(&buf[16..18], &[0x08, 0x00]); // inner ethertype IPv4 + + // IPv4 header sanity: version+IHL byte, protocol=UDP, src/dst. + assert_eq!(buf[VLAN_ETH_HEADER_SIZE], 0x45); + assert_eq!(buf[VLAN_ETH_HEADER_SIZE + 9], IPPROTO_UDP as u8); + assert_eq!( + &buf[VLAN_ETH_HEADER_SIZE + 12..VLAN_ETH_HEADER_SIZE + 16], + &src_ip.octets() + ); + assert_eq!( + &buf[VLAN_ETH_HEADER_SIZE + 16..VLAN_ETH_HEADER_SIZE + 20], + &dst_ip.octets() + ); + + // UDP header sanity: src/dst ports, length. + let udp_off = VLAN_ETH_HEADER_SIZE + IP_HEADER_SIZE; + assert_eq!(&buf[udp_off..udp_off + 2], &7000u16.to_be_bytes()); + assert_eq!(&buf[udp_off + 2..udp_off + 4], &7733u16.to_be_bytes()); + assert_eq!( + &buf[udp_off + 4..udp_off + 6], + &((UDP_HEADER_SIZE + payload.len()) as u16).to_be_bytes() + ); + + // Payload tail. + assert_eq!(&buf[udp_off + UDP_HEADER_SIZE..], payload.as_slice()); + } + + #[test] + fn test_construct_vlan_packet_rejects_undersized_buffer() { + let mut tiny = [0u8; 10]; + let ok = construct_vlan_packet( + &mut tiny, + &[0; 6], + &[0; 6], + &Ipv4Addr::UNSPECIFIED, + &Ipv4Addr::UNSPECIFIED, + 0, + 0, + 1, + 0, + b"too big", + None, + ); + assert!(!ok); + // Buffer must not be partially written. + assert!(tiny.iter().all(|&b| b == 0)); + } +} diff --git a/xdp/src/route.rs b/xdp/src/route.rs index bc57e7e8ce5..31636957cfd 100644 --- a/xdp/src/route.rs +++ b/xdp/src/route.rs @@ -40,6 +40,22 @@ pub struct GreRouteInfo { pub mac_addr: MacAddress, } +/// VLAN encapsulation directive carried on a resolved NextHop. +/// +/// When present, the tx loop prepends an 802.1Q tag to the Ethernet header before the inner IP +/// header. The MAC and IP fields on the NextHop are still the real packet destinations: VLAN +/// encapsulation changes the L2 framing only. XDP cannot transmit on a VLAN sub-interface +/// directly (it is bound to the physical parent), so the tag has to be written in userspace. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct VlanRouteInfo { + /// if_index of the VLAN sub-interface the route resolved to. + pub if_index: u32, + pub vid: u16, + /// Priority Code Point. Always 0 for now, matching the kernel's default egress QoS mapping; + /// a configurable mapping can be added later if needed. + pub pcp: u8, +} + #[derive(Debug, Clone)] pub struct NextHop { pub mac_addr: Option, @@ -48,6 +64,7 @@ pub struct NextHop { pub mtu: u32, pub preferred_src_ip: Option, pub gre: Option, + pub vlan: Option, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -406,6 +423,8 @@ pub struct Router { cached_default_route: Option, // cache for gre route info keyed by interface index. This stays tiny in practice. cached_gre_info: Vec, + // cache for vlan encap info keyed by interface index. One entry per VLAN sub-interface. + cached_vlan_info: Vec, } struct RoutingTableDisplay<'a>(&'a [Route]); @@ -441,6 +460,7 @@ impl Router { interfaces: tables.interfaces, cached_default_route: None, cached_gre_info: Vec::new(), + cached_vlan_info: Vec::new(), }; let mut has_gre_interface = false; @@ -451,6 +471,14 @@ impl Router { router.cached_gre_info.push(gre); } } + if let Some(vlan) = interface.vlan_link { + router.cached_vlan_info.push(VlanRouteInfo { + if_index: interface.if_index, + vid: vlan.vid, + // Kernel default egress QoS mapping; a configurable mapping can come later. + pcp: 0, + }); + } } if router.cached_gre_info.is_empty() && has_gre_interface { warn!("GRE cache: GRE interface(s) present but none with valid remote resolved"); @@ -487,6 +515,13 @@ impl Router { self.interface_gre_route_info(interface) } + fn cached_vlan_route_info(&self, if_index: u32) -> Option { + self.cached_vlan_info + .iter() + .find(|vlan| vlan.if_index == if_index) + .copied() + } + fn resolve_next_hop( &self, route_ip: Ipv4Addr, @@ -507,6 +542,7 @@ impl Router { mac_addr: default_route.mac_addr, preferred_src_ip, gre: default_route.gre.clone(), + vlan: default_route.vlan, }); } } @@ -519,6 +555,7 @@ impl Router { mac_addr: Some(gre.mac_addr), preferred_src_ip, gre: Some(gre), + vlan: None, }); } @@ -528,6 +565,7 @@ impl Router { .find(|interface| interface.if_index == if_index) .map(|interface| interface.mtu) .ok_or(RouteError::UnknownInterfaceIndex(if_index))?; + let vlan = self.cached_vlan_route_info(if_index); let mac_addr = self.neighbors.lookup(next_hop_ip, if_index).cloned(); Ok(NextHop { @@ -537,6 +575,7 @@ impl Router { mtu, preferred_src_ip, gre: None, + vlan, }) } @@ -642,7 +681,7 @@ fn format_route(f: &mut fmt::Formatter<'_>, route: &Route) -> fmt::Res mod tests { use { super::*, - crate::netlink::{MacAddress, NeighborEntry, RouteEntry}, + crate::netlink::{MacAddress, NeighborEntry, RouteEntry, VlanLinkInfo}, libc::{AF_INET, NUD_REACHABLE}, std::net::{IpAddr, Ipv4Addr}, }; @@ -723,11 +762,13 @@ mod tests { if_index: 1, mtu: DEFAULT_MTU_FOR_TESTS, gre_tunnel: None, + vlan_link: None, })); assert!(tables.upsert_interface(InterfaceInfo { if_index: 2, mtu: DEFAULT_MTU_FOR_TESTS, gre_tunnel: None, + vlan_link: None, })); assert!(tables.upsert_neighbor(NeighborEntry { destination: Some(IpAddr::V4(primary_gateway)), @@ -769,6 +810,7 @@ mod tests { if_index: 1, mtu: DEFAULT_MTU_FOR_TESTS, gre_tunnel: None, + vlan_link: None, })); let gateway = Ipv4Addr::new(10, 255, 255, 1); @@ -852,11 +894,13 @@ mod tests { if_index: 2, mtu: DEFAULT_MTU_FOR_TESTS, gre_tunnel: None, + vlan_link: None, }, InterfaceInfo { if_index: 3, mtu: DEFAULT_MTU_FOR_TESTS, gre_tunnel: None, + vlan_link: None, }, ], ); @@ -904,6 +948,7 @@ mod tests { if_index: test_if_index, mtu: DEFAULT_MTU_FOR_TESTS, gre_tunnel: None, + vlan_link: None, }; // Upsert new interface and check that it was inserted @@ -950,6 +995,7 @@ mod tests { if_index: 1, mtu: DEFAULT_MTU_FOR_TESTS, gre_tunnel: None, + vlan_link: None, })); assert!(tables.upsert_neighbor(NeighborEntry { destination: Some(IpAddr::V4(main_gateway)), @@ -990,6 +1036,7 @@ mod tests { if_index: 1, mtu: DEFAULT_MTU_FOR_TESTS, gre_tunnel: None, + vlan_link: None, })); assert!(tables.upsert_neighbor(NeighborEntry { destination: Some(IpAddr::V4(main_gateway)), @@ -1058,6 +1105,7 @@ mod tests { if_index: if_index_underlay as u32, mtu: DEFAULT_MTU_FOR_TESTS, gre_tunnel: None, + vlan_link: None, }, InterfaceInfo { if_index: if_index_gre1 as u32, @@ -1069,6 +1117,7 @@ mod tests { tos: 0, pmtudisc: 0, }), + vlan_link: None, }, InterfaceInfo { if_index: if_index_gre2 as u32, @@ -1080,6 +1129,7 @@ mod tests { tos: 0, pmtudisc: 0, }), + vlan_link: None, }, ]; @@ -1140,6 +1190,7 @@ mod tests { if_index: if_index_underlay as u32, mtu: DEFAULT_MTU_FOR_TESTS, gre_tunnel: None, + vlan_link: None, }, InterfaceInfo { if_index: if_index_gre as u32, @@ -1151,6 +1202,7 @@ mod tests { tos: 0, pmtudisc: 0, }), + vlan_link: None, }, ]; @@ -1189,6 +1241,7 @@ mod tests { if_index: 1, mtu: DEFAULT_MTU_FOR_TESTS, gre_tunnel: None, + vlan_link: None, })); assert!(tables.upsert_neighbor(NeighborEntry { destination: Some(IpAddr::V4(default_gateway)), @@ -1271,4 +1324,146 @@ mod tests { assert_eq!(next_hop.if_index, 2); assert_eq!(next_hop.ip_addr, IpAddr::V4(backup)); } + + #[test] + fn test_vlan_route_resolves_with_vlan_info() { + // A route through a configured VLAN sub-interface attaches VlanRouteInfo to the NextHop; + // the MAC still comes from the neighbor cache and no GRE encap is set. + let peer = Ipv4Addr::new(10, 228, 0, 99); + let vlan_if = 100u32; + let vlan_src = Ipv4Addr::new(10, 228, 0, 5); + let peer_mac = MacAddress([0x02, 0xaa, 0xbb, 0xcc, 0xdd, 0x09]); + + let neighbors = vec![NeighborEntry { + destination: Some(IpAddr::V4(peer)), + lladdr: Some(peer_mac), + ifindex: vlan_if as i32, + state: NUD_REACHABLE, + }]; + let interfaces = vec![InterfaceInfo { + if_index: vlan_if, + mtu: DEFAULT_MTU_FOR_TESTS, + gre_tunnel: None, + vlan_link: Some(VlanLinkInfo { vid: 900 }), + }]; + let routes = vec![Route { + destination: Some(Ipv4Addr::new(10, 228, 0, 0)), + gateway: None, + preferred_src: Some(vlan_src), + out_if_index: Some(vlan_if), + priority: None, + type_: 0, + dst_len: 22, + }]; + + let router = router_from_tables(neighbors, routes, interfaces); + let next_hop = router.route_v4(peer).unwrap(); + assert_eq!(next_hop.if_index, vlan_if); + assert_eq!(next_hop.preferred_src_ip, Some(vlan_src)); + assert_eq!(next_hop.mac_addr, Some(peer_mac)); + assert!(next_hop.gre.is_none()); + let vlan = next_hop.vlan.expect("vlan info must be populated"); + assert_eq!(vlan.if_index, vlan_if); + assert_eq!(vlan.vid, 900); + assert_eq!(vlan.pcp, 0); + } + + #[test] + fn test_multiple_vlan_interfaces_resolve_their_own_vid() { + // With more than one VLAN sub-interface, each route is tagged with the vid of the + // interface it egresses through. + let peer_a = Ipv4Addr::new(10, 1, 0, 9); + let peer_b = Ipv4Addr::new(10, 2, 0, 9); + let vlan_if_a = 100u32; + let vlan_if_b = 101u32; + let mac_a = MacAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x0a]); + let mac_b = MacAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x0b]); + + let neighbors = vec![ + NeighborEntry { + destination: Some(IpAddr::V4(peer_a)), + lladdr: Some(mac_a), + ifindex: vlan_if_a as i32, + state: NUD_REACHABLE, + }, + NeighborEntry { + destination: Some(IpAddr::V4(peer_b)), + lladdr: Some(mac_b), + ifindex: vlan_if_b as i32, + state: NUD_REACHABLE, + }, + ]; + let interfaces = vec![ + InterfaceInfo { + if_index: vlan_if_a, + mtu: DEFAULT_MTU_FOR_TESTS, + gre_tunnel: None, + vlan_link: Some(VlanLinkInfo { vid: 900 }), + }, + InterfaceInfo { + if_index: vlan_if_b, + mtu: DEFAULT_MTU_FOR_TESTS, + gre_tunnel: None, + vlan_link: Some(VlanLinkInfo { vid: 901 }), + }, + ]; + let routes = vec![ + test_route(Some(peer_a), vlan_if_a), + test_route(Some(peer_b), vlan_if_b), + ]; + + let router = router_from_tables(neighbors, routes, interfaces); + let hop_a = router.route_v4(peer_a).unwrap(); + assert_eq!(hop_a.vlan.map(|v| v.vid), Some(900)); + assert_eq!(hop_a.mac_addr, Some(mac_a)); + let hop_b = router.route_v4(peer_b).unwrap(); + assert_eq!(hop_b.vlan.map(|v| v.vid), Some(901)); + assert_eq!(hop_b.mac_addr, Some(mac_b)); + } + + #[test] + fn test_gre_wins_when_both_gre_and_vlan_configured() { + // Defensive: if a configured VLAN if_index also happens to be a GRE interface, the resolve + // path picks GRE first (single-encap-per-interface contract). + let remote = Ipv4Addr::new(10, 0, 0, 1); + let gre_dest = Ipv4Addr::new(192, 168, 0, 1); + let if_index_underlay = 1u32; + let if_index_iface = 100u32; + let underlay_mac = MacAddress([0x02, 0xaa, 0xbb, 0xcc, 0xdd, 0x01]); + + let neighbors = vec![NeighborEntry { + destination: Some(IpAddr::V4(remote)), + lladdr: Some(underlay_mac), + ifindex: if_index_underlay as i32, + state: NUD_REACHABLE, + }]; + let routes = vec![ + test_route(Some(remote), if_index_underlay), + test_route(Some(gre_dest), if_index_iface), + ]; + let interfaces = vec![ + InterfaceInfo { + if_index: if_index_underlay, + mtu: DEFAULT_MTU_FOR_TESTS, + gre_tunnel: None, + vlan_link: None, + }, + InterfaceInfo { + if_index: if_index_iface, + mtu: DEFAULT_MTU_FOR_TESTS, + gre_tunnel: Some(GreTunnelInfo { + local: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 3)), + remote: IpAddr::V4(remote), + ttl: 0, + tos: 0, + pmtudisc: 0, + }), + vlan_link: Some(VlanLinkInfo { vid: 5 }), + }, + ]; + let router = router_from_tables(neighbors, routes, interfaces); + let next_hop = router.route_v4(gre_dest).unwrap(); + assert!(next_hop.gre.is_some()); + assert!(next_hop.vlan.is_none()); + } } diff --git a/xdp/src/tx_loop.rs b/xdp/src/tx_loop.rs index 80f383628c6..dfa3c8bae47 100644 --- a/xdp/src/tx_loop.rs +++ b/xdp/src/tx_loop.rs @@ -10,8 +10,8 @@ use { }, netlink::MacAddress, packet::{ - ETH_HEADER_SIZE, IP_HEADER_SIZE, UDP_HEADER_SIZE, write_eth_header, - write_ip_header_for_udp, write_udp_header, + IP_HEADER_SIZE, PACKET_HEADER_SIZE, UDP_HEADER_SIZE, VLAN_PACKET_HEADER_SIZE, + construct_packet, construct_vlan_packet, }, route::NextHop, socket::{Socket, Tx, TxRing}, @@ -391,6 +391,72 @@ impl TxLoop { umem.release(frame.offset()); continue; } + } else if let Some(vlan) = &next_hop.vlan { + // we need the MAC address to send the packet + let Some(dest_mac) = next_hop.mac_addr else { + log::warn!( + "dropping packet: peer {addr} must be routed through {} which has \ + no known MAC address", + next_hop.ip_addr + ); + batched_packets -= 1; + umem.release(frame.offset()); + continue; + }; + + // The 802.1Q tag is added at L2, so the L3 size compared against the MTU + // is the same as the untagged path. + let l3_packet_len = IP_HEADER_SIZE + UDP_HEADER_SIZE + len; + if l3_packet_len > next_hop.mtu as usize { + if !can_overflow_mtu { + log::warn!( + "dropping packet: packet size {l3_packet_len} exceeds MTU \ + {mtu} for {addr}", + mtu = next_hop.mtu + ); + } + batched_packets -= 1; + umem.release(frame.offset()); + continue; + } + + let packet_len = VLAN_PACKET_HEADER_SIZE + len; + if packet_len > umem_frame_size { + log::warn!( + "dropping packet: VLAN packet size {packet_len} exceeds frame \ + size {umem_frame_size} for {addr}" + ); + batched_packets -= 1; + umem.release(frame.offset()); + continue; + } + + frame.set_len(packet_len); + let packet = umem.map_frame_mut(&frame); + + // The route's preferred src is the IP assigned to the VLAN sub-interface, + // which is the right inner src for traffic egressing this VLAN. Fall back + // to the device's src IP if the route did not carry one. + let inner_src_ip = next_hop.preferred_src_ip.as_ref().unwrap_or(src_ip); + + if !construct_vlan_packet( + packet, + &src_mac.0, + &dest_mac.0, + inner_src_ip, + &dst_ip, + src_port, + addr.port(), + vlan.vid, + vlan.pcp, + payload, + ecn, + ) { + log::warn!("dropping packet: VLAN frame did not fit in UMEM slot"); + batched_packets -= 1; + umem.release(frame.offset()); + continue; + } } else { // we need the MAC address to send the packet let Some(dest_mac) = next_hop.mac_addr else { @@ -418,8 +484,6 @@ impl TxLoop { continue; } - const PACKET_HEADER_SIZE: usize = - ETH_HEADER_SIZE + IP_HEADER_SIZE + UDP_HEADER_SIZE; let packet_len = PACKET_HEADER_SIZE + len; if packet_len > umem_frame_size { log::warn!( @@ -434,29 +498,22 @@ impl TxLoop { frame.set_len(packet_len); let packet = umem.map_frame_mut(&frame); - // write the payload first as it's needed for checksum calculation (if enabled) - packet[PACKET_HEADER_SIZE..][..len].copy_from_slice(payload); - - write_eth_header(packet, &src_mac.0, &dest_mac.0); - - write_ip_header_for_udp( - &mut packet[ETH_HEADER_SIZE..], + if !construct_packet( + packet, + &src_mac.0, + &dest_mac.0, src_ip, &dst_ip, - ecn, - (UDP_HEADER_SIZE + len) as u16, - ); - - write_udp_header( - &mut packet[ETH_HEADER_SIZE + IP_HEADER_SIZE..], - src_ip, src_port, - &dst_ip, addr.port(), - len as u16, - // don't do checksums - false, - ); + payload, + ecn, + ) { + log::warn!("dropping packet: frame did not fit in UMEM slot"); + batched_packets -= 1; + umem.release(frame.offset()); + continue; + } } ring.write(frame, 0) From 14f333ed727c66782b827ea93cee8d3c70e7ba6b Mon Sep 17 00:00:00 2001 From: Mircea Colonescu Date: Mon, 15 Jun 2026 09:22:50 -0400 Subject: [PATCH 337/576] refactor(ci): port channel-info.sh to xtask (#12299) * refactor(ci): add channel-info auto-resolver to xtask Adds `cargo xtask channel-info` that derives EDGE/BETA/STABLE channels from each `vX.Y` branch's workspace.package.version (stage gating on the top branch). Pin-file path (`ci/channel-info.sh`) stays authoritative; the new resolver runs alongside it via a soft-failing `channel-info-divergence` buildkite step that diffs both outputs and annotates on mismatch. Per #12450 sequencing step 1: observe parity over a window before cutting over and removing the pin override. * refactor(ci): parse workspace version via semver serde * refactor(ci): replace Xy tuple with BranchVersion struct * refactor(ci): drop EnvInputs, pass branch/channel directly * refactor(ci): use git ls-remote + reqwest for channel-info * style(ci): cargo fmt --- ci/test-channel-info-divergence.sh | 50 +++ ci/xtask/Cargo.lock | 285 ++++++++++++++++- ci/xtask/Cargo.toml | 3 + ci/xtask/src/commands.rs | 1 + ci/xtask/src/commands/channel_info.rs | 40 +++ ci/xtask/src/commands/channel_info/fetch.rs | 86 ++++++ ci/xtask/src/commands/channel_info/resolve.rs | 290 ++++++++++++++++++ ci/xtask/src/commands/generate_pipeline.rs | 18 ++ ci/xtask/src/main.rs | 5 + 9 files changed, 763 insertions(+), 15 deletions(-) create mode 100755 ci/test-channel-info-divergence.sh create mode 100644 ci/xtask/src/commands/channel_info.rs create mode 100644 ci/xtask/src/commands/channel_info/fetch.rs create mode 100644 ci/xtask/src/commands/channel_info/resolve.rs diff --git a/ci/test-channel-info-divergence.sh b/ci/test-channel-info-divergence.sh new file mode 100755 index 00000000000..8d92e481ddb --- /dev/null +++ b/ci/test-channel-info-divergence.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# +# Runs `ci/channel-info.sh` (current authoritative source) and +# `cargo xtask channel-info` (auto-resolver), diffs their output, and +# annotates the build on mismatch. Never blocks the pipeline. + +set -u +cd "$(dirname "$0")/.." || exit 1 + +bash_out=$(ci/channel-info.sh 2>&1) || { + echo "ci/channel-info.sh failed (exit $?); skipping divergence check" >&2 + echo "$bash_out" >&2 + exit 0 +} + +xtask_out=$(cargo run --quiet --manifest-path ci/xtask/Cargo.toml -- channel-info 2>&1) || { + echo "cargo xtask channel-info failed (exit $?); skipping divergence check" >&2 + echo "$xtask_out" >&2 + exit 0 +} + +bash_sorted=$(printf '%s\n' "$bash_out" | LC_ALL=C sort) +xtask_sorted=$(printf '%s\n' "$xtask_out" | LC_ALL=C sort) + +if [[ "$bash_sorted" == "$xtask_sorted" ]]; then + echo "channel-info parity OK" + exit 0 +fi + +echo "channel-info divergence detected" >&2 +echo "--- bash (authoritative)" >&2 +echo "$bash_out" >&2 +echo "--- xtask" >&2 +echo "$xtask_out" >&2 + +if command -v buildkite-agent >/dev/null 2>&1; then + { + echo "**channel-info parity divergence**" + echo + echo '```' + echo "bash (authoritative):" + echo "$bash_out" + echo + echo "xtask:" + echo "$xtask_out" + echo '```' + } | buildkite-agent annotate --style warning --context channel-info-divergence || true +fi + +exit 0 diff --git a/ci/xtask/Cargo.lock b/ci/xtask/Cargo.lock index 23d413ac0fb..2eebff23442 100644 --- a/ci/xtask/Cargo.lock +++ b/ci/xtask/Cargo.lock @@ -211,6 +211,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.44" @@ -334,7 +340,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array", - "rand_core", + "rand_core 0.6.4", "subtle", "zeroize", ] @@ -478,7 +484,7 @@ dependencies = [ "hkdf", "pem-rfc7468", "pkcs8", - "rand_core", + "rand_core 0.6.4", "sec1", "subtle", "zeroize", @@ -519,7 +525,7 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -656,6 +662,20 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + [[package]] name = "globset" version = "0.4.18" @@ -676,7 +696,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff", - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -796,6 +816,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", + "webpki-roots", ] [[package]] @@ -817,13 +838,16 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ + "base64", "bytes", "futures-channel", "futures-util", "http", "http-body", "hyper", + "ipnet", "libc", + "percent-encoding", "pin-project-lite", "socket2", "tokio", @@ -983,6 +1007,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + [[package]] name = "iri-string" version = "0.7.10" @@ -1047,13 +1077,13 @@ checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" dependencies = [ "base64", "ed25519-dalek", - "getrandom", + "getrandom 0.2.17", "hmac", "js-sys", "p256", "p384", "pem", - "rand", + "rand 0.8.6", "rsa", "serde", "serde_json", @@ -1104,6 +1134,12 @@ version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "113b30b4cd05f7c06868fdb2854f66a7b9fece9a48425351cd532e810d74024f" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "memchr" version = "2.7.6" @@ -1142,7 +1178,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand", + "rand 0.8.6", "smallvec", "zeroize", ] @@ -1199,7 +1235,7 @@ dependencies = [ "futures", "futures-core", "futures-util", - "getrandom", + "getrandom 0.2.17", "http", "http-body", "http-body-util", @@ -1436,6 +1472,61 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.4", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + [[package]] name = "quote" version = "1.0.45" @@ -1445,6 +1536,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "rand" version = "0.8.6" @@ -1452,8 +1549,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", ] [[package]] @@ -1463,7 +1570,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", ] [[package]] @@ -1472,7 +1589,16 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", ] [[package]] @@ -1513,6 +1639,44 @@ version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + [[package]] name = "rfc6979" version = "0.4.0" @@ -1531,7 +1695,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", @@ -1550,13 +1714,19 @@ dependencies = [ "num-traits", "pkcs1", "pkcs8", - "rand_core", + "rand_core 0.6.4", "signature", "spki", "subtle", "zeroize", ] +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + [[package]] name = "rustc_version" version = "0.4.1" @@ -1599,6 +1769,7 @@ version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ + "web-time", "zeroize", ] @@ -1826,7 +1997,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -1934,6 +2105,9 @@ name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -2007,6 +2181,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.52.3" @@ -2058,6 +2247,21 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + [[package]] name = "toml_datetime" version = "0.7.5+spec-1.1.0" @@ -2263,6 +2467,15 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.114" @@ -2276,6 +2489,20 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.114" @@ -2308,6 +2535,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "web-time" version = "1.1.0" @@ -2319,6 +2556,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "winapi-util" version = "0.1.11" @@ -2552,6 +2798,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "writeable" version = "0.6.2" @@ -2570,10 +2822,13 @@ dependencies = [ "octocrab", "pretty_assertions", "regex", + "reqwest", + "semver", "serde", "serde_json", "serde_yaml", "tokio", + "toml", "xtask 0.1.0 (git+https://github.com/anza-xyz/xtask?rev=a9b0cf5facf85eb11458bbefc651132722c18dae)", ] diff --git a/ci/xtask/Cargo.toml b/ci/xtask/Cargo.toml index 35f4e7ecf4c..3edc84995d7 100644 --- a/ci/xtask/Cargo.toml +++ b/ci/xtask/Cargo.toml @@ -26,10 +26,13 @@ futures-util = "0.3.31" log = "0.4.28" octocrab = { version = "0.53.0", features = ["stream"] } regex = "1.12.3" +reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } +semver = { version = "1.0.27", features = ["serde"] } serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.145" serde_yaml = "0.9.34" tokio = { version = "1.52.3", features = ["full"] } +toml = "0.9" xtask_shared = { package = "xtask", git = "https://github.com/anza-xyz/xtask", rev = "a9b0cf5facf85eb11458bbefc651132722c18dae" } [dev-dependencies] diff --git a/ci/xtask/src/commands.rs b/ci/xtask/src/commands.rs index 3645e1472c6..b3adf4fb07b 100644 --- a/ci/xtask/src/commands.rs +++ b/ci/xtask/src/commands.rs @@ -1,2 +1,3 @@ +pub mod channel_info; pub mod generate_pipeline; pub mod hello; diff --git a/ci/xtask/src/commands/channel_info.rs b/ci/xtask/src/commands/channel_info.rs new file mode 100644 index 00000000000..33f8ca5c337 --- /dev/null +++ b/ci/xtask/src/commands/channel_info.rs @@ -0,0 +1,40 @@ +mod fetch; +mod resolve; + +use { + anyhow::Result, + futures_util::future::try_join_all, + resolve::{BranchVersion, derive_channels, print_channel_info}, + semver::Version, + std::{collections::BTreeMap, env}, +}; + +pub async fn run() -> Result<()> { + let mut heads = fetch::release_heads()?; + let tags = fetch::release_tags()?; + + heads.sort(); + heads.reverse(); + heads.truncate(3); + + let client = reqwest::Client::new(); + let fetched = try_join_all( + heads + .iter() + .copied() + .map(|bv| fetch::workspace_version(&client, bv)), + ) + .await?; + let versions: BTreeMap = heads.into_iter().zip(fetched).collect(); + + let branch = pick_env("CI_BASE_BRANCH").or_else(|| pick_env("CI_BRANCH")); + let channel = pick_env("CHANNEL"); + let info = derive_channels(&versions, &tags, branch.as_deref(), channel.as_deref())?; + print_channel_info(&info); + + Ok(()) +} + +fn pick_env(key: &str) -> Option { + env::var(key).ok().filter(|v| !v.is_empty()) +} diff --git a/ci/xtask/src/commands/channel_info/fetch.rs b/ci/xtask/src/commands/channel_info/fetch.rs new file mode 100644 index 00000000000..0c4bd102fdd --- /dev/null +++ b/ci/xtask/src/commands/channel_info/fetch.rs @@ -0,0 +1,86 @@ +use { + super::resolve::BranchVersion, + anyhow::{Result, anyhow, bail}, + semver::Version, + serde::Deserialize, + std::process::Command, +}; + +const REMOTE: &str = "https://github.com/anza-xyz/agave.git"; +const RAW_BASE: &str = "https://raw.githubusercontent.com/anza-xyz/agave"; + +fn ls_remote(flag: &str) -> Result> { + let output = Command::new("git") + .args(["ls-remote", flag, REMOTE]) + .output() + .map_err(|e| anyhow!("failed to invoke `git ls-remote`: {e}"))?; + if !output.status.success() { + bail!( + "`git ls-remote {flag} {REMOTE}` failed: {}", + String::from_utf8_lossy(&output.stderr).trim(), + ); + } + let stdout = String::from_utf8(output.stdout) + .map_err(|e| anyhow!("`git ls-remote` stdout is not utf-8: {e}"))?; + Ok(stdout.lines().map(str::to_owned).collect()) +} + +fn strip_ref(line: &str, prefix: &str) -> Option { + let (_sha, refname) = line.split_once('\t')?; + refname.strip_prefix(prefix).map(str::to_owned) +} + +pub fn release_heads() -> Result> { + let lines = ls_remote("--heads")?; + Ok(lines + .iter() + .filter_map(|l| strip_ref(l, "refs/heads/")) + .filter_map(|name| name.parse::().ok()) + .collect()) +} + +pub fn release_tags() -> Result> { + let lines = ls_remote("--tags")?; + Ok(lines + .iter() + .filter_map(|l| strip_ref(l, "refs/tags/")) + .filter_map(|name| { + let stripped = name.strip_prefix('v')?; + let v = Version::parse(stripped).ok()?; + (v.pre.is_empty() && v.build.is_empty()).then_some(v) + }) + .collect()) +} + +#[derive(Deserialize)] +struct CargoToml { + workspace: WorkspaceSection, +} + +#[derive(Deserialize)] +struct WorkspaceSection { + package: PackageSection, +} + +#[derive(Deserialize)] +struct PackageSection { + version: Version, +} + +pub async fn workspace_version(client: &reqwest::Client, bv: BranchVersion) -> Result { + let url = format!("{RAW_BASE}/{bv}/Cargo.toml"); + let resp = client + .get(&url) + .send() + .await + .map_err(|e| anyhow!("GET {url}: {e}"))? + .error_for_status() + .map_err(|e| anyhow!("GET {url}: {e}"))?; + let raw = resp + .text() + .await + .map_err(|e| anyhow!("read body for {url}: {e}"))?; + let parsed: CargoToml = + toml::from_str(&raw).map_err(|e| anyhow!("failed to parse Cargo.toml at {url}: {e}"))?; + Ok(parsed.workspace.package.version) +} diff --git a/ci/xtask/src/commands/channel_info/resolve.rs b/ci/xtask/src/commands/channel_info/resolve.rs new file mode 100644 index 00000000000..40cac08db06 --- /dev/null +++ b/ci/xtask/src/commands/channel_info/resolve.rs @@ -0,0 +1,290 @@ +use { + anyhow::{Result, anyhow, bail}, + semver::Version, + std::{collections::BTreeMap, fmt, str::FromStr}, +}; + +/// `vX.Y` release-line identifier. +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)] +pub struct BranchVersion { + pub major: u64, + pub minor: u64, +} + +impl FromStr for BranchVersion { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let rest = s + .strip_prefix('v') + .ok_or_else(|| anyhow!("missing `v` prefix in `{s}`"))?; + let (maj, min) = rest + .split_once('.') + .ok_or_else(|| anyhow!("missing `.` separator in `{s}`"))?; + Ok(Self { + major: maj.parse()?, + minor: min.parse()?, + }) + } +} + +impl fmt::Display for BranchVersion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "v{}.{}", self.major, self.minor) + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum Stage { + Alpha, + Beta, + Rc, + Ga, +} + +pub fn stage_of(v: &Version) -> Result { + if v.pre.is_empty() { + return Ok(Stage::Ga); + } + + let label = v.pre.as_str().split('.').next().unwrap_or(""); + + match label { + "alpha" => Ok(Stage::Alpha), + "beta" => Ok(Stage::Beta), + "rc" => Ok(Stage::Rc), + other => bail!("unknown prerelease label `{other}` in version {v}"), + } +} + +#[derive(Debug)] +pub struct ChannelInfo { + pub edge_channel: String, + pub beta_channel: String, + pub beta_channel_latest_tag: String, + pub stable_channel: String, + pub stable_channel_latest_tag: String, + pub channel: String, + pub channel_latest_tag: String, +} + +pub fn derive_channels( + versions: &BTreeMap, + tags: &[Version], + branch: Option<&str>, + channel: Option<&str>, +) -> Result { + for (bv, v) in versions { + if v.major != bv.major || v.minor != bv.minor { + bail!("branch {bv} workspace version {v} does not match branch (major, minor)"); + } + } + + let sorted: Vec = versions.keys().rev().copied().collect(); + let h1 = sorted.first().copied(); + let h2 = sorted.get(1).copied(); + let h3 = sorted.get(2).copied(); + + let h1_stage = h1 + .map(|bv| stage_of(versions.get(&bv).expect("h1 is in versions"))) + .transpose()?; + + let (beta, stable) = match h1_stage { + Some(Stage::Alpha) => (h2, h3), + Some(_) => (h1, h2), + None => (None, None), + }; + + let beta = beta.ok_or_else(|| anyhow!("no BETA-eligible vX.Y head"))?; + + if let Some(s) = stable + && s >= beta + { + bail!("STABLE {s} is not less than BETA {beta}"); + } + + let edge_channel = "master".to_string(); + let beta_channel = beta.to_string(); + let stable_channel = stable.map(|s| s.to_string()).unwrap_or_default(); + let beta_channel_latest_tag = latest_tag_for(tags, beta) + .map(|v| format!("v{v}")) + .unwrap_or_default(); + let stable_channel_latest_tag = stable + .and_then(|xy| latest_tag_for(tags, xy)) + .map(|v| format!("v{v}")) + .unwrap_or_default(); + + let channel = channel.map(str::to_owned).unwrap_or_else(|| match branch { + Some(b) if b == stable_channel => "stable".into(), + Some(b) if b == edge_channel => "edge".into(), + Some(b) if b == beta_channel => "beta".into(), + _ => String::new(), + }); + + let channel_latest_tag = match channel.as_str() { + "beta" => beta_channel_latest_tag.clone(), + "stable" => stable_channel_latest_tag.clone(), + _ => String::new(), + }; + + Ok(ChannelInfo { + edge_channel, + beta_channel, + beta_channel_latest_tag, + stable_channel, + stable_channel_latest_tag, + channel, + channel_latest_tag, + }) +} + +fn latest_tag_for(tags: &[Version], bv: BranchVersion) -> Option<&Version> { + tags.iter() + .filter(|t| t.major == bv.major && t.minor == bv.minor) + .max() +} + +pub fn print_channel_info(info: &ChannelInfo) { + println!("EDGE_CHANNEL={}", info.edge_channel); + println!("BETA_CHANNEL={}", info.beta_channel); + println!("BETA_CHANNEL_LATEST_TAG={}", info.beta_channel_latest_tag); + println!("STABLE_CHANNEL={}", info.stable_channel); + println!( + "STABLE_CHANNEL_LATEST_TAG={}", + info.stable_channel_latest_tag + ); + println!("CHANNEL={}", info.channel); + println!("CHANNEL_LATEST_TAG={}", info.channel_latest_tag); +} + +#[cfg(test)] +mod tests { + use super::*; + + fn v(s: &str) -> Version { + Version::parse(s).expect("valid version") + } + + fn bv(major: u64, minor: u64) -> BranchVersion { + BranchVersion { major, minor } + } + + fn versions(pairs: &[(BranchVersion, &str)]) -> BTreeMap { + pairs.iter().map(|(b, s)| (*b, v(s))).collect() + } + + #[test] + fn promotes_top_when_top_is_beta() { + let vs = versions(&[(bv(4, 0), "4.0.0"), (bv(4, 1), "4.1.0-beta.0")]); + + let info = derive_channels(&vs, &[], None, None).unwrap(); + + assert_eq!(info.beta_channel, "v4.1"); + assert_eq!(info.stable_channel, "v4.0"); + } + + #[test] + fn promotes_top_when_top_is_ga() { + let vs = versions(&[(bv(4, 0), "4.0.5"), (bv(4, 1), "4.1.0")]); + + let info = derive_channels(&vs, &[], None, None).unwrap(); + + assert_eq!(info.beta_channel, "v4.1"); + assert_eq!(info.stable_channel, "v4.0"); + } + + #[test] + fn holds_channels_when_top_is_alpha() { + let vs = versions(&[ + (bv(3, 1), "3.1.15"), + (bv(4, 0), "4.0.0"), + (bv(4, 1), "4.1.0-alpha.0"), + ]); + + let info = derive_channels(&vs, &[], None, None).unwrap(); + + assert_eq!(info.beta_channel, "v4.0"); + assert_eq!(info.stable_channel, "v3.1"); + } + + #[test] + fn rc_top_is_promoted() { + let vs = versions(&[(bv(4, 0), "4.0.10"), (bv(4, 1), "4.1.0-rc.2")]); + + let info = derive_channels(&vs, &[], None, None).unwrap(); + + assert_eq!(info.beta_channel, "v4.1"); + assert_eq!(info.stable_channel, "v4.0"); + } + + #[test] + fn rejects_mismatched_workspace_version() { + let vs = versions(&[(bv(4, 0), "5.0.0")]); + + let err = derive_channels(&vs, &[], None, None).unwrap_err(); + + assert!(err.to_string().contains("does not match branch")); + } + + #[test] + fn rejects_unknown_prerelease_label() { + let vs = versions(&[(bv(4, 0), "4.0.0"), (bv(4, 1), "4.1.0-dev.0")]); + + let err = derive_channels(&vs, &[], None, None).unwrap_err(); + + assert!(err.to_string().contains("unknown prerelease label")); + } + + #[test] + fn rejects_when_only_alpha_top_and_nothing_below() { + let vs = versions(&[(bv(4, 1), "4.1.0-alpha.0")]); + + let err = derive_channels(&vs, &[], None, None).unwrap_err(); + + assert!(err.to_string().contains("no BETA-eligible")); + } + + #[test] + fn rejects_empty() { + let err = derive_channels(&BTreeMap::new(), &[], None, None).unwrap_err(); + + assert!(err.to_string().contains("no BETA-eligible")); + } + + #[test] + fn picks_latest_tag_per_channel() { + let vs = versions(&[(bv(3, 0), "3.0.5"), (bv(3, 1), "3.1.0-beta.0")]); + let tags = vec![v("3.0.1"), v("3.0.5"), v("3.0.2"), v("2.9.9")]; + + let info = derive_channels(&vs, &tags, None, None).unwrap(); + + assert_eq!(info.beta_channel_latest_tag, ""); + assert_eq!(info.stable_channel_latest_tag, "v3.0.5"); + } + + #[test] + fn channel_from_branch_match() { + let vs = versions(&[(bv(4, 0), "4.0.0"), (bv(4, 1), "4.1.0-beta.0")]); + + let info = derive_channels(&vs, &[], Some("v4.1"), None).unwrap(); + + assert_eq!(info.channel, "beta"); + } + + #[test] + fn channel_env_var_wins() { + let vs = versions(&[(bv(4, 0), "4.0.0"), (bv(4, 1), "4.1.0-beta.0")]); + + let info = derive_channels(&vs, &[], Some("master"), Some("stable")).unwrap(); + + assert_eq!(info.channel, "stable"); + } + + #[test] + fn stage_of_classifies_known_labels() { + assert_eq!(stage_of(&v("4.0.0")).unwrap(), Stage::Ga); + assert_eq!(stage_of(&v("4.0.0-alpha.0")).unwrap(), Stage::Alpha); + assert_eq!(stage_of(&v("4.0.0-beta.3")).unwrap(), Stage::Beta); + assert_eq!(stage_of(&v("4.0.0-rc.1")).unwrap(), Stage::Rc); + } +} diff --git a/ci/xtask/src/commands/generate_pipeline.rs b/ci/xtask/src/commands/generate_pipeline.rs index 415902165a0..6a67716b6a4 100644 --- a/ci/xtask/src/commands/generate_pipeline.rs +++ b/ci/xtask/src/commands/generate_pipeline.rs @@ -130,6 +130,7 @@ fn generate_private_pipeline() -> Result { ..Default::default() })); + pipeline.add_step(default_channel_info_divergence_step()); pipeline.add_step(default_shellcheck_step()); pipeline.add_step(buildkite::Step::Wait(buildkite::WaitStep {})); @@ -187,6 +188,7 @@ fn generate_merge_queue_pipeline() -> Result { let mut pipeline = buildkite::Pipeline::new(); pipeline.set_priority(10); pipeline.add_step(default_sanity_step()); + pipeline.add_step(default_channel_info_divergence_step()); pipeline.add_step(default_checks_step()); Ok(pipeline) } @@ -352,6 +354,7 @@ async fn generate_pull_request_pipeline( let mut pipeline = buildkite::Pipeline::new(); pipeline.add_step(default_sanity_step()); + pipeline.add_step(default_channel_info_divergence_step()); if flags.shellcheck { pipeline.add_step(default_shellcheck_step()); } @@ -405,6 +408,7 @@ fn generate_full_pipeline() -> Result { let mut pipeline = buildkite::Pipeline::new(); pipeline.add_step(default_sanity_step()); + pipeline.add_step(default_channel_info_divergence_step()); pipeline.add_step(default_shellcheck_step()); pipeline.add_step(buildkite::Step::Wait(buildkite::WaitStep {})); @@ -448,6 +452,20 @@ fn default_sanity_step() -> buildkite::Step { }) } +fn default_channel_info_divergence_step() -> buildkite::Step { + buildkite::Step::Command(buildkite::CommandStep { + name: String::from("channel-info-divergence"), + command: String::from("ci/docker-run-default-image.sh ci/test-channel-info-divergence.sh"), + agents: Some(HashMap::from([( + String::from("queue"), + String::from("default"), + )])), + timeout_in_minutes: Some(10), + soft_fail: Some(true), + ..Default::default() + }) +} + fn default_shellcheck_step() -> buildkite::Step { buildkite::Step::Command(buildkite::CommandStep { name: String::from("shellcheck"), diff --git a/ci/xtask/src/main.rs b/ci/xtask/src/main.rs index 1a66585ae4a..dfa758628a0 100644 --- a/ci/xtask/src/main.rs +++ b/ci/xtask/src/main.rs @@ -28,6 +28,8 @@ enum Commands { Publish(xtask_shared::commands::publish::CommandArgs), #[command(about = "Generate Buildkite pipeline")] GeneratePipeline(commands::generate_pipeline::CommandArgs), + #[command(about = "Print release channel info")] + ChannelInfo, } #[derive(Args, Debug)] @@ -78,6 +80,9 @@ async fn try_main(xtask: Xtask) -> Result<()> { Commands::GeneratePipeline(args) => { commands::generate_pipeline::run(args).await?; } + Commands::ChannelInfo => { + commands::channel_info::run().await?; + } } Ok(()) From b976f5c45aba9065fe47305f751ddff82cbac91b Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Mon, 15 Jun 2026 09:39:55 -0400 Subject: [PATCH 338/576] retransmit: remove blocking send when sending VotorEvent::FirstShred (#13117) --- turbine/src/retransmit_stage.rs | 47 +++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/turbine/src/retransmit_stage.rs b/turbine/src/retransmit_stage.rs index 3e2359dd0c6..bd159210839 100644 --- a/turbine/src/retransmit_stage.rs +++ b/turbine/src/retransmit_stage.rs @@ -16,6 +16,7 @@ use { rayon::{ThreadPool, ThreadPoolBuilder, prelude::*}, solana_clock::Slot, solana_gossip::cluster_info::ClusterInfo, + solana_leader_schedule::NUM_CONSECUTIVE_LEADER_SLOTS, solana_ledger::{ leader_schedule_cache::LeaderScheduleCache, shred::{self, ShredFlags, ShredId, ShredType}, @@ -123,6 +124,7 @@ struct RetransmitState { stats: RetransmitStats, addr_cache: AddrCache, shred_buf: Vec>, + pending_first_shred_event: Option, } struct RetransmitNotifiers { @@ -152,6 +154,7 @@ impl RetransmitState { stats: RetransmitStats::new(now), addr_cache: AddrCache::with_capacity(/*capacity:*/ 4), shred_buf: Vec::with_capacity(RETRANSMIT_BATCH_SIZE), + pending_first_shred_event: None, } } } @@ -331,8 +334,17 @@ fn retransmit(context: &RetransmitContext, state: &mut RetransmitState) -> Resul stats, addr_cache, shred_buf, + pending_first_shred_event, } = state; + // Attempt to resend a pending first shred event to votor + if let Some(event) = pending_first_shred_event.take() + && let Err(TrySendError::Full(event)) = context.notifiers.votor_event_sender.try_send(event) + { + // Failed again, requeue + *pending_first_shred_event = Some(event); + } + // Try to receive shreds from the channel without blocking. If the channel // is empty precompute turbine trees speculatively. If no cache updates are // made then block on the channel until some shreds are received. @@ -459,7 +471,13 @@ fn retransmit(context: &RetransmitContext, state: &mut RetransmitState) -> Resul }) }; - stats.upsert_slot_stats(slot_stats, root_bank.slot(), addr_cache, &context.notifiers)?; + stats.upsert_slot_stats( + slot_stats, + root_bank.slot(), + addr_cache, + &context.notifiers, + pending_first_shred_event, + ); timer_start.stop(); stats.total_time += timer_start.as_us(); stats.maybe_submit( @@ -786,13 +804,19 @@ impl RetransmitStats { root: Slot, addr_cache: &mut AddrCache, notifiers: &RetransmitNotifiers, - ) -> Result<(), ()> { + pending_first_shred_event: &mut Option, + ) { for (slot, mut slot_stats) in feed { addr_cache.record(slot, &mut slot_stats); match self.slot_stats.get_mut(&slot) { None => { if slot > root { - notify_subscribers(slot, slot_stats.outset, notifiers)?; + notify_subscribers( + slot, + slot_stats.outset, + notifiers, + pending_first_shred_event, + ); } self.slot_stats.put(slot, slot_stats); } @@ -810,7 +834,6 @@ impl RetransmitStats { None => break, } } - Ok(()) } } @@ -885,7 +908,8 @@ fn notify_subscribers( slot: Slot, timestamp: u64, // When the first shred in the slot was received. notifiers: &RetransmitNotifiers, -) -> Result<(), ()> { + pending_first_shred_event: &mut Option, +) { if let Some(rpc_subscriptions) = notifiers.rpc_subscriptions.as_ref() { let slot_update = SlotUpdate::FirstShredReceived { slot, timestamp }; rpc_subscriptions.notify_slot_update(slot_update); @@ -898,7 +922,9 @@ fn notify_subscribers( .notify_first_shred_received(slot); } - if notifiers.migration_status.should_send_votor_event(slot) { + if notifiers.migration_status.should_send_votor_event(slot) + && slot.is_multiple_of(NUM_CONSECUTIVE_LEADER_SLOTS.get() as u64) + { match notifiers .votor_event_sender .try_send(VotorEvent::FirstShred(slot)) @@ -906,18 +932,17 @@ fn notify_subscribers( Ok(()) => (), Err(TrySendError::Full(event)) => { error!( - "Votor event channel is backed up len {}, something is wrong, blocking", + "Votor event channel is backed up len {}, something is wrong", notifiers.votor_event_sender.len(), ); - let _ = notifiers.votor_event_sender.send(event); + // Only the latest first shred notification matters, requeue + pending_first_shred_event.replace(event); } Err(TrySendError::Disconnected(_)) => { - info!("Votor event channel disconnectioned, we are shutting down") + info!("Votor event channel disconnected, we are shutting down") } } } - - Ok(()) } #[cfg(test)] From 96928cc984c1b4c807b283c6e74718429389f48e Mon Sep 17 00:00:00 2001 From: steviez Date: Mon, 15 Jun 2026 10:17:05 -0500 Subject: [PATCH 339/576] blockstore: Remove outdated cleanup logic (#13195) Long ago, several dummy values were inserted into the transaction status and address signature columns. Those values are no longer in use and code was added to remove those dummy entries if they were detected. It has been many release and multiple years since then so we can reasonably delete the cleanup step --- ledger/src/blockstore.rs | 32 +------------------------------- ledger/src/blockstore/tests.rs | 2 +- 2 files changed, 2 insertions(+), 32 deletions(-) diff --git a/ledger/src/blockstore.rs b/ledger/src/blockstore.rs index d805482414b..dd3fd7e593e 100644 --- a/ledger/src/blockstore.rs +++ b/ledger/src/blockstore.rs @@ -54,7 +54,7 @@ use { solana_sha256_hasher::hashv, solana_signature::Signature, solana_signer::Signer, - solana_storage_proto::{StoredExtendedRewards, StoredTransactionStatusMeta}, + solana_storage_proto::StoredExtendedRewards, solana_time_utils::timestamp, solana_transaction::{ TransactionVerificationMode, @@ -660,7 +660,6 @@ impl Blockstore { manual_purge_request_sender: Mutex::default(), slots_stats: SlotsStats::default(), }; - blockstore.cleanup_old_entries()?; Ok(blockstore) } @@ -4217,35 +4216,6 @@ impl Blockstore { .collect() } - fn cleanup_old_entries(&self) -> Result<()> { - if !self.is_primary_access() { - return Ok(()); - } - - // If present, delete dummy entries inserted by old software - // https://github.com/solana-labs/solana/blob/bc2b372/ledger/src/blockstore.rs#L2130-L2137 - let transaction_status_dummy_key = cf::TransactionStatus::as_index(2); - if self - .transaction_status_cf - .get_protobuf_or_wincode::(transaction_status_dummy_key)? - .is_some() - { - self.transaction_status_cf - .delete(transaction_status_dummy_key)?; - }; - let address_signatures_dummy_key = cf::AddressSignatures::as_index(2); - if self - .address_signatures_cf - .get(address_signatures_dummy_key)? - .is_some() - { - self.address_signatures_cf - .delete(address_signatures_dummy_key)?; - }; - - Ok(()) - } - pub fn read_transaction_status( &self, index: (Signature, Slot), diff --git a/ledger/src/blockstore/tests.rs b/ledger/src/blockstore/tests.rs index d9c13da5c69..a394f432d49 100644 --- a/ledger/src/blockstore/tests.rs +++ b/ledger/src/blockstore/tests.rs @@ -24,7 +24,7 @@ use { solana_sha256_hasher::hash, solana_shred_version::version_from_hash, solana_signature::Signature, - solana_storage_proto::convert::generated, + solana_storage_proto::{StoredTransactionStatusMeta, convert::generated}, solana_streamer::evicting_sender::EvictingSender, solana_transaction::Transaction, solana_transaction_context::transaction::TransactionReturnData, From 0b31f5d2648ed1e29c90cfd19bdc42a553697a54 Mon Sep 17 00:00:00 2001 From: Akhilesh Singhania Date: Mon, 15 Jun 2026 20:37:46 +0200 Subject: [PATCH 340/576] Updates `perf/src/packet.rs` to use wincode instead of bincode (#13175) Updates perf/src/packet.rs to use wincode instead of bincode --- Cargo.lock | 3 + bls-sigverify/Cargo.toml | 2 + bls-sigverify/src/bls_sigverifier.rs | 6 +- .../latest_validator_vote_packet.rs | 51 +++++------- core/src/banking_stage/vote_storage.rs | 4 +- core/src/repair/ancestor_hashes_service.rs | 7 +- core/src/repair/block_id_repair_service.rs | 25 +++--- core/src/repair/serve_repair.rs | 38 ++++----- dev-bins/Cargo.lock | 3 + gossip/src/cluster_info.rs | 2 +- gossip/src/wire_format_tests.rs | 4 +- local-cluster/Cargo.toml | 2 + local-cluster/src/cluster_tests.rs | 7 +- perf/Cargo.toml | 1 + perf/benches/sigverify.rs | 2 +- perf/src/packet.rs | 79 ++++--------------- perf/src/sigverify.rs | 24 +++--- programs/sbf/Cargo.lock | 3 + votor/src/voting_service.rs | 3 +- 19 files changed, 113 insertions(+), 153 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b045f4955c6..3dfb817af81 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -108,6 +108,7 @@ dependencies = [ "solana-measure", "solana-metrics", "solana-net-utils", + "solana-packet 4.1.0", "solana-perf", "solana-pubkey 4.2.0", "solana-runtime", @@ -8972,6 +8973,8 @@ dependencies = [ "solana-message", "solana-native-token", "solana-net-utils", + "solana-packet 4.1.0", + "solana-perf", "solana-poh-config", "solana-program-binaries", "solana-pubkey 4.2.0", diff --git a/bls-sigverify/Cargo.toml b/bls-sigverify/Cargo.toml index 438f13fe289..f94ece8393f 100644 --- a/bls-sigverify/Cargo.toml +++ b/bls-sigverify/Cargo.toml @@ -32,6 +32,8 @@ solana-gossip = { workspace = true } solana-ledger = { workspace = true } solana-measure = { workspace = true } solana-metrics = { workspace = true } +solana-packet = { workspace = true } +solana-perf = { workspace = true } solana-pubkey = { workspace = true } solana-runtime = { workspace = true } solana-signer-store = { workspace = true } diff --git a/bls-sigverify/src/bls_sigverifier.rs b/bls-sigverify/src/bls_sigverifier.rs index 670bba519ed..865bc567f72 100644 --- a/bls-sigverify/src/bls_sigverifier.rs +++ b/bls-sigverify/src/bls_sigverifier.rs @@ -25,6 +25,7 @@ use { solana_gossip::cluster_info::ClusterInfo, solana_ledger::leader_schedule_cache::LeaderScheduleCache, solana_measure::measure_us, + solana_perf::packet::packet_config, solana_pubkey::Pubkey, solana_runtime::{bank::Bank, bank_forks::SharableBanks}, solana_streamer::{nonblocking::simple_qos::SimpleQosBanlist, packet::PacketBatch}, @@ -219,7 +220,10 @@ impl SigVerifier { self.stats.num_discarded_pkts += 1; continue; } - let Ok(msg) = packet.deserialize_slice::(..) else { + let Ok(msg) = wincode::config::deserialize_exact( + packet.data(..).unwrap_or_default(), + packet_config(), + ) else { self.stats.num_malformed_pkts += 1; continue; }; diff --git a/core/src/banking_stage/latest_validator_vote_packet.rs b/core/src/banking_stage/latest_validator_vote_packet.rs index b24b9307d16..fa12fb529ae 100644 --- a/core/src/banking_stage/latest_validator_vote_packet.rs +++ b/core/src/banking_stage/latest_validator_vote_packet.rs @@ -182,47 +182,38 @@ mod tests { let keypairs = ValidatorVoteKeypairs::new_rand(); let blockhash = Hash::new_unique(); let switch_proof = Hash::new_unique(); - let mut tower_sync = BytesPacket::from_data( + let mut tower_sync = BytesPacket::from_data(new_tower_sync_transaction( + TowerSync::from(vec![(0, 3), (1, 2), (2, 1)]), + blockhash, + &keypairs.node_keypair, + &keypairs.vote_keypair, + &keypairs.vote_keypair, None, - new_tower_sync_transaction( - TowerSync::from(vec![(0, 3), (1, 2), (2, 1)]), - blockhash, - &keypairs.node_keypair, - &keypairs.vote_keypair, - &keypairs.vote_keypair, - None, - ), - ) + )) .unwrap(); tower_sync .meta_mut() .flags .set(PacketFlags::SIMPLE_VOTE_TX, true); - let mut tower_sync_switch = BytesPacket::from_data( - None, - new_tower_sync_transaction( - TowerSync::from(vec![(0, 3), (1, 2), (3, 1)]), - blockhash, - &keypairs.node_keypair, - &keypairs.vote_keypair, - &keypairs.vote_keypair, - Some(switch_proof), - ), - ) + let mut tower_sync_switch = BytesPacket::from_data(new_tower_sync_transaction( + TowerSync::from(vec![(0, 3), (1, 2), (3, 1)]), + blockhash, + &keypairs.node_keypair, + &keypairs.vote_keypair, + &keypairs.vote_keypair, + Some(switch_proof), + )) .unwrap(); tower_sync_switch .meta_mut() .flags .set(PacketFlags::SIMPLE_VOTE_TX, true); - let random_transaction = BytesPacket::from_data( - None, - transfer( - &keypairs.node_keypair, - &Pubkey::new_unique(), - 1000, - blockhash, - ), - ) + let random_transaction = BytesPacket::from_data(transfer( + &keypairs.node_keypair, + &Pubkey::new_unique(), + 1000, + blockhash, + )) .unwrap(); let packet_batch = PacketBatch::from(vec![tower_sync, tower_sync_switch, random_transaction]); diff --git a/core/src/banking_stage/vote_storage.rs b/core/src/banking_stage/vote_storage.rs index 144667fa20f..40f033b1b1f 100644 --- a/core/src/banking_stage/vote_storage.rs +++ b/core/src/banking_stage/vote_storage.rs @@ -473,7 +473,7 @@ pub(crate) mod tests { &keypairs.vote_keypair, None, ); - let mut packet = BytesPacket::from_data(None, vote_tx).unwrap(); + let mut packet = BytesPacket::from_data(vote_tx).unwrap(); packet .meta_mut() .flags @@ -509,7 +509,7 @@ pub(crate) mod tests { authorized_voter, None, ); - let mut packet = BytesPacket::from_data(None, vote_tx).unwrap(); + let mut packet = BytesPacket::from_data(vote_tx).unwrap(); packet .meta_mut() .flags diff --git a/core/src/repair/ancestor_hashes_service.rs b/core/src/repair/ancestor_hashes_service.rs index a49f79cb9d1..9ef93b46fb4 100644 --- a/core/src/repair/ancestor_hashes_service.rs +++ b/core/src/repair/ancestor_hashes_service.rs @@ -23,7 +23,7 @@ use { solana_keypair::{Keypair, Signer, signable::Signable}, solana_ledger::blockstore::Blockstore, solana_perf::{ - packet::{PacketBatch, PacketRef, deserialize_from_with_limit}, + packet::{PacketBatch, PacketRef, packet_config}, recycler::Recycler, }, solana_pubkey::Pubkey, @@ -375,7 +375,7 @@ impl AncestorHashesService { return None; }; let mut cursor = Cursor::new(packet_data); - let Ok(response) = deserialize_from_with_limit(&mut cursor) else { + let Ok(response) = wincode::config::deserialize_from(&mut cursor, packet_config()) else { stats.invalid_packets += 1; return None; }; @@ -383,7 +383,8 @@ impl AncestorHashesService { match response { AncestorHashesResponse::Hashes(ref hashes) => { // deserialize trailing nonce - let Ok(nonce) = deserialize_from_with_limit(&mut cursor) else { + let Ok(nonce) = wincode::config::deserialize_from(&mut cursor, packet_config()) + else { stats.invalid_packets += 1; return None; }; diff --git a/core/src/repair/block_id_repair_service.rs b/core/src/repair/block_id_repair_service.rs index 3e1e11dfab3..041a2e0f22e 100644 --- a/core/src/repair/block_id_repair_service.rs +++ b/core/src/repair/block_id_repair_service.rs @@ -40,7 +40,7 @@ use { shred::DATA_SHREDS_PER_FEC_BLOCK, }, solana_perf::{ - packet::{PacketBatch, PacketRef, deserialize_from_with_limit}, + packet::{PacketBatch, PacketRef, packet_config}, recycler::Recycler, }, solana_pubkey::Pubkey, @@ -543,7 +543,7 @@ impl BlockIdRepairService { return; }; let mut cursor = Cursor::new(packet_data); - let Ok(response) = deserialize_from_with_limit::<_, BlockIdRepairResponse>(&mut cursor) + let Ok(response) = wincode::config::deserialize_from(&mut cursor, packet_config()) .inspect_err(|e| { debug!("Failed to deserialize response: {e:?}"); }) @@ -565,7 +565,7 @@ impl BlockIdRepairService { return; } - let nonce: u32 = match deserialize_from_with_limit(&mut cursor) { + let nonce: u32 = match wincode::config::deserialize_from(&mut cursor, packet_config()) { Ok(n) => n, Err(e) => { debug!("{my_pubkey}: Failed to deserialize nonce: {e:?}"); @@ -1079,7 +1079,7 @@ mod tests { solana_perf::packet::Packet, solana_runtime::{bank::Bank, bank_forks::BankForks, genesis_utils::create_genesis_config}, solana_sha256_hasher::hashv, - std::{io::Cursor, sync::RwLock}, + std::sync::RwLock, }; /// Helper to build a merkle tree from leaf hashes and return the root and proofs @@ -1243,7 +1243,7 @@ mod tests { let packet_data = packet.data(..).unwrap(); let deser_response: BlockIdRepairResponse = - deserialize_from_with_limit(&mut Cursor::new(packet_data)).unwrap(); + wincode::config::deserialize(packet_data, packet_config()).unwrap(); match deser_response { BlockIdRepairResponse::ParentFecSetCount { @@ -1274,8 +1274,7 @@ mod tests { let packet = make_packet(&data); let packet_data = packet.data(..).unwrap(); - let deser_response: BlockIdRepairResponse = - deserialize_from_with_limit(&mut Cursor::new(packet_data)).unwrap(); + let deser_response: BlockIdRepairResponse = wincode::deserialize(packet_data).unwrap(); match deser_response { BlockIdRepairResponse::FecSetRoot { @@ -1294,18 +1293,14 @@ mod tests { // Empty packet let packet = make_packet(&[]); let packet_data = packet.data(..).unwrap(); - assert!( - deserialize_from_with_limit::<_, BlockIdRepairResponse>(&mut Cursor::new(packet_data)) - .is_err() - ); + wincode::config::deserialize::(packet_data, packet_config()) + .unwrap_err(); // Garbage data let packet = make_packet(&[0xff, 0xff, 0xff, 0xff]); let packet_data = packet.data(..).unwrap(); - assert!( - deserialize_from_with_limit::<_, BlockIdRepairResponse>(&mut Cursor::new(packet_data)) - .is_err() - ); + wincode::config::deserialize::(packet_data, packet_config()) + .unwrap_err(); } #[test] diff --git a/core/src/repair/serve_repair.rs b/core/src/repair/serve_repair.rs index 1b39d1ad414..1df21c37824 100644 --- a/core/src/repair/serve_repair.rs +++ b/core/src/repair/serve_repair.rs @@ -71,6 +71,7 @@ use { thread::{Builder, JoinHandle}, time::{Duration, Instant}, }, + wincode::{SchemaRead, SchemaWrite}, }; /// the number of slots to respond with when responding to `Orphan` requests @@ -189,7 +190,7 @@ impl AncestorHashesRepairType { } } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, SchemaRead, SchemaWrite)] pub enum AncestorHashesResponse { Hashes(Vec<(Slot, Hash)>), Ping(Ping), @@ -235,7 +236,7 @@ impl BlockIdRepairType { } } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, SchemaRead, SchemaWrite)] pub enum BlockIdRepairResponse { ParentFecSetCount { fec_set_count: u32, @@ -371,7 +372,7 @@ struct ServeRepairStats { } #[cfg_attr(feature = "frozen-abi", derive(AbiExample, StableAbi))] -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, SchemaRead, SchemaWrite)] pub struct RepairRequestHeader { signature: Signature, sender: Pubkey, @@ -428,7 +429,7 @@ type PingCache = ping_pong::PingCache; abi_digest = "D5RRQygn3D6ux1TYxeyXdksWD2KGA8PYi315hXP3JJ7c" ) )] -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, SchemaRead, SchemaWrite)] pub enum RepairProtocol { LegacyWindowIndex, LegacyHighestWindowIndex, @@ -545,7 +546,7 @@ fn is_well_formed_repair_request(packet: &PacketRef, stats: &mut ServeRepairStat well_formed } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, SchemaRead, SchemaWrite)] pub(crate) enum RepairResponse { Ping(Ping), } @@ -1854,7 +1855,9 @@ impl ServeRepair { if packet.meta().size != REPAIR_RESPONSE_SERIALIZED_PING_BYTES { continue; } - if let Ok(RepairResponse::Ping(ping)) = packet.deserialize_slice(..) { + if let Some(data) = packet.data(..) + && let Ok(RepairResponse::Ping(ping)) = wincode::deserialize(data) + { if !ping.verify() { // Do _not_ set `discard` to allow shred processing to attempt to // handle the packet. @@ -1954,7 +1957,7 @@ mod tests { }, }, solana_net_utils::SocketAddrSpace, - solana_perf::packet::{Packet, PacketFlags, PacketRef, deserialize_from_with_limit}, + solana_perf::packet::{Packet, PacketFlags, PacketRef}, solana_pubkey::Pubkey, solana_runtime::bank::Bank, solana_time_utils::timestamp, @@ -2153,8 +2156,7 @@ mod tests { .unwrap(); let mut cursor = Cursor::new(&rsp[..]); - let deserialized_request: RepairProtocol = - deserialize_from_with_limit(&mut cursor).unwrap(); + let deserialized_request: RepairProtocol = wincode::deserialize_from(&mut cursor).unwrap(); assert_eq!(cursor.position(), rsp.len() as u64); if let RepairProtocol::Orphan { header, slot } = deserialized_request { assert_eq!(slot, 123); @@ -2194,8 +2196,7 @@ mod tests { .ancestor_repair_request_bytes(&keypair, &repair_peer_id, slot, nonce) .unwrap(); let mut cursor = Cursor::new(&request_bytes[..]); - let deserialized_request: RepairProtocol = - deserialize_from_with_limit(&mut cursor).unwrap(); + let deserialized_request: RepairProtocol = wincode::deserialize_from(&mut cursor).unwrap(); assert_eq!(cursor.position(), request_bytes.len() as u64); if let RepairProtocol::AncestorHashes { header, @@ -2247,8 +2248,7 @@ mod tests { .unwrap(); let mut cursor = Cursor::new(&request_bytes[..]); - let deserialized_request: RepairProtocol = - deserialize_from_with_limit(&mut cursor).unwrap(); + let deserialized_request: RepairProtocol = wincode::deserialize_from(&mut cursor).unwrap(); assert_eq!(cursor.position(), request_bytes.len() as u64); if let RepairProtocol::WindowIndex { header, @@ -2283,8 +2283,7 @@ mod tests { .unwrap(); let mut cursor = Cursor::new(&request_bytes[..]); - let deserialized_request: RepairProtocol = - deserialize_from_with_limit(&mut cursor).unwrap(); + let deserialized_request: RepairProtocol = wincode::deserialize_from(&mut cursor).unwrap(); assert_eq!(cursor.position(), request_bytes.len() as u64); if let RepairProtocol::HighestWindowIndex { header, @@ -2760,9 +2759,12 @@ mod tests { #[test] fn test_run_ancestor_hashes() { fn deserialize_ancestor_hashes_response(packet: PacketRef) -> AncestorHashesResponse { - packet - .deserialize_slice(..packet.meta().size - SIZE_OF_NONCE) - .unwrap() + wincode::deserialize( + packet + .data(..(packet.meta().size - SIZE_OF_NONCE)) + .unwrap_or_default(), + ) + .unwrap() } agave_logger::setup(); diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 19e401d33e6..0d41e26e822 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -95,6 +95,8 @@ dependencies = [ "solana-ledger", "solana-measure", "solana-metrics", + "solana-packet 4.1.0", + "solana-perf", "solana-pubkey 4.2.0", "solana-runtime", "solana-signer-store", @@ -7494,6 +7496,7 @@ dependencies = [ "solana-transaction-context", "solana-vote", "solana-vote-program", + "wincode", ] [[package]] diff --git a/gossip/src/cluster_info.rs b/gossip/src/cluster_info.rs index f5c7f08cacf..03f227324fb 100644 --- a/gossip/src/cluster_info.rs +++ b/gossip/src/cluster_info.rs @@ -2808,7 +2808,7 @@ mod tests { assert_eq!(packet.meta().socket_addr(), socket); let bytes = wincode::serialize(&pong).unwrap(); assert_eq!(bytes, bincode::serialize(&pong).unwrap()); - match packet.deserialize_slice(..).unwrap() { + match deserialize_protocol(packet.data(..).unwrap_or_default()).unwrap() { Protocol::PongMessage(pong) => { let pong_bytes = wincode::serialize(&pong).unwrap(); assert_eq!(pong_bytes, bincode::serialize(&pong).unwrap()); diff --git a/gossip/src/wire_format_tests.rs b/gossip/src/wire_format_tests.rs index da2a6e79f3f..2c6db284f28 100644 --- a/gossip/src/wire_format_tests.rs +++ b/gossip/src/wire_format_tests.rs @@ -4,7 +4,7 @@ mod tests { use { - crate::protocol::Protocol, + crate::protocol::{Protocol, deserialize_protocol}, serde::Serialize, solana_net_utils::tooling_for_tests::{hexdump, validate_packet_format}, solana_sanitize::Sanitize, @@ -12,7 +12,7 @@ mod tests { }; fn parse_gossip(bytes: &[u8]) -> anyhow::Result { - let pkt: Protocol = solana_perf::packet::deserialize_from_with_limit(bytes)?; + let pkt = deserialize_protocol(bytes)?; pkt.sanitize()?; Ok(pkt) } diff --git a/local-cluster/Cargo.toml b/local-cluster/Cargo.toml index 95a892743b1..d4d73e78ceb 100644 --- a/local-cluster/Cargo.toml +++ b/local-cluster/Cargo.toml @@ -53,6 +53,8 @@ solana-ledger = { workspace = true } solana-message = { workspace = true } solana-native-token = { workspace = true } solana-net-utils = { workspace = true } +solana-packet = { workspace = true } +solana-perf = { workspace = true } solana-poh-config = { workspace = true } solana-program-binaries = { workspace = true } solana-pubkey = { workspace = true } diff --git a/local-cluster/src/cluster_tests.rs b/local-cluster/src/cluster_tests.rs index 956904a5ab0..89262098b25 100644 --- a/local-cluster/src/cluster_tests.rs +++ b/local-cluster/src/cluster_tests.rs @@ -30,6 +30,7 @@ use { solana_keypair::Keypair, solana_ledger::blockstore::Blockstore, solana_net_utils::{SocketAddrSpace, sockets::bind_to_localhost_unique}, + solana_perf::packet::packet_config, solana_poh_config::PohConfig, solana_pubkey::Pubkey, solana_rpc_client::rpc_client::RpcClient, @@ -672,7 +673,11 @@ pub fn check_for_new_notarized_votes( continue; }; for packet in packet_batch.iter() { - let Ok(ConsensusMessage::Vote(vote_message)) = packet.deserialize_slice(..) + let Ok(ConsensusMessage::Vote(vote_message)) = + wincode::config::deserialize_exact( + packet.data(..).unwrap_or_default(), + packet_config(), + ) else { continue; }; diff --git a/perf/Cargo.toml b/perf/Cargo.toml index 234cda447e2..9603336df11 100644 --- a/perf/Cargo.toml +++ b/perf/Cargo.toml @@ -71,6 +71,7 @@ solana-transaction = { workspace = true, optional = true, features = ["serde"] } solana-transaction-context = { workspace = true } solana-vote = { workspace = true, optional = true } solana-vote-program = { workspace = true, optional = true } +wincode = { workspace = true } [target."cfg(target_os = \"linux\")".dependencies] caps = { workspace = true } diff --git a/perf/benches/sigverify.rs b/perf/benches/sigverify.rs index c255e1f359c..931dd10b228 100644 --- a/perf/benches/sigverify.rs +++ b/perf/benches/sigverify.rs @@ -132,7 +132,7 @@ fn bench_sigverify_uneven(b: &mut Bencher) { } else { tx = multi_tx.clone(); }; - let mut packet = BytesPacket::from_data(None, &tx).expect("serialize request"); + let mut packet = BytesPacket::from_data(&tx).expect("serialize request"); if rand::rng().random_ratio((num_packets - NUM) as u32, num_packets as u32) { packet.meta_mut().set_discard(true); } else { diff --git a/perf/src/packet.rs b/perf/src/packet.rs index bec0a2ce949..97100a1ed6d 100644 --- a/perf/src/packet.rs +++ b/perf/src/packet.rs @@ -1,22 +1,21 @@ //! The `packet` module defines data structures and methods to pull data from the network. #[cfg(feature = "dev-context-only-utils")] -use bytes::{BufMut, BytesMut}; +use wincode::{SchemaWrite, WriteResult, config::DefaultConfig}; use { crate::{recycled_vec::RecycledVec, recycler::Recycler}, - bincode::config::Options, bytes::Bytes, rayon::{ iter::{IndexedParallelIterator, ParallelIterator}, prelude::{IntoParallelIterator, IntoParallelRefIterator, IntoParallelRefMutIterator}, }, - serde::{Deserialize, Serialize, de::DeserializeOwned}, + serde::{Deserialize, Serialize}, std::{ borrow::Borrow, - io::Read, net::SocketAddr, ops::{Deref, DerefMut, Index, IndexMut}, slice::{Iter, SliceIndex}, }, + wincode::config::{Config, Configuration}, }; pub use { bytes, @@ -28,6 +27,14 @@ pub const NUM_PACKETS: usize = 1024 * 8; pub const PACKETS_PER_BATCH: usize = 64; pub const NUM_RCVMMSGS: usize = 64; +/// wincode configuration setup to use for deserializing a Packet. +/// - Zero-copy alignment check is enabled. +/// - Preallocation size limit is PACKET_DATA_SIZE. +#[inline] +pub const fn packet_config() -> impl Config { + Configuration::default().with_preallocation_size_limit::<{ solana_packet::PACKET_DATA_SIZE }>() +} + /// Representation of a packet used in TPU. #[cfg_attr(feature = "frozen-abi", derive(AbiExample))] #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] @@ -62,22 +69,13 @@ impl BytesPacket { } #[cfg(feature = "dev-context-only-utils")] - pub fn from_data(dest: Option<&SocketAddr>, data: T) -> bincode::Result + pub fn from_data(data: T) -> WriteResult where - T: solana_packet::Encode, + T: SchemaWrite, { - let buffer = BytesMut::with_capacity(PACKET_DATA_SIZE); - let mut writer = buffer.writer(); - data.encode(&mut writer)?; - let buffer = writer.into_inner(); - let buffer = buffer.freeze(); - + let buffer = Bytes::from(wincode::serialize(&data)?); let mut meta = Meta::default(); meta.size = buffer.len(); - if let Some(dest) = dest { - meta.set_socket_addr(dest); - } - Ok(Self { buffer, meta }) } @@ -103,19 +101,6 @@ impl BytesPacket { &mut self.meta } - pub fn deserialize_slice(&self, index: I) -> bincode::Result - where - T: serde::de::DeserializeOwned, - I: SliceIndex<[u8], Output = [u8]>, - { - let bytes = self.data(index).ok_or(bincode::ErrorKind::SizeLimit)?; - bincode::options() - .with_limit(self.meta().size as u64) - .with_fixint_encoding() - .reject_trailing_bytes() - .deserialize(bytes) - } - #[cfg(feature = "dev-context-only-utils")] pub fn copy_from_slice(&mut self, slice: &[u8]) { self.buffer = Bytes::from(slice.to_vec()); @@ -359,17 +344,6 @@ impl<'a> PacketRef<'a> { } } - pub fn deserialize_slice(&self, index: I) -> bincode::Result - where - T: serde::de::DeserializeOwned, - I: SliceIndex<[u8], Output = [u8]>, - { - match self { - Self::Packet(packet) => packet.deserialize_slice(index), - Self::Bytes(packet) => packet.deserialize_slice(index), - } - } - pub fn to_bytes_packet(&self) -> BytesPacket { match self { // In case of the legacy `Packet` variant, we unfortunately need to @@ -442,17 +416,6 @@ impl PacketRefMut<'_> { } } - pub fn deserialize_slice(&self, index: I) -> bincode::Result - where - T: serde::de::DeserializeOwned, - I: SliceIndex<[u8], Output = [u8]>, - { - match self { - Self::Packet(packet) => packet.deserialize_slice(index), - Self::Bytes(packet) => packet.deserialize_slice(index), - } - } - #[cfg(feature = "dev-context-only-utils")] #[inline] pub fn copy_from_slice(&mut self, src: &[u8]) { @@ -883,20 +846,6 @@ impl<'a> IntoParallelIterator for &'a mut BytesPacketBatch { } } -pub fn deserialize_from_with_limit(reader: R) -> bincode::Result -where - R: Read, - T: DeserializeOwned, -{ - // with_limit causes pre-allocation size to be limited - // to prevent against memory exhaustion attacks. - bincode::options() - .with_limit(PACKET_DATA_SIZE as u64) - .with_fixint_encoding() - .allow_trailing_bytes() - .deserialize_from(reader) -} - #[cfg(test)] mod tests { use { diff --git a/perf/src/sigverify.rs b/perf/src/sigverify.rs index 388c3d5ff4b..fecdfdacbaf 100644 --- a/perf/src/sigverify.rs +++ b/perf/src/sigverify.rs @@ -245,7 +245,7 @@ mod tests { }; let mut tx = Transaction::new_unsigned(message); tx.signatures = vec![Signature::default(); actual_num_sigs]; - BytesPacket::from_data(None, tx).unwrap() + BytesPacket::from_data(tx).unwrap() } #[test] @@ -287,7 +287,7 @@ mod tests { tx.signatures = vec![sig; NUM_SIG]; tx.message.account_keys = vec![]; tx.message.header.num_required_signatures = NUM_SIG as u8; - let mut packet = BytesPacket::from_data(None, tx).unwrap(); + let mut packet = BytesPacket::from_data(tx).unwrap(); assert!(!verify_packet(&mut packet.as_mut(), false, false)); @@ -318,7 +318,7 @@ mod tests { let sig = keypair1.try_sign_message(&tx.message_data()).unwrap(); tx.signatures = vec![sig; NUM_SIG]; - let mut packet = BytesPacket::from_data(None, tx).unwrap(); + let mut packet = BytesPacket::from_data(tx).unwrap(); assert!(!verify_packet(&mut packet.as_mut(), false, false)); @@ -395,7 +395,7 @@ mod tests { }; let mut tx = Transaction::new_unsigned(message); tx.signatures = vec![Signature::default()]; - let mut packet = BytesPacket::from_data(None, tx).unwrap(); + let mut packet = BytesPacket::from_data(tx).unwrap(); assert!(!sigverify::verify_packet( &mut packet.as_mut(), false, @@ -498,7 +498,7 @@ mod tests { let mut tx = test_tx(); // pretend malicious leader dropped a signature... tx.signatures.pop(); - let packet = BytesPacket::from_data(None, tx).unwrap(); + let packet = BytesPacket::from_data(tx).unwrap(); let mut batches = generate_packet_batches(&packet, 1, 1); @@ -601,7 +601,7 @@ mod tests { { let mut tx = test_tx(); tx.message.instructions[0].data = vec![1, 2, 3]; - let packet = BytesPacket::from_data(None, tx).unwrap(); + let packet = BytesPacket::from_data(tx).unwrap(); let view = SanitizedTransactionView::try_new_sanitized( packet.as_ref().data(..).unwrap(), true, @@ -614,7 +614,7 @@ mod tests { { let mut tx = new_test_vote_tx(&mut rng); tx.message.instructions[0].data = vec![1, 2, 3]; - let packet = BytesPacket::from_data(None, tx).unwrap(); + let packet = BytesPacket::from_data(tx).unwrap(); let view = SanitizedTransactionView::try_new_sanitized( packet.as_ref().data(..).unwrap(), true, @@ -626,7 +626,7 @@ mod tests { // single versioned vote tx is not { let tx = new_test_vote_tx_v0(); - let packet = BytesPacket::from_data(None, tx).unwrap(); + let packet = BytesPacket::from_data(tx).unwrap(); let view = SanitizedTransactionView::try_new_sanitized( packet.as_ref().data(..).unwrap(), @@ -652,7 +652,7 @@ mod tests { CompiledInstruction::new(4, &(), vec![0, 2]), ], ); - let packet = BytesPacket::from_data(None, tx).unwrap(); + let packet = BytesPacket::from_data(tx).unwrap(); let view = SanitizedTransactionView::try_new_sanitized( packet.as_ref().data(..).unwrap(), true, @@ -667,7 +667,7 @@ mod tests { tx.signatures.push(Signature::default()); tx.message.header.num_required_signatures = 3; tx.message.instructions[0].data = vec![1, 2, 3]; - let packet = BytesPacket::from_data(None, tx).unwrap(); + let packet = BytesPacket::from_data(tx).unwrap(); let view = SanitizedTransactionView::try_new_sanitized( packet.as_ref().data(..).unwrap(), true, @@ -689,10 +689,10 @@ mod tests { let mut packet = if is_versioned_tx { let tx: VersionedTransaction = new_test_tx_with_number_of_ixs(number_of_ixs); - BytesPacket::from_data(None, tx.clone()).unwrap() + BytesPacket::from_data(tx.clone()).unwrap() } else { let tx: Transaction = new_test_tx_with_number_of_ixs(number_of_ixs); - BytesPacket::from_data(None, tx.clone()).unwrap() + BytesPacket::from_data(tx.clone()).unwrap() }; assert_eq!( diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index 7a9389e9515..3d4931dd865 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -95,6 +95,8 @@ dependencies = [ "solana-ledger", "solana-measure", "solana-metrics", + "solana-packet 4.1.0", + "solana-perf", "solana-pubkey 4.2.0", "solana-runtime", "solana-signer-store", @@ -7595,6 +7597,7 @@ dependencies = [ "solana-signature", "solana-time-utils", "solana-transaction-context", + "wincode", ] [[package]] diff --git a/votor/src/voting_service.rs b/votor/src/voting_service.rs index cbf8588bde5..1fbef3b8f19 100644 --- a/votor/src/voting_service.rs +++ b/votor/src/voting_service.rs @@ -423,8 +423,7 @@ mod tests { let packets = receiver.recv().unwrap(); let packet = packets.first().expect("No packets received"); - let received_message = packet - .deserialize_slice::(..) + let received_message = wincode::deserialize::(packet.data(..).unwrap()) .unwrap_or_else(|err| { panic!( "Failed to deserialize BLSMessage: {:?} {:?}", From b36fd5400a047f2ae4290eee251557850ff68d02 Mon Sep 17 00:00:00 2001 From: Zach Brown Date: Mon, 15 Jun 2026 11:39:27 -0700 Subject: [PATCH 341/576] Leverage blockstore `Index` in `find_missing_data_indexes` (#13185) --- ledger/src/bit_vec.rs | 103 +++++++++++++++++++++++++++------- ledger/src/blockstore.rs | 99 +++++++++++++------------------- ledger/src/blockstore_db.rs | 73 ++++++++++++++++++++++-- ledger/src/blockstore_meta.rs | 81 +++++++++++++++++++++++++- 4 files changed, 271 insertions(+), 85 deletions(-) diff --git a/ledger/src/bit_vec.rs b/ledger/src/bit_vec.rs index e2eedd1b301..b1c74e96141 100644 --- a/ledger/src/bit_vec.rs +++ b/ledger/src/bit_vec.rs @@ -12,6 +12,10 @@ use { type Word = u8; const BITS_PER_WORD: usize = std::mem::size_of::() * 8; +const fn num_words() -> usize { + NUM_BITS.div_ceil(BITS_PER_WORD) +} + /// A bit vector implementation optimized for efficient bidirectional range /// scanning and iteration. /// @@ -38,6 +42,34 @@ impl Default for BitVec { } } +#[inline] +fn location_of(idx: usize) -> (usize, usize) { + let word_idx = idx / BITS_PER_WORD; + let bit_idx = idx & (BITS_PER_WORD - 1); + (word_idx, bit_idx) +} + +#[inline] +fn check_bounds(idx: usize) -> Result<(), BitVecError> { + if idx >= NUM_BITS { + return Err(BitVecError::OutOfBounds { + index: idx, + num_bits: NUM_BITS, + }); + } + Ok(()) +} + +#[inline] +fn contains(words: &[Word], idx: usize) -> bool { + if check_bounds::(idx).is_err() { + return false; + } + + let (word_idx, bit_idx) = location_of(idx); + (words[word_idx] & (1 << bit_idx)) != 0 +} + // Note: bincode/wincode would construct a variable-length buffer, // which violates `BitVec`'s invariant that its backing vector length (in words) // is exactly `NUM_WORDS`. Bounds checks and performance rely on this fixed size. @@ -66,7 +98,7 @@ pub enum BitVecError { } impl BitVec { - const NUM_WORDS: usize = NUM_BITS.div_ceil(BITS_PER_WORD); + const NUM_WORDS: usize = num_words::(); /// Get the word and bit offset for the given index. /// @@ -78,20 +110,14 @@ impl BitVec { /// assert_eq!(word_idx, 7); /// assert_eq!(bit_idx, 7); /// ``` + #[inline] pub fn location_of(idx: usize) -> (usize, usize) { - let word_idx = idx / BITS_PER_WORD; - let bit_idx = idx & (BITS_PER_WORD - 1); - (word_idx, bit_idx) + location_of(idx) } + #[inline] fn check_bounds(&self, idx: usize) -> Result<(), BitVecError> { - if idx >= NUM_BITS { - return Err(BitVecError::OutOfBounds { - index: idx, - num_bits: NUM_BITS, - }); - } - Ok(()) + check_bounds::(idx) } /// Remove a bit at the given index. @@ -228,13 +254,9 @@ impl BitVec { /// bit_vec.insert(63); /// assert!(bit_vec.contains(63)); /// ``` + #[inline] pub fn contains(&self, idx: usize) -> bool { - if self.check_bounds(idx).is_err() { - return false; - } - - let (word_idx, bit_idx) = Self::location_of(idx); - (self.words[word_idx] & (1 << bit_idx)) != 0 + contains::(&self.words, idx) } /// Get an iterator over the bits in the array within the given range. @@ -253,7 +275,7 @@ impl BitVec { /// assert_eq!(bit_vec.range(1..).count_ones(), 1); /// ``` pub fn range(&self, bounds: impl RangeBounds) -> BitVecSlice<'_, NUM_BITS> { - BitVecSlice::from_range_bounds(self, bounds) + BitVecSlice::from_range_bounds(&self.words, bounds) } /// Get an iterator over the positions of the set bits in the array. @@ -310,6 +332,47 @@ impl FromIterator for BitVec { } } +/// A reference to a [`BitVec`] that does not own the underlying data. +#[derive(Debug, PartialEq, Eq)] +pub struct BitVecRef<'a, const NUM_BITS: usize> { + words: &'a [Word], +} + +impl BitVecRef<'_, NUM_BITS> { + const NUM_WORDS: usize = num_words::(); + + /// Check if a bit is set at the given index. + /// + /// See [`BitVec::contains`]. + #[inline] + pub fn contains(&self, idx: usize) -> bool { + contains::(self.words, idx) + } + + /// Get an iterator over the bits in the array within the given range. + /// + /// See [`BitVec::range`]. + pub fn range(&self, bounds: impl RangeBounds) -> BitVecSlice<'_, NUM_BITS> { + BitVecSlice::from_range_bounds(self.words, bounds) + } +} + +unsafe impl<'de, const NUM_BITS: usize, C: Config> SchemaRead<'de, C> for BitVecRef<'de, NUM_BITS> { + type Dst = Self; + + #[inline] + fn read(reader: impl Reader<'de>, dst: &mut MaybeUninit) -> ReadResult<()> { + use wincode::ReadError; + + let words = <&'de [Word] as SchemaRead>::get(reader)?; + if words.len() != Self::NUM_WORDS { + return Err(ReadError::Custom("Invalid BitVec length")); + } + dst.write(Self { words }); + Ok(()) + } +} + /// A slice of a [`BitVec`] that provides efficient bit-level iteration. pub struct BitVecSlice<'a, const NUM_BITS: usize> { mask_iter: BitVecMaskIter<'a, NUM_BITS>, @@ -319,7 +382,7 @@ impl<'a, const NUM_BITS: usize> BitVecSlice<'a, NUM_BITS> { /// Construct a new [`BitVecSlice`] from a [`BitVec`] and a range. /// /// Internal function -- use [`BitVec::range`]. - fn from_range_bounds(bit_vec: &'a BitVec, bounds: impl RangeBounds) -> Self { + fn from_range_bounds(bit_vec: &'a [u8], bounds: impl RangeBounds) -> Self { let start = match bounds.start_bound() { Bound::Included(&n) => n, Bound::Excluded(&n) => n + 1, @@ -340,7 +403,7 @@ impl<'a, const NUM_BITS: usize> BitVecSlice<'a, NUM_BITS> { start, end, start_word, - iter: bit_vec.words[start_word..end_word].iter().enumerate(), + iter: bit_vec[start_word..end_word].iter().enumerate(), }, } } diff --git a/ledger/src/blockstore.rs b/ledger/src/blockstore.rs index dd3fd7e593e..9a13f77249e 100644 --- a/ledger/src/blockstore.rs +++ b/ledger/src/blockstore.rs @@ -7,8 +7,10 @@ use trees::{Tree, TreeWalk}; use { crate::{ ancestor_iterator::AncestorIterator, - blockstore::column::{Column, TypedColumn, columns as cf}, - blockstore_db::{IteratorDirection, IteratorMode, LedgerColumn, Rocks, WriteBatch}, + blockstore::column::{TypedColumn, columns as cf}, + blockstore_db::{ + DBPinnedT, IteratorDirection, IteratorMode, LedgerColumn, Rocks, WriteBatch, + }, blockstore_meta::*, blockstore_options::{ BLOCKSTORE_DIRECTORY_ROCKS_LEVEL, BlockstoreOptions, LedgerColumnOptions, @@ -34,7 +36,7 @@ use { lru::LruCache, rand::Rng, rayon::iter::{IntoParallelIterator, ParallelIterator}, - rocksdb::{DBRawIterator, LiveFile}, + rocksdb::LiveFile, solana_account::ReadableAccount, solana_address_lookup_table_interface::state::AddressLookupTable, solana_clock::{Slot, UnixTimestamp}, @@ -90,7 +92,7 @@ use { tar, tempfile::{Builder, TempDir}, thiserror::Error, - wincode::{Deserialize as _, containers::Vec as WincodeVec}, + wincode::{Deserialize as _, config::DefaultConfig, containers::Vec as WincodeVec}, }; pub mod blockstore_purge; @@ -3848,6 +3850,14 @@ impl Blockstore { self.index_cf.get(slot) } + /// Get a DB pinned reference to the [`Index`] via [`IndexRef`]. + fn get_index_ref( + &self, + slot: Slot, + ) -> Result>>> { + self.index_cf.get_pinned_t(slot) + } + pub fn get_index_from_location( &self, slot: Slot, @@ -3894,87 +3904,56 @@ impl Blockstore { /// [`start_index`, `end_index`]. /// /// Arguments: - /// - `db_iterator`: Iterator to run search over. /// - `slot`: The slot to search for missing shreds for. /// - `start_index`: Begin search (inclusively) at this shred index. /// - `end_index`: Finish search (exclusively) at this shred index. /// - `max_missing`: Limit result to this many indices. - fn find_missing_indexes( - db_iterator: &mut DBRawIterator, + pub fn find_missing_data_indexes( + &self, slot: Slot, start_index: u64, end_index: u64, max_missing: usize, - ) -> Vec - where - C: Column, - { + ) -> Vec { if start_index >= end_index || max_missing == 0 { return vec![]; } - let mut missing_indexes = vec![]; + let max_missing = max_missing.min((end_index - start_index) as usize); - // Seek to the first shred with index >= start_index - db_iterator.seek(C::key(&(slot, start_index))); - - // The index of the first missing shred in the slot - let mut prev_index = start_index; - loop { - if !db_iterator.valid() { - let num_to_take = max_missing - missing_indexes.len(); - missing_indexes.extend((prev_index..end_index).take(num_to_take)); - break; + let index = match self.get_index_ref(slot) { + Ok(Some(index)) => index, + Ok(None) => return (start_index..end_index).take(max_missing).collect(), + Err(err) => { + warn!("failed to read index for slot {slot}: {err}"); + return vec![]; } - let (current_slot, index) = C::index(db_iterator.key().expect("Expect a valid key")); + }; + let index = index.data(); - let current_index = { - if current_slot > slot { - end_index - } else { - index - } - }; + if index.num_shreds() == 0 { + return (start_index..end_index).take(max_missing).collect(); + } - let upper_index = cmp::min(current_index, end_index); + let mut missing_indexes = Vec::with_capacity(max_missing); + let mut prev_index = start_index; + for current_index in index.range(start_index..end_index) { let num_to_take = max_missing - missing_indexes.len(); - missing_indexes.extend((prev_index..upper_index).take(num_to_take)); + missing_indexes.extend((prev_index..current_index).take(num_to_take)); - if missing_indexes.len() == max_missing - || current_slot > slot - || current_index >= end_index - { + if missing_indexes.len() == max_missing { break; } prev_index = current_index + 1; - db_iterator.next(); } - missing_indexes - } - - /// Find missing data shreds for the given `slot`. - /// - /// For more details on the arguments, see [`find_missing_indexes`]. - pub fn find_missing_data_indexes( - &self, - slot: Slot, - start_index: u64, - end_index: u64, - max_missing: usize, - ) -> Vec { - let Ok(mut db_iterator) = self.db.raw_iterator_cf(self.data_shred_cf.handle()) else { - return vec![]; - }; + if missing_indexes.len() < max_missing { + let num_to_take = max_missing - missing_indexes.len(); + missing_indexes.extend((prev_index..end_index).take(num_to_take)); + } - Self::find_missing_indexes::( - &mut db_iterator, - slot, - start_index, - end_index, - max_missing, - ) + missing_indexes } fn get_block_time(&self, slot: Slot) -> Result> { diff --git a/ledger/src/blockstore_db.rs b/ledger/src/blockstore_db.rs index d0fcd4830df..007af8eb84b 100644 --- a/ledger/src/blockstore_db.rs +++ b/ledger/src/blockstore_db.rs @@ -20,8 +20,8 @@ use { prost::Message, rocksdb::{ self, ColumnFamily, ColumnFamilyDescriptor, CompactionDecision, DB, DBCompressionType, - DBIterator, DBPinnableSlice, DBRawIterator, IteratorMode as RocksIteratorMode, LiveFile, - Options, WriteBatch as RWriteBatch, + DBIterator, DBPinnableSlice, IteratorMode as RocksIteratorMode, LiveFile, Options, + WriteBatch as RWriteBatch, compaction_filter::CompactionFilter, compaction_filter_factory::{CompactionFilterContext, CompactionFilterFactory}, properties as RocksProperties, @@ -39,7 +39,11 @@ use { atomic::{AtomicU64, Ordering}, }, }, - wincode::{SchemaReadOwned, deserialize}, + wincode::{ + SchemaRead, SchemaReadOwned, + config::{ConfigCore, DefaultConfig}, + deserialize, + }, }; const BLOCKSTORE_METRICS_ERROR: i64 = -1; @@ -396,7 +400,8 @@ impl Rocks { self.db.iterator_cf(cf, iterator_mode) } - pub(crate) fn raw_iterator_cf(&self, cf: &ColumnFamily) -> Result> { + #[cfg(test)] + pub(crate) fn raw_iterator_cf(&self, cf: &ColumnFamily) -> Result> { Ok(self.db.raw_iterator_cf(cf)) } @@ -566,6 +571,55 @@ impl WriteBatch { } } +/// A pinned deserialized value with the ability to hold references into a [`DBPinnableSlice`]. +/// +/// All references in the deserialized payload will be tied to the lifetime of the [`DBPinnableSlice`]. +/// +/// Use this for zero-copy access to a value stored in the blockstore -- no reason to keep +/// the [`DBPinnableSlice`] alive for owned values. +pub struct DBPinnedT<'a, C: ConfigCore, T: SchemaRead<'a, C>> { + val: T::Dst, + _slice: DBPinnableSlice<'a>, + _phantom: PhantomData, +} + +impl<'a, C, T> std::ops::Deref for DBPinnedT<'a, C, T> +where + C: ConfigCore, + T: SchemaRead<'a, C>, +{ + type Target = T::Dst; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.val + } +} + +impl<'de, T, C> DBPinnedT<'de, C, T> +where + C: ConfigCore, + T: SchemaRead<'de, C>, +{ + fn decode(slice: DBPinnableSlice<'de>) -> Result { + let bytes = slice.as_ref(); + // SAFETY: `DBPinnableSlice` provides a stable reference into the underlying storage and keeps + // the RocksDB value pointer valid until it is dropped. + // `DBPinnedT` stores `val` before `_slice`, so `val` is dropped first; + // any references decoded into `val` cannot outlive the pinned bytes. + let val = T::get(unsafe { std::slice::from_raw_parts(bytes.as_ptr(), bytes.len()) })?; + Ok(Self { + val, + _slice: slice, + _phantom: PhantomData, + }) + } + + #[inline] + pub fn get(&self) -> &T::Dst { + &self.val + } +} impl LedgerColumn where C: Column + ColumnName, @@ -610,6 +664,17 @@ where result } + #[inline] + pub fn get_pinned_t<'a, T>( + &'a self, + index: C::Index, + ) -> Result>> + where + T: SchemaRead<'a, DefaultConfig>, + { + self.get_slice(index)?.map(DBPinnedT::decode).transpose() + } + /// Create a key type suitable for use with multi_get_bytes() and /// multi_get(). Those functions return iterators, so the keys must be /// created with a separate function in order to live long enough diff --git a/ledger/src/blockstore_meta.rs b/ledger/src/blockstore_meta.rs index 8d30a6e0d2d..493ed752e15 100644 --- a/ledger/src/blockstore_meta.rs +++ b/ledger/src/blockstore_meta.rs @@ -1,6 +1,6 @@ use { crate::{ - bit_vec::BitVec, + bit_vec::{BitVec, BitVecRef}, blockstore::ParentInfo, shred::{ self, DATA_SHREDS_PER_FEC_BLOCK, MAX_DATA_SHREDS_PER_SLOT, Shred, ShredType, @@ -295,6 +295,13 @@ pub struct Index { coding: ShredIndex, } +#[derive(Debug, SchemaRead, PartialEq, Eq)] +pub struct IndexRef<'a> { + pub slot: Slot, + data: ShredIndexRef<'a>, + coding: ShredIndexRef<'a>, +} + #[derive(Clone, Copy, Debug, SchemaRead, SchemaWrite, Eq, PartialEq)] /// Erasure coding information pub struct ErasureMeta { @@ -466,6 +473,12 @@ impl Index { } } +impl IndexRef<'_> { + pub fn data(&self) -> &ShredIndexRef<'_> { + &self.data + } +} + /// A bitvec (`Box<[u8]>`) of shred indices, where each u8 represents 8 shred indices. /// /// Bit vec implementation provides: @@ -535,6 +548,31 @@ impl FromIterator for ShredIndex { } } +/// A reference to a [`ShredIndex`]. +#[derive(Debug, SchemaRead, PartialEq, Eq)] +pub struct ShredIndexRef<'a> { + index: BitVecRef<'a, MAX_DATA_SHREDS_PER_SLOT>, + num_shreds: usize, +} + +impl ShredIndexRef<'_> { + pub fn num_shreds(&self) -> usize { + self.num_shreds + } + + pub(crate) fn range(&self, bounds: R) -> impl Iterator + '_ + where + R: RangeBounds, + { + let start = bounds.start_bound().map(|&b| b as usize); + let end = bounds.end_bound().map(|&b| b as usize); + self.index + .range((start, end)) + .iter_ones() + .map(|idx| idx as u64) + } +} + fn slot_meta_is_full(last_index: Option, consumed: u64) -> bool { // last_index is None when it has no information about how // many shreds will fill this slot. @@ -1052,6 +1090,24 @@ mod test { ) } + fn arb_index() -> impl Strategy { + ( + any::(), + proptest::collection::vec(0..MAX_DATA_SHREDS_PER_SLOT as u64, 0..128), + proptest::collection::vec(0..MAX_DATA_SHREDS_PER_SLOT as u64, 0..128), + ) + .prop_map(|(slot, data_indexes, coding_indexes)| { + let mut index = Index::new(slot); + for index_to_insert in data_indexes { + index.data_mut().insert(index_to_insert); + } + for index_to_insert in coding_indexes { + index.coding_mut().insert(index_to_insert); + } + index + }) + } + proptest! { // Property: `SlotMetaRepair` is always deserializable from serialized `SlotMeta`. #[test] @@ -1074,6 +1130,29 @@ mod test { } } + proptest! { + // Property: `IndexRef` is always deserializable from `Index`. + #[test] + fn test_index_ref_deserialization( + index in arb_index(), + ) { + let serialized = wincode::serialize(&index).unwrap(); + let deserialized: IndexRef = wincode::deserialize(&serialized).unwrap(); + + prop_assert_eq!(deserialized.slot, index.slot); + prop_assert_eq!( + deserialized.data().range(..).collect::>(), + index.data().range(..).collect::>() + ); + prop_assert_eq!( + deserialized.coding.range(..).collect::>(), + index.coding().range(..).collect::>() + ); + prop_assert_eq!(deserialized.data().num_shreds(), index.data().num_shreds()); + prop_assert_eq!(deserialized.coding.num_shreds(), index.coding().num_shreds()); + } + } + #[test] fn test_shred_index_range_bounds() { let mut index = ShredIndex::default(); From d4454fea48c76ece71a0b9493e36dbec96037dff Mon Sep 17 00:00:00 2001 From: steviez Date: Mon, 15 Jun 2026 14:24:11 -0500 Subject: [PATCH 342/576] blockstore: Remove bincode support in Rewards column (#13196) The column was originally populated with bincode values but converted to protobuf values with 359707c85ea in Oct 2020. Values have been written only as protobuf since. Fallback to bincode was left to support the transition as well as to support reading old Blockstores. The transition period has long passed, and there are numerous other breakages that prevent one from reading an old Blockstore with recent Agave. Thus, there is no benefit in supporting the fallback to bincode logic in the Rewards so column so rip it out --- CHANGELOG.md | 4 + ledger/benches/protobuf.rs | 95 ---------------------- ledger/src/blockstore.rs | 21 +++-- ledger/src/blockstore/tests.rs | 42 +--------- storage-proto/src/convert.rs | 10 +-- transaction-status-client-types/src/lib.rs | 2 +- 6 files changed, 23 insertions(+), 151 deletions(-) delete mode 100644 ledger/benches/protobuf.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index af5470b0fe5..fd9ec777787 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,10 @@ Release channels have their own copy of this changelog: ### RPC #### Breaking * The `jsonParsed` output for confidential transfer `Deposit` (`depositConfidentialTransfer`) and `Withdraw` (`withdrawConfidentialTransfer`) instructions has been corrected. These instructions operate on a single token account, so the mislabeled `source` and `destination` fields have been replaced by a single `account` field (the `mint` field is unchanged). +* Blockstore reward column legacy format support removed. + * The `Rewards` column was updated in v1.5 (Solana Labs client) to switch from + storing bincode serialized values to protobuf encoded values. The old bincode + format will no longer be supported for fallback reads as of v4.2 #### Changes * Added `RpcClient::get_latest_blockhash_with_commitment_and_context`, which returns the `getLatestBlockhash` response together with its context (notably `context.slot`). diff --git a/ledger/benches/protobuf.rs b/ledger/benches/protobuf.rs deleted file mode 100644 index 122f58ce0e5..00000000000 --- a/ledger/benches/protobuf.rs +++ /dev/null @@ -1,95 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -#![feature(test)] -extern crate test; - -use { - bincode::{deserialize, serialize}, - prost::Message, - solana_runtime::bank::RewardType, - solana_transaction_status::{Reward, Rewards}, - test::Bencher, -}; - -fn create_rewards() -> Rewards { - (0..100) - .map(|i| Reward { - pubkey: solana_pubkey::new_rand().to_string(), - lamports: 42 + i, - post_balance: u64::MAX, - reward_type: Some(RewardType::Fee), - commission: None, - commission_bps: None, - }) - .collect() -} - -fn bincode_serialize_rewards(rewards: Rewards) -> Vec { - serialize(&rewards).unwrap() -} - -fn protobuf_serialize_rewards(rewards: Rewards) -> Vec { - let rewards: solana_storage_proto::convert::generated::Rewards = rewards.into(); - let mut buffer = Vec::with_capacity(rewards.encoded_len()); - rewards.encode(&mut buffer).unwrap(); - buffer -} - -fn bincode_deserialize_rewards(bytes: &[u8]) -> Rewards { - deserialize(bytes).unwrap() -} - -fn protobuf_deserialize_rewards(bytes: &[u8]) -> Rewards { - solana_storage_proto::convert::generated::Rewards::decode(bytes) - .unwrap() - .into() -} - -fn bench_serialize_rewards(bench: &mut Bencher, serialize_method: S) -where - S: Fn(Rewards) -> Vec, -{ - let rewards = create_rewards(); - bench.iter(move || { - let _ = serialize_method(rewards.clone()); - }); -} - -fn bench_deserialize_rewards(bench: &mut Bencher, serialize_method: S, deserialize_method: D) -where - S: Fn(Rewards) -> Vec, - D: Fn(&[u8]) -> Rewards, -{ - let rewards = create_rewards(); - let rewards_bytes = serialize_method(rewards); - bench.iter(move || { - let _ = deserialize_method(&rewards_bytes); - }); -} - -#[bench] -fn bench_serialize_bincode(bencher: &mut Bencher) { - bench_serialize_rewards(bencher, bincode_serialize_rewards); -} - -#[bench] -fn bench_serialize_protobuf(bencher: &mut Bencher) { - bench_serialize_rewards(bencher, protobuf_serialize_rewards); -} - -#[bench] -fn bench_deserialize_bincode(bencher: &mut Bencher) { - bench_deserialize_rewards( - bencher, - bincode_serialize_rewards, - bincode_deserialize_rewards, - ); -} - -#[bench] -fn bench_deserialize_protobuf(bencher: &mut Bencher) { - bench_deserialize_rewards( - bencher, - protobuf_serialize_rewards, - protobuf_deserialize_rewards, - ); -} diff --git a/ledger/src/blockstore.rs b/ledger/src/blockstore.rs index 9a13f77249e..20f55e32804 100644 --- a/ledger/src/blockstore.rs +++ b/ledger/src/blockstore.rs @@ -56,14 +56,13 @@ use { solana_sha256_hasher::hashv, solana_signature::Signature, solana_signer::Signer, - solana_storage_proto::StoredExtendedRewards, solana_time_utils::timestamp, solana_transaction::{ TransactionVerificationMode, versioned::{VersionedTransaction, sanitized::SanitizedVersionedTransaction}, }, solana_transaction_status::{ - ConfirmedTransactionStatusWithSignature, ConfirmedTransactionWithStatusMeta, Rewards, + ConfirmedTransactionStatusWithSignature, ConfirmedTransactionWithStatusMeta, RewardsAndNumPartitions, TransactionStatusMeta, TransactionWithStatusMeta, VersionedConfirmedBlock, VersionedConfirmedBlockWithEntries, VersionedTransactionWithStatusMeta, @@ -4149,11 +4148,15 @@ impl Blockstore { } let previous_blockhash = previous_blockhash.unwrap_or_else(Hash::default); - let (rewards, num_partitions) = self - .rewards_cf - .get_protobuf_or_wincode::(slot)? - .unwrap_or_default() - .into(); + let RewardsAndNumPartitions { + rewards, + num_partitions, + } = self + .read_rewards(slot)? + .unwrap_or_else(|| RewardsAndNumPartitions { + rewards: Vec::new(), + num_partitions: None, + }); // The Blocktime and BlockHeight column families are updated asynchronously; they // may not be written by the time the complete slot entries are available. In this @@ -4700,9 +4703,9 @@ impl Blockstore { }) } - pub fn read_rewards(&self, index: Slot) -> Result> { + fn read_rewards(&self, slot: Slot) -> Result> { self.rewards_cf - .get_protobuf_or_wincode::(index) + .get_protobuf(slot) .map(|result| result.map(|option| option.into())) } diff --git a/ledger/src/blockstore/tests.rs b/ledger/src/blockstore/tests.rs index a394f432d49..047c1db6a3c 100644 --- a/ledger/src/blockstore/tests.rs +++ b/ledger/src/blockstore/tests.rs @@ -30,7 +30,7 @@ use { solana_transaction_context::transaction::TransactionReturnData, solana_transaction_error::TransactionError, solana_transaction_status::{ - InnerInstruction, InnerInstructions, Reward, Rewards, TransactionTokenBalance, + InnerInstruction, InnerInstructions, Reward, TransactionTokenBalance, }, std::{cmp::Ordering, num::NonZeroUsize, time::Duration}, test_case::{test_case, test_matrix}, @@ -5295,46 +5295,6 @@ fn test_update_completed_data_indexes_out_of_order() { assert!(completed_data_indexes.clone().iter().eq([0, 1, 3])); } -#[test] -fn test_rewards_protobuf_backward_compatibility() { - let ledger_path = get_tmp_ledger_path_auto_delete!(); - let blockstore = Blockstore::open(ledger_path.path()).unwrap(); - - let rewards: Rewards = (0..100) - .map(|i| Reward { - pubkey: solana_pubkey::new_rand().to_string(), - lamports: 42 + i, - post_balance: u64::MAX, - reward_type: Some(RewardType::Fee), - commission: None, - commission_bps: None, - }) - .collect(); - let protobuf_rewards: generated::Rewards = rewards.into(); - - let deprecated_rewards: StoredExtendedRewards = protobuf_rewards.clone().into(); - for slot in 0..2 { - let data = bincode::serialize(&deprecated_rewards).unwrap(); - blockstore.rewards_cf.put_bytes(slot, &data).unwrap(); - } - for slot in 2..4 { - blockstore - .rewards_cf - .put_protobuf(slot, &protobuf_rewards) - .unwrap(); - } - for slot in 0..4 { - assert_eq!( - blockstore - .rewards_cf - .get_protobuf_or_wincode::(slot) - .unwrap() - .unwrap(), - protobuf_rewards - ); - } -} - // This test is probably superfluous, since it is highly unlikely that bincode-format // TransactionStatus entries exist in any current ledger. They certainly exist in historical // ledger archives, but typically those require contemporaraneous software for other reasons. diff --git a/storage-proto/src/convert.rs b/storage-proto/src/convert.rs index 590e248a762..ba69e521b84 100644 --- a/storage-proto/src/convert.rs +++ b/storage-proto/src/convert.rs @@ -78,14 +78,14 @@ impl From for Vec { } } -impl From for (Vec, Option) { +impl From for RewardsAndNumPartitions { fn from(rewards: generated::Rewards) -> Self { - ( - rewards.rewards.into_iter().map(|r| r.into()).collect(), - rewards + Self { + rewards: rewards.rewards.into_iter().map(|r| r.into()).collect(), + num_partitions: rewards .num_partitions .map(|generated::NumPartitions { num_partitions }| num_partitions), - ) + } } } diff --git a/transaction-status-client-types/src/lib.rs b/transaction-status-client-types/src/lib.rs index 4391048bcb4..7e8c2a1fc73 100644 --- a/transaction-status-client-types/src/lib.rs +++ b/transaction-status-client-types/src/lib.rs @@ -202,7 +202,7 @@ pub struct EncodedTransactionWithStatusMeta { pub version: Option, } -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SchemaRead, SchemaWrite)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Reward { pub pubkey: String, From 497271fb02c160f0cf08f463e9c30d0e1ce353ef Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Mon, 15 Jun 2026 16:31:33 -0400 Subject: [PATCH 343/576] PER: plumb reward epoch delegated stakes (#13187) --- runtime/Cargo.toml | 2 +- runtime/src/alpenglow_epoch_type.rs | 221 +++++++++++++++++- runtime/src/bank.rs | 42 +--- .../partitioned_epoch_rewards/calculation.rs | 78 ++++++- .../vote_reward/migration_test.rs | 24 +- runtime/src/inflation_rewards/mod.rs | 14 +- runtime/src/inflation_rewards/points.rs | 4 +- runtime/src/stakes.rs | 73 ++++-- 8 files changed, 375 insertions(+), 83 deletions(-) diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 245ee54a1b1..8795a480014 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -141,7 +141,7 @@ solana-poh-config = { workspace = true } solana-precompile-error = { workspace = true } solana-program-binaries = { workspace = true, optional = true } solana-program-runtime = { workspace = true, features = ["metrics"] } -solana-pubkey = { workspace = true, features = ["rand"] } +solana-pubkey = { workspace = true, features = ["rand", "wincode"] } solana-rayon-threadlimit = { workspace = true } solana-rent = { workspace = true } solana-reward-info = { workspace = true } diff --git a/runtime/src/alpenglow_epoch_type.rs b/runtime/src/alpenglow_epoch_type.rs index f516622d271..78d0ed236c3 100644 --- a/runtime/src/alpenglow_epoch_type.rs +++ b/runtime/src/alpenglow_epoch_type.rs @@ -1,4 +1,138 @@ -use solana_clock::{Epoch, Slot}; +use { + crate::bank::{Bank, MAX_ALPENGLOW_VOTE_ACCOUNTS}, + solana_account::{AccountSharedData, ReadableAccount}, + solana_clock::{Epoch, Slot}, + solana_pubkey::Pubkey, + solana_sdk_ids::system_program, + solana_vote::vote_account::VoteAccounts, + std::{cmp::Ordering, collections::HashMap, sync::LazyLock}, + wincode::{SchemaRead, SchemaWrite}, +}; + +/// For alpenglow rewards we only reward lamports earned in the previous epoch. +/// For this prior epoch we need to know the delegated stake for each vote account. +/// Note that this is not the same as `epoch_stakes`, which is calculated an epoch +/// in advance. +#[derive(Debug)] +pub(crate) struct RewardEpochDelegatedStakes { + pub(crate) epoch: Epoch, + #[allow(dead_code)] + // This will be used in a follow up as part of non Tower PER calculations + pub(crate) delegated_stakes: HashMap, +} + +#[derive(Debug, SchemaRead, SchemaWrite)] +struct RewardEpochDelegatedStakesAccount { + pub(crate) epoch: Epoch, + pub(crate) delegated_stakes: Vec, +} + +#[derive(Debug, Clone, SchemaRead, SchemaWrite)] +struct RewardEpochDelegatedStake { + pub(crate) vote_pubkey: Pubkey, + pub(crate) delegated_stake: u64, +} + +/// The off-curve account where we store the bounded reward-epoch delegated stake +/// denominators used for non-Tower reward recalculation after snapshot restore. +static REWARD_EPOCH_DELEGATED_STAKES_ACCOUNT: LazyLock = LazyLock::new(|| { + let (pubkey, _) = Pubkey::find_program_address( + &[b"reward_epoch_delegated_stakes"], + &agave_feature_set::alpenglow::id(), + ); + pubkey +}); + +impl RewardEpochDelegatedStakesAccount { + pub(crate) fn max_size() -> usize { + static REWARD_EPOCH_DELEGATED_STAKES_ACCOUNT_MAX_SIZE: LazyLock = + LazyLock::new(|| { + let account = RewardEpochDelegatedStakesAccount { + epoch: u64::MAX, + delegated_stakes: vec![ + RewardEpochDelegatedStake { + vote_pubkey: Pubkey::new_from_array([u8::MAX; 32]), + delegated_stake: u64::MAX, + }; + MAX_ALPENGLOW_VOTE_ACCOUNTS + ], + }; + wincode::serialized_size(&account) + .expect("reward epoch delegated stakes account must serialize") + .try_into() + .expect( + "reward epoch delegated stakes account serialized size must fit in usize", + ) + }); + + *REWARD_EPOCH_DELEGATED_STAKES_ACCOUNT_MAX_SIZE + } +} + +impl RewardEpochDelegatedStakes { + pub(crate) fn set(&self, bank: &Bank, distribution_vote_accounts: &VoteAccounts) { + assert!( + distribution_vote_accounts.len() <= MAX_ALPENGLOW_VOTE_ACCOUNTS, + "reward epoch delegated stakes account must be bounded by MAX_ALPENGLOW_VOTE_ACCOUNTS" + ); + + let mut delegated_stakes = distribution_vote_accounts + .delegated_stakes() + .map( + |(vote_pubkey, _delegated_stake)| RewardEpochDelegatedStake { + vote_pubkey: *vote_pubkey, + delegated_stake: self + .delegated_stakes + .get(vote_pubkey) + .copied() + .unwrap_or_default(), + }, + ) + .collect::>(); + delegated_stakes.sort_unstable_by_key(|stake| stake.vote_pubkey); + + let account = RewardEpochDelegatedStakesAccount { + epoch: self.epoch, + delegated_stakes, + }; + let data = wincode::serialize(&account).unwrap(); + let lamports = bank + .get_minimum_balance_for_rent_exemption(RewardEpochDelegatedStakesAccount::max_size()); + let mut account = AccountSharedData::new(lamports, data.len(), &system_program::ID); + account.set_data_from_slice(&data); + + bank.store_account_and_update_capitalization( + &REWARD_EPOCH_DELEGATED_STAKES_ACCOUNT, + &account, + ); + } + + pub(crate) fn get(bank: &Bank) -> Option { + let account = bank.get_account(&REWARD_EPOCH_DELEGATED_STAKES_ACCOUNT)?; + (!account.data().is_empty()).then(|| { + let account: RewardEpochDelegatedStakesAccount = wincode::deserialize(account.data()) + .expect("Couldn't deserialize reward epoch delegated stakes"); + assert!( + account.delegated_stakes.len() <= MAX_ALPENGLOW_VOTE_ACCOUNTS, + "reward epoch delegated stakes account exceeds MAX_ALPENGLOW_VOTE_ACCOUNTS" + ); + account.into() + }) + } +} + +impl From for RewardEpochDelegatedStakes { + fn from(account: RewardEpochDelegatedStakesAccount) -> Self { + Self { + epoch: account.epoch, + delegated_stakes: account + .delegated_stakes + .into_iter() + .map(|stake| (stake.vote_pubkey, stake.delegated_stake)) + .collect(), + } + } +} #[derive(Debug)] pub(crate) enum AlpenglowEpochType { @@ -9,7 +143,90 @@ pub(crate) enum AlpenglowEpochType { num_tower_slots: Slot, num_ag_slots: Slot, migration_epoch: Epoch, + #[allow(dead_code)] + // This will be used in a follow up as part of PER calculations for the migration epoch + reward_epoch_delegated_stakes: RewardEpochDelegatedStakes, }, /// This is a full alpenglow epoch - Alpenglow { migration_epoch: Epoch }, + Alpenglow { + migration_epoch: Epoch, + #[allow(dead_code)] + // This will be used in a follow up as part of PER calculations for Alpenglow epochs + reward_epoch_delegated_stakes: RewardEpochDelegatedStakes, + }, +} + +impl AlpenglowEpochType { + pub(crate) fn is_alpenglow_or_migration_epoch(bank: &Bank, epoch: Epoch) -> bool { + debug_assert!(epoch < bank.epoch()); + bank.get_alpenglow_migration_slot() + .map(|migration_slot| bank.epoch_schedule().get_epoch(migration_slot) <= epoch) + .unwrap_or(false) + } + + /// Returns `AlpenglowEpochType` for the given `epoch`. + /// + /// Calling this function with an epoch >= `Bank::epoch` can return false information as it is + /// possible that we have not observed the genesis cert yet but will in the upcoming slots. + /// + /// We pass `reward_epoch_delegated_stakes` as a closure to avoid the potential deserialization + /// of `REWARD_EPOCH_DELEGATED_STAKES_ACCOUNT` in the Tower case. + pub(crate) fn get( + bank: &Bank, + epoch: Epoch, + reward_epoch_delegated_stakes: impl FnOnce() -> Option, + ) -> Self { + debug_assert!(epoch < bank.epoch()); + let Some(migration_slot) = bank.get_alpenglow_migration_slot() else { + return Self::Tower; + }; + let migration_epoch = bank.epoch_schedule().get_epoch(migration_slot); + match migration_epoch.cmp(&epoch) { + Ordering::Less => { + let reward_epoch_delegated_stakes = + reward_epoch_delegated_stakes().unwrap_or_else(|| { + panic!( + "Missing reward epoch delegated stakes for non-Tower reward epoch \ + {epoch}" + ) + }); + assert_eq!( + reward_epoch_delegated_stakes.epoch, epoch, + "reward epoch delegated stakes must match rewarded epoch" + ); + Self::Alpenglow { + migration_epoch, + reward_epoch_delegated_stakes, + } + } + Ordering::Greater => Self::Tower, + Ordering::Equal => { + let first_slot_in_epoch = bank + .epoch_schedule() + .get_first_slot_in_epoch(migration_epoch); + // + 1 because the migration_slot is still tower. + let num_tower_slots = migration_slot - first_slot_in_epoch + 1; + let slots_in_epoch = bank.epoch_schedule().get_slots_in_epoch(migration_epoch); + let num_ag_slots = slots_in_epoch - num_tower_slots; + assert_eq!(slots_in_epoch, num_tower_slots + num_ag_slots); + let reward_epoch_delegated_stakes = + reward_epoch_delegated_stakes().unwrap_or_else(|| { + panic!( + "Missing reward epoch delegated stakes for non-Tower reward epoch \ + {epoch}" + ) + }); + assert_eq!( + reward_epoch_delegated_stakes.epoch, epoch, + "reward epoch delegated stakes must match rewarded epoch" + ); + Self::MigrationEpoch { + num_tower_slots, + num_ag_slots, + migration_epoch, + reward_epoch_delegated_stakes, + } + } + } + } } diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 6d8c20c2b52..16e513d46b2 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -193,7 +193,6 @@ use { }, solana_vote_interface::state::VoteStateV4, std::{ - cmp::Ordering, collections::{HashMap, HashSet}, fmt, num::NonZero, @@ -1717,7 +1716,7 @@ impl Bank { let stakes = self.stakes_cache.stakes(); let stake_delegations = stakes.stake_delegations_vec(); let ( - (stake_history, unfiltered_distribution_vote_accounts), + (stake_history, unfiltered_distribution_vote_accounts, reward_epoch_delegated_stakes), calculate_activated_stake_time_us, ) = measure_us!(stakes.calculate_activated_stake( self.epoch(), @@ -1726,6 +1725,7 @@ impl Bank { &stake_delegations, self.use_fixed_point_stake_math(), )); + debug_assert_eq!(reward_epoch_delegated_stakes.epoch, rewarded_epoch); // Apply stake rewards and commission using the distribution vote-account // snapshot that matches VAT admission filtering when enabled. @@ -1738,6 +1738,13 @@ impl Bank { } else { unfiltered_distribution_vote_accounts.clone() }; + if AlpenglowEpochType::is_alpenglow_or_migration_epoch(self, rewarded_epoch) { + assert!( + self.feature_set.snapshot().validator_admission_ticket, + "Alpenglow should not be activated before the VAT" + ); + reward_epoch_delegated_stakes.set(self, &filtered_distribution_vote_accounts); + } let cached_vote_accounts = self.get_cached_vote_accounts(rewarded_epoch, &filtered_distribution_vote_accounts); let (rewards_calculation, update_rewards_with_thread_pool_time_us) = @@ -1746,6 +1753,7 @@ impl Bank { stake_delegations, cached_vote_accounts, rewarded_epoch, + reward_epoch_delegated_stakes, reward_calc_tracer, thread_pool, rewards_metrics, @@ -6562,36 +6570,6 @@ impl Bank { ); Some(genesis_cert.cert_type.slot()) } - - /// Returns `AlpenglowEpochType` for the given `epoch`. - /// - /// Calling this function with an epoch >= `Bank::epoch` can return false information as it is - /// possible that we have not observed the genesis cert yet but will in the upcoming slots. - pub(crate) fn get_alpenglow_epoch_type(&self, epoch: Epoch) -> AlpenglowEpochType { - debug_assert!(epoch < self.epoch); - let Some(migration_slot) = self.get_alpenglow_migration_slot() else { - return AlpenglowEpochType::Tower; - }; - let migration_epoch = self.epoch_schedule.get_epoch(migration_slot); - match migration_epoch.cmp(&epoch) { - Ordering::Less => AlpenglowEpochType::Alpenglow { migration_epoch }, - Ordering::Greater => AlpenglowEpochType::Tower, - Ordering::Equal => { - let first_slot_in_epoch = - self.epoch_schedule.get_first_slot_in_epoch(migration_epoch); - // + 1 because the migration_slot is still tower. - let num_tower_slots = migration_slot - first_slot_in_epoch + 1; - let slots_in_epoch = self.epoch_schedule.get_slots_in_epoch(migration_epoch); - let num_ag_slots = slots_in_epoch - num_tower_slots; - assert_eq!(slots_in_epoch, num_tower_slots + num_ag_slots); - AlpenglowEpochType::MigrationEpoch { - num_tower_slots, - num_ag_slots, - migration_epoch, - } - } - } - } } impl InvokeContextCallback for Bank { diff --git a/runtime/src/bank/partitioned_epoch_rewards/calculation.rs b/runtime/src/bank/partitioned_epoch_rewards/calculation.rs index 87e4e022c0a..8b2dea8168f 100644 --- a/runtime/src/bank/partitioned_epoch_rewards/calculation.rs +++ b/runtime/src/bank/partitioned_epoch_rewards/calculation.rs @@ -7,7 +7,7 @@ use { StakeRewardCalculation, epoch_rewards_hasher::hash_rewards_into_partitions, }, crate::{ - alpenglow_epoch_type::AlpenglowEpochType, + alpenglow_epoch_type::{AlpenglowEpochType, RewardEpochDelegatedStakes}, bank::{ RewardCalcTracer, RewardCalculationEvent, RewardsMetrics, fee_distribution::ExternalCollectorType, null_tracer, @@ -224,6 +224,7 @@ impl Bank { stake_delegations: Vec<(&Pubkey, &StakeAccount)>, cached_vote_accounts: CachedVoteAccounts<'_>, rewarded_epoch: Epoch, + reward_epoch_delegated_stakes: RewardEpochDelegatedStakes, reward_calc_tracer: Option, thread_pool: &ThreadPool, metrics: &mut RewardsMetrics, @@ -257,6 +258,7 @@ impl Bank { stake_delegations, cached_vote_accounts, rewarded_epoch, + reward_epoch_delegated_stakes, reward_calc_tracer, thread_pool, metrics, @@ -394,6 +396,7 @@ impl Bank { stake_delegations: Vec<(&'a Pubkey, &'a StakeAccount)>, cached_vote_accounts: CachedVoteAccounts<'_>, rewarded_epoch: Epoch, + reward_epoch_delegated_stakes: RewardEpochDelegatedStakes, reward_calc_tracer: Option, thread_pool: &ThreadPool, metrics: &mut RewardsMetrics, @@ -418,6 +421,7 @@ impl Bank { cached_vote_accounts, rewarded_epoch, epoch_inflation_rewards, + reward_epoch_delegated_stakes, reward_calc_tracer, thread_pool, metrics, @@ -439,6 +443,7 @@ impl Bank { } /// Calculate epoch reward and return stake rewards and commissions. + #[allow(clippy::too_many_arguments)] fn calculate_validator_rewards<'a>( &self, stake_history: &StakeHistory, @@ -446,11 +451,13 @@ impl Bank { cached_vote_accounts: CachedVoteAccounts<'_>, rewarded_epoch: Epoch, epoch_inflation_rewards: u64, + reward_epoch_delegated_stakes: RewardEpochDelegatedStakes, reward_calc_tracer: Option, thread_pool: &ThreadPool, metrics: &mut RewardsMetrics, ) -> Option { - let ag_epoch_type = self.get_alpenglow_epoch_type(rewarded_epoch); + let ag_epoch_type = + AlpenglowEpochType::get(self, rewarded_epoch, || Some(reward_epoch_delegated_stakes)); self.calculate_reward_points_partitioned( stake_history, &stake_delegations, @@ -905,7 +912,6 @@ impl Bank { // If rewards are active, the rewarded epoch is always the immediately // preceding epoch. let rewarded_epoch = self.epoch().saturating_sub(1); - let ag_epoch_type = self.get_alpenglow_epoch_type(rewarded_epoch); let point_value = PointValue { rewards: epoch_rewards_sysvar.total_rewards, @@ -918,6 +924,9 @@ impl Bank { stake_delegations, cached_vote_accounts, } = self.get_epoch_params_for_recalculation(rewarded_epoch, &stakes); + let ag_epoch_type = AlpenglowEpochType::get(self, rewarded_epoch, || { + RewardEpochDelegatedStakes::get(self) + }); // On recalculation, only the `StakeRewardCalculation::stake_rewards` // field is relevant. It is assumed that reward commission accounts have @@ -1137,12 +1146,19 @@ mod tests { }, solana_vote_program::vote_state::{self, create_bls_proof_of_possession}, std::{ - collections::HashSet, + collections::{HashMap, HashSet}, sync::{Arc, RwLock, RwLockReadGuard}, }, test_case::{test_case, test_matrix}, }; + fn reward_epoch_delegated_stakes_for_tests(epoch: Epoch) -> RewardEpochDelegatedStakes { + RewardEpochDelegatedStakes { + epoch, + delegated_stakes: HashMap::default(), + } + } + #[test] fn test_store_commission_accounts_partitioned() { let (genesis_config, _mint_keypair) = create_genesis_config(1_000_000 * LAMPORTS_PER_SOL); @@ -1254,6 +1270,7 @@ mod tests { cached_vote_accounts, rewarded_epoch, expected_rewards, + reward_epoch_delegated_stakes_for_tests(rewarded_epoch), null_tracer(), &thread_pool, &mut rewards_metrics, @@ -2114,6 +2131,7 @@ mod tests { stake_delegations, cached_vote_accounts, rewarded_epoch, + reward_epoch_delegated_stakes_for_tests(rewarded_epoch), null_tracer(), &thread_pool, &mut rewards_metrics, @@ -2235,6 +2253,7 @@ mod tests { stake_delegations, cached_vote_accounts, rewarded_epoch, + reward_epoch_delegated_stakes_for_tests(rewarded_epoch), null_tracer(), &thread_pool, &mut rewards_metrics, @@ -2330,6 +2349,56 @@ mod tests { assert_eq!(bank.epoch_reward_status, EpochRewardStatus::Inactive); } + #[test] + fn test_alpenglow_reward_epoch_delegated_stakes_account_is_bounded() { + let num_validators = crate::bank::MAX_ALPENGLOW_VOTE_ACCOUNTS + 1; + let validator_keypairs = (0..num_validators) + .map(|_| genesis_utils::ValidatorVoteKeypairs::new_rand()) + .collect::>(); + // Unique stakes make VAT filtering exclude only the lowest-staked vote + // account instead of dropping a tie group at the boundary. + let stakes = (0..num_validators) + .map(|index| 2_000_000_000 + (num_validators - index) as u64) + .collect::>(); + let filtered_vote_pubkey = validator_keypairs + .last() + .expect("validator keypairs must not be empty") + .vote_keypair + .pubkey(); + let GenesisConfigInfo { + mut genesis_config, .. + } = genesis_utils::create_genesis_config_with_alpenglow_vote_accounts( + 1_000_000_000 * LAMPORTS_PER_SOL, + &validator_keypairs, + stakes, + ); + genesis_config.epoch_schedule = EpochSchedule::new(SLOTS_PER_EPOCH); + let features_to_deactivate = crate::slot_params::slot_time_feature_ids().to_vec(); + deactivate_features(&mut genesis_config, &features_to_deactivate); + + let (bank, bank_forks) = + Bank::new_for_tests(&genesis_config).wrap_with_bank_forks_for_tests(); + let bank = Bank::new_from_parent_with_bank_forks( + bank_forks.as_ref(), + bank, + SlotLeader::default(), + SLOTS_PER_EPOCH, + ); + + let reward_epoch_delegated_stakes = RewardEpochDelegatedStakes::get(&bank) + .expect("AG reward epoch delegated stakes must be persisted"); + assert_eq!(reward_epoch_delegated_stakes.epoch, bank.epoch() - 1); + assert_eq!( + reward_epoch_delegated_stakes.delegated_stakes.len(), + crate::bank::MAX_ALPENGLOW_VOTE_ACCOUNTS + ); + assert!( + !reward_epoch_delegated_stakes + .delegated_stakes + .contains_key(&filtered_vote_pubkey) + ); + } + #[test] fn test_initialize_after_snapshot_restore() { let expected_num_stake_rewards = 4; @@ -2451,7 +2520,6 @@ mod tests { .enumerated_rewards_iter() .all(|(_, reward)| reward.stake_pubkey != filtered_stake_pubkey) ); - // Simulate snapshot restore: re-apply features from accounts and // rebuild epoch_reward_status from snapshot-stable state. bank.feature_set = Arc::new(FeatureSet::default()); diff --git a/runtime/src/block_component_processor/vote_reward/migration_test.rs b/runtime/src/block_component_processor/vote_reward/migration_test.rs index f6ba256d2f6..9a07c87f81f 100644 --- a/runtime/src/block_component_processor/vote_reward/migration_test.rs +++ b/runtime/src/block_component_processor/vote_reward/migration_test.rs @@ -20,7 +20,7 @@ mod tests { certificate::{Certificate, CertificateType}, consensus_message::Block, }, - solana_account::{Account, ReadableAccount}, + solana_account::{Account, ReadableAccount, from_account}, solana_bls_signatures::{BLS_SIGNATURE_AFFINE_SIZE, Signature as BLSSignature}, solana_cluster_type::ClusterType, solana_epoch_schedule::EpochSchedule, @@ -34,6 +34,7 @@ mod tests { solana_rent::Rent, solana_signer::Signer, solana_stake_interface::state::StakeStateV2, + solana_sysvar::{self as sysvar, epoch_rewards::EpochRewards}, std::{collections::HashMap, num::NonZero, sync::Arc}, test_case::test_matrix, }; @@ -271,21 +272,12 @@ mod tests { return ret; } - let total_points = self - .validators - .iter() - .map(|validator| { - let vote_pubkey = validator.vote_keypair.pubkey(); - let validator_stake = self.get_validator_stake(reward_bank, &vote_pubkey); - let points = validator_stake * self.pay_type.tower(); - points as u128 - }) - .sum::(); - - let epoch_inflation = payout_bank.calculate_epoch_inflation_rewards( - reward_bank.capitalization(), - reward_bank.epoch(), - ); + let epoch_rewards: EpochRewards = payout_bank + .get_account(&sysvar::epoch_rewards::id()) + .and_then(|account| from_account(&account)) + .unwrap(); + let epoch_inflation = epoch_rewards.total_rewards; + let total_points = epoch_rewards.total_points; let genesis_cert = payout_bank.get_alpenglow_genesis_certificate().unwrap(); let first_slot_in_reward_epoch = payout_bank .epoch_schedule diff --git a/runtime/src/inflation_rewards/mod.rs b/runtime/src/inflation_rewards/mod.rs index 6151b55d861..4c2d9cababa 100644 --- a/runtime/src/inflation_rewards/mod.rs +++ b/runtime/src/inflation_rewards/mod.rs @@ -297,7 +297,7 @@ fn calculate_stake_rewards<'a>( AlpenglowEpochType::MigrationEpoch { num_tower_slots, num_ag_slots, - migration_epoch: _, + .. } => { if tower_points == 0 && ag_points == 0 { if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() { @@ -404,7 +404,9 @@ mod tests { use { self::points::{PointValue, null_tracer}, super::*, - crate::epoch_stakes::VersionedEpochStakes, + crate::{ + alpenglow_epoch_type::RewardEpochDelegatedStakes, epoch_stakes::VersionedEpochStakes, + }, proptest::prelude::*, solana_clock::Epoch, solana_native_token::LAMPORTS_PER_SOL, @@ -451,7 +453,13 @@ mod tests { let migration_epoch = 0; let first_ag_epoch = migration_epoch + 1; ( - AlpenglowEpochType::Alpenglow { migration_epoch }, + AlpenglowEpochType::Alpenglow { + migration_epoch, + reward_epoch_delegated_stakes: RewardEpochDelegatedStakes { + epoch: first_ag_epoch, + delegated_stakes: [(Pubkey::default(), total_stake)].into_iter().collect(), + }, + }, epoch_stakes, total_stake, first_ag_epoch, diff --git a/runtime/src/inflation_rewards/points.rs b/runtime/src/inflation_rewards/points.rs index 5877197f17b..2c34b17496e 100644 --- a/runtime/src/inflation_rewards/points.rs +++ b/runtime/src/inflation_rewards/points.rs @@ -474,7 +474,9 @@ pub(crate) fn calculate_stake_points_and_credits( Err(e) => return e, } } - AlpenglowEpochType::Alpenglow { migration_epoch } => { + AlpenglowEpochType::Alpenglow { + migration_epoch, .. + } => { let (ag_points, credits) = match ag_epoch_credits_iter( *migration_epoch, false, diff --git a/runtime/src/stakes.rs b/runtime/src/stakes.rs index 4f0bf641ac1..dada5372f68 100644 --- a/runtime/src/stakes.rs +++ b/runtime/src/stakes.rs @@ -2,6 +2,7 @@ //! node stakes use { crate::{ + alpenglow_epoch_type::RewardEpochDelegatedStakes, stake_account, stake_delegation::{delegation_activation_status, delegation_effective_stake}, stake_history::StakeHistory, @@ -24,7 +25,6 @@ use { solana_vote_interface::state::VoteStateVersions, std::{ collections::HashMap, - ops::Add, sync::{Arc, RwLock, RwLockReadGuard}, }, thiserror::Error, @@ -411,15 +411,15 @@ impl Stakes { new_rate_activation_epoch: Option, stake_delegations: &[(&Pubkey, &StakeAccount)], use_fixed_point_stake_math: bool, - ) -> (StakeHistory, VoteAccounts) { + ) -> (StakeHistory, VoteAccounts, RewardEpochDelegatedStakes) { // Wrap up the prev epoch by adding new stake history entry for the // prev epoch. - let stake_history_entry = thread_pool.install(|| { + let (stake_history_entry, effective_delegated_stakes) = thread_pool.install(|| { stake_delegations .par_iter() .fold( - StakeActivationStatus::default, - |acc, (_stake_pubkey, stake_account)| { + || (StakeActivationStatus::default(), HashMap::default()), + |(acc, mut delegated_stakes), (_stake_pubkey, stake_account)| { let delegation = stake_account.delegation(); let activation_status = delegation_activation_status( delegation, @@ -428,10 +428,21 @@ impl Stakes { new_rate_activation_epoch, use_fixed_point_stake_math, ); - acc + activation_status + *delegated_stakes.entry(delegation.voter_pubkey).or_default() += + activation_status.effective; + (acc + activation_status, delegated_stakes) + }, + ) + .reduce( + || (StakeActivationStatus::default(), HashMap::default()), + |(activation_status_a, delegated_stakes_a), + (activation_status_b, delegated_stakes_b)| { + ( + activation_status_a + activation_status_b, + merge_delegated_stakes(delegated_stakes_a, delegated_stakes_b), + ) }, ) - .reduce(StakeActivationStatus::default, Add::add) }); let mut stake_history = self.stake_history.clone(); stake_history.add(self.epoch, stake_history_entry); @@ -446,7 +457,11 @@ impl Stakes { new_rate_activation_epoch, use_fixed_point_stake_math, ); - (stake_history, vote_accounts) + let reward_epoch_delegated_stakes = RewardEpochDelegatedStakes { + epoch: self.epoch, + delegated_stakes: effective_delegated_stakes, + }; + (stake_history, vote_accounts, reward_epoch_delegated_stakes) } pub(crate) fn activate_epoch( @@ -668,6 +683,19 @@ impl From> for Stakes { } } +fn merge_delegated_stakes( + mut stakes: HashMap, + other: HashMap, +) -> HashMap { + if stakes.len() < other.len() { + return merge_delegated_stakes(other, stakes); + } + for (pubkey, stake) in other { + *stakes.entry(pubkey).or_default() += stake; + } + stakes +} + fn refresh_vote_accounts( thread_pool: &ThreadPool, epoch: Epoch, @@ -677,16 +705,6 @@ fn refresh_vote_accounts( new_rate_activation_epoch: Option, use_fixed_point_stake_math: bool, ) -> VoteAccounts { - type StakesHashMap = HashMap; - fn merge(mut stakes: StakesHashMap, other: StakesHashMap) -> StakesHashMap { - if stakes.len() < other.len() { - return merge(other, stakes); - } - for (pubkey, stake) in other { - *stakes.entry(pubkey).or_default() += stake; - } - stakes - } let delegated_stakes = thread_pool.install(|| { stake_delegations .par_iter() @@ -706,7 +724,7 @@ fn refresh_vote_accounts( delegated_stakes }, ) - .reduce(HashMap::default, merge) + .reduce(HashMap::default, merge_delegated_stakes) }); vote_accounts .iter() @@ -1091,19 +1109,21 @@ pub(crate) mod tests { .stake() .unwrap(); + let initial_expected_stake = { + let stakes = stakes_cache.stakes(); + effective_stake(&stake, stakes.epoch, &stakes.stake_history, None, true) + }; { let stakes = stakes_cache.stakes(); let vote_accounts = stakes.vote_accounts(); - let expected_stake = - effective_stake(&stake, stakes.epoch, &stakes.stake_history, None, true); assert_eq!( vote_accounts.get_delegated_stake(&vote_pubkey), - expected_stake + initial_expected_stake ); } let thread_pool = ThreadPoolBuilder::new().num_threads(1).build().unwrap(); let next_epoch = 3; - let (stake_history, vote_accounts) = { + let (stake_history, vote_accounts, effective_delegated_stakes) = { let stakes = stakes_cache.stakes(); let stake_delegations = stakes.stake_delegations_vec(); stakes.calculate_activated_stake( @@ -1114,6 +1134,13 @@ pub(crate) mod tests { true, ) }; + assert_eq!( + effective_delegated_stakes + .delegated_stakes + .get(&vote_pubkey) + .copied(), + Some(initial_expected_stake) + ); stakes_cache.activate_epoch(next_epoch, stake_history, vote_accounts); { let stakes = stakes_cache.stakes(); From 8b02d0cf2beb6fccbd7fdd8bd8f35a2c12219ce5 Mon Sep 17 00:00:00 2001 From: Rory Harris Date: Mon, 15 Jun 2026 13:51:29 -0700 Subject: [PATCH 344/576] Remerge unflushed root simplification (#13214) * Reapply "Collapse unflushed roots (#13174)" (#13189) * Remove unflushed root debug assert - Assert is invalid because any new root adds all of its parents to unflushed roots - Sometimes that will include slots thaht have already been flushed, re-adding them to to the write cache - They will get cleared out benignly on the next cycle of flush_slots --- accounts-db/src/accounts_cache.rs | 196 ++++++++++----------------- accounts-db/src/accounts_db.rs | 11 +- accounts-db/src/accounts_db/tests.rs | 26 ++++ 3 files changed, 101 insertions(+), 132 deletions(-) diff --git a/accounts-db/src/accounts_cache.rs b/accounts-db/src/accounts_cache.rs index f2bf7736e45..e38975f50d3 100644 --- a/accounts-db/src/accounts_cache.rs +++ b/accounts-db/src/accounts_cache.rs @@ -234,12 +234,10 @@ pub struct AccountsCache { cache: DashMap, BuildNoHashHasher>, // Index to find which slots a pubkey is stored in, to speed up lookups in load_latest index: AccountsCacheIndex, - // Queue of potentially unflushed roots. Random eviction + cache too large - // could have triggered a flush of this slot already - maybe_unflushed_roots: RwLock>, - // Roots that have been claimed for flushing by `begin_flush_roots` but not yet - // cleared by `end_flush_roots` - roots_being_flushed: RwLock>, + // Rooted slots that may still have accounts in the write cache. A slot enters via `add_root` + // and is dropped either by `remove_slot` when its cache is flushed, or by + // `remove_unflushed_root` when a flush finds it had no cache. + unflushed_roots: RwLock>, max_flushed_root: MaxFlushedRoot, /// The size of account data stored in the whole AccountsCache, in bytes total_size: Arc, @@ -267,11 +265,7 @@ impl AccountsCache { pub fn report_size(&self) { datapoint_info!( "accounts_cache_size", - ( - "num_roots", - self.maybe_unflushed_roots.read().unwrap().len(), - i64 - ), + ("num_roots", self.unflushed_roots.read().unwrap().len(), i64), ("num_slots", self.cache.len(), i64), ("total_size", self.size(), i64), ( @@ -324,6 +318,8 @@ impl AccountsCache { if let Some(slot_cache) = &result { self.index.remove(slot_cache.iter().map(|item| *item.key())); } + // If this slot was a root, it has now left the cache, so stop tracking it as unflushed. + self.unflushed_roots.write().unwrap().remove(&slot); result } @@ -361,21 +357,13 @@ impl AccountsCache { .unwrap_or(index_max_slot) .min(index_max_slot); - let r_maybe_unflushed_roots = self.maybe_unflushed_roots.read().unwrap(); - for &slot in r_maybe_unflushed_roots.range(..=max_root_slot).rev() { + let r_unflushed_roots = self.unflushed_roots.read().unwrap(); + for &slot in r_unflushed_roots.range(..=max_root_slot).rev() { if let Some(account) = self.load(slot, pubkey) { return Some((account, slot)); } } - drop(r_maybe_unflushed_roots); - - let r_roots_being_flushed = self.roots_being_flushed.read().unwrap(); - for &slot in r_roots_being_flushed.range(..=max_root_slot).rev() { - if let Some(account) = self.load(slot, pubkey) { - return Some((account, slot)); - } - } - drop(r_roots_being_flushed); + drop(r_unflushed_roots); // Found nothing, the version of the account in the cache must be on a different fork None @@ -386,42 +374,25 @@ impl AccountsCache { } pub fn add_root(&self, root: Slot) { - self.maybe_unflushed_roots.write().unwrap().insert(root); + self.unflushed_roots.write().unwrap().insert(root); } pub fn num_unflushed_roots(&self) -> usize { - self.maybe_unflushed_roots.read().unwrap().len() - } - - /// Atomically moves roots up to and including `max_root` (or all roots if `None`) out of - /// `maybe_unflushed_roots` and into `roots_being_flushed`, then returns them - /// - /// Panics if there are already roots being flushed (i.e. `end_flush_roots` was not called after - /// a previous `begin_flush_roots`) - pub fn begin_flush_roots(&self, max_root: Option) -> BTreeSet { - let mut w_maybe_unflushed_roots = self.maybe_unflushed_roots.write().unwrap(); - let mut w_roots_being_flushed = self.roots_being_flushed.write().unwrap(); - - assert!( - w_roots_being_flushed.is_empty(), - "begin_flush_roots called while roots are already being flushed; end_flush_roots must \ - be called first" - ); - - let roots_to_flush = if let Some(max_root) = max_root { - let remaining = w_maybe_unflushed_roots.split_off(&(max_root + 1)); - std::mem::replace(&mut *w_maybe_unflushed_roots, remaining) - } else { - std::mem::take(&mut *w_maybe_unflushed_roots) - }; - - *w_roots_being_flushed = roots_to_flush.clone(); - roots_to_flush - } - - /// Signals that flushing is complete. Clears the roots that were staged by `begin_flush_roots` - pub fn end_flush_roots(&self) { - self.roots_being_flushed.write().unwrap().clear(); + self.unflushed_roots.read().unwrap().len() + } + + /// Returns the unflushed roots up to and including `max_root` (or all roots if `None`). The + /// returned roots remain tracked as unflushed until `remove_slot` drops each one when its cache + /// is flushed. + pub fn roots_to_flush(&self, max_root: Option) -> BTreeSet { + // `None` means no upper bound, i.e. every root. + let max_root = max_root.unwrap_or(Slot::MAX); + self.unflushed_roots + .read() + .unwrap() + .range(..=max_root) + .copied() + .collect() } pub fn cached_frozen_slots(&self) -> Vec { @@ -461,6 +432,12 @@ impl AccountsCache { self.max_flushed_root.get() } + /// Stop tracking `slot` as an unflushed root without touching its cache. Used when a flushed + /// root had no cache for `remove_slot` to drop (e.g. genesis), so it would otherwise linger. + pub fn remove_unflushed_root(&self, slot: Slot) { + self.unflushed_roots.write().unwrap().remove(&slot); + } + pub fn set_max_flush_root(&self, root: Slot) { self.max_flushed_root.fetch_max(root); } @@ -661,31 +638,25 @@ mod tests { } /// Tests that `load_latest` returns the correct slot and account value - /// given various combinations of ancestor slots, root slots, and flushing state. + /// given various combinations of ancestor slots and root slots. /// /// Ancestors always take priority over roots regardless of slot // None case // `uncached_ancestors` are slots added to the Ancestors set but with no account // data stored in the cache. This lets us test root bounding by min_slot // without the ancestor path short-circuiting the lookup. - #[test_case(&[], &[], &[], &[], None; "not ancestor not root")] - #[test_case(&[10], &[], &[], &[], Some(10); "ancestor only")] - #[test_case(&[5, 10, 15], &[], &[], &[], Some(15); "highest ancestor returned")] - #[test_case(&[], &[10, 20], &[], &[], Some(20); "rooted, with no ancestors")] - #[test_case(&[], &[], &[10], &[], Some(10); "root being flushed")] - #[test_case(&[10], &[], &[10], &[], Some(10); "ancestor being flushed")] - #[test_case(&[5], &[20], &[], &[], Some(5); "ancestor wins over higher root")] - #[test_case(&[], &[20], &[10], &[], Some(20);"unflushed root over flushing root")] - #[test_case(&[5], &[20], &[10], &[], Some(5);"ancestor over unflushed and flushing roots")] + #[test_case(&[], &[], &[], None; "not ancestor not root")] + #[test_case(&[10], &[], &[], Some(10); "ancestor only")] + #[test_case(&[5, 10, 15], &[], &[], Some(15); "highest ancestor returned")] + #[test_case(&[], &[10, 20], &[], Some(20); "rooted, with no ancestors")] + #[test_case(&[5], &[20], &[], Some(5); "ancestor wins over higher root")] // Root beyond ancestors.min_slot() is excluded; older root still found - #[test_case(&[], &[5, 11], &[], &[10], Some(5); "unflushed root beyond min ancestor excluded")] - #[test_case(&[], &[], &[5, 11], &[10], Some(5); "flushing root beyond min ancestor excluded")] + #[test_case(&[], &[5, 11], &[10], Some(5); "root beyond min ancestor excluded")] // Root within min_slot bound is still returned - #[test_case(&[], &[10], &[], &[15], Some(10); "unflushed root below min ancestor returned")] + #[test_case(&[], &[10], &[15], Some(10); "root below min ancestor returned")] fn test_load_latest_slot_priority( ancestor_slots: &[Slot], - unflushed_root_slots: &[Slot], - flushing_root_slots: &[Slot], + root_slots: &[Slot], uncached_ancestors: &[Slot], expected: Option, ) { @@ -699,7 +670,7 @@ mod tests { AccountSharedData::new(slot, 0, &Pubkey::default()), ); } - for &slot in unflushed_root_slots.iter().chain(flushing_root_slots) { + for &slot in root_slots { cache.store( slot, &pk, @@ -707,9 +678,6 @@ mod tests { ); cache.add_root(slot); } - if let Some(&max) = flushing_root_slots.iter().max() { - cache.begin_flush_roots(Some(max)); - } let mut all_ancestors: Vec = ancestor_slots.to_vec(); all_ancestors.extend_from_slice(uncached_ancestors); @@ -741,91 +709,63 @@ mod tests { cache.store(10, &pk, AccountSharedData::new(100, 0, &Pubkey::default())); cache.add_root(10); - cache.begin_flush_roots(None); - cache.end_flush_roots(); + // A flush finishes a slot with `remove_slot`, which drops both its cache and its + // unflushed-root tracking; call it directly here to stand in for that flush. + cache.remove_slot(10); - // After clearing flushed roots, slot is no longer visible + // With the slot gone from the cache and untracked as a root, it is not visible. let empty = Ancestors::default(); assert!(cache.load_latest(&pk, &empty).is_none()); + assert!(cache.unflushed_roots.read().unwrap().is_empty()); } #[test] - fn test_begin_flush_roots_with_max_root() { + fn test_roots_to_flush_with_max_root() { let cache = AccountsCache::default(); cache.add_root(1); cache.add_root(3); cache.add_root(5); cache.add_root(7); - // begin_flush_roots(Some(5)) should return {1, 3, 5} and leave {7} - let taken = cache.begin_flush_roots(Some(5)); - assert_eq!(taken, BTreeSet::from([1, 3, 5])); - - // Remaining unflushed roots should only contain 7 - assert_eq!(cache.maybe_unflushed_roots.read().unwrap().len(), 1); - assert!(cache.maybe_unflushed_roots.read().unwrap().contains(&7)); - - // Taken roots should now be in roots_being_flushed - assert!(cache.roots_being_flushed.read().unwrap().contains(&1)); - assert!(cache.roots_being_flushed.read().unwrap().contains(&3)); - assert!(cache.roots_being_flushed.read().unwrap().contains(&5)); - assert!(!cache.roots_being_flushed.read().unwrap().contains(&7)); + // roots_to_flush(Some(5)) returns {1, 3, 5}, excluding 7. + let to_flush = cache.roots_to_flush(Some(5)); + assert_eq!(to_flush, BTreeSet::from([1, 3, 5])); - cache.end_flush_roots(); + // roots_to_flush only reads: the tracked roots are unchanged. + assert_eq!( + *cache.unflushed_roots.read().unwrap(), + BTreeSet::from([1, 3, 5, 7]) + ); } #[test] - fn test_begin_flush_roots_none_takes_all() { + fn test_roots_to_flush_none_takes_all() { let cache = AccountsCache::default(); cache.add_root(2); cache.add_root(4); cache.add_root(6); - let taken = cache.begin_flush_roots(None); - assert_eq!(taken, BTreeSet::from([2, 4, 6])); + let to_flush = cache.roots_to_flush(None); + assert_eq!(to_flush, BTreeSet::from([2, 4, 6])); - // All unflushed roots should be drained - assert!(cache.maybe_unflushed_roots.read().unwrap().is_empty()); - - // All should be in roots_being_flushed + // roots_to_flush only reads: the tracked roots are unchanged. assert_eq!( - *cache.roots_being_flushed.read().unwrap(), + *cache.unflushed_roots.read().unwrap(), BTreeSet::from([2, 4, 6]) ); - - cache.end_flush_roots(); - } - - #[test] - #[should_panic(expected = "begin_flush_roots called while roots are already being flushed")] - fn test_begin_flush_roots_panics_if_already_flushing() { - let cache = AccountsCache::default(); - cache.add_root(1); - cache.begin_flush_roots(None); - // Calling again without end_flush_roots should panic - cache.add_root(2); - cache.begin_flush_roots(None); } #[test] - fn test_end_flush_roots_allows_next_flush() { + fn test_remove_slot_drops_unflushed_root() { let cache = AccountsCache::default(); + let pk = Pubkey::new_unique(); + cache.store(1, &pk, AccountSharedData::new(1, 0, &Pubkey::default())); cache.add_root(1); + cache.store(2, &pk, AccountSharedData::new(2, 0, &Pubkey::default())); cache.add_root(2); - // First cycle - let taken = cache.begin_flush_roots(None); - assert_eq!(taken, BTreeSet::from([1, 2])); - - cache.end_flush_roots(); - - // After clear, roots_being_flushed is empty - assert!(cache.roots_being_flushed.read().unwrap().is_empty()); - - // Second cycle works without panic - cache.add_root(3); - let taken = cache.begin_flush_roots(None); - assert_eq!(taken, BTreeSet::from([3])); - cache.end_flush_roots(); + // remove_slot drops slot 1 from both the cache and the tracked roots, leaving slot 2. + cache.remove_slot(1); + assert_eq!(*cache.unflushed_roots.read().unwrap(), BTreeSet::from([2])); } } diff --git a/accounts-db/src/accounts_db.rs b/accounts-db/src/accounts_db.rs index 118aced28b2..2b25609a493 100644 --- a/accounts-db/src/accounts_db.rs +++ b/accounts-db/src/accounts_db.rs @@ -4693,7 +4693,7 @@ impl AccountsDb { ) -> (usize, usize, FlushStats) { // Always flush up to `requested_flush_root`, which is necessary for things like snapshotting. let flushed_roots: BTreeSet = - self.accounts_cache.begin_flush_roots(requested_flush_root); + self.accounts_cache.roots_to_flush(requested_flush_root); let max_flush_root = flushed_roots.last().copied(); let num_new_roots = flushed_roots.len(); @@ -4721,14 +4721,17 @@ impl AccountsDb { if let Some(stats) = self.flush_slot_cache(root, &pubkeys_to_flush[&root]) { num_roots_flushed += 1; flush_stats.accumulate(&stats); + } else { + // A root with no cache to flush (e.g. genesis, whose accounts load straight to + // storage) was still tracked by `add_root`. `flush_slot_cache` couldn't drop it + // via `remove_slot`, so untrack it here to keep it from lingering as an unflushed + // root at or below max_flushed_root. + self.accounts_cache.remove_unflushed_root(root); } } max_flush_root.inspect(|&root| self.accounts_cache.set_max_flush_root(root)); - // Now that all the rooted slots are flushed, we can clear the roots from the cache - self.accounts_cache.end_flush_roots(); - (num_new_roots, num_roots_flushed, flush_stats) } diff --git a/accounts-db/src/accounts_db/tests.rs b/accounts-db/src/accounts_db/tests.rs index 5c5aab07dd1..221cf462aa0 100644 --- a/accounts-db/src/accounts_db/tests.rs +++ b/accounts-db/src/accounts_db/tests.rs @@ -4203,6 +4203,32 @@ fn test_flush_rooted_accounts_cache_with_clean() { fn test_flush_rooted_accounts_cache_without_clean() { run_flush_rooted_accounts_cache(false); } + +/// A rooted slot with no write-cache entry (e.g. genesis, whose accounts load straight to storage) +/// is still tracked by `add_root`. Flushing must untrack it rather than leave it stranded in +/// `unflushed_roots` at or below `max_flushed_root`. +#[test] +fn test_flush_untracks_cacheless_root() { + let db = AccountsDb::new_single_for_tests(); + + // Slot 0: rooted but never written to the write cache. + db.accounts_cache.add_root(0); + + // Slot 10: a normal rooted slot with a cached account. + let pubkey = Pubkey::new_unique(); + db.accounts_cache.store( + 10, + &pubkey, + AccountSharedData::new(10, 0, &Pubkey::default()), + ); + db.add_root(10); + + // Flushing through slot 10 must drop the cacheless root 0 instead of stranding it below + // max_flushed_root (which would otherwise trip the unflushed-root invariant). + db.flush_accounts_cache(true, Some(10)); + + assert_eq!(db.accounts_cache.num_unflushed_roots(), 0); +} #[test] fn test_shrink_unref() { let db = AccountsDb::new_single_for_tests(); From 20adfc0fed2eb369e0934c74279b6823d89d79c2 Mon Sep 17 00:00:00 2001 From: steviez Date: Mon, 15 Jun 2026 16:33:32 -0500 Subject: [PATCH 345/576] blockstore: Remove legacy protobuf_or_wincode path (#13218) This code was present for when bincode columns were switched to protobuf. The only remaining caller is test code so remove it --- ledger/src/blockstore/tests.rs | 95 +--------------------------------- ledger/src/blockstore_db.rs | 44 +--------------- 2 files changed, 3 insertions(+), 136 deletions(-) diff --git a/ledger/src/blockstore/tests.rs b/ledger/src/blockstore/tests.rs index 047c1db6a3c..1ddf58bce81 100644 --- a/ledger/src/blockstore/tests.rs +++ b/ledger/src/blockstore/tests.rs @@ -12,7 +12,6 @@ use { assert_matches::assert_matches, crossbeam_channel::unbounded, rand::{rng, seq::SliceRandom}, - solana_account_decoder::parse_token::UiTokenAmount, solana_entry::entry::next_entry_mut, solana_genesis_utils::{MAX_GENESIS_ARCHIVE_UNPACKED_SIZE, open_genesis_config}, solana_hash::Hash, @@ -20,18 +19,15 @@ use { solana_message::{compiled_instruction::CompiledInstruction, v0::LoadedAddresses}, solana_packet::PACKET_DATA_SIZE, solana_pubkey::Pubkey, - solana_runtime::bank::{Bank, RewardType}, + solana_runtime::bank::Bank, solana_sha256_hasher::hash, solana_shred_version::version_from_hash, solana_signature::Signature, - solana_storage_proto::{StoredTransactionStatusMeta, convert::generated}, solana_streamer::evicting_sender::EvictingSender, solana_transaction::Transaction, solana_transaction_context::transaction::TransactionReturnData, solana_transaction_error::TransactionError, - solana_transaction_status::{ - InnerInstruction, InnerInstructions, Reward, TransactionTokenBalance, - }, + solana_transaction_status::{InnerInstruction, InnerInstructions}, std::{cmp::Ordering, num::NonZeroUsize, time::Duration}, test_case::{test_case, test_matrix}, }; @@ -5295,93 +5291,6 @@ fn test_update_completed_data_indexes_out_of_order() { assert!(completed_data_indexes.clone().iter().eq([0, 1, 3])); } -// This test is probably superfluous, since it is highly unlikely that bincode-format -// TransactionStatus entries exist in any current ledger. They certainly exist in historical -// ledger archives, but typically those require contemporaraneous software for other reasons. -// However, we are persisting the test since the apis still exist in `blockstore_db`. -#[test] -fn test_transaction_status_protobuf_backward_compatibility() { - let ledger_path = get_tmp_ledger_path_auto_delete!(); - let blockstore = Blockstore::open(ledger_path.path()).unwrap(); - - let status = TransactionStatusMeta { - status: Ok(()), - fee: 42, - pre_balances: vec![1, 2, 3], - post_balances: vec![1, 2, 3], - inner_instructions: Some(vec![]), - log_messages: Some(vec![]), - pre_token_balances: Some(vec![TransactionTokenBalance { - account_index: 0, - mint: Pubkey::new_unique().to_string(), - ui_token_amount: UiTokenAmount { - ui_amount: Some(1.1), - decimals: 1, - amount: "11".to_string(), - ui_amount_string: "1.1".to_string(), - }, - owner: Pubkey::new_unique().to_string(), - program_id: Pubkey::new_unique().to_string(), - }]), - post_token_balances: Some(vec![TransactionTokenBalance { - account_index: 0, - mint: Pubkey::new_unique().to_string(), - ui_token_amount: UiTokenAmount { - ui_amount: None, - decimals: 1, - amount: "11".to_string(), - ui_amount_string: "1.1".to_string(), - }, - owner: Pubkey::new_unique().to_string(), - program_id: Pubkey::new_unique().to_string(), - }]), - rewards: Some(vec![Reward { - pubkey: "My11111111111111111111111111111111111111111".to_string(), - lamports: -42, - post_balance: 42, - reward_type: Some(RewardType::Rent), - commission: None, - commission_bps: None, - }]), - loaded_addresses: LoadedAddresses::default(), - return_data: Some(TransactionReturnData { - program_id: Pubkey::new_unique(), - data: vec![1, 2, 3], - }), - compute_units_consumed: Some(23456), - cost_units: Some(5678), - }; - let deprecated_status: StoredTransactionStatusMeta = status.clone().try_into().unwrap(); - let protobuf_status: generated::TransactionStatusMeta = status.into(); - - for slot in 0..2 { - let data = bincode::serialize(&deprecated_status).unwrap(); - blockstore - .transaction_status_cf - .put_bytes((Signature::default(), slot), &data) - .unwrap(); - } - for slot in 2..4 { - blockstore - .transaction_status_cf - .put_protobuf((Signature::default(), slot), &protobuf_status) - .unwrap(); - } - for slot in 0..4 { - assert_eq!( - blockstore - .transaction_status_cf - .get_protobuf_or_wincode::(( - Signature::default(), - slot - )) - .unwrap() - .unwrap(), - protobuf_status - ); - } -} - fn make_large_tx_entry(num_txs: usize) -> Entry { let txs: Vec<_> = (0..num_txs) .map(|_| { diff --git a/ledger/src/blockstore_db.rs b/ledger/src/blockstore_db.rs index 007af8eb84b..563e70c3e67 100644 --- a/ledger/src/blockstore_db.rs +++ b/ledger/src/blockstore_db.rs @@ -40,9 +40,8 @@ use { }, }, wincode::{ - SchemaRead, SchemaReadOwned, + SchemaRead, config::{ConfigCore, DefaultConfig}, - deserialize, }, }; @@ -953,47 +952,6 @@ impl LedgerColumn where C: ProtobufColumn + ColumnName, { - pub fn get_protobuf_or_wincode< - T: SchemaReadOwned + Into, - >( - &self, - index: C::Index, - ) -> Result> { - let key = ::key(&index); - self.get_raw_protobuf_or_wincode::(key) - } - - pub(crate) fn get_raw_protobuf_or_wincode< - T: SchemaReadOwned + Into, - >( - &self, - key: impl AsRef<[u8]>, - ) -> Result> { - let is_perf_enabled = maybe_enable_rocksdb_perf( - self.column_options.rocks_perf_sample_interval, - &self.read_perf_status, - ); - let result = self.backend.get_pinned_cf(self.handle(), key); - if let Some(op_start_instant) = is_perf_enabled { - report_rocksdb_read_perf( - C::NAME, - PERF_METRIC_OP_NAME_GET, - &op_start_instant.elapsed(), - &self.column_options, - ); - } - - if let Some(pinnable_slice) = result? { - let value = match C::Type::decode(pinnable_slice.as_ref()) { - Ok(value) => value, - Err(_) => deserialize::(pinnable_slice.as_ref())?.into(), - }; - Ok(Some(value)) - } else { - Ok(None) - } - } - pub fn get_protobuf(&self, index: C::Index) -> Result> { let is_perf_enabled = maybe_enable_rocksdb_perf( self.column_options.rocks_perf_sample_interval, From 0ed4a58ddd7352166596fa9bc888ada42f980a9d Mon Sep 17 00:00:00 2001 From: Faycel Kouteib Date: Mon, 15 Jun 2026 16:37:29 -0700 Subject: [PATCH 346/576] ledger: switch to wincode (#12969) --- Cargo.lock | 3 ++- dev-bins/Cargo.lock | 3 ++- ledger/Cargo.toml | 5 ++--- ledger/src/blockstore/blockstore_purge.rs | 2 +- ledger/src/blockstore/tests.rs | 4 ++-- ledger/src/blockstore_processor.rs | 6 +++--- ledger/src/staking_utils.rs | 2 +- ledger/src/transaction_address_lookup_table_scanner.rs | 2 +- programs/sbf/Cargo.lock | 3 ++- 9 files changed, 16 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3dfb817af81..18cd5181e49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7028,6 +7028,7 @@ dependencies = [ "solana-pubkey 4.2.0", "solana-sdk-ids", "solana-slot-hashes", + "wincode", ] [[package]] @@ -8793,7 +8794,6 @@ dependencies = [ "agave-votor-messages", "anyhow", "assert_matches", - "bincode", "bitflags 2.12.1", "bs58", "bytes", @@ -10488,6 +10488,7 @@ dependencies = [ "solana-system-interface 3.2.0", "solana-sysvar", "solana-sysvar-id", + "wincode", ] [[package]] diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 0d41e26e822..42440409309 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -6015,6 +6015,7 @@ dependencies = [ "solana-pubkey 4.2.0", "solana-sdk-ids", "solana-slot-hashes", + "wincode", ] [[package]] @@ -7170,7 +7171,6 @@ dependencies = [ "agave-votor-messages", "anyhow", "assert_matches", - "bincode", "bitflags 2.13.0", "bytes", "bzip2", @@ -8409,6 +8409,7 @@ dependencies = [ "solana-system-interface 3.2.0", "solana-sysvar", "solana-sysvar-id", + "wincode", ] [[package]] diff --git a/ledger/Cargo.toml b/ledger/Cargo.toml index f704c82b36b..b3dd98d7556 100644 --- a/ledger/Cargo.toml +++ b/ledger/Cargo.toml @@ -32,7 +32,6 @@ agave-snapshots = { workspace = true } agave-votor-messages = { workspace = true } anyhow = { workspace = true } assert_matches = { workspace = true } -bincode = { workspace = true } bitflags = { workspace = true } bytes = { workspace = true } bzip2 = { workspace = true } @@ -61,7 +60,7 @@ serde = { workspace = true } solana-account = { workspace = true } solana-account-decoder = { workspace = true } solana-accounts-db = { workspace = true } -solana-address-lookup-table-interface = { workspace = true } +solana-address-lookup-table-interface = { workspace = true, features = ["wincode"] } solana-clock = { workspace = true } solana-cost-model = { workspace = true } solana-entry = { workspace = true } @@ -95,7 +94,7 @@ solana-sha256-hasher = { workspace = true } solana-shred-version = { workspace = true } solana-signature = { workspace = true, features = ["wincode"] } solana-signer = { workspace = true } -solana-stake-interface = { workspace = true } +solana-stake-interface = { workspace = true, features = ["wincode"] } solana-storage-bigtable = { workspace = true } solana-storage-proto = { workspace = true } solana-streamer = { workspace = true } diff --git a/ledger/src/blockstore/blockstore_purge.rs b/ledger/src/blockstore/blockstore_purge.rs index a243ced9d2d..dbdee1f64fe 100644 --- a/ledger/src/blockstore/blockstore_purge.rs +++ b/ledger/src/blockstore/blockstore_purge.rs @@ -466,13 +466,13 @@ pub mod tests { crate::{ blockstore::tests::make_slot_entries_with_transactions, get_tmp_ledger_path_auto_delete, }, - bincode::serialize, solana_entry::entry::next_entry_mut, solana_hash::Hash, solana_message::Message, solana_sha256_hasher::hash, solana_transaction::Transaction, test_case::test_case, + wincode::serialize, }; fn all_columns_empty_or_greater_than_slot(blockstore: &Blockstore, min_slot: Slot) { diff --git a/ledger/src/blockstore/tests.rs b/ledger/src/blockstore/tests.rs index 1ddf58bce81..b3db68cae24 100644 --- a/ledger/src/blockstore/tests.rs +++ b/ledger/src/blockstore/tests.rs @@ -44,7 +44,7 @@ pub(crate) fn make_slot_entries_with_transactions(num_entries: u64) -> Vec> = diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index 3d4931dd865..229cd9abbc3 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -6006,6 +6006,7 @@ dependencies = [ "solana-pubkey 4.2.0", "solana-sdk-ids", "solana-slot-hashes", + "wincode", ] [[package]] @@ -7279,7 +7280,6 @@ dependencies = [ "agave-votor-messages", "anyhow", "assert_matches", - "bincode", "bitflags 2.12.1", "bytes", "bzip2", @@ -9336,6 +9336,7 @@ dependencies = [ "solana-system-interface 3.2.0", "solana-sysvar", "solana-sysvar-id", + "wincode", ] [[package]] From b43680421d9339e90436da4ea436d370279259a8 Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Mon, 15 Jun 2026 20:15:51 -0400 Subject: [PATCH 347/576] test-validator add --alpenglow option and tests (#13217) --- Cargo.lock | 4 + programs/sbf/Cargo.lock | 12 ++ runtime/src/genesis_utils.rs | 8 + test-validator/Cargo.toml | 9 +- test-validator/src/lib.rs | 178 ++++++++++++++++++++- validator/Cargo.toml | 1 + validator/src/bin/solana-test-validator.rs | 22 ++- validator/src/cli.rs | 9 ++ 8 files changed, 235 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 18cd5181e49..7bb483f4186 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -386,6 +386,7 @@ name = "agave-validator" version = "4.2.0-alpha.0" dependencies = [ "agave-cpu-utils", + "agave-feature-set", "agave-logger", "agave-snapshots", "agave-votor", @@ -10916,6 +10917,7 @@ version = "4.2.0-alpha.0" dependencies = [ "agave-feature-set", "agave-snapshots", + "agave-votor-messages", "arc-swap", "base64 0.22.1", "bincode", @@ -10924,6 +10926,7 @@ dependencies = [ "serde_json", "solana-account 4.3.0", "solana-accounts-db", + "solana-bls-signatures", "solana-cli-output", "solana-clock", "solana-cluster-type", @@ -10961,6 +10964,7 @@ dependencies = [ "solana-test-validator", "solana-transaction", "solana-validator-exit", + "solana-vote-interface", "tokio", ] diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index 229cd9abbc3..2e692baaf12 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -290,6 +290,7 @@ name = "agave-validator" version = "4.2.0-alpha.0" dependencies = [ "agave-cpu-utils", + "agave-feature-set", "agave-logger", "agave-snapshots", "agave-votor", @@ -7587,7 +7588,9 @@ dependencies = [ "rand 0.9.4", "rayon", "serde", + "solana-clock", "solana-hash 4.4.0", + "solana-keypair", "solana-message", "solana-metrics", "solana-packet 4.1.0", @@ -7595,8 +7598,14 @@ dependencies = [ "solana-sdk-ids", "solana-short-vec", "solana-signature", + "solana-signer", + "solana-system-interface 3.2.0", + "solana-system-transaction", "solana-time-utils", + "solana-transaction", "solana-transaction-context", + "solana-vote", + "solana-vote-program", "wincode", ] @@ -9695,6 +9704,7 @@ version = "4.2.0-alpha.0" dependencies = [ "agave-feature-set", "agave-snapshots", + "agave-votor-messages", "arc-swap", "base64 0.22.1", "bincode", @@ -9703,6 +9713,7 @@ dependencies = [ "serde_json", "solana-account 4.3.0", "solana-accounts-db", + "solana-bls-signatures", "solana-cli-output", "solana-clock", "solana-cluster-type", @@ -9739,6 +9750,7 @@ dependencies = [ "solana-syscalls", "solana-transaction", "solana-validator-exit", + "solana-vote-interface", "tokio", ] diff --git a/runtime/src/genesis_utils.rs b/runtime/src/genesis_utils.rs index 5d18ceade73..cb8c7980201 100644 --- a/runtime/src/genesis_utils.rs +++ b/runtime/src/genesis_utils.rs @@ -322,7 +322,15 @@ pub fn create_genesis_config_with_leader_with_mint_keypair( pub fn activate_all_features_alpenglow(genesis_config: &mut GenesisConfig) { do_activate_all_features::(genesis_config); + configure_alpenglow_at_genesis(genesis_config); +} + +pub fn activate_alpenglow_at_genesis(genesis_config: &mut GenesisConfig) { + activate_feature(genesis_config, agave_feature_set::alpenglow::id()); + configure_alpenglow_at_genesis(genesis_config); +} +fn configure_alpenglow_at_genesis(genesis_config: &mut GenesisConfig) { // PoH is in low power mode genesis_config.poh_config.hashes_per_tick = None; diff --git a/test-validator/Cargo.toml b/test-validator/Cargo.toml index 81bdad4c208..6be86b55a32 100644 --- a/test-validator/Cargo.toml +++ b/test-validator/Cargo.toml @@ -13,11 +13,16 @@ targets = ["x86_64-unknown-linux-gnu"] [features] agave-unstable-api = [] -dev-context-only-utils = [] +dev-context-only-utils = [ + "solana-core/dev-context-only-utils", + "solana-ledger/dev-context-only-utils", + "solana-runtime/dev-context-only-utils", +] [dependencies] agave-feature-set = { workspace = true } agave-snapshots = { workspace = true } +agave-votor-messages = { workspace = true } arc-swap.workspace = true base64 = { workspace = true } bincode = { workspace = true } @@ -26,6 +31,7 @@ log = { workspace = true } serde_json = { workspace = true } solana-account = { workspace = true } solana-accounts-db = { workspace = true } +solana-bls-signatures = { workspace = true } solana-cli-output = { workspace = true } solana-clock = { workspace = true } solana-cluster-type = { workspace = true } @@ -62,6 +68,7 @@ solana-streamer = { workspace = true } solana-syscalls = { workspace = true } solana-transaction = { workspace = true } solana-validator-exit = { workspace = true } +solana-vote-interface = { workspace = true } tokio = { workspace = true, features = ["full"] } [dev-dependencies] diff --git a/test-validator/src/lib.rs b/test-validator/src/lib.rs index f689ec94345..908a4db9ec4 100644 --- a/test-validator/src/lib.rs +++ b/test-validator/src/lib.rs @@ -1,10 +1,14 @@ #![cfg(feature = "agave-unstable-api")] #![allow(clippy::arithmetic_side_effects)] use { - agave_feature_set::{FEATURE_NAMES, FeatureSet, alpenglow, raise_cpi_nesting_limit_to_8}, + agave_feature_set::{ + FEATURE_NAMES, FeatureSet, alpenglow, raise_cpi_nesting_limit_to_8, + validator_admission_ticket, + }, agave_snapshots::{ SnapshotInterval, paths::BANK_SNAPSHOTS_DIR, snapshot_config::SnapshotConfig, }, + agave_votor_messages::consensus_message::BLS_KEYPAIR_DERIVE_SEED, arc_swap::ArcSwap, base64::{Engine, prelude::BASE64_STANDARD}, crossbeam_channel::Receiver, @@ -15,6 +19,7 @@ use { accounts_index::{AccountsIndexConfig, ScanFilter}, utils::create_accounts_run_and_snapshot_dirs, }, + solana_bls_signatures::keypair::Keypair as BLSKeypair, solana_cli_output::CliAccount, solana_clock::{DEFAULT_MS_PER_SLOT, Slot}, solana_commitment_config::CommitmentConfig, @@ -58,7 +63,8 @@ use { client_error::Error as RpcClientError, request::MAX_MULTIPLE_ACCOUNTS, }, solana_runtime::{ - bank_forks::BankForks, genesis_utils::create_genesis_config_with_leader_ex, + bank_forks::BankForks, + genesis_utils::{activate_alpenglow_at_genesis, create_genesis_config_with_leader_ex}, runtime_config::RuntimeConfig, }, solana_sbpf::{elf::Executable, verifier::RequisiteVerifier}, @@ -68,6 +74,7 @@ use { solana_syscalls::create_program_runtime_environment, solana_transaction::{Transaction, TransactionError}, solana_validator_exit::Exit, + solana_vote_interface::state::BLS_PUBLIC_KEY_COMPRESSED_SIZE, std::{ collections::{HashMap, HashSet}, ffi::OsStr, @@ -201,6 +208,15 @@ impl Default for TestValidatorGenesis { } } +fn derive_bls_pubkey_from_authorized_voter_keypair( + authorized_voter_keypair: &Keypair, +) -> [u8; BLS_PUBLIC_KEY_COMPRESSED_SIZE] { + BLSKeypair::derive_from_signer(authorized_voter_keypair, BLS_KEYPAIR_DERIVE_SEED) + .unwrap() + .public + .to_bytes_compressed() +} + #[cfg(feature = "dev-context-only-utils")] impl TestValidatorGenesis { /// Defaults suitable for unit tests; a disjoint port range will be used to @@ -257,6 +273,14 @@ impl TestValidatorGenesis { self.deactivate_feature_set.extend(deactivate_list); self } + + pub fn activate_alpenglow(&mut self) -> &mut Self { + self.deactivate_feature_set.remove(&alpenglow::id()); + self.deactivate_feature_set + .remove(&validator_admission_ticket::id()); + self + } + pub fn ledger_path>(&mut self, ledger_path: P) -> &mut Self { self.ledger_path = Some(ledger_path.into()); self @@ -537,6 +561,8 @@ impl TestValidatorGenesis { .is_none() { self.deactivate_feature_set.insert(*feature_id); + } else { + self.deactivate_feature_set.remove(feature_id); } }); } @@ -893,6 +919,12 @@ impl TestValidator { warn!("Feature {feature:?} set for deactivation is not a known Feature public key",) } } + let is_alpenglow_active = feature_set.is_active(&alpenglow::id()); + if is_alpenglow_active && !feature_set.is_active(&validator_admission_ticket::id()) { + return Err( + "Alpenglow requires the validator_admission_ticket feature to be active".into(), + ); + } let runtime_features = feature_set.runtime_features(); let program_runtime_environment = create_program_runtime_environment( @@ -963,13 +995,17 @@ impl TestValidator { ); } + // Test validator genesis uses the vote account pubkey as the authorized voter, + // so the Alpenglow BLS pubkey is derived from the vote account keypair. let mut genesis_config = create_genesis_config_with_leader_ex( mint_lamports, &mint_address, &validator_identity.pubkey(), &validator_vote_account.pubkey(), &validator_stake_account.pubkey(), - None, + Some(derive_bls_pubkey_from_authorized_voter_keypair( + &validator_vote_account, + )), validator_stake_lamports, validator_identity_lamports, config.fee_rate_governor.clone(), @@ -978,6 +1014,9 @@ impl TestValidator { &feature_set, accounts.into_iter().collect(), ); + if is_alpenglow_active { + activate_alpenglow_at_genesis(&mut genesis_config); + } genesis_config.epoch_schedule = config .epoch_schedule .as_ref() @@ -1106,7 +1145,8 @@ impl TestValidator { .iter() .any(|x| x.pubkey() == vote_account_address) { - authorized_voter_keypairs.push(Arc::new(validator_vote_account)) + // Test validator genesis uses the vote account pubkey as the authorized voter. + authorized_voter_keypairs.push(Arc::new(validator_vote_account)); } } @@ -1387,7 +1427,63 @@ impl Drop for TestValidator { #[cfg(test)] mod test { - use {super::*, solana_feature_gate_interface::Feature}; + use { + super::*, + solana_feature_gate_interface::{Feature, create_account as create_feature_account}, + }; + + async fn assert_feature_accounts( + rpc_client: &nonblocking::rpc_client::RpcClient, + active_features: &[Pubkey], + inactive_features: &[Pubkey], + ) { + for chunk in active_features.chunks(100) { + let active_feature_accounts = rpc_client.get_multiple_accounts(chunk).await.unwrap(); + for feature_account in active_feature_accounts { + let account = feature_account.unwrap(); + let feature_state: Feature = bincode::deserialize(account.data()).unwrap(); + assert!(feature_state.activated_at.is_some()); + } + } + + if !inactive_features.is_empty() { + let inactive_feature_accounts = rpc_client + .get_multiple_accounts(inactive_features) + .await + .unwrap(); + for feature_account in inactive_feature_accounts { + assert!(feature_account.is_none()); + } + } + } + + async fn wait_for_alpenglow_enabled(test_validator: &TestValidator) { + for _ in 0..240 { + let migration_status = test_validator + .bank_forks() + .read() + .unwrap() + .migration_status(); + if migration_status.is_alpenglow_enabled() { + return; + } + sleep(Duration::from_millis(250)).await; + } + let bank_forks = test_validator.bank_forks(); + let bank_forks = bank_forks.read().unwrap(); + let root_bank = bank_forks.root_bank(); + let migration_status = bank_forks.migration_status(); + panic!( + "Timed out waiting for Alpenglow migration: migration_status={migration_status:?}, \ + root_slot={}, working_slot={}, feature_activation_slot={:?}, \ + eligible_genesis_block={:?}, genesis_certificate={:?}", + root_bank.slot(), + bank_forks.working_bank().slot(), + root_bank.feature_set.activated_slot(&alpenglow::id()), + migration_status.eligible_genesis_block(), + migration_status.genesis_certificate(), + ); + } #[test] fn get_health() { @@ -1405,6 +1501,78 @@ mod test { rpc_client.get_health().await.expect("health"); } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_all_features_active_except_alpenglow_by_default() { + let (test_validator, _payer) = TestValidatorGenesis::default_for_tests() + .start_async() + .await; + let rpc_client = test_validator.get_async_rpc_client(); + + let active_features = FEATURE_NAMES + .keys() + .copied() + .filter(|feature| *feature != alpenglow::id()) + .collect::>(); + assert_feature_accounts(&rpc_client, &active_features, &[alpenglow::id()]).await; + assert!( + test_validator + .bank_forks() + .read() + .unwrap() + .migration_status() + .is_pre_feature_activation() + ); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_all_features_active_with_alpenglow_at_genesis() { + let (test_validator, _payer) = TestValidatorGenesis::default_for_tests() + .activate_alpenglow() + .start_async() + .await; + let rpc_client = test_validator.get_async_rpc_client(); + + let active_features = FEATURE_NAMES.keys().copied().collect::>(); + assert_feature_accounts(&rpc_client, &active_features, &[]).await; + assert!( + test_validator + .bank_forks() + .read() + .unwrap() + .root_bank() + .get_alpenglow_genesis_certificate() + .is_some() + ); + wait_for_alpenglow_enabled(&test_validator).await; + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_pending_alpenglow_activation_migrates() { + let feature_lamports = Rent::default().minimum_balance(Feature::size_of()).max(1); + let (test_validator, _payer) = TestValidatorGenesis::default_for_tests() + .ticks_per_slot(8) + .epoch_schedule(EpochSchedule::custom(64, 64, false)) + .add_account( + alpenglow::id(), + create_feature_account(&Feature::default(), feature_lamports), + ) + .start_async() + .await; + let rpc_client = test_validator.get_async_rpc_client(); + + for _ in 0..120 { + let account = rpc_client.get_account(&alpenglow::id()).await.unwrap(); + let feature_state = solana_feature_gate_interface::from_account(&account).unwrap(); + if feature_state.activated_at.is_some() { + wait_for_alpenglow_enabled(&test_validator).await; + rpc_client.get_health().await.expect("health"); + return; + } + sleep(Duration::from_millis(250)).await; + } + panic!("Timed out waiting for Alpenglow feature activation"); + } + #[test] fn test_upgradeable_program_deploayment() { let program_id = Pubkey::new_unique(); diff --git a/validator/Cargo.toml b/validator/Cargo.toml index cd7b91d4459..f1c1b32d3f2 100644 --- a/validator/Cargo.toml +++ b/validator/Cargo.toml @@ -19,6 +19,7 @@ agave-unstable-api = [] [dependencies] agave-cpu-utils = { workspace = true } +agave-feature-set = { workspace = true } agave-logger = { workspace = true } agave-snapshots = { workspace = true } agave-votor = { workspace = true } diff --git a/validator/src/bin/solana-test-validator.rs b/validator/src/bin/solana-test-validator.rs index 692e7680c38..f5148b424d7 100644 --- a/validator/src/bin/solana-test-validator.rs +++ b/validator/src/bin/solana-test-validator.rs @@ -386,6 +386,18 @@ fn main() { }); let features_to_deactivate = pubkeys_of(&matches, "deactivate_feature").unwrap_or_default(); + if matches.is_present("alpenglow") + && features_to_deactivate.iter().any(|feature| { + *feature == agave_feature_set::alpenglow::id() + || *feature == agave_feature_set::validator_admission_ticket::id() + }) + { + println!( + "Error: --alpenglow requires both the alpenglow and validator_admission_ticket \ + features to be active" + ); + exit(1); + } if TestValidatorGenesis::ledger_exists(&ledger_path) { for (name, long) in &[ @@ -397,6 +409,7 @@ fn main() { ("slots_per_epoch", "--slots-per-epoch"), ("inflation_fixed", "--inflation-fixed"), ("faucet_sol", "--faucet-sol"), + ("alpenglow", "--alpenglow"), ("deactivate_feature", "--deactivate-feature"), ] { if matches.is_present(name) { @@ -493,8 +506,7 @@ fn main() { .unwrap_or_else(|e| { println!("Error: add_accounts_from_directories failed: {e}"); exit(1); - }) - .deactivate_features(&features_to_deactivate); + }); genesis.rpc_config(JsonRpcConfig { enable_rpc_transaction_history: true, @@ -566,6 +578,12 @@ fn main() { } } + if matches.is_present("alpenglow") { + genesis.activate_alpenglow(); + } + + genesis.deactivate_features(&features_to_deactivate); + if let Some(warp_slot) = warp_slot { genesis.warp_slot(warp_slot); } diff --git a/validator/src/cli.rs b/validator/src/cli.rs index 4011ed283b8..2a318b29801 100644 --- a/validator/src/cli.rs +++ b/validator/src/cli.rs @@ -843,6 +843,15 @@ pub fn test_app<'a>(version: &'a str, default_args: &'a DefaultTestArgs) -> App< .takes_value(false) .help("Enables external processes to connect and manage block production"), ) + .arg( + Arg::with_name("alpenglow") + .long("alpenglow") + .takes_value(false) + .help( + "Activate Alpenglow at genesis. The validator_admission_ticket feature must \ + remain active", + ), + ) .arg( Arg::with_name("deactivate_feature") .long("deactivate-feature") From 05f81d41453c8f439573be49ce6e795a5978fc7e Mon Sep 17 00:00:00 2001 From: Yihau Chen Date: Tue, 16 Jun 2026 09:15:55 +0800 Subject: [PATCH 348/576] chore: remove docs (#13200) * rm -rf docs/** * rm .github/workflows/docs.yml * update docs/README.md * update .mergify.yml * update .github/dependabot.yml --- .github/dependabot.yml | 14 - .github/workflows/docs.yml | 134 - .mergify.yml | 4 - docs/README.md | 125 +- docs/art/fork-generation.bob | 13 - docs/art/forks-pruned.bob | 9 - docs/art/forks-pruned2.bob | 11 - docs/art/forks.bob | 13 - docs/art/passive-staking-callflow.msc | 30 - docs/art/retransmit_stage.bob | 45 - docs/art/runtime.bob | 10 - docs/art/sdk-tools.bob | 20 - docs/art/spv-bank-hash.bob | 19 - docs/art/spv-block-merkle.bob | 19 - docs/art/tpu.bob | 25 - docs/art/transaction.bob | 9 - docs/art/tvu.bob | 37 - docs/art/validator-proposal.bob | 60 - docs/art/validator.bob | 30 - docs/babel.config.js | 3 - docs/build-cli-usage.sh | 60 - docs/build.sh | 17 - docs/components/Card.jsx | 53 - docs/components/HomeCtaLinks.jsx | 34 - docs/convert-ascii-to-svg.sh | 27 - docs/deploy.sh | 186 - docs/docusaurus.config.js | 176 - docs/offline-cmd-md-links.sh | 11 - docs/package.json | 59 - docs/pnpm-lock.yaml | 11000 ---------- docs/set-solana-release-tag.sh | 20 - docs/sidebars.js | 130 - docs/src/architecture.md | 8 - docs/src/backwards-compatibility.md | 153 - docs/src/cli/.usage.md.header | 62 - docs/src/cli/examples/_category_.json | 7 - docs/src/cli/examples/choose-a-cluster.md | 45 - docs/src/cli/examples/delegate-stake.md | 250 - docs/src/cli/examples/deploy-a-program.md | 7 - docs/src/cli/examples/durable-nonce.md | 256 - docs/src/cli/examples/offline-signing.md | 176 - .../src/cli/examples/sign-offchain-message.md | 100 - docs/src/cli/examples/test-validator.md | 171 - docs/src/cli/examples/transfer-tokens.md | 168 - docs/src/cli/index.md | 25 - docs/src/cli/install.md | 257 - docs/src/cli/intro.md | 83 - docs/src/cli/wallets/_category_.json | 7 - docs/src/cli/wallets/file-system.md | 68 - docs/src/cli/wallets/hardware/_category_.json | 7 - docs/src/cli/wallets/hardware/index.md | 57 - docs/src/cli/wallets/hardware/ledger.md | 218 - docs/src/cli/wallets/hardware/trezor.md | 118 - docs/src/cli/wallets/index.md | 63 - docs/src/cli/wallets/paper.md | 218 - docs/src/clusters/available.md | 176 - docs/src/clusters/benchmark.md | 136 - docs/src/clusters/index.md | 42 - docs/src/clusters/metrics.md | 27 - docs/src/clusters/testnet.md | 202 - docs/src/consensus/commitments.md | 33 - docs/src/consensus/fork-generation.md | 139 - docs/src/consensus/leader-rotation.md | 90 - docs/src/consensus/managing-forks.md | 60 - .../consensus/stake-delegation-and-rewards.md | 339 - docs/src/consensus/synchronization.md | 28 - .../consensus/turbine-block-propagation.md | 124 - docs/src/consensus/vote-signing.md | 17 - docs/src/css/custom.css | 130 - docs/src/faq.md | 54 - .../implemented-proposals/abi-management.md | 166 - .../bank-timestamp-correction.md | 77 - docs/src/implemented-proposals/commitment.md | 92 - .../durable-tx-nonces.md | 131 - .../ed_overview/ed_economic_sustainability.md | 9 - .../ed_overview/ed_mvp.md | 24 - .../ed_overview/ed_overview.md | 19 - .../ed_overview/ed_references.md | 7 - .../ed_overview/ed_storage_rent_economics.md | 17 - .../ed_vce_overview.md | 9 - ...state_validation_protocol_based_rewards.md | 64 - ...d_vce_state_validation_transaction_fees.md | 20 - .../ed_vce_validation_stake_delegation.md | 28 - .../epoch_accounts_hash.md | 331 - docs/src/implemented-proposals/index.md | 15 - docs/src/implemented-proposals/installer.md | 222 - .../instruction_introspection.md | 28 - .../leader-leader-transition.md | 55 - .../leader-validator-transition.md | 83 - .../persistent-account-storage.md | 83 - .../readonly-accounts.md | 25 - .../reliable-vote-transmission.md | 60 - docs/src/implemented-proposals/rent.md | 69 - .../implemented-proposals/repair-service.md | 108 - .../rpc-transaction-history.md | 121 - .../snapshot-verification.md | 56 - .../implemented-proposals/staking-rewards.md | 33 - .../implemented-proposals/testing-programs.md | 49 - docs/src/implemented-proposals/tower-bft.md | 183 - .../implemented-proposals/transaction-fees.md | 19 - .../validator-timestamp-oracle.md | 107 - docs/src/index.mdx | 61 - docs/src/operations/_category_.json | 6 - .../operations/best-practices/_category_.json | 7 - docs/src/operations/best-practices/general.md | 197 - .../operations/best-practices/monitoring.md | 96 - .../src/operations/best-practices/security.md | 48 - docs/src/operations/guides/_category_.json | 7 - docs/src/operations/guides/restart-cluster.md | 135 - .../operations/guides/validator-failover.md | 104 - docs/src/operations/guides/validator-info.md | 52 - .../operations/guides/validator-monitor.md | 50 - docs/src/operations/guides/validator-stake.md | 164 - docs/src/operations/guides/validator-start.md | 452 - .../guides/validator-troubleshoot.md | 52 - docs/src/operations/guides/vote-accounts.md | 332 - docs/src/operations/index.md | 8 - docs/src/operations/prerequisites.md | 38 - docs/src/operations/requirements.md | 92 - docs/src/operations/running-with-af-xdp.md | 84 - docs/src/operations/setup-a-validator.md | 622 - docs/src/operations/setup-an-rpc-node.md | 93 - docs/src/operations/validator-or-rpc-node.md | 92 - docs/src/pages/styles.module.css | 45 - docs/src/proposals.md | 50 - .../proposals/accepted-design-proposals.md | 15 - docs/src/proposals/accounts-db-replication.md | 185 - docs/src/proposals/bankless-leader.md | 95 - docs/src/proposals/block-confirmation.md | 85 - docs/src/proposals/cluster-test-framework.md | 103 - .../proposals/comprehensive-compute-fees.md | 153 - docs/src/proposals/embedding-move.md | 39 - .../src/proposals/fee_transaction_priority.md | 210 - docs/src/proposals/handle-duplicate-block.md | 92 - .../interchain-transaction-verification.md | 105 - .../ledger-replication-to-implement.md | 415 - docs/src/proposals/log_data.md | 146 - .../proposals/off-chain-message-signing.md | 232 - .../optimistic-confirmation-and-slashing.md | 89 - ...timistic-transaction-propagation-signal.md | 109 - docs/src/proposals/optimistic_confirmation.md | 408 - ...ioned-inflationary-rewards-distribution.md | 151 - .../proposals/program-instruction-macro.md | 226 - docs/src/proposals/return-data.md | 144 - docs/src/proposals/rip-curl.md | 56 - .../simple-payment-and-state-verification.md | 258 - docs/src/proposals/slashing.md | 60 - docs/src/proposals/snapshot-verification.md | 11 - docs/src/proposals/tick-verification.md | 70 - docs/src/proposals/timely-vote-credits.md | 190 - docs/src/proposals/validator-proposal.md | 54 - docs/src/proposals/versioned-transactions.md | 338 - .../proposals/vote-signing-to-implement.md | 116 - docs/src/runtime/programs.md | 7 - docs/src/runtime/sysvars.md | 186 - .../ciphertext_ciphertext_equality.pdf | Bin 268270 -> 0 bytes .../ciphertext_commitment_equality.pdf | Bin 266927 -> 0 bytes .../runtime/zk-docs/ciphertext_validity.pdf | Bin 277913 -> 0 bytes .../runtime/zk-docs/percentage_with_cap.pdf | Bin 265177 -> 0 bytes docs/src/runtime/zk-docs/pubkey_proof.pdf | Bin 268625 -> 0 bytes docs/src/runtime/zk-docs/twisted_elgamal.pdf | Bin 203595 -> 0 bytes docs/src/runtime/zk-docs/zero_proof.pdf | Bin 262270 -> 0 bytes docs/src/runtime/zk-elgamal-proof.md | 174 - docs/src/theme/Footer/index.js | 126 - docs/src/theme/Footer/styles.module.css | 15 - docs/src/validator/anatomy.md | 16 - docs/src/validator/blockstore.md | 93 - docs/src/validator/geyser.md | 537 - docs/src/validator/gossip.md | 140 - docs/src/validator/runtime.md | 71 - docs/src/validator/tpu.md | 46 - docs/src/validator/tvu.md | 67 - docs/src/what-is-a-validator.md | 39 - docs/src/what-is-an-rpc-node.md | 12 - docs/static/.nojekyll | 0 docs/static/img/android-chrome-192x192.png | Bin 2217 -> 0 bytes docs/static/img/android-chrome-512x512.png | Bin 6622 -> 0 bytes docs/static/img/apple-touch-icon.png | Bin 1998 -> 0 bytes docs/static/img/dark-mark-white.inline.svg | 5 - docs/static/img/data-plane-propagation.png | Bin 143309 -> 0 bytes .../img/economic_design_infl_230719.png | Bin 65880 -> 0 bytes docs/static/img/favicon-16x16.png | Bin 204 -> 0 bytes docs/static/img/favicon-32x32.png | Bin 319 -> 0 bytes docs/static/img/favicon.ico | Bin 15086 -> 0 bytes .../img/ledger-live-install-solana-app.png | Bin 199342 -> 0 bytes .../ledger-live-latest-version-installed.png | Bin 251127 -> 0 bytes .../ledger-live-update-available-v0.2.2.png | Bin 194587 -> 0 bytes docs/static/img/logo-horizontal-dark.svg | 8 - docs/static/img/logo-horizontal.svg | 8 - docs/static/img/logo.svg | 6 - .../img/p_ex_adjusted_staked_yields.png | Bin 565142 -> 0 bytes docs/static/img/p_ex_interest.png | Bin 555107 -> 0 bytes docs/static/img/p_ex_relative_dilution.png | Bin 445785 -> 0 bytes docs/static/img/p_ex_schedule.png | Bin 261726 -> 0 bytes docs/static/img/p_ex_staked_dilution.png | Bin 568687 -> 0 bytes ...ex_staked_supply_w_range_initial_stake.png | Bin 521287 -> 0 bytes docs/static/img/p_ex_staked_yields.png | Bin 608677 -> 0 bytes docs/static/img/p_ex_supply.png | Bin 275689 -> 0 bytes docs/static/img/p_ex_unstaked_dilution.png | Bin 389981 -> 0 bytes docs/static/img/p_inflation_schedule.png | Bin 327755 -> 0 bytes ...p_inflation_schedule_ranges_w_comments.png | Bin 121737 -> 0 bytes docs/static/img/p_total_supply.png | Bin 276555 -> 0 bytes docs/static/img/p_total_supply_ranges.png | Bin 329821 -> 0 bytes docs/static/img/porep_reward.png | Bin 380503 -> 0 bytes docs/static/img/ramp-tps-rounds.png | Bin 197458 -> 0 bytes .../rt-dropped-minority-fork-post-process.png | Bin 172642 -> 0 bytes .../rt-dropped-minority-fork-pre-process.png | Bin 153134 -> 0 bytes docs/static/img/rt-dropped-via-rpc-pool.png | Bin 145269 -> 0 bytes docs/static/img/rt-tpu-jito-labs.png | Bin 72038 -> 0 bytes docs/static/img/rt-tx-journey.png | Bin 115943 -> 0 bytes docs/static/img/spiral.svg | 206 - docs/static/katex/README.md | 91 - docs/static/katex/contrib/auto-render.js | 350 - docs/static/katex/contrib/auto-render.min.js | 1 - docs/static/katex/contrib/auto-render.mjs | 226 - docs/static/katex/contrib/copy-tex.css | 13 - docs/static/katex/contrib/copy-tex.js | 213 - docs/static/katex/contrib/copy-tex.min.css | 1 - docs/static/katex/contrib/copy-tex.min.js | 1 - docs/static/katex/contrib/copy-tex.mjs | 85 - .../katex/contrib/mathtex-script-type.js | 137 - .../katex/contrib/mathtex-script-type.min.js | 1 - .../katex/contrib/mathtex-script-type.mjs | 24 - docs/static/katex/contrib/mhchem.js | 3241 --- docs/static/katex/contrib/mhchem.min.js | 1 - docs/static/katex/contrib/mhchem.mjs | 3109 --- .../katex/contrib/render-a11y-string.js | 870 - .../katex/contrib/render-a11y-string.min.js | 1 - .../katex/contrib/render-a11y-string.mjs | 753 - docs/static/katex/fonts/KaTeX_AMS-Regular.ttf | Bin 70936 -> 0 bytes .../static/katex/fonts/KaTeX_AMS-Regular.woff | Bin 36912 -> 0 bytes .../katex/fonts/KaTeX_AMS-Regular.woff2 | Bin 31136 -> 0 bytes .../katex/fonts/KaTeX_Caligraphic-Bold.ttf | Bin 15416 -> 0 bytes .../katex/fonts/KaTeX_Caligraphic-Bold.woff | Bin 9376 -> 0 bytes .../katex/fonts/KaTeX_Caligraphic-Bold.woff2 | Bin 8392 -> 0 bytes .../katex/fonts/KaTeX_Caligraphic-Regular.ttf | Bin 14908 -> 0 bytes .../fonts/KaTeX_Caligraphic-Regular.woff | Bin 9148 -> 0 bytes .../fonts/KaTeX_Caligraphic-Regular.woff2 | Bin 8248 -> 0 bytes .../static/katex/fonts/KaTeX_Fraktur-Bold.ttf | Bin 24400 -> 0 bytes .../katex/fonts/KaTeX_Fraktur-Bold.woff | Bin 16208 -> 0 bytes .../katex/fonts/KaTeX_Fraktur-Bold.woff2 | Bin 13912 -> 0 bytes .../katex/fonts/KaTeX_Fraktur-Regular.ttf | Bin 23904 -> 0 bytes .../katex/fonts/KaTeX_Fraktur-Regular.woff | Bin 15880 -> 0 bytes .../katex/fonts/KaTeX_Fraktur-Regular.woff2 | Bin 13668 -> 0 bytes docs/static/katex/fonts/KaTeX_Main-Bold.ttf | Bin 59972 -> 0 bytes docs/static/katex/fonts/KaTeX_Main-Bold.woff | Bin 35056 -> 0 bytes docs/static/katex/fonts/KaTeX_Main-Bold.woff2 | Bin 29932 -> 0 bytes .../katex/fonts/KaTeX_Main-BoldItalic.ttf | Bin 42872 -> 0 bytes .../katex/fonts/KaTeX_Main-BoldItalic.woff | Bin 24500 -> 0 bytes .../katex/fonts/KaTeX_Main-BoldItalic.woff2 | Bin 21244 -> 0 bytes docs/static/katex/fonts/KaTeX_Main-Italic.ttf | Bin 46028 -> 0 bytes .../static/katex/fonts/KaTeX_Main-Italic.woff | Bin 25352 -> 0 bytes .../katex/fonts/KaTeX_Main-Italic.woff2 | Bin 22076 -> 0 bytes .../static/katex/fonts/KaTeX_Main-Regular.ttf | Bin 68880 -> 0 bytes .../katex/fonts/KaTeX_Main-Regular.woff | Bin 37856 -> 0 bytes .../katex/fonts/KaTeX_Main-Regular.woff2 | Bin 32312 -> 0 bytes .../katex/fonts/KaTeX_Math-BoldItalic.ttf | Bin 42300 -> 0 bytes .../katex/fonts/KaTeX_Math-BoldItalic.woff | Bin 23980 -> 0 bytes .../katex/fonts/KaTeX_Math-BoldItalic.woff2 | Bin 21192 -> 0 bytes docs/static/katex/fonts/KaTeX_Math-Italic.ttf | Bin 44484 -> 0 bytes .../static/katex/fonts/KaTeX_Math-Italic.woff | Bin 24668 -> 0 bytes .../katex/fonts/KaTeX_Math-Italic.woff2 | Bin 21668 -> 0 bytes .../katex/fonts/KaTeX_SansSerif-Bold.ttf | Bin 32588 -> 0 bytes .../katex/fonts/KaTeX_SansSerif-Bold.woff | Bin 17988 -> 0 bytes .../katex/fonts/KaTeX_SansSerif-Bold.woff2 | Bin 15296 -> 0 bytes .../katex/fonts/KaTeX_SansSerif-Italic.ttf | Bin 29860 -> 0 bytes .../katex/fonts/KaTeX_SansSerif-Italic.woff | Bin 17044 -> 0 bytes .../katex/fonts/KaTeX_SansSerif-Italic.woff2 | Bin 14484 -> 0 bytes .../katex/fonts/KaTeX_SansSerif-Regular.ttf | Bin 28708 -> 0 bytes .../katex/fonts/KaTeX_SansSerif-Regular.woff | Bin 15712 -> 0 bytes .../katex/fonts/KaTeX_SansSerif-Regular.woff2 | Bin 13300 -> 0 bytes .../katex/fonts/KaTeX_Script-Regular.ttf | Bin 23520 -> 0 bytes .../katex/fonts/KaTeX_Script-Regular.woff | Bin 12992 -> 0 bytes .../katex/fonts/KaTeX_Script-Regular.woff2 | Bin 11792 -> 0 bytes .../katex/fonts/KaTeX_Size1-Regular.ttf | Bin 11932 -> 0 bytes .../katex/fonts/KaTeX_Size1-Regular.woff | Bin 6300 -> 0 bytes .../katex/fonts/KaTeX_Size1-Regular.woff2 | Bin 5332 -> 0 bytes .../katex/fonts/KaTeX_Size2-Regular.ttf | Bin 11080 -> 0 bytes .../katex/fonts/KaTeX_Size2-Regular.woff | Bin 6012 -> 0 bytes .../katex/fonts/KaTeX_Size2-Regular.woff2 | Bin 5080 -> 0 bytes .../katex/fonts/KaTeX_Size3-Regular.ttf | Bin 7028 -> 0 bytes .../katex/fonts/KaTeX_Size3-Regular.woff | Bin 4148 -> 0 bytes .../katex/fonts/KaTeX_Size3-Regular.woff2 | Bin 3400 -> 0 bytes .../katex/fonts/KaTeX_Size4-Regular.ttf | Bin 10008 -> 0 bytes .../katex/fonts/KaTeX_Size4-Regular.woff | Bin 5820 -> 0 bytes .../katex/fonts/KaTeX_Size4-Regular.woff2 | Bin 4720 -> 0 bytes .../katex/fonts/KaTeX_Typewriter-Regular.ttf | Bin 34560 -> 0 bytes .../katex/fonts/KaTeX_Typewriter-Regular.woff | Bin 19700 -> 0 bytes .../fonts/KaTeX_Typewriter-Regular.woff2 | Bin 16868 -> 0 bytes docs/static/katex/katex.css | 1034 - docs/static/katex/katex.js | 17308 ---------------- docs/static/katex/katex.min.css | 1 - docs/static/katex/katex.min.js | 1 - docs/static/katex/katex.mjs | 16911 --------------- 294 files changed, 3 insertions(+), 72008 deletions(-) delete mode 100644 .github/workflows/docs.yml delete mode 100644 docs/art/fork-generation.bob delete mode 100644 docs/art/forks-pruned.bob delete mode 100644 docs/art/forks-pruned2.bob delete mode 100644 docs/art/forks.bob delete mode 100644 docs/art/passive-staking-callflow.msc delete mode 100644 docs/art/retransmit_stage.bob delete mode 100644 docs/art/runtime.bob delete mode 100644 docs/art/sdk-tools.bob delete mode 100644 docs/art/spv-bank-hash.bob delete mode 100644 docs/art/spv-block-merkle.bob delete mode 100644 docs/art/tpu.bob delete mode 100644 docs/art/transaction.bob delete mode 100644 docs/art/tvu.bob delete mode 100644 docs/art/validator-proposal.bob delete mode 100644 docs/art/validator.bob delete mode 100644 docs/babel.config.js delete mode 100755 docs/build-cli-usage.sh delete mode 100755 docs/build.sh delete mode 100644 docs/components/Card.jsx delete mode 100644 docs/components/HomeCtaLinks.jsx delete mode 100755 docs/convert-ascii-to-svg.sh delete mode 100755 docs/deploy.sh delete mode 100644 docs/docusaurus.config.js delete mode 100755 docs/offline-cmd-md-links.sh delete mode 100644 docs/package.json delete mode 100644 docs/pnpm-lock.yaml delete mode 100755 docs/set-solana-release-tag.sh delete mode 100644 docs/sidebars.js delete mode 100644 docs/src/architecture.md delete mode 100644 docs/src/backwards-compatibility.md delete mode 100644 docs/src/cli/.usage.md.header delete mode 100644 docs/src/cli/examples/_category_.json delete mode 100644 docs/src/cli/examples/choose-a-cluster.md delete mode 100644 docs/src/cli/examples/delegate-stake.md delete mode 100644 docs/src/cli/examples/deploy-a-program.md delete mode 100644 docs/src/cli/examples/durable-nonce.md delete mode 100644 docs/src/cli/examples/offline-signing.md delete mode 100644 docs/src/cli/examples/sign-offchain-message.md delete mode 100644 docs/src/cli/examples/test-validator.md delete mode 100644 docs/src/cli/examples/transfer-tokens.md delete mode 100644 docs/src/cli/index.md delete mode 100644 docs/src/cli/install.md delete mode 100644 docs/src/cli/intro.md delete mode 100644 docs/src/cli/wallets/_category_.json delete mode 100644 docs/src/cli/wallets/file-system.md delete mode 100644 docs/src/cli/wallets/hardware/_category_.json delete mode 100644 docs/src/cli/wallets/hardware/index.md delete mode 100644 docs/src/cli/wallets/hardware/ledger.md delete mode 100644 docs/src/cli/wallets/hardware/trezor.md delete mode 100644 docs/src/cli/wallets/index.md delete mode 100644 docs/src/cli/wallets/paper.md delete mode 100644 docs/src/clusters/available.md delete mode 100644 docs/src/clusters/benchmark.md delete mode 100644 docs/src/clusters/index.md delete mode 100644 docs/src/clusters/metrics.md delete mode 100644 docs/src/clusters/testnet.md delete mode 100644 docs/src/consensus/commitments.md delete mode 100644 docs/src/consensus/fork-generation.md delete mode 100644 docs/src/consensus/leader-rotation.md delete mode 100644 docs/src/consensus/managing-forks.md delete mode 100644 docs/src/consensus/stake-delegation-and-rewards.md delete mode 100644 docs/src/consensus/synchronization.md delete mode 100644 docs/src/consensus/turbine-block-propagation.md delete mode 100644 docs/src/consensus/vote-signing.md delete mode 100644 docs/src/css/custom.css delete mode 100644 docs/src/faq.md delete mode 100644 docs/src/implemented-proposals/abi-management.md delete mode 100644 docs/src/implemented-proposals/bank-timestamp-correction.md delete mode 100644 docs/src/implemented-proposals/commitment.md delete mode 100644 docs/src/implemented-proposals/durable-tx-nonces.md delete mode 100644 docs/src/implemented-proposals/ed_overview/ed_economic_sustainability.md delete mode 100644 docs/src/implemented-proposals/ed_overview/ed_mvp.md delete mode 100644 docs/src/implemented-proposals/ed_overview/ed_overview.md delete mode 100644 docs/src/implemented-proposals/ed_overview/ed_references.md delete mode 100644 docs/src/implemented-proposals/ed_overview/ed_storage_rent_economics.md delete mode 100644 docs/src/implemented-proposals/ed_overview/ed_validation_client_economics/ed_vce_overview.md delete mode 100644 docs/src/implemented-proposals/ed_overview/ed_validation_client_economics/ed_vce_state_validation_protocol_based_rewards.md delete mode 100644 docs/src/implemented-proposals/ed_overview/ed_validation_client_economics/ed_vce_state_validation_transaction_fees.md delete mode 100644 docs/src/implemented-proposals/ed_overview/ed_validation_client_economics/ed_vce_validation_stake_delegation.md delete mode 100644 docs/src/implemented-proposals/epoch_accounts_hash.md delete mode 100644 docs/src/implemented-proposals/index.md delete mode 100644 docs/src/implemented-proposals/installer.md delete mode 100644 docs/src/implemented-proposals/instruction_introspection.md delete mode 100644 docs/src/implemented-proposals/leader-leader-transition.md delete mode 100644 docs/src/implemented-proposals/leader-validator-transition.md delete mode 100644 docs/src/implemented-proposals/persistent-account-storage.md delete mode 100644 docs/src/implemented-proposals/readonly-accounts.md delete mode 100644 docs/src/implemented-proposals/reliable-vote-transmission.md delete mode 100644 docs/src/implemented-proposals/rent.md delete mode 100644 docs/src/implemented-proposals/repair-service.md delete mode 100644 docs/src/implemented-proposals/rpc-transaction-history.md delete mode 100644 docs/src/implemented-proposals/snapshot-verification.md delete mode 100644 docs/src/implemented-proposals/staking-rewards.md delete mode 100644 docs/src/implemented-proposals/testing-programs.md delete mode 100644 docs/src/implemented-proposals/tower-bft.md delete mode 100644 docs/src/implemented-proposals/transaction-fees.md delete mode 100644 docs/src/implemented-proposals/validator-timestamp-oracle.md delete mode 100644 docs/src/index.mdx delete mode 100644 docs/src/operations/_category_.json delete mode 100644 docs/src/operations/best-practices/_category_.json delete mode 100644 docs/src/operations/best-practices/general.md delete mode 100644 docs/src/operations/best-practices/monitoring.md delete mode 100644 docs/src/operations/best-practices/security.md delete mode 100644 docs/src/operations/guides/_category_.json delete mode 100644 docs/src/operations/guides/restart-cluster.md delete mode 100644 docs/src/operations/guides/validator-failover.md delete mode 100644 docs/src/operations/guides/validator-info.md delete mode 100644 docs/src/operations/guides/validator-monitor.md delete mode 100644 docs/src/operations/guides/validator-stake.md delete mode 100644 docs/src/operations/guides/validator-start.md delete mode 100644 docs/src/operations/guides/validator-troubleshoot.md delete mode 100644 docs/src/operations/guides/vote-accounts.md delete mode 100644 docs/src/operations/index.md delete mode 100644 docs/src/operations/prerequisites.md delete mode 100644 docs/src/operations/requirements.md delete mode 100644 docs/src/operations/running-with-af-xdp.md delete mode 100644 docs/src/operations/setup-a-validator.md delete mode 100644 docs/src/operations/setup-an-rpc-node.md delete mode 100644 docs/src/operations/validator-or-rpc-node.md delete mode 100644 docs/src/pages/styles.module.css delete mode 100644 docs/src/proposals.md delete mode 100644 docs/src/proposals/accepted-design-proposals.md delete mode 100644 docs/src/proposals/accounts-db-replication.md delete mode 100644 docs/src/proposals/bankless-leader.md delete mode 100644 docs/src/proposals/block-confirmation.md delete mode 100644 docs/src/proposals/cluster-test-framework.md delete mode 100644 docs/src/proposals/comprehensive-compute-fees.md delete mode 100644 docs/src/proposals/embedding-move.md delete mode 100644 docs/src/proposals/fee_transaction_priority.md delete mode 100644 docs/src/proposals/handle-duplicate-block.md delete mode 100644 docs/src/proposals/interchain-transaction-verification.md delete mode 100644 docs/src/proposals/ledger-replication-to-implement.md delete mode 100644 docs/src/proposals/log_data.md delete mode 100644 docs/src/proposals/off-chain-message-signing.md delete mode 100644 docs/src/proposals/optimistic-confirmation-and-slashing.md delete mode 100644 docs/src/proposals/optimistic-transaction-propagation-signal.md delete mode 100644 docs/src/proposals/optimistic_confirmation.md delete mode 100644 docs/src/proposals/partitioned-inflationary-rewards-distribution.md delete mode 100644 docs/src/proposals/program-instruction-macro.md delete mode 100644 docs/src/proposals/return-data.md delete mode 100644 docs/src/proposals/rip-curl.md delete mode 100644 docs/src/proposals/simple-payment-and-state-verification.md delete mode 100644 docs/src/proposals/slashing.md delete mode 100644 docs/src/proposals/snapshot-verification.md delete mode 100644 docs/src/proposals/tick-verification.md delete mode 100644 docs/src/proposals/timely-vote-credits.md delete mode 100644 docs/src/proposals/validator-proposal.md delete mode 100644 docs/src/proposals/versioned-transactions.md delete mode 100644 docs/src/proposals/vote-signing-to-implement.md delete mode 100644 docs/src/runtime/programs.md delete mode 100644 docs/src/runtime/sysvars.md delete mode 100644 docs/src/runtime/zk-docs/ciphertext_ciphertext_equality.pdf delete mode 100644 docs/src/runtime/zk-docs/ciphertext_commitment_equality.pdf delete mode 100644 docs/src/runtime/zk-docs/ciphertext_validity.pdf delete mode 100644 docs/src/runtime/zk-docs/percentage_with_cap.pdf delete mode 100644 docs/src/runtime/zk-docs/pubkey_proof.pdf delete mode 100644 docs/src/runtime/zk-docs/twisted_elgamal.pdf delete mode 100644 docs/src/runtime/zk-docs/zero_proof.pdf delete mode 100644 docs/src/runtime/zk-elgamal-proof.md delete mode 100644 docs/src/theme/Footer/index.js delete mode 100644 docs/src/theme/Footer/styles.module.css delete mode 100644 docs/src/validator/anatomy.md delete mode 100644 docs/src/validator/blockstore.md delete mode 100644 docs/src/validator/geyser.md delete mode 100644 docs/src/validator/gossip.md delete mode 100644 docs/src/validator/runtime.md delete mode 100644 docs/src/validator/tpu.md delete mode 100644 docs/src/validator/tvu.md delete mode 100644 docs/src/what-is-a-validator.md delete mode 100644 docs/src/what-is-an-rpc-node.md delete mode 100644 docs/static/.nojekyll delete mode 100644 docs/static/img/android-chrome-192x192.png delete mode 100644 docs/static/img/android-chrome-512x512.png delete mode 100644 docs/static/img/apple-touch-icon.png delete mode 100644 docs/static/img/dark-mark-white.inline.svg delete mode 100644 docs/static/img/data-plane-propagation.png delete mode 100644 docs/static/img/economic_design_infl_230719.png delete mode 100644 docs/static/img/favicon-16x16.png delete mode 100644 docs/static/img/favicon-32x32.png delete mode 100644 docs/static/img/favicon.ico delete mode 100644 docs/static/img/ledger-live-install-solana-app.png delete mode 100644 docs/static/img/ledger-live-latest-version-installed.png delete mode 100644 docs/static/img/ledger-live-update-available-v0.2.2.png delete mode 100644 docs/static/img/logo-horizontal-dark.svg delete mode 100644 docs/static/img/logo-horizontal.svg delete mode 100644 docs/static/img/logo.svg delete mode 100644 docs/static/img/p_ex_adjusted_staked_yields.png delete mode 100755 docs/static/img/p_ex_interest.png delete mode 100644 docs/static/img/p_ex_relative_dilution.png delete mode 100644 docs/static/img/p_ex_schedule.png delete mode 100644 docs/static/img/p_ex_staked_dilution.png delete mode 100644 docs/static/img/p_ex_staked_supply_w_range_initial_stake.png delete mode 100644 docs/static/img/p_ex_staked_yields.png delete mode 100644 docs/static/img/p_ex_supply.png delete mode 100644 docs/static/img/p_ex_unstaked_dilution.png delete mode 100644 docs/static/img/p_inflation_schedule.png delete mode 100644 docs/static/img/p_inflation_schedule_ranges_w_comments.png delete mode 100644 docs/static/img/p_total_supply.png delete mode 100644 docs/static/img/p_total_supply_ranges.png delete mode 100644 docs/static/img/porep_reward.png delete mode 100644 docs/static/img/ramp-tps-rounds.png delete mode 100644 docs/static/img/rt-dropped-minority-fork-post-process.png delete mode 100644 docs/static/img/rt-dropped-minority-fork-pre-process.png delete mode 100644 docs/static/img/rt-dropped-via-rpc-pool.png delete mode 100644 docs/static/img/rt-tpu-jito-labs.png delete mode 100644 docs/static/img/rt-tx-journey.png delete mode 100644 docs/static/img/spiral.svg delete mode 100644 docs/static/katex/README.md delete mode 100644 docs/static/katex/contrib/auto-render.js delete mode 100644 docs/static/katex/contrib/auto-render.min.js delete mode 100644 docs/static/katex/contrib/auto-render.mjs delete mode 100644 docs/static/katex/contrib/copy-tex.css delete mode 100644 docs/static/katex/contrib/copy-tex.js delete mode 100644 docs/static/katex/contrib/copy-tex.min.css delete mode 100644 docs/static/katex/contrib/copy-tex.min.js delete mode 100644 docs/static/katex/contrib/copy-tex.mjs delete mode 100644 docs/static/katex/contrib/mathtex-script-type.js delete mode 100644 docs/static/katex/contrib/mathtex-script-type.min.js delete mode 100644 docs/static/katex/contrib/mathtex-script-type.mjs delete mode 100644 docs/static/katex/contrib/mhchem.js delete mode 100644 docs/static/katex/contrib/mhchem.min.js delete mode 100644 docs/static/katex/contrib/mhchem.mjs delete mode 100644 docs/static/katex/contrib/render-a11y-string.js delete mode 100644 docs/static/katex/contrib/render-a11y-string.min.js delete mode 100644 docs/static/katex/contrib/render-a11y-string.mjs delete mode 100644 docs/static/katex/fonts/KaTeX_AMS-Regular.ttf delete mode 100644 docs/static/katex/fonts/KaTeX_AMS-Regular.woff delete mode 100644 docs/static/katex/fonts/KaTeX_AMS-Regular.woff2 delete mode 100644 docs/static/katex/fonts/KaTeX_Caligraphic-Bold.ttf delete mode 100644 docs/static/katex/fonts/KaTeX_Caligraphic-Bold.woff delete mode 100644 docs/static/katex/fonts/KaTeX_Caligraphic-Bold.woff2 delete mode 100644 docs/static/katex/fonts/KaTeX_Caligraphic-Regular.ttf delete mode 100644 docs/static/katex/fonts/KaTeX_Caligraphic-Regular.woff delete mode 100644 docs/static/katex/fonts/KaTeX_Caligraphic-Regular.woff2 delete mode 100644 docs/static/katex/fonts/KaTeX_Fraktur-Bold.ttf delete mode 100644 docs/static/katex/fonts/KaTeX_Fraktur-Bold.woff delete mode 100644 docs/static/katex/fonts/KaTeX_Fraktur-Bold.woff2 delete mode 100644 docs/static/katex/fonts/KaTeX_Fraktur-Regular.ttf delete mode 100644 docs/static/katex/fonts/KaTeX_Fraktur-Regular.woff delete mode 100644 docs/static/katex/fonts/KaTeX_Fraktur-Regular.woff2 delete mode 100644 docs/static/katex/fonts/KaTeX_Main-Bold.ttf delete mode 100644 docs/static/katex/fonts/KaTeX_Main-Bold.woff delete mode 100644 docs/static/katex/fonts/KaTeX_Main-Bold.woff2 delete mode 100644 docs/static/katex/fonts/KaTeX_Main-BoldItalic.ttf delete mode 100644 docs/static/katex/fonts/KaTeX_Main-BoldItalic.woff delete mode 100644 docs/static/katex/fonts/KaTeX_Main-BoldItalic.woff2 delete mode 100644 docs/static/katex/fonts/KaTeX_Main-Italic.ttf delete mode 100644 docs/static/katex/fonts/KaTeX_Main-Italic.woff delete mode 100644 docs/static/katex/fonts/KaTeX_Main-Italic.woff2 delete mode 100644 docs/static/katex/fonts/KaTeX_Main-Regular.ttf delete mode 100644 docs/static/katex/fonts/KaTeX_Main-Regular.woff delete mode 100644 docs/static/katex/fonts/KaTeX_Main-Regular.woff2 delete mode 100644 docs/static/katex/fonts/KaTeX_Math-BoldItalic.ttf delete mode 100644 docs/static/katex/fonts/KaTeX_Math-BoldItalic.woff delete mode 100644 docs/static/katex/fonts/KaTeX_Math-BoldItalic.woff2 delete mode 100644 docs/static/katex/fonts/KaTeX_Math-Italic.ttf delete mode 100644 docs/static/katex/fonts/KaTeX_Math-Italic.woff delete mode 100644 docs/static/katex/fonts/KaTeX_Math-Italic.woff2 delete mode 100644 docs/static/katex/fonts/KaTeX_SansSerif-Bold.ttf delete mode 100644 docs/static/katex/fonts/KaTeX_SansSerif-Bold.woff delete mode 100644 docs/static/katex/fonts/KaTeX_SansSerif-Bold.woff2 delete mode 100644 docs/static/katex/fonts/KaTeX_SansSerif-Italic.ttf delete mode 100644 docs/static/katex/fonts/KaTeX_SansSerif-Italic.woff delete mode 100644 docs/static/katex/fonts/KaTeX_SansSerif-Italic.woff2 delete mode 100644 docs/static/katex/fonts/KaTeX_SansSerif-Regular.ttf delete mode 100644 docs/static/katex/fonts/KaTeX_SansSerif-Regular.woff delete mode 100644 docs/static/katex/fonts/KaTeX_SansSerif-Regular.woff2 delete mode 100644 docs/static/katex/fonts/KaTeX_Script-Regular.ttf delete mode 100644 docs/static/katex/fonts/KaTeX_Script-Regular.woff delete mode 100644 docs/static/katex/fonts/KaTeX_Script-Regular.woff2 delete mode 100644 docs/static/katex/fonts/KaTeX_Size1-Regular.ttf delete mode 100644 docs/static/katex/fonts/KaTeX_Size1-Regular.woff delete mode 100644 docs/static/katex/fonts/KaTeX_Size1-Regular.woff2 delete mode 100644 docs/static/katex/fonts/KaTeX_Size2-Regular.ttf delete mode 100644 docs/static/katex/fonts/KaTeX_Size2-Regular.woff delete mode 100644 docs/static/katex/fonts/KaTeX_Size2-Regular.woff2 delete mode 100644 docs/static/katex/fonts/KaTeX_Size3-Regular.ttf delete mode 100644 docs/static/katex/fonts/KaTeX_Size3-Regular.woff delete mode 100644 docs/static/katex/fonts/KaTeX_Size3-Regular.woff2 delete mode 100644 docs/static/katex/fonts/KaTeX_Size4-Regular.ttf delete mode 100644 docs/static/katex/fonts/KaTeX_Size4-Regular.woff delete mode 100644 docs/static/katex/fonts/KaTeX_Size4-Regular.woff2 delete mode 100644 docs/static/katex/fonts/KaTeX_Typewriter-Regular.ttf delete mode 100644 docs/static/katex/fonts/KaTeX_Typewriter-Regular.woff delete mode 100644 docs/static/katex/fonts/KaTeX_Typewriter-Regular.woff2 delete mode 100644 docs/static/katex/katex.css delete mode 100644 docs/static/katex/katex.js delete mode 100644 docs/static/katex/katex.min.css delete mode 100644 docs/static/katex/katex.min.js delete mode 100644 docs/static/katex/katex.mjs diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 2d6592bd6cd..cebba555c88 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -41,20 +41,6 @@ updates: - "shaq" - "wincode" - - package-ecosystem: "npm" - directory: "/docs" - schedule: - interval: weekly - time: "08:00" - timezone: Etc/UTC - open-pull-requests-limit: 3 - cooldown: - default-days: 7 - groups: - docs-dependencies: - patterns: - - "*" - - package-ecosystem: "github-actions" directory: "/" schedule: diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml deleted file mode 100644 index bd8b4c4bc94..00000000000 --- a/.github/workflows/docs.yml +++ /dev/null @@ -1,134 +0,0 @@ -name: Docs - -on: - push: - branches: - - master - - v[0-9]+.[0-9]+ - tags: - - v[0-9]+.[0-9]+.[0-9]+ - - v[0-9]+.[0-9]+.[0-9]+-* - pull_request: - branches: - - master - - v[0-9]+.[0-9]+ - -jobs: - check: - if: github.repository_owner == 'anza-xyz' - outputs: - continue: ${{ steps.check.outputs.need_to_build }} - runs-on: ubuntu-22.04 - steps: - - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - fetch-depth: 0 - - - name: Get commit range (push) - if: ${{ github.event_name == 'push' }} - run: | - echo "COMMIT_RANGE=${{ github.event.before }}..$GITHUB_SHA" >> $GITHUB_ENV - - - name: Get commit range (pull_request) - if: ${{ github.event_name == 'pull_request' }} - run: | - echo "COMMIT_RANGE=${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}" >> $GITHUB_ENV - - - name: Get file status - run: | - set +e - git diff --name-only $COMMIT_RANGE | grep \ - -e '.github/workflows/docs.yml' \ - -e 'docs/**' - echo "FILE_CHANGED=$?" >> $GITHUB_ENV - - - name: Check - id: check - shell: bash - run: | - source ci/env.sh - eval "$(ci/channel-info.sh)" - TAG=$CI_TAG - - echo "TAG: $TAG" - echo "CHANNEL: $CHANNEL" - echo "FILE_CHANGED: $FILE_CHANGED" - - echo need_to_build="$( - if [ "$TAG" != '' ] - then - echo 1 - elif [ $FILE_CHANGED = 0 ] && ( [ "$CHANNEL" = "beta" ] || [ "$CHANNEL" = "edge" ] ) - then - echo 1 - else - echo 0 - fi - )" >> $GITHUB_OUTPUT - - build: - needs: - - check - if: > - github.repository_owner == 'anza-xyz' && - needs.check.outputs.continue == 1 - runs-on: ubuntu-22.04 - steps: - - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - - name: Setup pnpm - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 - with: - version: 10 - - - name: Setup Node - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: 24 - package-manager-cache: false - - - name: Build - working-directory: docs - run: | - pnpm install --frozen-lockfile - ./build.sh - - - name: Upload artifacts - if: ${{ github.event_name == 'push' }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: docs-build - path: ./docs/build - retention-days: 1 - - deploy: - needs: - - build - if: > - github.repository_owner == 'anza-xyz' && - github.event_name == 'push' - runs-on: ubuntu-22.04 - steps: - - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - - name: Setup Node - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: 24 - package-manager-cache: false - - - name: Download artifacts - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: docs-build - path: ./docs/build - - - name: Deploy - working-directory: docs - run: ./deploy.sh - env: - VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} - VERCEL_SCOPE: ${{ secrets.VERCEL_SCOPE }} diff --git a/.mergify.yml b/.mergify.yml index fbf17d99a40..49b68271986 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -35,10 +35,6 @@ pull_request_rules: - status-success=ci-gate - label=automerge - label!=no-automerge - - or: - # only require docs checks if docs files changed - - -files~=^docs/ - - status-success=build & deploy docs - or: - -files~=(\.rs|Cargo\.toml|Cargo\.lock|\.github/scripts/cargo-clippy-before-script\.sh|\.github/workflows/cargo\.yml)$ - or: diff --git a/docs/README.md b/docs/README.md index ebc9ad78ee7..f9e72e94921 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,124 +1,5 @@ -# Solana Validator Docs Readme +# Documentation -This validator's documentation is built using [Docusaurus v2](https://v2.docusaurus.io/) with `npm`. -Static content delivery is handled using `vercel`. +The documentation has moved to its own repository: -> Note: The documentation within this repo is specifically focused on the -> Agave validator client maintained by Anza. Solana protocol documentation, which applies -> to all Solana validator implementations, is maintained within the -> [`developer-content`](https://github.com/solana-foundation/developer-content/) -> repo. Those protocol docs are managed by the Solana Foundation within their -> GitHub organization and are publicly accessible via -> [solana.com/docs](https://solana.com/docs) - -## Local Development - -To set up the Solana Validator Docs site locally: - -- install dependencies using `npm` -- build locally via `./build.sh` -- run the local development server -- make your changes and updates as needed - -> Note: After cloning this repo to your local machine, all the local development commands are run from within this `docs` directory. - -### Install dependencies - -Install the site's dependencies via `npm`: - -```bash -npm install -``` - -### Build locally - -The build script generates static content into the `build` directory and can be served using any static content hosting service. - -```bash -./build.sh -``` - -Running this build script requires **Docker**, and will auto fetch the [solanalabs/rust](https://hub.docker.com/r/solanalabs/rust) image from Docker hub to compile the desired version of the [Solana CLI](https://docs.anza.xyz/cli) from source. - -This build script will also: - -- generate the `cli/usage.md` document from the output of each of the Solana CLI commands and sub-commands -- convert each of the `art/*.bob` files into SVG images used throughout the docs - -> Note: Running this build script is **required** before being able to run the site locally via the `npm run start` command since it will generate the `cli/usage.md` document. - -If you run into errors or issues with this step, see [Common Issues](#common-issues) below. See also [CI Build Flow](#ci-build-flow) for more details on production deployments of the docs. - -### Local development server - -This command starts the Docusaurus local development server and opens up a browser window. - -```bash -npm run start -``` - -> Note: Most changes are reflected live without having to restart the server or refresh the page. However, some changes may require a manual refresh of the page or a restart of the development server (via the command above). - -## CI Build Flow - -The docs are built and published in GitHub Actions with the `docs.yml` workflow. On each PR, the docs are built but not published. - -In each post-commit build, docs are built and published using `vercel` to their respective domain depending on the build branch. - -- Master branch docs are published to `edge.docs.anza.xyz` -- Beta branch docs are published to `beta.docs.anza.xyz` -- Latest release tag docs are published to `docs.anza.xyz` - -## Common Issues - -### Bad sidebars file (or `cli/usage` not found) - -```bash -Error: Bad sidebars file. -These sidebar document ids do not exist: -- cli/usage, -``` - -If you have NOT successfully run the build script, then the `cli/usage.md` file will not exist within your local repo (since it is in `.gitignore`). Not having this doc file, will result in the error message above. - -If the Rust toolchain (specifically `cargo`) is installed on your system, you can specifically build the `cli/usage.md` document via: - -```bash -./build-cli-usage.sh -``` - -Or using Docker and the normal build script, you can perform a full production build of the docs to generate this file: - -```bash -./build.sh -``` - -### Permission denied for the Docker socket - -```bash -Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Post -``` - -Running docs build script (`./build.sh`) required the use of Docker.\*\*\*\* - -Ensuring you have Docker installed on your system and it is running. - -You may also try running either of these build scripts (and by association, Docker) with elevation permissions via `sudo`: - -```bash -sudo ./build.sh -# or -sudo ./build-cli-usage.sh -``` - -### Multiple SVG images not found - -```bash -Error: Image static/img/***.svg used in src/***.md not found. -``` - -During the build process of the docs (specifically within the `./convert-ascii-to-svg.sh` script run by `./build.sh`), each of the `art/*.bob` files are converted to SVG images and saved to the `static/img` directory. - -To correct this issue, use the steps above to [build the docs locally](#build-locally). - -> Note: While not generating and saving these SVG images within your local repo will **NOT** prevent you from running the local development server, it will result in numerous output errors in your terminal. +https://github.com/anza-xyz/docs.anza.xyz diff --git a/docs/art/fork-generation.bob b/docs/art/fork-generation.bob deleted file mode 100644 index e8f60e02523..00000000000 --- a/docs/art/fork-generation.bob +++ /dev/null @@ -1,13 +0,0 @@ - validator action - +----+ ---------------- - | | L1 | E1 - | +----+ / \ vote(E1) - | | L2 | E2 x - | +----+ / \ / \ vote(E2) - time | | L3 | E3 x E3' x - | +----+ / \ / \ / \ / \ slash(E3) - | | L4 | x x E4 x x x x x - | +----+ | | | | | | | | vote(E4) - v | L5 | xx xx xx E5 xx xx xx xx - +----+ hang on to E4 and E5 for more... - diff --git a/docs/art/forks-pruned.bob b/docs/art/forks-pruned.bob deleted file mode 100644 index 78a951c3afe..00000000000 --- a/docs/art/forks-pruned.bob +++ /dev/null @@ -1,9 +0,0 @@ - 1 - | - 2 - /| - / | - | | - | 4 - | - 5 diff --git a/docs/art/forks-pruned2.bob b/docs/art/forks-pruned2.bob deleted file mode 100644 index 64e3d4cb1f7..00000000000 --- a/docs/art/forks-pruned2.bob +++ /dev/null @@ -1,11 +0,0 @@ - 1 - | - 3 - |\ - | \ - | | - | | - | | - 6 | - | - 7 diff --git a/docs/art/forks.bob b/docs/art/forks.bob deleted file mode 100644 index 0c335e95cc5..00000000000 --- a/docs/art/forks.bob +++ /dev/null @@ -1,13 +0,0 @@ - 1 - |\ - 2 \ - /| | - / | 3 - | | |\ - | 4 | \ - | | | - 5 | | - | | - 6 | - | - 7 diff --git a/docs/art/passive-staking-callflow.msc b/docs/art/passive-staking-callflow.msc deleted file mode 100644 index ae754411e48..00000000000 --- a/docs/art/passive-staking-callflow.msc +++ /dev/null @@ -1,30 +0,0 @@ -msc { - hscale="2.2"; - VoteSigner, - Validator, - Cluster, - StakerX, - StakerY; - - |||; - Validator box Validator [label="boot.."]; - - VoteSigner <:> Validator [label="register\n\n(optional)"]; - Validator => Cluster [label="VoteState::Initialize(VoteSigner)"]; - StakerX => Cluster [label="StakeStateV2::Delegate(Validator)"]; - StakerY => Cluster [label="StakeStateV2::Delegate(Validator)"]; - - |||; - Validator box Cluster [label="\nvalidate\n"]; - Validator => VoteSigner [label="sign(vote)"]; - VoteSigner >> Validator [label="signed vote"]; - - Validator => Cluster [label="gossip(vote)"]; - ...; - ... ; - Validator abox Validator [label="\nmax\nlockout\n"]; - |||; - Cluster box Cluster [label="credits redeemed (at epoch)"]; - - -} diff --git a/docs/art/retransmit_stage.bob b/docs/art/retransmit_stage.bob deleted file mode 100644 index 425aed753a4..00000000000 --- a/docs/art/retransmit_stage.bob +++ /dev/null @@ -1,45 +0,0 @@ - +------------+ - | Gossip | - | Service | - | | - +------------+ - |ContactInfo - | - +------------------------------------------------------------------------+ - | | | - | Retransmit Stage | | - | | | - | +--------------------------------------+ | - | |Window Service | | | +---------+ - | | | | +---------------+ | +-----------+ | | - Packets | | +----------------+ | | Shreds | | | | Deshredder| | Replay | - +------------------->+ ShredFilter +----------------------------->+ Blockstore +------| (entries) |------>+ Stage | - | | +-----------+-+--+ | | | | | +-----------+ | | -+--------+ | | ^ | | | | | | +---------+ -| | Leader Schedule| | | | | | | | | -| Bank +----------------------------+ | +------+----+ | +-------+-------+ | -| | | | | | Repair | | | | -+--------+ +----------------------------------->+ Service | | Incomplete | | - | | | | | +<----------------------+ | - | | | | +-----------+ | Slots | - | | | | | | - | | +--------------------------------------+ | - | | |Shreds | - | | v | - v | +-------+---------+ | - +--------+-----+ | | | | - | | | | Retransmitter | | - | Peer | | | | | - | Validators +<---------------+ | | - | | | +--------+--------+ | - | | | ^ | - +--------------+ | | | - +------------------------------------------------------------------------+ - | - ContactInfo| - | - +-------+-+ - | Gossip | - | Service | - | | - +---------+ diff --git a/docs/art/runtime.bob b/docs/art/runtime.bob deleted file mode 100644 index 3f56d8d145b..00000000000 --- a/docs/art/runtime.bob +++ /dev/null @@ -1,10 +0,0 @@ -.------------. .-----------. .---------------. .--------------. .-----------------------. -| PoH verify +---> | sigverify +--->| lock accounts +--->| validate fee +--->| allocate new accounts +---> -| TVU | `-----------` `---------------` `--------------` `-----------------------` -`------------` - - .---------------. .---------. .------------. .-----------------. .-----------------. ---->| load accounts +--->| execute +--->| PoH record +--->| commit accounts +-->| unlock accounts | - `---------------` `---------` | TPU | `-----------------` `-----------------` - `------------` - diff --git a/docs/art/sdk-tools.bob b/docs/art/sdk-tools.bob deleted file mode 100644 index 64c1affee6b..00000000000 --- a/docs/art/sdk-tools.bob +++ /dev/null @@ -1,20 +0,0 @@ - - .-------------------------------------. - | Solana Runtime | - | | - .----------. | .------------. .------------. | - | Program | | | SBF | | Executable | | - | Author +------>| Bytecode +-->| Account | | - | | | | Verifier | | | | - `----------` | `------------` `------------` | - | | | - | .----------------` | - | | LoadAccounts | - | V | - .----------. | .------------. .-------------. | - | | | | SBF | | SBF | | - | Client +------>| Loader +-->| Interpreter | | - | | | | | | | | - `----------` | `------------` `-------------` | - | | - `-------------------------------------` diff --git a/docs/art/spv-bank-hash.bob b/docs/art/spv-bank-hash.bob deleted file mode 100644 index fce7870d631..00000000000 --- a/docs/art/spv-bank-hash.bob +++ /dev/null @@ -1,19 +0,0 @@ - +----------+ - | Bank-Hash| - +----------+ - ^ - | - +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+ - : : - : +--------------+ +-------------+ : - : Hash( | Accounts-Hash| + | Block-Merkle| ) : - : +--------------+ +-------------+ : - : ^ : - +~~~~~~~~~~~~~ | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+ - | - +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+ - : +---------------+ +---------------+ +---------------+ : - : Hash( | Hash(Account1)| + | Hash(Account2)| + ... + | Hash(AccountN)| ) : - : +---------------+ +---------------+ +---------------+ : - +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+ - diff --git a/docs/art/spv-block-merkle.bob b/docs/art/spv-block-merkle.bob deleted file mode 100644 index 8b236e6b5ce..00000000000 --- a/docs/art/spv-block-merkle.bob +++ /dev/null @@ -1,19 +0,0 @@ - +---------------+ - | Block-Merkle | - +---------------+ - ^ ^ - / \ - +-------------+ +-------------+ - | Entry-Merkle| | Entry-Merkle| - +-------------+ +-------------+ - ^ ^ - / \ - +-------+ +-------+ - | Hash | | Hash | - +-------+ +-------+ - ^ ^ ^ ^ - / | | \ - +-----------------+ +-----------------+ +-----------------+ +---+ - | Hash(T1, status)| | Hash(T2, status)| | Hash(T3, status)| | 0 | - +-----------------+ +-----------------+ +-----------------+ +---+ - diff --git a/docs/art/tpu.bob b/docs/art/tpu.bob deleted file mode 100644 index 6c431efb077..00000000000 --- a/docs/art/tpu.bob +++ /dev/null @@ -1,25 +0,0 @@ - - .-------------. - | PoH Service | - `--------+----` - ^ | - .--------------------------------|----|--------------------. - | TPU | v | - | .-------. .-----------. .-+-------. .-----------. | .------------. - .---------. | | Fetch | | SigVerify | | Banking | | Broadcast | | | Downstream | - | Clients |--->| Stage |->| Stage |-+->| Stage |->| Stage |---->| Validators | - `---------` | | | | | | | | | | | | | - | `-------` `-----------` | `----+----` `-----------` | `------------` - | v | | - | .--------+---. | | - | | Forwarding | | | - | | Stage | | | - | | | | | - | `------------` | | - | | | - `-----------------------------------|----------------------` - | - v - .------. - | Bank | - `------` diff --git a/docs/art/transaction.bob b/docs/art/transaction.bob deleted file mode 100644 index 90e439e5f7f..00000000000 --- a/docs/art/transaction.bob +++ /dev/null @@ -1,9 +0,0 @@ - - +------------+-----------------------------------------------------+ - | | Message | - | |----------+-------------+-------------+--------------| - | | | | | | - | Signatures | | Account | Recent | | - | | Header | addresses | blockhash | Instructions | - | | | | | | - +------------+----------+-------------+-------------+--------------+ \ No newline at end of file diff --git a/docs/art/tvu.bob b/docs/art/tvu.bob deleted file mode 100644 index c298653478d..00000000000 --- a/docs/art/tvu.bob +++ /dev/null @@ -1,37 +0,0 @@ - +------------+ - | Gossip | - | Service | - +-------------+ | | - |Child | +------------+ +-+-----+----+ - |Validators | |Neighborhood| | ^ - | | |Validators | | | - +-------+-----+ | | | | - ^ +---------+--+ Peer| |Votes - | ^ List| | - +------------------------+ | | | - shreds(forward = true) | | | | - +---------------------------------------------------------------------------------------------+ - | | | | | | - | TVU | | +--------+ | | - | | | v | | -+------------+ | +-------+ +------------+ +-+--+---+---+ +---+--------------+ +-------------+ | -| | Repair | | | | | | Retransmit | | Replay | | Transaction | | -| Upstream +------------->+ +->+ Shred +--->+ Stage +----->+ Stage +----->+ Status | | -| Validators | Turbine | | Shred | | Verify | | | | +--------------+ | | Service | | -| +------------->+ Fetch | | Leader Sig | +------+-----+ | | PoH Verify | | | (optional) | | -| | | | Stage | | Stage | ^ | | TX Sig Verify| | +-------------+ | -| +------------->+ | | | | | | | | | -| | Turbine | | | +--+---------+ | | +-+------------+ | | -+------------+ Forwards | +-------+ ^ | | | | | - | | | +------------------+ | - | | | | | - | | | | | - | | | | | - +---------------------------------------------------------------------------------------------+ - | | | - | |Validator v - | |Stakes +---+-----+ - | +------------+ | - +----------------------------------+ Bank | - Leader | | - Schedule +---------+ diff --git a/docs/art/validator-proposal.bob b/docs/art/validator-proposal.bob deleted file mode 100644 index cde432bbc66..00000000000 --- a/docs/art/validator-proposal.bob +++ /dev/null @@ -1,60 +0,0 @@ - - .------------. - | Upstream | - | Validators | - `----+-------` - | - | - .-----------------------------------. - | Validator | | - | v | - | .-----------. .------------. | - .--------. | | Fetch | | Repair | | - | Client +---->| Stage | | Stage | | - `--------` | `---+-------` `----+-------` | - | | | | - | v v | - | .-----------. .------------. | - | | TPU |<-->| Blockstore | | - | | | | | | - | `-----------` `----+-------` | - | | | - | v | - | .------------. | - | | Multicast | | - | | Stage | | - | `----+-------` | - | | | - `-----------------------------------` - | - v - .------------. - | Downstream | - | Validators | - `------------` - - - - .------------. - | PoH | - | Service | - `-------+----` - ^ | - | | - .-----------------------------------. - | TPU | | | - | | v | - .-------. | .-----------. .---+--------. | .------------. - | Fetch +---->| SigVerify +--->| Banking |<--->| Blockstore | - | Stage | | | Stage | | Stage | | | | - `-------` | `-----------` `-----+------` | `------------` - | | | - | | | - `-----------------------------------` - | - v - .------------. - | Banktree | - | | - `------------` - diff --git a/docs/art/validator.bob b/docs/art/validator.bob deleted file mode 100644 index 87c7610a9a0..00000000000 --- a/docs/art/validator.bob +++ /dev/null @@ -1,30 +0,0 @@ - .---------------------------------------. - | Validator | - | | - .--------. | .-------------------. | - | |---->| | | - | Client | | | JSON RPC Service | | - | |<----| | | - `----+---` | `-------------------` | - | | ^ | - | | | .----------------. | .------------------. - | | | | Gossip Service |<-----------| Validators | - | | | `----------------` | | | - | | | ^ | | | - | | | | | | .------------. | - | | .---+---. .----+---. .------------. | | | | | - | | | Bank |<-+ Replay | | ShredFetch |<------+ Upstream | | - | | | Forks | | Stage | | Stage | | | | Validators | | - | | `-------` `--------` `--+---------` | | | | | - | | ^ ^ | | | `------------` | - | | | | v | | | - | | | .--+---------. | | | - | | | | Blockstore | | | | - | | | `------------` | | .------------. | - | | | ^ | | | | | - | | | | | | | Downstream | | - | | .--+--. .-------+---. | | | Validators | | - `-------->| TPU +---->| Broadcast +---------------->| | | - | `-----` | Stage | | | `------------` | - | `-----------` | `------------------` - `---------------------------------------` diff --git a/docs/babel.config.js b/docs/babel.config.js deleted file mode 100644 index bfd75dbdfc7..00000000000 --- a/docs/babel.config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - presets: [require.resolve("@docusaurus/core/lib/babel/preset")], -}; diff --git a/docs/build-cli-usage.sh b/docs/build-cli-usage.sh deleted file mode 100755 index bc0d850337d..00000000000 --- a/docs/build-cli-usage.sh +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env bash -set -e - -cd "$(dirname "$0")" - -# shellcheck source=ci/env.sh -source ../ci/env.sh - -# Get current channel -eval "$(../ci/channel-info.sh)" - -# set the output file' location -out=${1:-src/cli/usage.md} - -# load the usage file's header -cat src/cli/.usage.md.header > "$out" - -# Skip generating the detailed usage doc for non deployment commits of the docs -if [[ -n $CI ]]; then - if [[ $CI_BRANCH != $EDGE_CHANNEL* ]] && [[ $CI_BRANCH != $BETA_CHANNEL* ]] && [[ $CI_BRANCH != $STABLE_CHANNEL* ]]; then - echo "**NOTE:** The usage doc is only auto-generated during full production deployments of the docs" - echo "**NOTE:** This usage doc is auto-generated during deployments" >> "$out" - exit - fi -fi - -echo 'Building the solana cli from source...' - -# shellcheck source=ci/rust-version.sh -source ../ci/rust-version.sh stable - -: "${rust_stable:=}" # Pacify shellcheck - -usage=$(cargo -q run -p solana-cli -- -C ~/.foo --help | sed -e 's|'"$HOME"'|~|g' -e 's/[[:space:]]\+$//') - -section() { - declare mark=${2:-"###"} - declare section=$1 - read -r name rest <<<"$section" - - printf '%s %s -' "$mark" "$name" - printf '```text -%s -``` - -' "$section" -} - -section "$usage" >> "$out" - -usage=$(sed -e '/^ \{5,\}/d' <<<"$usage") - -in_subcommands=0 -while read -r subcommand rest; do - [[ $subcommand == "SUBCOMMANDS:" ]] && in_subcommands=1 && continue - if ((in_subcommands)); then - section "$(cargo -q run -p solana-cli -- help "$subcommand" | sed -e 's|'"$HOME"'|~|g' -e 's/[[:space:]]\+$//')" "###" >> "$out" - fi -done <<<"$usage">>"$out" diff --git a/docs/build.sh b/docs/build.sh deleted file mode 100755 index 860e12d35f0..00000000000 --- a/docs/build.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash -set -ex - -cd "$(dirname "$0")" - -# shellcheck source=ci/env.sh -source ../ci/env.sh - -# shellcheck source=ci/rust-version.sh -source ../ci/rust-version.sh -../ci/docker-run-default-image.sh docs/build-cli-usage.sh -../ci/docker-run-default-image.sh docs/convert-ascii-to-svg.sh -./set-solana-release-tag.sh - -# Build from /src into /build -pnpm run build -echo $? diff --git a/docs/components/Card.jsx b/docs/components/Card.jsx deleted file mode 100644 index 67989e67dc1..00000000000 --- a/docs/components/Card.jsx +++ /dev/null @@ -1,53 +0,0 @@ -import React from "react"; -import clsx from "clsx"; -import Link from "@docusaurus/Link"; -import styles from "../src/pages/styles.module.css"; -import Translate from "@docusaurus/Translate"; - -function Card({ to, header, body, externalIcon = false }) { - /* - Both the `header` and `body` expect an object with the following type - header = { - label: String, // - translateId: String // - } - */ - - return ( -
- -
-

- - {header.label} - - {externalIcon && ( - - )} -

-
- - {typeof body === "object" && ( -
-

- {body.label} -

-
- )} - -
- ); -} - -export default Card; diff --git a/docs/components/HomeCtaLinks.jsx b/docs/components/HomeCtaLinks.jsx deleted file mode 100644 index 71d20014adf..00000000000 --- a/docs/components/HomeCtaLinks.jsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from "react"; -import Card from "./Card"; - -export default function HomeCtaLinks() { - return ( -
-
- - - - - -
-
- ); -} diff --git a/docs/convert-ascii-to-svg.sh b/docs/convert-ascii-to-svg.sh deleted file mode 100755 index f5abd7b229f..00000000000 --- a/docs/convert-ascii-to-svg.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env bash - -# Convert .bob and .msc files in docs/art to .svg files located where the -# site build will find them. - -set -e - -cd "$(dirname "$0")" -output_dir=static/img - -svgbob_cli="$(command -v svgbob_cli || true)" -if [[ -z "$svgbob_cli" ]]; then - svgbob_cli="$(command -v svgbob || true)" - [[ -n "$svgbob_cli" ]] || ( echo "svgbob_cli binary not found" && exit 1 ) -fi - -mkdir -p "$output_dir" - -while read -r bob_file; do - out_file=$(basename "${bob_file%.*}".svg) - "$svgbob_cli" "$bob_file" --output "$output_dir/$out_file" -done < <(find art/*.bob) - -while read -r msc_file; do - out_file=$(basename "${msc_file%.*}".png) - mscgen -T png -o "$output_dir/$out_file" -i "$msc_file" -done < <(find art/*.msc) diff --git a/docs/deploy.sh b/docs/deploy.sh deleted file mode 100755 index 0e090fb7be2..00000000000 --- a/docs/deploy.sh +++ /dev/null @@ -1,186 +0,0 @@ -#!/usr/bin/env bash - -set -eo pipefail - -here=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) - -# shellcheck source=ci/env.sh -source "$here/../ci/env.sh" - -if [[ -z $CI ]]; then - echo "Publishing docs from local" - if [[ -z $PROJECT_NAME ]]; then - echo "❌ PROJECT_NAME is undefined" - exit 1 - fi - PROJECT_NAMES=("$PROJECT_NAME") -else - echo "Publishing docs from CI" - - # skip docs publish from pull requests - if [[ -n $CI_PULL_REQUEST ]]; then - echo "skipping docs publish from pull requests" - exit 0 - fi - - # get the channel info - eval "$("$here/../ci/channel-info.sh")" - if [[ -n $CI_TAG ]]; then - if [[ $CI_TAG != $BETA_CHANNEL* ]]; then - echo "skipping docs publish from non-beta tag" - exit 0 - fi - PROJECT_NAMES=("docs-anza-xyz" "beta-docs-anza-xyz") - else - case $CHANNEL in - edge) - PROJECT_NAMES=("edge-docs-anza-xyz") - ;; - beta) - PROJECT_NAMES=("beta-docs-anza-xyz") - ;; - stable) - echo "skipping docs publish from stable channel" - exit 0 - ;; - *) - echo "❌ unknown channel: '$CHANNEL'" - exit 1 - ;; - esac - fi -fi -echo "PROJECT_NAMES: ${PROJECT_NAMES[*]}" - -if [[ -z $VERCEL_TOKEN ]]; then - echo "❌ VERCEL_TOKEN is undefined" - exit 1 -fi - -for PROJECT_NAME in "${PROJECT_NAMES[@]}"; do - echo "Publishing docs for $PROJECT_NAME" - # create the vercel.json file - CONFIG_FILE=vercel.json - cat >"$CONFIG_FILE" <0.2%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] - } -} diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml deleted file mode 100644 index b03764aa8bf..00000000000 --- a/docs/pnpm-lock.yaml +++ /dev/null @@ -1,11000 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -overrides: - got@<11.8.5: ^11.8.5 - katex@^0.12: ^0.16.21 - minimatch@^3.0.0: ^3.0.5 - path-to-regexp@^1.7.0: ^1.9.0 - serve-handler@^6.1.0: ^6.1.6 - trim@^0.0.1: ^0.0.3 - wait-on@^6.0.0: ^8.0.3 - -importers: - - .: - dependencies: - '@docusaurus/core': - specifier: ^2.4.3 - version: 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/plugin-google-gtag': - specifier: ^2.4.3 - version: 2.4.3(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/preset-classic': - specifier: ^2.4.3 - version: 2.4.3(@algolia/client-search@4.14.2)(@types/react@18.0.26)(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/theme-search-algolia': - specifier: ^2.4.3 - version: 2.4.3(@algolia/client-search@4.14.2)(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(@types/react@18.0.26)(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@mdx-js/react': - specifier: ^1.6.22 - version: 1.6.22(react@16.14.0) - babel-eslint: - specifier: ^10.1.0 - version: 10.1.0(eslint@7.32.0) - clsx: - specifier: ^1.2.1 - version: 1.2.1 - eslint: - specifier: ^7.32.0 - version: 7.32.0 - eslint-plugin-react: - specifier: ^7.37.4 - version: 7.37.4(eslint@7.32.0) - postcss: - specifier: ^8.5.3 - version: 8.5.3 - postcss-loader: - specifier: ^4.3.0 - version: 4.3.0(postcss@8.5.3)(webpack@5.105.0) - prettier: - specifier: ^2.8.8 - version: 2.8.8 - react: - specifier: ^16.14.0 - version: 16.14.0 - react-dom: - specifier: ^16.14.0 - version: 16.14.0(react@16.14.0) - rehype-katex: - specifier: ^4.0.0 - version: 4.0.0 - remark-math: - specifier: ^3.0.1 - version: 3.0.1 - -packages: - - '@algolia/autocomplete-core@1.7.1': - resolution: {integrity: sha512-eiZw+fxMzNQn01S8dA/hcCpoWCOCwcIIEUtHHdzN5TGB3IpzLbuhqFeTfh2OUhhgkE8Uo17+wH+QJ/wYyQmmzg==} - - '@algolia/autocomplete-preset-algolia@1.7.1': - resolution: {integrity: sha512-pJwmIxeJCymU1M6cGujnaIYcY3QPOVYZOXhFkWVM7IxKzy272BwCvMFMyc5NpG/QmiObBxjo7myd060OeTNJXg==} - peerDependencies: - '@algolia/client-search': ^4.9.1 - algoliasearch: ^4.9.1 - - '@algolia/autocomplete-shared@1.7.1': - resolution: {integrity: sha512-eTmGVqY3GeyBTT8IWiB2K5EuURAqhnumfktAEoHxfDY2o7vg2rSnO16ZtIG0fMgt3py28Vwgq42/bVEuaQV7pg==} - - '@algolia/cache-browser-local-storage@4.14.2': - resolution: {integrity: sha512-FRweBkK/ywO+GKYfAWbrepewQsPTIEirhi1BdykX9mxvBPtGNKccYAxvGdDCumU1jL4r3cayio4psfzKMejBlA==} - - '@algolia/cache-common@4.14.2': - resolution: {integrity: sha512-SbvAlG9VqNanCErr44q6lEKD2qoK4XtFNx9Qn8FK26ePCI8I9yU7pYB+eM/cZdS9SzQCRJBbHUumVr4bsQ4uxg==} - - '@algolia/cache-in-memory@4.14.2': - resolution: {integrity: sha512-HrOukWoop9XB/VFojPv1R5SVXowgI56T9pmezd/djh2JnVN/vXswhXV51RKy4nCpqxyHt/aGFSq2qkDvj6KiuQ==} - - '@algolia/client-account@4.14.2': - resolution: {integrity: sha512-WHtriQqGyibbb/Rx71YY43T0cXqyelEU0lB2QMBRXvD2X0iyeGl4qMxocgEIcbHyK7uqE7hKgjT8aBrHqhgc1w==} - - '@algolia/client-analytics@4.14.2': - resolution: {integrity: sha512-yBvBv2mw+HX5a+aeR0dkvUbFZsiC4FKSnfqk9rrfX+QrlNOKEhCG0tJzjiOggRW4EcNqRmaTULIYvIzQVL2KYQ==} - - '@algolia/client-common@4.14.2': - resolution: {integrity: sha512-43o4fslNLcktgtDMVaT5XwlzsDPzlqvqesRi4MjQz2x4/Sxm7zYg5LRYFol1BIhG6EwxKvSUq8HcC/KxJu3J0Q==} - - '@algolia/client-personalization@4.14.2': - resolution: {integrity: sha512-ACCoLi0cL8CBZ1W/2juehSltrw2iqsQBnfiu/Rbl9W2yE6o2ZUb97+sqN/jBqYNQBS+o0ekTMKNkQjHHAcEXNw==} - - '@algolia/client-search@4.14.2': - resolution: {integrity: sha512-L5zScdOmcZ6NGiVbLKTvP02UbxZ0njd5Vq9nJAmPFtjffUSOGEp11BmD2oMJ5QvARgx2XbX4KzTTNS5ECYIMWw==} - - '@algolia/events@4.0.1': - resolution: {integrity: sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ==} - - '@algolia/logger-common@4.14.2': - resolution: {integrity: sha512-/JGlYvdV++IcMHBnVFsqEisTiOeEr6cUJtpjz8zc0A9c31JrtLm318Njc72p14Pnkw3A/5lHHh+QxpJ6WFTmsA==} - - '@algolia/logger-console@4.14.2': - resolution: {integrity: sha512-8S2PlpdshbkwlLCSAB5f8c91xyc84VM9Ar9EdfE9UmX+NrKNYnWR1maXXVDQQoto07G1Ol/tYFnFVhUZq0xV/g==} - - '@algolia/requester-browser-xhr@4.14.2': - resolution: {integrity: sha512-CEh//xYz/WfxHFh7pcMjQNWgpl4wFB85lUMRyVwaDPibNzQRVcV33YS+63fShFWc2+42YEipFGH2iPzlpszmDw==} - - '@algolia/requester-common@4.14.2': - resolution: {integrity: sha512-73YQsBOKa5fvVV3My7iZHu1sUqmjjfs9TteFWwPwDmnad7T0VTCopttcsM3OjLxZFtBnX61Xxl2T2gmG2O4ehg==} - - '@algolia/requester-node-http@4.14.2': - resolution: {integrity: sha512-oDbb02kd1o5GTEld4pETlPZLY0e+gOSWjWMJHWTgDXbv9rm/o2cF7japO6Vj1ENnrqWvLBmW1OzV9g6FUFhFXg==} - - '@algolia/transporter@4.14.2': - resolution: {integrity: sha512-t89dfQb2T9MFQHidjHcfhh6iGMNwvuKUvojAj+JsrHAGbuSy7yE4BylhLX6R0Q1xYRoC4Vvv+O5qIw/LdnQfsQ==} - - '@ampproject/remapping@2.2.0': - resolution: {integrity: sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==} - engines: {node: '>=6.0.0'} - - '@babel/code-frame@7.12.11': - resolution: {integrity: sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==} - - '@babel/code-frame@7.26.2': - resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} - engines: {node: '>=6.9.0'} - - '@babel/compat-data@7.19.4': - resolution: {integrity: sha512-CHIGpJcUQ5lU9KrPHTjBMhVwQG6CQjxfg36fGXl3qk/Gik1WwWachaXFuo0uCWJT/mStOKtcbFJCaVLihC1CMw==} - engines: {node: '>=6.9.0'} - - '@babel/core@7.12.9': - resolution: {integrity: sha512-gTXYh3M5wb7FRXQy+FErKFAv90BnlOuNn1QkCK2lREoPAjrQCO49+HVSrFoe5uakFAF5eenS75KbO2vQiLrTMQ==} - engines: {node: '>=6.9.0'} - - '@babel/core@7.19.6': - resolution: {integrity: sha512-D2Ue4KHpc6Ys2+AxpIx1BZ8+UegLLLE2p3KJEuJRKmokHOtl49jQ5ny1773KsGLZs8MQvBidAF6yWUJxRqtKtg==} - engines: {node: '>=6.9.0'} - - '@babel/generator@7.23.0': - resolution: {integrity: sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==} - engines: {node: '>=6.9.0'} - - '@babel/helper-annotate-as-pure@7.18.6': - resolution: {integrity: sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-builder-binary-assignment-operator-visitor@7.18.9': - resolution: {integrity: sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-compilation-targets@7.19.3': - resolution: {integrity: sha512-65ESqLGyGmLvgR0mst5AdW1FkNlj9rQsCKduzEoEPhBCDFGXvz2jW6bXFG6i0/MrV2s7hhXjjb2yAzcPuQlLwg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-create-class-features-plugin@7.19.0': - resolution: {integrity: sha512-NRz8DwF4jT3UfrmUoZjd0Uph9HQnP30t7Ash+weACcyNkiYTywpIjDBgReJMKgr+n86sn2nPVVmJ28Dm053Kqw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-create-regexp-features-plugin@7.19.0': - resolution: {integrity: sha512-htnV+mHX32DF81amCDrwIDr8nrp1PTm+3wfBN9/v8QJOLEioOCOG7qNyq0nHeFiWbT3Eb7gsPwEmV64UCQ1jzw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-define-polyfill-provider@0.3.3': - resolution: {integrity: sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==} - peerDependencies: - '@babel/core': ^7.4.0-0 - - '@babel/helper-environment-visitor@7.22.20': - resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-explode-assignable-expression@7.18.6': - resolution: {integrity: sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==} - engines: {node: '>=6.9.0'} - - '@babel/helper-function-name@7.23.0': - resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-hoist-variables@7.22.5': - resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-member-expression-to-functions@7.18.9': - resolution: {integrity: sha512-RxifAh2ZoVU67PyKIO4AMi1wTenGfMR/O/ae0CCRqwgBAt5v7xjdtRw7UoSbsreKrQn5t7r89eruK/9JjYHuDg==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-imports@7.18.6': - resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-transforms@7.19.6': - resolution: {integrity: sha512-fCmcfQo/KYr/VXXDIyd3CBGZ6AFhPFy1TfSEJ+PilGVlQT6jcbqtHAM4C1EciRqMza7/TpOUZliuSH+U6HAhJw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-optimise-call-expression@7.18.6': - resolution: {integrity: sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-plugin-utils@7.10.4': - resolution: {integrity: sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==} - - '@babel/helper-plugin-utils@7.20.2': - resolution: {integrity: sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==} - engines: {node: '>=6.9.0'} - - '@babel/helper-remap-async-to-generator@7.18.9': - resolution: {integrity: sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-replace-supers@7.19.1': - resolution: {integrity: sha512-T7ahH7wV0Hfs46SFh5Jz3s0B6+o8g3c+7TMxu7xKfmHikg7EAZ3I2Qk9LFhjxXq8sL7UkP5JflezNwoZa8WvWw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-simple-access@7.19.4': - resolution: {integrity: sha512-f9Xq6WqBFqaDfbCzn2w85hwklswz5qsKlh7f08w4Y9yhJHpnNC0QemtSkK5YyOY8kPGvyiwdzZksGUhnGdaUIg==} - engines: {node: '>=6.9.0'} - - '@babel/helper-skip-transparent-expression-wrappers@7.18.9': - resolution: {integrity: sha512-imytd2gHi3cJPsybLRbmFrF7u5BIEuI2cNheyKi3/iOBC63kNn3q8Crn2xVuESli0aM4KYsyEqKyS7lFL8YVtw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-split-export-declaration@7.22.6': - resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} - engines: {node: '>=6.9.0'} - - '@babel/helper-string-parser@7.25.9': - resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-identifier@7.25.9': - resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-option@7.18.6': - resolution: {integrity: sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-wrap-function@7.19.0': - resolution: {integrity: sha512-txX8aN8CZyYGTwcLhlk87KRqncAzhh5TpQamZUa0/u3an36NtDpUP6bQgBCBcLeBs09R/OwQu3OjK0k/HwfNDg==} - engines: {node: '>=6.9.0'} - - '@babel/helpers@7.27.0': - resolution: {integrity: sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==} - engines: {node: '>=6.9.0'} - - '@babel/highlight@7.22.20': - resolution: {integrity: sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==} - engines: {node: '>=6.9.0'} - - '@babel/parser@7.27.0': - resolution: {integrity: sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.18.6': - resolution: {integrity: sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.18.9': - resolution: {integrity: sha512-AHrP9jadvH7qlOj6PINbgSuphjQUAK7AOT7DPjBo9EHoLhQTnnK5u45e1Hd4DbSQEO9nqPWtQ89r+XEOWFScKg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.13.0 - - '@babel/plugin-proposal-async-generator-functions@7.19.1': - resolution: {integrity: sha512-0yu8vNATgLy4ivqMNBIwb1HebCelqN7YX8SL3FDXORv/RqT0zEEWUCH4GH44JsSrvCu6GqnAdR5EBFAPeNBB4Q==} - engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-async-generator-functions instead. - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-class-properties@7.18.6': - resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==} - engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead. - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-class-static-block@7.18.6': - resolution: {integrity: sha512-+I3oIiNxrCpup3Gi8n5IGMwj0gOCAjcJUSQEcotNnCCPMEnixawOQ+KeJPlgfjzx+FKQ1QSyZOWe7wmoJp7vhw==} - engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-static-block instead. - peerDependencies: - '@babel/core': ^7.12.0 - - '@babel/plugin-proposal-dynamic-import@7.18.6': - resolution: {integrity: sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==} - engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-dynamic-import instead. - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-export-namespace-from@7.18.9': - resolution: {integrity: sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==} - engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-export-namespace-from instead. - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-json-strings@7.18.6': - resolution: {integrity: sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==} - engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-json-strings instead. - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-logical-assignment-operators@7.18.9': - resolution: {integrity: sha512-128YbMpjCrP35IOExw2Fq+x55LMP42DzhOhX2aNNIdI9avSWl2PI0yuBWarr3RYpZBSPtabfadkH2yeRiMD61Q==} - engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-logical-assignment-operators instead. - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-nullish-coalescing-operator@7.18.6': - resolution: {integrity: sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==} - engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead. - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-numeric-separator@7.18.6': - resolution: {integrity: sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==} - engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-numeric-separator instead. - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-object-rest-spread@7.12.1': - resolution: {integrity: sha512-s6SowJIjzlhx8o7lsFx5zmY4At6CTtDvgNQDdPzkBQucle58A6b/TTeEBYtyDgmcXjUTM+vE8YOGHZzzbc/ioA==} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-object-rest-spread instead. - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-object-rest-spread@7.19.4': - resolution: {integrity: sha512-wHmj6LDxVDnL+3WhXteUBaoM1aVILZODAUjg11kHqG4cOlfgMQGxw6aCgvrXrmaJR3Bn14oZhImyCPZzRpC93Q==} - engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-object-rest-spread instead. - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-optional-catch-binding@7.18.6': - resolution: {integrity: sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==} - engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-catch-binding instead. - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-optional-chaining@7.18.9': - resolution: {integrity: sha512-v5nwt4IqBXihxGsW2QmCWMDS3B3bzGIk/EQVZz2ei7f3NJl8NzAJVvUmpDW5q1CRNY+Beb/k58UAH1Km1N411w==} - engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead. - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-private-methods@7.18.6': - resolution: {integrity: sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==} - engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead. - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-private-property-in-object@7.18.6': - resolution: {integrity: sha512-9Rysx7FOctvT5ouj5JODjAFAkgGoudQuLPamZb0v1TGLpapdNaftzifU8NTWQm0IRjqoYypdrSmyWgkocDQ8Dw==} - engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-property-in-object instead. - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-unicode-property-regex@7.18.6': - resolution: {integrity: sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==} - engines: {node: '>=4'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-unicode-property-regex instead. - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-async-generators@7.8.4': - resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-class-properties@7.12.13': - resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-class-static-block@7.14.5': - resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-dynamic-import@7.8.3': - resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-export-namespace-from@7.8.3': - resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-import-assertions@7.18.6': - resolution: {integrity: sha512-/DU3RXad9+bZwrgWJQKbr39gYbJpLJHezqEzRzi/BHRlJ9zsQb4CK2CA/5apllXNomwA1qHwzvHl+AdEmC5krQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-json-strings@7.8.3': - resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-jsx@7.12.1': - resolution: {integrity: sha512-1yRi7yAtB0ETgxdY9ti/p2TivUxJkTdhu/ZbF9MshVGqOx1TdB3b7xCXs49Fupgg50N45KcAsRP/ZqWjs9SRjg==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-jsx@7.18.6': - resolution: {integrity: sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-logical-assignment-operators@7.10.4': - resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': - resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-numeric-separator@7.10.4': - resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-object-rest-spread@7.8.3': - resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-optional-catch-binding@7.8.3': - resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-optional-chaining@7.8.3': - resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-private-property-in-object@7.14.5': - resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-top-level-await@7.14.5': - resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-typescript@7.18.6': - resolution: {integrity: sha512-mAWAuq4rvOepWCBid55JuRNvpTNf2UGVgoz4JV0fXEKolsVZDzsa4NqCef758WZJj/GDu0gVGItjKFiClTAmZA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-arrow-functions@7.18.6': - resolution: {integrity: sha512-9S9X9RUefzrsHZmKMbDXxweEH+YlE8JJEuat9FdvW9Qh1cw7W64jELCtWNkPBPX5En45uy28KGvA/AySqUh8CQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-async-to-generator@7.18.6': - resolution: {integrity: sha512-ARE5wZLKnTgPW7/1ftQmSi1CmkqqHo2DNmtztFhvgtOWSDfq0Cq9/9L+KnZNYSNrydBekhW3rwShduf59RoXag==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-block-scoped-functions@7.18.6': - resolution: {integrity: sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-block-scoping@7.19.4': - resolution: {integrity: sha512-934S2VLLlt2hRJwPf4MczaOr4hYF0z+VKPwqTNxyKX7NthTiPfhuKFWQZHXRM0vh/wo/VyXB3s4bZUNA08l+tQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-classes@7.19.0': - resolution: {integrity: sha512-YfeEE9kCjqTS9IitkgfJuxjcEtLUHMqa8yUJ6zdz8vR7hKuo6mOy2C05P0F1tdMmDCeuyidKnlrw/iTppHcr2A==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-computed-properties@7.18.9': - resolution: {integrity: sha512-+i0ZU1bCDymKakLxn5srGHrsAPRELC2WIbzwjLhHW9SIE1cPYkLCL0NlnXMZaM1vhfgA2+M7hySk42VBvrkBRw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-destructuring@7.19.4': - resolution: {integrity: sha512-t0j0Hgidqf0aM86dF8U+vXYReUgJnlv4bZLsyoPnwZNrGY+7/38o8YjaELrvHeVfTZao15kjR0PVv0nju2iduA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-dotall-regex@7.18.6': - resolution: {integrity: sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-duplicate-keys@7.18.9': - resolution: {integrity: sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-exponentiation-operator@7.18.6': - resolution: {integrity: sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-for-of@7.18.8': - resolution: {integrity: sha512-yEfTRnjuskWYo0k1mHUqrVWaZwrdq8AYbfrpqULOJOaucGSp4mNMVps+YtA8byoevxS/urwU75vyhQIxcCgiBQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-function-name@7.18.9': - resolution: {integrity: sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-literals@7.18.9': - resolution: {integrity: sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-member-expression-literals@7.18.6': - resolution: {integrity: sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-modules-amd@7.19.6': - resolution: {integrity: sha512-uG3od2mXvAtIFQIh0xrpLH6r5fpSQN04gIVovl+ODLdUMANokxQLZnPBHcjmv3GxRjnqwLuHvppjjcelqUFZvg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-modules-commonjs@7.19.6': - resolution: {integrity: sha512-8PIa1ym4XRTKuSsOUXqDG0YaOlEuTVvHMe5JCfgBMOtHvJKw/4NGovEGN33viISshG/rZNVrACiBmPQLvWN8xQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-modules-systemjs@7.19.6': - resolution: {integrity: sha512-fqGLBepcc3kErfR9R3DnVpURmckXP7gj7bAlrTQyBxrigFqszZCkFkcoxzCp2v32XmwXLvbw+8Yq9/b+QqksjQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-modules-umd@7.18.6': - resolution: {integrity: sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-named-capturing-groups-regex@7.19.1': - resolution: {integrity: sha512-oWk9l9WItWBQYS4FgXD4Uyy5kq898lvkXpXQxoJEY1RnvPk4R/Dvu2ebXU9q8lP+rlMwUQTFf2Ok6d78ODa0kw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/plugin-transform-new-target@7.18.6': - resolution: {integrity: sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-object-super@7.18.6': - resolution: {integrity: sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-parameters@7.18.8': - resolution: {integrity: sha512-ivfbE3X2Ss+Fj8nnXvKJS6sjRG4gzwPMsP+taZC+ZzEGjAYlvENixmt1sZ5Ca6tWls+BlKSGKPJ6OOXvXCbkFg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-property-literals@7.18.6': - resolution: {integrity: sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-react-constant-elements@7.20.2': - resolution: {integrity: sha512-KS/G8YI8uwMGKErLFOHS/ekhqdHhpEloxs43NecQHVgo2QuQSyJhGIY1fL8UGl9wy5ItVwwoUL4YxVqsplGq2g==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-react-display-name@7.18.6': - resolution: {integrity: sha512-TV4sQ+T013n61uMoygyMRm+xf04Bd5oqFpv2jAEQwSZ8NwQA7zeRPg1LMVg2PWi3zWBz+CLKD+v5bcpZ/BS0aA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-react-jsx-development@7.18.6': - resolution: {integrity: sha512-SA6HEjwYFKF7WDjWcMcMGUimmw/nhNRDWxr+KaLSCrkD/LMDBvWRmHAYgE1HDeF8KUuI8OAu+RT6EOtKxSW2qA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-react-jsx@7.19.0': - resolution: {integrity: sha512-UVEvX3tXie3Szm3emi1+G63jyw1w5IcMY0FSKM+CRnKRI5Mr1YbCNgsSTwoTwKphQEG9P+QqmuRFneJPZuHNhg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-react-pure-annotations@7.18.6': - resolution: {integrity: sha512-I8VfEPg9r2TRDdvnHgPepTKvuRomzA8+u+nhY7qSI1fR2hRNebasZEETLyM5mAUr0Ku56OkXJ0I7NHJnO6cJiQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-regenerator@7.18.6': - resolution: {integrity: sha512-poqRI2+qiSdeldcz4wTSTXBRryoq3Gc70ye7m7UD5Ww0nE29IXqMl6r7Nd15WBgRd74vloEMlShtH6CKxVzfmQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-reserved-words@7.18.6': - resolution: {integrity: sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-runtime@7.19.6': - resolution: {integrity: sha512-PRH37lz4JU156lYFW1p8OxE5i7d6Sl/zV58ooyr+q1J1lnQPyg5tIiXlIwNVhJaY4W3TmOtdc8jqdXQcB1v5Yw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-shorthand-properties@7.18.6': - resolution: {integrity: sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-spread@7.19.0': - resolution: {integrity: sha512-RsuMk7j6n+r752EtzyScnWkQyuJdli6LdO5Klv8Yx0OfPVTcQkIUfS8clx5e9yHXzlnhOZF3CbQ8C2uP5j074w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-sticky-regex@7.18.6': - resolution: {integrity: sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-template-literals@7.18.9': - resolution: {integrity: sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-typeof-symbol@7.18.9': - resolution: {integrity: sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-typescript@7.19.3': - resolution: {integrity: sha512-z6fnuK9ve9u/0X0rRvI9MY0xg+DOUaABDYOe+/SQTxtlptaBB/V9JIUxJn6xp3lMBeb9qe8xSFmHU35oZDXD+w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-unicode-escapes@7.18.10': - resolution: {integrity: sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-unicode-regex@7.18.6': - resolution: {integrity: sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/preset-env@7.19.4': - resolution: {integrity: sha512-5QVOTXUdqTCjQuh2GGtdd7YEhoRXBMVGROAtsBeLGIbIz3obCBIfRMT1I3ZKkMgNzwkyCkftDXSSkHxnfVf4qg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/preset-modules@0.1.5': - resolution: {integrity: sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/preset-react@7.18.6': - resolution: {integrity: sha512-zXr6atUmyYdiWRVLOZahakYmOBHtWc2WGCkP8PYTgZi0iJXDY2CN180TdrIW4OGOAdLc7TifzDIvtx6izaRIzg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/preset-typescript@7.18.6': - resolution: {integrity: sha512-s9ik86kXBAnD760aybBucdpnLsAt0jK1xqJn2juOn9lkOvSHV60os5hxoVJsPzMQxvnUJFAlkont2DvvaYEBtQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/runtime-corejs3@7.27.0': - resolution: {integrity: sha512-UWjX6t+v+0ckwZ50Y5ShZLnlk95pP5MyW/pon9tiYzl3+18pkTHTFNTKr7rQbfRXPkowt2QAn30o1b6oswszew==} - engines: {node: '>=6.9.0'} - - '@babel/runtime@7.27.0': - resolution: {integrity: sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==} - engines: {node: '>=6.9.0'} - - '@babel/template@7.27.0': - resolution: {integrity: sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==} - engines: {node: '>=6.9.0'} - - '@babel/traverse@7.23.2': - resolution: {integrity: sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==} - engines: {node: '>=6.9.0'} - - '@babel/types@7.27.0': - resolution: {integrity: sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==} - engines: {node: '>=6.9.0'} - - '@colors/colors@1.5.0': - resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} - engines: {node: '>=0.1.90'} - - '@docsearch/css@3.2.2': - resolution: {integrity: sha512-VB0Evx4ikS1ZlW1YVUw+vI9b3H/UXMCo4W/ZWy+n56Sho4KOqyCHcINVays91TJt7HTV/CKO3FCbm2VJg5Wipw==} - - '@docsearch/react@3.2.2': - resolution: {integrity: sha512-1Hn2SNQUFVPrzqvaj+vxXZfsfn3rnW8CoyGAJ1LqXMY9py8GbxK8VfmJ5Z6z4LwG9849tGru/N6dp0cQO6r9Ag==} - peerDependencies: - '@types/react': '>= 16.8.0 < 19.0.0' - react: '>= 16.8.0 < 19.0.0' - react-dom: '>= 16.8.0 < 19.0.0' - peerDependenciesMeta: - '@types/react': - optional: true - react: - optional: true - react-dom: - optional: true - - '@docusaurus/core@2.4.3': - resolution: {integrity: sha512-dWH5P7cgeNSIg9ufReX6gaCl/TmrGKD38Orbwuz05WPhAQtFXHd5B8Qym1TiXfvUNvwoYKkAJOJuGe8ou0Z7PA==} - engines: {node: '>=16.14'} - hasBin: true - peerDependencies: - react: ^16.8.4 || ^17.0.0 - react-dom: ^16.8.4 || ^17.0.0 - - '@docusaurus/cssnano-preset@2.4.3': - resolution: {integrity: sha512-ZvGSRCi7z9wLnZrXNPG6DmVPHdKGd8dIn9pYbEOFiYihfv4uDR3UtxogmKf+rT8ZlKFf5Lqne8E8nt08zNM8CA==} - engines: {node: '>=16.14'} - - '@docusaurus/logger@2.4.3': - resolution: {integrity: sha512-Zxws7r3yLufk9xM1zq9ged0YHs65mlRmtsobnFkdZTxWXdTYlWWLWdKyNKAsVC+D7zg+pv2fGbyabdOnyZOM3w==} - engines: {node: '>=16.14'} - - '@docusaurus/mdx-loader@2.4.3': - resolution: {integrity: sha512-b1+fDnWtl3GiqkL0BRjYtc94FZrcDDBV1j8446+4tptB9BAOlePwG2p/pK6vGvfL53lkOsszXMghr2g67M0vCw==} - engines: {node: '>=16.14'} - peerDependencies: - react: ^16.8.4 || ^17.0.0 - react-dom: ^16.8.4 || ^17.0.0 - - '@docusaurus/module-type-aliases@2.4.3': - resolution: {integrity: sha512-cwkBkt1UCiduuvEAo7XZY01dJfRn7UR/75mBgOdb1hKknhrabJZ8YH+7savd/y9kLExPyrhe0QwdS9GuzsRRIA==} - peerDependencies: - react: '*' - react-dom: '*' - - '@docusaurus/plugin-content-blog@2.4.3': - resolution: {integrity: sha512-PVhypqaA0t98zVDpOeTqWUTvRqCEjJubtfFUQ7zJNYdbYTbS/E/ytq6zbLVsN/dImvemtO/5JQgjLxsh8XLo8Q==} - engines: {node: '>=16.14'} - peerDependencies: - react: ^16.8.4 || ^17.0.0 - react-dom: ^16.8.4 || ^17.0.0 - - '@docusaurus/plugin-content-docs@2.4.3': - resolution: {integrity: sha512-N7Po2LSH6UejQhzTCsvuX5NOzlC+HiXOVvofnEPj0WhMu1etpLEXE6a4aTxrtg95lQ5kf0xUIdjX9sh3d3G76A==} - engines: {node: '>=16.14'} - peerDependencies: - react: ^16.8.4 || ^17.0.0 - react-dom: ^16.8.4 || ^17.0.0 - - '@docusaurus/plugin-content-pages@2.4.3': - resolution: {integrity: sha512-txtDVz7y3zGk67q0HjG0gRttVPodkHqE0bpJ+7dOaTH40CQFLSh7+aBeGnPOTl+oCPG+hxkim4SndqPqXjQ8Bg==} - engines: {node: '>=16.14'} - peerDependencies: - react: ^16.8.4 || ^17.0.0 - react-dom: ^16.8.4 || ^17.0.0 - - '@docusaurus/plugin-debug@2.4.3': - resolution: {integrity: sha512-LkUbuq3zCmINlFb+gAd4ZvYr+bPAzMC0hwND4F7V9bZ852dCX8YoWyovVUBKq4er1XsOwSQaHmNGtObtn8Av8Q==} - engines: {node: '>=16.14'} - peerDependencies: - react: ^16.8.4 || ^17.0.0 - react-dom: ^16.8.4 || ^17.0.0 - - '@docusaurus/plugin-google-analytics@2.4.3': - resolution: {integrity: sha512-KzBV3k8lDkWOhg/oYGxlK5o9bOwX7KpPc/FTWoB+SfKhlHfhq7qcQdMi1elAaVEIop8tgK6gD1E58Q+XC6otSQ==} - engines: {node: '>=16.14'} - peerDependencies: - react: ^16.8.4 || ^17.0.0 - react-dom: ^16.8.4 || ^17.0.0 - - '@docusaurus/plugin-google-gtag@2.4.3': - resolution: {integrity: sha512-5FMg0rT7sDy4i9AGsvJC71MQrqQZwgLNdDetLEGDHLfSHLvJhQbTCUGbGXknUgWXQJckcV/AILYeJy+HhxeIFA==} - engines: {node: '>=16.14'} - peerDependencies: - react: ^16.8.4 || ^17.0.0 - react-dom: ^16.8.4 || ^17.0.0 - - '@docusaurus/plugin-google-tag-manager@2.4.3': - resolution: {integrity: sha512-1jTzp71yDGuQiX9Bi0pVp3alArV0LSnHXempvQTxwCGAEzUWWaBg4d8pocAlTpbP9aULQQqhgzrs8hgTRPOM0A==} - engines: {node: '>=16.14'} - peerDependencies: - react: ^16.8.4 || ^17.0.0 - react-dom: ^16.8.4 || ^17.0.0 - - '@docusaurus/plugin-sitemap@2.4.3': - resolution: {integrity: sha512-LRQYrK1oH1rNfr4YvWBmRzTL0LN9UAPxBbghgeFRBm5yloF6P+zv1tm2pe2hQTX/QP5bSKdnajCvfnScgKXMZQ==} - engines: {node: '>=16.14'} - peerDependencies: - react: ^16.8.4 || ^17.0.0 - react-dom: ^16.8.4 || ^17.0.0 - - '@docusaurus/preset-classic@2.4.3': - resolution: {integrity: sha512-tRyMliepY11Ym6hB1rAFSNGwQDpmszvWYJvlK1E+md4SW8i6ylNHtpZjaYFff9Mdk3i/Pg8ItQq9P0daOJAvQw==} - engines: {node: '>=16.14'} - peerDependencies: - react: ^16.8.4 || ^17.0.0 - react-dom: ^16.8.4 || ^17.0.0 - - '@docusaurus/react-loadable@5.5.2': - resolution: {integrity: sha512-A3dYjdBGuy0IGT+wyLIGIKLRE+sAk1iNk0f1HjNDysO7u8lhL4N3VEm+FAubmJbAztn94F7MxBTPmnixbiyFdQ==} - peerDependencies: - react: '*' - - '@docusaurus/theme-classic@2.4.3': - resolution: {integrity: sha512-QKRAJPSGPfDY2yCiPMIVyr+MqwZCIV2lxNzqbyUW0YkrlmdzzP3WuQJPMGLCjWgQp/5c9kpWMvMxjhpZx1R32Q==} - engines: {node: '>=16.14'} - peerDependencies: - react: ^16.8.4 || ^17.0.0 - react-dom: ^16.8.4 || ^17.0.0 - - '@docusaurus/theme-common@2.4.3': - resolution: {integrity: sha512-7KaDJBXKBVGXw5WOVt84FtN8czGWhM0lbyWEZXGp8AFfL6sZQfRTluFp4QriR97qwzSyOfQb+nzcDZZU4tezUw==} - engines: {node: '>=16.14'} - peerDependencies: - react: ^16.8.4 || ^17.0.0 - react-dom: ^16.8.4 || ^17.0.0 - - '@docusaurus/theme-search-algolia@2.4.3': - resolution: {integrity: sha512-jziq4f6YVUB5hZOB85ELATwnxBz/RmSLD3ksGQOLDPKVzat4pmI8tddNWtriPpxR04BNT+ZfpPUMFkNFetSW1Q==} - engines: {node: '>=16.14'} - peerDependencies: - react: ^16.8.4 || ^17.0.0 - react-dom: ^16.8.4 || ^17.0.0 - - '@docusaurus/theme-translations@2.4.3': - resolution: {integrity: sha512-H4D+lbZbjbKNS/Zw1Lel64PioUAIT3cLYYJLUf3KkuO/oc9e0QCVhIYVtUI2SfBCF2NNdlyhBDQEEMygsCedIg==} - engines: {node: '>=16.14'} - - '@docusaurus/types@2.4.3': - resolution: {integrity: sha512-W6zNLGQqfrp/EoPD0bhb9n7OobP+RHpmvVzpA+Z/IuU3Q63njJM24hmT0GYboovWcDtFmnIJC9wcyx4RVPQscw==} - peerDependencies: - react: ^16.8.4 || ^17.0.0 - react-dom: ^16.8.4 || ^17.0.0 - - '@docusaurus/utils-common@2.4.3': - resolution: {integrity: sha512-/jascp4GbLQCPVmcGkPzEQjNaAk3ADVfMtudk49Ggb+131B1WDD6HqlSmDf8MxGdy7Dja2gc+StHf01kiWoTDQ==} - engines: {node: '>=16.14'} - peerDependencies: - '@docusaurus/types': '*' - peerDependenciesMeta: - '@docusaurus/types': - optional: true - - '@docusaurus/utils-validation@2.4.3': - resolution: {integrity: sha512-G2+Vt3WR5E/9drAobP+hhZQMaswRwDlp6qOMi7o7ZypB+VO7N//DZWhZEwhcRGepMDJGQEwtPv7UxtYwPL9PBw==} - engines: {node: '>=16.14'} - - '@docusaurus/utils@2.4.3': - resolution: {integrity: sha512-fKcXsjrD86Smxv8Pt0TBFqYieZZCPh4cbf9oszUq/AMhZn3ujwpKaVYZACPX8mmjtYx0JOgNx52CREBfiGQB4A==} - engines: {node: '>=16.14'} - peerDependencies: - '@docusaurus/types': '*' - peerDependenciesMeta: - '@docusaurus/types': - optional: true - - '@eslint/eslintrc@0.4.3': - resolution: {integrity: sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==} - engines: {node: ^10.12.0 || >=12.0.0} - - '@hapi/hoek@9.3.0': - resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} - - '@hapi/topo@5.1.0': - resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==} - - '@humanwhocodes/config-array@0.5.0': - resolution: {integrity: sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==} - engines: {node: '>=10.10.0'} - deprecated: Use @eslint/config-array instead - - '@humanwhocodes/object-schema@1.2.1': - resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} - deprecated: Use @eslint/object-schema instead - - '@jest/schemas@29.0.0': - resolution: {integrity: sha512-3Ab5HgYIIAnS0HjqJHQYZS+zXc4tUmTmBH3z83ajI6afXp8X3ZtdLX+nXx+I7LNkJD7uN9LAVhgnjDgZa2z0kA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/types@29.3.1': - resolution: {integrity: sha512-d0S0jmmTpjnhCmNpApgX3jrUZgZ22ivKJRvL2lli5hpCRoNnp1f85r2/wpKfXuYu8E7Jjh1hGfhPyup1NM5AmA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jridgewell/gen-mapping@0.1.1': - resolution: {integrity: sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==} - engines: {node: '>=6.0.0'} - - '@jridgewell/gen-mapping@0.3.8': - resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} - engines: {node: '>=6.0.0'} - - '@jridgewell/resolve-uri@3.1.0': - resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} - engines: {node: '>=6.0.0'} - - '@jridgewell/set-array@1.2.1': - resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} - engines: {node: '>=6.0.0'} - - '@jridgewell/source-map@0.3.6': - resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} - - '@jridgewell/sourcemap-codec@1.4.14': - resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} - - '@jridgewell/trace-mapping@0.3.25': - resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} - - '@leichtgewicht/ip-codec@2.0.4': - resolution: {integrity: sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==} - - '@mdx-js/mdx@1.6.22': - resolution: {integrity: sha512-AMxuLxPz2j5/6TpF/XSdKpQP1NlG0z11dFOlq+2IP/lSgl11GY8ji6S/rgsViN/L0BDvHvUMruRb7ub+24LUYA==} - - '@mdx-js/react@1.6.22': - resolution: {integrity: sha512-TDoPum4SHdfPiGSAaRBw7ECyI8VaHpK8GJugbJIJuqyh6kzw9ZLJZW3HGL3NNrJGxcAixUvqROm+YuQOo5eXtg==} - peerDependencies: - react: ^16.13.1 || ^17.0.0 - - '@mdx-js/util@1.6.22': - resolution: {integrity: sha512-H1rQc1ZOHANWBvPcW+JpGwr+juXSxM8Q8YCkm3GhZd8REu1fHR3z99CErO1p9pkcfcxZnMdIZdIsXkOHY0NilA==} - - '@nodelib/fs.scandir@2.1.5': - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - - '@nodelib/fs.stat@2.0.5': - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - - '@nodelib/fs.walk@1.2.8': - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - - '@polka/url@1.0.0-next.21': - resolution: {integrity: sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==} - - '@sideway/address@4.1.5': - resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==} - - '@sideway/formula@3.0.1': - resolution: {integrity: sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==} - - '@sideway/pinpoint@2.0.0': - resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==} - - '@sinclair/typebox@0.24.51': - resolution: {integrity: sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==} - - '@sindresorhus/is@4.6.0': - resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} - engines: {node: '>=10'} - - '@slorber/static-site-generator-webpack-plugin@4.0.7': - resolution: {integrity: sha512-Ug7x6z5lwrz0WqdnNFOMYrDQNTPAprvHLSh6+/fmml3qUiz6l5eq+2MzLKWtn/q5K5NpSiFsZTP/fck/3vjSxA==} - engines: {node: '>=14'} - - '@svgr/babel-plugin-add-jsx-attribute@6.5.1': - resolution: {integrity: sha512-9PYGcXrAxitycIjRmZB+Q0JaN07GZIWaTBIGQzfaZv+qr1n8X1XUEJ5rZ/vx6OVD9RRYlrNnXWExQXcmZeD/BQ==} - engines: {node: '>=10'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@svgr/babel-plugin-remove-jsx-attribute@6.5.0': - resolution: {integrity: sha512-8zYdkym7qNyfXpWvu4yq46k41pyNM9SOstoWhKlm+IfdCE1DdnRKeMUPsWIEO/DEkaWxJ8T9esNdG3QwQ93jBA==} - engines: {node: '>=10'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@svgr/babel-plugin-remove-jsx-empty-expression@6.5.0': - resolution: {integrity: sha512-NFdxMq3xA42Kb1UbzCVxplUc0iqSyM9X8kopImvFnB+uSDdzIHOdbs1op8ofAvVRtbg4oZiyRl3fTYeKcOe9Iw==} - engines: {node: '>=10'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@svgr/babel-plugin-replace-jsx-attribute-value@6.5.1': - resolution: {integrity: sha512-8DPaVVE3fd5JKuIC29dqyMB54sA6mfgki2H2+swh+zNJoynC8pMPzOkidqHOSc6Wj032fhl8Z0TVn1GiPpAiJg==} - engines: {node: '>=10'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@svgr/babel-plugin-svg-dynamic-title@6.5.1': - resolution: {integrity: sha512-FwOEi0Il72iAzlkaHrlemVurgSQRDFbk0OC8dSvD5fSBPHltNh7JtLsxmZUhjYBZo2PpcU/RJvvi6Q0l7O7ogw==} - engines: {node: '>=10'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@svgr/babel-plugin-svg-em-dimensions@6.5.1': - resolution: {integrity: sha512-gWGsiwjb4tw+ITOJ86ndY/DZZ6cuXMNE/SjcDRg+HLuCmwpcjOktwRF9WgAiycTqJD/QXqL2f8IzE2Rzh7aVXA==} - engines: {node: '>=10'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@svgr/babel-plugin-transform-react-native-svg@6.5.1': - resolution: {integrity: sha512-2jT3nTayyYP7kI6aGutkyfJ7UMGtuguD72OjeGLwVNyfPRBD8zQthlvL+fAbAKk5n9ZNcvFkp/b1lZ7VsYqVJg==} - engines: {node: '>=10'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@svgr/babel-plugin-transform-svg-component@6.5.1': - resolution: {integrity: sha512-a1p6LF5Jt33O3rZoVRBqdxL350oge54iZWHNI6LJB5tQ7EelvD/Mb1mfBiZNAan0dt4i3VArkFRjA4iObuNykQ==} - engines: {node: '>=12'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@svgr/babel-preset@6.5.1': - resolution: {integrity: sha512-6127fvO/FF2oi5EzSQOAjo1LE3OtNVh11R+/8FXa+mHx1ptAaS4cknIjnUA7e6j6fwGGJ17NzaTJFUwOV2zwCw==} - engines: {node: '>=10'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@svgr/core@6.5.1': - resolution: {integrity: sha512-/xdLSWxK5QkqG524ONSjvg3V/FkNyCv538OIBdQqPNaAta3AsXj/Bd2FbvR87yMbXO2hFSWiAe/Q6IkVPDw+mw==} - engines: {node: '>=10'} - - '@svgr/hast-util-to-babel-ast@6.5.1': - resolution: {integrity: sha512-1hnUxxjd83EAxbL4a0JDJoD3Dao3hmjvyvyEV8PzWmLK3B9m9NPlW7GKjFyoWE8nM7HnXzPcmmSyOW8yOddSXw==} - engines: {node: '>=10'} - - '@svgr/plugin-jsx@6.5.1': - resolution: {integrity: sha512-+UdQxI3jgtSjCykNSlEMuy1jSRQlGC7pqBCPvkG/2dATdWo082zHTTK3uhnAju2/6XpE6B5mZ3z4Z8Ns01S8Gw==} - engines: {node: '>=10'} - peerDependencies: - '@svgr/core': ^6.0.0 - - '@svgr/plugin-svgo@6.5.1': - resolution: {integrity: sha512-omvZKf8ixP9z6GWgwbtmP9qQMPX4ODXi+wzbVZgomNFsUIlHA1sf4fThdwTWSsZGgvGAG6yE+b/F5gWUkcZ/iQ==} - engines: {node: '>=10'} - peerDependencies: - '@svgr/core': '*' - - '@svgr/webpack@6.5.1': - resolution: {integrity: sha512-cQ/AsnBkXPkEK8cLbv4Dm7JGXq2XrumKnL1dRpJD9rIO2fTIlJI9a1uCciYG1F2aUsox/hJQyNGbt3soDxSRkA==} - engines: {node: '>=10'} - - '@szmarczak/http-timer@4.0.6': - resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} - engines: {node: '>=10'} - - '@trysound/sax@0.2.0': - resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} - engines: {node: '>=10.13.0'} - - '@types/body-parser@1.19.2': - resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==} - - '@types/bonjour@3.5.10': - resolution: {integrity: sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==} - - '@types/cacheable-request@6.0.3': - resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} - - '@types/connect-history-api-fallback@1.3.5': - resolution: {integrity: sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw==} - - '@types/connect@3.4.35': - resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} - - '@types/eslint-scope@3.7.7': - resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} - - '@types/eslint@9.6.1': - resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} - - '@types/estree@1.0.8': - resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - - '@types/express-serve-static-core@4.17.32': - resolution: {integrity: sha512-aI5h/VOkxOF2Z1saPy0Zsxs5avets/iaiAJYznQFm5By/pamU31xWKL//epiF4OfUA2qTOc9PV6tCUjhO8wlZA==} - - '@types/express@4.17.15': - resolution: {integrity: sha512-Yv0k4bXGOH+8a+7bELd2PqHQsuiANB+A8a4gnQrkRWzrkKlb6KHaVvyXhqs04sVW/OWlbPyYxRgYlIXLfrufMQ==} - - '@types/hast@2.3.4': - resolution: {integrity: sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==} - - '@types/history@4.7.11': - resolution: {integrity: sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==} - - '@types/html-minifier-terser@6.1.0': - resolution: {integrity: sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==} - - '@types/http-cache-semantics@4.0.4': - resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} - - '@types/http-proxy@1.17.9': - resolution: {integrity: sha512-QsbSjA/fSk7xB+UXlCT3wHBy5ai9wOcNDWwZAtud+jXhwOM3l+EYZh8Lng4+/6n8uar0J7xILzqftJdJ/Wdfkw==} - - '@types/istanbul-lib-coverage@2.0.4': - resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==} - - '@types/istanbul-lib-report@3.0.0': - resolution: {integrity: sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==} - - '@types/istanbul-reports@3.0.1': - resolution: {integrity: sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==} - - '@types/json-schema@7.0.15': - resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - - '@types/katex@0.11.1': - resolution: {integrity: sha512-DUlIj2nk0YnJdlWgsFuVKcX27MLW0KbKmGVoUHmFr+74FYYNUDAaj9ZqTADvsbE8rfxuVmSFc7KczYn5Y09ozg==} - - '@types/keyv@3.1.4': - resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} - - '@types/mdast@3.0.10': - resolution: {integrity: sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA==} - - '@types/mime@3.0.1': - resolution: {integrity: sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==} - - '@types/node@17.0.45': - resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==} - - '@types/node@18.11.3': - resolution: {integrity: sha512-fNjDQzzOsZeKZu5NATgXUPsaFaTxeRgFXoosrHivTl8RGeV733OLawXsGfEk9a8/tySyZUyiZ6E8LcjPFZ2y1A==} - - '@types/parse-json@4.0.0': - resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==} - - '@types/parse5@5.0.3': - resolution: {integrity: sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==} - - '@types/prop-types@15.7.5': - resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} - - '@types/qs@6.9.7': - resolution: {integrity: sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==} - - '@types/range-parser@1.2.4': - resolution: {integrity: sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==} - - '@types/react-router-config@5.0.11': - resolution: {integrity: sha512-WmSAg7WgqW7m4x8Mt4N6ZyKz0BubSj/2tVUMsAHp+Yd2AMwcSbeFq9WympT19p5heCFmF97R9eD5uUR/t4HEqw==} - - '@types/react-router-dom@5.3.3': - resolution: {integrity: sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==} - - '@types/react-router@5.1.20': - resolution: {integrity: sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==} - - '@types/react@18.0.26': - resolution: {integrity: sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug==} - - '@types/responselike@1.0.3': - resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} - - '@types/retry@0.12.0': - resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} - - '@types/sax@1.2.7': - resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==} - - '@types/scheduler@0.16.2': - resolution: {integrity: sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==} - - '@types/serve-index@1.9.1': - resolution: {integrity: sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg==} - - '@types/serve-static@1.15.0': - resolution: {integrity: sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==} - - '@types/sockjs@0.3.33': - resolution: {integrity: sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw==} - - '@types/unist@2.0.6': - resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==} - - '@types/ws@8.18.0': - resolution: {integrity: sha512-8svvI3hMyvN0kKCJMvTJP/x6Y/EoQbepff882wL+Sn5QsXb3etnamgrJq4isrBxSJj5L2AuXcI0+bgkoAXGUJw==} - - '@types/yargs-parser@21.0.0': - resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==} - - '@types/yargs@17.0.19': - resolution: {integrity: sha512-cAx3qamwaYX9R0fzOIZAlFpo4A+1uBVCxqpKz9D26uTF4srRXaGTTsikQmaotCtNdbhzyUH7ft6p9ktz9s6UNQ==} - - '@webassemblyjs/ast@1.14.1': - resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} - - '@webassemblyjs/floating-point-hex-parser@1.13.2': - resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==} - - '@webassemblyjs/helper-api-error@1.13.2': - resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==} - - '@webassemblyjs/helper-buffer@1.14.1': - resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==} - - '@webassemblyjs/helper-numbers@1.13.2': - resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==} - - '@webassemblyjs/helper-wasm-bytecode@1.13.2': - resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==} - - '@webassemblyjs/helper-wasm-section@1.14.1': - resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==} - - '@webassemblyjs/ieee754@1.13.2': - resolution: {integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==} - - '@webassemblyjs/leb128@1.13.2': - resolution: {integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==} - - '@webassemblyjs/utf8@1.13.2': - resolution: {integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==} - - '@webassemblyjs/wasm-edit@1.14.1': - resolution: {integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==} - - '@webassemblyjs/wasm-gen@1.14.1': - resolution: {integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==} - - '@webassemblyjs/wasm-opt@1.14.1': - resolution: {integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==} - - '@webassemblyjs/wasm-parser@1.14.1': - resolution: {integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==} - - '@webassemblyjs/wast-printer@1.14.1': - resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} - - '@xtuc/ieee754@1.2.0': - resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} - - '@xtuc/long@4.2.2': - resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} - - accepts@1.3.8: - resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} - engines: {node: '>= 0.6'} - - acorn-import-phases@1.0.4: - resolution: {integrity: sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==} - engines: {node: '>=10.13.0'} - peerDependencies: - acorn: ^8.14.0 - - acorn-jsx@5.3.2: - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - - acorn-walk@8.2.0: - resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} - engines: {node: '>=0.4.0'} - - acorn@7.4.1: - resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} - engines: {node: '>=0.4.0'} - hasBin: true - - acorn@8.15.0: - resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} - engines: {node: '>=0.4.0'} - hasBin: true - - address@1.2.1: - resolution: {integrity: sha512-B+6bi5D34+fDYENiH5qOlA0cV2rAGKuWZ9LeyUUehbXy8e0VS9e498yO0Jeeh+iM+6KbfudHTFjXw2MmJD4QRA==} - engines: {node: '>= 10.0.0'} - - aggregate-error@3.1.0: - resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} - engines: {node: '>=8'} - - ajv-formats@2.1.1: - resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} - peerDependencies: - ajv: ^8.0.0 - peerDependenciesMeta: - ajv: - optional: true - - ajv-keywords@3.5.2: - resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} - peerDependencies: - ajv: ^6.9.1 - - ajv-keywords@5.1.0: - resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} - peerDependencies: - ajv: ^8.8.2 - - ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - - ajv@8.17.1: - resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} - - algoliasearch-helper@3.11.1: - resolution: {integrity: sha512-mvsPN3eK4E0bZG0/WlWJjeqe/bUD2KOEVOl0GyL/TGXn6wcpZU8NOuztGHCUKXkyg5gq6YzUakVTmnmSSO5Yiw==} - peerDependencies: - algoliasearch: '>= 3.1 < 6' - - algoliasearch@4.14.2: - resolution: {integrity: sha512-ngbEQonGEmf8dyEh5f+uOIihv4176dgbuOZspiuhmTTBRBuzWu3KCGHre6uHj5YyuC7pNvQGzB6ZNJyZi0z+Sg==} - - ansi-align@3.0.1: - resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} - - ansi-colors@4.1.3: - resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} - engines: {node: '>=6'} - - ansi-html-community@0.0.8: - resolution: {integrity: sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==} - engines: {'0': node >= 0.8.0} - hasBin: true - - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - - ansi-regex@6.0.1: - resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} - engines: {node: '>=12'} - - ansi-styles@3.2.1: - resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} - engines: {node: '>=4'} - - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - - ansi-styles@6.2.1: - resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} - engines: {node: '>=12'} - - anymatch@3.1.2: - resolution: {integrity: sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==} - engines: {node: '>= 8'} - - arg@5.0.2: - resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} - - argparse@1.0.10: - resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} - - argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - - array-buffer-byte-length@1.0.2: - resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} - engines: {node: '>= 0.4'} - - array-flatten@1.1.1: - resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} - - array-flatten@2.1.2: - resolution: {integrity: sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==} - - array-includes@3.1.8: - resolution: {integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==} - engines: {node: '>= 0.4'} - - array-union@2.1.0: - resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} - engines: {node: '>=8'} - - array.prototype.findlast@1.2.5: - resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} - engines: {node: '>= 0.4'} - - array.prototype.flatmap@1.3.3: - resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} - engines: {node: '>= 0.4'} - - array.prototype.tosorted@1.1.4: - resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} - engines: {node: '>= 0.4'} - - arraybuffer.prototype.slice@1.0.4: - resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} - engines: {node: '>= 0.4'} - - asap@2.0.6: - resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} - - astral-regex@2.0.0: - resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} - engines: {node: '>=8'} - - async-function@1.0.0: - resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} - engines: {node: '>= 0.4'} - - asynckit@0.4.0: - resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - - at-least-node@1.0.0: - resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} - engines: {node: '>= 4.0.0'} - - autoprefixer@10.4.12: - resolution: {integrity: sha512-WrCGV9/b97Pa+jtwf5UGaRjgQIg7OK3D06GnoYoZNcG1Xb8Gt3EfuKjlhh9i/VtT16g6PYjZ69jdJ2g8FxSC4Q==} - engines: {node: ^10 || ^12 || >=14} - hasBin: true - peerDependencies: - postcss: ^8.1.0 - - available-typed-arrays@1.0.7: - resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} - engines: {node: '>= 0.4'} - - axios@1.13.5: - resolution: {integrity: sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==} - - babel-eslint@10.1.0: - resolution: {integrity: sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==} - engines: {node: '>=6'} - deprecated: babel-eslint is now @babel/eslint-parser. This package will no longer receive updates. - peerDependencies: - eslint: '>= 4.12.1' - - babel-loader@8.2.5: - resolution: {integrity: sha512-OSiFfH89LrEMiWd4pLNqGz4CwJDtbs2ZVc+iGu2HrkRfPxId9F2anQj38IxWpmRfsUY0aBZYi1EFcd3mhtRMLQ==} - engines: {node: '>= 8.9'} - peerDependencies: - '@babel/core': ^7.0.0 - webpack: '>=2' - - babel-plugin-apply-mdx-type-prop@1.6.22: - resolution: {integrity: sha512-VefL+8o+F/DfK24lPZMtJctrCVOfgbqLAGZSkxwhazQv4VxPg3Za/i40fu22KR2m8eEda+IfSOlPLUSIiLcnCQ==} - peerDependencies: - '@babel/core': ^7.11.6 - - babel-plugin-dynamic-import-node@2.3.3: - resolution: {integrity: sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==} - - babel-plugin-extract-import-names@1.6.22: - resolution: {integrity: sha512-yJ9BsJaISua7d8zNT7oRG1ZLBJCIdZ4PZqmH8qa9N5AK01ifk3fnkc98AXhtzE7UkfCsEumvoQWgoYLhOnJ7jQ==} - - babel-plugin-polyfill-corejs2@0.3.3: - resolution: {integrity: sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - babel-plugin-polyfill-corejs3@0.6.0: - resolution: {integrity: sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - babel-plugin-polyfill-regenerator@0.4.1: - resolution: {integrity: sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - bail@1.0.5: - resolution: {integrity: sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==} - - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - - base16@1.0.0: - resolution: {integrity: sha512-pNdYkNPiJUnEhnfXV56+sQy8+AaPcG3POZAUnwr4EeqCUZFz4u2PePbo3e5Gj4ziYPCWGUZT9RHisvJKnwFuBQ==} - - baseline-browser-mapping@2.9.19: - resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} - hasBin: true - - batch@0.6.1: - resolution: {integrity: sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==} - - big.js@5.2.2: - resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} - - binary-extensions@2.2.0: - resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} - engines: {node: '>=8'} - - body-parser@1.20.3: - resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - - bonjour-service@1.0.14: - resolution: {integrity: sha512-HIMbgLnk1Vqvs6B4Wq5ep7mxvj9sGz5d1JJyDNSGNIdA/w2MCz6GTjWTdjqOJV1bEPj+6IkxDvWNFKEBxNt4kQ==} - - boolbase@1.0.0: - resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - - boxen@5.1.2: - resolution: {integrity: sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==} - engines: {node: '>=10'} - - boxen@6.2.1: - resolution: {integrity: sha512-H4PEsJXfFI/Pt8sjDWbHlQPx4zL/bvSQjcilJmaulGt5mLDorHOHpmdXAJcBcmru7PhYSp/cDMWRko4ZUMFkSw==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - brace-expansion@1.1.12: - resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} - - braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} - - browserslist@4.28.1: - resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - - buffer-from@1.1.2: - resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - - bytes@3.0.0: - resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==} - engines: {node: '>= 0.8'} - - bytes@3.1.2: - resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} - engines: {node: '>= 0.8'} - - cacheable-lookup@5.0.4: - resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} - engines: {node: '>=10.6.0'} - - cacheable-request@7.0.4: - resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==} - engines: {node: '>=8'} - - call-bind-apply-helpers@1.0.2: - resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} - engines: {node: '>= 0.4'} - - call-bind@1.0.8: - resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} - engines: {node: '>= 0.4'} - - call-bound@1.0.4: - resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} - engines: {node: '>= 0.4'} - - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - - camel-case@4.1.2: - resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} - - camelcase-css@2.0.1: - resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} - engines: {node: '>= 6'} - - camelcase@6.3.0: - resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} - engines: {node: '>=10'} - - caniuse-api@3.0.0: - resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} - - caniuse-lite@1.0.30001768: - resolution: {integrity: sha512-qY3aDRZC5nWPgHUgIB84WL+nySuo19wk0VJpp/XI9T34lrvkyhRvNVOFJOp2kxClQhiFBu+TaUSudf6oa3vkSA==} - - ccount@1.1.0: - resolution: {integrity: sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg==} - - chalk@2.4.2: - resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} - engines: {node: '>=4'} - - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - - character-entities-legacy@1.1.4: - resolution: {integrity: sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==} - - character-entities@1.2.4: - resolution: {integrity: sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==} - - character-reference-invalid@1.1.4: - resolution: {integrity: sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==} - - cheerio-select@2.1.0: - resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} - - cheerio@1.0.0: - resolution: {integrity: sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==} - engines: {node: '>=18.17'} - - chokidar@3.5.3: - resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} - engines: {node: '>= 8.10.0'} - - chrome-trace-event@1.0.3: - resolution: {integrity: sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==} - engines: {node: '>=6.0'} - - ci-info@2.0.0: - resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==} - - ci-info@3.7.1: - resolution: {integrity: sha512-4jYS4MOAaCIStSRwiuxc4B8MYhIe676yO1sYGzARnjXkWpmzZMMYxY6zu8WYWDhSuth5zhrQ1rhNSibyyvv4/w==} - engines: {node: '>=8'} - - clean-css@5.3.1: - resolution: {integrity: sha512-lCr8OHhiWCTw4v8POJovCoh4T7I9U11yVsPjMWWnnMmp9ZowCxyad1Pathle/9HjaDp+fdQKjO9fQydE6RHTZg==} - engines: {node: '>= 10.0'} - - clean-stack@2.2.0: - resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} - engines: {node: '>=6'} - - cli-boxes@2.2.1: - resolution: {integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==} - engines: {node: '>=6'} - - cli-boxes@3.0.0: - resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} - engines: {node: '>=10'} - - cli-table3@0.6.3: - resolution: {integrity: sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==} - engines: {node: 10.* || >= 12.*} - - clone-deep@4.0.1: - resolution: {integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==} - engines: {node: '>=6'} - - clone-response@1.0.3: - resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} - - clsx@1.2.1: - resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} - engines: {node: '>=6'} - - collapse-white-space@1.0.6: - resolution: {integrity: sha512-jEovNnrhMuqyCcjfEJA56v0Xq8SkIoPKDyaHahwo3POf4qcSXqMYuwNcOTzp74vTsR9Tn08z4MxWqAhcekogkQ==} - - color-convert@1.9.3: - resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} - - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-name@1.1.3: - resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - - colord@2.9.3: - resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} - - colorette@2.0.19: - resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==} - - combine-promises@1.1.0: - resolution: {integrity: sha512-ZI9jvcLDxqwaXEixOhArm3r7ReIivsXkpbyEWyeOhzz1QS0iSgBPnWvEqvIQtYyamGCYA88gFhmUrs9hrrQ0pg==} - engines: {node: '>=10'} - - combined-stream@1.0.8: - resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} - engines: {node: '>= 0.8'} - - comma-separated-tokens@1.0.8: - resolution: {integrity: sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==} - - commander@2.20.3: - resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} - - commander@5.1.0: - resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==} - engines: {node: '>= 6'} - - commander@7.2.0: - resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} - engines: {node: '>= 10'} - - commander@8.3.0: - resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} - engines: {node: '>= 12'} - - commondir@1.0.1: - resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} - - compressible@2.0.18: - resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} - engines: {node: '>= 0.6'} - - compression@1.7.4: - resolution: {integrity: sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==} - engines: {node: '>= 0.8.0'} - - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - - configstore@5.0.1: - resolution: {integrity: sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==} - engines: {node: '>=8'} - - connect-history-api-fallback@2.0.0: - resolution: {integrity: sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==} - engines: {node: '>=0.8'} - - consola@2.15.3: - resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==} - - content-disposition@0.5.2: - resolution: {integrity: sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==} - engines: {node: '>= 0.6'} - - content-disposition@0.5.4: - resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} - engines: {node: '>= 0.6'} - - content-type@1.0.5: - resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} - engines: {node: '>= 0.6'} - - convert-source-map@1.9.0: - resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} - - cookie-signature@1.0.6: - resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} - - cookie@0.7.1: - resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} - engines: {node: '>= 0.6'} - - copy-text-to-clipboard@3.2.0: - resolution: {integrity: sha512-RnJFp1XR/LOBDckxTib5Qjr/PMfkatD0MUCQgdpqS8MdKiNUzBjAQBEN6oUy+jW7LI93BBG3DtMB2KOOKpGs2Q==} - engines: {node: '>=12'} - - copy-webpack-plugin@11.0.0: - resolution: {integrity: sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==} - engines: {node: '>= 14.15.0'} - peerDependencies: - webpack: ^5.1.0 - - core-js-compat@3.25.5: - resolution: {integrity: sha512-ovcyhs2DEBUIE0MGEKHP4olCUW/XYte3Vroyxuh38rD1wAO4dHohsovUC4eAOuzFxE6b+RXvBU3UZ9o0YhUTkA==} - - core-js-pure@3.41.0: - resolution: {integrity: sha512-71Gzp96T9YPk63aUvE5Q5qP+DryB4ZloUZPSOebGM88VNw8VNfvdA7z6kGA8iGOTEzAomsRidp4jXSmUIJsL+Q==} - - core-js@3.25.5: - resolution: {integrity: sha512-nbm6eZSjm+ZuBQxCUPQKQCoUEfFOXjUZ8dTTyikyKaWrTYmAVbykQfwsKE5dBK88u3QCkCrzsx/PPlKfhsvgpw==} - - core-util-is@1.0.3: - resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - - cosmiconfig@6.0.0: - resolution: {integrity: sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==} - engines: {node: '>=8'} - - cosmiconfig@7.0.1: - resolution: {integrity: sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==} - engines: {node: '>=10'} - - cross-fetch@3.2.0: - resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==} - - cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} - - crypto-random-string@2.0.0: - resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} - engines: {node: '>=8'} - - css-declaration-sorter@6.3.1: - resolution: {integrity: sha512-fBffmak0bPAnyqc/HO8C3n2sHrp9wcqQz6ES9koRF2/mLOVAx9zIQ3Y7R29sYCteTPqMCwns4WYQoCX91Xl3+w==} - engines: {node: ^10 || ^12 || >=14} - peerDependencies: - postcss: ^8.0.9 - - css-loader@6.7.3: - resolution: {integrity: sha512-qhOH1KlBMnZP8FzRO6YCH9UHXQhVMcEGLyNdb7Hv2cpcmJbW0YrddO+tG1ab5nT41KpHIYGsbeHqxB9xPu1pKQ==} - engines: {node: '>= 12.13.0'} - peerDependencies: - webpack: ^5.0.0 - - css-minimizer-webpack-plugin@4.2.2: - resolution: {integrity: sha512-s3Of/4jKfw1Hj9CxEO1E5oXhQAxlayuHO2y/ML+C6I9sQ7FdzfEV6QgMLN3vI+qFsjJGIAFLKtQK7t8BOXAIyA==} - engines: {node: '>= 14.15.0'} - peerDependencies: - '@parcel/css': '*' - '@swc/css': '*' - clean-css: '*' - csso: '*' - esbuild: '*' - lightningcss: '*' - webpack: ^5.0.0 - peerDependenciesMeta: - '@parcel/css': - optional: true - '@swc/css': - optional: true - clean-css: - optional: true - csso: - optional: true - esbuild: - optional: true - lightningcss: - optional: true - - css-select@4.3.0: - resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} - - css-select@5.1.0: - resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} - - css-tree@1.1.3: - resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==} - engines: {node: '>=8.0.0'} - - css-what@6.1.0: - resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} - engines: {node: '>= 6'} - - cssesc@3.0.0: - resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} - engines: {node: '>=4'} - hasBin: true - - cssnano-preset-advanced@5.3.9: - resolution: {integrity: sha512-njnh4pp1xCsibJcEHnWZb4EEzni0ePMqPuPNyuWT4Z+YeXmsgqNuTPIljXFEXhxGsWs9183JkXgHxc1TcsahIg==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - cssnano-preset-default@5.2.13: - resolution: {integrity: sha512-PX7sQ4Pb+UtOWuz8A1d+Rbi+WimBIxJTRyBdgGp1J75VU0r/HFQeLnMYgHiCAp6AR4rqrc7Y4R+1Rjk3KJz6DQ==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - cssnano-utils@3.1.0: - resolution: {integrity: sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - cssnano@5.1.13: - resolution: {integrity: sha512-S2SL2ekdEz6w6a2epXn4CmMKU4K3KpcyXLKfAYc9UQQqJRkD/2eLUG0vJ3Db/9OvO5GuAdgXw3pFbR6abqghDQ==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - csso@4.2.0: - resolution: {integrity: sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==} - engines: {node: '>=8.0.0'} - - csstype@3.1.1: - resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==} - - data-view-buffer@1.0.2: - resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} - engines: {node: '>= 0.4'} - - data-view-byte-length@1.0.2: - resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} - engines: {node: '>= 0.4'} - - data-view-byte-offset@1.0.1: - resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} - engines: {node: '>= 0.4'} - - debug@2.6.9: - resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - debug@4.3.4: - resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - decompress-response@6.0.0: - resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} - engines: {node: '>=10'} - - deep-extend@0.6.0: - resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} - engines: {node: '>=4.0.0'} - - deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - - deepmerge@4.2.2: - resolution: {integrity: sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==} - engines: {node: '>=0.10.0'} - - default-gateway@6.0.3: - resolution: {integrity: sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==} - engines: {node: '>= 10'} - - defer-to-connect@2.0.1: - resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} - engines: {node: '>=10'} - - define-data-property@1.1.4: - resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} - engines: {node: '>= 0.4'} - - define-lazy-prop@2.0.0: - resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} - engines: {node: '>=8'} - - define-properties@1.2.1: - resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} - engines: {node: '>= 0.4'} - - del@6.1.1: - resolution: {integrity: sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==} - engines: {node: '>=10'} - - delayed-stream@1.0.0: - resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} - engines: {node: '>=0.4.0'} - - depd@1.1.2: - resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} - engines: {node: '>= 0.6'} - - depd@2.0.0: - resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} - engines: {node: '>= 0.8'} - - destroy@1.2.0: - resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - - detab@2.0.4: - resolution: {integrity: sha512-8zdsQA5bIkoRECvCrNKPla84lyoR7DSAyf7p0YgXzBO9PDJx8KntPUay7NS6yp+KdxdVtiE5SpHKtbp2ZQyA9g==} - - detect-node@2.1.0: - resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} - - detect-port-alt@1.1.6: - resolution: {integrity: sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q==} - engines: {node: '>= 4.2.1'} - hasBin: true - - detect-port@1.5.1: - resolution: {integrity: sha512-aBzdj76lueB6uUst5iAs7+0H/oOjqI5D16XUWxlWMIMROhcM0rfsNVk93zTngq1dDNpoXRr++Sus7ETAExppAQ==} - hasBin: true - - dir-glob@3.0.1: - resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} - engines: {node: '>=8'} - - dns-equal@1.0.0: - resolution: {integrity: sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==} - - dns-packet@5.4.0: - resolution: {integrity: sha512-EgqGeaBB8hLiHLZtp/IbaDQTL8pZ0+IvwzSHA6d7VyMDM+B9hgddEMa9xjK5oYnw0ci0JQ6g2XCD7/f6cafU6g==} - engines: {node: '>=6'} - - doctrine@2.1.0: - resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} - engines: {node: '>=0.10.0'} - - doctrine@3.0.0: - resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} - engines: {node: '>=6.0.0'} - - dom-converter@0.2.0: - resolution: {integrity: sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==} - - dom-serializer@1.4.1: - resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} - - dom-serializer@2.0.0: - resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} - - domelementtype@2.3.0: - resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} - - domhandler@4.3.1: - resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} - engines: {node: '>= 4'} - - domhandler@5.0.3: - resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} - engines: {node: '>= 4'} - - domutils@2.8.0: - resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} - - domutils@3.2.2: - resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} - - dot-case@3.0.4: - resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} - - dot-prop@5.3.0: - resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} - engines: {node: '>=8'} - - dunder-proto@1.0.1: - resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} - engines: {node: '>= 0.4'} - - duplexer@0.1.2: - resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} - - eastasianwidth@0.2.0: - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - - ee-first@1.1.1: - resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - - electron-to-chromium@1.5.286: - resolution: {integrity: sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==} - - emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - - emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - - emojis-list@3.0.0: - resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==} - engines: {node: '>= 4'} - - emoticon@3.2.0: - resolution: {integrity: sha512-SNujglcLTTg+lDAcApPNgEdudaqQFiAbJCqzjNxJkvN9vAwCGi0uu8IUVvx+f16h+V44KCY6Y2yboroc9pilHg==} - - encodeurl@1.0.2: - resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} - engines: {node: '>= 0.8'} - - encodeurl@2.0.0: - resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} - engines: {node: '>= 0.8'} - - encoding-sniffer@0.2.0: - resolution: {integrity: sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==} - - end-of-stream@1.4.4: - resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} - - enhanced-resolve@5.19.0: - resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==} - engines: {node: '>=10.13.0'} - - enquirer@2.3.6: - resolution: {integrity: sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==} - engines: {node: '>=8.6'} - - entities@2.2.0: - resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} - - entities@4.5.0: - resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} - engines: {node: '>=0.12'} - - error-ex@1.3.2: - resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} - - es-abstract@1.23.9: - resolution: {integrity: sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==} - engines: {node: '>= 0.4'} - - es-define-property@1.0.1: - resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} - engines: {node: '>= 0.4'} - - es-errors@1.3.0: - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} - engines: {node: '>= 0.4'} - - es-iterator-helpers@1.2.1: - resolution: {integrity: sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==} - engines: {node: '>= 0.4'} - - es-module-lexer@2.0.0: - resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} - - es-object-atoms@1.1.1: - resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} - engines: {node: '>= 0.4'} - - es-set-tostringtag@2.1.0: - resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} - engines: {node: '>= 0.4'} - - es-shim-unscopables@1.1.0: - resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} - engines: {node: '>= 0.4'} - - es-to-primitive@1.3.0: - resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} - engines: {node: '>= 0.4'} - - escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} - - escape-goat@2.1.1: - resolution: {integrity: sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==} - engines: {node: '>=8'} - - escape-html@1.0.3: - resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} - - escape-string-regexp@1.0.5: - resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} - engines: {node: '>=0.8.0'} - - escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - - eslint-plugin-react@7.37.4: - resolution: {integrity: sha512-BGP0jRmfYyvOyvMoRX/uoUeW+GqNj9y16bPQzqAHf3AYII/tDs+jMN0dBVkl88/OZwNGwrVFxE7riHsXVfy/LQ==} - engines: {node: '>=4'} - peerDependencies: - eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 - - eslint-scope@5.1.1: - resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} - engines: {node: '>=8.0.0'} - - eslint-utils@2.1.0: - resolution: {integrity: sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==} - engines: {node: '>=6'} - - eslint-visitor-keys@1.3.0: - resolution: {integrity: sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==} - engines: {node: '>=4'} - - eslint-visitor-keys@2.1.0: - resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} - engines: {node: '>=10'} - - eslint@7.32.0: - resolution: {integrity: sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==} - engines: {node: ^10.12.0 || >=12.0.0} - deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. - hasBin: true - - espree@7.3.1: - resolution: {integrity: sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==} - engines: {node: ^10.12.0 || >=12.0.0} - - esprima@4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} - hasBin: true - - esquery@1.4.0: - resolution: {integrity: sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==} - engines: {node: '>=0.10'} - - esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} - - estraverse@4.3.0: - resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} - engines: {node: '>=4.0'} - - estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} - - esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - - eta@2.2.0: - resolution: {integrity: sha512-UVQ72Rqjy/ZKQalzV5dCCJP80GrmPrMxh6NlNf+erV6ObL0ZFkhCstWRawS85z3smdr3d2wXPsZEY7rDPfGd2g==} - engines: {node: '>=6.0.0'} - - etag@1.8.1: - resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} - engines: {node: '>= 0.6'} - - eval@0.1.8: - resolution: {integrity: sha512-EzV94NYKoO09GLXGjXj9JIlXijVck4ONSr5wiCWDvhsvj5jxSrzTmRU/9C1DyB6uToszLs8aifA6NQ7lEQdvFw==} - engines: {node: '>= 0.8'} - - eventemitter3@4.0.7: - resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} - - events@3.3.0: - resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} - engines: {node: '>=0.8.x'} - - execa@5.1.1: - resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} - engines: {node: '>=10'} - - express@4.21.2: - resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} - engines: {node: '>= 0.10.0'} - - extend-shallow@2.0.1: - resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} - engines: {node: '>=0.10.0'} - - extend@3.0.2: - resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} - - fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - - fast-glob@3.2.12: - resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==} - engines: {node: '>=8.6.0'} - - fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - - fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - - fast-uri@3.0.6: - resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==} - - fastq@1.13.0: - resolution: {integrity: sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==} - - faye-websocket@0.11.4: - resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} - engines: {node: '>=0.8.0'} - - fbemitter@3.0.0: - resolution: {integrity: sha512-KWKaceCwKQU0+HPoop6gn4eOHk50bBv/VxjJtGMfwmJt3D29JpN4H4eisCtIPA+a8GVBam+ldMMpMjJUvpDyHw==} - - fbjs-css-vars@1.0.2: - resolution: {integrity: sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==} - - fbjs@3.0.5: - resolution: {integrity: sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==} - - feed@4.2.2: - resolution: {integrity: sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==} - engines: {node: '>=0.4.0'} - - file-entry-cache@6.0.1: - resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} - engines: {node: ^10.12.0 || >=12.0.0} - - file-loader@6.2.0: - resolution: {integrity: sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==} - engines: {node: '>= 10.13.0'} - peerDependencies: - webpack: ^4.0.0 || ^5.0.0 - - filesize@8.0.7: - resolution: {integrity: sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==} - engines: {node: '>= 0.4.0'} - - fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} - - finalhandler@1.3.1: - resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} - engines: {node: '>= 0.8'} - - find-cache-dir@3.3.2: - resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} - engines: {node: '>=8'} - - find-up@3.0.0: - resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} - engines: {node: '>=6'} - - find-up@4.1.0: - resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} - engines: {node: '>=8'} - - find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} - - flat-cache@3.0.4: - resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==} - engines: {node: ^10.12.0 || >=12.0.0} - - flatted@3.2.7: - resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==} - - flux@4.0.4: - resolution: {integrity: sha512-NCj3XlayA2UsapRpM7va6wU1+9rE5FIL7qoMcmxWHRzbp0yujihMBm9BBHZ1MDIk5h5o2Bl6eGiCe8rYELAmYw==} - peerDependencies: - react: ^15.0.2 || ^16.0.0 || ^17.0.0 - - follow-redirects@1.15.11: - resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} - engines: {node: '>=4.0'} - peerDependencies: - debug: '*' - peerDependenciesMeta: - debug: - optional: true - - for-each@0.3.5: - resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} - engines: {node: '>= 0.4'} - - fork-ts-checker-webpack-plugin@6.5.2: - resolution: {integrity: sha512-m5cUmF30xkZ7h4tWUgTAcEaKmUW7tfyUyTqNNOz7OxWJ0v1VWKTcOvH8FWHUwSjlW/356Ijc9vi3XfcPstpQKA==} - engines: {node: '>=10', yarn: '>=1.0.0'} - peerDependencies: - eslint: '>= 6' - typescript: '>= 2.7' - vue-template-compiler: '*' - webpack: '>= 4' - peerDependenciesMeta: - eslint: - optional: true - vue-template-compiler: - optional: true - - form-data@4.0.5: - resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} - engines: {node: '>= 6'} - - forwarded@0.2.0: - resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} - engines: {node: '>= 0.6'} - - fraction.js@4.2.0: - resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==} - - fresh@0.5.2: - resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} - engines: {node: '>= 0.6'} - - fs-extra@10.1.0: - resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} - engines: {node: '>=12'} - - fs-extra@9.1.0: - resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} - engines: {node: '>=10'} - - fs-monkey@1.0.3: - resolution: {integrity: sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==} - - fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - - function.prototype.name@1.1.8: - resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} - engines: {node: '>= 0.4'} - - functional-red-black-tree@1.0.1: - resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==} - - functions-have-names@1.2.3: - resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} - - gensync@1.0.0-beta.2: - resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} - engines: {node: '>=6.9.0'} - - get-intrinsic@1.3.0: - resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} - engines: {node: '>= 0.4'} - - get-own-enumerable-property-symbols@3.0.2: - resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==} - - get-proto@1.0.1: - resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} - engines: {node: '>= 0.4'} - - get-stream@5.2.0: - resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} - engines: {node: '>=8'} - - get-stream@6.0.1: - resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} - engines: {node: '>=10'} - - get-symbol-description@1.1.0: - resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} - engines: {node: '>= 0.4'} - - github-slugger@1.4.0: - resolution: {integrity: sha512-w0dzqw/nt51xMVmlaV1+JRzN+oCa1KfcgGEWhxUG16wbdA+Xnt/yoFO8Z8x/V82ZcZ0wy6ln9QDup5avbhiDhQ==} - - glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - - glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} - - glob-to-regexp@0.4.1: - resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} - - glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - - global-dirs@3.0.0: - resolution: {integrity: sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==} - engines: {node: '>=10'} - - global-modules@2.0.0: - resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==} - engines: {node: '>=6'} - - global-prefix@3.0.0: - resolution: {integrity: sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==} - engines: {node: '>=6'} - - globals@11.12.0: - resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} - engines: {node: '>=4'} - - globals@13.17.0: - resolution: {integrity: sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==} - engines: {node: '>=8'} - - globalthis@1.0.4: - resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} - engines: {node: '>= 0.4'} - - globby@11.1.0: - resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} - engines: {node: '>=10'} - - globby@13.1.3: - resolution: {integrity: sha512-8krCNHXvlCgHDpegPzleMq07yMYTO2sXKASmZmquEYWEmCx6J5UTRbp5RwMJkTJGtcQ44YpiUYUiN0b9mzy8Bw==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - gopd@1.2.0: - resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} - engines: {node: '>= 0.4'} - - got@11.8.6: - resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} - engines: {node: '>=10.19.0'} - - graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - - gray-matter@4.0.3: - resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} - engines: {node: '>=6.0'} - - gzip-size@6.0.0: - resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} - engines: {node: '>=10'} - - handle-thing@2.0.1: - resolution: {integrity: sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==} - - has-bigints@1.1.0: - resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} - engines: {node: '>= 0.4'} - - has-flag@3.0.0: - resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} - engines: {node: '>=4'} - - has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - - has-property-descriptors@1.0.2: - resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} - - has-proto@1.2.0: - resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} - engines: {node: '>= 0.4'} - - has-symbols@1.1.0: - resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} - engines: {node: '>= 0.4'} - - has-tostringtag@1.0.2: - resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} - engines: {node: '>= 0.4'} - - has-yarn@2.1.0: - resolution: {integrity: sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==} - engines: {node: '>=8'} - - hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} - engines: {node: '>= 0.4'} - - hast-to-hyperscript@9.0.1: - resolution: {integrity: sha512-zQgLKqF+O2F72S1aa4y2ivxzSlko3MAvxkwG8ehGmNiqd98BIN3JM1rAJPmplEyLmGLO2QZYJtIneOSZ2YbJuA==} - - hast-util-from-parse5@6.0.1: - resolution: {integrity: sha512-jeJUWiN5pSxW12Rh01smtVkZgZr33wBokLzKLwinYOUfSzm1Nl/c3GUGebDyOKjdsRgMvoVbV0VpAcpjF4NrJA==} - - hast-util-is-element@1.1.0: - resolution: {integrity: sha512-oUmNua0bFbdrD/ELDSSEadRVtWZOf3iF6Lbv81naqsIV99RnSCieTbWuWCY8BAeEfKJTKl0gRdokv+dELutHGQ==} - - hast-util-parse-selector@2.2.5: - resolution: {integrity: sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==} - - hast-util-raw@6.0.1: - resolution: {integrity: sha512-ZMuiYA+UF7BXBtsTBNcLBF5HzXzkyE6MLzJnL605LKE8GJylNjGc4jjxazAHUtcwT5/CEt6afRKViYB4X66dig==} - - hast-util-to-parse5@6.0.0: - resolution: {integrity: sha512-Lu5m6Lgm/fWuz8eWnrKezHtVY83JeRGaNQ2kn9aJgqaxvVkFCZQBEhgodZUDUvoodgyROHDb3r5IxAEdl6suJQ==} - - hast-util-to-text@2.0.1: - resolution: {integrity: sha512-8nsgCARfs6VkwH2jJU9b8LNTuR4700na+0h3PqCaEk4MAnMDeu5P0tP8mjk9LLNGxIeQRLbiDbZVw6rku+pYsQ==} - - hastscript@6.0.0: - resolution: {integrity: sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==} - - he@1.2.0: - resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} - hasBin: true - - history@4.10.1: - resolution: {integrity: sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==} - - hoist-non-react-statics@3.3.2: - resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} - - hpack.js@2.1.6: - resolution: {integrity: sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==} - - html-entities@2.3.3: - resolution: {integrity: sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==} - - html-minifier-terser@6.1.0: - resolution: {integrity: sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==} - engines: {node: '>=12'} - hasBin: true - - html-tags@3.2.0: - resolution: {integrity: sha512-vy7ClnArOZwCnqZgvv+ddgHgJiAFXe3Ge9ML5/mBctVJoUoYPCdxVucOywjDARn6CVoh3dRSFdPHy2sX80L0Wg==} - engines: {node: '>=8'} - - html-void-elements@1.0.5: - resolution: {integrity: sha512-uE/TxKuyNIcx44cIWnjr/rfIATDH7ZaOMmstu0CwhFG1Dunhlp4OC6/NMbhiwoq5BpW0ubi303qnEk/PZj614w==} - - html-webpack-plugin@5.5.0: - resolution: {integrity: sha512-sy88PC2cRTVxvETRgUHFrL4No3UxvcH8G1NepGhqaTT+GXN2kTamqasot0inS5hXeg1cMbFDt27zzo9p35lZVw==} - engines: {node: '>=10.13.0'} - peerDependencies: - webpack: ^5.20.0 - - htmlparser2@6.1.0: - resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==} - - htmlparser2@9.1.0: - resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==} - - http-cache-semantics@4.2.0: - resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} - - http-deceiver@1.2.7: - resolution: {integrity: sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==} - - http-errors@1.6.3: - resolution: {integrity: sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==} - engines: {node: '>= 0.6'} - - http-errors@2.0.0: - resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} - engines: {node: '>= 0.8'} - - http-parser-js@0.5.8: - resolution: {integrity: sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==} - - http-proxy-middleware@2.0.9: - resolution: {integrity: sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==} - engines: {node: '>=12.0.0'} - peerDependencies: - '@types/express': ^4.17.13 - peerDependenciesMeta: - '@types/express': - optional: true - - http-proxy@1.18.1: - resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} - engines: {node: '>=8.0.0'} - - http2-wrapper@1.0.3: - resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==} - engines: {node: '>=10.19.0'} - - human-signals@2.1.0: - resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} - engines: {node: '>=10.17.0'} - - iconv-lite@0.4.24: - resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} - engines: {node: '>=0.10.0'} - - iconv-lite@0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} - engines: {node: '>=0.10.0'} - - icss-utils@5.1.0: - resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - - ignore@4.0.6: - resolution: {integrity: sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==} - engines: {node: '>= 4'} - - ignore@5.2.4: - resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} - engines: {node: '>= 4'} - - image-size@1.0.2: - resolution: {integrity: sha512-xfOoWjceHntRb3qFCrh5ZFORYH8XCdYpASltMhZ/Q0KZiOwjdE/Yl2QCiWdwD+lygV5bMCvauzgu5PxBX/Yerg==} - engines: {node: '>=14.0.0'} - hasBin: true - - immer@9.0.17: - resolution: {integrity: sha512-+hBruaLSQvkPfxRiTLK/mi4vLH+/VQS6z2KJahdoxlleFOI8ARqzOF17uy12eFDlqWmPoygwc5evgwcp+dlHhg==} - - import-fresh@3.3.0: - resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} - engines: {node: '>=6'} - - import-lazy@2.1.0: - resolution: {integrity: sha512-m7ZEHgtw69qOGw+jwxXkHlrlIPdTGkyh66zXZ1ajZbxkDBNjSY/LGbmjc7h0s2ELsUDTAhFr55TrPSSqJGPG0A==} - engines: {node: '>=4'} - - imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - - indent-string@4.0.0: - resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} - engines: {node: '>=8'} - - infima@0.2.0-alpha.43: - resolution: {integrity: sha512-2uw57LvUqW0rK/SWYnd/2rRfxNA5DDNOh33jxF7fy46VWoNhGxiUQyVZHbBMjQ33mQem0cjdDVwgWVAmlRfgyQ==} - engines: {node: '>=12'} - - inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. - - inherits@2.0.3: - resolution: {integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==} - - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - - ini@1.3.8: - resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - - ini@2.0.0: - resolution: {integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==} - engines: {node: '>=10'} - - inline-style-parser@0.1.1: - resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==} - - internal-slot@1.1.0: - resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} - engines: {node: '>= 0.4'} - - interpret@1.4.0: - resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} - engines: {node: '>= 0.10'} - - invariant@2.2.4: - resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} - - ipaddr.js@1.9.1: - resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} - engines: {node: '>= 0.10'} - - ipaddr.js@2.0.1: - resolution: {integrity: sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==} - engines: {node: '>= 10'} - - is-alphabetical@1.0.4: - resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==} - - is-alphanumerical@1.0.4: - resolution: {integrity: sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==} - - is-array-buffer@3.0.5: - resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} - engines: {node: '>= 0.4'} - - is-arrayish@0.2.1: - resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - - is-async-function@2.1.1: - resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} - engines: {node: '>= 0.4'} - - is-bigint@1.1.0: - resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} - engines: {node: '>= 0.4'} - - is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} - engines: {node: '>=8'} - - is-boolean-object@1.2.2: - resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} - engines: {node: '>= 0.4'} - - is-buffer@2.0.5: - resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==} - engines: {node: '>=4'} - - is-callable@1.2.7: - resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} - engines: {node: '>= 0.4'} - - is-ci@2.0.0: - resolution: {integrity: sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==} - hasBin: true - - is-core-module@2.16.1: - resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} - engines: {node: '>= 0.4'} - - is-data-view@1.0.2: - resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} - engines: {node: '>= 0.4'} - - is-date-object@1.1.0: - resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} - engines: {node: '>= 0.4'} - - is-decimal@1.0.4: - resolution: {integrity: sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==} - - is-docker@2.2.1: - resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} - engines: {node: '>=8'} - hasBin: true - - is-extendable@0.1.1: - resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} - engines: {node: '>=0.10.0'} - - is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - - is-finalizationregistry@1.1.1: - resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} - engines: {node: '>= 0.4'} - - is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - - is-generator-function@1.1.0: - resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==} - engines: {node: '>= 0.4'} - - is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - - is-hexadecimal@1.0.4: - resolution: {integrity: sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==} - - is-installed-globally@0.4.0: - resolution: {integrity: sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==} - engines: {node: '>=10'} - - is-map@2.0.3: - resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} - engines: {node: '>= 0.4'} - - is-npm@5.0.0: - resolution: {integrity: sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA==} - engines: {node: '>=10'} - - is-number-object@1.1.1: - resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} - engines: {node: '>= 0.4'} - - is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - - is-obj@1.0.1: - resolution: {integrity: sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==} - engines: {node: '>=0.10.0'} - - is-obj@2.0.0: - resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} - engines: {node: '>=8'} - - is-path-cwd@2.2.0: - resolution: {integrity: sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==} - engines: {node: '>=6'} - - is-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} - - is-plain-obj@2.1.0: - resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} - engines: {node: '>=8'} - - is-plain-obj@3.0.0: - resolution: {integrity: sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==} - engines: {node: '>=10'} - - is-plain-object@2.0.4: - resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} - engines: {node: '>=0.10.0'} - - is-regex@1.2.1: - resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} - engines: {node: '>= 0.4'} - - is-regexp@1.0.0: - resolution: {integrity: sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==} - engines: {node: '>=0.10.0'} - - is-root@2.1.0: - resolution: {integrity: sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg==} - engines: {node: '>=6'} - - is-set@2.0.3: - resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} - engines: {node: '>= 0.4'} - - is-shared-array-buffer@1.0.4: - resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} - engines: {node: '>= 0.4'} - - is-stream@2.0.1: - resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} - engines: {node: '>=8'} - - is-string@1.1.1: - resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} - engines: {node: '>= 0.4'} - - is-symbol@1.1.1: - resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} - engines: {node: '>= 0.4'} - - is-typed-array@1.1.15: - resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} - engines: {node: '>= 0.4'} - - is-typedarray@1.0.0: - resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} - - is-weakmap@2.0.2: - resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} - engines: {node: '>= 0.4'} - - is-weakref@1.1.1: - resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} - engines: {node: '>= 0.4'} - - is-weakset@2.0.4: - resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} - engines: {node: '>= 0.4'} - - is-whitespace-character@1.0.4: - resolution: {integrity: sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w==} - - is-word-character@1.0.4: - resolution: {integrity: sha512-5SMO8RVennx3nZrqtKwCGyyetPE9VDba5ugvKLaD4KopPG5kR4mQ7tNt/r7feL5yt5h3lpuBbIUmCOG2eSzXHA==} - - is-wsl@2.2.0: - resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} - engines: {node: '>=8'} - - is-yarn-global@0.3.0: - resolution: {integrity: sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==} - - isarray@0.0.1: - resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} - - isarray@1.0.0: - resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} - - isarray@2.0.5: - resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} - - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - isobject@3.0.1: - resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} - engines: {node: '>=0.10.0'} - - iterator.prototype@1.1.5: - resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} - engines: {node: '>= 0.4'} - - jest-util@29.3.1: - resolution: {integrity: sha512-7YOVZaiX7RJLv76ZfHt4nbNEzzTRiMW/IiOG7ZOKmTXmoGBxUDefgMAxQubu6WPVqP5zSzAdZG0FfLcC7HOIFQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-worker@27.5.1: - resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} - engines: {node: '>= 10.13.0'} - - jest-worker@29.3.1: - resolution: {integrity: sha512-lY4AnnmsEWeiXirAIA0c9SDPbuCBq8IYuDVL8PMm0MZ2PEs2yPvRA/J64QBXuZp7CYKrDM/rmNrc9/i3KJQncw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - joi@17.13.3: - resolution: {integrity: sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==} - - js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - - js-yaml@3.14.1: - resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} - hasBin: true - - js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} - hasBin: true - - jsesc@0.5.0: - resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} - hasBin: true - - jsesc@2.5.2: - resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} - engines: {node: '>=4'} - hasBin: true - - json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - - json-parse-even-better-errors@2.3.1: - resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - - json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - - json-schema-traverse@1.0.0: - resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - - json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - - json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} - hasBin: true - - jsonfile@6.1.0: - resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} - - jsx-ast-utils@3.3.3: - resolution: {integrity: sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==} - engines: {node: '>=4.0'} - - katex@0.16.28: - resolution: {integrity: sha512-YHzO7721WbmAL6Ov1uzN/l5mY5WWWhJBSW+jq4tkfZfsxmo1hu6frS0EOswvjBUnWE6NtjEs48SFn5CQESRLZg==} - hasBin: true - - keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - - kind-of@6.0.3: - resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} - engines: {node: '>=0.10.0'} - - kleur@3.0.3: - resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} - engines: {node: '>=6'} - - klona@2.0.6: - resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==} - engines: {node: '>= 8'} - - latest-version@5.1.0: - resolution: {integrity: sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==} - engines: {node: '>=8'} - - leven@3.1.0: - resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} - engines: {node: '>=6'} - - levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} - - lilconfig@2.0.6: - resolution: {integrity: sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==} - engines: {node: '>=10'} - - lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - - loader-runner@4.3.1: - resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==} - engines: {node: '>=6.11.5'} - - loader-utils@2.0.4: - resolution: {integrity: sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==} - engines: {node: '>=8.9.0'} - - loader-utils@3.2.1: - resolution: {integrity: sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==} - engines: {node: '>= 12.13.0'} - - locate-path@3.0.0: - resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} - engines: {node: '>=6'} - - locate-path@5.0.0: - resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} - engines: {node: '>=8'} - - locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} - - lodash.curry@4.1.1: - resolution: {integrity: sha512-/u14pXGviLaweY5JI0IUzgzF2J6Ne8INyzAZjImcryjgkZ+ebruBxy2/JaOOkTqScddcYtakjhSaeemV8lR0tA==} - - lodash.debounce@4.0.8: - resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} - - lodash.flow@3.5.0: - resolution: {integrity: sha512-ff3BX/tSioo+XojX4MOsOMhJw0nZoUEF011LX8g8d3gvjVbxd89cCio4BCXronjxcTUIJUoqKEUA+n4CqvvRPw==} - - lodash.memoize@4.1.2: - resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} - - lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - - lodash.truncate@4.4.2: - resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} - - lodash.uniq@4.5.0: - resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} - - lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - - loose-envify@1.4.0: - resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} - hasBin: true - - lower-case@2.0.2: - resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} - - lowercase-keys@2.0.0: - resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} - engines: {node: '>=8'} - - make-dir@3.1.0: - resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} - engines: {node: '>=8'} - - markdown-escapes@1.0.4: - resolution: {integrity: sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg==} - - math-intrinsics@1.1.0: - resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} - engines: {node: '>= 0.4'} - - mdast-squeeze-paragraphs@4.0.0: - resolution: {integrity: sha512-zxdPn69hkQ1rm4J+2Cs2j6wDEv7O17TfXTJ33tl/+JPIoEmtV9t2ZzBM5LPHE8QlHsmVD8t3vPKCyY3oH+H8MQ==} - - mdast-util-definitions@4.0.0: - resolution: {integrity: sha512-k8AJ6aNnUkB7IE+5azR9h81O5EQ/cTDXtWdMq9Kk5KcEW/8ritU5CeLg/9HhOC++nALHBlaogJ5jz0Ybk3kPMQ==} - - mdast-util-to-hast@10.0.1: - resolution: {integrity: sha512-BW3LM9SEMnjf4HXXVApZMt8gLQWVNXc3jryK0nJu/rOXPOnlkUjmdkDlmxMirpbU9ILncGFIwLH/ubnWBbcdgA==} - - mdast-util-to-string@2.0.0: - resolution: {integrity: sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==} - - mdn-data@2.0.14: - resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} - - mdurl@1.0.1: - resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==} - - media-typer@0.3.0: - resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} - engines: {node: '>= 0.6'} - - memfs@3.4.12: - resolution: {integrity: sha512-BcjuQn6vfqP+k100e0E9m61Hyqa//Brp+I3f0OBmN0ATHlFA8vx3Lt8z57R3u2bPqe3WGDBC+nF72fTH7isyEw==} - engines: {node: '>= 4.0.0'} - - merge-descriptors@1.0.3: - resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} - - merge-stream@2.0.0: - resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} - - merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - - methods@1.1.2: - resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} - engines: {node: '>= 0.6'} - - micromatch@4.0.8: - resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} - engines: {node: '>=8.6'} - - mime-db@1.33.0: - resolution: {integrity: sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==} - engines: {node: '>= 0.6'} - - mime-db@1.52.0: - resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} - engines: {node: '>= 0.6'} - - mime-types@2.1.18: - resolution: {integrity: sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==} - engines: {node: '>= 0.6'} - - mime-types@2.1.35: - resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} - engines: {node: '>= 0.6'} - - mime@1.6.0: - resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} - engines: {node: '>=4'} - hasBin: true - - mimic-fn@2.1.0: - resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} - engines: {node: '>=6'} - - mimic-response@1.0.1: - resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} - engines: {node: '>=4'} - - mimic-response@3.1.0: - resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} - engines: {node: '>=10'} - - mini-css-extract-plugin@2.7.2: - resolution: {integrity: sha512-EdlUizq13o0Pd+uCp+WO/JpkLvHRVGt97RqfeGhXqAcorYo1ypJSpkV+WDT0vY/kmh/p7wRdJNJtuyK540PXDw==} - engines: {node: '>= 12.13.0'} - peerDependencies: - webpack: ^5.0.0 - - minimalistic-assert@1.0.1: - resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} - - minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - - minimist@1.2.8: - resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - - mrmime@1.0.1: - resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==} - engines: {node: '>=10'} - - ms@2.0.0: - resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} - - ms@2.1.2: - resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - - multicast-dns@7.2.5: - resolution: {integrity: sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==} - hasBin: true - - nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - - negotiator@0.6.3: - resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} - engines: {node: '>= 0.6'} - - neo-async@2.6.2: - resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - - no-case@3.0.4: - resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} - - node-emoji@1.11.0: - resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} - - node-fetch@2.7.0: - resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} - engines: {node: 4.x || >=6.0.0} - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - - node-forge@1.3.1: - resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} - engines: {node: '>= 6.13.0'} - - node-releases@2.0.27: - resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} - - normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} - - normalize-range@0.1.2: - resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} - engines: {node: '>=0.10.0'} - - normalize-url@6.1.0: - resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} - engines: {node: '>=10'} - - npm-run-path@4.0.1: - resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} - engines: {node: '>=8'} - - nprogress@0.2.0: - resolution: {integrity: sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==} - - nth-check@2.1.1: - resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} - - object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} - - object-inspect@1.13.4: - resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} - engines: {node: '>= 0.4'} - - object-keys@1.1.1: - resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} - engines: {node: '>= 0.4'} - - object.assign@4.1.7: - resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} - engines: {node: '>= 0.4'} - - object.entries@1.1.9: - resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} - engines: {node: '>= 0.4'} - - object.fromentries@2.0.8: - resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} - engines: {node: '>= 0.4'} - - object.values@1.2.1: - resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} - engines: {node: '>= 0.4'} - - obuf@1.1.2: - resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} - - on-finished@2.4.1: - resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} - engines: {node: '>= 0.8'} - - on-headers@1.0.2: - resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} - engines: {node: '>= 0.8'} - - once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - - onetime@5.1.2: - resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} - engines: {node: '>=6'} - - open@8.4.0: - resolution: {integrity: sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==} - engines: {node: '>=12'} - - opener@1.5.2: - resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} - hasBin: true - - optionator@0.9.1: - resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==} - engines: {node: '>= 0.8.0'} - - own-keys@1.0.1: - resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} - engines: {node: '>= 0.4'} - - p-cancelable@2.1.1: - resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} - engines: {node: '>=8'} - - p-limit@2.3.0: - resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} - engines: {node: '>=6'} - - p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - - p-locate@3.0.0: - resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} - engines: {node: '>=6'} - - p-locate@4.1.0: - resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} - engines: {node: '>=8'} - - p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} - - p-map@4.0.0: - resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} - engines: {node: '>=10'} - - p-retry@4.6.2: - resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} - engines: {node: '>=8'} - - p-try@2.2.0: - resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} - engines: {node: '>=6'} - - package-json@6.5.0: - resolution: {integrity: sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==} - engines: {node: '>=8'} - - param-case@3.0.4: - resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} - - parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} - - parse-entities@2.0.0: - resolution: {integrity: sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==} - - parse-json@5.2.0: - resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} - engines: {node: '>=8'} - - parse-numeric-range@1.3.0: - resolution: {integrity: sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==} - - parse5-htmlparser2-tree-adapter@7.1.0: - resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==} - - parse5-parser-stream@7.1.2: - resolution: {integrity: sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==} - - parse5@6.0.1: - resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} - - parse5@7.2.1: - resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==} - - parseurl@1.3.3: - resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} - engines: {node: '>= 0.8'} - - pascal-case@3.1.2: - resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} - - path-exists@3.0.0: - resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} - engines: {node: '>=4'} - - path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - - path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} - - path-is-inside@1.0.2: - resolution: {integrity: sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==} - - path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - - path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - - path-to-regexp@0.1.12: - resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} - - path-to-regexp@1.9.0: - resolution: {integrity: sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==} - - path-to-regexp@3.3.0: - resolution: {integrity: sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==} - - path-type@4.0.0: - resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} - engines: {node: '>=8'} - - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - - pkg-dir@4.2.0: - resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} - engines: {node: '>=8'} - - pkg-up@3.1.0: - resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==} - engines: {node: '>=8'} - - possible-typed-array-names@1.1.0: - resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} - engines: {node: '>= 0.4'} - - postcss-calc@8.2.4: - resolution: {integrity: sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==} - peerDependencies: - postcss: ^8.2.2 - - postcss-colormin@5.3.0: - resolution: {integrity: sha512-WdDO4gOFG2Z8n4P8TWBpshnL3JpmNmJwdnfP2gbk2qBA8PWwOYcmjmI/t3CmMeL72a7Hkd+x/Mg9O2/0rD54Pg==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-convert-values@5.1.3: - resolution: {integrity: sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-discard-comments@5.1.2: - resolution: {integrity: sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-discard-duplicates@5.1.0: - resolution: {integrity: sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-discard-empty@5.1.1: - resolution: {integrity: sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-discard-overridden@5.1.0: - resolution: {integrity: sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-discard-unused@5.1.0: - resolution: {integrity: sha512-KwLWymI9hbwXmJa0dkrzpRbSJEh0vVUd7r8t0yOGPcfKzyJJxFM8kLyC5Ev9avji6nY95pOp1W6HqIrfT+0VGw==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-loader@4.3.0: - resolution: {integrity: sha512-M/dSoIiNDOo8Rk0mUqoj4kpGq91gcxCfb9PoyZVdZ76/AuhxylHDYZblNE8o+EQ9AMSASeMFEKxZf5aU6wlx1Q==} - engines: {node: '>= 10.13.0'} - peerDependencies: - postcss: ^7.0.0 || ^8.0.1 - webpack: ^4.0.0 || ^5.0.0 - - postcss-loader@7.0.2: - resolution: {integrity: sha512-fUJzV/QH7NXUAqV8dWJ9Lg4aTkDCezpTS5HgJ2DvqznexTbSTxgi/dTECvTZ15BwKTtk8G/bqI/QTu2HPd3ZCg==} - engines: {node: '>= 14.15.0'} - peerDependencies: - postcss: ^7.0.0 || ^8.0.1 - webpack: ^5.0.0 - - postcss-merge-idents@5.1.1: - resolution: {integrity: sha512-pCijL1TREiCoog5nQp7wUe+TUonA2tC2sQ54UGeMmryK3UFGIYKqDyjnqd6RcuI4znFn9hWSLNN8xKE/vWcUQw==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-merge-longhand@5.1.7: - resolution: {integrity: sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-merge-rules@5.1.3: - resolution: {integrity: sha512-LbLd7uFC00vpOuMvyZop8+vvhnfRGpp2S+IMQKeuOZZapPRY4SMq5ErjQeHbHsjCUgJkRNrlU+LmxsKIqPKQlA==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-minify-font-values@5.1.0: - resolution: {integrity: sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-minify-gradients@5.1.1: - resolution: {integrity: sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-minify-params@5.1.4: - resolution: {integrity: sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-minify-selectors@5.2.1: - resolution: {integrity: sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-modules-extract-imports@3.0.0: - resolution: {integrity: sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - - postcss-modules-local-by-default@4.0.0: - resolution: {integrity: sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - - postcss-modules-scope@3.0.0: - resolution: {integrity: sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - - postcss-modules-values@4.0.0: - resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - - postcss-normalize-charset@5.1.0: - resolution: {integrity: sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-normalize-display-values@5.1.0: - resolution: {integrity: sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-normalize-positions@5.1.1: - resolution: {integrity: sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-normalize-repeat-style@5.1.1: - resolution: {integrity: sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-normalize-string@5.1.0: - resolution: {integrity: sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-normalize-timing-functions@5.1.0: - resolution: {integrity: sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-normalize-unicode@5.1.1: - resolution: {integrity: sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-normalize-url@5.1.0: - resolution: {integrity: sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-normalize-whitespace@5.1.1: - resolution: {integrity: sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-ordered-values@5.1.3: - resolution: {integrity: sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-reduce-idents@5.2.0: - resolution: {integrity: sha512-BTrLjICoSB6gxbc58D5mdBK8OhXRDqud/zodYfdSi52qvDHdMwk+9kB9xsM8yJThH/sZU5A6QVSmMmaN001gIg==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-reduce-initial@5.1.1: - resolution: {integrity: sha512-//jeDqWcHPuXGZLoolFrUXBDyuEGbr9S2rMo19bkTIjBQ4PqkaO+oI8wua5BOUxpfi97i3PCoInsiFIEBfkm9w==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-reduce-transforms@5.1.0: - resolution: {integrity: sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-selector-parser@6.0.10: - resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} - engines: {node: '>=4'} - - postcss-sort-media-queries@4.3.0: - resolution: {integrity: sha512-jAl8gJM2DvuIJiI9sL1CuiHtKM4s5aEIomkU8G3LFvbP+p8i7Sz8VV63uieTgoewGqKbi+hxBTiOKJlB35upCg==} - engines: {node: '>=10.0.0'} - peerDependencies: - postcss: ^8.4.16 - - postcss-svgo@5.1.0: - resolution: {integrity: sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-unique-selectors@5.1.1: - resolution: {integrity: sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-value-parser@4.2.0: - resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - - postcss-zindex@5.1.0: - resolution: {integrity: sha512-fgFMf0OtVSBR1va1JNHYgMxYk73yhn/qb4uQDq1DLGYolz8gHCyr/sesEuGUaYs58E3ZJRcpoGuPVoB7Meiq9A==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss@8.5.3: - resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} - engines: {node: ^10 || ^12 || >=14} - - prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} - - prettier@2.8.8: - resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} - engines: {node: '>=10.13.0'} - hasBin: true - - pretty-error@4.0.0: - resolution: {integrity: sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==} - - pretty-time@1.1.0: - resolution: {integrity: sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA==} - engines: {node: '>=4'} - - prism-react-renderer@1.3.5: - resolution: {integrity: sha512-IJ+MSwBWKG+SM3b2SUfdrhC+gu01QkV2KmRQgREThBfSQRoufqRfxfHUxpG1WcaFjP+kojcFyO9Qqtpgt3qLCg==} - peerDependencies: - react: '>=0.14.9' - - prismjs@1.30.0: - resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} - engines: {node: '>=6'} - - process-nextick-args@2.0.1: - resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} - - progress@2.0.3: - resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} - engines: {node: '>=0.4.0'} - - promise@7.3.1: - resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==} - - prompts@2.4.2: - resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} - engines: {node: '>= 6'} - - prop-types@15.8.1: - resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} - - property-information@5.6.0: - resolution: {integrity: sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==} - - proxy-addr@2.0.7: - resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} - engines: {node: '>= 0.10'} - - proxy-from-env@1.1.0: - resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - - pump@3.0.2: - resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} - - punycode@2.1.1: - resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==} - engines: {node: '>=6'} - - pupa@2.1.1: - resolution: {integrity: sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==} - engines: {node: '>=8'} - - pure-color@1.3.0: - resolution: {integrity: sha512-QFADYnsVoBMw1srW7OVKEYjG+MbIa49s54w1MA1EDY6r2r/sTcKKYqRX1f4GYvnXP7eN/Pe9HFcX+hwzmrXRHA==} - - qs@6.13.0: - resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} - engines: {node: '>=0.6'} - - queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - - queue@6.0.2: - resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==} - - quick-lru@5.1.1: - resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} - engines: {node: '>=10'} - - randombytes@2.1.0: - resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} - - range-parser@1.2.0: - resolution: {integrity: sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==} - engines: {node: '>= 0.6'} - - range-parser@1.2.1: - resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} - engines: {node: '>= 0.6'} - - raw-body@2.5.2: - resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} - engines: {node: '>= 0.8'} - - rc@1.2.8: - resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} - hasBin: true - - react-base16-styling@0.6.0: - resolution: {integrity: sha512-yvh/7CArceR/jNATXOKDlvTnPKPmGZz7zsenQ3jUwLzHkNUR0CvY3yGYJbWJ/nnxsL8Sgmt5cO3/SILVuPO6TQ==} - - react-dev-utils@12.0.1: - resolution: {integrity: sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==} - engines: {node: '>=14'} - peerDependencies: - typescript: '>=2.7' - webpack: '>=4' - peerDependenciesMeta: - typescript: - optional: true - - react-dom@16.14.0: - resolution: {integrity: sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==} - peerDependencies: - react: ^16.14.0 - - react-error-overlay@6.0.11: - resolution: {integrity: sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==} - - react-fast-compare@3.2.0: - resolution: {integrity: sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==} - - react-helmet-async@1.3.0: - resolution: {integrity: sha512-9jZ57/dAn9t3q6hneQS0wukqC2ENOBgMNVEhb/ZG9ZSxUetzVIw4iAmEU38IaVg3QGYauQPhSeUTuIUtFglWpg==} - peerDependencies: - react: ^16.6.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.6.0 || ^17.0.0 || ^18.0.0 - - react-is@16.13.1: - resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - - react-json-view@1.21.3: - resolution: {integrity: sha512-13p8IREj9/x/Ye4WI/JpjhoIwuzEgUAtgJZNBJckfzJt1qyh24BdTm6UQNGnyTq9dapQdrqvquZTo3dz1X6Cjw==} - peerDependencies: - react: ^17.0.0 || ^16.3.0 || ^15.5.4 - react-dom: ^17.0.0 || ^16.3.0 || ^15.5.4 - - react-lifecycles-compat@3.0.4: - resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} - - react-loadable-ssr-addon-v5-slorber@1.0.1: - resolution: {integrity: sha512-lq3Lyw1lGku8zUEJPDxsNm1AfYHBrO9Y1+olAYwpUJ2IGFBskM0DMKok97A6LWUpHm+o7IvQBOWu9MLenp9Z+A==} - engines: {node: '>=10.13.0'} - peerDependencies: - react-loadable: '*' - webpack: '>=4.41.1 || 5.x' - - react-router-config@5.1.1: - resolution: {integrity: sha512-DuanZjaD8mQp1ppHjgnnUnyOlqYXZVjnov/JzFhjLEwd3Z4dYjMSnqrEzzGThH47vpCOqPPwJM2FtthLeJ8Pbg==} - peerDependencies: - react: '>=15' - react-router: '>=5' - - react-router-dom@5.3.4: - resolution: {integrity: sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==} - peerDependencies: - react: '>=15' - - react-router@5.3.4: - resolution: {integrity: sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==} - peerDependencies: - react: '>=15' - - react-textarea-autosize@8.5.8: - resolution: {integrity: sha512-iUiIj70JefrTuSJ4LbVFiSqWiHHss5L63L717bqaWHMgkm9sz6eEvro4vZ3uQfGJbevzwT6rHOszHKA8RkhRMg==} - engines: {node: '>=10'} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - - react@16.14.0: - resolution: {integrity: sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==} - engines: {node: '>=0.10.0'} - - readable-stream@2.3.7: - resolution: {integrity: sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==} - - readable-stream@3.6.0: - resolution: {integrity: sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==} - engines: {node: '>= 6'} - - readdirp@3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} - - reading-time@1.5.0: - resolution: {integrity: sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg==} - - rechoir@0.6.2: - resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} - engines: {node: '>= 0.10'} - - recursive-readdir@2.2.3: - resolution: {integrity: sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==} - engines: {node: '>=6.0.0'} - - reflect.getprototypeof@1.0.10: - resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} - engines: {node: '>= 0.4'} - - regenerate-unicode-properties@10.1.0: - resolution: {integrity: sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==} - engines: {node: '>=4'} - - regenerate@1.4.2: - resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} - - regenerator-runtime@0.14.1: - resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} - - regenerator-transform@0.15.0: - resolution: {integrity: sha512-LsrGtPmbYg19bcPHwdtmXwbW+TqNvtY4riE3P83foeHRroMbH6/2ddFBfab3t7kbzc7v7p4wbkIecHImqt0QNg==} - - regexp.prototype.flags@1.5.4: - resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} - engines: {node: '>= 0.4'} - - regexpp@3.2.0: - resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} - engines: {node: '>=8'} - - regexpu-core@5.2.1: - resolution: {integrity: sha512-HrnlNtpvqP1Xkb28tMhBUO2EbyUHdQlsnlAhzWcwHy8WJR53UWr7/MAvqrsQKMbV4qdpv03oTMG8iIhfsPFktQ==} - engines: {node: '>=4'} - - registry-auth-token@4.2.2: - resolution: {integrity: sha512-PC5ZysNb42zpFME6D/XlIgtNGdTl8bBOCw90xQLVMpzuuubJKYDWFAEuUNc+Cn8Z8724tg2SDhDRrkVEsqfDMg==} - engines: {node: '>=6.0.0'} - - registry-url@5.1.0: - resolution: {integrity: sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==} - engines: {node: '>=8'} - - regjsgen@0.7.1: - resolution: {integrity: sha512-RAt+8H2ZEzHeYWxZ3H2z6tF18zyyOnlcdaafLrm21Bguj7uZy6ULibiAFdXEtKQY4Sy7wDTwDiOazasMLc4KPA==} - - regjsparser@0.9.1: - resolution: {integrity: sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==} - hasBin: true - - rehype-katex@4.0.0: - resolution: {integrity: sha512-0mgBqYugQyIW0eUl6RDOZ28Cat2YzrnWGaYgKCMQnJw6ClmKgLqXBnkDAPGh2mwxvkkKwQOUMUpSLpA5rt7rzA==} - - rehype-parse@7.0.1: - resolution: {integrity: sha512-fOiR9a9xH+Le19i4fGzIEowAbwG7idy2Jzs4mOrFWBSJ0sNUgy0ev871dwWnbOo371SjgjG4pwzrbgSVrKxecw==} - - relateurl@0.2.7: - resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==} - engines: {node: '>= 0.10'} - - remark-emoji@2.2.0: - resolution: {integrity: sha512-P3cj9s5ggsUvWw5fS2uzCHJMGuXYRb0NnZqYlNecewXt8QBU9n5vW3DUUKOhepS8F9CwdMx9B8a3i7pqFWAI5w==} - - remark-footnotes@2.0.0: - resolution: {integrity: sha512-3Clt8ZMH75Ayjp9q4CorNeyjwIxHFcTkaektplKGl2A1jNGEUey8cKL0ZC5vJwfcD5GFGsNLImLG/NGzWIzoMQ==} - - remark-math@3.0.1: - resolution: {integrity: sha512-epT77R/HK0x7NqrWHdSV75uNLwn8g9qTyMqCRCDujL0vj/6T6+yhdrR7mjELWtkse+Fw02kijAaBuVcHBor1+Q==} - - remark-mdx@1.6.22: - resolution: {integrity: sha512-phMHBJgeV76uyFkH4rvzCftLfKCr2RZuF+/gmVcaKrpsihyzmhXjA0BEMDaPTXG5y8qZOKPVo83NAOX01LPnOQ==} - - remark-parse@8.0.3: - resolution: {integrity: sha512-E1K9+QLGgggHxCQtLt++uXltxEprmWzNfg+MxpfHsZlrddKzZ/hZyWHDbK3/Ap8HJQqYJRXP+jHczdL6q6i85Q==} - - remark-squeeze-paragraphs@4.0.0: - resolution: {integrity: sha512-8qRqmL9F4nuLPIgl92XUuxI3pFxize+F1H0e/W3llTk0UsjJaj01+RrirkMw7P21RKe4X6goQhYRSvNWX+70Rw==} - - renderkid@3.0.0: - resolution: {integrity: sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==} - - repeat-string@1.6.1: - resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} - engines: {node: '>=0.10'} - - require-from-string@2.0.2: - resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} - engines: {node: '>=0.10.0'} - - require-like@0.1.2: - resolution: {integrity: sha512-oyrU88skkMtDdauHDuKVrgR+zuItqr6/c//FXzvmxRGMexSDc6hNvJInGW3LL46n+8b50RykrvwSUIIQH2LQ5A==} - - requires-port@1.0.0: - resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} - - resolve-alpn@1.2.1: - resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} - - resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - - resolve-pathname@3.0.0: - resolution: {integrity: sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==} - - resolve@1.22.1: - resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==} - hasBin: true - - resolve@2.0.0-next.5: - resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} - hasBin: true - - responselike@2.0.1: - resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} - - retry@0.13.1: - resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} - engines: {node: '>= 4'} - - reusify@1.0.4: - resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - - rimraf@3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - deprecated: Rimraf versions prior to v4 are no longer supported - hasBin: true - - rtl-detect@1.0.4: - resolution: {integrity: sha512-EBR4I2VDSSYr7PkBmFy04uhycIpDKp+21p/jARYXlCSjQksTBQcJ0HFUPOO79EPPH5JS6VAhiIQbycf0O3JAxQ==} - - rtlcss@3.5.0: - resolution: {integrity: sha512-wzgMaMFHQTnyi9YOwsx9LjOxYXJPzS8sYnFaKm6R5ysvTkwzHiB0vxnbHwchHQT65PTdBjDG21/kQBWI7q9O7A==} - hasBin: true - - run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - - rxjs@7.8.2: - resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} - - safe-array-concat@1.1.3: - resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} - engines: {node: '>=0.4'} - - safe-buffer@5.1.2: - resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} - - safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - - safe-push-apply@1.0.0: - resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} - engines: {node: '>= 0.4'} - - safe-regex-test@1.1.0: - resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} - engines: {node: '>= 0.4'} - - safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - - sax@1.4.1: - resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} - - scheduler@0.19.1: - resolution: {integrity: sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==} - - schema-utils@2.7.0: - resolution: {integrity: sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==} - engines: {node: '>= 8.9.0'} - - schema-utils@2.7.1: - resolution: {integrity: sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==} - engines: {node: '>= 8.9.0'} - - schema-utils@3.1.1: - resolution: {integrity: sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==} - engines: {node: '>= 10.13.0'} - - schema-utils@4.3.3: - resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} - engines: {node: '>= 10.13.0'} - - section-matter@1.0.0: - resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} - engines: {node: '>=4'} - - select-hose@2.0.0: - resolution: {integrity: sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==} - - selfsigned@2.1.1: - resolution: {integrity: sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ==} - engines: {node: '>=10'} - - semver-diff@3.1.1: - resolution: {integrity: sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==} - engines: {node: '>=8'} - - semver@5.7.2: - resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} - hasBin: true - - semver@6.3.1: - resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} - hasBin: true - - semver@7.7.1: - resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} - engines: {node: '>=10'} - hasBin: true - - send@0.19.0: - resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} - engines: {node: '>= 0.8.0'} - - serialize-javascript@6.0.2: - resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} - - serve-handler@6.1.6: - resolution: {integrity: sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==} - - serve-index@1.9.1: - resolution: {integrity: sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==} - engines: {node: '>= 0.8.0'} - - serve-static@1.16.2: - resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} - engines: {node: '>= 0.8.0'} - - set-function-length@1.2.2: - resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} - engines: {node: '>= 0.4'} - - set-function-name@2.0.2: - resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} - engines: {node: '>= 0.4'} - - set-proto@1.0.0: - resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} - engines: {node: '>= 0.4'} - - setimmediate@1.0.5: - resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} - - setprototypeof@1.1.0: - resolution: {integrity: sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==} - - setprototypeof@1.2.0: - resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - - shallow-clone@3.0.1: - resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} - engines: {node: '>=8'} - - shallowequal@1.1.0: - resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} - - shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - - shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - - shell-quote@1.7.4: - resolution: {integrity: sha512-8o/QEhSSRb1a5i7TFR0iM4G16Z0vYB2OQVs4G3aAFXjn3T6yEx8AZxy1PgDF7I00LZHYA3WxaSYIf5e5sAX8Rw==} - - shelljs@0.8.5: - resolution: {integrity: sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==} - engines: {node: '>=4'} - hasBin: true - - side-channel-list@1.0.0: - resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} - engines: {node: '>= 0.4'} - - side-channel-map@1.0.1: - resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} - engines: {node: '>= 0.4'} - - side-channel-weakmap@1.0.2: - resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} - engines: {node: '>= 0.4'} - - side-channel@1.1.0: - resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} - engines: {node: '>= 0.4'} - - signal-exit@3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - - sirv@1.0.19: - resolution: {integrity: sha512-JuLThK3TnZG1TAKDwNIqNq6QA2afLOCcm+iE8D1Kj3GA40pSPsxQjjJl0J8X3tsR7T+CP1GavpzLwYkgVLWrZQ==} - engines: {node: '>= 10'} - - sisteransi@1.0.5: - resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} - - sitemap@7.1.2: - resolution: {integrity: sha512-ARCqzHJ0p4gWt+j7NlU5eDlIO9+Rkr/JhPFZKKQ1l5GCus7rJH4UdrlVAh0xC/gDS/Qir2UMxqYNHtsKr2rpCw==} - engines: {node: '>=12.0.0', npm: '>=5.6.0'} - hasBin: true - - slash@3.0.0: - resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} - engines: {node: '>=8'} - - slash@4.0.0: - resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==} - engines: {node: '>=12'} - - slice-ansi@4.0.0: - resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} - engines: {node: '>=10'} - - sockjs@0.3.24: - resolution: {integrity: sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==} - - sort-css-media-queries@2.1.0: - resolution: {integrity: sha512-IeWvo8NkNiY2vVYdPa27MCQiR0MN0M80johAYFVxWWXQ44KU84WNxjslwBHmc/7ZL2ccwkM7/e6S5aiKZXm7jA==} - engines: {node: '>= 6.3.0'} - - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} - - source-map-support@0.5.21: - resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} - - source-map@0.5.7: - resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} - engines: {node: '>=0.10.0'} - - source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} - - space-separated-tokens@1.1.5: - resolution: {integrity: sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==} - - spdy-transport@3.0.0: - resolution: {integrity: sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==} - - spdy@4.0.2: - resolution: {integrity: sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==} - engines: {node: '>=6.0.0'} - - sprintf-js@1.0.3: - resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - - stable@0.1.8: - resolution: {integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==} - deprecated: 'Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility' - - state-toggle@1.0.3: - resolution: {integrity: sha512-d/5Z4/2iiCnHw6Xzghyhb+GcmF89bxwgXG60wjIiZaxnymbyOmI8Hk4VqHXiVVp6u2ysaskFfXg3ekCj4WNftQ==} - - statuses@1.5.0: - resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} - engines: {node: '>= 0.6'} - - statuses@2.0.1: - resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} - engines: {node: '>= 0.8'} - - std-env@3.3.0: - resolution: {integrity: sha512-cNNS+VYsXIs5gI6gJipO4qZ8YYT274JHvNnQ1/R/x8Q8mdP0qj0zoMchRXmBNPqp/0eOEhX+3g7g6Fgb7meLIQ==} - - string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - - string-width@5.1.2: - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} - engines: {node: '>=12'} - - string.prototype.matchall@4.0.12: - resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} - engines: {node: '>= 0.4'} - - string.prototype.repeat@1.0.0: - resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} - - string.prototype.trim@1.2.10: - resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} - engines: {node: '>= 0.4'} - - string.prototype.trimend@1.0.9: - resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} - engines: {node: '>= 0.4'} - - string.prototype.trimstart@1.0.8: - resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} - engines: {node: '>= 0.4'} - - string_decoder@1.1.1: - resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} - - string_decoder@1.3.0: - resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} - - stringify-object@3.3.0: - resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==} - engines: {node: '>=4'} - - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - - strip-ansi@7.0.1: - resolution: {integrity: sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==} - engines: {node: '>=12'} - - strip-bom-string@1.0.0: - resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} - engines: {node: '>=0.10.0'} - - strip-final-newline@2.0.0: - resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} - engines: {node: '>=6'} - - strip-json-comments@2.0.1: - resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} - engines: {node: '>=0.10.0'} - - strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - - style-to-object@0.3.0: - resolution: {integrity: sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA==} - - stylehacks@5.1.1: - resolution: {integrity: sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - supports-color@5.5.0: - resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} - engines: {node: '>=4'} - - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - - supports-color@8.1.1: - resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} - engines: {node: '>=10'} - - supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} - - svg-parser@2.0.4: - resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==} - - svgo@2.8.0: - resolution: {integrity: sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==} - engines: {node: '>=10.13.0'} - hasBin: true - - table@6.8.0: - resolution: {integrity: sha512-s/fitrbVeEyHKFa7mFdkuQMWlH1Wgw/yEXMt5xACT4ZpzWFluehAxRtUUQKPuWhaLAWhFcVx6w3oC8VKaUfPGA==} - engines: {node: '>=10.0.0'} - - tapable@1.1.3: - resolution: {integrity: sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==} - engines: {node: '>=6'} - - tapable@2.3.0: - resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} - engines: {node: '>=6'} - - terser-webpack-plugin@5.3.16: - resolution: {integrity: sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==} - engines: {node: '>= 10.13.0'} - peerDependencies: - '@swc/core': '*' - esbuild: '*' - uglify-js: '*' - webpack: ^5.1.0 - peerDependenciesMeta: - '@swc/core': - optional: true - esbuild: - optional: true - uglify-js: - optional: true - - terser@5.39.0: - resolution: {integrity: sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==} - engines: {node: '>=10'} - hasBin: true - - text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - - thunky@1.1.0: - resolution: {integrity: sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==} - - tiny-invariant@1.3.1: - resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==} - - tiny-warning@1.0.3: - resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} - - to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - - toidentifier@1.0.1: - resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} - engines: {node: '>=0.6'} - - totalist@1.1.0: - resolution: {integrity: sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==} - engines: {node: '>=6'} - - tr46@0.0.3: - resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - - trim-trailing-lines@1.1.4: - resolution: {integrity: sha512-rjUWSqnfTNrjbB9NQWfPMH/xRK1deHeGsHoVfpxJ++XeYXE0d6B1En37AHfw3jtfTU7dzMzZL2jjpe8Qb5gLIQ==} - - trim@0.0.3: - resolution: {integrity: sha512-h82ywcYhHK7veeelXrCScdH7HkWfbIT1D/CgYO+nmDarz3SGNssVBMws6jU16Ga60AJCRAvPV6w6RLuNerQqjg==} - deprecated: Use String.prototype.trim() instead - - trough@1.0.5: - resolution: {integrity: sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==} - - tslib@2.4.0: - resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==} - - type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} - - type-fest@0.20.2: - resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} - engines: {node: '>=10'} - - type-fest@2.19.0: - resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} - engines: {node: '>=12.20'} - - type-is@1.6.18: - resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} - engines: {node: '>= 0.6'} - - typed-array-buffer@1.0.3: - resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} - engines: {node: '>= 0.4'} - - typed-array-byte-length@1.0.3: - resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} - engines: {node: '>= 0.4'} - - typed-array-byte-offset@1.0.4: - resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} - engines: {node: '>= 0.4'} - - typed-array-length@1.0.7: - resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} - engines: {node: '>= 0.4'} - - typedarray-to-buffer@3.1.5: - resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} - - typescript@5.8.2: - resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==} - engines: {node: '>=14.17'} - hasBin: true - - ua-parser-js@1.0.40: - resolution: {integrity: sha512-z6PJ8Lml+v3ichVojCiB8toQJBuwR42ySM4ezjXIqXK3M0HczmKQ3LF4rhU55PfD99KEEXQG6yb7iOMyvYuHew==} - hasBin: true - - unbox-primitive@1.1.0: - resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} - engines: {node: '>= 0.4'} - - undici@6.21.2: - resolution: {integrity: sha512-uROZWze0R0itiAKVPsYhFov9LxrPMHLMEQFszeI2gCN6bnIIZ8twzBCJcN2LJrBBLfrP0t1FW0g+JmKVl8Vk1g==} - engines: {node: '>=18.17'} - - unherit@1.1.3: - resolution: {integrity: sha512-Ft16BJcnapDKp0+J/rqFC3Rrk6Y/Ng4nzsC028k2jdDII/rdZ7Wd3pPT/6+vIIxRagwRc9K0IUX0Ra4fKvw+WQ==} - - unicode-canonical-property-names-ecmascript@2.0.0: - resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==} - engines: {node: '>=4'} - - unicode-match-property-ecmascript@2.0.0: - resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} - engines: {node: '>=4'} - - unicode-match-property-value-ecmascript@2.0.0: - resolution: {integrity: sha512-7Yhkc0Ye+t4PNYzOGKedDhXbYIBe1XEQYQxOPyhcXNMJ0WCABqqj6ckydd6pWRZTHV4GuCPKdBAUiMc60tsKVw==} - engines: {node: '>=4'} - - unicode-property-aliases-ecmascript@2.1.0: - resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} - engines: {node: '>=4'} - - unified@9.2.0: - resolution: {integrity: sha512-vx2Z0vY+a3YoTj8+pttM3tiJHCwY5UFbYdiWrwBEbHmK8pvsPj2rtAX2BFfgXen8T39CJWblWRDT4L5WGXtDdg==} - - unified@9.2.2: - resolution: {integrity: sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ==} - - unique-string@2.0.0: - resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} - engines: {node: '>=8'} - - unist-builder@2.0.3: - resolution: {integrity: sha512-f98yt5pnlMWlzP539tPc4grGMsFaQQlP/vM396b00jngsiINumNmsY8rkXjfoi1c6QaM8nQ3vaGDuoKWbe/1Uw==} - - unist-util-find-after@3.0.0: - resolution: {integrity: sha512-ojlBqfsBftYXExNu3+hHLfJQ/X1jYY/9vdm4yZWjIbf0VuWF6CRufci1ZyoD/wV2TYMKxXUoNuoqwy+CkgzAiQ==} - - unist-util-generated@1.1.6: - resolution: {integrity: sha512-cln2Mm1/CZzN5ttGK7vkoGw+RZ8VcUH6BtGbq98DDtRGquAAOXig1mrBQYelOwMXYS8rK+vZDyyojSjp7JX+Lg==} - - unist-util-is@4.1.0: - resolution: {integrity: sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==} - - unist-util-position@3.1.0: - resolution: {integrity: sha512-w+PkwCbYSFw8vpgWD0v7zRCl1FpY3fjDSQ3/N/wNd9Ffa4gPi8+4keqt99N3XW6F99t/mUzp2xAhNmfKWp95QA==} - - unist-util-remove-position@2.0.1: - resolution: {integrity: sha512-fDZsLYIe2uT+oGFnuZmy73K6ZxOPG/Qcm+w7jbEjaFcJgbQ6cqjs/eSPzXhsmGpAsWPkqZM9pYjww5QTn3LHMA==} - - unist-util-remove@2.1.0: - resolution: {integrity: sha512-J8NYPyBm4baYLdCbjmf1bhPu45Cr1MWTm77qd9istEkzWpnN6O9tMsEbB2JhNnBCqGENRqEWomQ+He6au0B27Q==} - - unist-util-stringify-position@2.0.3: - resolution: {integrity: sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==} - - unist-util-visit-parents@3.1.1: - resolution: {integrity: sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==} - - unist-util-visit@2.0.3: - resolution: {integrity: sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==} - - universalify@2.0.0: - resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} - engines: {node: '>= 10.0.0'} - - unpipe@1.0.0: - resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} - engines: {node: '>= 0.8'} - - update-browserslist-db@1.2.3: - resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - - update-notifier@5.1.0: - resolution: {integrity: sha512-ItnICHbeMh9GqUy31hFPrD1kcuZ3rpxDZbf4KUDavXwS0bW5m7SLbDQpGX3UYr072cbrF5hFUs3r5tUsPwjfHw==} - engines: {node: '>=10'} - - uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - - url-loader@4.1.1: - resolution: {integrity: sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA==} - engines: {node: '>= 10.13.0'} - peerDependencies: - file-loader: '*' - webpack: ^4.0.0 || ^5.0.0 - peerDependenciesMeta: - file-loader: - optional: true - - use-composed-ref@1.4.0: - resolution: {integrity: sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w==} - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - - use-isomorphic-layout-effect@1.2.0: - resolution: {integrity: sha512-q6ayo8DWoPZT0VdG4u3D3uxcgONP3Mevx2i2b0434cwWBoL+aelL1DzkXI6w3PhTZzUeR2kaVlZn70iCiseP6w==} - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - - use-latest@1.3.0: - resolution: {integrity: sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ==} - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - - use-sync-external-store@1.4.0: - resolution: {integrity: sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - - util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - - utila@0.4.0: - resolution: {integrity: sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==} - - utility-types@3.10.0: - resolution: {integrity: sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==} - engines: {node: '>= 4'} - - utils-merge@1.0.1: - resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} - engines: {node: '>= 0.4.0'} - - uuid@8.3.2: - resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} - hasBin: true - - v8-compile-cache@2.3.0: - resolution: {integrity: sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==} - - value-equal@1.0.1: - resolution: {integrity: sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==} - - vary@1.1.2: - resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} - engines: {node: '>= 0.8'} - - vfile-location@3.2.0: - resolution: {integrity: sha512-aLEIZKv/oxuCDZ8lkJGhuhztf/BW4M+iHdCwglA/eWc+vtuRFJj8EtgceYFX4LRjOhCAAiNHsKGssC6onJ+jbA==} - - vfile-message@2.0.4: - resolution: {integrity: sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==} - - vfile@4.2.1: - resolution: {integrity: sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA==} - - wait-on@8.0.3: - resolution: {integrity: sha512-nQFqAFzZDeRxsu7S3C7LbuxslHhk+gnJZHyethuGKAn2IVleIbTB9I3vJSQiSR+DifUqmdzfPMoMPJfLqMF2vw==} - engines: {node: '>=12.0.0'} - hasBin: true - - watchpack@2.5.1: - resolution: {integrity: sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==} - engines: {node: '>=10.13.0'} - - wbuf@1.7.3: - resolution: {integrity: sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==} - - web-namespaces@1.1.4: - resolution: {integrity: sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw==} - - webidl-conversions@3.0.1: - resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - - webpack-bundle-analyzer@4.6.1: - resolution: {integrity: sha512-oKz9Oz9j3rUciLNfpGFjOb49/jEpXNmWdVH8Ls//zNcnLlQdTGXQQMsBbb/gR7Zl8WNLxVCq+0Hqbx3zv6twBw==} - engines: {node: '>= 10.13.0'} - hasBin: true - - webpack-dev-middleware@5.3.4: - resolution: {integrity: sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==} - engines: {node: '>= 12.13.0'} - peerDependencies: - webpack: ^4.0.0 || ^5.0.0 - - webpack-dev-server@4.11.1: - resolution: {integrity: sha512-lILVz9tAUy1zGFwieuaQtYiadImb5M3d+H+L1zDYalYoDl0cksAB1UNyuE5MMWJrG6zR1tXkCP2fitl7yoUJiw==} - engines: {node: '>= 12.13.0'} - hasBin: true - peerDependencies: - webpack: ^4.37.0 || ^5.0.0 - webpack-cli: '*' - peerDependenciesMeta: - webpack-cli: - optional: true - - webpack-merge@5.8.0: - resolution: {integrity: sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==} - engines: {node: '>=10.0.0'} - - webpack-sources@3.3.3: - resolution: {integrity: sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==} - engines: {node: '>=10.13.0'} - - webpack@5.105.0: - resolution: {integrity: sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==} - engines: {node: '>=10.13.0'} - hasBin: true - peerDependencies: - webpack-cli: '*' - peerDependenciesMeta: - webpack-cli: - optional: true - - webpackbar@5.0.2: - resolution: {integrity: sha512-BmFJo7veBDgQzfWXl/wwYXr/VFus0614qZ8i9znqcl9fnEdiVkdbi0TedLQ6xAK92HZHDJ0QmyQ0fmuZPAgCYQ==} - engines: {node: '>=12'} - peerDependencies: - webpack: 3 || 4 || 5 - - websocket-driver@0.7.4: - resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==} - engines: {node: '>=0.8.0'} - - websocket-extensions@0.1.4: - resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==} - engines: {node: '>=0.8.0'} - - whatwg-encoding@3.1.1: - resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} - engines: {node: '>=18'} - deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation - - whatwg-mimetype@4.0.0: - resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} - engines: {node: '>=18'} - - whatwg-url@5.0.0: - resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} - - which-boxed-primitive@1.1.1: - resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} - engines: {node: '>= 0.4'} - - which-builtin-type@1.2.1: - resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} - engines: {node: '>= 0.4'} - - which-collection@1.0.2: - resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} - engines: {node: '>= 0.4'} - - which-typed-array@1.1.19: - resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} - engines: {node: '>= 0.4'} - - which@1.3.1: - resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} - hasBin: true - - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - - widest-line@3.1.0: - resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==} - engines: {node: '>=8'} - - widest-line@4.0.1: - resolution: {integrity: sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==} - engines: {node: '>=12'} - - wildcard@2.0.0: - resolution: {integrity: sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==} - - word-wrap@1.2.4: - resolution: {integrity: sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==} - engines: {node: '>=0.10.0'} - - wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} - - wrap-ansi@8.0.1: - resolution: {integrity: sha512-QFF+ufAqhoYHvoHdajT/Po7KoXVBPXS2bgjIam5isfWJPfIOnQZ50JtUiVvCv/sjgacf3yRrt2ZKUZ/V4itN4g==} - engines: {node: '>=12'} - - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - - write-file-atomic@3.0.3: - resolution: {integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==} - - ws@7.5.10: - resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} - engines: {node: '>=8.3.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - - ws@8.18.1: - resolution: {integrity: sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - - xdg-basedir@4.0.0: - resolution: {integrity: sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==} - engines: {node: '>=8'} - - xml-js@1.6.11: - resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==} - hasBin: true - - xtend@4.0.2: - resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} - engines: {node: '>=0.4'} - - yaml@1.10.2: - resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} - engines: {node: '>= 6'} - - yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - - zwitch@1.0.5: - resolution: {integrity: sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==} - -snapshots: - - '@algolia/autocomplete-core@1.7.1': - dependencies: - '@algolia/autocomplete-shared': 1.7.1 - - '@algolia/autocomplete-preset-algolia@1.7.1(@algolia/client-search@4.14.2)(algoliasearch@4.14.2)': - dependencies: - '@algolia/autocomplete-shared': 1.7.1 - '@algolia/client-search': 4.14.2 - algoliasearch: 4.14.2 - - '@algolia/autocomplete-shared@1.7.1': {} - - '@algolia/cache-browser-local-storage@4.14.2': - dependencies: - '@algolia/cache-common': 4.14.2 - - '@algolia/cache-common@4.14.2': {} - - '@algolia/cache-in-memory@4.14.2': - dependencies: - '@algolia/cache-common': 4.14.2 - - '@algolia/client-account@4.14.2': - dependencies: - '@algolia/client-common': 4.14.2 - '@algolia/client-search': 4.14.2 - '@algolia/transporter': 4.14.2 - - '@algolia/client-analytics@4.14.2': - dependencies: - '@algolia/client-common': 4.14.2 - '@algolia/client-search': 4.14.2 - '@algolia/requester-common': 4.14.2 - '@algolia/transporter': 4.14.2 - - '@algolia/client-common@4.14.2': - dependencies: - '@algolia/requester-common': 4.14.2 - '@algolia/transporter': 4.14.2 - - '@algolia/client-personalization@4.14.2': - dependencies: - '@algolia/client-common': 4.14.2 - '@algolia/requester-common': 4.14.2 - '@algolia/transporter': 4.14.2 - - '@algolia/client-search@4.14.2': - dependencies: - '@algolia/client-common': 4.14.2 - '@algolia/requester-common': 4.14.2 - '@algolia/transporter': 4.14.2 - - '@algolia/events@4.0.1': {} - - '@algolia/logger-common@4.14.2': {} - - '@algolia/logger-console@4.14.2': - dependencies: - '@algolia/logger-common': 4.14.2 - - '@algolia/requester-browser-xhr@4.14.2': - dependencies: - '@algolia/requester-common': 4.14.2 - - '@algolia/requester-common@4.14.2': {} - - '@algolia/requester-node-http@4.14.2': - dependencies: - '@algolia/requester-common': 4.14.2 - - '@algolia/transporter@4.14.2': - dependencies: - '@algolia/cache-common': 4.14.2 - '@algolia/logger-common': 4.14.2 - '@algolia/requester-common': 4.14.2 - - '@ampproject/remapping@2.2.0': - dependencies: - '@jridgewell/gen-mapping': 0.1.1 - '@jridgewell/trace-mapping': 0.3.25 - - '@babel/code-frame@7.12.11': - dependencies: - '@babel/highlight': 7.22.20 - - '@babel/code-frame@7.26.2': - dependencies: - '@babel/helper-validator-identifier': 7.25.9 - js-tokens: 4.0.0 - picocolors: 1.1.1 - - '@babel/compat-data@7.19.4': {} - - '@babel/core@7.12.9': - dependencies: - '@babel/code-frame': 7.26.2 - '@babel/generator': 7.23.0 - '@babel/helper-module-transforms': 7.19.6 - '@babel/helpers': 7.27.0 - '@babel/parser': 7.27.0 - '@babel/template': 7.27.0 - '@babel/traverse': 7.23.2 - '@babel/types': 7.27.0 - convert-source-map: 1.9.0 - debug: 4.3.4 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - lodash: 4.17.21 - resolve: 1.22.1 - semver: 5.7.2 - source-map: 0.5.7 - transitivePeerDependencies: - - supports-color - - '@babel/core@7.19.6': - dependencies: - '@ampproject/remapping': 2.2.0 - '@babel/code-frame': 7.26.2 - '@babel/generator': 7.23.0 - '@babel/helper-compilation-targets': 7.19.3(@babel/core@7.19.6) - '@babel/helper-module-transforms': 7.19.6 - '@babel/helpers': 7.27.0 - '@babel/parser': 7.27.0 - '@babel/template': 7.27.0 - '@babel/traverse': 7.23.2 - '@babel/types': 7.27.0 - convert-source-map: 1.9.0 - debug: 4.3.4 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - '@babel/generator@7.23.0': - dependencies: - '@babel/types': 7.27.0 - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 - jsesc: 2.5.2 - - '@babel/helper-annotate-as-pure@7.18.6': - dependencies: - '@babel/types': 7.27.0 - - '@babel/helper-builder-binary-assignment-operator-visitor@7.18.9': - dependencies: - '@babel/helper-explode-assignable-expression': 7.18.6 - '@babel/types': 7.27.0 - - '@babel/helper-compilation-targets@7.19.3(@babel/core@7.19.6)': - dependencies: - '@babel/compat-data': 7.19.4 - '@babel/core': 7.19.6 - '@babel/helper-validator-option': 7.18.6 - browserslist: 4.28.1 - semver: 6.3.1 - - '@babel/helper-create-class-features-plugin@7.19.0(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-function-name': 7.23.0 - '@babel/helper-member-expression-to-functions': 7.18.9 - '@babel/helper-optimise-call-expression': 7.18.6 - '@babel/helper-replace-supers': 7.19.1 - '@babel/helper-split-export-declaration': 7.22.6 - transitivePeerDependencies: - - supports-color - - '@babel/helper-create-regexp-features-plugin@7.19.0(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-annotate-as-pure': 7.18.6 - regexpu-core: 5.2.1 - - '@babel/helper-define-polyfill-provider@0.3.3(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-compilation-targets': 7.19.3(@babel/core@7.19.6) - '@babel/helper-plugin-utils': 7.20.2 - debug: 4.3.4 - lodash.debounce: 4.0.8 - resolve: 1.22.1 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - '@babel/helper-environment-visitor@7.22.20': {} - - '@babel/helper-explode-assignable-expression@7.18.6': - dependencies: - '@babel/types': 7.27.0 - - '@babel/helper-function-name@7.23.0': - dependencies: - '@babel/template': 7.27.0 - '@babel/types': 7.27.0 - - '@babel/helper-hoist-variables@7.22.5': - dependencies: - '@babel/types': 7.27.0 - - '@babel/helper-member-expression-to-functions@7.18.9': - dependencies: - '@babel/types': 7.27.0 - - '@babel/helper-module-imports@7.18.6': - dependencies: - '@babel/types': 7.27.0 - - '@babel/helper-module-transforms@7.19.6': - dependencies: - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-module-imports': 7.18.6 - '@babel/helper-simple-access': 7.19.4 - '@babel/helper-split-export-declaration': 7.22.6 - '@babel/helper-validator-identifier': 7.25.9 - '@babel/template': 7.27.0 - '@babel/traverse': 7.23.2 - '@babel/types': 7.27.0 - transitivePeerDependencies: - - supports-color - - '@babel/helper-optimise-call-expression@7.18.6': - dependencies: - '@babel/types': 7.27.0 - - '@babel/helper-plugin-utils@7.10.4': {} - - '@babel/helper-plugin-utils@7.20.2': {} - - '@babel/helper-remap-async-to-generator@7.18.9(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-wrap-function': 7.19.0 - '@babel/types': 7.27.0 - transitivePeerDependencies: - - supports-color - - '@babel/helper-replace-supers@7.19.1': - dependencies: - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-member-expression-to-functions': 7.18.9 - '@babel/helper-optimise-call-expression': 7.18.6 - '@babel/traverse': 7.23.2 - '@babel/types': 7.27.0 - transitivePeerDependencies: - - supports-color - - '@babel/helper-simple-access@7.19.4': - dependencies: - '@babel/types': 7.27.0 - - '@babel/helper-skip-transparent-expression-wrappers@7.18.9': - dependencies: - '@babel/types': 7.27.0 - - '@babel/helper-split-export-declaration@7.22.6': - dependencies: - '@babel/types': 7.27.0 - - '@babel/helper-string-parser@7.25.9': {} - - '@babel/helper-validator-identifier@7.25.9': {} - - '@babel/helper-validator-option@7.18.6': {} - - '@babel/helper-wrap-function@7.19.0': - dependencies: - '@babel/helper-function-name': 7.23.0 - '@babel/template': 7.27.0 - '@babel/traverse': 7.23.2 - '@babel/types': 7.27.0 - transitivePeerDependencies: - - supports-color - - '@babel/helpers@7.27.0': - dependencies: - '@babel/template': 7.27.0 - '@babel/types': 7.27.0 - - '@babel/highlight@7.22.20': - dependencies: - '@babel/helper-validator-identifier': 7.25.9 - chalk: 2.4.2 - js-tokens: 4.0.0 - - '@babel/parser@7.27.0': - dependencies: - '@babel/types': 7.27.0 - - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.18.9(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-skip-transparent-expression-wrappers': 7.18.9 - '@babel/plugin-proposal-optional-chaining': 7.18.9(@babel/core@7.19.6) - - '@babel/plugin-proposal-async-generator-functions@7.19.1(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-remap-async-to-generator': 7.18.9(@babel/core@7.19.6) - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.19.6) - transitivePeerDependencies: - - supports-color - - '@babel/plugin-proposal-class-properties@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-create-class-features-plugin': 7.19.0(@babel/core@7.19.6) - '@babel/helper-plugin-utils': 7.20.2 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-proposal-class-static-block@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-create-class-features-plugin': 7.19.0(@babel/core@7.19.6) - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.19.6) - transitivePeerDependencies: - - supports-color - - '@babel/plugin-proposal-dynamic-import@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.19.6) - - '@babel/plugin-proposal-export-namespace-from@7.18.9(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.19.6) - - '@babel/plugin-proposal-json-strings@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.19.6) - - '@babel/plugin-proposal-logical-assignment-operators@7.18.9(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.19.6) - - '@babel/plugin-proposal-nullish-coalescing-operator@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.19.6) - - '@babel/plugin-proposal-numeric-separator@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.19.6) - - '@babel/plugin-proposal-object-rest-spread@7.12.1(@babel/core@7.12.9)': - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.12.9) - '@babel/plugin-transform-parameters': 7.18.8(@babel/core@7.12.9) - - '@babel/plugin-proposal-object-rest-spread@7.19.4(@babel/core@7.19.6)': - dependencies: - '@babel/compat-data': 7.19.4 - '@babel/core': 7.19.6 - '@babel/helper-compilation-targets': 7.19.3(@babel/core@7.19.6) - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.19.6) - '@babel/plugin-transform-parameters': 7.18.8(@babel/core@7.19.6) - - '@babel/plugin-proposal-optional-catch-binding@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.19.6) - - '@babel/plugin-proposal-optional-chaining@7.18.9(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-skip-transparent-expression-wrappers': 7.18.9 - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.19.6) - - '@babel/plugin-proposal-private-methods@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-create-class-features-plugin': 7.19.0(@babel/core@7.19.6) - '@babel/helper-plugin-utils': 7.20.2 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-proposal-private-property-in-object@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-create-class-features-plugin': 7.19.0(@babel/core@7.19.6) - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.19.6) - transitivePeerDependencies: - - supports-color - - '@babel/plugin-proposal-unicode-property-regex@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-create-regexp-features-plugin': 7.19.0(@babel/core@7.19.6) - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-import-assertions@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-jsx@7.12.1(@babel/core@7.12.9)': - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-jsx@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.12.9)': - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-typescript@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-arrow-functions@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-async-to-generator@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-module-imports': 7.18.6 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-remap-async-to-generator': 7.18.9(@babel/core@7.19.6) - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-block-scoped-functions@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-block-scoping@7.19.4(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-classes@7.19.0(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-compilation-targets': 7.19.3(@babel/core@7.19.6) - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-function-name': 7.23.0 - '@babel/helper-optimise-call-expression': 7.18.6 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-replace-supers': 7.19.1 - '@babel/helper-split-export-declaration': 7.22.6 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-computed-properties@7.18.9(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-destructuring@7.19.4(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-dotall-regex@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-create-regexp-features-plugin': 7.19.0(@babel/core@7.19.6) - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-duplicate-keys@7.18.9(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-exponentiation-operator@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-builder-binary-assignment-operator-visitor': 7.18.9 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-for-of@7.18.8(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-function-name@7.18.9(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-compilation-targets': 7.19.3(@babel/core@7.19.6) - '@babel/helper-function-name': 7.23.0 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-literals@7.18.9(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-member-expression-literals@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-modules-amd@7.19.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-module-transforms': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-modules-commonjs@7.19.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-module-transforms': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-simple-access': 7.19.4 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-modules-systemjs@7.19.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-hoist-variables': 7.22.5 - '@babel/helper-module-transforms': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-validator-identifier': 7.25.9 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-modules-umd@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-module-transforms': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-named-capturing-groups-regex@7.19.1(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-create-regexp-features-plugin': 7.19.0(@babel/core@7.19.6) - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-new-target@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-object-super@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-replace-supers': 7.19.1 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-parameters@7.18.8(@babel/core@7.12.9)': - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-parameters@7.18.8(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-property-literals@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-react-constant-elements@7.20.2(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-react-display-name@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-react-jsx-development@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/plugin-transform-react-jsx': 7.19.0(@babel/core@7.19.6) - - '@babel/plugin-transform-react-jsx@7.19.0(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-module-imports': 7.18.6 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-jsx': 7.18.6(@babel/core@7.19.6) - '@babel/types': 7.27.0 - - '@babel/plugin-transform-react-pure-annotations@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-regenerator@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - regenerator-transform: 0.15.0 - - '@babel/plugin-transform-reserved-words@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-runtime@7.19.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-module-imports': 7.18.6 - '@babel/helper-plugin-utils': 7.20.2 - babel-plugin-polyfill-corejs2: 0.3.3(@babel/core@7.19.6) - babel-plugin-polyfill-corejs3: 0.6.0(@babel/core@7.19.6) - babel-plugin-polyfill-regenerator: 0.4.1(@babel/core@7.19.6) - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-shorthand-properties@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-spread@7.19.0(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-skip-transparent-expression-wrappers': 7.18.9 - - '@babel/plugin-transform-sticky-regex@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-template-literals@7.18.9(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-typeof-symbol@7.18.9(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-typescript@7.19.3(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-create-class-features-plugin': 7.19.0(@babel/core@7.19.6) - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-typescript': 7.18.6(@babel/core@7.19.6) - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-unicode-escapes@7.18.10(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-unicode-regex@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-create-regexp-features-plugin': 7.19.0(@babel/core@7.19.6) - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/preset-env@7.19.4(@babel/core@7.19.6)': - dependencies: - '@babel/compat-data': 7.19.4 - '@babel/core': 7.19.6 - '@babel/helper-compilation-targets': 7.19.3(@babel/core@7.19.6) - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-validator-option': 7.18.6 - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.18.9(@babel/core@7.19.6) - '@babel/plugin-proposal-async-generator-functions': 7.19.1(@babel/core@7.19.6) - '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-proposal-class-static-block': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-proposal-dynamic-import': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-proposal-export-namespace-from': 7.18.9(@babel/core@7.19.6) - '@babel/plugin-proposal-json-strings': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-proposal-logical-assignment-operators': 7.18.9(@babel/core@7.19.6) - '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-proposal-numeric-separator': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-proposal-object-rest-spread': 7.19.4(@babel/core@7.19.6) - '@babel/plugin-proposal-optional-catch-binding': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-proposal-optional-chaining': 7.18.9(@babel/core@7.19.6) - '@babel/plugin-proposal-private-methods': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-proposal-private-property-in-object': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-proposal-unicode-property-regex': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.19.6) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.19.6) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.19.6) - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.19.6) - '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.19.6) - '@babel/plugin-syntax-import-assertions': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.19.6) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.19.6) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.19.6) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.19.6) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.19.6) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.19.6) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.19.6) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.19.6) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.19.6) - '@babel/plugin-transform-arrow-functions': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-transform-async-to-generator': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-transform-block-scoped-functions': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-transform-block-scoping': 7.19.4(@babel/core@7.19.6) - '@babel/plugin-transform-classes': 7.19.0(@babel/core@7.19.6) - '@babel/plugin-transform-computed-properties': 7.18.9(@babel/core@7.19.6) - '@babel/plugin-transform-destructuring': 7.19.4(@babel/core@7.19.6) - '@babel/plugin-transform-dotall-regex': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-transform-duplicate-keys': 7.18.9(@babel/core@7.19.6) - '@babel/plugin-transform-exponentiation-operator': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-transform-for-of': 7.18.8(@babel/core@7.19.6) - '@babel/plugin-transform-function-name': 7.18.9(@babel/core@7.19.6) - '@babel/plugin-transform-literals': 7.18.9(@babel/core@7.19.6) - '@babel/plugin-transform-member-expression-literals': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-transform-modules-amd': 7.19.6(@babel/core@7.19.6) - '@babel/plugin-transform-modules-commonjs': 7.19.6(@babel/core@7.19.6) - '@babel/plugin-transform-modules-systemjs': 7.19.6(@babel/core@7.19.6) - '@babel/plugin-transform-modules-umd': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-transform-named-capturing-groups-regex': 7.19.1(@babel/core@7.19.6) - '@babel/plugin-transform-new-target': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-transform-object-super': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-transform-parameters': 7.18.8(@babel/core@7.19.6) - '@babel/plugin-transform-property-literals': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-transform-regenerator': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-transform-reserved-words': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-transform-shorthand-properties': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-transform-spread': 7.19.0(@babel/core@7.19.6) - '@babel/plugin-transform-sticky-regex': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-transform-template-literals': 7.18.9(@babel/core@7.19.6) - '@babel/plugin-transform-typeof-symbol': 7.18.9(@babel/core@7.19.6) - '@babel/plugin-transform-unicode-escapes': 7.18.10(@babel/core@7.19.6) - '@babel/plugin-transform-unicode-regex': 7.18.6(@babel/core@7.19.6) - '@babel/preset-modules': 0.1.5(@babel/core@7.19.6) - '@babel/types': 7.27.0 - babel-plugin-polyfill-corejs2: 0.3.3(@babel/core@7.19.6) - babel-plugin-polyfill-corejs3: 0.6.0(@babel/core@7.19.6) - babel-plugin-polyfill-regenerator: 0.4.1(@babel/core@7.19.6) - core-js-compat: 3.25.5 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - '@babel/preset-modules@0.1.5(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-proposal-unicode-property-regex': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-transform-dotall-regex': 7.18.6(@babel/core@7.19.6) - '@babel/types': 7.27.0 - esutils: 2.0.3 - - '@babel/preset-react@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-validator-option': 7.18.6 - '@babel/plugin-transform-react-display-name': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-transform-react-jsx': 7.19.0(@babel/core@7.19.6) - '@babel/plugin-transform-react-jsx-development': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-transform-react-pure-annotations': 7.18.6(@babel/core@7.19.6) - - '@babel/preset-typescript@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-validator-option': 7.18.6 - '@babel/plugin-transform-typescript': 7.19.3(@babel/core@7.19.6) - transitivePeerDependencies: - - supports-color - - '@babel/runtime-corejs3@7.27.0': - dependencies: - core-js-pure: 3.41.0 - regenerator-runtime: 0.14.1 - - '@babel/runtime@7.27.0': - dependencies: - regenerator-runtime: 0.14.1 - - '@babel/template@7.27.0': - dependencies: - '@babel/code-frame': 7.26.2 - '@babel/parser': 7.27.0 - '@babel/types': 7.27.0 - - '@babel/traverse@7.23.2': - dependencies: - '@babel/code-frame': 7.26.2 - '@babel/generator': 7.23.0 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-function-name': 7.23.0 - '@babel/helper-hoist-variables': 7.22.5 - '@babel/helper-split-export-declaration': 7.22.6 - '@babel/parser': 7.27.0 - '@babel/types': 7.27.0 - debug: 4.3.4 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color - - '@babel/types@7.27.0': - dependencies: - '@babel/helper-string-parser': 7.25.9 - '@babel/helper-validator-identifier': 7.25.9 - - '@colors/colors@1.5.0': - optional: true - - '@docsearch/css@3.2.2': {} - - '@docsearch/react@3.2.2(@algolia/client-search@4.14.2)(@types/react@18.0.26)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)': - dependencies: - '@algolia/autocomplete-core': 1.7.1 - '@algolia/autocomplete-preset-algolia': 1.7.1(@algolia/client-search@4.14.2)(algoliasearch@4.14.2) - '@docsearch/css': 3.2.2 - algoliasearch: 4.14.2 - optionalDependencies: - '@types/react': 18.0.26 - react: 16.14.0 - react-dom: 16.14.0(react@16.14.0) - transitivePeerDependencies: - - '@algolia/client-search' - - '@docusaurus/core@2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2)': - dependencies: - '@babel/core': 7.19.6 - '@babel/generator': 7.23.0 - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.19.6) - '@babel/plugin-transform-runtime': 7.19.6(@babel/core@7.19.6) - '@babel/preset-env': 7.19.4(@babel/core@7.19.6) - '@babel/preset-react': 7.18.6(@babel/core@7.19.6) - '@babel/preset-typescript': 7.18.6(@babel/core@7.19.6) - '@babel/runtime': 7.27.0 - '@babel/runtime-corejs3': 7.27.0 - '@babel/traverse': 7.23.2 - '@docusaurus/cssnano-preset': 2.4.3 - '@docusaurus/logger': 2.4.3 - '@docusaurus/mdx-loader': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - '@docusaurus/react-loadable': 5.5.2(react@16.14.0) - '@docusaurus/utils': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - '@docusaurus/utils-common': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - '@docusaurus/utils-validation': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - '@slorber/static-site-generator-webpack-plugin': 4.0.7 - '@svgr/webpack': 6.5.1 - autoprefixer: 10.4.12(postcss@8.5.3) - babel-loader: 8.2.5(@babel/core@7.19.6)(webpack@5.105.0) - babel-plugin-dynamic-import-node: 2.3.3 - boxen: 6.2.1 - chalk: 4.1.2 - chokidar: 3.5.3 - clean-css: 5.3.1 - cli-table3: 0.6.3 - combine-promises: 1.1.0 - commander: 5.1.0 - copy-webpack-plugin: 11.0.0(webpack@5.105.0) - core-js: 3.25.5 - css-loader: 6.7.3(webpack@5.105.0) - css-minimizer-webpack-plugin: 4.2.2(clean-css@5.3.1)(webpack@5.105.0) - cssnano: 5.1.13(postcss@8.5.3) - del: 6.1.1 - detect-port: 1.5.1 - escape-html: 1.0.3 - eta: 2.2.0 - file-loader: 6.2.0(webpack@5.105.0) - fs-extra: 10.1.0 - html-minifier-terser: 6.1.0 - html-tags: 3.2.0 - html-webpack-plugin: 5.5.0(webpack@5.105.0) - import-fresh: 3.3.0 - leven: 3.1.0 - lodash: 4.17.21 - mini-css-extract-plugin: 2.7.2(webpack@5.105.0) - postcss: 8.5.3 - postcss-loader: 7.0.2(postcss@8.5.3)(webpack@5.105.0) - prompts: 2.4.2 - react: 16.14.0 - react-dev-utils: 12.0.1(eslint@7.32.0)(typescript@5.8.2)(webpack@5.105.0) - react-dom: 16.14.0(react@16.14.0) - react-helmet-async: 1.3.0(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - react-loadable: '@docusaurus/react-loadable@5.5.2(react@16.14.0)' - react-loadable-ssr-addon-v5-slorber: 1.0.1(@docusaurus/react-loadable@5.5.2(react@16.14.0))(webpack@5.105.0) - react-router: 5.3.4(react@16.14.0) - react-router-config: 5.1.1(react-router@5.3.4(react@16.14.0))(react@16.14.0) - react-router-dom: 5.3.4(react@16.14.0) - rtl-detect: 1.0.4 - semver: 7.7.1 - serve-handler: 6.1.6 - shelljs: 0.8.5 - terser-webpack-plugin: 5.3.16(webpack@5.105.0) - tslib: 2.4.0 - update-notifier: 5.1.0 - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.105.0))(webpack@5.105.0) - wait-on: 8.0.3 - webpack: 5.105.0 - webpack-bundle-analyzer: 4.6.1 - webpack-dev-server: 4.11.1(webpack@5.105.0) - webpack-merge: 5.8.0 - webpackbar: 5.0.2(webpack@5.105.0) - transitivePeerDependencies: - - '@docusaurus/types' - - '@parcel/css' - - '@swc/core' - - '@swc/css' - - bufferutil - - csso - - debug - - esbuild - - eslint - - lightningcss - - supports-color - - typescript - - uglify-js - - utf-8-validate - - vue-template-compiler - - webpack-cli - - '@docusaurus/cssnano-preset@2.4.3': - dependencies: - cssnano-preset-advanced: 5.3.9(postcss@8.5.3) - postcss: 8.5.3 - postcss-sort-media-queries: 4.3.0(postcss@8.5.3) - tslib: 2.4.0 - - '@docusaurus/logger@2.4.3': - dependencies: - chalk: 4.1.2 - tslib: 2.4.0 - - '@docusaurus/mdx-loader@2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(react-dom@16.14.0(react@16.14.0))(react@16.14.0)': - dependencies: - '@babel/parser': 7.27.0 - '@babel/traverse': 7.23.2 - '@docusaurus/logger': 2.4.3 - '@docusaurus/utils': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - '@mdx-js/mdx': 1.6.22 - escape-html: 1.0.3 - file-loader: 6.2.0(webpack@5.105.0) - fs-extra: 10.1.0 - image-size: 1.0.2 - mdast-util-to-string: 2.0.0 - react: 16.14.0 - react-dom: 16.14.0(react@16.14.0) - remark-emoji: 2.2.0 - stringify-object: 3.3.0 - tslib: 2.4.0 - unified: 9.2.2 - unist-util-visit: 2.0.3 - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.105.0))(webpack@5.105.0) - webpack: 5.105.0 - transitivePeerDependencies: - - '@docusaurus/types' - - '@swc/core' - - esbuild - - supports-color - - uglify-js - - webpack-cli - - '@docusaurus/module-type-aliases@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)': - dependencies: - '@docusaurus/react-loadable': 5.5.2(react@16.14.0) - '@docusaurus/types': 2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - '@types/history': 4.7.11 - '@types/react': 18.0.26 - '@types/react-router-config': 5.0.11 - '@types/react-router-dom': 5.3.3 - react: 16.14.0 - react-dom: 16.14.0(react@16.14.0) - react-helmet-async: 1.3.0(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - react-loadable: '@docusaurus/react-loadable@5.5.2(react@16.14.0)' - transitivePeerDependencies: - - '@swc/core' - - esbuild - - uglify-js - - webpack-cli - - '@docusaurus/plugin-content-blog@2.4.3(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2)': - dependencies: - '@docusaurus/core': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/logger': 2.4.3 - '@docusaurus/mdx-loader': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - '@docusaurus/types': 2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - '@docusaurus/utils': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - '@docusaurus/utils-common': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - '@docusaurus/utils-validation': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - cheerio: 1.0.0 - feed: 4.2.2 - fs-extra: 10.1.0 - lodash: 4.17.21 - react: 16.14.0 - react-dom: 16.14.0(react@16.14.0) - reading-time: 1.5.0 - tslib: 2.4.0 - unist-util-visit: 2.0.3 - utility-types: 3.10.0 - webpack: 5.105.0 - transitivePeerDependencies: - - '@parcel/css' - - '@swc/core' - - '@swc/css' - - bufferutil - - csso - - debug - - esbuild - - eslint - - lightningcss - - supports-color - - typescript - - uglify-js - - utf-8-validate - - vue-template-compiler - - webpack-cli - - '@docusaurus/plugin-content-docs@2.4.3(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2)': - dependencies: - '@docusaurus/core': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/logger': 2.4.3 - '@docusaurus/mdx-loader': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - '@docusaurus/module-type-aliases': 2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - '@docusaurus/types': 2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - '@docusaurus/utils': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - '@docusaurus/utils-validation': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - '@types/react-router-config': 5.0.11 - combine-promises: 1.1.0 - fs-extra: 10.1.0 - import-fresh: 3.3.0 - js-yaml: 4.1.0 - lodash: 4.17.21 - react: 16.14.0 - react-dom: 16.14.0(react@16.14.0) - tslib: 2.4.0 - utility-types: 3.10.0 - webpack: 5.105.0 - transitivePeerDependencies: - - '@parcel/css' - - '@swc/core' - - '@swc/css' - - bufferutil - - csso - - debug - - esbuild - - eslint - - lightningcss - - supports-color - - typescript - - uglify-js - - utf-8-validate - - vue-template-compiler - - webpack-cli - - '@docusaurus/plugin-content-pages@2.4.3(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2)': - dependencies: - '@docusaurus/core': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/mdx-loader': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - '@docusaurus/types': 2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - '@docusaurus/utils': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - '@docusaurus/utils-validation': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - fs-extra: 10.1.0 - react: 16.14.0 - react-dom: 16.14.0(react@16.14.0) - tslib: 2.4.0 - webpack: 5.105.0 - transitivePeerDependencies: - - '@parcel/css' - - '@swc/core' - - '@swc/css' - - bufferutil - - csso - - debug - - esbuild - - eslint - - lightningcss - - supports-color - - typescript - - uglify-js - - utf-8-validate - - vue-template-compiler - - webpack-cli - - '@docusaurus/plugin-debug@2.4.3(@types/react@18.0.26)(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2)': - dependencies: - '@docusaurus/core': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/types': 2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - '@docusaurus/utils': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - fs-extra: 10.1.0 - react: 16.14.0 - react-dom: 16.14.0(react@16.14.0) - react-json-view: 1.21.3(@types/react@18.0.26)(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - tslib: 2.4.0 - transitivePeerDependencies: - - '@parcel/css' - - '@swc/core' - - '@swc/css' - - '@types/react' - - bufferutil - - csso - - debug - - encoding - - esbuild - - eslint - - lightningcss - - supports-color - - typescript - - uglify-js - - utf-8-validate - - vue-template-compiler - - webpack-cli - - '@docusaurus/plugin-google-analytics@2.4.3(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2)': - dependencies: - '@docusaurus/core': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/types': 2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - '@docusaurus/utils-validation': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - react: 16.14.0 - react-dom: 16.14.0(react@16.14.0) - tslib: 2.4.0 - transitivePeerDependencies: - - '@parcel/css' - - '@swc/core' - - '@swc/css' - - bufferutil - - csso - - debug - - esbuild - - eslint - - lightningcss - - supports-color - - typescript - - uglify-js - - utf-8-validate - - vue-template-compiler - - webpack-cli - - '@docusaurus/plugin-google-gtag@2.4.3(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2)': - dependencies: - '@docusaurus/core': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/types': 2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - '@docusaurus/utils-validation': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - react: 16.14.0 - react-dom: 16.14.0(react@16.14.0) - tslib: 2.4.0 - transitivePeerDependencies: - - '@parcel/css' - - '@swc/core' - - '@swc/css' - - bufferutil - - csso - - debug - - esbuild - - eslint - - lightningcss - - supports-color - - typescript - - uglify-js - - utf-8-validate - - vue-template-compiler - - webpack-cli - - '@docusaurus/plugin-google-tag-manager@2.4.3(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2)': - dependencies: - '@docusaurus/core': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/types': 2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - '@docusaurus/utils-validation': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - react: 16.14.0 - react-dom: 16.14.0(react@16.14.0) - tslib: 2.4.0 - transitivePeerDependencies: - - '@parcel/css' - - '@swc/core' - - '@swc/css' - - bufferutil - - csso - - debug - - esbuild - - eslint - - lightningcss - - supports-color - - typescript - - uglify-js - - utf-8-validate - - vue-template-compiler - - webpack-cli - - '@docusaurus/plugin-sitemap@2.4.3(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2)': - dependencies: - '@docusaurus/core': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/logger': 2.4.3 - '@docusaurus/types': 2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - '@docusaurus/utils': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - '@docusaurus/utils-common': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - '@docusaurus/utils-validation': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - fs-extra: 10.1.0 - react: 16.14.0 - react-dom: 16.14.0(react@16.14.0) - sitemap: 7.1.2 - tslib: 2.4.0 - transitivePeerDependencies: - - '@parcel/css' - - '@swc/core' - - '@swc/css' - - bufferutil - - csso - - debug - - esbuild - - eslint - - lightningcss - - supports-color - - typescript - - uglify-js - - utf-8-validate - - vue-template-compiler - - webpack-cli - - '@docusaurus/preset-classic@2.4.3(@algolia/client-search@4.14.2)(@types/react@18.0.26)(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2)': - dependencies: - '@docusaurus/core': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/plugin-content-blog': 2.4.3(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/plugin-content-docs': 2.4.3(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/plugin-content-pages': 2.4.3(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/plugin-debug': 2.4.3(@types/react@18.0.26)(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/plugin-google-analytics': 2.4.3(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/plugin-google-gtag': 2.4.3(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/plugin-google-tag-manager': 2.4.3(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/plugin-sitemap': 2.4.3(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/theme-classic': 2.4.3(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/theme-common': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/theme-search-algolia': 2.4.3(@algolia/client-search@4.14.2)(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(@types/react@18.0.26)(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/types': 2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - react: 16.14.0 - react-dom: 16.14.0(react@16.14.0) - transitivePeerDependencies: - - '@algolia/client-search' - - '@parcel/css' - - '@swc/core' - - '@swc/css' - - '@types/react' - - bufferutil - - csso - - debug - - encoding - - esbuild - - eslint - - lightningcss - - supports-color - - typescript - - uglify-js - - utf-8-validate - - vue-template-compiler - - webpack-cli - - '@docusaurus/react-loadable@5.5.2(react@16.14.0)': - dependencies: - '@types/react': 18.0.26 - prop-types: 15.8.1 - react: 16.14.0 - - '@docusaurus/theme-classic@2.4.3(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2)': - dependencies: - '@docusaurus/core': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/mdx-loader': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - '@docusaurus/module-type-aliases': 2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - '@docusaurus/plugin-content-blog': 2.4.3(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/plugin-content-docs': 2.4.3(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/plugin-content-pages': 2.4.3(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/theme-common': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/theme-translations': 2.4.3 - '@docusaurus/types': 2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - '@docusaurus/utils': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - '@docusaurus/utils-common': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - '@docusaurus/utils-validation': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - '@mdx-js/react': 1.6.22(react@16.14.0) - clsx: 1.2.1 - copy-text-to-clipboard: 3.2.0 - infima: 0.2.0-alpha.43 - lodash: 4.17.21 - nprogress: 0.2.0 - postcss: 8.5.3 - prism-react-renderer: 1.3.5(react@16.14.0) - prismjs: 1.30.0 - react: 16.14.0 - react-dom: 16.14.0(react@16.14.0) - react-router-dom: 5.3.4(react@16.14.0) - rtlcss: 3.5.0 - tslib: 2.4.0 - utility-types: 3.10.0 - transitivePeerDependencies: - - '@parcel/css' - - '@swc/core' - - '@swc/css' - - bufferutil - - csso - - debug - - esbuild - - eslint - - lightningcss - - supports-color - - typescript - - uglify-js - - utf-8-validate - - vue-template-compiler - - webpack-cli - - '@docusaurus/theme-common@2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2)': - dependencies: - '@docusaurus/mdx-loader': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - '@docusaurus/module-type-aliases': 2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - '@docusaurus/plugin-content-blog': 2.4.3(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/plugin-content-docs': 2.4.3(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/plugin-content-pages': 2.4.3(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/utils': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - '@docusaurus/utils-common': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - '@types/history': 4.7.11 - '@types/react': 18.0.26 - '@types/react-router-config': 5.0.11 - clsx: 1.2.1 - parse-numeric-range: 1.3.0 - prism-react-renderer: 1.3.5(react@16.14.0) - react: 16.14.0 - react-dom: 16.14.0(react@16.14.0) - tslib: 2.4.0 - use-sync-external-store: 1.4.0(react@16.14.0) - utility-types: 3.10.0 - transitivePeerDependencies: - - '@docusaurus/types' - - '@parcel/css' - - '@swc/core' - - '@swc/css' - - bufferutil - - csso - - debug - - esbuild - - eslint - - lightningcss - - supports-color - - typescript - - uglify-js - - utf-8-validate - - vue-template-compiler - - webpack-cli - - '@docusaurus/theme-search-algolia@2.4.3(@algolia/client-search@4.14.2)(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(@types/react@18.0.26)(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2)': - dependencies: - '@docsearch/react': 3.2.2(@algolia/client-search@4.14.2)(@types/react@18.0.26)(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - '@docusaurus/core': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/logger': 2.4.3 - '@docusaurus/plugin-content-docs': 2.4.3(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/theme-common': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/theme-translations': 2.4.3 - '@docusaurus/utils': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - '@docusaurus/utils-validation': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - algoliasearch: 4.14.2 - algoliasearch-helper: 3.11.1(algoliasearch@4.14.2) - clsx: 1.2.1 - eta: 2.2.0 - fs-extra: 10.1.0 - lodash: 4.17.21 - react: 16.14.0 - react-dom: 16.14.0(react@16.14.0) - tslib: 2.4.0 - utility-types: 3.10.0 - transitivePeerDependencies: - - '@algolia/client-search' - - '@docusaurus/types' - - '@parcel/css' - - '@swc/core' - - '@swc/css' - - '@types/react' - - bufferutil - - csso - - debug - - esbuild - - eslint - - lightningcss - - supports-color - - typescript - - uglify-js - - utf-8-validate - - vue-template-compiler - - webpack-cli - - '@docusaurus/theme-translations@2.4.3': - dependencies: - fs-extra: 10.1.0 - tslib: 2.4.0 - - '@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)': - dependencies: - '@types/history': 4.7.11 - '@types/react': 18.0.26 - commander: 5.1.0 - joi: 17.13.3 - react: 16.14.0 - react-dom: 16.14.0(react@16.14.0) - react-helmet-async: 1.3.0(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - utility-types: 3.10.0 - webpack: 5.105.0 - webpack-merge: 5.8.0 - transitivePeerDependencies: - - '@swc/core' - - esbuild - - uglify-js - - webpack-cli - - '@docusaurus/utils-common@2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))': - dependencies: - tslib: 2.4.0 - optionalDependencies: - '@docusaurus/types': 2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - - '@docusaurus/utils-validation@2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))': - dependencies: - '@docusaurus/logger': 2.4.3 - '@docusaurus/utils': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - joi: 17.13.3 - js-yaml: 4.1.0 - tslib: 2.4.0 - transitivePeerDependencies: - - '@docusaurus/types' - - '@swc/core' - - esbuild - - supports-color - - uglify-js - - webpack-cli - - '@docusaurus/utils@2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))': - dependencies: - '@docusaurus/logger': 2.4.3 - '@svgr/webpack': 6.5.1 - escape-string-regexp: 4.0.0 - file-loader: 6.2.0(webpack@5.105.0) - fs-extra: 10.1.0 - github-slugger: 1.4.0 - globby: 11.1.0 - gray-matter: 4.0.3 - js-yaml: 4.1.0 - lodash: 4.17.21 - micromatch: 4.0.8 - resolve-pathname: 3.0.0 - shelljs: 0.8.5 - tslib: 2.4.0 - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.105.0))(webpack@5.105.0) - webpack: 5.105.0 - optionalDependencies: - '@docusaurus/types': 2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - transitivePeerDependencies: - - '@swc/core' - - esbuild - - supports-color - - uglify-js - - webpack-cli - - '@eslint/eslintrc@0.4.3': - dependencies: - ajv: 6.12.6 - debug: 4.3.4 - espree: 7.3.1 - globals: 13.17.0 - ignore: 4.0.6 - import-fresh: 3.3.0 - js-yaml: 3.14.1 - minimatch: 3.1.2 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - - '@hapi/hoek@9.3.0': {} - - '@hapi/topo@5.1.0': - dependencies: - '@hapi/hoek': 9.3.0 - - '@humanwhocodes/config-array@0.5.0': - dependencies: - '@humanwhocodes/object-schema': 1.2.1 - debug: 4.3.4 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color - - '@humanwhocodes/object-schema@1.2.1': {} - - '@jest/schemas@29.0.0': - dependencies: - '@sinclair/typebox': 0.24.51 - - '@jest/types@29.3.1': - dependencies: - '@jest/schemas': 29.0.0 - '@types/istanbul-lib-coverage': 2.0.4 - '@types/istanbul-reports': 3.0.1 - '@types/node': 18.11.3 - '@types/yargs': 17.0.19 - chalk: 4.1.2 - - '@jridgewell/gen-mapping@0.1.1': - dependencies: - '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.4.14 - - '@jridgewell/gen-mapping@0.3.8': - dependencies: - '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.4.14 - '@jridgewell/trace-mapping': 0.3.25 - - '@jridgewell/resolve-uri@3.1.0': {} - - '@jridgewell/set-array@1.2.1': {} - - '@jridgewell/source-map@0.3.6': - dependencies: - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 - - '@jridgewell/sourcemap-codec@1.4.14': {} - - '@jridgewell/trace-mapping@0.3.25': - dependencies: - '@jridgewell/resolve-uri': 3.1.0 - '@jridgewell/sourcemap-codec': 1.4.14 - - '@leichtgewicht/ip-codec@2.0.4': {} - - '@mdx-js/mdx@1.6.22': - dependencies: - '@babel/core': 7.12.9 - '@babel/plugin-syntax-jsx': 7.12.1(@babel/core@7.12.9) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.12.9) - '@mdx-js/util': 1.6.22 - babel-plugin-apply-mdx-type-prop: 1.6.22(@babel/core@7.12.9) - babel-plugin-extract-import-names: 1.6.22 - camelcase-css: 2.0.1 - detab: 2.0.4 - hast-util-raw: 6.0.1 - lodash.uniq: 4.5.0 - mdast-util-to-hast: 10.0.1 - remark-footnotes: 2.0.0 - remark-mdx: 1.6.22 - remark-parse: 8.0.3 - remark-squeeze-paragraphs: 4.0.0 - style-to-object: 0.3.0 - unified: 9.2.0 - unist-builder: 2.0.3 - unist-util-visit: 2.0.3 - transitivePeerDependencies: - - supports-color - - '@mdx-js/react@1.6.22(react@16.14.0)': - dependencies: - react: 16.14.0 - - '@mdx-js/util@1.6.22': {} - - '@nodelib/fs.scandir@2.1.5': - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - - '@nodelib/fs.stat@2.0.5': {} - - '@nodelib/fs.walk@1.2.8': - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.13.0 - - '@polka/url@1.0.0-next.21': {} - - '@sideway/address@4.1.5': - dependencies: - '@hapi/hoek': 9.3.0 - - '@sideway/formula@3.0.1': {} - - '@sideway/pinpoint@2.0.0': {} - - '@sinclair/typebox@0.24.51': {} - - '@sindresorhus/is@4.6.0': {} - - '@slorber/static-site-generator-webpack-plugin@4.0.7': - dependencies: - eval: 0.1.8 - p-map: 4.0.0 - webpack-sources: 3.3.3 - - '@svgr/babel-plugin-add-jsx-attribute@6.5.1(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - - '@svgr/babel-plugin-remove-jsx-attribute@6.5.0(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - - '@svgr/babel-plugin-remove-jsx-empty-expression@6.5.0(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - - '@svgr/babel-plugin-replace-jsx-attribute-value@6.5.1(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - - '@svgr/babel-plugin-svg-dynamic-title@6.5.1(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - - '@svgr/babel-plugin-svg-em-dimensions@6.5.1(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - - '@svgr/babel-plugin-transform-react-native-svg@6.5.1(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - - '@svgr/babel-plugin-transform-svg-component@6.5.1(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - - '@svgr/babel-preset@6.5.1(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@svgr/babel-plugin-add-jsx-attribute': 6.5.1(@babel/core@7.19.6) - '@svgr/babel-plugin-remove-jsx-attribute': 6.5.0(@babel/core@7.19.6) - '@svgr/babel-plugin-remove-jsx-empty-expression': 6.5.0(@babel/core@7.19.6) - '@svgr/babel-plugin-replace-jsx-attribute-value': 6.5.1(@babel/core@7.19.6) - '@svgr/babel-plugin-svg-dynamic-title': 6.5.1(@babel/core@7.19.6) - '@svgr/babel-plugin-svg-em-dimensions': 6.5.1(@babel/core@7.19.6) - '@svgr/babel-plugin-transform-react-native-svg': 6.5.1(@babel/core@7.19.6) - '@svgr/babel-plugin-transform-svg-component': 6.5.1(@babel/core@7.19.6) - - '@svgr/core@6.5.1': - dependencies: - '@babel/core': 7.19.6 - '@svgr/babel-preset': 6.5.1(@babel/core@7.19.6) - '@svgr/plugin-jsx': 6.5.1(@svgr/core@6.5.1) - camelcase: 6.3.0 - cosmiconfig: 7.0.1 - transitivePeerDependencies: - - supports-color - - '@svgr/hast-util-to-babel-ast@6.5.1': - dependencies: - '@babel/types': 7.27.0 - entities: 4.5.0 - - '@svgr/plugin-jsx@6.5.1(@svgr/core@6.5.1)': - dependencies: - '@babel/core': 7.19.6 - '@svgr/babel-preset': 6.5.1(@babel/core@7.19.6) - '@svgr/core': 6.5.1 - '@svgr/hast-util-to-babel-ast': 6.5.1 - svg-parser: 2.0.4 - transitivePeerDependencies: - - supports-color - - '@svgr/plugin-svgo@6.5.1(@svgr/core@6.5.1)': - dependencies: - '@svgr/core': 6.5.1 - cosmiconfig: 7.0.1 - deepmerge: 4.2.2 - svgo: 2.8.0 - - '@svgr/webpack@6.5.1': - dependencies: - '@babel/core': 7.19.6 - '@babel/plugin-transform-react-constant-elements': 7.20.2(@babel/core@7.19.6) - '@babel/preset-env': 7.19.4(@babel/core@7.19.6) - '@babel/preset-react': 7.18.6(@babel/core@7.19.6) - '@babel/preset-typescript': 7.18.6(@babel/core@7.19.6) - '@svgr/core': 6.5.1 - '@svgr/plugin-jsx': 6.5.1(@svgr/core@6.5.1) - '@svgr/plugin-svgo': 6.5.1(@svgr/core@6.5.1) - transitivePeerDependencies: - - supports-color - - '@szmarczak/http-timer@4.0.6': - dependencies: - defer-to-connect: 2.0.1 - - '@trysound/sax@0.2.0': {} - - '@types/body-parser@1.19.2': - dependencies: - '@types/connect': 3.4.35 - '@types/node': 18.11.3 - - '@types/bonjour@3.5.10': - dependencies: - '@types/node': 18.11.3 - - '@types/cacheable-request@6.0.3': - dependencies: - '@types/http-cache-semantics': 4.0.4 - '@types/keyv': 3.1.4 - '@types/node': 18.11.3 - '@types/responselike': 1.0.3 - - '@types/connect-history-api-fallback@1.3.5': - dependencies: - '@types/express-serve-static-core': 4.17.32 - '@types/node': 18.11.3 - - '@types/connect@3.4.35': - dependencies: - '@types/node': 18.11.3 - - '@types/eslint-scope@3.7.7': - dependencies: - '@types/eslint': 9.6.1 - '@types/estree': 1.0.8 - - '@types/eslint@9.6.1': - dependencies: - '@types/estree': 1.0.8 - '@types/json-schema': 7.0.15 - - '@types/estree@1.0.8': {} - - '@types/express-serve-static-core@4.17.32': - dependencies: - '@types/node': 18.11.3 - '@types/qs': 6.9.7 - '@types/range-parser': 1.2.4 - - '@types/express@4.17.15': - dependencies: - '@types/body-parser': 1.19.2 - '@types/express-serve-static-core': 4.17.32 - '@types/qs': 6.9.7 - '@types/serve-static': 1.15.0 - - '@types/hast@2.3.4': - dependencies: - '@types/unist': 2.0.6 - - '@types/history@4.7.11': {} - - '@types/html-minifier-terser@6.1.0': {} - - '@types/http-cache-semantics@4.0.4': {} - - '@types/http-proxy@1.17.9': - dependencies: - '@types/node': 18.11.3 - - '@types/istanbul-lib-coverage@2.0.4': {} - - '@types/istanbul-lib-report@3.0.0': - dependencies: - '@types/istanbul-lib-coverage': 2.0.4 - - '@types/istanbul-reports@3.0.1': - dependencies: - '@types/istanbul-lib-report': 3.0.0 - - '@types/json-schema@7.0.15': {} - - '@types/katex@0.11.1': {} - - '@types/keyv@3.1.4': - dependencies: - '@types/node': 18.11.3 - - '@types/mdast@3.0.10': - dependencies: - '@types/unist': 2.0.6 - - '@types/mime@3.0.1': {} - - '@types/node@17.0.45': {} - - '@types/node@18.11.3': {} - - '@types/parse-json@4.0.0': {} - - '@types/parse5@5.0.3': {} - - '@types/prop-types@15.7.5': {} - - '@types/qs@6.9.7': {} - - '@types/range-parser@1.2.4': {} - - '@types/react-router-config@5.0.11': - dependencies: - '@types/history': 4.7.11 - '@types/react': 18.0.26 - '@types/react-router': 5.1.20 - - '@types/react-router-dom@5.3.3': - dependencies: - '@types/history': 4.7.11 - '@types/react': 18.0.26 - '@types/react-router': 5.1.20 - - '@types/react-router@5.1.20': - dependencies: - '@types/history': 4.7.11 - '@types/react': 18.0.26 - - '@types/react@18.0.26': - dependencies: - '@types/prop-types': 15.7.5 - '@types/scheduler': 0.16.2 - csstype: 3.1.1 - - '@types/responselike@1.0.3': - dependencies: - '@types/node': 18.11.3 - - '@types/retry@0.12.0': {} - - '@types/sax@1.2.7': - dependencies: - '@types/node': 18.11.3 - - '@types/scheduler@0.16.2': {} - - '@types/serve-index@1.9.1': - dependencies: - '@types/express': 4.17.15 - - '@types/serve-static@1.15.0': - dependencies: - '@types/mime': 3.0.1 - '@types/node': 18.11.3 - - '@types/sockjs@0.3.33': - dependencies: - '@types/node': 18.11.3 - - '@types/unist@2.0.6': {} - - '@types/ws@8.18.0': - dependencies: - '@types/node': 18.11.3 - - '@types/yargs-parser@21.0.0': {} - - '@types/yargs@17.0.19': - dependencies: - '@types/yargs-parser': 21.0.0 - - '@webassemblyjs/ast@1.14.1': - dependencies: - '@webassemblyjs/helper-numbers': 1.13.2 - '@webassemblyjs/helper-wasm-bytecode': 1.13.2 - - '@webassemblyjs/floating-point-hex-parser@1.13.2': {} - - '@webassemblyjs/helper-api-error@1.13.2': {} - - '@webassemblyjs/helper-buffer@1.14.1': {} - - '@webassemblyjs/helper-numbers@1.13.2': - dependencies: - '@webassemblyjs/floating-point-hex-parser': 1.13.2 - '@webassemblyjs/helper-api-error': 1.13.2 - '@xtuc/long': 4.2.2 - - '@webassemblyjs/helper-wasm-bytecode@1.13.2': {} - - '@webassemblyjs/helper-wasm-section@1.14.1': - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/helper-buffer': 1.14.1 - '@webassemblyjs/helper-wasm-bytecode': 1.13.2 - '@webassemblyjs/wasm-gen': 1.14.1 - - '@webassemblyjs/ieee754@1.13.2': - dependencies: - '@xtuc/ieee754': 1.2.0 - - '@webassemblyjs/leb128@1.13.2': - dependencies: - '@xtuc/long': 4.2.2 - - '@webassemblyjs/utf8@1.13.2': {} - - '@webassemblyjs/wasm-edit@1.14.1': - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/helper-buffer': 1.14.1 - '@webassemblyjs/helper-wasm-bytecode': 1.13.2 - '@webassemblyjs/helper-wasm-section': 1.14.1 - '@webassemblyjs/wasm-gen': 1.14.1 - '@webassemblyjs/wasm-opt': 1.14.1 - '@webassemblyjs/wasm-parser': 1.14.1 - '@webassemblyjs/wast-printer': 1.14.1 - - '@webassemblyjs/wasm-gen@1.14.1': - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/helper-wasm-bytecode': 1.13.2 - '@webassemblyjs/ieee754': 1.13.2 - '@webassemblyjs/leb128': 1.13.2 - '@webassemblyjs/utf8': 1.13.2 - - '@webassemblyjs/wasm-opt@1.14.1': - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/helper-buffer': 1.14.1 - '@webassemblyjs/wasm-gen': 1.14.1 - '@webassemblyjs/wasm-parser': 1.14.1 - - '@webassemblyjs/wasm-parser@1.14.1': - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/helper-api-error': 1.13.2 - '@webassemblyjs/helper-wasm-bytecode': 1.13.2 - '@webassemblyjs/ieee754': 1.13.2 - '@webassemblyjs/leb128': 1.13.2 - '@webassemblyjs/utf8': 1.13.2 - - '@webassemblyjs/wast-printer@1.14.1': - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@xtuc/long': 4.2.2 - - '@xtuc/ieee754@1.2.0': {} - - '@xtuc/long@4.2.2': {} - - accepts@1.3.8: - dependencies: - mime-types: 2.1.35 - negotiator: 0.6.3 - - acorn-import-phases@1.0.4(acorn@8.15.0): - dependencies: - acorn: 8.15.0 - - acorn-jsx@5.3.2(acorn@7.4.1): - dependencies: - acorn: 7.4.1 - - acorn-walk@8.2.0: {} - - acorn@7.4.1: {} - - acorn@8.15.0: {} - - address@1.2.1: {} - - aggregate-error@3.1.0: - dependencies: - clean-stack: 2.2.0 - indent-string: 4.0.0 - - ajv-formats@2.1.1(ajv@8.17.1): - optionalDependencies: - ajv: 8.17.1 - - ajv-keywords@3.5.2(ajv@6.12.6): - dependencies: - ajv: 6.12.6 - - ajv-keywords@5.1.0(ajv@8.17.1): - dependencies: - ajv: 8.17.1 - fast-deep-equal: 3.1.3 - - ajv@6.12.6: - dependencies: - fast-deep-equal: 3.1.3 - fast-json-stable-stringify: 2.1.0 - json-schema-traverse: 0.4.1 - uri-js: 4.4.1 - - ajv@8.17.1: - dependencies: - fast-deep-equal: 3.1.3 - fast-uri: 3.0.6 - json-schema-traverse: 1.0.0 - require-from-string: 2.0.2 - - algoliasearch-helper@3.11.1(algoliasearch@4.14.2): - dependencies: - '@algolia/events': 4.0.1 - algoliasearch: 4.14.2 - - algoliasearch@4.14.2: - dependencies: - '@algolia/cache-browser-local-storage': 4.14.2 - '@algolia/cache-common': 4.14.2 - '@algolia/cache-in-memory': 4.14.2 - '@algolia/client-account': 4.14.2 - '@algolia/client-analytics': 4.14.2 - '@algolia/client-common': 4.14.2 - '@algolia/client-personalization': 4.14.2 - '@algolia/client-search': 4.14.2 - '@algolia/logger-common': 4.14.2 - '@algolia/logger-console': 4.14.2 - '@algolia/requester-browser-xhr': 4.14.2 - '@algolia/requester-common': 4.14.2 - '@algolia/requester-node-http': 4.14.2 - '@algolia/transporter': 4.14.2 - - ansi-align@3.0.1: - dependencies: - string-width: 4.2.3 - - ansi-colors@4.1.3: {} - - ansi-html-community@0.0.8: {} - - ansi-regex@5.0.1: {} - - ansi-regex@6.0.1: {} - - ansi-styles@3.2.1: - dependencies: - color-convert: 1.9.3 - - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - - ansi-styles@6.2.1: {} - - anymatch@3.1.2: - dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.1 - - arg@5.0.2: {} - - argparse@1.0.10: - dependencies: - sprintf-js: 1.0.3 - - argparse@2.0.1: {} - - array-buffer-byte-length@1.0.2: - dependencies: - call-bound: 1.0.4 - is-array-buffer: 3.0.5 - - array-flatten@1.1.1: {} - - array-flatten@2.1.2: {} - - array-includes@3.1.8: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.9 - es-object-atoms: 1.1.1 - get-intrinsic: 1.3.0 - is-string: 1.1.1 - - array-union@2.1.0: {} - - array.prototype.findlast@1.2.5: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.9 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - es-shim-unscopables: 1.1.0 - - array.prototype.flatmap@1.3.3: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.9 - es-shim-unscopables: 1.1.0 - - array.prototype.tosorted@1.1.4: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.9 - es-errors: 1.3.0 - es-shim-unscopables: 1.1.0 - - arraybuffer.prototype.slice@1.0.4: - dependencies: - array-buffer-byte-length: 1.0.2 - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.9 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - is-array-buffer: 3.0.5 - - asap@2.0.6: {} - - astral-regex@2.0.0: {} - - async-function@1.0.0: {} - - asynckit@0.4.0: {} - - at-least-node@1.0.0: {} - - autoprefixer@10.4.12(postcss@8.5.3): - dependencies: - browserslist: 4.28.1 - caniuse-lite: 1.0.30001768 - fraction.js: 4.2.0 - normalize-range: 0.1.2 - picocolors: 1.1.1 - postcss: 8.5.3 - postcss-value-parser: 4.2.0 - - available-typed-arrays@1.0.7: - dependencies: - possible-typed-array-names: 1.1.0 - - axios@1.13.5: - dependencies: - follow-redirects: 1.15.11 - form-data: 4.0.5 - proxy-from-env: 1.1.0 - transitivePeerDependencies: - - debug - - babel-eslint@10.1.0(eslint@7.32.0): - dependencies: - '@babel/code-frame': 7.26.2 - '@babel/parser': 7.27.0 - '@babel/traverse': 7.23.2 - '@babel/types': 7.27.0 - eslint: 7.32.0 - eslint-visitor-keys: 1.3.0 - resolve: 1.22.1 - transitivePeerDependencies: - - supports-color - - babel-loader@8.2.5(@babel/core@7.19.6)(webpack@5.105.0): - dependencies: - '@babel/core': 7.19.6 - find-cache-dir: 3.3.2 - loader-utils: 2.0.4 - make-dir: 3.1.0 - schema-utils: 2.7.1 - webpack: 5.105.0 - - babel-plugin-apply-mdx-type-prop@1.6.22(@babel/core@7.12.9): - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.10.4 - '@mdx-js/util': 1.6.22 - - babel-plugin-dynamic-import-node@2.3.3: - dependencies: - object.assign: 4.1.7 - - babel-plugin-extract-import-names@1.6.22: - dependencies: - '@babel/helper-plugin-utils': 7.10.4 - - babel-plugin-polyfill-corejs2@0.3.3(@babel/core@7.19.6): - dependencies: - '@babel/compat-data': 7.19.4 - '@babel/core': 7.19.6 - '@babel/helper-define-polyfill-provider': 0.3.3(@babel/core@7.19.6) - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - babel-plugin-polyfill-corejs3@0.6.0(@babel/core@7.19.6): - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-define-polyfill-provider': 0.3.3(@babel/core@7.19.6) - core-js-compat: 3.25.5 - transitivePeerDependencies: - - supports-color - - babel-plugin-polyfill-regenerator@0.4.1(@babel/core@7.19.6): - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-define-polyfill-provider': 0.3.3(@babel/core@7.19.6) - transitivePeerDependencies: - - supports-color - - bail@1.0.5: {} - - balanced-match@1.0.2: {} - - base16@1.0.0: {} - - baseline-browser-mapping@2.9.19: {} - - batch@0.6.1: {} - - big.js@5.2.2: {} - - binary-extensions@2.2.0: {} - - body-parser@1.20.3: - dependencies: - bytes: 3.1.2 - content-type: 1.0.5 - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 - http-errors: 2.0.0 - iconv-lite: 0.4.24 - on-finished: 2.4.1 - qs: 6.13.0 - raw-body: 2.5.2 - type-is: 1.6.18 - unpipe: 1.0.0 - transitivePeerDependencies: - - supports-color - - bonjour-service@1.0.14: - dependencies: - array-flatten: 2.1.2 - dns-equal: 1.0.0 - fast-deep-equal: 3.1.3 - multicast-dns: 7.2.5 - - boolbase@1.0.0: {} - - boxen@5.1.2: - dependencies: - ansi-align: 3.0.1 - camelcase: 6.3.0 - chalk: 4.1.2 - cli-boxes: 2.2.1 - string-width: 4.2.3 - type-fest: 0.20.2 - widest-line: 3.1.0 - wrap-ansi: 7.0.0 - - boxen@6.2.1: - dependencies: - ansi-align: 3.0.1 - camelcase: 6.3.0 - chalk: 4.1.2 - cli-boxes: 3.0.0 - string-width: 5.1.2 - type-fest: 2.19.0 - widest-line: 4.0.1 - wrap-ansi: 8.0.1 - - brace-expansion@1.1.12: - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - - braces@3.0.3: - dependencies: - fill-range: 7.1.1 - - browserslist@4.28.1: - dependencies: - baseline-browser-mapping: 2.9.19 - caniuse-lite: 1.0.30001768 - electron-to-chromium: 1.5.286 - node-releases: 2.0.27 - update-browserslist-db: 1.2.3(browserslist@4.28.1) - - buffer-from@1.1.2: {} - - bytes@3.0.0: {} - - bytes@3.1.2: {} - - cacheable-lookup@5.0.4: {} - - cacheable-request@7.0.4: - dependencies: - clone-response: 1.0.3 - get-stream: 5.2.0 - http-cache-semantics: 4.2.0 - keyv: 4.5.4 - lowercase-keys: 2.0.0 - normalize-url: 6.1.0 - responselike: 2.0.1 - - call-bind-apply-helpers@1.0.2: - dependencies: - es-errors: 1.3.0 - function-bind: 1.1.2 - - call-bind@1.0.8: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - get-intrinsic: 1.3.0 - set-function-length: 1.2.2 - - call-bound@1.0.4: - dependencies: - call-bind-apply-helpers: 1.0.2 - get-intrinsic: 1.3.0 - - callsites@3.1.0: {} - - camel-case@4.1.2: - dependencies: - pascal-case: 3.1.2 - tslib: 2.4.0 - - camelcase-css@2.0.1: {} - - camelcase@6.3.0: {} - - caniuse-api@3.0.0: - dependencies: - browserslist: 4.28.1 - caniuse-lite: 1.0.30001768 - lodash.memoize: 4.1.2 - lodash.uniq: 4.5.0 - - caniuse-lite@1.0.30001768: {} - - ccount@1.1.0: {} - - chalk@2.4.2: - dependencies: - ansi-styles: 3.2.1 - escape-string-regexp: 1.0.5 - supports-color: 5.5.0 - - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - - character-entities-legacy@1.1.4: {} - - character-entities@1.2.4: {} - - character-reference-invalid@1.1.4: {} - - cheerio-select@2.1.0: - dependencies: - boolbase: 1.0.0 - css-select: 5.1.0 - css-what: 6.1.0 - domelementtype: 2.3.0 - domhandler: 5.0.3 - domutils: 3.2.2 - - cheerio@1.0.0: - dependencies: - cheerio-select: 2.1.0 - dom-serializer: 2.0.0 - domhandler: 5.0.3 - domutils: 3.2.2 - encoding-sniffer: 0.2.0 - htmlparser2: 9.1.0 - parse5: 7.2.1 - parse5-htmlparser2-tree-adapter: 7.1.0 - parse5-parser-stream: 7.1.2 - undici: 6.21.2 - whatwg-mimetype: 4.0.0 - - chokidar@3.5.3: - dependencies: - anymatch: 3.1.2 - braces: 3.0.3 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.3 - - chrome-trace-event@1.0.3: {} - - ci-info@2.0.0: {} - - ci-info@3.7.1: {} - - clean-css@5.3.1: - dependencies: - source-map: 0.6.1 - - clean-stack@2.2.0: {} - - cli-boxes@2.2.1: {} - - cli-boxes@3.0.0: {} - - cli-table3@0.6.3: - dependencies: - string-width: 4.2.3 - optionalDependencies: - '@colors/colors': 1.5.0 - - clone-deep@4.0.1: - dependencies: - is-plain-object: 2.0.4 - kind-of: 6.0.3 - shallow-clone: 3.0.1 - - clone-response@1.0.3: - dependencies: - mimic-response: 1.0.1 - - clsx@1.2.1: {} - - collapse-white-space@1.0.6: {} - - color-convert@1.9.3: - dependencies: - color-name: 1.1.3 - - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - - color-name@1.1.3: {} - - color-name@1.1.4: {} - - colord@2.9.3: {} - - colorette@2.0.19: {} - - combine-promises@1.1.0: {} - - combined-stream@1.0.8: - dependencies: - delayed-stream: 1.0.0 - - comma-separated-tokens@1.0.8: {} - - commander@2.20.3: {} - - commander@5.1.0: {} - - commander@7.2.0: {} - - commander@8.3.0: {} - - commondir@1.0.1: {} - - compressible@2.0.18: - dependencies: - mime-db: 1.52.0 - - compression@1.7.4: - dependencies: - accepts: 1.3.8 - bytes: 3.0.0 - compressible: 2.0.18 - debug: 2.6.9 - on-headers: 1.0.2 - safe-buffer: 5.1.2 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - - concat-map@0.0.1: {} - - configstore@5.0.1: - dependencies: - dot-prop: 5.3.0 - graceful-fs: 4.2.11 - make-dir: 3.1.0 - unique-string: 2.0.0 - write-file-atomic: 3.0.3 - xdg-basedir: 4.0.0 - - connect-history-api-fallback@2.0.0: {} - - consola@2.15.3: {} - - content-disposition@0.5.2: {} - - content-disposition@0.5.4: - dependencies: - safe-buffer: 5.2.1 - - content-type@1.0.5: {} - - convert-source-map@1.9.0: {} - - cookie-signature@1.0.6: {} - - cookie@0.7.1: {} - - copy-text-to-clipboard@3.2.0: {} - - copy-webpack-plugin@11.0.0(webpack@5.105.0): - dependencies: - fast-glob: 3.2.12 - glob-parent: 6.0.2 - globby: 13.1.3 - normalize-path: 3.0.0 - schema-utils: 4.3.3 - serialize-javascript: 6.0.2 - webpack: 5.105.0 - - core-js-compat@3.25.5: - dependencies: - browserslist: 4.28.1 - - core-js-pure@3.41.0: {} - - core-js@3.25.5: {} - - core-util-is@1.0.3: {} - - cosmiconfig@6.0.0: - dependencies: - '@types/parse-json': 4.0.0 - import-fresh: 3.3.0 - parse-json: 5.2.0 - path-type: 4.0.0 - yaml: 1.10.2 - - cosmiconfig@7.0.1: - dependencies: - '@types/parse-json': 4.0.0 - import-fresh: 3.3.0 - parse-json: 5.2.0 - path-type: 4.0.0 - yaml: 1.10.2 - - cross-fetch@3.2.0: - dependencies: - node-fetch: 2.7.0 - transitivePeerDependencies: - - encoding - - cross-spawn@7.0.6: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - - crypto-random-string@2.0.0: {} - - css-declaration-sorter@6.3.1(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - - css-loader@6.7.3(webpack@5.105.0): - dependencies: - icss-utils: 5.1.0(postcss@8.5.3) - postcss: 8.5.3 - postcss-modules-extract-imports: 3.0.0(postcss@8.5.3) - postcss-modules-local-by-default: 4.0.0(postcss@8.5.3) - postcss-modules-scope: 3.0.0(postcss@8.5.3) - postcss-modules-values: 4.0.0(postcss@8.5.3) - postcss-value-parser: 4.2.0 - semver: 7.7.1 - webpack: 5.105.0 - - css-minimizer-webpack-plugin@4.2.2(clean-css@5.3.1)(webpack@5.105.0): - dependencies: - cssnano: 5.1.13(postcss@8.5.3) - jest-worker: 29.3.1 - postcss: 8.5.3 - schema-utils: 4.3.3 - serialize-javascript: 6.0.2 - source-map: 0.6.1 - webpack: 5.105.0 - optionalDependencies: - clean-css: 5.3.1 - - css-select@4.3.0: - dependencies: - boolbase: 1.0.0 - css-what: 6.1.0 - domhandler: 4.3.1 - domutils: 2.8.0 - nth-check: 2.1.1 - - css-select@5.1.0: - dependencies: - boolbase: 1.0.0 - css-what: 6.1.0 - domhandler: 5.0.3 - domutils: 3.2.2 - nth-check: 2.1.1 - - css-tree@1.1.3: - dependencies: - mdn-data: 2.0.14 - source-map: 0.6.1 - - css-what@6.1.0: {} - - cssesc@3.0.0: {} - - cssnano-preset-advanced@5.3.9(postcss@8.5.3): - dependencies: - autoprefixer: 10.4.12(postcss@8.5.3) - cssnano-preset-default: 5.2.13(postcss@8.5.3) - postcss: 8.5.3 - postcss-discard-unused: 5.1.0(postcss@8.5.3) - postcss-merge-idents: 5.1.1(postcss@8.5.3) - postcss-reduce-idents: 5.2.0(postcss@8.5.3) - postcss-zindex: 5.1.0(postcss@8.5.3) - - cssnano-preset-default@5.2.13(postcss@8.5.3): - dependencies: - css-declaration-sorter: 6.3.1(postcss@8.5.3) - cssnano-utils: 3.1.0(postcss@8.5.3) - postcss: 8.5.3 - postcss-calc: 8.2.4(postcss@8.5.3) - postcss-colormin: 5.3.0(postcss@8.5.3) - postcss-convert-values: 5.1.3(postcss@8.5.3) - postcss-discard-comments: 5.1.2(postcss@8.5.3) - postcss-discard-duplicates: 5.1.0(postcss@8.5.3) - postcss-discard-empty: 5.1.1(postcss@8.5.3) - postcss-discard-overridden: 5.1.0(postcss@8.5.3) - postcss-merge-longhand: 5.1.7(postcss@8.5.3) - postcss-merge-rules: 5.1.3(postcss@8.5.3) - postcss-minify-font-values: 5.1.0(postcss@8.5.3) - postcss-minify-gradients: 5.1.1(postcss@8.5.3) - postcss-minify-params: 5.1.4(postcss@8.5.3) - postcss-minify-selectors: 5.2.1(postcss@8.5.3) - postcss-normalize-charset: 5.1.0(postcss@8.5.3) - postcss-normalize-display-values: 5.1.0(postcss@8.5.3) - postcss-normalize-positions: 5.1.1(postcss@8.5.3) - postcss-normalize-repeat-style: 5.1.1(postcss@8.5.3) - postcss-normalize-string: 5.1.0(postcss@8.5.3) - postcss-normalize-timing-functions: 5.1.0(postcss@8.5.3) - postcss-normalize-unicode: 5.1.1(postcss@8.5.3) - postcss-normalize-url: 5.1.0(postcss@8.5.3) - postcss-normalize-whitespace: 5.1.1(postcss@8.5.3) - postcss-ordered-values: 5.1.3(postcss@8.5.3) - postcss-reduce-initial: 5.1.1(postcss@8.5.3) - postcss-reduce-transforms: 5.1.0(postcss@8.5.3) - postcss-svgo: 5.1.0(postcss@8.5.3) - postcss-unique-selectors: 5.1.1(postcss@8.5.3) - - cssnano-utils@3.1.0(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - - cssnano@5.1.13(postcss@8.5.3): - dependencies: - cssnano-preset-default: 5.2.13(postcss@8.5.3) - lilconfig: 2.0.6 - postcss: 8.5.3 - yaml: 1.10.2 - - csso@4.2.0: - dependencies: - css-tree: 1.1.3 - - csstype@3.1.1: {} - - data-view-buffer@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-data-view: 1.0.2 - - data-view-byte-length@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-data-view: 1.0.2 - - data-view-byte-offset@1.0.1: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-data-view: 1.0.2 - - debug@2.6.9: - dependencies: - ms: 2.0.0 - - debug@4.3.4: - dependencies: - ms: 2.1.2 - - decompress-response@6.0.0: - dependencies: - mimic-response: 3.1.0 - - deep-extend@0.6.0: {} - - deep-is@0.1.4: {} - - deepmerge@4.2.2: {} - - default-gateway@6.0.3: - dependencies: - execa: 5.1.1 - - defer-to-connect@2.0.1: {} - - define-data-property@1.1.4: - dependencies: - es-define-property: 1.0.1 - es-errors: 1.3.0 - gopd: 1.2.0 - - define-lazy-prop@2.0.0: {} - - define-properties@1.2.1: - dependencies: - define-data-property: 1.1.4 - has-property-descriptors: 1.0.2 - object-keys: 1.1.1 - - del@6.1.1: - dependencies: - globby: 11.1.0 - graceful-fs: 4.2.11 - is-glob: 4.0.3 - is-path-cwd: 2.2.0 - is-path-inside: 3.0.3 - p-map: 4.0.0 - rimraf: 3.0.2 - slash: 3.0.0 - - delayed-stream@1.0.0: {} - - depd@1.1.2: {} - - depd@2.0.0: {} - - destroy@1.2.0: {} - - detab@2.0.4: - dependencies: - repeat-string: 1.6.1 - - detect-node@2.1.0: {} - - detect-port-alt@1.1.6: - dependencies: - address: 1.2.1 - debug: 2.6.9 - transitivePeerDependencies: - - supports-color - - detect-port@1.5.1: - dependencies: - address: 1.2.1 - debug: 4.3.4 - transitivePeerDependencies: - - supports-color - - dir-glob@3.0.1: - dependencies: - path-type: 4.0.0 - - dns-equal@1.0.0: {} - - dns-packet@5.4.0: - dependencies: - '@leichtgewicht/ip-codec': 2.0.4 - - doctrine@2.1.0: - dependencies: - esutils: 2.0.3 - - doctrine@3.0.0: - dependencies: - esutils: 2.0.3 - - dom-converter@0.2.0: - dependencies: - utila: 0.4.0 - - dom-serializer@1.4.1: - dependencies: - domelementtype: 2.3.0 - domhandler: 4.3.1 - entities: 2.2.0 - - dom-serializer@2.0.0: - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - entities: 4.5.0 - - domelementtype@2.3.0: {} - - domhandler@4.3.1: - dependencies: - domelementtype: 2.3.0 - - domhandler@5.0.3: - dependencies: - domelementtype: 2.3.0 - - domutils@2.8.0: - dependencies: - dom-serializer: 1.4.1 - domelementtype: 2.3.0 - domhandler: 4.3.1 - - domutils@3.2.2: - dependencies: - dom-serializer: 2.0.0 - domelementtype: 2.3.0 - domhandler: 5.0.3 - - dot-case@3.0.4: - dependencies: - no-case: 3.0.4 - tslib: 2.4.0 - - dot-prop@5.3.0: - dependencies: - is-obj: 2.0.0 - - dunder-proto@1.0.1: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-errors: 1.3.0 - gopd: 1.2.0 - - duplexer@0.1.2: {} - - eastasianwidth@0.2.0: {} - - ee-first@1.1.1: {} - - electron-to-chromium@1.5.286: {} - - emoji-regex@8.0.0: {} - - emoji-regex@9.2.2: {} - - emojis-list@3.0.0: {} - - emoticon@3.2.0: {} - - encodeurl@1.0.2: {} - - encodeurl@2.0.0: {} - - encoding-sniffer@0.2.0: - dependencies: - iconv-lite: 0.6.3 - whatwg-encoding: 3.1.1 - - end-of-stream@1.4.4: - dependencies: - once: 1.4.0 - - enhanced-resolve@5.19.0: - dependencies: - graceful-fs: 4.2.11 - tapable: 2.3.0 - - enquirer@2.3.6: - dependencies: - ansi-colors: 4.1.3 - - entities@2.2.0: {} - - entities@4.5.0: {} - - error-ex@1.3.2: - dependencies: - is-arrayish: 0.2.1 - - es-abstract@1.23.9: - dependencies: - array-buffer-byte-length: 1.0.2 - arraybuffer.prototype.slice: 1.0.4 - available-typed-arrays: 1.0.7 - call-bind: 1.0.8 - call-bound: 1.0.4 - data-view-buffer: 1.0.2 - data-view-byte-length: 1.0.2 - data-view-byte-offset: 1.0.1 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - es-set-tostringtag: 2.1.0 - es-to-primitive: 1.3.0 - function.prototype.name: 1.1.8 - get-intrinsic: 1.3.0 - get-proto: 1.0.1 - get-symbol-description: 1.1.0 - globalthis: 1.0.4 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 - has-proto: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.2 - internal-slot: 1.1.0 - is-array-buffer: 3.0.5 - is-callable: 1.2.7 - is-data-view: 1.0.2 - is-regex: 1.2.1 - is-shared-array-buffer: 1.0.4 - is-string: 1.1.1 - is-typed-array: 1.1.15 - is-weakref: 1.1.1 - math-intrinsics: 1.1.0 - object-inspect: 1.13.4 - object-keys: 1.1.1 - object.assign: 4.1.7 - own-keys: 1.0.1 - regexp.prototype.flags: 1.5.4 - safe-array-concat: 1.1.3 - safe-push-apply: 1.0.0 - safe-regex-test: 1.1.0 - set-proto: 1.0.0 - string.prototype.trim: 1.2.10 - string.prototype.trimend: 1.0.9 - string.prototype.trimstart: 1.0.8 - typed-array-buffer: 1.0.3 - typed-array-byte-length: 1.0.3 - typed-array-byte-offset: 1.0.4 - typed-array-length: 1.0.7 - unbox-primitive: 1.1.0 - which-typed-array: 1.1.19 - - es-define-property@1.0.1: {} - - es-errors@1.3.0: {} - - es-iterator-helpers@1.2.1: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-abstract: 1.23.9 - es-errors: 1.3.0 - es-set-tostringtag: 2.1.0 - function-bind: 1.1.2 - get-intrinsic: 1.3.0 - globalthis: 1.0.4 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 - has-proto: 1.2.0 - has-symbols: 1.1.0 - internal-slot: 1.1.0 - iterator.prototype: 1.1.5 - safe-array-concat: 1.1.3 - - es-module-lexer@2.0.0: {} - - es-object-atoms@1.1.1: - dependencies: - es-errors: 1.3.0 - - es-set-tostringtag@2.1.0: - dependencies: - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - has-tostringtag: 1.0.2 - hasown: 2.0.2 - - es-shim-unscopables@1.1.0: - dependencies: - hasown: 2.0.2 - - es-to-primitive@1.3.0: - dependencies: - is-callable: 1.2.7 - is-date-object: 1.1.0 - is-symbol: 1.1.1 - - escalade@3.2.0: {} - - escape-goat@2.1.1: {} - - escape-html@1.0.3: {} - - escape-string-regexp@1.0.5: {} - - escape-string-regexp@4.0.0: {} - - eslint-plugin-react@7.37.4(eslint@7.32.0): - dependencies: - array-includes: 3.1.8 - array.prototype.findlast: 1.2.5 - array.prototype.flatmap: 1.3.3 - array.prototype.tosorted: 1.1.4 - doctrine: 2.1.0 - es-iterator-helpers: 1.2.1 - eslint: 7.32.0 - estraverse: 5.3.0 - hasown: 2.0.2 - jsx-ast-utils: 3.3.3 - minimatch: 3.1.2 - object.entries: 1.1.9 - object.fromentries: 2.0.8 - object.values: 1.2.1 - prop-types: 15.8.1 - resolve: 2.0.0-next.5 - semver: 6.3.1 - string.prototype.matchall: 4.0.12 - string.prototype.repeat: 1.0.0 - - eslint-scope@5.1.1: - dependencies: - esrecurse: 4.3.0 - estraverse: 4.3.0 - - eslint-utils@2.1.0: - dependencies: - eslint-visitor-keys: 1.3.0 - - eslint-visitor-keys@1.3.0: {} - - eslint-visitor-keys@2.1.0: {} - - eslint@7.32.0: - dependencies: - '@babel/code-frame': 7.12.11 - '@eslint/eslintrc': 0.4.3 - '@humanwhocodes/config-array': 0.5.0 - ajv: 6.12.6 - chalk: 4.1.2 - cross-spawn: 7.0.6 - debug: 4.3.4 - doctrine: 3.0.0 - enquirer: 2.3.6 - escape-string-regexp: 4.0.0 - eslint-scope: 5.1.1 - eslint-utils: 2.1.0 - eslint-visitor-keys: 2.1.0 - espree: 7.3.1 - esquery: 1.4.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 6.0.1 - functional-red-black-tree: 1.0.1 - glob-parent: 5.1.2 - globals: 13.17.0 - ignore: 4.0.6 - import-fresh: 3.3.0 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - js-yaml: 3.14.1 - json-stable-stringify-without-jsonify: 1.0.1 - levn: 0.4.1 - lodash.merge: 4.6.2 - minimatch: 3.1.2 - natural-compare: 1.4.0 - optionator: 0.9.1 - progress: 2.0.3 - regexpp: 3.2.0 - semver: 7.7.1 - strip-ansi: 6.0.1 - strip-json-comments: 3.1.1 - table: 6.8.0 - text-table: 0.2.0 - v8-compile-cache: 2.3.0 - transitivePeerDependencies: - - supports-color - - espree@7.3.1: - dependencies: - acorn: 7.4.1 - acorn-jsx: 5.3.2(acorn@7.4.1) - eslint-visitor-keys: 1.3.0 - - esprima@4.0.1: {} - - esquery@1.4.0: - dependencies: - estraverse: 5.3.0 - - esrecurse@4.3.0: - dependencies: - estraverse: 5.3.0 - - estraverse@4.3.0: {} - - estraverse@5.3.0: {} - - esutils@2.0.3: {} - - eta@2.2.0: {} - - etag@1.8.1: {} - - eval@0.1.8: - dependencies: - '@types/node': 18.11.3 - require-like: 0.1.2 - - eventemitter3@4.0.7: {} - - events@3.3.0: {} - - execa@5.1.1: - dependencies: - cross-spawn: 7.0.6 - get-stream: 6.0.1 - human-signals: 2.1.0 - is-stream: 2.0.1 - merge-stream: 2.0.0 - npm-run-path: 4.0.1 - onetime: 5.1.2 - signal-exit: 3.0.7 - strip-final-newline: 2.0.0 - - express@4.21.2: - dependencies: - accepts: 1.3.8 - array-flatten: 1.1.1 - body-parser: 1.20.3 - content-disposition: 0.5.4 - content-type: 1.0.5 - cookie: 0.7.1 - cookie-signature: 1.0.6 - debug: 2.6.9 - depd: 2.0.0 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - finalhandler: 1.3.1 - fresh: 0.5.2 - http-errors: 2.0.0 - merge-descriptors: 1.0.3 - methods: 1.1.2 - on-finished: 2.4.1 - parseurl: 1.3.3 - path-to-regexp: 0.1.12 - proxy-addr: 2.0.7 - qs: 6.13.0 - range-parser: 1.2.1 - safe-buffer: 5.2.1 - send: 0.19.0 - serve-static: 1.16.2 - setprototypeof: 1.2.0 - statuses: 2.0.1 - type-is: 1.6.18 - utils-merge: 1.0.1 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - - extend-shallow@2.0.1: - dependencies: - is-extendable: 0.1.1 - - extend@3.0.2: {} - - fast-deep-equal@3.1.3: {} - - fast-glob@3.2.12: - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.8 - - fast-json-stable-stringify@2.1.0: {} - - fast-levenshtein@2.0.6: {} - - fast-uri@3.0.6: {} - - fastq@1.13.0: - dependencies: - reusify: 1.0.4 - - faye-websocket@0.11.4: - dependencies: - websocket-driver: 0.7.4 - - fbemitter@3.0.0: - dependencies: - fbjs: 3.0.5 - transitivePeerDependencies: - - encoding - - fbjs-css-vars@1.0.2: {} - - fbjs@3.0.5: - dependencies: - cross-fetch: 3.2.0 - fbjs-css-vars: 1.0.2 - loose-envify: 1.4.0 - object-assign: 4.1.1 - promise: 7.3.1 - setimmediate: 1.0.5 - ua-parser-js: 1.0.40 - transitivePeerDependencies: - - encoding - - feed@4.2.2: - dependencies: - xml-js: 1.6.11 - - file-entry-cache@6.0.1: - dependencies: - flat-cache: 3.0.4 - - file-loader@6.2.0(webpack@5.105.0): - dependencies: - loader-utils: 2.0.4 - schema-utils: 3.1.1 - webpack: 5.105.0 - - filesize@8.0.7: {} - - fill-range@7.1.1: - dependencies: - to-regex-range: 5.0.1 - - finalhandler@1.3.1: - dependencies: - debug: 2.6.9 - encodeurl: 2.0.0 - escape-html: 1.0.3 - on-finished: 2.4.1 - parseurl: 1.3.3 - statuses: 2.0.1 - unpipe: 1.0.0 - transitivePeerDependencies: - - supports-color - - find-cache-dir@3.3.2: - dependencies: - commondir: 1.0.1 - make-dir: 3.1.0 - pkg-dir: 4.2.0 - - find-up@3.0.0: - dependencies: - locate-path: 3.0.0 - - find-up@4.1.0: - dependencies: - locate-path: 5.0.0 - path-exists: 4.0.0 - - find-up@5.0.0: - dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 - - flat-cache@3.0.4: - dependencies: - flatted: 3.2.7 - rimraf: 3.0.2 - - flatted@3.2.7: {} - - flux@4.0.4(react@16.14.0): - dependencies: - fbemitter: 3.0.0 - fbjs: 3.0.5 - react: 16.14.0 - transitivePeerDependencies: - - encoding - - follow-redirects@1.15.11: {} - - for-each@0.3.5: - dependencies: - is-callable: 1.2.7 - - fork-ts-checker-webpack-plugin@6.5.2(eslint@7.32.0)(typescript@5.8.2)(webpack@5.105.0): - dependencies: - '@babel/code-frame': 7.26.2 - '@types/json-schema': 7.0.15 - chalk: 4.1.2 - chokidar: 3.5.3 - cosmiconfig: 6.0.0 - deepmerge: 4.2.2 - fs-extra: 9.1.0 - glob: 7.2.3 - memfs: 3.4.12 - minimatch: 3.1.2 - schema-utils: 2.7.0 - semver: 7.7.1 - tapable: 1.1.3 - typescript: 5.8.2 - webpack: 5.105.0 - optionalDependencies: - eslint: 7.32.0 - - form-data@4.0.5: - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - es-set-tostringtag: 2.1.0 - hasown: 2.0.2 - mime-types: 2.1.35 - - forwarded@0.2.0: {} - - fraction.js@4.2.0: {} - - fresh@0.5.2: {} - - fs-extra@10.1.0: - dependencies: - graceful-fs: 4.2.11 - jsonfile: 6.1.0 - universalify: 2.0.0 - - fs-extra@9.1.0: - dependencies: - at-least-node: 1.0.0 - graceful-fs: 4.2.11 - jsonfile: 6.1.0 - universalify: 2.0.0 - - fs-monkey@1.0.3: {} - - fs.realpath@1.0.0: {} - - fsevents@2.3.3: - optional: true - - function-bind@1.1.2: {} - - function.prototype.name@1.1.8: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - functions-have-names: 1.2.3 - hasown: 2.0.2 - is-callable: 1.2.7 - - functional-red-black-tree@1.0.1: {} - - functions-have-names@1.2.3: {} - - gensync@1.0.0-beta.2: {} - - get-intrinsic@1.3.0: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - function-bind: 1.1.2 - get-proto: 1.0.1 - gopd: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.2 - math-intrinsics: 1.1.0 - - get-own-enumerable-property-symbols@3.0.2: {} - - get-proto@1.0.1: - dependencies: - dunder-proto: 1.0.1 - es-object-atoms: 1.1.1 - - get-stream@5.2.0: - dependencies: - pump: 3.0.2 - - get-stream@6.0.1: {} - - get-symbol-description@1.1.0: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - - github-slugger@1.4.0: {} - - glob-parent@5.1.2: - dependencies: - is-glob: 4.0.3 - - glob-parent@6.0.2: - dependencies: - is-glob: 4.0.3 - - glob-to-regexp@0.4.1: {} - - glob@7.2.3: - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.1.2 - once: 1.4.0 - path-is-absolute: 1.0.1 - - global-dirs@3.0.0: - dependencies: - ini: 2.0.0 - - global-modules@2.0.0: - dependencies: - global-prefix: 3.0.0 - - global-prefix@3.0.0: - dependencies: - ini: 1.3.8 - kind-of: 6.0.3 - which: 1.3.1 - - globals@11.12.0: {} - - globals@13.17.0: - dependencies: - type-fest: 0.20.2 - - globalthis@1.0.4: - dependencies: - define-properties: 1.2.1 - gopd: 1.2.0 - - globby@11.1.0: - dependencies: - array-union: 2.1.0 - dir-glob: 3.0.1 - fast-glob: 3.2.12 - ignore: 5.2.4 - merge2: 1.4.1 - slash: 3.0.0 - - globby@13.1.3: - dependencies: - dir-glob: 3.0.1 - fast-glob: 3.2.12 - ignore: 5.2.4 - merge2: 1.4.1 - slash: 4.0.0 - - gopd@1.2.0: {} - - got@11.8.6: - dependencies: - '@sindresorhus/is': 4.6.0 - '@szmarczak/http-timer': 4.0.6 - '@types/cacheable-request': 6.0.3 - '@types/responselike': 1.0.3 - cacheable-lookup: 5.0.4 - cacheable-request: 7.0.4 - decompress-response: 6.0.0 - http2-wrapper: 1.0.3 - lowercase-keys: 2.0.0 - p-cancelable: 2.1.1 - responselike: 2.0.1 - - graceful-fs@4.2.11: {} - - gray-matter@4.0.3: - dependencies: - js-yaml: 3.14.1 - kind-of: 6.0.3 - section-matter: 1.0.0 - strip-bom-string: 1.0.0 - - gzip-size@6.0.0: - dependencies: - duplexer: 0.1.2 - - handle-thing@2.0.1: {} - - has-bigints@1.1.0: {} - - has-flag@3.0.0: {} - - has-flag@4.0.0: {} - - has-property-descriptors@1.0.2: - dependencies: - es-define-property: 1.0.1 - - has-proto@1.2.0: - dependencies: - dunder-proto: 1.0.1 - - has-symbols@1.1.0: {} - - has-tostringtag@1.0.2: - dependencies: - has-symbols: 1.1.0 - - has-yarn@2.1.0: {} - - hasown@2.0.2: - dependencies: - function-bind: 1.1.2 - - hast-to-hyperscript@9.0.1: - dependencies: - '@types/unist': 2.0.6 - comma-separated-tokens: 1.0.8 - property-information: 5.6.0 - space-separated-tokens: 1.1.5 - style-to-object: 0.3.0 - unist-util-is: 4.1.0 - web-namespaces: 1.1.4 - - hast-util-from-parse5@6.0.1: - dependencies: - '@types/parse5': 5.0.3 - hastscript: 6.0.0 - property-information: 5.6.0 - vfile: 4.2.1 - vfile-location: 3.2.0 - web-namespaces: 1.1.4 - - hast-util-is-element@1.1.0: {} - - hast-util-parse-selector@2.2.5: {} - - hast-util-raw@6.0.1: - dependencies: - '@types/hast': 2.3.4 - hast-util-from-parse5: 6.0.1 - hast-util-to-parse5: 6.0.0 - html-void-elements: 1.0.5 - parse5: 6.0.1 - unist-util-position: 3.1.0 - vfile: 4.2.1 - web-namespaces: 1.1.4 - xtend: 4.0.2 - zwitch: 1.0.5 - - hast-util-to-parse5@6.0.0: - dependencies: - hast-to-hyperscript: 9.0.1 - property-information: 5.6.0 - web-namespaces: 1.1.4 - xtend: 4.0.2 - zwitch: 1.0.5 - - hast-util-to-text@2.0.1: - dependencies: - hast-util-is-element: 1.1.0 - repeat-string: 1.6.1 - unist-util-find-after: 3.0.0 - - hastscript@6.0.0: - dependencies: - '@types/hast': 2.3.4 - comma-separated-tokens: 1.0.8 - hast-util-parse-selector: 2.2.5 - property-information: 5.6.0 - space-separated-tokens: 1.1.5 - - he@1.2.0: {} - - history@4.10.1: - dependencies: - '@babel/runtime': 7.27.0 - loose-envify: 1.4.0 - resolve-pathname: 3.0.0 - tiny-invariant: 1.3.1 - tiny-warning: 1.0.3 - value-equal: 1.0.1 - - hoist-non-react-statics@3.3.2: - dependencies: - react-is: 16.13.1 - - hpack.js@2.1.6: - dependencies: - inherits: 2.0.4 - obuf: 1.1.2 - readable-stream: 2.3.7 - wbuf: 1.7.3 - - html-entities@2.3.3: {} - - html-minifier-terser@6.1.0: - dependencies: - camel-case: 4.1.2 - clean-css: 5.3.1 - commander: 8.3.0 - he: 1.2.0 - param-case: 3.0.4 - relateurl: 0.2.7 - terser: 5.39.0 - - html-tags@3.2.0: {} - - html-void-elements@1.0.5: {} - - html-webpack-plugin@5.5.0(webpack@5.105.0): - dependencies: - '@types/html-minifier-terser': 6.1.0 - html-minifier-terser: 6.1.0 - lodash: 4.17.21 - pretty-error: 4.0.0 - tapable: 2.3.0 - webpack: 5.105.0 - - htmlparser2@6.1.0: - dependencies: - domelementtype: 2.3.0 - domhandler: 4.3.1 - domutils: 2.8.0 - entities: 2.2.0 - - htmlparser2@9.1.0: - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - domutils: 3.2.2 - entities: 4.5.0 - - http-cache-semantics@4.2.0: {} - - http-deceiver@1.2.7: {} - - http-errors@1.6.3: - dependencies: - depd: 1.1.2 - inherits: 2.0.3 - setprototypeof: 1.1.0 - statuses: 1.5.0 - - http-errors@2.0.0: - dependencies: - depd: 2.0.0 - inherits: 2.0.4 - setprototypeof: 1.2.0 - statuses: 2.0.1 - toidentifier: 1.0.1 - - http-parser-js@0.5.8: {} - - http-proxy-middleware@2.0.9(@types/express@4.17.15): - dependencies: - '@types/http-proxy': 1.17.9 - http-proxy: 1.18.1 - is-glob: 4.0.3 - is-plain-obj: 3.0.0 - micromatch: 4.0.8 - optionalDependencies: - '@types/express': 4.17.15 - transitivePeerDependencies: - - debug - - http-proxy@1.18.1: - dependencies: - eventemitter3: 4.0.7 - follow-redirects: 1.15.11 - requires-port: 1.0.0 - transitivePeerDependencies: - - debug - - http2-wrapper@1.0.3: - dependencies: - quick-lru: 5.1.1 - resolve-alpn: 1.2.1 - - human-signals@2.1.0: {} - - iconv-lite@0.4.24: - dependencies: - safer-buffer: 2.1.2 - - iconv-lite@0.6.3: - dependencies: - safer-buffer: 2.1.2 - - icss-utils@5.1.0(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - - ignore@4.0.6: {} - - ignore@5.2.4: {} - - image-size@1.0.2: - dependencies: - queue: 6.0.2 - - immer@9.0.17: {} - - import-fresh@3.3.0: - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 - - import-lazy@2.1.0: {} - - imurmurhash@0.1.4: {} - - indent-string@4.0.0: {} - - infima@0.2.0-alpha.43: {} - - inflight@1.0.6: - dependencies: - once: 1.4.0 - wrappy: 1.0.2 - - inherits@2.0.3: {} - - inherits@2.0.4: {} - - ini@1.3.8: {} - - ini@2.0.0: {} - - inline-style-parser@0.1.1: {} - - internal-slot@1.1.0: - dependencies: - es-errors: 1.3.0 - hasown: 2.0.2 - side-channel: 1.1.0 - - interpret@1.4.0: {} - - invariant@2.2.4: - dependencies: - loose-envify: 1.4.0 - - ipaddr.js@1.9.1: {} - - ipaddr.js@2.0.1: {} - - is-alphabetical@1.0.4: {} - - is-alphanumerical@1.0.4: - dependencies: - is-alphabetical: 1.0.4 - is-decimal: 1.0.4 - - is-array-buffer@3.0.5: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - - is-arrayish@0.2.1: {} - - is-async-function@2.1.1: - dependencies: - async-function: 1.0.0 - call-bound: 1.0.4 - get-proto: 1.0.1 - has-tostringtag: 1.0.2 - safe-regex-test: 1.1.0 - - is-bigint@1.1.0: - dependencies: - has-bigints: 1.1.0 - - is-binary-path@2.1.0: - dependencies: - binary-extensions: 2.2.0 - - is-boolean-object@1.2.2: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - - is-buffer@2.0.5: {} - - is-callable@1.2.7: {} - - is-ci@2.0.0: - dependencies: - ci-info: 2.0.0 - - is-core-module@2.16.1: - dependencies: - hasown: 2.0.2 - - is-data-view@1.0.2: - dependencies: - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - is-typed-array: 1.1.15 - - is-date-object@1.1.0: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - - is-decimal@1.0.4: {} - - is-docker@2.2.1: {} - - is-extendable@0.1.1: {} - - is-extglob@2.1.1: {} - - is-finalizationregistry@1.1.1: - dependencies: - call-bound: 1.0.4 - - is-fullwidth-code-point@3.0.0: {} - - is-generator-function@1.1.0: - dependencies: - call-bound: 1.0.4 - get-proto: 1.0.1 - has-tostringtag: 1.0.2 - safe-regex-test: 1.1.0 - - is-glob@4.0.3: - dependencies: - is-extglob: 2.1.1 - - is-hexadecimal@1.0.4: {} - - is-installed-globally@0.4.0: - dependencies: - global-dirs: 3.0.0 - is-path-inside: 3.0.3 - - is-map@2.0.3: {} - - is-npm@5.0.0: {} - - is-number-object@1.1.1: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - - is-number@7.0.0: {} - - is-obj@1.0.1: {} - - is-obj@2.0.0: {} - - is-path-cwd@2.2.0: {} - - is-path-inside@3.0.3: {} - - is-plain-obj@2.1.0: {} - - is-plain-obj@3.0.0: {} - - is-plain-object@2.0.4: - dependencies: - isobject: 3.0.1 - - is-regex@1.2.1: - dependencies: - call-bound: 1.0.4 - gopd: 1.2.0 - has-tostringtag: 1.0.2 - hasown: 2.0.2 - - is-regexp@1.0.0: {} - - is-root@2.1.0: {} - - is-set@2.0.3: {} - - is-shared-array-buffer@1.0.4: - dependencies: - call-bound: 1.0.4 - - is-stream@2.0.1: {} - - is-string@1.1.1: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - - is-symbol@1.1.1: - dependencies: - call-bound: 1.0.4 - has-symbols: 1.1.0 - safe-regex-test: 1.1.0 - - is-typed-array@1.1.15: - dependencies: - which-typed-array: 1.1.19 - - is-typedarray@1.0.0: {} - - is-weakmap@2.0.2: {} - - is-weakref@1.1.1: - dependencies: - call-bound: 1.0.4 - - is-weakset@2.0.4: - dependencies: - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - - is-whitespace-character@1.0.4: {} - - is-word-character@1.0.4: {} - - is-wsl@2.2.0: - dependencies: - is-docker: 2.2.1 - - is-yarn-global@0.3.0: {} - - isarray@0.0.1: {} - - isarray@1.0.0: {} - - isarray@2.0.5: {} - - isexe@2.0.0: {} - - isobject@3.0.1: {} - - iterator.prototype@1.1.5: - dependencies: - define-data-property: 1.1.4 - es-object-atoms: 1.1.1 - get-intrinsic: 1.3.0 - get-proto: 1.0.1 - has-symbols: 1.1.0 - set-function-name: 2.0.2 - - jest-util@29.3.1: - dependencies: - '@jest/types': 29.3.1 - '@types/node': 18.11.3 - chalk: 4.1.2 - ci-info: 3.7.1 - graceful-fs: 4.2.11 - picomatch: 2.3.1 - - jest-worker@27.5.1: - dependencies: - '@types/node': 18.11.3 - merge-stream: 2.0.0 - supports-color: 8.1.1 - - jest-worker@29.3.1: - dependencies: - '@types/node': 18.11.3 - jest-util: 29.3.1 - merge-stream: 2.0.0 - supports-color: 8.1.1 - - joi@17.13.3: - dependencies: - '@hapi/hoek': 9.3.0 - '@hapi/topo': 5.1.0 - '@sideway/address': 4.1.5 - '@sideway/formula': 3.0.1 - '@sideway/pinpoint': 2.0.0 - - js-tokens@4.0.0: {} - - js-yaml@3.14.1: - dependencies: - argparse: 1.0.10 - esprima: 4.0.1 - - js-yaml@4.1.0: - dependencies: - argparse: 2.0.1 - - jsesc@0.5.0: {} - - jsesc@2.5.2: {} - - json-buffer@3.0.1: {} - - json-parse-even-better-errors@2.3.1: {} - - json-schema-traverse@0.4.1: {} - - json-schema-traverse@1.0.0: {} - - json-stable-stringify-without-jsonify@1.0.1: {} - - json5@2.2.3: {} - - jsonfile@6.1.0: - dependencies: - universalify: 2.0.0 - optionalDependencies: - graceful-fs: 4.2.11 - - jsx-ast-utils@3.3.3: - dependencies: - array-includes: 3.1.8 - object.assign: 4.1.7 - - katex@0.16.28: - dependencies: - commander: 8.3.0 - - keyv@4.5.4: - dependencies: - json-buffer: 3.0.1 - - kind-of@6.0.3: {} - - kleur@3.0.3: {} - - klona@2.0.6: {} - - latest-version@5.1.0: - dependencies: - package-json: 6.5.0 - - leven@3.1.0: {} - - levn@0.4.1: - dependencies: - prelude-ls: 1.2.1 - type-check: 0.4.0 - - lilconfig@2.0.6: {} - - lines-and-columns@1.2.4: {} - - loader-runner@4.3.1: {} - - loader-utils@2.0.4: - dependencies: - big.js: 5.2.2 - emojis-list: 3.0.0 - json5: 2.2.3 - - loader-utils@3.2.1: {} - - locate-path@3.0.0: - dependencies: - p-locate: 3.0.0 - path-exists: 3.0.0 - - locate-path@5.0.0: - dependencies: - p-locate: 4.1.0 - - locate-path@6.0.0: - dependencies: - p-locate: 5.0.0 - - lodash.curry@4.1.1: {} - - lodash.debounce@4.0.8: {} - - lodash.flow@3.5.0: {} - - lodash.memoize@4.1.2: {} - - lodash.merge@4.6.2: {} - - lodash.truncate@4.4.2: {} - - lodash.uniq@4.5.0: {} - - lodash@4.17.21: {} - - loose-envify@1.4.0: - dependencies: - js-tokens: 4.0.0 - - lower-case@2.0.2: - dependencies: - tslib: 2.4.0 - - lowercase-keys@2.0.0: {} - - make-dir@3.1.0: - dependencies: - semver: 6.3.1 - - markdown-escapes@1.0.4: {} - - math-intrinsics@1.1.0: {} - - mdast-squeeze-paragraphs@4.0.0: - dependencies: - unist-util-remove: 2.1.0 - - mdast-util-definitions@4.0.0: - dependencies: - unist-util-visit: 2.0.3 - - mdast-util-to-hast@10.0.1: - dependencies: - '@types/mdast': 3.0.10 - '@types/unist': 2.0.6 - mdast-util-definitions: 4.0.0 - mdurl: 1.0.1 - unist-builder: 2.0.3 - unist-util-generated: 1.1.6 - unist-util-position: 3.1.0 - unist-util-visit: 2.0.3 - - mdast-util-to-string@2.0.0: {} - - mdn-data@2.0.14: {} - - mdurl@1.0.1: {} - - media-typer@0.3.0: {} - - memfs@3.4.12: - dependencies: - fs-monkey: 1.0.3 - - merge-descriptors@1.0.3: {} - - merge-stream@2.0.0: {} - - merge2@1.4.1: {} - - methods@1.1.2: {} - - micromatch@4.0.8: - dependencies: - braces: 3.0.3 - picomatch: 2.3.1 - - mime-db@1.33.0: {} - - mime-db@1.52.0: {} - - mime-types@2.1.18: - dependencies: - mime-db: 1.33.0 - - mime-types@2.1.35: - dependencies: - mime-db: 1.52.0 - - mime@1.6.0: {} - - mimic-fn@2.1.0: {} - - mimic-response@1.0.1: {} - - mimic-response@3.1.0: {} - - mini-css-extract-plugin@2.7.2(webpack@5.105.0): - dependencies: - schema-utils: 4.3.3 - webpack: 5.105.0 - - minimalistic-assert@1.0.1: {} - - minimatch@3.1.2: - dependencies: - brace-expansion: 1.1.12 - - minimist@1.2.8: {} - - mrmime@1.0.1: {} - - ms@2.0.0: {} - - ms@2.1.2: {} - - ms@2.1.3: {} - - multicast-dns@7.2.5: - dependencies: - dns-packet: 5.4.0 - thunky: 1.1.0 - - nanoid@3.3.11: {} - - natural-compare@1.4.0: {} - - negotiator@0.6.3: {} - - neo-async@2.6.2: {} - - no-case@3.0.4: - dependencies: - lower-case: 2.0.2 - tslib: 2.4.0 - - node-emoji@1.11.0: - dependencies: - lodash: 4.17.21 - - node-fetch@2.7.0: - dependencies: - whatwg-url: 5.0.0 - - node-forge@1.3.1: {} - - node-releases@2.0.27: {} - - normalize-path@3.0.0: {} - - normalize-range@0.1.2: {} - - normalize-url@6.1.0: {} - - npm-run-path@4.0.1: - dependencies: - path-key: 3.1.1 - - nprogress@0.2.0: {} - - nth-check@2.1.1: - dependencies: - boolbase: 1.0.0 - - object-assign@4.1.1: {} - - object-inspect@1.13.4: {} - - object-keys@1.1.1: {} - - object.assign@4.1.7: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - has-symbols: 1.1.0 - object-keys: 1.1.1 - - object.entries@1.1.9: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - - object.fromentries@2.0.8: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.9 - es-object-atoms: 1.1.1 - - object.values@1.2.1: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - - obuf@1.1.2: {} - - on-finished@2.4.1: - dependencies: - ee-first: 1.1.1 - - on-headers@1.0.2: {} - - once@1.4.0: - dependencies: - wrappy: 1.0.2 - - onetime@5.1.2: - dependencies: - mimic-fn: 2.1.0 - - open@8.4.0: - dependencies: - define-lazy-prop: 2.0.0 - is-docker: 2.2.1 - is-wsl: 2.2.0 - - opener@1.5.2: {} - - optionator@0.9.1: - dependencies: - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.4.1 - prelude-ls: 1.2.1 - type-check: 0.4.0 - word-wrap: 1.2.4 - - own-keys@1.0.1: - dependencies: - get-intrinsic: 1.3.0 - object-keys: 1.1.1 - safe-push-apply: 1.0.0 - - p-cancelable@2.1.1: {} - - p-limit@2.3.0: - dependencies: - p-try: 2.2.0 - - p-limit@3.1.0: - dependencies: - yocto-queue: 0.1.0 - - p-locate@3.0.0: - dependencies: - p-limit: 2.3.0 - - p-locate@4.1.0: - dependencies: - p-limit: 2.3.0 - - p-locate@5.0.0: - dependencies: - p-limit: 3.1.0 - - p-map@4.0.0: - dependencies: - aggregate-error: 3.1.0 - - p-retry@4.6.2: - dependencies: - '@types/retry': 0.12.0 - retry: 0.13.1 - - p-try@2.2.0: {} - - package-json@6.5.0: - dependencies: - got: 11.8.6 - registry-auth-token: 4.2.2 - registry-url: 5.1.0 - semver: 6.3.1 - - param-case@3.0.4: - dependencies: - dot-case: 3.0.4 - tslib: 2.4.0 - - parent-module@1.0.1: - dependencies: - callsites: 3.1.0 - - parse-entities@2.0.0: - dependencies: - character-entities: 1.2.4 - character-entities-legacy: 1.1.4 - character-reference-invalid: 1.1.4 - is-alphanumerical: 1.0.4 - is-decimal: 1.0.4 - is-hexadecimal: 1.0.4 - - parse-json@5.2.0: - dependencies: - '@babel/code-frame': 7.26.2 - error-ex: 1.3.2 - json-parse-even-better-errors: 2.3.1 - lines-and-columns: 1.2.4 - - parse-numeric-range@1.3.0: {} - - parse5-htmlparser2-tree-adapter@7.1.0: - dependencies: - domhandler: 5.0.3 - parse5: 7.2.1 - - parse5-parser-stream@7.1.2: - dependencies: - parse5: 7.2.1 - - parse5@6.0.1: {} - - parse5@7.2.1: - dependencies: - entities: 4.5.0 - - parseurl@1.3.3: {} - - pascal-case@3.1.2: - dependencies: - no-case: 3.0.4 - tslib: 2.4.0 - - path-exists@3.0.0: {} - - path-exists@4.0.0: {} - - path-is-absolute@1.0.1: {} - - path-is-inside@1.0.2: {} - - path-key@3.1.1: {} - - path-parse@1.0.7: {} - - path-to-regexp@0.1.12: {} - - path-to-regexp@1.9.0: - dependencies: - isarray: 0.0.1 - - path-to-regexp@3.3.0: {} - - path-type@4.0.0: {} - - picocolors@1.1.1: {} - - picomatch@2.3.1: {} - - pkg-dir@4.2.0: - dependencies: - find-up: 4.1.0 - - pkg-up@3.1.0: - dependencies: - find-up: 3.0.0 - - possible-typed-array-names@1.1.0: {} - - postcss-calc@8.2.4(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - postcss-selector-parser: 6.0.10 - postcss-value-parser: 4.2.0 - - postcss-colormin@5.3.0(postcss@8.5.3): - dependencies: - browserslist: 4.28.1 - caniuse-api: 3.0.0 - colord: 2.9.3 - postcss: 8.5.3 - postcss-value-parser: 4.2.0 - - postcss-convert-values@5.1.3(postcss@8.5.3): - dependencies: - browserslist: 4.28.1 - postcss: 8.5.3 - postcss-value-parser: 4.2.0 - - postcss-discard-comments@5.1.2(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - - postcss-discard-duplicates@5.1.0(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - - postcss-discard-empty@5.1.1(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - - postcss-discard-overridden@5.1.0(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - - postcss-discard-unused@5.1.0(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - postcss-selector-parser: 6.0.10 - - postcss-loader@4.3.0(postcss@8.5.3)(webpack@5.105.0): - dependencies: - cosmiconfig: 7.0.1 - klona: 2.0.6 - loader-utils: 2.0.4 - postcss: 8.5.3 - schema-utils: 3.1.1 - semver: 7.7.1 - webpack: 5.105.0 - - postcss-loader@7.0.2(postcss@8.5.3)(webpack@5.105.0): - dependencies: - cosmiconfig: 7.0.1 - klona: 2.0.6 - postcss: 8.5.3 - semver: 7.7.1 - webpack: 5.105.0 - - postcss-merge-idents@5.1.1(postcss@8.5.3): - dependencies: - cssnano-utils: 3.1.0(postcss@8.5.3) - postcss: 8.5.3 - postcss-value-parser: 4.2.0 - - postcss-merge-longhand@5.1.7(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - postcss-value-parser: 4.2.0 - stylehacks: 5.1.1(postcss@8.5.3) - - postcss-merge-rules@5.1.3(postcss@8.5.3): - dependencies: - browserslist: 4.28.1 - caniuse-api: 3.0.0 - cssnano-utils: 3.1.0(postcss@8.5.3) - postcss: 8.5.3 - postcss-selector-parser: 6.0.10 - - postcss-minify-font-values@5.1.0(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - postcss-value-parser: 4.2.0 - - postcss-minify-gradients@5.1.1(postcss@8.5.3): - dependencies: - colord: 2.9.3 - cssnano-utils: 3.1.0(postcss@8.5.3) - postcss: 8.5.3 - postcss-value-parser: 4.2.0 - - postcss-minify-params@5.1.4(postcss@8.5.3): - dependencies: - browserslist: 4.28.1 - cssnano-utils: 3.1.0(postcss@8.5.3) - postcss: 8.5.3 - postcss-value-parser: 4.2.0 - - postcss-minify-selectors@5.2.1(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - postcss-selector-parser: 6.0.10 - - postcss-modules-extract-imports@3.0.0(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - - postcss-modules-local-by-default@4.0.0(postcss@8.5.3): - dependencies: - icss-utils: 5.1.0(postcss@8.5.3) - postcss: 8.5.3 - postcss-selector-parser: 6.0.10 - postcss-value-parser: 4.2.0 - - postcss-modules-scope@3.0.0(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - postcss-selector-parser: 6.0.10 - - postcss-modules-values@4.0.0(postcss@8.5.3): - dependencies: - icss-utils: 5.1.0(postcss@8.5.3) - postcss: 8.5.3 - - postcss-normalize-charset@5.1.0(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - - postcss-normalize-display-values@5.1.0(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - postcss-value-parser: 4.2.0 - - postcss-normalize-positions@5.1.1(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - postcss-value-parser: 4.2.0 - - postcss-normalize-repeat-style@5.1.1(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - postcss-value-parser: 4.2.0 - - postcss-normalize-string@5.1.0(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - postcss-value-parser: 4.2.0 - - postcss-normalize-timing-functions@5.1.0(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - postcss-value-parser: 4.2.0 - - postcss-normalize-unicode@5.1.1(postcss@8.5.3): - dependencies: - browserslist: 4.28.1 - postcss: 8.5.3 - postcss-value-parser: 4.2.0 - - postcss-normalize-url@5.1.0(postcss@8.5.3): - dependencies: - normalize-url: 6.1.0 - postcss: 8.5.3 - postcss-value-parser: 4.2.0 - - postcss-normalize-whitespace@5.1.1(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - postcss-value-parser: 4.2.0 - - postcss-ordered-values@5.1.3(postcss@8.5.3): - dependencies: - cssnano-utils: 3.1.0(postcss@8.5.3) - postcss: 8.5.3 - postcss-value-parser: 4.2.0 - - postcss-reduce-idents@5.2.0(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - postcss-value-parser: 4.2.0 - - postcss-reduce-initial@5.1.1(postcss@8.5.3): - dependencies: - browserslist: 4.28.1 - caniuse-api: 3.0.0 - postcss: 8.5.3 - - postcss-reduce-transforms@5.1.0(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - postcss-value-parser: 4.2.0 - - postcss-selector-parser@6.0.10: - dependencies: - cssesc: 3.0.0 - util-deprecate: 1.0.2 - - postcss-sort-media-queries@4.3.0(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - sort-css-media-queries: 2.1.0 - - postcss-svgo@5.1.0(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - postcss-value-parser: 4.2.0 - svgo: 2.8.0 - - postcss-unique-selectors@5.1.1(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - postcss-selector-parser: 6.0.10 - - postcss-value-parser@4.2.0: {} - - postcss-zindex@5.1.0(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - - postcss@8.5.3: - dependencies: - nanoid: 3.3.11 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - prelude-ls@1.2.1: {} - - prettier@2.8.8: {} - - pretty-error@4.0.0: - dependencies: - lodash: 4.17.21 - renderkid: 3.0.0 - - pretty-time@1.1.0: {} - - prism-react-renderer@1.3.5(react@16.14.0): - dependencies: - react: 16.14.0 - - prismjs@1.30.0: {} - - process-nextick-args@2.0.1: {} - - progress@2.0.3: {} - - promise@7.3.1: - dependencies: - asap: 2.0.6 - - prompts@2.4.2: - dependencies: - kleur: 3.0.3 - sisteransi: 1.0.5 - - prop-types@15.8.1: - dependencies: - loose-envify: 1.4.0 - object-assign: 4.1.1 - react-is: 16.13.1 - - property-information@5.6.0: - dependencies: - xtend: 4.0.2 - - proxy-addr@2.0.7: - dependencies: - forwarded: 0.2.0 - ipaddr.js: 1.9.1 - - proxy-from-env@1.1.0: {} - - pump@3.0.2: - dependencies: - end-of-stream: 1.4.4 - once: 1.4.0 - - punycode@2.1.1: {} - - pupa@2.1.1: - dependencies: - escape-goat: 2.1.1 - - pure-color@1.3.0: {} - - qs@6.13.0: - dependencies: - side-channel: 1.1.0 - - queue-microtask@1.2.3: {} - - queue@6.0.2: - dependencies: - inherits: 2.0.4 - - quick-lru@5.1.1: {} - - randombytes@2.1.0: - dependencies: - safe-buffer: 5.2.1 - - range-parser@1.2.0: {} - - range-parser@1.2.1: {} - - raw-body@2.5.2: - dependencies: - bytes: 3.1.2 - http-errors: 2.0.0 - iconv-lite: 0.4.24 - unpipe: 1.0.0 - - rc@1.2.8: - dependencies: - deep-extend: 0.6.0 - ini: 1.3.8 - minimist: 1.2.8 - strip-json-comments: 2.0.1 - - react-base16-styling@0.6.0: - dependencies: - base16: 1.0.0 - lodash.curry: 4.1.1 - lodash.flow: 3.5.0 - pure-color: 1.3.0 - - react-dev-utils@12.0.1(eslint@7.32.0)(typescript@5.8.2)(webpack@5.105.0): - dependencies: - '@babel/code-frame': 7.26.2 - address: 1.2.1 - browserslist: 4.28.1 - chalk: 4.1.2 - cross-spawn: 7.0.6 - detect-port-alt: 1.1.6 - escape-string-regexp: 4.0.0 - filesize: 8.0.7 - find-up: 5.0.0 - fork-ts-checker-webpack-plugin: 6.5.2(eslint@7.32.0)(typescript@5.8.2)(webpack@5.105.0) - global-modules: 2.0.0 - globby: 11.1.0 - gzip-size: 6.0.0 - immer: 9.0.17 - is-root: 2.1.0 - loader-utils: 3.2.1 - open: 8.4.0 - pkg-up: 3.1.0 - prompts: 2.4.2 - react-error-overlay: 6.0.11 - recursive-readdir: 2.2.3 - shell-quote: 1.7.4 - strip-ansi: 6.0.1 - text-table: 0.2.0 - webpack: 5.105.0 - optionalDependencies: - typescript: 5.8.2 - transitivePeerDependencies: - - eslint - - supports-color - - vue-template-compiler - - react-dom@16.14.0(react@16.14.0): - dependencies: - loose-envify: 1.4.0 - object-assign: 4.1.1 - prop-types: 15.8.1 - react: 16.14.0 - scheduler: 0.19.1 - - react-error-overlay@6.0.11: {} - - react-fast-compare@3.2.0: {} - - react-helmet-async@1.3.0(react-dom@16.14.0(react@16.14.0))(react@16.14.0): - dependencies: - '@babel/runtime': 7.27.0 - invariant: 2.2.4 - prop-types: 15.8.1 - react: 16.14.0 - react-dom: 16.14.0(react@16.14.0) - react-fast-compare: 3.2.0 - shallowequal: 1.1.0 - - react-is@16.13.1: {} - - react-json-view@1.21.3(@types/react@18.0.26)(react-dom@16.14.0(react@16.14.0))(react@16.14.0): - dependencies: - flux: 4.0.4(react@16.14.0) - react: 16.14.0 - react-base16-styling: 0.6.0 - react-dom: 16.14.0(react@16.14.0) - react-lifecycles-compat: 3.0.4 - react-textarea-autosize: 8.5.8(@types/react@18.0.26)(react@16.14.0) - transitivePeerDependencies: - - '@types/react' - - encoding - - react-lifecycles-compat@3.0.4: {} - - react-loadable-ssr-addon-v5-slorber@1.0.1(@docusaurus/react-loadable@5.5.2(react@16.14.0))(webpack@5.105.0): - dependencies: - '@babel/runtime': 7.27.0 - react-loadable: '@docusaurus/react-loadable@5.5.2(react@16.14.0)' - webpack: 5.105.0 - - react-router-config@5.1.1(react-router@5.3.4(react@16.14.0))(react@16.14.0): - dependencies: - '@babel/runtime': 7.27.0 - react: 16.14.0 - react-router: 5.3.4(react@16.14.0) - - react-router-dom@5.3.4(react@16.14.0): - dependencies: - '@babel/runtime': 7.27.0 - history: 4.10.1 - loose-envify: 1.4.0 - prop-types: 15.8.1 - react: 16.14.0 - react-router: 5.3.4(react@16.14.0) - tiny-invariant: 1.3.1 - tiny-warning: 1.0.3 - - react-router@5.3.4(react@16.14.0): - dependencies: - '@babel/runtime': 7.27.0 - history: 4.10.1 - hoist-non-react-statics: 3.3.2 - loose-envify: 1.4.0 - path-to-regexp: 1.9.0 - prop-types: 15.8.1 - react: 16.14.0 - react-is: 16.13.1 - tiny-invariant: 1.3.1 - tiny-warning: 1.0.3 - - react-textarea-autosize@8.5.8(@types/react@18.0.26)(react@16.14.0): - dependencies: - '@babel/runtime': 7.27.0 - react: 16.14.0 - use-composed-ref: 1.4.0(@types/react@18.0.26)(react@16.14.0) - use-latest: 1.3.0(@types/react@18.0.26)(react@16.14.0) - transitivePeerDependencies: - - '@types/react' - - react@16.14.0: - dependencies: - loose-envify: 1.4.0 - object-assign: 4.1.1 - prop-types: 15.8.1 - - readable-stream@2.3.7: - dependencies: - core-util-is: 1.0.3 - inherits: 2.0.4 - isarray: 1.0.0 - process-nextick-args: 2.0.1 - safe-buffer: 5.1.2 - string_decoder: 1.1.1 - util-deprecate: 1.0.2 - - readable-stream@3.6.0: - dependencies: - inherits: 2.0.4 - string_decoder: 1.3.0 - util-deprecate: 1.0.2 - - readdirp@3.6.0: - dependencies: - picomatch: 2.3.1 - - reading-time@1.5.0: {} - - rechoir@0.6.2: - dependencies: - resolve: 1.22.1 - - recursive-readdir@2.2.3: - dependencies: - minimatch: 3.1.2 - - reflect.getprototypeof@1.0.10: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.9 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - get-intrinsic: 1.3.0 - get-proto: 1.0.1 - which-builtin-type: 1.2.1 - - regenerate-unicode-properties@10.1.0: - dependencies: - regenerate: 1.4.2 - - regenerate@1.4.2: {} - - regenerator-runtime@0.14.1: {} - - regenerator-transform@0.15.0: - dependencies: - '@babel/runtime': 7.27.0 - - regexp.prototype.flags@1.5.4: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-errors: 1.3.0 - get-proto: 1.0.1 - gopd: 1.2.0 - set-function-name: 2.0.2 - - regexpp@3.2.0: {} - - regexpu-core@5.2.1: - dependencies: - regenerate: 1.4.2 - regenerate-unicode-properties: 10.1.0 - regjsgen: 0.7.1 - regjsparser: 0.9.1 - unicode-match-property-ecmascript: 2.0.0 - unicode-match-property-value-ecmascript: 2.0.0 - - registry-auth-token@4.2.2: - dependencies: - rc: 1.2.8 - - registry-url@5.1.0: - dependencies: - rc: 1.2.8 - - regjsgen@0.7.1: {} - - regjsparser@0.9.1: - dependencies: - jsesc: 0.5.0 - - rehype-katex@4.0.0: - dependencies: - '@types/katex': 0.11.1 - hast-util-to-text: 2.0.1 - katex: 0.16.28 - rehype-parse: 7.0.1 - unified: 9.2.2 - unist-util-visit: 2.0.3 - - rehype-parse@7.0.1: - dependencies: - hast-util-from-parse5: 6.0.1 - parse5: 6.0.1 - - relateurl@0.2.7: {} - - remark-emoji@2.2.0: - dependencies: - emoticon: 3.2.0 - node-emoji: 1.11.0 - unist-util-visit: 2.0.3 - - remark-footnotes@2.0.0: {} - - remark-math@3.0.1: {} - - remark-mdx@1.6.22: - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.10.4 - '@babel/plugin-proposal-object-rest-spread': 7.12.1(@babel/core@7.12.9) - '@babel/plugin-syntax-jsx': 7.12.1(@babel/core@7.12.9) - '@mdx-js/util': 1.6.22 - is-alphabetical: 1.0.4 - remark-parse: 8.0.3 - unified: 9.2.0 - transitivePeerDependencies: - - supports-color - - remark-parse@8.0.3: - dependencies: - ccount: 1.1.0 - collapse-white-space: 1.0.6 - is-alphabetical: 1.0.4 - is-decimal: 1.0.4 - is-whitespace-character: 1.0.4 - is-word-character: 1.0.4 - markdown-escapes: 1.0.4 - parse-entities: 2.0.0 - repeat-string: 1.6.1 - state-toggle: 1.0.3 - trim: 0.0.3 - trim-trailing-lines: 1.1.4 - unherit: 1.1.3 - unist-util-remove-position: 2.0.1 - vfile-location: 3.2.0 - xtend: 4.0.2 - - remark-squeeze-paragraphs@4.0.0: - dependencies: - mdast-squeeze-paragraphs: 4.0.0 - - renderkid@3.0.0: - dependencies: - css-select: 4.3.0 - dom-converter: 0.2.0 - htmlparser2: 6.1.0 - lodash: 4.17.21 - strip-ansi: 6.0.1 - - repeat-string@1.6.1: {} - - require-from-string@2.0.2: {} - - require-like@0.1.2: {} - - requires-port@1.0.0: {} - - resolve-alpn@1.2.1: {} - - resolve-from@4.0.0: {} - - resolve-pathname@3.0.0: {} - - resolve@1.22.1: - dependencies: - is-core-module: 2.16.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - - resolve@2.0.0-next.5: - dependencies: - is-core-module: 2.16.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - - responselike@2.0.1: - dependencies: - lowercase-keys: 2.0.0 - - retry@0.13.1: {} - - reusify@1.0.4: {} - - rimraf@3.0.2: - dependencies: - glob: 7.2.3 - - rtl-detect@1.0.4: {} - - rtlcss@3.5.0: - dependencies: - find-up: 5.0.0 - picocolors: 1.1.1 - postcss: 8.5.3 - strip-json-comments: 3.1.1 - - run-parallel@1.2.0: - dependencies: - queue-microtask: 1.2.3 - - rxjs@7.8.2: - dependencies: - tslib: 2.4.0 - - safe-array-concat@1.1.3: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - has-symbols: 1.1.0 - isarray: 2.0.5 - - safe-buffer@5.1.2: {} - - safe-buffer@5.2.1: {} - - safe-push-apply@1.0.0: - dependencies: - es-errors: 1.3.0 - isarray: 2.0.5 - - safe-regex-test@1.1.0: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-regex: 1.2.1 - - safer-buffer@2.1.2: {} - - sax@1.4.1: {} - - scheduler@0.19.1: - dependencies: - loose-envify: 1.4.0 - object-assign: 4.1.1 - - schema-utils@2.7.0: - dependencies: - '@types/json-schema': 7.0.15 - ajv: 6.12.6 - ajv-keywords: 3.5.2(ajv@6.12.6) - - schema-utils@2.7.1: - dependencies: - '@types/json-schema': 7.0.15 - ajv: 6.12.6 - ajv-keywords: 3.5.2(ajv@6.12.6) - - schema-utils@3.1.1: - dependencies: - '@types/json-schema': 7.0.15 - ajv: 6.12.6 - ajv-keywords: 3.5.2(ajv@6.12.6) - - schema-utils@4.3.3: - dependencies: - '@types/json-schema': 7.0.15 - ajv: 8.17.1 - ajv-formats: 2.1.1(ajv@8.17.1) - ajv-keywords: 5.1.0(ajv@8.17.1) - - section-matter@1.0.0: - dependencies: - extend-shallow: 2.0.1 - kind-of: 6.0.3 - - select-hose@2.0.0: {} - - selfsigned@2.1.1: - dependencies: - node-forge: 1.3.1 - - semver-diff@3.1.1: - dependencies: - semver: 6.3.1 - - semver@5.7.2: {} - - semver@6.3.1: {} - - semver@7.7.1: {} - - send@0.19.0: - dependencies: - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 - encodeurl: 1.0.2 - escape-html: 1.0.3 - etag: 1.8.1 - fresh: 0.5.2 - http-errors: 2.0.0 - mime: 1.6.0 - ms: 2.1.3 - on-finished: 2.4.1 - range-parser: 1.2.1 - statuses: 2.0.1 - transitivePeerDependencies: - - supports-color - - serialize-javascript@6.0.2: - dependencies: - randombytes: 2.1.0 - - serve-handler@6.1.6: - dependencies: - bytes: 3.0.0 - content-disposition: 0.5.2 - mime-types: 2.1.18 - minimatch: 3.1.2 - path-is-inside: 1.0.2 - path-to-regexp: 3.3.0 - range-parser: 1.2.0 - - serve-index@1.9.1: - dependencies: - accepts: 1.3.8 - batch: 0.6.1 - debug: 2.6.9 - escape-html: 1.0.3 - http-errors: 1.6.3 - mime-types: 2.1.35 - parseurl: 1.3.3 - transitivePeerDependencies: - - supports-color - - serve-static@1.16.2: - dependencies: - encodeurl: 2.0.0 - escape-html: 1.0.3 - parseurl: 1.3.3 - send: 0.19.0 - transitivePeerDependencies: - - supports-color - - set-function-length@1.2.2: - dependencies: - define-data-property: 1.1.4 - es-errors: 1.3.0 - function-bind: 1.1.2 - get-intrinsic: 1.3.0 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 - - set-function-name@2.0.2: - dependencies: - define-data-property: 1.1.4 - es-errors: 1.3.0 - functions-have-names: 1.2.3 - has-property-descriptors: 1.0.2 - - set-proto@1.0.0: - dependencies: - dunder-proto: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - - setimmediate@1.0.5: {} - - setprototypeof@1.1.0: {} - - setprototypeof@1.2.0: {} - - shallow-clone@3.0.1: - dependencies: - kind-of: 6.0.3 - - shallowequal@1.1.0: {} - - shebang-command@2.0.0: - dependencies: - shebang-regex: 3.0.0 - - shebang-regex@3.0.0: {} - - shell-quote@1.7.4: {} - - shelljs@0.8.5: - dependencies: - glob: 7.2.3 - interpret: 1.4.0 - rechoir: 0.6.2 - - side-channel-list@1.0.0: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - - side-channel-map@1.0.1: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - - side-channel-weakmap@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - side-channel-map: 1.0.1 - - side-channel@1.1.0: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - side-channel-list: 1.0.0 - side-channel-map: 1.0.1 - side-channel-weakmap: 1.0.2 - - signal-exit@3.0.7: {} - - sirv@1.0.19: - dependencies: - '@polka/url': 1.0.0-next.21 - mrmime: 1.0.1 - totalist: 1.1.0 - - sisteransi@1.0.5: {} - - sitemap@7.1.2: - dependencies: - '@types/node': 17.0.45 - '@types/sax': 1.2.7 - arg: 5.0.2 - sax: 1.4.1 - - slash@3.0.0: {} - - slash@4.0.0: {} - - slice-ansi@4.0.0: - dependencies: - ansi-styles: 4.3.0 - astral-regex: 2.0.0 - is-fullwidth-code-point: 3.0.0 - - sockjs@0.3.24: - dependencies: - faye-websocket: 0.11.4 - uuid: 8.3.2 - websocket-driver: 0.7.4 - - sort-css-media-queries@2.1.0: {} - - source-map-js@1.2.1: {} - - source-map-support@0.5.21: - dependencies: - buffer-from: 1.1.2 - source-map: 0.6.1 - - source-map@0.5.7: {} - - source-map@0.6.1: {} - - space-separated-tokens@1.1.5: {} - - spdy-transport@3.0.0: - dependencies: - debug: 4.3.4 - detect-node: 2.1.0 - hpack.js: 2.1.6 - obuf: 1.1.2 - readable-stream: 3.6.0 - wbuf: 1.7.3 - transitivePeerDependencies: - - supports-color - - spdy@4.0.2: - dependencies: - debug: 4.3.4 - handle-thing: 2.0.1 - http-deceiver: 1.2.7 - select-hose: 2.0.0 - spdy-transport: 3.0.0 - transitivePeerDependencies: - - supports-color - - sprintf-js@1.0.3: {} - - stable@0.1.8: {} - - state-toggle@1.0.3: {} - - statuses@1.5.0: {} - - statuses@2.0.1: {} - - std-env@3.3.0: {} - - string-width@4.2.3: - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - - string-width@5.1.2: - dependencies: - eastasianwidth: 0.2.0 - emoji-regex: 9.2.2 - strip-ansi: 7.0.1 - - string.prototype.matchall@4.0.12: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-abstract: 1.23.9 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - get-intrinsic: 1.3.0 - gopd: 1.2.0 - has-symbols: 1.1.0 - internal-slot: 1.1.0 - regexp.prototype.flags: 1.5.4 - set-function-name: 2.0.2 - side-channel: 1.1.0 - - string.prototype.repeat@1.0.0: - dependencies: - define-properties: 1.2.1 - es-abstract: 1.23.9 - - string.prototype.trim@1.2.10: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-data-property: 1.1.4 - define-properties: 1.2.1 - es-abstract: 1.23.9 - es-object-atoms: 1.1.1 - has-property-descriptors: 1.0.2 - - string.prototype.trimend@1.0.9: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - - string.prototype.trimstart@1.0.8: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - - string_decoder@1.1.1: - dependencies: - safe-buffer: 5.1.2 - - string_decoder@1.3.0: - dependencies: - safe-buffer: 5.2.1 - - stringify-object@3.3.0: - dependencies: - get-own-enumerable-property-symbols: 3.0.2 - is-obj: 1.0.1 - is-regexp: 1.0.0 - - strip-ansi@6.0.1: - dependencies: - ansi-regex: 5.0.1 - - strip-ansi@7.0.1: - dependencies: - ansi-regex: 6.0.1 - - strip-bom-string@1.0.0: {} - - strip-final-newline@2.0.0: {} - - strip-json-comments@2.0.1: {} - - strip-json-comments@3.1.1: {} - - style-to-object@0.3.0: - dependencies: - inline-style-parser: 0.1.1 - - stylehacks@5.1.1(postcss@8.5.3): - dependencies: - browserslist: 4.28.1 - postcss: 8.5.3 - postcss-selector-parser: 6.0.10 - - supports-color@5.5.0: - dependencies: - has-flag: 3.0.0 - - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - - supports-color@8.1.1: - dependencies: - has-flag: 4.0.0 - - supports-preserve-symlinks-flag@1.0.0: {} - - svg-parser@2.0.4: {} - - svgo@2.8.0: - dependencies: - '@trysound/sax': 0.2.0 - commander: 7.2.0 - css-select: 4.3.0 - css-tree: 1.1.3 - csso: 4.2.0 - picocolors: 1.1.1 - stable: 0.1.8 - - table@6.8.0: - dependencies: - ajv: 8.17.1 - lodash.truncate: 4.4.2 - slice-ansi: 4.0.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - - tapable@1.1.3: {} - - tapable@2.3.0: {} - - terser-webpack-plugin@5.3.16(webpack@5.105.0): - dependencies: - '@jridgewell/trace-mapping': 0.3.25 - jest-worker: 27.5.1 - schema-utils: 4.3.3 - serialize-javascript: 6.0.2 - terser: 5.39.0 - webpack: 5.105.0 - - terser@5.39.0: - dependencies: - '@jridgewell/source-map': 0.3.6 - acorn: 8.15.0 - commander: 2.20.3 - source-map-support: 0.5.21 - - text-table@0.2.0: {} - - thunky@1.1.0: {} - - tiny-invariant@1.3.1: {} - - tiny-warning@1.0.3: {} - - to-regex-range@5.0.1: - dependencies: - is-number: 7.0.0 - - toidentifier@1.0.1: {} - - totalist@1.1.0: {} - - tr46@0.0.3: {} - - trim-trailing-lines@1.1.4: {} - - trim@0.0.3: {} - - trough@1.0.5: {} - - tslib@2.4.0: {} - - type-check@0.4.0: - dependencies: - prelude-ls: 1.2.1 - - type-fest@0.20.2: {} - - type-fest@2.19.0: {} - - type-is@1.6.18: - dependencies: - media-typer: 0.3.0 - mime-types: 2.1.35 - - typed-array-buffer@1.0.3: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-typed-array: 1.1.15 - - typed-array-byte-length@1.0.3: - dependencies: - call-bind: 1.0.8 - for-each: 0.3.5 - gopd: 1.2.0 - has-proto: 1.2.0 - is-typed-array: 1.1.15 - - typed-array-byte-offset@1.0.4: - dependencies: - available-typed-arrays: 1.0.7 - call-bind: 1.0.8 - for-each: 0.3.5 - gopd: 1.2.0 - has-proto: 1.2.0 - is-typed-array: 1.1.15 - reflect.getprototypeof: 1.0.10 - - typed-array-length@1.0.7: - dependencies: - call-bind: 1.0.8 - for-each: 0.3.5 - gopd: 1.2.0 - is-typed-array: 1.1.15 - possible-typed-array-names: 1.1.0 - reflect.getprototypeof: 1.0.10 - - typedarray-to-buffer@3.1.5: - dependencies: - is-typedarray: 1.0.0 - - typescript@5.8.2: {} - - ua-parser-js@1.0.40: {} - - unbox-primitive@1.1.0: - dependencies: - call-bound: 1.0.4 - has-bigints: 1.1.0 - has-symbols: 1.1.0 - which-boxed-primitive: 1.1.1 - - undici@6.21.2: {} - - unherit@1.1.3: - dependencies: - inherits: 2.0.4 - xtend: 4.0.2 - - unicode-canonical-property-names-ecmascript@2.0.0: {} - - unicode-match-property-ecmascript@2.0.0: - dependencies: - unicode-canonical-property-names-ecmascript: 2.0.0 - unicode-property-aliases-ecmascript: 2.1.0 - - unicode-match-property-value-ecmascript@2.0.0: {} - - unicode-property-aliases-ecmascript@2.1.0: {} - - unified@9.2.0: - dependencies: - '@types/unist': 2.0.6 - bail: 1.0.5 - extend: 3.0.2 - is-buffer: 2.0.5 - is-plain-obj: 2.1.0 - trough: 1.0.5 - vfile: 4.2.1 - - unified@9.2.2: - dependencies: - '@types/unist': 2.0.6 - bail: 1.0.5 - extend: 3.0.2 - is-buffer: 2.0.5 - is-plain-obj: 2.1.0 - trough: 1.0.5 - vfile: 4.2.1 - - unique-string@2.0.0: - dependencies: - crypto-random-string: 2.0.0 - - unist-builder@2.0.3: {} - - unist-util-find-after@3.0.0: - dependencies: - unist-util-is: 4.1.0 - - unist-util-generated@1.1.6: {} - - unist-util-is@4.1.0: {} - - unist-util-position@3.1.0: {} - - unist-util-remove-position@2.0.1: - dependencies: - unist-util-visit: 2.0.3 - - unist-util-remove@2.1.0: - dependencies: - unist-util-is: 4.1.0 - - unist-util-stringify-position@2.0.3: - dependencies: - '@types/unist': 2.0.6 - - unist-util-visit-parents@3.1.1: - dependencies: - '@types/unist': 2.0.6 - unist-util-is: 4.1.0 - - unist-util-visit@2.0.3: - dependencies: - '@types/unist': 2.0.6 - unist-util-is: 4.1.0 - unist-util-visit-parents: 3.1.1 - - universalify@2.0.0: {} - - unpipe@1.0.0: {} - - update-browserslist-db@1.2.3(browserslist@4.28.1): - dependencies: - browserslist: 4.28.1 - escalade: 3.2.0 - picocolors: 1.1.1 - - update-notifier@5.1.0: - dependencies: - boxen: 5.1.2 - chalk: 4.1.2 - configstore: 5.0.1 - has-yarn: 2.1.0 - import-lazy: 2.1.0 - is-ci: 2.0.0 - is-installed-globally: 0.4.0 - is-npm: 5.0.0 - is-yarn-global: 0.3.0 - latest-version: 5.1.0 - pupa: 2.1.1 - semver: 7.7.1 - semver-diff: 3.1.1 - xdg-basedir: 4.0.0 - - uri-js@4.4.1: - dependencies: - punycode: 2.1.1 - - url-loader@4.1.1(file-loader@6.2.0(webpack@5.105.0))(webpack@5.105.0): - dependencies: - loader-utils: 2.0.4 - mime-types: 2.1.35 - schema-utils: 3.1.1 - webpack: 5.105.0 - optionalDependencies: - file-loader: 6.2.0(webpack@5.105.0) - - use-composed-ref@1.4.0(@types/react@18.0.26)(react@16.14.0): - dependencies: - react: 16.14.0 - optionalDependencies: - '@types/react': 18.0.26 - - use-isomorphic-layout-effect@1.2.0(@types/react@18.0.26)(react@16.14.0): - dependencies: - react: 16.14.0 - optionalDependencies: - '@types/react': 18.0.26 - - use-latest@1.3.0(@types/react@18.0.26)(react@16.14.0): - dependencies: - react: 16.14.0 - use-isomorphic-layout-effect: 1.2.0(@types/react@18.0.26)(react@16.14.0) - optionalDependencies: - '@types/react': 18.0.26 - - use-sync-external-store@1.4.0(react@16.14.0): - dependencies: - react: 16.14.0 - - util-deprecate@1.0.2: {} - - utila@0.4.0: {} - - utility-types@3.10.0: {} - - utils-merge@1.0.1: {} - - uuid@8.3.2: {} - - v8-compile-cache@2.3.0: {} - - value-equal@1.0.1: {} - - vary@1.1.2: {} - - vfile-location@3.2.0: {} - - vfile-message@2.0.4: - dependencies: - '@types/unist': 2.0.6 - unist-util-stringify-position: 2.0.3 - - vfile@4.2.1: - dependencies: - '@types/unist': 2.0.6 - is-buffer: 2.0.5 - unist-util-stringify-position: 2.0.3 - vfile-message: 2.0.4 - - wait-on@8.0.3: - dependencies: - axios: 1.13.5 - joi: 17.13.3 - lodash: 4.17.21 - minimist: 1.2.8 - rxjs: 7.8.2 - transitivePeerDependencies: - - debug - - watchpack@2.5.1: - dependencies: - glob-to-regexp: 0.4.1 - graceful-fs: 4.2.11 - - wbuf@1.7.3: - dependencies: - minimalistic-assert: 1.0.1 - - web-namespaces@1.1.4: {} - - webidl-conversions@3.0.1: {} - - webpack-bundle-analyzer@4.6.1: - dependencies: - acorn: 8.15.0 - acorn-walk: 8.2.0 - chalk: 4.1.2 - commander: 7.2.0 - gzip-size: 6.0.0 - lodash: 4.17.21 - opener: 1.5.2 - sirv: 1.0.19 - ws: 7.5.10 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - webpack-dev-middleware@5.3.4(webpack@5.105.0): - dependencies: - colorette: 2.0.19 - memfs: 3.4.12 - mime-types: 2.1.35 - range-parser: 1.2.1 - schema-utils: 4.3.3 - webpack: 5.105.0 - - webpack-dev-server@4.11.1(webpack@5.105.0): - dependencies: - '@types/bonjour': 3.5.10 - '@types/connect-history-api-fallback': 1.3.5 - '@types/express': 4.17.15 - '@types/serve-index': 1.9.1 - '@types/serve-static': 1.15.0 - '@types/sockjs': 0.3.33 - '@types/ws': 8.18.0 - ansi-html-community: 0.0.8 - bonjour-service: 1.0.14 - chokidar: 3.5.3 - colorette: 2.0.19 - compression: 1.7.4 - connect-history-api-fallback: 2.0.0 - default-gateway: 6.0.3 - express: 4.21.2 - graceful-fs: 4.2.11 - html-entities: 2.3.3 - http-proxy-middleware: 2.0.9(@types/express@4.17.15) - ipaddr.js: 2.0.1 - open: 8.4.0 - p-retry: 4.6.2 - rimraf: 3.0.2 - schema-utils: 4.3.3 - selfsigned: 2.1.1 - serve-index: 1.9.1 - sockjs: 0.3.24 - spdy: 4.0.2 - webpack: 5.105.0 - webpack-dev-middleware: 5.3.4(webpack@5.105.0) - ws: 8.18.1 - transitivePeerDependencies: - - bufferutil - - debug - - supports-color - - utf-8-validate - - webpack-merge@5.8.0: - dependencies: - clone-deep: 4.0.1 - wildcard: 2.0.0 - - webpack-sources@3.3.3: {} - - webpack@5.105.0: - dependencies: - '@types/eslint-scope': 3.7.7 - '@types/estree': 1.0.8 - '@types/json-schema': 7.0.15 - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/wasm-edit': 1.14.1 - '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.15.0 - acorn-import-phases: 1.0.4(acorn@8.15.0) - browserslist: 4.28.1 - chrome-trace-event: 1.0.3 - enhanced-resolve: 5.19.0 - es-module-lexer: 2.0.0 - eslint-scope: 5.1.1 - events: 3.3.0 - glob-to-regexp: 0.4.1 - graceful-fs: 4.2.11 - json-parse-even-better-errors: 2.3.1 - loader-runner: 4.3.1 - mime-types: 2.1.35 - neo-async: 2.6.2 - schema-utils: 4.3.3 - tapable: 2.3.0 - terser-webpack-plugin: 5.3.16(webpack@5.105.0) - watchpack: 2.5.1 - webpack-sources: 3.3.3 - transitivePeerDependencies: - - '@swc/core' - - esbuild - - uglify-js - - webpackbar@5.0.2(webpack@5.105.0): - dependencies: - chalk: 4.1.2 - consola: 2.15.3 - pretty-time: 1.1.0 - std-env: 3.3.0 - webpack: 5.105.0 - - websocket-driver@0.7.4: - dependencies: - http-parser-js: 0.5.8 - safe-buffer: 5.2.1 - websocket-extensions: 0.1.4 - - websocket-extensions@0.1.4: {} - - whatwg-encoding@3.1.1: - dependencies: - iconv-lite: 0.6.3 - - whatwg-mimetype@4.0.0: {} - - whatwg-url@5.0.0: - dependencies: - tr46: 0.0.3 - webidl-conversions: 3.0.1 - - which-boxed-primitive@1.1.1: - dependencies: - is-bigint: 1.1.0 - is-boolean-object: 1.2.2 - is-number-object: 1.1.1 - is-string: 1.1.1 - is-symbol: 1.1.1 - - which-builtin-type@1.2.1: - dependencies: - call-bound: 1.0.4 - function.prototype.name: 1.1.8 - has-tostringtag: 1.0.2 - is-async-function: 2.1.1 - is-date-object: 1.1.0 - is-finalizationregistry: 1.1.1 - is-generator-function: 1.1.0 - is-regex: 1.2.1 - is-weakref: 1.1.1 - isarray: 2.0.5 - which-boxed-primitive: 1.1.1 - which-collection: 1.0.2 - which-typed-array: 1.1.19 - - which-collection@1.0.2: - dependencies: - is-map: 2.0.3 - is-set: 2.0.3 - is-weakmap: 2.0.2 - is-weakset: 2.0.4 - - which-typed-array@1.1.19: - dependencies: - available-typed-arrays: 1.0.7 - call-bind: 1.0.8 - call-bound: 1.0.4 - for-each: 0.3.5 - get-proto: 1.0.1 - gopd: 1.2.0 - has-tostringtag: 1.0.2 - - which@1.3.1: - dependencies: - isexe: 2.0.0 - - which@2.0.2: - dependencies: - isexe: 2.0.0 - - widest-line@3.1.0: - dependencies: - string-width: 4.2.3 - - widest-line@4.0.1: - dependencies: - string-width: 5.1.2 - - wildcard@2.0.0: {} - - word-wrap@1.2.4: {} - - wrap-ansi@7.0.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - - wrap-ansi@8.0.1: - dependencies: - ansi-styles: 6.2.1 - string-width: 5.1.2 - strip-ansi: 7.0.1 - - wrappy@1.0.2: {} - - write-file-atomic@3.0.3: - dependencies: - imurmurhash: 0.1.4 - is-typedarray: 1.0.0 - signal-exit: 3.0.7 - typedarray-to-buffer: 3.1.5 - - ws@7.5.10: {} - - ws@8.18.1: {} - - xdg-basedir@4.0.0: {} - - xml-js@1.6.11: - dependencies: - sax: 1.4.1 - - xtend@4.0.2: {} - - yaml@1.10.2: {} - - yocto-queue@0.1.0: {} - - zwitch@1.0.5: {} diff --git a/docs/set-solana-release-tag.sh b/docs/set-solana-release-tag.sh deleted file mode 100755 index af8426526a8..00000000000 --- a/docs/set-solana-release-tag.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash -set -e - -cd "$(dirname "$0")" - -eval "$(../ci/channel-info.sh)" - -if [[ -n $BETA_CHANNEL_LATEST_TAG ]]; then - LATEST_AGAVE_RELEASE_VERSION=$BETA_CHANNEL_LATEST_TAG -else - LATEST_AGAVE_RELEASE_VERSION=$STABLE_CHANNEL_LATEST_TAG -fi -VERSION_FOR_DOCS_RS="${LATEST_AGAVE_RELEASE_VERSION:1}" - -set -x -if [[ -n $CI ]]; then - sed --version || { echo "Error: Incompatible version of sed, use gnu sed"; exit 1; } - find src/ -name \*.md -exec sed -i "s/LATEST_AGAVE_RELEASE_VERSION/$LATEST_AGAVE_RELEASE_VERSION/g" {} \; - find src/ -name \*.md -exec sed -i "s/VERSION_FOR_DOCS_RS/$VERSION_FOR_DOCS_RS/g" {} \; -fi diff --git a/docs/sidebars.js b/docs/sidebars.js deleted file mode 100644 index e60502edfb8..00000000000 --- a/docs/sidebars.js +++ /dev/null @@ -1,130 +0,0 @@ -module.exports = { - masterSidebar: [ - "home", - { - type: "category", - label: "Introduction", - collapsed: false, - items: [ - "what-is-a-validator", - "what-is-an-rpc-node", - "faq", - "backwards-compatibility", - ], - }, - { - type: "category", - label: "Command Line Tools", - // collapsed: false, - items: [ - { - type: "autogenerated", - dirName: "cli", - }, - ], - }, - { - type: "category", - label: "Architecture", - // collapsed: false, - items: [ - "architecture", - { - type: "category", - label: "Clusters", - // collapsed: false, - items: [ - { - type: "autogenerated", - dirName: "clusters", - }, - ], - }, - { - type: "category", - label: "Consensus", - // collapsed: false, - items: [ - { - type: "autogenerated", - dirName: "consensus", - }, - ], - }, - { - type: "category", - label: "Runtime", - // collapsed: false, - items: [ - { - type: "autogenerated", - dirName: "runtime", - }, - ], - }, - { - type: "category", - label: "Validators", - // collapsed: false, - items: [ - { - type: "autogenerated", - dirName: "validator", - }, - ], - }, - ], - }, - { - type: "category", - label: "Operating a Validator", - // collapsed: false, - items: [ - { - type: "autogenerated", - dirName: "operations", - }, - ], - }, - ], - proposalsSidebar: [ - { - type: "category", - label: "System Design Proposals", - collapsed: false, - items: [ - "proposals", - { - type: "category", - label: "Accepted Proposals", - collapsed: true, - link: { - type: "doc", - id: "proposals/accepted-design-proposals", - }, - items: [ - { - type: "autogenerated", - dirName: "proposals", - }, - ], - }, - { - type: "category", - label: "Implemented Proposals", - collapsed: true, - link: { - type: "doc", - id: "implemented-proposals/index", - }, - items: [ - { - type: "autogenerated", - dirName: "implemented-proposals", - }, - ], - }, - ], - }, - ], -}; diff --git a/docs/src/architecture.md b/docs/src/architecture.md deleted file mode 100644 index 798860f9adf..00000000000 --- a/docs/src/architecture.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: Agave Validator Architecture -sidebar_position: 0 -sidebar_label: Overview -pagination_label: Agave Validator Architecture ---- - -In this section, we will describe the architecture of the Agave Validator. diff --git a/docs/src/backwards-compatibility.md b/docs/src/backwards-compatibility.md deleted file mode 100644 index 7a8ef3caab2..00000000000 --- a/docs/src/backwards-compatibility.md +++ /dev/null @@ -1,153 +0,0 @@ ---- -title: Backward Compatibility Policy ---- - -As the Solana developer ecosystem grows, so does the need for clear expectations around -breaking API and behavior changes affecting applications and tooling built for Solana by Anza. -In a perfect world, Solana development could continue at a very fast pace without ever -causing issues for existing developers. However, some compromises will need to be made -and so this document attempts to clarify and codify the process for new releases. Furthermore, -there will be a growing number of validator clients maintained separately by distinct teams. -Coordinating across these teams to ensure the reliability of the network will require ongoing -communication. - -### Expectations - -- Agave software releases include APIs, SDKs, and CLI tooling (with a few [exceptions](#exceptions)). -- Agave software releases follow semantic versioning, more details below. -- Software for a `MINOR` version release will be compatible across all software on the - same `MAJOR` version. - -### Deprecation Process - -1. In any `PATCH` or `MINOR` release, a feature, API, endpoint, etc. could be marked as deprecated. -2. According to code upgrade difficulty, some features will remain deprecated for a few release - cycles. -3. In a future `MAJOR` release, deprecated features will be removed in an incompatible way. - -### Release Cadence - -The Solana RPC API, Rust SDK, CLI tooling, and SBF Program SDK are all updated and shipped -along with each Solana software release and should always be compatible between `PATCH` -updates of a particular `MINOR` version release. - -#### Release Channels - -- `edge` software that contains cutting-edge features with no backward compatibility policy -- `beta` software that runs on the Solana Testnet cluster -- `stable` software that run on the Solana Mainnet Beta and Devnet clusters - -#### Major Releases (x.0.0) - -`MAJOR` version releases (e.g. 2.0.0) may contain breaking changes and removal of previously -deprecated features. Client SDKs and tooling will begin using new features and endpoints -that were enabled in the previous `MAJOR` version. - -#### Minor Releases (1.x.0) - -New features and proposal implementations are added to _new_ `MINOR` version -releases (e.g. 1.4.0) and are first run on Solana's Testnet cluster. While running -on the testnet, `MINOR` versions are considered to be in the `beta` release channel. After -those changes have been patched as needed and proven to be reliable, the `MINOR` version will -be upgraded to the `stable` release channel and deployed to the Mainnet Beta cluster. - -#### Patch Releases (1.0.x) - -Low risk features, non-breaking changes, and security and bug fixes are shipped as part -of `PATCH` version releases (e.g. 1.0.11). Patches may be applied to both `beta` and `stable` -release channels. - -### RPC API - -Patch releases: - -- Bug fixes -- Security fixes -- Endpoint / feature deprecation - -Minor releases: - -- New RPC endpoints and features - -Major releases: - -- Removal of deprecated features - -### Rust Crates - -- [`solana-sdk`](https://docs.rs/solana-sdk/) - Rust SDK for creating transactions and parsing account state -- [`solana-program`](https://docs.rs/solana-program/) - Rust SDK for writing programs -- [`solana-client`](https://docs.rs/solana-client/) - Rust client for connecting to RPC API -- [`solana-cli-config`](https://docs.rs/solana-cli-config/) - Rust client for managing Solana CLI config files -- [`agave-geyser-plugin-interface`](https://docs.rs/agave-geyser-plugin-interface/) - Rust interface for developing Solana Geyser plugins. - -Patch releases: - -- Bug fixes -- Security fixes -- Performance improvements - -Minor releases: - -- New APIs - -Major releases - -- Removal of deprecated APIs -- Backwards incompatible behavior changes - -### CLI Tools - -Patch releases: - -- Bug and security fixes -- Performance improvements -- Subcommand / argument deprecation - -Minor releases: - -- New subcommands - -Major releases: - -- Switch to new RPC API endpoints / configuration introduced in the previous major version. -- Removal of deprecated features - -### Runtime Features - -New Agave runtime features are feature-switched and manually activated. Runtime features -include: the introduction of new native programs, sysvars, and syscalls; and changes to -their behavior. Feature activation is cluster agnostic, allowing confidence to be built on -Testnet before activation on Mainnet-beta. - -The release process is as follows: - -1. New runtime feature is included in a new release, deactivated by default -2. Once sufficient staked validators upgrade to the new release, the runtime feature switch - is activated manually with an instruction -3. The feature takes effect at the beginning of the next epoch - -### Infrastructure Changes - -#### Local cluster scripts and Docker images - -Breaking changes will be limited to `MAJOR` version updates. `MINOR` and `PATCH` updates should always -be backwards compatible. - -### Exceptions - -#### Web3 JavaScript SDK - -The Web3.JS SDK also follows semantic versioning specifications but is shipped separately from Solana -software releases. - -#### Attack Vectors - -If a new attack vector is discovered in existing code, the above processes may be -circumvented in order to rapidly deploy a fix, depending on the severity of the issue. - -#### CLI Tooling Output - -CLI tooling json output (`output --json`) compatibility will be preserved; however, output directed -for a human reader is subject to change. This includes output as well as potential help, warning, or -error messages. diff --git a/docs/src/cli/.usage.md.header b/docs/src/cli/.usage.md.header deleted file mode 100644 index 07cdfec744a..00000000000 --- a/docs/src/cli/.usage.md.header +++ /dev/null @@ -1,62 +0,0 @@ ---- -title: Solana CLI Reference and Usage -pagination_label: Solana CLI Reference and Usage -sidebar_label: Reference & Usage -sidebar_position: 3 ---- - -The [solana-cli crate](https://crates.io/crates/solana-cli) provides a command-line interface tool for Solana - -## Examples - -### Get Pubkey - -```bash -// Command -$ solana-keygen pubkey - -// Return - -``` - -### Airdrop SOL/Lamports - -```bash -// Command -$ solana airdrop 1 - -// Return -"1 SOL" -``` - -### Get Balance - -```bash -// Command -$ solana balance - -// Return -"3.00050001 SOL" -``` - -### Confirm Transaction - -```bash -// Command -$ solana confirm - -// Return -"Confirmed" / "Not found" / "Transaction failed with error " -``` - -### Deploy program - -```bash -// Command -$ solana program deploy - -// Return - -``` - -## Usage diff --git a/docs/src/cli/examples/_category_.json b/docs/src/cli/examples/_category_.json deleted file mode 100644 index 9a1a43e3072..00000000000 --- a/docs/src/cli/examples/_category_.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "position": 4.5, - "label": "Command Examples", - "collapsible": true, - "collapsed": false, - "link": null -} diff --git a/docs/src/cli/examples/choose-a-cluster.md b/docs/src/cli/examples/choose-a-cluster.md deleted file mode 100644 index c554c39ab2a..00000000000 --- a/docs/src/cli/examples/choose-a-cluster.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -title: Connecting to a Cluster with the Solana CLI -pagination_label: "Solana CLI: Connecting to a Cluster" -sidebar_label: Connecting to a Cluster ---- - -See [Solana Clusters](../../clusters/available.md) for general information about the -available clusters. - -## Configure the command-line tool - -You can check what cluster the Solana command-line tool (CLI) is currently targeting by -running the following command: - -```bash -solana config get -``` - -Use `solana config set` command to target a particular cluster. After setting -a cluster target, any future subcommands will send/receive information from that -cluster. - -For example to target the Devnet cluster, run: - -```bash -solana config set --url https://api.devnet.solana.com -``` - -## Ensure Versions Match - -Though not strictly necessary, the CLI will generally work best when its version -matches the software version running on the cluster. To get the locally-installed -CLI version, run: - -```bash -solana --version -``` - -To get the cluster version, run: - -```bash -solana cluster-version -``` - -Ensure the local CLI version is greater than or equal to the cluster version. diff --git a/docs/src/cli/examples/delegate-stake.md b/docs/src/cli/examples/delegate-stake.md deleted file mode 100644 index 161d4aa8776..00000000000 --- a/docs/src/cli/examples/delegate-stake.md +++ /dev/null @@ -1,250 +0,0 @@ ---- -title: Staking SOL with the Solana CLI -pagination_label: "Solana CLI: Staking" -sidebar_label: Staking ---- - -After you have [received SOL](./transfer-tokens.md), you might consider putting it -to use by delegating _stake_ to a validator. Stake is what we call tokens in a -_stake account_. Solana weights validator votes by the amount of stake delegated -to them, which gives those validators more influence in determining the next -valid block of transactions in the blockchain. Solana then generates new SOL -periodically to reward stakers and validators. You earn more rewards the more -stake you delegate. - -:::info -For an overview of staking, read first the -[Staking and Inflation FAQ](https://solana.com/staking). -::: - -## Create a Stake Account - -To delegate stake, you will need to transfer some tokens into a stake account. -To create an account, you will need a keypair. Its public key will be used as -the -[stake account address](https://solana.com/docs/references/staking/stake-accounts#account-address). -No need for a password or encryption here; this keypair will be discarded right -after creating the stake account. - -```bash -solana-keygen new --no-passphrase -o stake-account.json -``` - -The output will contain the public key after the text `pubkey:`. - -```text -pubkey: GKvqsuNcnwWqPzzuhLmGi4rzzh55FhJtGizkhHaEJqiV -``` - -Copy the public key and store it for safekeeping. You will need it any time you -want to perform an action on the stake account you create next. - -Now, create a stake account: - -```bash -solana create-stake-account --from stake-account.json \ - --stake-authority --withdraw-authority \ - --fee-payer -``` - -`` tokens are transferred from the account at the "from" `` to -a new stake account at the public key of stake-account.json. - -Instead of a numeric amount, you can also use the keyword `ALL` to send all available SOL from the `--from` account. - -The stake-account.json file can now be discarded. To authorize additional -actions, you will use the `--stake-authority` or `--withdraw-authority` keypair, -not stake-account.json. - -View the new stake account with the `solana stake-account` command: - -```bash -solana stake-account -``` - -The output will look similar to this: - -```text -Total Stake: 5000 SOL -Stake account is undelegated -Stake Authority: EXU95vqs93yPeCeAU7mPPu6HbRUmTFPEiGug9oCdvQ5F -Withdraw Authority: EXU95vqs93yPeCeAU7mPPu6HbRUmTFPEiGug9oCdvQ5F -``` - -### Set Stake and Withdraw Authorities - -[Stake and withdraw authorities](https://solana.com/docs/references/staking/stake-accounts#understanding-account-authorities) -can be set when creating an account via the `--stake-authority` and -`--withdraw-authority` options, or afterward with the `solana stake-authorize` -command. For example, to set a new stake authority, run: - -```bash -solana stake-authorize \ - --stake-authority --new-stake-authority \ - --fee-payer -``` - -This will use the existing stake authority `` to authorize a new stake -authority `` on the stake account ``. - -### Advanced: Derive Stake Account Addresses - -When you delegate stake, you delegate all tokens in the stake account to a -single validator. To delegate to multiple validators, you will need multiple -stake accounts. Creating a new keypair for each account and managing those -addresses can be cumbersome. Fortunately, you can derive stake addresses using -the `--seed` option: - -```bash -solana create-stake-account --from --seed \ - --stake-authority --withdraw-authority --fee-payer -``` - -`` is an arbitrary string up to 32 bytes, but will typically be a number -corresponding to which derived account this is. The first account might be "0", -then "1", and so on. The public key of `` acts as the -base address. The command derives a new address from the base address and seed -string. To see what stake address the command will derive, use -`solana create-address-with-seed`: - -```bash -solana create-address-with-seed --from STAKE -``` - -`` is the public key of the `` passed to -`solana create-stake-account`. - -The command will output a derived address, which can be used for the -`` argument in staking operations. - -## Delegate Stake - -To delegate your stake to a validator, you will need its vote account address. -Find it by querying the cluster for the list of all validators and their vote -accounts with the `solana validators` command: - -```bash -solana validators -``` - -The first column of each row contains the validator's identity and the second is -the vote account address. Choose a validator and use its vote account address in -`solana delegate-stake`: - -```bash -solana delegate-stake --stake-authority \ - --fee-payer -``` - -The stake authority `` authorizes the operation on the account with -address ``. The stake is delegated to the vote account -with address ``. - -After delegating stake, use `solana stake-account` to observe the changes to the -stake account: - -```bash -solana stake-account -``` - -You will see new fields "Delegated Stake" and "Delegated Vote Account Address" -in the output. The output will look similar to this: - -```text -Total Stake: 5000 SOL -Credits Observed: 147462 -Delegated Stake: 4999.99771712 SOL -Delegated Vote Account Address: CcaHc2L43ZWjwCHART3oZoJvHLAe9hzT2DJNUpBzoTN1 -Stake activates starting from epoch: 42 -Stake Authority: EXU95vqs93yPeCeAU7mPPu6HbRUmTFPEiGug9oCdvQ5F -Withdraw Authority: EXU95vqs93yPeCeAU7mPPu6HbRUmTFPEiGug9oCdvQ5F -``` - -## Deactivate Stake - -Once delegated, you can undelegate stake with the `solana deactivate-stake` -command: - -```bash -solana deactivate-stake --stake-authority \ - --fee-payer -``` - -The stake authority `` authorizes the operation on the account with -address ``. - -If the stake account was created at a derived address, pass the same seed when -deactivating: - -```bash -solana deactivate-stake --stake-authority --seed \ - --fee-payer -``` - -To deactivate stake delegated to a delinquent validator, use: - -```bash -solana deactivate-stake --stake-authority --delinquent \ - --fee-payer -``` - -Note that stake takes several epochs to "cool down". Attempts to delegate stake -in the cool down period will fail. - -## Withdraw Stake - -Transfer tokens out of a stake account with the `solana withdraw-stake` command: - -```bash -solana withdraw-stake --withdraw-authority \ - --fee-payer -``` - -`` is the existing stake account, the `` provided -as `--withdraw-authority` is the withdraw authority, and `` is the number -of tokens to transfer to ``. - -`` may be a numeric value in SOL or one of the keywords: - -- `ALL` – withdraw the maximum amount allowed from the stake account -- `AVAILABLE` – withdraw only the portion that is currently withdrawable. - -The exact amount that can be withdrawn depends on whether the stake is active, -cooling down, or fully inactive, and on any lockup that may apply. - -## Split Stake - -You may want to delegate stake to additional validators while your existing -stake is not eligible for withdrawal. It might not be eligible because it is -currently staked, cooling down, or locked up. To transfer tokens from an -existing stake account to a new one, use the `solana split-stake` command: - -```bash -solana split-stake --stake-authority \ - --fee-payer -``` - -`` is the existing stake account, the stake authority -`` is the stake authority, `` is the keypair -for the new account, and `` is the number of tokens to transfer to the -new account. - -When splitting stake, the amount moved into the new stake account must be at least -the cluster's minimum delegation amount, otherwise the command will fail. - -The new stake account must also be rent-exempt. When signing the transaction -offline, you must provide the rent-exempt reserve explicitly using the -`--rent-exempt-reserve-sol` flag: - -```bash -solana split-stake --stake-authority \ - --rent-exempt-reserve-sol --fee-payer -``` - -`` is the additional SOL sent to the new stake account to make -it rent-exempt, in addition to `` of stake being split. - -To split a stake account into a derived account address, use the `--seed` -option. See -[Derive Stake Account Addresses](#advanced-derive-stake-account-addresses) for -details. diff --git a/docs/src/cli/examples/deploy-a-program.md b/docs/src/cli/examples/deploy-a-program.md deleted file mode 100644 index a5e0eb60d2c..00000000000 --- a/docs/src/cli/examples/deploy-a-program.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Deploy a Solana Program with the CLI -pagination_label: "Solana CLI: Deploy a Program" -sidebar_label: Deploy a Program ---- - -[Redirect](https://solana.com/docs/programs/deploying) diff --git a/docs/src/cli/examples/durable-nonce.md b/docs/src/cli/examples/durable-nonce.md deleted file mode 100644 index 11c90c3936b..00000000000 --- a/docs/src/cli/examples/durable-nonce.md +++ /dev/null @@ -1,256 +0,0 @@ ---- -title: Durable Transaction Nonces in the Solana CLI -pagination_label: "Solana CLI: Durable Transaction Nonces" -sidebar_label: Durable Transaction Nonces ---- - -Durable transaction nonces are a mechanism for getting around the typical short -lifetime of a transaction's -[`recent_blockhash`](https://solana.com/docs/core/transactions#recent-blockhash). -They are implemented as a Solana Program, the mechanics of which can be read -about in the [proposal](../../implemented-proposals/durable-tx-nonces.md). - -## Usage Examples - -Full usage details for durable nonce CLI commands can be found in the -[CLI reference](../usage.md). - -### Nonce Authority - -Authority over a nonce account can optionally be assigned to another account. In -doing so the new authority inherits full control over the nonce account from the -previous authority, including the account creator. This feature enables the -creation of more complex account ownership arrangements and derived account -addresses not associated with a keypair. The -`--nonce-authority ` argument is used to specify this account -and is supported by the following commands - -- `create-nonce-account` -- `new-nonce` -- `withdraw-from-nonce-account` -- `authorize-nonce-account` - -### Nonce Account Creation - -The durable transaction nonce feature uses an account to store the next nonce -value. Durable nonce accounts must be -[rent-exempt](../../implemented-proposals/rent.md#two-tiered-rent-regime), so need -to carry the minimum balance to achieve this. - -A nonce account is created by first generating a new keypair, then create the -account on chain - -- Command - -```bash -solana-keygen new -o nonce-keypair.json -solana create-nonce-account nonce-keypair.json 1 -``` - -- Output - -```text -2SymGjGV4ksPdpbaqWFiDoBz8okvtiik4KE9cnMQgRHrRLySSdZ6jrEcpPifW4xUpp4z66XM9d9wM48sA7peG2XL -``` - -> To keep the keypair entirely offline, use the -> [Paper Wallet](../wallets/paper.md) keypair generation -> [instructions](../wallets/paper.md#seed-phrase-generation) instead - -> [Full usage documentation](../usage.md#solana-create-nonce-account) - -### Querying the Stored Nonce Value - -Creating a durable nonce transaction requires passing the stored nonce value as -the value to the `--blockhash` argument upon signing and submission. Obtain the -presently stored nonce value with - -- Command - -```bash -solana nonce nonce-keypair.json -``` - -- Output - -```text -8GRipryfxcsxN8mAGjy8zbFo9ezaUsh47TsPzmZbuytU -``` - -> [Full usage documentation](../usage.md#solana-get-nonce) - -### Advancing the Stored Nonce Value - -While not typically needed outside a more useful transaction, the stored nonce -value can be advanced by - -- Command - -```bash -solana new-nonce nonce-keypair.json -``` - -- Output - -```text -44jYe1yPKrjuYDmoFTdgPjg8LFpYyh1PFKJqm5SC1PiSyAL8iw1bhadcAX1SL7KDmREEkmHpYvreKoNv6fZgfvUK -``` - -> [Full usage documentation](../usage.md#solana-new-nonce) - -### Display Nonce Account - -Inspect a nonce account in a more human friendly format with - -- Command - -```bash -solana nonce-account nonce-keypair.json -``` - -- Output - -```text -balance: 0.5 SOL -minimum balance required: 0.00136416 SOL -nonce: DZar6t2EaCFQTbUP4DHKwZ1wT8gCPW2aRfkVWhydkBvS -``` - -> [Full usage documentation](../usage.md#solana-nonce-account) - -### Withdraw Funds from a Nonce Account - -Withdraw funds from a nonce account with - -- Command - -```bash -solana withdraw-from-nonce-account nonce-keypair.json ~/.config/solana/id.json 0.5 -``` - -- Output - -```text -3foNy1SBqwXSsfSfTdmYKDuhnVheRnKXpoPySiUDBVeDEs6iMVokgqm7AqfTjbk7QBE8mqomvMUMNQhtdMvFLide -``` - -> Close a nonce account by withdrawing the full balance - -> [Full usage documentation](../usage.md#solana-withdraw-from-nonce-account) - -### Assign a New Authority to a Nonce Account - -Reassign the authority of a nonce account after creation with - -- Command - -```bash -solana authorize-nonce-account nonce-keypair.json nonce-authority.json -``` - -- Output - -```text -3F9cg4zN9wHxLGx4c3cUKmqpej4oa67QbALmChsJbfxTgTffRiL3iUehVhR9wQmWgPua66jPuAYeL1K2pYYjbNoT -``` - -> [Full usage documentation](../usage.md#solana-authorize-nonce-account) - -## Other Commands Supporting Durable Nonces - -To make use of durable nonces with other CLI subcommands, two arguments must be -supported. - -- `--nonce`, specifies the account storing the nonce value -- `--nonce-authority`, specifies an optional [nonce authority](#nonce-authority) - -The following subcommands have received this treatment so far - -- [`pay`](../usage.md#solana-pay) -- [`delegate-stake`](../usage.md#solana-delegate-stake) -- [`deactivate-stake`](../usage.md#solana-deactivate-stake) - -### Example Pay Using Durable Nonce - -Here we demonstrate Alice paying Bob 1 SOL using a durable nonce. The procedure -is the same for all subcommands supporting durable nonces - -#### - Create accounts - -First we need some accounts for Alice, Alice's nonce and Bob - -```bash -$ solana-keygen new -o alice.json -$ solana-keygen new -o nonce.json -$ solana-keygen new -o bob.json -``` - -#### - Fund Alice's account - -Alice will need some funds to create a nonce account and send to Bob. Airdrop -her some SOL - -```bash -$ solana airdrop -k alice.json 1 -1 SOL -``` - -#### - Create Alice's nonce account - -Now Alice needs a nonce account. Create one - -> Here, no separate [nonce authority](#nonce-authority) is employed, so -> `alice.json` has full authority over the nonce account - -```bash -$ solana create-nonce-account -k alice.json nonce.json 0.1 -3KPZr96BTsL3hqera9up82KAU462Gz31xjqJ6eHUAjF935Yf8i1kmfEbo6SVbNaACKE5z6gySrNjVRvmS8DcPuwV -``` - -#### - A failed first attempt to pay Bob - -Alice attempts to pay Bob, but takes too long to sign. The specified blockhash -expires and the transaction fails - -```bash -$ solana transfer -k alice.json --blockhash expiredDTaxfagttWjQweib42b6ZHADSx94Tw8gHx11 bob.json 0.01 -[2020-01-02T18:48:28.462911000Z ERROR solana_cli::cli] Io(Custom { kind: Other, error: "Transaction \"33gQQaoPc9jWePMvDAeyJpcnSPiGUAdtVg8zREWv4GiKjkcGNufgpcbFyRKRrA25NkgjZySEeKue5rawyeH5TzsV\" failed: None" }) -Error: Io(Custom { kind: Other, error: "Transaction \"33gQQaoPc9jWePMvDAeyJpcnSPiGUAdtVg8zREWv4GiKjkcGNufgpcbFyRKRrA25NkgjZySEeKue5rawyeH5TzsV\" failed: None" }) -``` - -#### - Nonce to the rescue! - -Alice retries the transaction, this time specifying her nonce account and the -blockhash stored there - -> Remember, `alice.json` is the [nonce authority](#nonce-authority) in this -> example - -```bash -$ solana nonce-account nonce.json -balance: 0.1 SOL -minimum balance required: 0.00136416 SOL -nonce: F7vmkY3DTaxfagttWjQweib42b6ZHADSx94Tw8gHx3W7 -``` - -```bash -$ solana transfer -k alice.json --blockhash F7vmkY3DTaxfagttWjQweib42b6ZHADSx94Tw8gHx3W7 --nonce nonce.json bob.json 0.01 -HR1368UKHVZyenmH7yVz5sBAijV6XAPeWbEiXEGVYQorRMcoijeNAbzZqEZiH8cDB8tk65ckqeegFjK8dHwNFgQ -``` - -#### - Success! - -The transaction succeeds! Bob receives 0.01 SOL from Alice and Alice's stored -nonce advances to a new value - -```bash -$ solana balance -k bob.json -0.01 SOL -``` - -```bash -$ solana nonce-account nonce.json -balance: 0.1 SOL -minimum balance required: 0.00136416 SOL -nonce: 6bjroqDcZgTv6Vavhqf81oBHTv3aMnX19UTB51YhAZnN -``` diff --git a/docs/src/cli/examples/offline-signing.md b/docs/src/cli/examples/offline-signing.md deleted file mode 100644 index 28b54561732..00000000000 --- a/docs/src/cli/examples/offline-signing.md +++ /dev/null @@ -1,176 +0,0 @@ ---- -title: Offline Transaction Signing with the Solana CLI -pagination_label: "Solana CLI: Offline Transaction Signing" -sidebar_label: Offline Transaction Signing ---- - -Some security models require keeping signing keys, and thus the signing -process, separated from transaction creation and network broadcast. Examples -include: - -- Collecting signatures from geographically disparate signers in a - [multi-signature scheme](https://spl.solana.com/token#multisig-usage) -- Signing transactions using an [air-gapped]() - signing device - -This document describes using Solana's CLI to separately sign and submit a -transaction. - -## Commands Supporting Offline Signing - -At present, the following commands support offline signing: - -- [`create-stake-account`](../usage.md#solana-create-stake-account) -- [`create-stake-account-checked`](../usage.md#solana-create-stake-account-checked) -- [`deactivate-stake`](../usage.md#solana-deactivate-stake) -- [`delegate-stake`](../usage.md#solana-delegate-stake) -- [`split-stake`](../usage.md#solana-split-stake) -- [`stake-authorize`](../usage.md#solana-stake-authorize) -- [`stake-authorize-checked`](../usage.md#solana-stake-authorize-checked) -- [`stake-set-lockup`](../usage.md#solana-stake-set-lockup) -- [`stake-set-lockup-checked`](../usage.md#solana-stake-set-lockup-checked) -- [`transfer`](../usage.md#solana-transfer) -- [`withdraw-stake`](../usage.md#solana-withdraw-stake) - -- [`create-vote-account`](../usage.md#solana-create-vote-account) -- [`vote-authorize-voter`](../usage.md#solana-vote-authorize-voter) -- [`vote-authorize-voter-checked`](../usage.md#solana-vote-authorize-voter-checked) -- [`vote-authorize-withdrawer`](../usage.md#solana-vote-authorize-withdrawer) -- [`vote-authorize-withdrawer-checked`](../usage.md#solana-vote-authorize-withdrawer-checked) -- [`vote-update-commission`](../usage.md#solana-vote-update-commission) -- [`vote-update-validator`](../usage.md#solana-vote-update-validator) -- [`withdraw-from-vote-account`](../usage.md#solana-withdraw-from-vote-account) - -## Signing Transactions Offline - -To sign a transaction offline, pass the following arguments on the command line - -1. `--sign-only`, prevents the client from submitting the signed transaction - to the network. Instead, the pubkey/signature pairs are printed to stdout. -2. `--blockhash BASE58_HASH`, allows the caller to specify the value used to - fill the transaction's `recent_blockhash` field. This serves a number of - purposes, namely: - _ Eliminates the need to connect to the network and query a recent blockhash - via RPC - _ Enables the signers to coordinate the blockhash in a multiple-signature - scheme - -### Example: Offline Signing a Payment - -Command - -```bash -solana@offline$ solana transfer --sign-only --blockhash 5Tx8F3jgSHx21CbtjwmdaKPLM5tWmreWAnPrbqHomSJF \ - recipient-keypair.json 1 -``` - -Output - -```text - -Blockhash: 5Tx8F3jgSHx21CbtjwmdaKPLM5tWmreWAnPrbqHomSJF -Signers (Pubkey=Signature): - FhtzLVsmcV7S5XqGD79ErgoseCLhZYmEZnz9kQg1Rp7j=4vC38p4bz7XyiXrk6HtaooUqwxTWKocf45cstASGtmrD398biNJnmTcUCVEojE7wVQvgdYbjHJqRFZPpzfCQpmUN - -{"blockhash":"5Tx8F3jgSHx21CbtjwmdaKPLM5tWmreWAnPrbqHomSJF","signers":["FhtzLVsmcV7S5XqGD79ErgoseCLhZYmEZnz9kQg1Rp7j=4vC38p4bz7XyiXrk6HtaooUqwxTWKocf45cstASGtmrD398biNJnmTcUCVEojE7wVQvgdYbjHJqRFZPpzfCQpmUN"]}' -``` - -## Submitting Offline Signed Transactions to the Network - -To submit a transaction that has been signed offline to the network, pass the -following arguments on the command line - -1. `--blockhash BASE58_HASH`, must be the same blockhash as was used to sign -2. `--signer BASE58_PUBKEY=BASE58_SIGNATURE`, one for each offline signer. This - includes the pubkey/signature pairs directly in the transaction rather than - signing it with any local keypair(s) - -### Example: Submitting an Offline Signed Payment - -Command - -```bash -solana@online$ solana transfer --blockhash 5Tx8F3jgSHx21CbtjwmdaKPLM5tWmreWAnPrbqHomSJF \ - --signer FhtzLVsmcV7S5XqGD79ErgoseCLhZYmEZnz9kQg1Rp7j=4vC38p4bz7XyiXrk6HtaooUqwxTWKocf45cstASGtmrD398biNJnmTcUCVEojE7wVQvgdYbjHJqRFZPpzfCQpmUN - recipient-keypair.json 1 -``` - -Output - -```text -4vC38p4bz7XyiXrk6HtaooUqwxTWKocf45cstASGtmrD398biNJnmTcUCVEojE7wVQvgdYbjHJqRFZPpzfCQpmUN -``` - -## Offline Signing Over Multiple Sessions - -Offline signing can also take place over multiple sessions. In this scenario, -pass the absent signer's public key for each role. All pubkeys that were specified, -but no signature was generated for will be listed as absent in the offline signing -output - -### Example: Transfer with Two Offline Signing Sessions - -Command (Offline Session #1) - -```text -solana@offline1$ solana transfer Fdri24WUGtrCXZ55nXiewAj6RM18hRHPGAjZk3o6vBut 10 \ - --blockhash 7ALDjLv56a8f6sH6upAZALQKkXyjAwwENH9GomyM8Dbc \ - --sign-only \ - --keypair fee_payer.json \ - --from 674RgFMgdqdRoVtMqSBg7mHFbrrNm1h1r721H1ZMquHL -``` - -Output (Offline Session #1) - -```text -Blockhash: 7ALDjLv56a8f6sH6upAZALQKkXyjAwwENH9GomyM8Dbc -Signers (Pubkey=Signature): - 3bo5YiRagwmRikuH6H1d2gkKef5nFZXE3gJeoHxJbPjy=ohGKvpRC46jAduwU9NW8tP91JkCT5r8Mo67Ysnid4zc76tiiV1Ho6jv3BKFSbBcr2NcPPCarmfTLSkTHsJCtdYi -Absent Signers (Pubkey): - 674RgFMgdqdRoVtMqSBg7mHFbrrNm1h1r721H1ZMquHL -``` - -Command (Offline Session #2) - -```text -solana@offline2$ solana transfer Fdri24WUGtrCXZ55nXiewAj6RM18hRHPGAjZk3o6vBut 10 \ - --blockhash 7ALDjLv56a8f6sH6upAZALQKkXyjAwwENH9GomyM8Dbc \ - --sign-only \ - --keypair from.json \ - --fee-payer 3bo5YiRagwmRikuH6H1d2gkKef5nFZXE3gJeoHxJbPjy -``` - -Output (Offline Session #2) - -```text -Blockhash: 7ALDjLv56a8f6sH6upAZALQKkXyjAwwENH9GomyM8Dbc -Signers (Pubkey=Signature): - 674RgFMgdqdRoVtMqSBg7mHFbrrNm1h1r721H1ZMquHL=3vJtnba4dKQmEAieAekC1rJnPUndBcpvqRPRMoPWqhLEMCty2SdUxt2yvC1wQW6wVUa5putZMt6kdwCaTv8gk7sQ -Absent Signers (Pubkey): - 3bo5YiRagwmRikuH6H1d2gkKef5nFZXE3gJeoHxJbPjy -``` - -Command (Online Submission) - -```text -solana@online$ solana transfer Fdri24WUGtrCXZ55nXiewAj6RM18hRHPGAjZk3o6vBut 10 \ - --blockhash 7ALDjLv56a8f6sH6upAZALQKkXyjAwwENH9GomyM8Dbc \ - --from 674RgFMgdqdRoVtMqSBg7mHFbrrNm1h1r721H1ZMquHL \ - --signer 674RgFMgdqdRoVtMqSBg7mHFbrrNm1h1r721H1ZMquHL=3vJtnba4dKQmEAieAekC1rJnPUndBcpvqRPRMoPWqhLEMCty2SdUxt2yvC1wQW6wVUa5putZMt6kdwCaTv8gk7sQ \ - --fee-payer 3bo5YiRagwmRikuH6H1d2gkKef5nFZXE3gJeoHxJbPjy \ - --signer 3bo5YiRagwmRikuH6H1d2gkKef5nFZXE3gJeoHxJbPjy=ohGKvpRC46jAduwU9NW8tP91JkCT5r8Mo67Ysnid4zc76tiiV1Ho6jv3BKFSbBcr2NcPPCarmfTLSkTHsJCtdYi -``` - -Output (Online Submission) - -```text -ohGKvpRC46jAduwU9NW8tP91JkCT5r8Mo67Ysnid4zc76tiiV1Ho6jv3BKFSbBcr2NcPPCarmfTLSkTHsJCtdYi -``` - -## Buying More Time to Sign - -Typically a Solana transaction must be signed and accepted by the network within -a number of slots from the blockhash in its `recent_blockhash` field (~1min at -the time of this writing). If your signing procedure takes longer than this, a -[Durable Transaction Nonce](./durable-nonce.md) can give you the extra time you -need. diff --git a/docs/src/cli/examples/sign-offchain-message.md b/docs/src/cli/examples/sign-offchain-message.md deleted file mode 100644 index bb0b44b35e1..00000000000 --- a/docs/src/cli/examples/sign-offchain-message.md +++ /dev/null @@ -1,100 +0,0 @@ ---- -title: Off-Chain Message Signing with the Solana CLI -pagination_label: "Solana CLI: Off-Chain Message Signing" -sidebar_label: Off-Chain Message Signing ---- - -Off-chain message signing is a method of signing non-transaction messages with -a Solana wallet. This feature can be used to authenticate users or provide -proof of wallet ownership. - -## Sign Off-Chain Message - -To sign an arbitrary off-chain message, run the following command: - -```bash -solana sign-offchain-message -``` - -The message will be encoded and signed with CLI's default private key and -signature printed to the output. If you want to sign it with another key, just -use the `-k/--keypair` option: - -```bash -solana sign-offchain-message -k -``` - -By default, the messages constructed are version 0, the only version currently -supported. When other versions become available, you can override the default -value with the `--version` option: - -```bash -solana sign-offchain-message -k --version -``` - -The message format is determined automatically based on the version and text -of the message. - -Version `0` headers specify three message formats allowing for trade-offs -between compatibility and composition of messages: - -| ID | Encoding | Maximum Length | Hardware Wallet Support | -| :-: | :-----------------: | :------------: | :---------------------: | -| 0 | Restricted ASCII \* | 1212 | Yes | -| 1 | UTF-8 | 1212 | Blind sign only | -| 2 | UTF-8 | 65515 | No | - -\* Those characters for which [`isprint(3)`](https://linux.die.net/man/3/isprint) -returns true. That is, `0x20..=0x7e`. - -Formats `0` and `1` are motivated by hardware wallet support where both RAM to -store the payload and font character support are limited. - -To sign an off-chain message with Ledger, ensure your Ledger is running latest -firmware and Solana Ledger App version 1.3.0 or later. After Ledger is -unlocked and Solana Ledger App is open, run: - -```bash -solana sign-offchain-message -k usb://ledger -``` - -For more information on how to setup and work with the ledger device see this -[link](../wallets/hardware/ledger.md). - -Please note that UTF-8 encoded messages require `Allow blind sign` option -enabled in Solana Ledger App. Also, due to the lack of UTF-8 support in Ledger -devices, only the hash of the message will be displayed in such cases. - -If `Display mode` is set to `Expert`, Ledger will display technical -information about the message to be signed. - -## Verify Off-Chain Message Signature - -To verify the off-chain message signature, run the following command: - -```bash -solana verify-offchain-signature -``` - -The public key of the default CLI signer will be used. You can specify another -key with the `--signer` option: - -```bash -solana verify-offchain-signature --signer -``` - -If the signed message has a version different from the default, you need to -specify the matching version explicitly: - -```bash -solana verify-offchain-signature --version -``` - -## Protocol Specification - -To ensure that off-chain messages are not valid transactions, they are encoded -with a fixed prefix: `\xffsolana offchain`, where first byte is chosen such -that it is implicitly illegal as the first byte in a transaction -`MessageHeader` today. More details about the payload format and other -considerations are available in the -[proposal](https://github.com/anza-xyz/agave/blob/master/docs/src/proposals/off-chain-message-signing.md). diff --git a/docs/src/cli/examples/test-validator.md b/docs/src/cli/examples/test-validator.md deleted file mode 100644 index dc8a484d6c7..00000000000 --- a/docs/src/cli/examples/test-validator.md +++ /dev/null @@ -1,171 +0,0 @@ ---- -title: Solana Test Validator -pagination_label: "Solana CLI: Test Validator" -sidebar_label: Test Validator ---- - -During early stage development, it is often convenient to target a cluster with -fewer restrictions and more configuration options than the public offerings -provide. This is easily achieved with the `solana-test-validator` binary, which -starts a full-featured, single-node cluster on the developer's workstation. - -## Advantages - -- No RPC rate-limits -- No airdrop limits -- Direct [on-chain program](https://solana.com/docs/programs/rust) deployment - (`--bpf-program ...`) -- Clone accounts from a public cluster, including programs (`--clone ...`) -- Load accounts from files -- Configurable transaction history retention (`--limit-ledger-size ...`) -- Configurable epoch length (`--slots-per-epoch ...`) -- Jump to an arbitrary slot (`--warp-slot ...`) - -## Installation - -The `solana-test-validator` binary ships with the Solana CLI Tool Suite. -[Install](../install.md) before continuing. - -## Running - -First take a look at the configuration options - -``` -solana-test-validator --help -``` - -Next start the test validator - -``` -solana-test-validator -``` - -By default, basic status information is printed while the process is running. -See [Appendix I](#appendix-i-status-output) for details - -``` -Ledger location: test-ledger -Log: test-ledger/validator.log -Identity: EPhgPANa5Rh2wa4V2jxt7YbtWa3Uyw4sTeZ13cQjDDB8 -Genesis Hash: 4754oPEMhAKy14CZc8GzQUP93CB4ouELyaTs4P8ittYn -Version: 1.6.7 -Shred Version: 13286 -Gossip Address: 127.0.0.1:1024 -TPU Address: 127.0.0.1:1027 -JSON RPC URL: http://127.0.0.1:8899 -⠈ 00:36:02 | Processed Slot: 5142 | Confirmed Slot: 5142 | Finalized Slot: 5110 | Snapshot Slot: 5100 | Transactions: 5142 | ◎499.974295000 -``` - -Leave `solana-test-validator` running in its own terminal. When it is no longer -needed, it can be stopped with ctrl-c. - -## Interacting - -Open a new terminal to interact with a [running](#running) `solana-test-validator` -instance using other binaries from the Solana CLI Tool Suite or your own client -software. - -#### Configure the CLI Tool Suite to target a local cluster by default - -``` -solana config set --url http://127.0.0.1:8899 -``` - -#### Verify the CLI Tool Suite configuration - -``` -solana genesis-hash -``` - -- **NOTE:** The result should match the `Genesis Hash:` field in the - `solana-test-validator` status output - -#### Check the wallet balance - -``` -solana balance -``` - -- **NOTE:** `Error: No such file or directory (os error 2)` means that the default - wallet does not yet exist. Create it with `solana-keygen new`. -- **NOTE:** If the wallet has a zero SOL balance, airdrop some localnet SOL with - `solana airdrop 10` - -#### Perform a basic transfer transaction - -``` -solana transfer EPhgPANa5Rh2wa4V2jxt7YbtWa3Uyw4sTeZ13cQjDDB8 1 -``` - -#### Monitor `msg!()` output from on-chain programs - -``` -solana logs -``` - -- **NOTE:** This command needs to be running when the target transaction is - executed. Run it in its own terminal - -## Appendix I: Status Output - -``` -Ledger location: test-ledger -``` - -- File path of the ledger storage directory. This directory can get large. Store - less transaction history with `--limit-ledger-size ...` or relocate it with - `--ledger ...` - -``` -Log: test-ledger/validator.log -``` - -- File path of the validator text log file. The log can also be streamed by - passing `--log`. Status output is suppressed in this case. - -``` -Identity: EPhgPANa5Rh2wa4V2jxt7YbtWa3Uyw4sTeZ13cQjDDB8 -``` - -- The validator's identity in the [gossip network](../../validator/gossip.md#gossip-overview) - -``` -Version: 1.6.7 -``` - -- The software version - -``` -Gossip Address: 127.0.0.1:1024 -TPU Address: 127.0.0.1:1027 -JSON RPC URL: http://127.0.0.1:8899 -``` - -- The network address of the [Gossip](../../validator/gossip.md#gossip-overview), - [Transaction Processing Unit](../../validator/tpu.md) and [JSON RPC](https://solana.com/docs/rpc) - service, respectively - -``` -⠈ 00:36:02 | Processed Slot: 5142 | Confirmed Slot: 5142 | Finalized Slot: 5110 | Snapshot Slot: 5100 | Transactions: 5142 | ◎499.974295000 -``` - -- Session running time, current slot of the three block - [commitment levels](https://solana.com/docs/rpc#configuring-state-commitment), - slot height of the last snapshot, transaction count, - [voting authority](../../operations/guides/vote-accounts.md#vote-authority) balance - -## Appendix II: Runtime Features - -By default, the test validator runs with all [runtime features](https://solana.com/docs/core/runtime#features) activated. - -You can verify this using the [Solana command-line tools](../install.md): - -```bash -solana feature status -ul -``` - -Since this may not always be desired, especially when testing programs meant for deployment to mainnet, the CLI provides an option to deactivate specific features: - -```bash -solana-test-validator --deactivate-feature --deactivate-feature -``` diff --git a/docs/src/cli/examples/transfer-tokens.md b/docs/src/cli/examples/transfer-tokens.md deleted file mode 100644 index 28c933be666..00000000000 --- a/docs/src/cli/examples/transfer-tokens.md +++ /dev/null @@ -1,168 +0,0 @@ ---- -title: Send and Receive Tokens with the Solana CLI -pagination_label: "Solana CLI: Send and Receive Tokens" -sidebar_label: Send and Receive Tokens ---- - -This page describes how to receive and send SOL tokens using the command line -tools with a command line wallet such as a [paper wallet](../wallets/paper.md), -a [file system wallet](../wallets/file-system.md), or a -[hardware wallet](../wallets/hardware/index.md). Before you begin, make sure -you have created a wallet and have access to its address (pubkey) and the -signing keypair. Check out our -[conventions for entering keypairs for different wallet types](../intro.md#keypair-conventions). - -## Testing your Wallet - -Before sharing your public key with others, you may want to first ensure the -key is valid and that you indeed hold the corresponding private key. - -In this example, we will create a second wallet in addition to your first wallet, -and then transfer some tokens to it. This will confirm that you can send and -receive tokens on your wallet type of choice. - -This test example uses our Developer Testnet, called devnet. Tokens issued -on devnet have **no** value, so don't worry if you lose them. - -#### Airdrop some tokens to get started - -First, _airdrop_ yourself some play tokens on the devnet. - -```bash -solana airdrop 1 --url https://api.devnet.solana.com -``` - -where you replace the text `` with your base58-encoded -public key/wallet address. - -A response with the signature of the transaction will be returned. If the balance -of the address does not change by the expected amount, run the following command -for more information on what potentially went wrong: - -```bash -solana confirm -v -``` - -#### Check your balance - -Confirm the airdrop was successful by checking the account's balance. -It should output `1 SOL`: - -```bash -solana balance --url https://api.devnet.solana.com -``` - -#### Create a second wallet address - -We will need a new address to receive our tokens. Create a second -keypair and record its pubkey: - -```bash -solana-keygen new --no-passphrase --no-outfile -``` - -The output will contain the address after the text `pubkey:`. Copy the -address. We will use it in the next step. - -```text -pubkey: GKvqsuNcnwWqPzzuhLmGi4rzzh55FhJtGizkhHaEJqiV -``` - -You can also create a second (or more) wallet of any type: -[paper](../wallets/paper.md#creating-multiple-paper-wallet-addresses), -[file system](../wallets/file-system.md#creating-multiple-file-system-wallet-addresses), -or [hardware](../wallets/hardware/index.md#multiple-addresses-on-a-single-hardware-wallet). - -#### Transfer tokens from your first wallet to the second address - -Next, prove that you own the airdropped tokens by transferring them. -The Solana cluster will only accept the transfer if you sign the transaction -with the private keypair corresponding to the sender's public key in the -transaction. - -```bash -solana transfer --from 0.5 --allow-unfunded-recipient --url https://api.devnet.solana.com --fee-payer -``` - -where you replace `` with the path to a keypair in your first wallet, -and replace `` with the address of your second -wallet. - -Confirm the updated balances with `solana balance`: - -```bash -solana balance --url http://api.devnet.solana.com -``` - -where `` is either the public key from your keypair or the -recipient's public key. - -#### Full example of test transfer - -```bash -$ solana-keygen new --outfile my_solana_wallet.json # Creating my first wallet, a file system wallet -Generating a new keypair -For added security, enter a passphrase (empty for no passphrase): -Wrote new keypair to my_solana_wallet.json -========================================================================== -pubkey: DYw8jCTfwHNRJhhmFcbXvVDTqWMEVFBX6ZKUmG5CNSKK # Here is the address of the first wallet -========================================================================== -Save this seed phrase to recover your new keypair: -width enhance concert vacant ketchup eternal spy craft spy guard tag punch # If this was a real wallet, never share these words on the internet like this! -========================================================================== - -$ solana airdrop 1 DYw8jCTfwHNRJhhmFcbXvVDTqWMEVFBX6ZKUmG5CNSKK --url https://api.devnet.solana.com # Airdropping 1 SOL to my wallet's address/pubkey -Requesting airdrop of 1 SOL from 35.233.193.70:9900 -1 SOL - -$ solana balance DYw8jCTfwHNRJhhmFcbXvVDTqWMEVFBX6ZKUmG5CNSKK --url https://api.devnet.solana.com # Check the address's balance -1 SOL - -$ solana-keygen new --no-outfile # Creating a second wallet, a paper wallet -Generating a new keypair -For added security, enter a passphrase (empty for no passphrase): -==================================================================== -pubkey: 7S3P4HxJpyyigGzodYwHtCxZyUQe9JiBMHyRWXArAaKv # Here is the address of the second, paper, wallet. -==================================================================== -Save this seed phrase to recover your new keypair: -clump panic cousin hurt coast charge engage fall eager urge win love # If this was a real wallet, never share these words on the internet like this! -==================================================================== - -$ solana transfer --from my_solana_wallet.json 7S3P4HxJpyyigGzodYwHtCxZyUQe9JiBMHyRWXArAaKv 0.5 --allow-unfunded-recipient --url https://api.devnet.solana.com --fee-payer my_solana_wallet.json # Transferring tokens to the public address of the paper wallet -3gmXvykAd1nCQQ7MjosaHLf69Xyaqyq1qw2eu1mgPyYXd5G4v1rihhg1CiRw35b9fHzcftGKKEu4mbUeXY2pEX2z # This is the transaction signature - -$ solana balance DYw8jCTfwHNRJhhmFcbXvVDTqWMEVFBX6ZKUmG5CNSKK --url https://api.devnet.solana.com -0.499995 SOL # The sending account has slightly less than 0.5 SOL remaining due to the 0.000005 SOL transaction fee payment - -$ solana balance 7S3P4HxJpyyigGzodYwHtCxZyUQe9JiBMHyRWXArAaKv --url https://api.devnet.solana.com -0.5 SOL # The second wallet has now received the 0.5 SOL transfer from the first wallet - -``` - -## Receive Tokens - -To receive tokens, you will need an address for others to send tokens to. In -Solana, the wallet address is the public key of a keypair. There are a variety -of techniques for generating keypairs. The method you choose will depend on how -you choose to store keypairs. Keypairs are stored in wallets. Before receiving -tokens, you will need to [create a wallet](../wallets/index.md). -Once completed, you should have a public key -for each keypair you generated. The public key is a long string of base58 -characters. Its length varies from 32 to 44 characters. - -## Send Tokens - -If you already hold SOL and want to send tokens to someone, you will need -a path to your keypair, their base58-encoded public key, and a number of -tokens to transfer. Once you have that collected, you can transfer tokens -with the `solana transfer` command: - -```bash -solana transfer --from --fee-payer -``` - -Confirm the updated balances with `solana balance`: - -```bash -solana balance -``` diff --git a/docs/src/cli/index.md b/docs/src/cli/index.md deleted file mode 100644 index 27deffd485d..00000000000 --- a/docs/src/cli/index.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -title: Solana CLI Tool Suite -sidebar_position: 0 -sidebar_label: Overview -pagination_label: Solana CLI Tool Suite ---- - -In this section, we will describe how to use the Solana command-line tools to -create a _wallet_, to send and receive SOL tokens, and to participate in the -cluster by delegating stake. - -To interact with a Solana cluster, we will use its command-line interface, also -known as the CLI. We use the command-line because it is the first place the -Anza core team deploys new functionality. The command-line interface is not -necessarily the easiest to use, but it provides the most direct, flexible, and -secure access to your Solana accounts. - -## Getting Started - -To get started using the Solana Command Line (CLI) tools: - -- [Install the Solana CLI Tool Suite](./install.md) -- [Introduction to our CLI conventions](./intro.md) -- [Create a Wallet using the CLI](./wallets/index.md) -- [Choose a Cluster to connect to using the CLI](./examples/choose-a-cluster.md) diff --git a/docs/src/cli/install.md b/docs/src/cli/install.md deleted file mode 100644 index 01548d20baa..00000000000 --- a/docs/src/cli/install.md +++ /dev/null @@ -1,257 +0,0 @@ ---- -title: Install the Solana CLI -pagination_label: Install the Solana CLI -sidebar_label: Installation -sidebar_position: 1 ---- - -There are multiple ways to install the Solana tools on your computer depending -on your preferred workflow: - -- [Use the Solana Install Tool (Simplest option)](#use-solanas-install-tool) -- [Download Prebuilt Binaries](#download-prebuilt-binaries) -- [Build from Source](#build-from-source) -- [Use Homebrew](#use-homebrew) - -## Use The Solana Install Tool - -### MacOS & Linux - -- Open your favorite Terminal application - -- Install the Agave release - [LATEST_AGAVE_RELEASE_VERSION](https://github.com/anza-xyz/agave/releases/tag/LATEST_AGAVE_RELEASE_VERSION) - on your machine by running: - -```bash -sh -c "$(curl -sSfL https://release.anza.xyz/LATEST_AGAVE_RELEASE_VERSION/install)" -``` - -- You can replace `LATEST_AGAVE_RELEASE_VERSION` with the release tag matching - the software version of your desired release, or use one of the three symbolic - channel names: `stable`, `beta`, or `edge`. - -- The following output indicates a successful update: - -```text -downloading LATEST_AGAVE_RELEASE_VERSION installer -Configuration: /home/solana/.config/solana/install/config.yml -Active release directory: /home/solana/.local/share/solana/install/active_release -* Release version: LATEST_AGAVE_RELEASE_VERSION -* Release URL: https://github.com/anza-xyz/agave/releases/download/LATEST_AGAVE_RELEASE_VERSION/solana-release-x86_64-unknown-linux-gnu.tar.bz2 -Update successful -``` - -- Depending on your system, the end of the installer messaging may prompt you to - -```bash -Please update your PATH environment variable to include the solana programs: -``` - -- If you get the above message, copy and paste the recommended command below it - to update `PATH` -- Confirm you have the desired version of `solana` installed by running: - -```bash -solana --version -``` - -- After a successful install, `agave-install update` may be used to easily - update the Solana software to a newer version at any time. - ---- - -### Windows - -- Open a Command Prompt (`cmd.exe`) as an Administrator - - - Search for Command Prompt in the Windows search bar. When the Command Prompt - app appears, right-click and select "Open as Administrator". If you are - prompted by a pop-up window asking "Do you want to allow this app to make - changes to your device?", click Yes. - -- Copy and paste the following command, then press Enter to download the Solana - installer into a temporary directory: - -```bash -cmd /c "curl https://release.anza.xyz/LATEST_AGAVE_RELEASE_VERSION/agave-install-init-x86_64-pc-windows-msvc.exe --output C:\agave-install-tmp\agave-install-init.exe --create-dirs" -``` - -- Copy and paste the following command, then press Enter to install the latest - version of Solana. If you see a security pop-up by your system, please select - to allow the program to run. - -```bash -C:\agave-install-tmp\agave-install-init.exe LATEST_AGAVE_RELEASE_VERSION -``` - -- When the installer is finished, press Enter. - -- Close the command prompt window and re-open a new command prompt window as a - normal user - - Search for "Command Prompt" in the search bar, then left click on the - Command Prompt app icon, no need to run as Administrator) -- Confirm you have the desired version of `solana` installed by entering: - -```bash -solana --version -``` - -- After a successful install, `agave-install update` may be used to easily - update the Solana software to a newer version at any time. - -## Download Prebuilt Binaries - -If you would rather not use `agave-install` to manage the install, you can -manually download and install the binaries. - -### Linux - -Download the binaries by navigating to -[https://github.com/anza-xyz/agave/releases/latest](https://github.com/anza-xyz/agave/releases/latest), -download **solana-release-x86_64-unknown-linux-gnu.tar.bz2**, then extract the -archive: - -```bash -tar jxf solana-release-x86_64-unknown-linux-gnu.tar.bz2 -cd solana-release/ -export PATH=$PWD/bin:$PATH -``` - -### MacOS - -Download the binaries by navigating to -[https://github.com/anza-xyz/agave/releases/latest](https://github.com/anza-xyz/agave/releases/latest), -download **solana-release-x86_64-apple-darwin.tar.bz2**, then extract the -archive: - -```bash -tar jxf solana-release-x86_64-apple-darwin.tar.bz2 -cd solana-release/ -export PATH=$PWD/bin:$PATH -``` - -### Windows - -- Download the binaries by navigating to - [https://github.com/anza-xyz/agave/releases/latest](https://github.com/anza-xyz/agave/releases/latest), - download **solana-release-x86_64-pc-windows-msvc.tar.bz2**, then extract the - archive using WinZip or similar. - -- Open a Command Prompt and navigate to the directory into which you extracted - the binaries and run: - -```bash -cd solana-release/ -set PATH=%cd%/bin;%PATH% -``` - -## Build From Source - -If you are unable to use the prebuilt binaries or prefer to build it yourself -from source, follow these steps, ensuring you have the necessary prerequisites -installed on your system. - -### Prerequisites - -Before building from source, make sure to install the following prerequisites: - -#### Rust - -For all platforms, check "Install Rust" at -[https://www.rust-lang.org/tools/install](https://www.rust-lang.org/tools/install) -for the latest installation instructions. - -#### For Debian and Other Linux Distributions: - -Install build dependencies: - -- Build essential -- Package config -- Udev & LLM & libclang -- Protocol buffers - -```bash -apt-get install \ - build-essential \ - pkg-config \ - libudev-dev llvm libclang-dev \ - protobuf-compiler -``` - -#### For Other Linux Distributions: - -Replace `apt` with your distribution's package manager (e.g., `yum`, `dnf`, -`pacman`) and adjust package names as needed. - -#### For macOS: - -Check "Install Homebrew" at [https://brew.sh/](https://brew.sh/) for the latest -installation instruction for Homebrew if not already installed. - -Then, install build dependencies with `brew`: - -```bash -brew install pkg-config libudev protobuf llvm coreutils -``` - -Follow the instructions given at the end of the brew install command about -`PATH` configurations. - -#### For Windows: - -- Download and install the Build Tools for Visual Studio (2019 or later) from - the - [Visual Studio downloads page](https://visualstudio.microsoft.com/downloads/). - Make sure to include the C++ build tools in the installation. -- Install LLVM: Download and install LLVM from the - [official LLVM download page](https://releases.llvm.org/download.html). -- Install Protocol Buffers Compiler (protoc): Download `protoc` from the - [GitHub releases page of Protocol Buffers](https://github.com/protocolbuffers/protobuf/releases), - and add it to your `PATH`. - -:::info - -Users on Windows 10 or 11 may need to install -[Windows Subsystem for Linux](https://learn.microsoft.com/en-us/windows/wsl/install) -(WSL) in order to be able to build from source. WSL provides a Linux environment -that runs inside your existing Windows installation. You can then run regular -Linux software, including the Linux versions of Solana CLI. - -After installed, run `wsl` from your Windows terminal, then continue through the -[Debian and Other Linux Distributions](#for-debian-and-other-linux-distributions) -above. - -::: - -### Building from Source - -After installing the prerequisites, proceed with building Solana from source, -navigate to the -[Agave GitHub releases page](https://github.com/anza-xyz/agave/releases/latest), -and download the **Source Code** archive. Extract the code and build the -binaries with: - -```bash -./scripts/cargo-install-all.sh . -export PATH=$PWD/bin:$PATH -``` - -## Use Homebrew - -This option requires you to have [Homebrew](https://brew.sh/) package manager on -your MacOS or Linux machine. - -### MacOS & Linux - -- Follow instructions at: https://formulae.brew.sh/formula/solana - -[Homebrew formulae](https://github.com/Homebrew/homebrew-core/blob/HEAD/Formula/s/solana.rb) -is updated after each `solana` release, however it is possible that the Homebrew -version is outdated. - -- Confirm you have the desired version of `solana` installed by entering: - -```bash -solana --version -``` diff --git a/docs/src/cli/intro.md b/docs/src/cli/intro.md deleted file mode 100644 index 436776ee718..00000000000 --- a/docs/src/cli/intro.md +++ /dev/null @@ -1,83 +0,0 @@ ---- -title: Introduction to the Solana CLI -pagination_label: Introduction to the Solana CLI -sidebar_label: Introduction -sidebar_position: 2 ---- - -Before running any Solana CLI commands, let's go over some conventions that -you will see across all commands. First, the Solana CLI is actually a collection -of different commands for each action you might want to take. You can view the list -of all possible commands by running: - -```bash -solana --help -``` - -To zoom in on how to use a particular command, run: - -```bash -solana --help -``` - -where you replace the text `` with the name of the command you want -to learn more about. - -The command's usage message will typically contain words such as ``, -`` or ``. Each word is a placeholder for the _type_ of -text you can execute the command with. For example, you can replace `` -with a number such as `42` or `100.42`. You can replace `` with -the base58 encoding of your public key, such as -`9grmKMwTiZwUHSExjtbFzHLPTdWoXgcg1bZkhvwTrTww`. - -## Keypair conventions - -Many commands using the CLI tools require a value for a ``. The value -you should use for the keypair depends on what type of -[command line wallet you created](./wallets/index.md). - -For example, the CLI help shows that the way to display any wallet's address -(also known as the keypair's pubkey), is: - -```bash -solana-keygen pubkey -``` - -Below, we show how to resolve what you should put in `` depending -on your wallet type. - -## Paper Wallet - -In a paper wallet, the keypair is securely derived from the seed words and -optional passphrase you entered when the wallet was created. To use a paper -wallet keypair anywhere the `` text is shown in examples or help -documents, enter the uri scheme `prompt://` and the program will prompt you to -enter your seed words when you run the command. - -To display the wallet address of a Paper Wallet: - -```bash -solana-keygen pubkey prompt:// -``` - -## File System Wallet - -With a file system wallet, the keypair is stored in a file on your computer. -Replace `` with the complete file path to the keypair file. - -For example, if the file system keypair file location is -`/home/solana/my_wallet.json`, to display the address, do: - -```bash -solana-keygen pubkey /home/solana/my_wallet.json -``` - -## Hardware Wallet - -If you chose a hardware wallet, use your -[keypair URL](./wallets/hardware/index.md#specify-a-hardware-wallet-key), -such as `usb://ledger?key=0`. - -```bash -solana-keygen pubkey usb://ledger?key=0 -``` diff --git a/docs/src/cli/wallets/_category_.json b/docs/src/cli/wallets/_category_.json deleted file mode 100644 index e420a33bb4a..00000000000 --- a/docs/src/cli/wallets/_category_.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "position": 3.5, - "label": "Command-line Wallets", - "collapsible": true, - "collapsed": false, - "link": null -} diff --git a/docs/src/cli/wallets/file-system.md b/docs/src/cli/wallets/file-system.md deleted file mode 100644 index dd21203c4e1..00000000000 --- a/docs/src/cli/wallets/file-system.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -title: File System Wallets using the CLI -pagination_label: File System Wallets using the CLI -sidebar_label: File System Wallets -sidebar_position: 2 ---- - -This document describes how to create and use a file system wallet with the -Solana CLI tools. A file system wallet exists as an unencrypted keypair file -on your computer system's filesystem. - -> File system wallets are the **least secure** method of storing SOL tokens. Storing large amounts of tokens in a file system wallet is **not recommended**. - -## Before you Begin - -Make sure you have -[installed the Solana Command Line Tools](../install.md) - -## Generate a File System Wallet Keypair - -Use Solana's command-line tool `solana-keygen` to generate keypair files. For -example, run the following from a command-line shell: - -```bash -mkdir ~/my-solana-wallet -solana-keygen new --outfile ~/my-solana-wallet/my-keypair.json -``` - -This file contains your **unencrypted** keypair. In fact, even if you specify -a password, that password applies to the recovery seed phrase, not the file. Do -not share this file with others. Anyone with access to this file will have access -to all tokens sent to its public key. Instead, you should share only its public -key. To display its public key, run: - -```bash -solana-keygen pubkey ~/my-solana-wallet/my-keypair.json -``` - -It will output a string of characters, such as: - -```text -ErRr1caKzK8L8nn4xmEWtimYRiTCAZXjBtVphuZ5vMKy -``` - -This is the public key corresponding to the keypair in -`~/my-solana-wallet/my-keypair.json`. The public key of the keypair file is -your _wallet address_. - -## Verify your Address against your Keypair file - -To verify you hold the private key for a given address, use -`solana-keygen verify`: - -```bash -solana-keygen verify ~/my-solana-wallet/my-keypair.json -``` - -where `` is replaced with your wallet address. -The command will output "Success" if the given address matches the -one in your keypair file, and "Failed" otherwise. - -## Creating Multiple File System Wallet Addresses - -You can create as many wallet addresses as you like. Simply re-run the -steps in [Generate a File System Wallet](#generate-a-file-system-wallet-keypair) -and make sure to use a new filename or path with the `--outfile` argument. -Multiple wallet addresses can be useful if you want to transfer tokens between -your own accounts for different purposes. diff --git a/docs/src/cli/wallets/hardware/_category_.json b/docs/src/cli/wallets/hardware/_category_.json deleted file mode 100644 index d000fb2312b..00000000000 --- a/docs/src/cli/wallets/hardware/_category_.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "position": 5, - "label": "Hardware Wallets", - "collapsible": false, - "collapsed": false, - "link": null -} diff --git a/docs/src/cli/wallets/hardware/index.md b/docs/src/cli/wallets/hardware/index.md deleted file mode 100644 index c2491e4b3de..00000000000 --- a/docs/src/cli/wallets/hardware/index.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -title: Using Hardware Wallets in the Solana CLI -pagination_label: "Using Hardware Wallets in the Solana CLI" -sidebar_label: Using in the Solana CLI -sidebar_position: 0 ---- - -Signing a transaction requires a private key, but storing a private -key on your personal computer or phone leaves it subject to theft. -Adding a password to your key adds security, but many people prefer -to take it a step further and move their private keys to a separate -physical device called a _hardware wallet_. A hardware wallet is a -small handheld device that stores private keys and provides some -interface for signing transactions. - -The Solana CLI has first class support for hardware wallets. Anywhere -you use a keypair filepath (denoted as `` in usage docs), you -can pass a _keypair URL_ that uniquely identifies a keypair in a -hardware wallet. - -## Supported Hardware Wallets - -The Solana CLI supports the following hardware wallets: - -- [Ledger Nano S, Nano S Plus, and Nano X](./ledger.md) -- [Trezor Model T, Safe 3, and Safe 5](./trezor.md) - -## Specify a Keypair URL - -Solana defines a keypair URL format to uniquely locate any Solana keypair on a -hardware wallet connected to your computer. - -The keypair URL has the following form, where square brackets denote optional -fields: - -```text -usb://[/][?key=] -``` - -`WALLET_ID` is a globally unique key used to disambiguate multiple devices. - -`DERVIATION_PATH` is used to navigate to Solana keys within your hardware wallet. -The path has the form `[/]`, where each `ACCOUNT` and `CHANGE` -are nonnegative integers. - -For example, a fully qualified URL for a Ledger device might be: - -```text -usb://ledger/BsNsvfXqQTtJnagwFWdBS7FBXgnsK8VZ5CmuznN85swK?key=0/0 -``` - -All derivation paths implicitly include the prefix `44'/501'`, which indicates -the path follows the [BIP44 specifications](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki) -and that any derived keys are Solana keys (Coin type 501). The single quote -indicates a "hardened" derivation. Because Solana uses Ed25519 keypairs, all -derivations are hardened and therefore adding the quote is optional and -unnecessary. diff --git a/docs/src/cli/wallets/hardware/ledger.md b/docs/src/cli/wallets/hardware/ledger.md deleted file mode 100644 index e5a45c63df0..00000000000 --- a/docs/src/cli/wallets/hardware/ledger.md +++ /dev/null @@ -1,218 +0,0 @@ ---- -title: Using Ledger Nano Hardware Wallets in the Solana CLI -pagination_label: "Hardware Wallets in the Solana CLI: Ledger Nano" -sidebar_label: Ledger Nano ---- - -This page describes how to use a Ledger Nano S, Nano S Plus, or Nano X to -interact with Solana using the command line tools. - -## Before You Begin - -- [Set up a Nano with the Solana App](https://support.ledger.com/hc/en-us/articles/360016265659-Solana-SOL-?docs=true) -- [Install the Solana command-line tools](../../install.md) - -## Use Ledger Nano with Solana CLI - -1. Ensure the Ledger Live application is closed -2. Plug your Nano into your computer's USB port -3. Enter your pin and start the Solana app on the Nano -4. Ensure the screen reads "Application is ready" - -### View your Wallet ID - -On your computer, run: - -```bash -solana-keygen pubkey usb://ledger -``` - -This confirms your Ledger device is connected properly and in the correct state -to interact with the Solana CLI. The command returns your Ledger's unique -_wallet ID_. When you have multiple Nano devices connected to the same computer, -you can use your wallet ID to specify which Ledger hardware wallet you want to -use. If you only plan to use a single Nano on your computer at a time, you don't -need to include the wallet ID. For information on using the wallet ID to use a -specific Ledger, see -[Manage Multiple Hardware Wallets](#manage-multiple-hardware-wallets). - -### View your Wallet Addresses - -Your Nano supports an arbitrary number of valid wallet addresses and signers. To -view any address, use the `solana-keygen pubkey` command, as shown below, -followed by a valid [keypair URL](./index.md#specify-a-keypair-url). - -Multiple wallet addresses can be useful if you want to transfer tokens between -your own accounts for different purposes, or use different keypairs on the -device as signing authorities for a stake account, for example. - -All of the following commands will display different addresses, associated with -the keypair path given. Try them out! - -```bash -solana-keygen pubkey usb://ledger -solana-keygen pubkey usb://ledger?key=0 -solana-keygen pubkey usb://ledger?key=1 -solana-keygen pubkey usb://ledger?key=2 -``` - -- NOTE: keypair url parameters are ignored in **zsh** -  [see troubleshooting for more info](#troubleshooting) - -You can use other values for the number after `key=` as well. Any of the -addresses displayed by these commands are valid Solana wallet addresses. The -private portion associated with each address is stored securely on the Nano, and -is used to sign transactions from this address. Just make a note of which -keypair URL you used to derive any address you will be using to receive tokens. - -If you are only planning to use a single address/keypair on your device, a good -easy-to-remember path might be to use the address at `key=0`. View this address -with: - -```bash -solana-keygen pubkey usb://ledger?key=0 -``` - -Now you have a wallet address (or multiple addresses), you can share any of -these addresses publicly to act as a receiving address, and you can use the -associated keypair URL as the signer for transactions from that address. - -### View your Balance - -To view the balance of any account, regardless of which wallet it uses, use the -`solana balance` command: - -```bash -solana balance SOME_WALLET_ADDRESS -``` - -For example, if your address is `7cvkjYAkUYs4W8XcXsca7cBrEGFeSUjeZmKoNBvEwyri`, -then enter the following command to view the balance: - -```bash -solana balance 7cvkjYAkUYs4W8XcXsca7cBrEGFeSUjeZmKoNBvEwyri -``` - -You can also view the balance of any account address on the Accounts tab in the -[Explorer](https://explorer.solana.com/accounts) and paste the address in the -box to view the balance in your web browser. - -Note: Any address with a balance of 0 SOL, such as a newly created one on your -Ledger, will show as "Not Found" in the explorer. Empty accounts and -non-existent accounts are treated the same in Solana. This will change when your -account address has some SOL in it. - -### Send SOL from a Nano - -To send some tokens from an address controlled by your Nano, you will need to -use the device to sign a transaction, using the same keypair URL you used to -derive the address. To do this, make sure your Nano is plugged in, unlocked with -the PIN, Ledger Live is not running, and the Solana App is open on the device, -showing "Application is Ready". - -The `solana transfer` command is used to specify to which address to send -tokens, how many tokens to send, and uses the `--keypair` argument to specify -which keypair is sending the tokens, which will sign the transaction, and the -balance from the associated address will decrease. - -```bash -solana transfer RECIPIENT_ADDRESS AMOUNT --keypair KEYPAIR_URL_OF_SENDER -``` - -Below is a full example. First, an address is viewed at a certain keypair URL. -Second, the balance of that address is checked. Lastly, a transfer transaction -is entered to send `1` SOL to the recipient address -`7cvkjYAkUYs4W8XcXsca7cBrEGFeSUjeZmKoNBvEwyri`. When you hit Enter for a -transfer command, you will be prompted to approve the transaction details on -your Ledger device. On the device, use the right and left buttons to review the -transaction details. If they look correct, click both buttons on the "Approve" -screen, otherwise push both buttons on the "Reject" screen. - -```bash -~$ solana-keygen pubkey usb://ledger?key=42 -CjeqzArkZt6xwdnZ9NZSf8D1CNJN1rjeFiyd8q7iLWAV - -~$ solana balance CjeqzArkZt6xwdnZ9NZSf8D1CNJN1rjeFiyd8q7iLWAV -1.000005 SOL - -~$ solana transfer 7cvkjYAkUYs4W8XcXsca7cBrEGFeSUjeZmKoNBvEwyri 1 --keypair usb://ledger?key=42 -Waiting for your approval on Ledger hardware wallet usb://ledger/2JT2Xvy6T8hSmT8g6WdeDbHUgoeGdj6bE2VueCZUJmyN -✅ Approved - -Signature: kemu9jDEuPirKNRKiHan7ycybYsZp7pFefAdvWZRq5VRHCLgXTXaFVw3pfh87MQcWX4kQY4TjSBmESrwMApom1V -``` - -After approving the transaction on your device, the program will display the -transaction signature, and wait for the maximum number of confirmations (32) -before returning. This only takes a few seconds, and then the transaction is -finalized on the Solana network. You can view details of this or any other -transaction by going to the Transaction tab in the -[Explorer](https://explorer.solana.com/transactions) and paste in the -transaction signature. - -## Advanced Operations - -### Manage Multiple Hardware Wallets - -It is sometimes useful to sign a transaction with keys from multiple hardware -wallets. Signing with multiple wallets requires _fully qualified keypair URLs_. -When the URL is not fully qualified, the Solana CLI will prompt you with the -fully qualified URLs of all connected hardware wallets, and ask you to choose -which wallet to use for each signature. - -Instead of using the interactive prompts, you can generate fully qualified URLs -using the Solana CLI `resolve-signer` command. For example, try connecting a -Nano to USB, unlock it with your pin, and running the following command: - -```text -solana resolve-signer usb://ledger?key=0/0 -``` - -You will see output similar to: - -```text -usb://ledger/BsNsvfXqQTtJnagwFWdBS7FBXgnsK8VZ5CmuznN85swK?key=0/0 -``` - -but where `BsNsvfXqQTtJnagwFWdBS7FBXgnsK8VZ5CmuznN85swK` is your `WALLET_ID`. - -With your fully qualified URL, you can connect multiple hardware wallets to the -same computer and uniquely identify a keypair from any of them. Use the output -from the `resolve-signer` command anywhere a `solana` command expects a -`` entry to use that resolved path as the signer for that part of the -given transaction. - -## Troubleshooting - -### Keypair URL parameters are ignored in zsh - -The question mark character is a special character in zsh. If that's not a -feature you use, add the following line to your `~/.zshrc` to treat it as a -normal character: - -```bash -unsetopt nomatch -``` - -Then either restart your shell window or run `~/.zshrc`: - -```bash -source ~/.zshrc -``` - -If you would prefer not to disable zsh's special handling of the question mark -character, you can disable it explicitly with a backslash in your keypair URLs. -For example: - -```bash -solana-keygen pubkey usb://ledger\?key=0 -``` - -## Support - -You can find additional support and get help on the -[Solana StackExchange](https://solana.stackexchange.com). - -Read more about [sending and receiving tokens](../../examples/transfer-tokens.md) and -[delegating stake](../../examples/delegate-stake.md). You can use your Ledger keypair -URL anywhere you see an option or argument that accepts a ``. diff --git a/docs/src/cli/wallets/hardware/trezor.md b/docs/src/cli/wallets/hardware/trezor.md deleted file mode 100644 index 4f372ca93e0..00000000000 --- a/docs/src/cli/wallets/hardware/trezor.md +++ /dev/null @@ -1,118 +0,0 @@ ---- -title: Using Trezor Hardware Wallets in the Solana CLI -pagination_label: "Hardware Wallets in the Solana CLI: Trezor" -sidebar_label: Trezor ---- - -This page describes how to use a Trezor Model T, Safe 3, or Safe 5 device to -interact with Solana using the command line tools. - -## Before You Begin - -- [Install the Solana command-line tools](../../install.md) -- [Review Trezor and BIP-32](https://trezor.io/learn/a/what-is-bip32) -- [Review Trezor and BIP-44](https://trezor.io/learn/a/what-is-bip44) - -## Use Trezor Model T, Safe 3, or Safe 5 with Solana CLI - -1. Plug your Trezor device into your computer's USB port -2. Tap to connect the device -3. Enter your pin -3. Ensure the screen reads the name of your device - -### View your Wallet Addresses - -On your computer, run: - -```bash -solana-keygen pubkey usb://trezor?key=0/0 -``` - -This confirms your Trezor device is connected properly and in the correct state -to interact with the Solana CLI. The command returns your Trezor device's first -Solana account's external (receiving) wallet address using the -[BIP-32](https://trezor.io/learn/a/what-is-bip32) derivation path -`m/44'/501'/0'/0'`. - -Your Trezor device supports an arbitrary number of valid wallet addresses and signers. To -view any address, use the `solana-keygen pubkey` command, as shown below, -followed by a valid [keypair URL](./index.md#specify-a-keypair-url). - -Multiple wallet addresses can be useful if you want to transfer tokens between -your own accounts for different purposes, or use different keypairs on the -device as signing authorities for a stake account, for example. - -All of the following commands will display different addresses, associated with -the keypair path given. Try them out! - -```bash -solana-keygen pubkey usb://trezor?key=0/0 -solana-keygen pubkey usb://trezor?key=0/1 -solana-keygen pubkey usb://trezor?key=1/0 -solana-keygen pubkey usb://trezor?key=1/1 -``` - -- NOTE: keypair url parameters are ignored in **zsh** -  [see troubleshooting for more info](#troubleshooting) - -You can use other values for the number after `key=` as well. Any of the -addresses displayed by these commands are valid Solana wallet addresses. The -private portion associated with each address is stored securely on the Trezor device, and -is used to sign transactions from this address. Just make a note of which -keypair URL you used to derive any address you will be using to receive tokens. - -If you are only planning to use a single address/keypair on your device, a good -easy-to-remember path might be to use the address at `key=0/`. View this address -with: - -```bash -solana-keygen pubkey usb://trezor?key=0/0 -solana-keygen pubkey usb://trezor?key=0/1 -``` - -Now you have a wallet address (or multiple addresses), you can share any of -these addresses publicly to act as a receiving address, and you can use the -associated keypair URL as the signer for transactions from that address. - -### Wallet Operations - -To use the device for wallet operations, such as balance fetching or -transferring SOL, follow the guides for -[viewing balance](./ledger.md#view-your-balance) or -[sending SOL](./ledger.md#send-sol-from-a-nano), substituting `ledger` with -`trezor` and your key path. - -## Troubleshooting - -### Keypair URL parameters are ignored in zsh - -The question mark character is a special character in zsh. If that's not a -feature you use, add the following line to your `~/.zshrc` to treat it as a -normal character: - -```bash -unsetopt nomatch -``` - -Then either restart your shell window or run `~/.zshrc`: - -```bash -source ~/.zshrc -``` - -If you would prefer not to disable zsh's special handling of the question mark -character, you can disable it explicitly with a backslash in your keypair URLs. -For example: - -```bash -solana-keygen pubkey usb://trezor\?key=0/0 -``` - -## Support - -You can find additional support and get help on the -[Solana StackExchange](https://solana.stackexchange.com). - -Read more about [sending and receiving tokens](../../examples/transfer-tokens.md) and -[delegating stake](../../examples/delegate-stake.md). You can use your Ledger keypair -URL anywhere you see an option or argument that accepts a ``. diff --git a/docs/src/cli/wallets/index.md b/docs/src/cli/wallets/index.md deleted file mode 100644 index 33fc0f28488..00000000000 --- a/docs/src/cli/wallets/index.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -title: Solana Wallets with the CLI -pagination_label: Command Line Wallets -sidebar_label: Overview -sidebar_position: 0 ---- - -Solana supports several different types of wallets that can be used to interface -directly with the Solana command-line tools. - -To use a Command Line Wallet, you must first [install the Solana CLI tools](../install.md) - -## File System Wallet - -A _file system wallet_, aka an FS wallet, is a directory in your computer's -file system. Each file in the directory holds a keypair. - -### File System Wallet Security - -A file system wallet is the most convenient and least secure form of wallet. It -is convenient because the keypair is stored in a simple file. You can generate as -many keys as you would like and trivially back them up by copying the files. It -is insecure because the keypair files are **unencrypted**. If you are the only -user of your computer and you are confident it is free of malware, an FS wallet -is a fine solution for small amounts of cryptocurrency. If, however, your -computer contains malware and is connected to the Internet, that malware may -upload your keys and use them to take your tokens. Likewise, because the -keypairs are stored on your computer as files, a skilled hacker with physical -access to your computer may be able to access it. Using an encrypted hard -drive, such as FileVault on MacOS, minimizes that risk. - -See [File System Wallets](./file-system.md) for more details. - -## Paper Wallet - -A _paper wallet_ is a collection of _seed phrases_ written on paper. A seed -phrase is some number of words (typically 12 or 24) that can be used to -regenerate a keypair on demand. - -### Paper Wallet Security - -In terms of convenience versus security, a paper wallet sits at the opposite -side of the spectrum from an FS wallet. It is terribly inconvenient to use, but -offers excellent security. That high security is further amplified when paper -wallets are used in conjunction with [offline signing](../examples/offline-signing.md). - -See [Paper Wallets](./paper.md) for more details - -## Hardware Wallet - -A hardware wallet is a small handheld device that stores keypairs and provides -some interface for signing transactions. - -### Hardware Wallet Security - -A hardware wallet, such as the -[Ledger hardware wallet](https://www.ledger.com/) or -[Trezor hardware wallet](https://trezor.io/), offers a great blend of -security and convenience for cryptocurrencies. It effectively automates the -process of offline signing while retaining nearly all the convenience of a file -system wallet. - -See [Hardware Wallets](./hardware/index.md) for more details diff --git a/docs/src/cli/wallets/paper.md b/docs/src/cli/wallets/paper.md deleted file mode 100644 index b90b1b9bfd3..00000000000 --- a/docs/src/cli/wallets/paper.md +++ /dev/null @@ -1,218 +0,0 @@ ---- -title: Paper Wallets using the Solana CLI -pagination_label: Paper Wallets using the CLI -sidebar_label: Paper Wallets -sidebar_position: 1 ---- - -This document describes how to create and use a paper wallet with the Solana CLI -tools. - -> We do not intend to advise on how to _securely_ create or manage paper -> wallets. Please research the security concerns carefully. - -## Overview - -Solana provides a key generation tool to derive keys from -[BIP39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki)-compliant -seed phrases. Solana CLI commands for running a validator and staking tokens all -support keypair input via seed phrases. - -## Paper Wallet Usage - -Solana commands can be run without ever saving a keypair to disk on a machine. -If avoiding writing a private key to disk is a security concern of yours, you've -come to the right place. - -> Even using this secure input method, it's still possible that a private key -> gets written to disk by unencrypted memory swaps. It is the user's -> responsibility to protect against this scenario. - -## Before You Begin - -- [Install the Solana command-line tools](../install.md) - -### Check your installation - -Check that `solana-keygen` is installed correctly by running: - -```bash -solana-keygen --version -``` - -## Creating a Paper Wallet - -Using the `solana-keygen` tool, it is possible to generate new seed phrases as -well as derive a keypair from an existing seed phrase and (optional) passphrase. -The seed phrase and passphrase can be used together as a paper wallet. As long -as you keep your seed phrase and passphrase stored safely, you can use them to -access your account. - -> For more information about how seed phrases work, review this -> [Bitcoin Wiki page](https://en.bitcoin.it/wiki/Seed_phrase). - -### Seed Phrase Generation - -Generating a new keypair can be done using the `solana-keygen new` command. The -command will generate a random seed phrase, ask you to enter an optional -passphrase, and then will display the derived public key and the generated seed -phrase for your paper wallet. - -After copying down your seed phrase, you can use the -[public key derivation](#public-key-derivation) instructions to verify that you -have not made any errors. - -```bash -solana-keygen new --no-outfile -``` - -> If the `--no-outfile` flag is **omitted**, the default behavior is to write -> the keypair to `~/.config/solana/id.json`, resulting in a -> [file system wallet](./file-system.md). - -The output of this command will display a line like this: - -```bash -pubkey: 9ZNTfG4NyQgxy2SWjSiQoUyBPEvXT2xo7fKc5hPYYJ7b -``` - -The value shown after `pubkey:` is your _wallet address_. - -**Note:** In working with paper wallets and file system wallets, the terms -"pubkey" and "wallet address" are sometimes used interchangeably. - -> For added security, increase the seed phrase word count using the -> `--word-count` argument - -For full usage details, run: - -```bash -solana-keygen new --help -``` - -### Public Key Derivation - -Public keys can be derived from a seed phrase and a passphrase if you choose to -use one. This is useful for using an offline-generated seed phrase to derive a -valid public key. The `solana-keygen pubkey` command will walk you through how -to use your seed phrase (and a passphrase if you chose to use one) as a signer -with the solana command-line tools using the `prompt` URI scheme. - -```bash -solana-keygen pubkey prompt:// -``` - -> Note that you could potentially use different passphrases for the same seed -> phrase. Each unique passphrase will yield a different keypair. - -The `solana-keygen` tool uses the same BIP39 standard English word list as it -does to generate seed phrases. If your seed phrase was generated with another -tool that uses a different word list, you can still use `solana-keygen`, but -will need to pass the `--skip-seed-phrase-validation` argument and forego this -validation. - -```bash -solana-keygen pubkey prompt:// --skip-seed-phrase-validation -``` - -After entering your seed phrase with `solana-keygen pubkey prompt://` the -console will display a string of base-58 characters. This is the -[derived](#hierarchical-derivation) solana BIP44 _wallet address_ associated -with your seed phrase. - -> Copy the derived address to a USB stick for easy usage on networked computers - -If needed, you can access the legacy, raw keypair's pubkey by instead passing -the `ASK` keyword: - -```bash -solana-keygen pubkey ASK -``` - -> A common next step is to [check the balance](#checking-account-balance) of the -> account associated with a public key - -For full usage details, run: - -```bash -solana-keygen pubkey --help -``` - -### Hierarchical Derivation - -The solana-cli supports -[BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) and -[BIP44](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki) -hierarchical derivation of private keys from your seed phrase and passphrase by -adding either the `?key=` query string or the `?full-path=` query string. - -By default, `prompt:` will derive solana's base derivation path `m/44'/501'`. To -derive a child key, supply the `?key=/` query string. - -```bash -solana-keygen pubkey 'prompt://?key=0/1' -``` - -To use a derivation path other than solana's standard BIP44, you can supply -`?full-path=m////`. - -```bash -solana-keygen pubkey 'prompt://?full-path=m/44/2017/0/1' -``` - -Because Solana uses Ed25519 keypairs, as per -[SLIP-0010](https://github.com/satoshilabs/slips/blob/master/slip-0010.md) all -derivation-path indexes will be promoted to hardened indexes -- eg. -`?key=0'/0'`, `?full-path=m/44'/2017'/0'/1'` -- regardless of whether ticks are -included in the query-string input. - -## Verifying the Keypair - -To verify you control the private key of a paper wallet address, use -`solana-keygen verify`: - -```bash -solana-keygen verify prompt:// -``` - -where `` is replaced with the wallet address and the keyword `prompt://` -tells the command to prompt you for the keypair's seed phrase; `key` and -`full-path` query-strings accepted. Note that for security reasons, your seed -phrase will not be displayed as you type. After entering your seed phrase, the -command will output "Success" if the given public key matches the keypair -generated from your seed phrase, and "Failed" otherwise. - -## Checking Account Balance - -All that is needed to check an account balance is the public key of an account. -To retrieve public keys securely from a paper wallet, follow the -[Public Key Derivation](#public-key-derivation) instructions on an -[air gapped computer](). -Public keys can then be typed manually or transferred via a USB stick to a -networked machine. - -Next, configure the `solana` CLI tool to -[connect to a particular cluster](../examples/choose-a-cluster.md): - -```bash -solana config set --url # (i.e. https://api.mainnet-beta.solana.com) -``` - -Finally, to check the balance, run the following command: - -```bash -solana balance -``` - -## Creating Multiple Paper Wallet Addresses - -You can create as many wallet addresses as you like. Simply re-run the steps in -[Seed Phrase Generation](#seed-phrase-generation) or -[Public Key Derivation](#public-key-derivation) to create a new address. -Multiple wallet addresses can be useful if you want to transfer tokens between -your own accounts for different purposes. - -## Support - -You can find additional support and get help on the -[Solana StackExchange](https://solana.stackexchange.com). diff --git a/docs/src/clusters/available.md b/docs/src/clusters/available.md deleted file mode 100644 index 498c77436ea..00000000000 --- a/docs/src/clusters/available.md +++ /dev/null @@ -1,176 +0,0 @@ ---- -title: Available Solana Clusters -sidebar_label: Solana Clusters -pagination_label: Available Solana Clusters ---- - -Solana maintains several different clusters with different purposes. - -Before you begin, make sure you have first built the Solana command line tools. -See [Installing the Solana Command Line Tools](../cli/install.md#build-from-source). - -Explorers: - -- [http://explorer.solana.com/](https://explorer.solana.com/). -- [http://solanabeach.io/](http://solanabeach.io/). - -## Devnet - -- Devnet serves as a playground for anyone who wants to take Solana for a - test drive, as a user, token holder, app developer or validator. -- Application developers should target Devnet. -- Potential validators should first target Devnet. -- Key differences between Devnet and Mainnet Beta: - - Devnet tokens are **not real** - - Devnet includes a token faucet for airdrops for application testing - - Devnet may be subject to ledger resets - - Devnet typically runs the same software release branch version as Mainnet Beta, - but may run a newer minor release version than Mainnet Beta. -- Gossip entrypoint for Devnet: `entrypoint.devnet.solana.com:8001` -- Metrics environment variable for Devnet: - -```bash -export SOLANA_METRICS_CONFIG="host=https://metrics.solana.com:8086,db=devnet,u=scratch_writer,p=topsecret" -``` - -- RPC URL for Devnet: `https://api.devnet.solana.com` - -##### Example `solana` command-line configuration - -```bash -solana config set --url https://api.devnet.solana.com -``` - -##### Example `agave-validator` command-line - -```bash -$ agave-validator \ - --identity validator-keypair.json \ - --vote-account vote-account-keypair.json \ - --known-validator dv1ZAGvdsz5hHLwWXsVnM94hWf1pjbKVau1QVkaMJ92 \ - --known-validator dv2eQHeP4RFrJZ6UeiZWoc3XTtmtZCUKxxCApCDcRNV \ - --known-validator dv4ACNkpYPcE3aKmYDqZm9G5EB3J4MRoeE7WNDRBVJB \ - --known-validator dv3qDFk1DTF36Z62bNvrCXe9sKATA6xvVy6A798xxAS \ - --only-known-rpc \ - --ledger ledger \ - --rpc-port 8899 \ - --dynamic-port-range 8000-8020 \ - --entrypoint entrypoint.devnet.solana.com:8001 \ - --entrypoint entrypoint2.devnet.solana.com:8001 \ - --entrypoint entrypoint3.devnet.solana.com:8001 \ - --entrypoint entrypoint4.devnet.solana.com:8001 \ - --entrypoint entrypoint5.devnet.solana.com:8001 \ - --expected-genesis-hash EtWTRABZaYq6iMfeYKouRu166VU2xqa1wcaWoxPkrZBG \ - --wal-recovery-mode skip_any_corrupted_record \ - --limit-ledger-size -``` - -The [`--known-validator`s](../operations/guides/validator-start.md#known-validators) -are operated by Anza - -## Testnet - -- Testnet is where the Solana core contributors stress test recent release features on a live - cluster, particularly focused on network performance, stability and validator - behavior. -- Testnet tokens are **not real** -- Testnet may be subject to ledger resets. -- Testnet includes a token faucet for airdrops for application testing -- Testnet typically runs a newer software release branch than both - Devnet and Mainnet Beta -- Gossip entrypoint for Testnet: `entrypoint.testnet.solana.com:8001` -- Metrics environment variable for Testnet: - -```bash -export SOLANA_METRICS_CONFIG="host=https://metrics.solana.com:8086,db=tds,u=testnet_write,p=c4fa841aa918bf8274e3e2a44d77568d9861b3ea" -``` - -- RPC URL for Testnet: `https://api.testnet.solana.com` - -##### Example `solana` command-line configuration - -```bash -solana config set --url https://api.testnet.solana.com -``` - -##### Example `agave-validator` command-line - -```bash -$ agave-validator \ - --identity validator-keypair.json \ - --vote-account vote-account-keypair.json \ - --known-validator 5D1fNXzvv5NjV1ysLjirC4WY92RNsVH18vjmcszZd8on \ - --known-validator dDzy5SR3AXdYWVqbDEkVFdvSPCtS9ihF5kJkHCtXoFs \ - --known-validator Ft5fbkqNa76vnsjYNwjDZUXoTWpP7VYm3mtsaQckQADN \ - --known-validator eoKpUABi59aT4rR9HGS3LcMecfut9x7zJyodWWP43YQ \ - --known-validator 9QxCLckBiJc783jnMvXZubK4wH86Eqqvashtrwvcsgkv \ - --only-known-rpc \ - --ledger ledger \ - --rpc-port 8899 \ - --dynamic-port-range 8000-8020 \ - --entrypoint entrypoint.testnet.solana.com:8001 \ - --entrypoint entrypoint2.testnet.solana.com:8001 \ - --entrypoint entrypoint3.testnet.solana.com:8001 \ - --expected-genesis-hash 4uhcVJyU9pJkvQyS88uRDiswHXSCkY3zQawwpjk2NsNY \ - --wal-recovery-mode skip_any_corrupted_record \ - --limit-ledger-size -``` - -The identities of the -[`--known-validator`s](../operations/guides/validator-start.md#known-validators) are: - -- `5D1fNXzvv5NjV1ysLjirC4WY92RNsVH18vjmcszZd8on` - Anza -- `dDzy5SR3AXdYWVqbDEkVFdvSPCtS9ihF5kJkHCtXoFs` - MonkeDAO -- `Ft5fbkqNa76vnsjYNwjDZUXoTWpP7VYm3mtsaQckQADN` - Certus One -- `eoKpUABi59aT4rR9HGS3LcMecfut9x7zJyodWWP43YQ` - SerGo -- `9QxCLckBiJc783jnMvXZubK4wH86Eqqvashtrwvcsgkv` - Algo|Stake - -## Mainnet Beta - -A permissionless, persistent cluster for Solana users, builders, validators and token holders. - -- Tokens that are issued on Mainnet Beta are **real** SOL -- Gossip entrypoint for Mainnet Beta: `entrypoint.mainnet-beta.solana.com:8001` -- Metrics environment variable for Mainnet Beta: - -```bash -export SOLANA_METRICS_CONFIG="host=https://metrics.solana.com:8086,db=mainnet-beta,u=mainnet-beta_write,p=password" -``` - -- RPC URL for Mainnet Beta: `https://api.mainnet-beta.solana.com` - -##### Example `solana` command-line configuration - -```bash -solana config set --url https://api.mainnet-beta.solana.com -``` - -##### Example `agave-validator` command-line - -```bash -$ agave-validator \ - --identity ~/validator-keypair.json \ - --vote-account ~/vote-account-keypair.json \ - --known-validator 7Np41oeYqPefeNQEHSv1UDhYrehxin3NStELsSKCT4K2 \ - --known-validator GdnSyH3YtwcxFvQrVVJMm1JhTS4QVX7MFsX56uJLUfiZ \ - --known-validator DE1bawNcRJB9rVm3buyMVfr8mBEoyyu73NBovf2oXJsJ \ - --known-validator CakcnaRDHka2gXyfbEd2d3xsvkJkqsLw2akB3zsN1D2S \ - --only-known-rpc \ - --ledger ledger \ - --rpc-port 8899 \ - --private-rpc \ - --dynamic-port-range 8000-8020 \ - --entrypoint entrypoint.mainnet-beta.solana.com:8001 \ - --entrypoint entrypoint2.mainnet-beta.solana.com:8001 \ - --entrypoint entrypoint3.mainnet-beta.solana.com:8001 \ - --entrypoint entrypoint4.mainnet-beta.solana.com:8001 \ - --entrypoint entrypoint5.mainnet-beta.solana.com:8001 \ - --expected-genesis-hash 5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d \ - --wal-recovery-mode skip_any_corrupted_record \ - --limit-ledger-size -``` - -:::info -The above four [`--known-validator`s](../operations/guides/validator-start.md#known-validators) -are operated by Anza. -::: \ No newline at end of file diff --git a/docs/src/clusters/benchmark.md b/docs/src/clusters/benchmark.md deleted file mode 100644 index 722fd2bde8d..00000000000 --- a/docs/src/clusters/benchmark.md +++ /dev/null @@ -1,136 +0,0 @@ ---- -title: Benchmark a Cluster ---- - -The Solana git repository contains all the scripts you might need to spin up your own local testnet. Depending on what you're looking to achieve, you may want to run a different variation, as the full-fledged, performance-enhanced multinode testnet is considerably more complex to set up than a Rust-only, singlenode testnode. If you are looking to develop high-level features, such as experimenting with smart contracts, save yourself some setup headaches and stick to the Rust-only singlenode demo. If you're doing performance optimization of the transaction pipeline, consider the enhanced singlenode demo. If you're doing consensus work, you'll need at least a Rust-only multinode demo. If you want to reproduce our TPS metrics, run the enhanced multinode demo. - -For all four variations, you'd need the latest Rust toolchain and the Solana source code: - -First, setup Rust, Cargo and system packages as described in the Solana [README](https://github.com/solana-labs/solana#1-install-rustc-cargo-and-rustfmt) - -Now checkout the code from github: - -```bash -git clone https://github.com/solana-labs/solana.git -cd solana -``` - -The demo code is sometimes broken between releases as we add new low-level features, so if this is your first time running the demo, you'll improve your odds of success if you check out the [latest release](https://github.com/solana-labs/solana/releases) before proceeding: - -```bash -TAG=$(git describe --tags $(git rev-list --tags --max-count=1)) -git checkout $TAG -``` - -### Configuration Setup - -Ensure important programs such as the vote program are built before any nodes are started. Note that we are using the release build here for good performance. - -```bash -cargo build --release -``` - -Then set the following environment variable to enforce release builds across all commands we are going to use later: - -```bash -export CARGO_BUILD_PROFILE=release -``` - -If you want to profile the validator(s), you can use the `release-with-debug` build profile, which leverages the most of optimizations used in the `release` profile, while keeping the debug symbols. - -```bash -cargo build --profile release-with-debug -export CARGO_BUILD_PROFILE=release-with-debug -``` - -If you want the debug build without optimizations, use just `cargo build` and do not set any environment variables mentioned above. - -The network is initialized with a genesis ledger generated by running the following script. - -```bash -./multinode-demo/setup.sh -``` - -### Faucet - -In order for the validators and clients to work, we'll need to spin up a faucet to give out some test tokens. The faucet delivers Milton Friedman-style "air drops" \(free tokens to requesting clients\) to be used in test transactions. - -Start the faucet with: - -```bash -./multinode-demo/faucet.sh -``` - -### Singlenode Testnet - -Before you start a validator, make sure you know the IP address of the machine you want to be the bootstrap validator for the demo, and make sure that udp ports 8000-10000 are open on all the machines you want to test with. - -Now start the bootstrap validator in a separate shell: - -```bash -./multinode-demo/bootstrap-validator.sh -``` - -Wait a few seconds for the server to initialize. It will print "leader ready..." when it's ready to receive transactions. The leader will request some tokens from the faucet if it doesn't have any. The faucet does not need to be running for subsequent leader starts. - -### Multinode Testnet - -To run a multinode testnet, after starting a leader node, spin up some additional validators in separate shells: - -```bash -./multinode-demo/validator-x.sh -``` - - -### Testnet Client Demo - -Now that your singlenode or multinode testnet is up and running let's send it some transactions! - -In a separate shell start the client: - -```bash -./multinode-demo/txs-bench.sh # runs against localhost by default -``` - -What just happened? The client demo starts `solana-transaction-bench`, creates funded payer accounts from the configured authority, and sends transfer transactions to the testnet. You should see TPS measurements printed to the screen. In the multinode variation, you'll see TPS measurements for each validator node as well. - -### Testnet Debugging - -There are some useful debug messages in the code, you can enable them on a per-module and per-level basis. Before running a leader or validator set the normal RUST_LOG environment variable. - -For example - -- To enable `info` everywhere and `debug` only in the solana::banking_stage module: - - ```bash - export RUST_LOG=solana=info,solana::banking_stage=debug - ``` - -- To enable SBF program logging: - - ```bash - export RUST_LOG=solana_bpf_loader=trace - ``` - -Generally we are using `debug` for infrequent debug messages, `trace` for potentially frequent messages and `info` for performance-related logging. - -You can also attach to a running process with GDB. The leader's process is named _agave-validator_: - -```bash -sudo gdb -attach -set logging on -thread apply all bt -``` - -This will dump all the threads stack traces into gdb.txt - -## Developer Testnet - -In this example the client connects to our public testnet. To run validators on the testnet you would need to open udp ports `8000-10000`. - -```bash -./multinode-demo/txs-bench.sh --url https://api.devnet.solana.com --authority /path/to/funded-keypair.json --duration 60 --target-tps 50 ws-leader-tracker -``` - -You can observe the effects of your client's transactions on our [metrics dashboard](https://metrics.solana.com:3000/d/monitor/cluster-telemetry?var-testnet=devnet) diff --git a/docs/src/clusters/index.md b/docs/src/clusters/index.md deleted file mode 100644 index e2d25c603b4..00000000000 --- a/docs/src/clusters/index.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -title: Overview of a Solana Cluster -sidebar_position: 0 -sidebar_label: Overview -pagination_label: Overview of a Solana Cluster ---- - -A Solana cluster is a set of validators working together to serve client transactions and maintain the integrity of the ledger. Many clusters may coexist. When two clusters share a common genesis block, they attempt to converge. Otherwise, they simply ignore the existence of the other. Transactions sent to the wrong one are quietly rejected. In this section, we'll discuss how a cluster is created, how nodes join the cluster, how they share the ledger, how they ensure the ledger is replicated, and how they cope with buggy and malicious nodes. - -## Creating a Cluster - -Before starting any validators, one first needs to create a _genesis config_. The config references two public keys, a _mint_ and a _bootstrap validator_. The validator holding the bootstrap validator's private key is responsible for appending the first entries to the ledger. It initializes its internal state with the mint's account. That account will hold the number of native tokens defined by the genesis config. The second validator then contacts the bootstrap validator to register as a _validator_. Additional validators then register with any registered member of the cluster. - -A validator receives all entries from the leader and submits votes confirming those entries are valid. After voting, the validator is expected to store those entries. Once the validator observes a sufficient number of copies exist, it deletes its copy. - -## Joining a Cluster - -Validators enter the cluster via registration messages sent to its _control plane_. The control plane is implemented using a _gossip_ protocol, meaning that a node may register with any existing node, and expect its registration to propagate to all nodes in the cluster. The time it takes for all nodes to synchronize is proportional to the square of the number of nodes participating in the cluster. Algorithmically, that's considered very slow, but in exchange for that time, a node is assured that it eventually has all the same information as every other node, and that information cannot be censored by any one node. - -## Sending Transactions to a Cluster - -Clients send transactions to any validator's Transaction Processing Unit \(TPU\) port. If the node is in the validator role, it forwards the transaction to the designated leader. If in the leader role, the node bundles incoming transactions, timestamps them creating an _entry_, and pushes them onto the cluster's _data plane_. Once on the data plane, the transactions are validated by validator nodes, effectively appending them to the ledger. - -## Confirming Transactions - -A Solana cluster is capable of subsecond _confirmation_ for thousands of nodes with plans to scale up to hundreds of thousands of nodes. Confirmation times are expected to increase only with the logarithm of the number of validators, where the logarithm's base is very high. If the base is one thousand, for example, it means that for the first thousand nodes, confirmation will be the duration of three network hops plus the time it takes the slowest validator of a supermajority to vote. For the next million nodes, confirmation increases by only one network hop. - -Solana defines confirmation as the duration of time from when the leader timestamps a new entry to the moment when it recognizes a supermajority of ledger votes. - -Scalable confirmation can be achieved using the following combination of techniques: - -1. Timestamp transactions with a VDF sample and sign the timestamp. - -2. Split the transactions into batches, send each to separate nodes and have each node share its batch with its peers. - -3. Repeat the previous step recursively until all nodes have all batches. - -Solana rotates leaders at fixed intervals, called _slots_. Each leader may only produce entries during its allotted slot. The leader therefore timestamps transactions so that validators may lookup the public key of the designated leader. The leader then signs the timestamp so that a validator may verify the signature, proving the signer is owner of the designated leader's public key. - -Next, transactions are broken into batches so that a node can send transactions to multiple parties without making multiple copies. If, for example, the leader needed to send 60 transactions to 6 nodes, it would break that collection of 60 into batches of 10 transactions and send one to each node. This allows the leader to put 60 transactions on the wire, not 60 transactions for each node. Each node then shares its batch with its peers. Once the node has collected all 6 batches, it reconstructs the original set of 60 transactions. - -A batch of transactions can only be split so many times before it is so small that header information becomes the primary consumer of network bandwidth. At the time of this writing (December, 2021), the approach is scaling well up to about 1,250 validators. To scale up to hundreds of thousands of validators, each node can apply the same technique as the leader node to another set of nodes of equal size. We call the technique [_Turbine Block Propagation_](../consensus/turbine-block-propagation.md). diff --git a/docs/src/clusters/metrics.md b/docs/src/clusters/metrics.md deleted file mode 100644 index 7a442375257..00000000000 --- a/docs/src/clusters/metrics.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -title: Solana Cluster Performance Metrics -sidebar_label: Performance Metrics -pagination_label: Cluster Performance Metrics ---- - -Solana cluster performance is measured as average number of transactions per second that the network can sustain \(TPS\). And, how long it takes for a transaction to be confirmed by super majority of the cluster \(Confirmation Time\). - -Each cluster node maintains various counters that are incremented on certain events. These counters are periodically uploaded to a cloud based database. Solana's metrics dashboard fetches these counters, and computes the performance metrics and displays it on the dashboard. - -## TPS - -Each node's bank runtime maintains a count of transactions that it has processed. The dashboard first calculates the median count of transactions across all metrics enabled nodes in the cluster. The median cluster transaction count is then averaged over a 2-second period and displayed in the TPS time series graph. The dashboard also shows the Mean TPS, Max TPS and Total Transaction Count stats which are all calculated from the median transaction count. - -## Confirmation Time - -Each validator node maintains a list of active ledger forks that are visible to the node. A fork is considered to be frozen when the node has received and processed all entries corresponding to the fork. A fork is considered to be confirmed when it receives cumulative super majority vote, and when one of its children forks is frozen. - -The node assigns a timestamp to every new fork, and computes the time it took to confirm the fork. This time is reflected as validator confirmation time in performance metrics. The performance dashboard displays the average of each validator node's confirmation time as a time series graph. - -## Hardware setup - -The validator software is deployed to GCP n1-standard-16 instances with 1TB pd-ssd disk, and 2x Nvidia V100 GPUs. These are deployed in the us-west-1 region. - -Load generation is started after the network converges from a client machine with n1-standard-16 CPU-only instance. - -TPS and confirmation metrics are captured from the dashboard numbers over a 5-minute average while the transaction workload is running. diff --git a/docs/src/clusters/testnet.md b/docs/src/clusters/testnet.md deleted file mode 100644 index aa315c28036..00000000000 --- a/docs/src/clusters/testnet.md +++ /dev/null @@ -1,202 +0,0 @@ -# Test Network Management -The `./net/` directory in the monorepo contains scripts useful for creation and manipulation of a test network. -The test network allows you to run a fully isolated set of validators and clients on a configurable hardware setup. -It's intended to be both dev and CD friendly. - - -### Cloud account prerequisites - -The test networks to be created can run in GCP, AWS or colo. Whichever cloud provider you choose, you will need the credentials set up on your machine. - -#### GCP -You will need a working `gcloud` command from google SDK, -if you do not have it follow the guide in [https://cloud.google.com/sdk?hl=en](https://cloud.google.com/sdk?hl=en) - -Before running any scripts, authenticate with -```bash -$ gcloud auth login -``` -If you are running the scripts on a headless machine, you can use curl to issue requests to confirm your auth. - -If you are doing it the first time, you might need to set up project -```bash -gcloud config set project principal-lane-200702 -``` - -#### AWS -Obtain your credentials from the AWS IAM Console and configure the AWS CLI with -```bash -$ aws configure -``` -More information on AWS CLI configuration can be found [here](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html#cli-quick-configuration) - -## Metrics configuration (Optional) -Metrics collection relies on 2 environment variables that are patched to the remote nodes by net.sh: - * `RUST_LOG` to enable metrics reporting in principle - * `SOLANA_METRICS_CONFIG` to tell agave where to log the metrics - -### Preparation -> [!NOTE] -> Anza employees should follow the guide in notion to set up the influxDB account. - - * Ensure that `${host}` is the host name of the InfluxDB you can access, for example `https://internal-metrics.solana.com:8086` - * Ensure that `${user}` is the name of an InfluxDB user account with enough -rights to create a new InfluxDB database, for example `solana`. - -### To set up the metrics -You will normally only need to do this once. Once this is done, you will be able to save the metrics configuration and load it later from the environment. - -* Go to ./net/ in agave repo -* Run `./init-metrics.sh -c testnet-dev-${user} ${user} ` - * Script will ask for a password, it is the same one you’ve created when making a user in the InfluxDB UI - * Put the username you have used in preparation, not your login user name - * If you need to set influxDb host, edit the script -* The script will configure the database (recreating one if necessary) and append a config line in the very end of `net/config/config` file like the following: - * `export SOLANA_METRICS_CONFIG="host=${host},db=testnet-dev-${user},u=${user},p=some_secret"` - * You can store that line somewhere and append it to the config file when you need to reuse the database. - * You can also store it into your shell’s environment so you can run `./init-metrics.sh -e` to quickly load it - * Alternatively, you'll need to run `./init-metrics.sh` with appropriate arguments every time you set up a new cluster -* Assuming no errors, your influxDB setup is now done. -* For simple cases, storing `SOLANA_METRICS_CONFIG` in your env is appropriate, but you may want to use different databases for different runs of net.sh - * You can call ./init-metrics.sh before you call net.sh start, this will change the metrics config for a particular run. - * You can manually write `SOLANA_METRICS_CONFIG` in the `./net/config/config` file -* By default, metrics are only logged by agave if `RUST_LOG` is set to `info` or higher. You can provide it as environment for `./net.sh start` command, or set this in your shell environment. - ```bash - RUST_LOG="solana_metrics=info" - ``` - -### To validate that your database and metrics environment variables are set up 100% correctly - -Note: this only works if you store `SOLANA_METRICS_CONFIG` in your shell environment - -```bash - cd ./scripts/ - source ./configure-metrics.sh - INFLUX_HOST=https://internal-metrics.solana.com:8086 - INFLUX_DATABASE=testnet-dev-solana - INFLUX_USERNAME=solana - INFLUX_PASSWORD=******** - ./metrics-write-datapoint.sh "testnet-deploy net-create-begin=1" - - ``` - * All commands should complete with no errors, this indicates your influxDB config is usable - * Ensure that `RUST_LOG` is set to `info` or `debug` - -## Quick Start - -NOTE: This example uses GCE. If you are using AWS EC2, replace `./gce.sh` with -`./ec2.sh` in the commands. - -```bash -# In Agave repo -cd net/ - -# Create a GCE testnet with 4 additional validator nodes (beyond the bootstrap node) and 1 client (billing starts here) -./gce.sh create -n 4 -c 1 - -# Configure the metrics database and validate credentials using environment variable `SOLANA_METRICS_CONFIG` (skip this if you are not using metrics) -./init-metrics.sh -c testnet-dev-${USER} ${USER} - -# Deploy the network from the local workspace and start processes on all nodes including transaction-bench on the client node -RUST_LOG=info ./net.sh start - -# Show a help to ssh into any testnet node to access logs/etc -./ssh.sh - -# Stop running processes on all nodes -./net.sh stop - -# Dispose of the network (billing stops here) -./gce.sh delete -``` - -## Full guide -* If you expect metrics to work, make sure you have configured them before proceeding -* Go to `./net/` directory in agave repo -* `./gce.sh` command controls creation and destruction of the nodes in the test net. It does not actually run any software. - * `./gce.sh create \-n 4 \-c 2` creates cluster with 4 validators and 1 node for load generation, this is minimal viable setup for all solana features to work - * If the creation succeeds, `net/config/config` will contain the config file of the testnet just created - * If you do not have `SOLANA_METRICS_CONFIG` set in your shell env, `gce.sh` may complain about metrics not being configured, this is perfectly fine - * `./gce.sh info` lists active test cluster nodes, this allows you to get their IP addresses for SSH access and/or debugging - * `./gce.sh delete` destroys the nodes (save the electricity and $$$ - destroy your test nets the moment you no longer need them). - * On GCE, if you do not delete nodes, they will self-destruct in 8 hours anyway, you can configure self-destruct timer by supplying `--self-destruct-hours=N` argument to `gce.sh` - * On other cloud platforms the testnet will not self-destruct! -* To enable metrics in the testnet, at this point you need to either: - * `./init-metrics.sh -c testnet-dev-${user} ${user}` to create a new metrics database from scratch - * Manually set `SOLANA_METRICS_CONFIG` in `./net/config/config` (which is exactly what `init-metrics.sh` does for you) - * `./init-metrics.sh -e` to load metrics config from `SOLANA_METRICS_CONFIG` into the testnet config file or -* `./net.sh` controls the payload on the testnet nodes, i.e. bootstrapping, the validators and transaction-bench clients. In principle, you can run everything by hand, but `./net.sh` makes it easier. - * `./net.sh start` to actually run the test network. - * This will actually upload your current sources to the bootstrap host, build them there and upload the result to all the nodes - * The script will take 5-10 of minutes to run, in the end it should print something like - ``` - --- Deployment Successful - Bootstrap validator deployment took 164 seconds - ``` - * You can also make sure it logs successful test transfers: - ```✅ 1 lamport(s) transferred: seq=0 time= 402ms signature=33uJtPJM6ekBGrWCgWHKw1TTQJVrLxYMe3sp2PUmSRVb21LyXn3nDbQmzsgQyihE7VP2zD2iR66Du8aDUnSSd6pb``` - * `./net.sh start -c transaction-bench=2="--target-tps 5000 ws-leader-tracker"` will start 2 clients with transaction-bench workloads targeting 5000 TPS. - * Arguments are passed to `solana-transaction-bench`; see its manual for more options. - * `./net.sh sanity` to test the deployment, it is also run by start command - * `./net.sh stop` to stop the validators and client. This does not kill the machines, so you can study the logs etc. - * `./net.sh start --nobuild` will skip the source compilation, you will generally want that if you are only changing configuration files rather than code, or just want to re-run the last test. -* To connect to the nodes: - * `./gce.sh info ` to get the public IPs - * `./ssh.sh ` to get a shell on the node - * `sudo su` will give you root access on the nodes - * Nodes run latest ubuntu LTS image -* You can also interact with the nodes using solana cli: -```bash -# source ip list use as ${validatorIpList[4]} -source net/config/config - -# airdrop -../target/release/solana -u http://${validatorIpList[1]}:8899 airdrop 1 - -# check feature -../target/release/solana -u http://${validatorIpList[1]}:8899 feature status - -# activate a feature -../target/release/solana -u http://${validatorIpList[1]}:8899 feature activate - -# check the stakes on current validators -../target/release/solana --url http://${validatorIpList[0]}:8899 validators -``` - -## Tips - -### Automation -You will want to have a script like this pretty much immediately to avoid making mistakes in the init process: -```bash -# Create the testnet with reasonable node sizes for a small test -# This particular one will have 7 nodes: 1 bootstrap validator, 4 regular validators, and 2 clients -./gce.sh create -n4 -c2 --custom-machine-type "--machine-type n1-standard-16" --client-machine-type "--machine-type n1-standard-4" -# Patch metrics config from env into config file -./init-metrics.sh -e -# Enable metrics and start the network (this will also build software) -RUST_LOG=info ./net.sh start -c transaction-bench=2="--target-tps 5000 ws-leader-tracker" -``` - -### Inscrutable "nothing works everything times out state" - Note that net.sh and `gce.sh info` commands do not actually check if all the nodes are still alive in gcloud, - they just assume the config file information is correct. So if your nodes got killed/timed out they will lie to you. In such case, just use `gce.sh delete` to reset. - -### Running the network over public IP addresses -By default private IP addresses are used with all instances in the same -availability zone to avoid GCE network egress charges. However to run the -network over public IP addresses: -```bash -$ ./gce.sh create -P ... -``` -or -```bash -$ ./ec2.sh create -P ... -``` - -### Deploying a tarball-based network -To deploy the latest pre-built `edge` channel tarball (ie, latest from the `master` -branch), once the testnet has been created run: - -```bash -$ ./net.sh start -t edge -``` diff --git a/docs/src/consensus/commitments.md b/docs/src/consensus/commitments.md deleted file mode 100644 index 404f41dc7f5..00000000000 --- a/docs/src/consensus/commitments.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: Solana Commitment Status -sidebar_label: Commitment Status -pagination_label: Consensus Commitment Status -description: - "Processed, confirmed, and finalized. Learn the differences between the - different commitment statuses on the Solana blockchain." -keywords: - - processed - - confirmed - - finalized - - stake level - - block - - blockhash ---- - -The [commitment](https://solana.com/docs/terminology#commitment) metric gives -clients a standard measure of the network confirmation for the block. Clients -can then use this information to derive their own measures of commitment. - -There are three specific commitment statuses: - -- Processed -- Confirmed -- Finalized - -| Property | Processed | Confirmed | Finalized | -| ------------------------------------- | --------- | --------- | --------- | -| Received block | X | X | X | -| Block on majority fork | X | X | X | -| Block contains target tx | X | X | X | -| 66%+ stake voted on block | - | X | X | -| 31+ confirmed blocks built atop block | - | - | X | diff --git a/docs/src/consensus/fork-generation.md b/docs/src/consensus/fork-generation.md deleted file mode 100644 index c62cacdfee2..00000000000 --- a/docs/src/consensus/fork-generation.md +++ /dev/null @@ -1,139 +0,0 @@ ---- -title: Fork Generation -description: - "A fork is created when validators do not agree on a newly produced block. - Using a consensus algorithm validators vote on which will be finalized." ---- - -The Solana protocol doesn’t wait for all validators to agree on a newly produced -block before the next block is produced. Because of that, it’s quite common for -two different blocks to be chained to the same parent block. In those -situations, we call each conflicting chain a ["fork."](./fork-generation.md) - -Solana validators need to vote on one of these forks and reach agreement on -which one to use through a consensus algorithm (that is beyond the scope of this -article). The main point you need to remember is that when there are competing -forks, only one fork will be finalized by the cluster and the abandoned blocks -in competing forks are all discarded. - -This section describes how forks naturally occur as a consequence of -[leader rotation](./leader-rotation.md). - -## Overview - -Nodes take turns being [leader](https://solana.com/docs/terminology#leader) and -generating the PoH that encodes state changes. The cluster can tolerate loss of -connection to any leader by synthesizing what the leader _**would**_ have -generated had it been connected but not ingesting any state changes. - -The possible number of forks is thereby limited to a "there/not-there" skip list -of forks that may arise on leader rotation slot boundaries. At any given slot, -only a single leader's transactions will be accepted. - -### Forking example - -The table below illustrates what competing forks could look like. Time -progresses from left to right and each slot is assigned to a validator that -temporarily becomes the cluster "leader" and may produce a block for that slot. - -In this example, the leader for slot 3 chose to chain its "Block 3" directly to -"Block 1" and in doing so skipped "Block 2". Similarly, the leader for slot 5 -chose to chain "Block 5" directly to "Block 3" and skipped "Block 4". - -> Note that across different forks, the block produced for a given slot is -> _always_ the same because producing two different blocks for the same slot is -> a slashable offense. So the conflicting forks above can be distinguished from -> each other by which slots they have _skipped_. - -| | Slot 1 | Slot 2 | Slot 3 | Slot 4 | Slot 5 | -| ------ | ------- | ------- | ------- | ------- | ------- | -| Fork 1 | Block 1 | | Block 3 | | Block 5 | -| Fork 2 | Block 1 | | Block 3 | Block 4 | | -| Fork 3 | Block 1 | Block 2 | | | | - -## Message Flow - -1. Transactions are ingested by the current leader. -2. Leader filters valid transactions. -3. Leader executes valid transactions updating its state. -4. Leader packages transactions into entries based off its current PoH slot. -5. Leader transmits the entries to validator nodes \(in signed shreds\) - 1. The PoH stream includes ticks; empty entries that indicate liveness of the - leader and the passage of time on the cluster. - 2. A leader's stream begins with the tick entries necessary to complete PoH - back to the leader's most recently observed prior leader slot. -6. Validators retransmit entries to peers in their set and to further downstream - nodes. -7. Validators validate the transactions and execute them on their state. -8. Validators compute the hash of the state. -9. At specific times, i.e. specific PoH tick counts, validators transmit votes - to the leader. - 1. Votes are signatures of the hash of the computed state at that PoH tick - count. - 2. Votes are also propagated via gossip. -10. Leader executes the votes, the same as any other transaction, and broadcasts - them to the cluster. -11. Validators observe their votes and all the votes from the cluster. - -## Partitions, Forks - -Forks can arise at PoH tick counts that correspond to a vote. The next leader -may not have observed the last vote slot and may start their slot with generated -virtual PoH entries. These empty ticks are generated by all nodes in the cluster -at a cluster-configured rate for hashes/per/tick `Z`. - -There are only two possible versions of the PoH during a voting slot: PoH with -`T` ticks and entries generated by the current leader, or PoH with just ticks. -The "just ticks" version of the PoH can be thought of as a virtual ledger, one -that all nodes in the cluster can derive from the last tick in the previous -slot. - -Validators can ignore forks at other points \(e.g. from the wrong leader\), or -slash the leader responsible for the fork. - -Validators vote based on a greedy choice to maximize their reward described in -[Tower BFT](../implemented-proposals/tower-bft.md). - -### Validator's View - -#### Time Progression - -The diagram below represents a validator's view of the PoH stream with possible -forks over time. L1, L2, etc. are leader slots, and `E`s represent entries from -that leader during that leader's slot. The `x`s represent ticks only, and time -flows downwards in the diagram. - -![Fork generation](/img/fork-generation.svg) - -Note that an `E` appearing on 2 forks at the same slot is a slashable condition, -so a validator observing `E3` and `E3'` can slash L3 and safely choose `x` for -that slot. Once a validator commits to a fork, other forks can be discarded -below that tick count. For any slot, validators need only consider a single "has -entries" chain or a "ticks only" chain to be proposed by a leader. But multiple -virtual entries may overlap as they link back to the previous slot. - -#### Time Division - -It's useful to consider leader rotation over PoH tick count as time division of -the job of encoding state for the cluster. The following table presents the -above tree of forks as a time-divided ledger. - -| leader slot | L1 | L2 | L3 | L4 | L5 | -| :--------------- | :-- | :-- | :-- | :-- | :-- | -| data | E1 | E2 | E3 | E4 | E5 | -| ticks since prev | | | | x | xx | - -Note that only data from leader L3 will be accepted during leader slot L3. Data -from L3 may include "catchup" ticks back to a slot other than L2 if L3 did not -observe L2's data. L4 and L5's transmissions include the "ticks to prev" PoH -entries. - -This arrangement of the network data streams permits nodes to save exactly this -to the ledger for replay, restart, and checkpoints. - -### Leader's View - -When a new leader begins a slot, it must first transmit any PoH \(ticks\) -required to link the new slot with the most recently observed and voted slot. -The fork the leader proposes would link the current slot to a previous fork that -the leader has voted on with virtual ticks. diff --git a/docs/src/consensus/leader-rotation.md b/docs/src/consensus/leader-rotation.md deleted file mode 100644 index 8e6088e397f..00000000000 --- a/docs/src/consensus/leader-rotation.md +++ /dev/null @@ -1,90 +0,0 @@ ---- -title: Solana Leader Rotation -sidebar_label: Leader Rotation -pagination_label: Leader Rotation ---- - -At any given moment, a cluster expects only one validator to produce ledger entries. By having only one leader at a time, all validators are able to replay identical copies of the ledger. The drawback of only one leader at a time, however, is that a malicious leader is capable of censoring votes and transactions. Since censoring cannot be distinguished from the network dropping packets, the cluster cannot simply elect a single node to hold the leader role indefinitely. Instead, the cluster minimizes the influence of a malicious leader by rotating which node takes the lead. - -Each validator selects the expected leader using the same algorithm, described below. When the validator receives a new signed ledger entry, it can be certain that an entry was produced by the expected leader. The order of slots which each leader is assigned a slot is called a _leader schedule_. - -## Leader Schedule Rotation - -A validator rejects blocks that are not signed by the _slot leader_. The list of identities of all slot leaders is called a _leader schedule_. The leader schedule is recomputed locally and periodically. It assigns slot leaders for a duration of time called an _epoch_. The schedule must be computed far in advance of the slots it assigns, such that the ledger state it uses to compute the schedule is finalized. That duration is called the _leader schedule offset_. Solana sets the offset to the duration of slots until the next epoch. That is, the leader schedule for an epoch is calculated from the ledger state at the start of the previous epoch. The offset of one epoch is fairly arbitrary and assumed to be sufficiently long such that all validators will have finalized their ledger state before the next schedule is generated. A cluster may choose to shorten the offset to reduce the time between stake changes and leader schedule updates. - -While operating without partitions lasting longer than an epoch, the schedule only needs to be generated when the root fork crosses the epoch boundary. Since the schedule is for the next epoch, any new stakes committed to the root fork will not be active until the next epoch. The block used for generating the leader schedule is the first block to cross the epoch boundary. - -Without a partition lasting longer than an epoch, the cluster will work as follows: - -1. A validator continuously updates its own root fork as it votes. -2. The validator updates its leader schedule each time the slot height crosses an epoch boundary. - -For example: - -Let's assume an epoch duration of 100 slots, which in reality is magnitudes higher. The root fork is updated from fork computed at slot height 99 to a fork computed at slot height 102. Forks with slots at height 100, 101 were skipped because of failures. The new leader schedule is computed using fork at slot height 102. It is active from slot 200 until it is updated again. - -No inconsistency can exist because every validator that is voting with the cluster has skipped 100 and 101 when its root passes 102. All validators, regardless of voting pattern, would be committing to a root that is either 102, or a descendant of 102. - -### Leader Schedule Rotation with Epoch Sized Partitions. - -The duration of the leader schedule offset has a direct relationship to the likelihood of a cluster having an inconsistent view of the correct leader schedule. - -Consider the following scenario: - -Two partitions that are generating half of the blocks each. Neither is coming to a definitive supermajority fork. Both will cross epoch 100 and 200 without actually committing to a root and therefore a cluster-wide commitment to a new leader schedule. - -In this unstable scenario, multiple valid leader schedules exist. - -- A leader schedule is generated for every fork whose direct parent is in the previous epoch. -- The leader schedule is valid after the start of the next epoch for descendant forks until it is updated. - -Each partition's schedule will diverge after the partition lasts more than an epoch. For this reason, the epoch duration should be selected to be much larger then slot time and the expected length for a fork to be committed to root. - -After observing the cluster for a sufficient amount of time, the leader schedule offset can be selected based on the median partition duration and its standard deviation. For example, an offset longer then the median partition duration plus six standard deviations would reduce the likelihood of an inconsistent ledger schedule in the cluster to 1 in 1 million. - -## Leader Schedule Generation at Genesis - -The genesis config declares the first leader for the first epoch. This leader ends up scheduled for the first two epochs because the leader schedule is also generated at slot 0 for the next epoch. The length of the first two epochs can be specified in the genesis config as well. The minimum length of the first epochs must be greater than or equal to the maximum rollback depth as defined in [Tower BFT](../implemented-proposals/tower-bft.md). - -## Leader Schedule Generation Algorithm - -Leader schedule is generated using a predefined seed. The process is as follows: - -1. Periodically use the PoH tick height \(a monotonically increasing counter\) to seed a stable pseudo-random algorithm. -2. At that height, sample the bank for all the staked accounts with leader identities that have voted within a cluster-configured number of ticks. The sample is called the _active set_. -3. Sort the active set by stake weight. -4. Use the random seed to select nodes weighted by stake to create a stake-weighted ordering. -5. This ordering becomes valid after a cluster-configured number of ticks. - -## Schedule Attack Vectors - -### Seed - -The seed that is selected is predictable but unbiasable. There is no grinding attack to influence its outcome. - -### Active Set - -A leader can bias the active set by censoring validator votes. Two possible ways exist for leaders to censor the active set: - -- Ignore votes from validators -- Refuse to vote for blocks with votes from validators - -To reduce the likelihood of censorship, the active set is calculated at the leader schedule offset boundary over an _active set sampling duration_. The active set sampling duration is long enough such that votes will have been collected by multiple leaders. - -### Staking - -Leaders can censor new staking transactions or refuse to validate blocks with new stakes. This attack is similar to censorship of validator votes. - -### Validator operational key loss - -Leaders and validators are expected to use ephemeral keys for operation, and stake owners authorize the validators to do work with their stake via delegation. - -The cluster should be able to recover from the loss of all the ephemeral keys used by leaders and validators, which could occur through a common software vulnerability shared by all the nodes. Stake owners should be able to vote directly by co-signing a validator vote even though the stake is currently delegated to a validator. - -## Appending Entries - -The lifetime of a leader schedule is called an _epoch_. The epoch is split into _slots_, where each slot has a duration of `T` PoH ticks. - -A leader transmits entries during its slot. After `T` ticks, all the validators switch to the next scheduled leader. Validators must ignore entries sent outside a leader's assigned slot. - -All `T` ticks must be observed by the next leader for it to build its own entries on. If entries are not observed \(leader is down\) or entries are invalid \(leader is buggy or malicious\), the next leader must produce ticks to fill the previous leader's slot. Note that the next leader should do repair requests in parallel, and postpone sending ticks until it is confident other validators also failed to observe the previous leader's entries. If a leader incorrectly builds on its own ticks, the leader following it must replace all its ticks. diff --git a/docs/src/consensus/managing-forks.md b/docs/src/consensus/managing-forks.md deleted file mode 100644 index 291d67d83f9..00000000000 --- a/docs/src/consensus/managing-forks.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -title: Managing Forks ---- - -The ledger is permitted to fork at slot boundaries. The resulting data structure forms a tree called a _blockstore_. When the validator interprets the blockstore, it must maintain state for each fork in the chain. It is the responsibility of a validator to weigh those forks, such that it may eventually select a fork. Details for selection and voting on these forks can be found in [Tower Bft](../implemented-proposals/tower-bft.md) - -## Forks - -A fork is as a sequence of slots originating from some root. For example: - -``` - 2 - 4 - 6 - 8 - / -0 - 1 12 - 13 - \ / - 3 - 5 - \ - 7 - 9 - 10 - 11 -``` - -The following sequences are forks: - -``` -- {0, 1, 2, 4, 6, 8} -- {0, 1, 3, 5, 12, 13} -- {0, 1, 3, 5, 7, 9, 10, 11} -``` - -## Pruning and Squashing - -As the chain grows, storing the local forks view becomes detrimental to performance. Fortunately we can take advantage of the properties of tower bft roots to prune this data structure. Recall a root is a slot that has reached the max lockout depth. The assumption is that this slot has accrued enough lockout that it would be impossible to roll this slot back. - -Thus, the validator prunes forks that do not originate from its local root, and then takes the opportunity to minimize its memory usage by squashing any nodes it can into the root. Although not necessary for consensus, to enable some RPC use cases the validator chooses to keep ancestors of its local root up until the last slot rooted by the super majority of the cluster. We call this the super majority root (SMR). - -Starting from the above example imagine a max lockout depth of 3. Our validator votes on slots `0, 1, 3, 5, 7, 9`. Upon the final vote at `9`, our local root is `3`. Assume the latest super majority root is `0`. After pruning this is our local fork view. - -``` -SMR - 0 - 1 12 - 13 - \ / - 3 - 5 - ROOT \ - 7 - 9 - 10 - 11 -``` - -Now imagine we vote on `10`, which roots `5`. At the same time the cluster catches up and the latest super majority root is now `3`. After pruning this is our local fork view. - -``` - 12 - 13 - / - 3 - 5 ROOT - SMR \ - 7 - 9 - 10 - 11 -``` - -Finally a vote on `11` will root `7`, pruning the final fork -``` - 3 - 5 - 7 - 9 - 10 - 11 - SMR ROOT -``` diff --git a/docs/src/consensus/stake-delegation-and-rewards.md b/docs/src/consensus/stake-delegation-and-rewards.md deleted file mode 100644 index 8d2f6b31c61..00000000000 --- a/docs/src/consensus/stake-delegation-and-rewards.md +++ /dev/null @@ -1,339 +0,0 @@ ---- -title: Stake Delegation and Rewards ---- - -Stakers are rewarded for helping to validate the ledger. They do this by -delegating their stake to validator nodes. Those validators do the legwork of -replaying the ledger and sending votes to a per-node vote account to which -stakers can delegate their stakes. The rest of the cluster uses those -stake-weighted votes to select a block when forks arise. Both the validator and -staker need some economic incentive to play their part. The validator needs to -be compensated for its hardware and the staker needs to be compensated for the -risk of getting its stake slashed. The economics are covered in -[staking rewards](../implemented-proposals/staking-rewards.md). This section, on -the other hand, describes the underlying mechanics of its implementation. - -## Basic Design - -The general idea is that the validator owns a Vote account. The Vote account -tracks validator votes, counts validator generated credits, and provides any -additional validator specific state. The Vote account is not aware of any stakes -delegated to it and has no staking weight. - -A separate Stake account \(created by a staker\) names a Vote account to which -the stake is delegated. Rewards generated are proportional to the amount of -lamports staked. The Stake account is owned by the staker only. Some portion of -the lamports stored in this account are the stake. - -## Passive Delegation - -Any number of Stake accounts can delegate to a single Vote account without an -interactive action from the identity controlling the Vote account or submitting -votes to the account. - -The total stake allocated to a Vote account can be calculated by the sum of all -the Stake accounts that have the Vote account pubkey as the -`StakeStateV2::Stake::voter_pubkey`. - -## Vote and Stake accounts - -The rewards process is split into two on-chain programs. The Vote program solves -the problem of making stakes slashable. The Stake program acts as custodian of -the rewards pool and provides for passive delegation. The Stake program is -responsible for paying rewards to staker and voter when shown that a staker's -delegate has participated in validating the ledger. - -### VoteState - -VoteState is the current state of all the votes the validator has submitted to -the network. VoteState contains the following state information: - -- `votes` - The submitted votes data structure. -- `credits` - The total number of rewards this Vote program has generated over - its lifetime. -- `root_slot` - The last slot to reach the full lockout commitment necessary for - rewards. -- `commission` - The commission taken by this VoteState for any rewards claimed - by staker's Stake accounts. This is the percentage ceiling of the reward. -- Account::lamports - The accumulated lamports from the commission. These do not - count as stakes. -- `authorized_voter` - Only this identity is authorized to submit votes. This - field can only modified by this identity. -- `node_pubkey` - The Solana node that votes in this account. -- `authorized_withdrawer` - the identity of the entity in charge of the lamports - of this account, separate from the account's address and the authorized vote - signer. - -### VoteInstruction::Initialize\(VoteInit\) - -- `account[0]` - RW - The VoteState. - - `VoteInit` carries the new vote account's `node_pubkey`, `authorized_voter`, - `authorized_withdrawer`, and `commission`. - - other VoteState members defaulted. - -### VoteInstruction::Authorize\(Pubkey, VoteAuthorize\) - -Updates the account with a new authorized voter or withdrawer, according to the -VoteAuthorize parameter \(`Voter` or `Withdrawer`\). The transaction must be -signed by the Vote account's current `authorized_voter` or -`authorized_withdrawer`. - -- `account[0]` - RW - The VoteState. `VoteState::authorized_voter` or - `authorized_withdrawer` is set to `Pubkey`. - -### VoteInstruction::AuthorizeWithSeed\(VoteAuthorizeWithSeedArgs\) - -Updates the account with a new authorized voter or withdrawer, according to the -VoteAuthorize parameter \(`Voter` or `Withdrawer`\). Unlike -`VoteInstruction::Authorize` this instruction is for use when the Vote account's -current `authorized_voter` or `authorized_withdrawer` is a derived key. The -transaction must be signed by someone who can sign for the base key of that -derived key. - -- `account[0]` - RW - The VoteState. `VoteState::authorized_voter` or - `authorized_withdrawer` is set to `Pubkey`. - -### VoteInstruction::Vote\(Vote\) - -- `account[0]` - RW - The VoteState. `VoteState::lockouts` and - `VoteState::credits` are updated according to voting lockout rules see - [Tower BFT](../implemented-proposals/tower-bft.md). -- `account[1]` - RO - `sysvar::slot_hashes` A list of some N most recent slots - and their hashes for the vote to be verified against. -- `account[2]` - RO - `sysvar::clock` The current network time, expressed in - slots, epochs. - -### StakeStateV2 - -A StakeStateV2 takes one of four forms, StakeStateV2::Uninitialized, -StakeStateV2::Initialized, StakeStateV2::Stake, and StakeStateV2::RewardsPool. -Only the first three forms are used in staking, but only StakeStateV2::Stake is -interesting. All RewardsPools are created at genesis. - -### StakeStateV2::Stake - -StakeStateV2::Stake is the current delegation preference of the **staker** and -contains the following state information: - -- Account::lamports - The lamports available for staking. -- `stake` - the staked amount \(subject to warmup and cooldown\) for generating - rewards, always less than or equal to Account::lamports. -- `voter_pubkey` - The pubkey of the VoteState instance the lamports are - delegated to. -- `credits_observed` - The total credits claimed over the lifetime of the - program. -- `activated` - the epoch at which this stake was activated/delegated. The full - stake will be counted after warmup. -- `deactivated` - the epoch at which this stake was de-activated, some cooldown - epochs are required before the account is fully deactivated, and the stake - available for withdrawal. -- `authorized_staker` - the pubkey of the entity that must sign delegation, - activation, and deactivation transactions. -- `authorized_withdrawer` - the identity of the entity in charge of the lamports - of this account, separate from the account's address, and the authorized - staker. - -### StakeStateV2::RewardsPool - -To avoid a single network-wide lock or contention in redemption, 256 -RewardsPools are part of genesis under pre-determined keys, each with -std::u64::MAX credits to be able to satisfy redemptions according to point -value. - -The Stakes and the RewardsPool are accounts that are owned by the same `Stake` -program. - -### StakeInstruction::DelegateStake - -The Stake account is moved from Initialized to StakeStateV2::Stake form, or from -a deactivated (i.e. fully cooled-down) StakeStateV2::Stake to activated -StakeStateV2::Stake. This is how stakers choose the vote account and validator -node to which their stake account lamports are delegated. The transaction must -be signed by the stake's `authorized_staker`. - -- `account[0]` - RW - The StakeStateV2::Stake instance. - `StakeStateV2::Stake::credits_observed` is initialized to - `VoteState::credits`, `StakeStateV2::Stake::voter_pubkey` is initialized to - `account[1]`. If this is the initial delegation of stake, - `StakeStateV2::Stake::stake` is initialized to the account's balance in - lamports, `StakeStateV2::Stake::activated` is initialized to the current Bank - epoch, and `StakeStateV2::Stake::deactivated` is initialized to std::u64::MAX -- `account[1]` - R - The VoteState instance. -- `account[2]` - R - sysvar::clock account, carries information about current - Bank epoch. -- `account[3]` - R - sysvar::stakehistory account, carries information about - stake history. -- `account[4]` - R - stake::Config account, carries warmup, cooldown, and - slashing configuration. - -### StakeInstruction::Authorize\(Pubkey, StakeAuthorize\) - -Updates the account with a new authorized staker or withdrawer, according to the -StakeAuthorize parameter \(`Staker` or `Withdrawer`\). The transaction must be -by signed by the Stakee account's current `authorized_staker` or -`authorized_withdrawer`. Any stake lock-up must have expired, or the lock-up -custodian must also sign the transaction. - -- `account[0]` - RW - The StakeStateV2. - - `StakeStateV2::authorized_staker` or `authorized_withdrawer` is set to - `Pubkey`. - -### StakeInstruction::Deactivate - -A staker may wish to withdraw from the network. To do so he must first -deactivate his stake, and wait for cooldown. The transaction must be signed by -the stake's `authorized_staker`. - -- `account[0]` - RW - The StakeStateV2::Stake instance that is deactivating. -- `account[1]` - R - sysvar::clock account from the Bank that carries current - epoch. - -StakeStateV2::Stake::deactivated is set to the current epoch + cooldown. The -account's stake will ramp down to zero by that epoch, and Account::lamports will -be available for withdrawal. - -### StakeInstruction::Withdraw\(u64\) - -Lamports build up over time in a Stake account and any excess over activated -stake can be withdrawn. The transaction must be signed by the stake's -`authorized_withdrawer`. - -- `account[0]` - RW - The StakeStateV2::Stake from which to withdraw. -- `account[1]` - RW - Account that should be credited with the withdrawn - lamports. -- `account[2]` - R - sysvar::clock account from the Bank that carries current - epoch, to calculate stake. -- `account[3]` - R - sysvar::stake_history account from the Bank that carries - stake warmup/cooldown history. - -## Benefits of the design - -- Single vote for all the stakers. -- Clearing of the credit variable is not necessary for claiming rewards. -- Each delegated stake can claim its rewards independently. -- Commission for the work is deposited when a reward is claimed by the delegated - stake. - -## Example Callflow - -![Passive Staking Callflow](/img/passive-staking-callflow.png) - -## Staking Rewards - -The specific mechanics and rules of the validator rewards regime is outlined -here. Rewards are earned by delegating stake to a validator that is voting -correctly. Voting incorrectly exposes that validator's stakes to -[slashing](../proposals/slashing.md). - -### Basics - -The network pays rewards from a portion of network -[inflation](https://solana.com/docs/terminology#inflation). The number of -lamports available to pay rewards for an epoch is fixed and must be evenly -divided among all staked nodes according to their relative stake weight and -participation. The weighting unit is called a -[point](https://solana.com/docs/terminology#point). - -Rewards for an epoch are not available until the end of that epoch. - -At the end of each epoch, the total number of points earned during the epoch is -summed and used to divide the rewards portion of epoch inflation to arrive at a -point value. This value is recorded in the bank in a -[sysvar](https://solana.com/docs/terminology#sysvar) that maps epochs to point -values. - -During redemption, the stake program counts the points earned by the stake for -each epoch, multiplies that by the epoch's point value, and transfers lamports -in that amount from a rewards account into the stake and vote accounts according -to the vote account's commission setting. - -### Economics - -Point value for an epoch depends on aggregate network participation. If -participation in an epoch drops off, point values are higher for those that do -participate. - -### Earning credits - -Validators earn one vote credit for every correct vote that exceeds maximum -lockout, i.e. every time the validator's vote account retires a slot from its -lockout list, making that vote a root for the node. - -Stakers who have delegated to that validator earn points in proportion to their -stake. Points earned is the product of vote credits and stake. - -### Stake warmup, cooldown, withdrawal - -Stakes, once delegated, do not become effective immediately. They must first -pass through a warmup period. During this period some portion of the stake is -considered "effective", the rest is considered "activating". Changes occur on -epoch boundaries. - -The stake program limits the rate of change to total network stake, reflected in -the stake program's `config::warmup_rate` \(set to 25% per epoch in the current -implementation\). - -The amount of stake that can be warmed up each epoch is a function of the -previous epoch's total effective stake, total activating stake, and the stake -program's configured warmup rate. - -Cooldown works the same way. Once a stake is deactivated, some part of it is -considered "effective", and also "deactivating". As the stake cools down, it -continues to earn rewards and be exposed to slashing, but it also becomes -available for withdrawal. - -Bootstrap stakes are not subject to warmup. - -Rewards are paid against the "effective" portion of the stake for that epoch. - -#### Warmup example - -Consider the situation of a single stake of 1,000 activated at epoch N, with -network warmup rate of 20%, and a quiescent total network stake at epoch N of -2,000. - -At epoch N+1, the amount available to be activated for the network is 400 \(20% -of 2000\), and at epoch N, this example stake is the only stake activating, and -so is entitled to all of the warmup room available. - -| epoch | effective | activating | total effective | total activating | -| :---- | --------: | ---------: | --------------: | ---------------: | -| N-1 | | | 2,000 | 0 | -| N | 0 | 1,000 | 2,000 | 1,000 | -| N+1 | 400 | 600 | 2,400 | 600 | -| N+2 | 880 | 120 | 2,880 | 120 | -| N+3 | 1000 | 0 | 3,000 | 0 | - -Were 2 stakes \(X and Y\) to activate at epoch N, they would be awarded a -portion of the 20% in proportion to their stakes. At each epoch effective and -activating for each stake is a function of the previous epoch's state. - -| epoch | X eff | X act | Y eff | Y act | total effective | total activating | -| :---- | ----: | ----: | ----: | ----: | --------------: | ---------------: | -| N-1 | | | | | 2,000 | 0 | -| N | 0 | 1,000 | 0 | 200 | 2,000 | 1,200 | -| N+1 | 333 | 667 | 67 | 133 | 2,400 | 800 | -| N+2 | 733 | 267 | 146 | 54 | 2,880 | 321 | -| N+3 | 1000 | 0 | 200 | 0 | 3,200 | 0 | - -### Withdrawal - -Only lamports in excess of effective+activating stake may be withdrawn at any -time. This means that during warmup, effectively no stake can be withdrawn. -During cooldown, any tokens in excess of effective stake may be withdrawn -\(activating == 0\). Because earned rewards are automatically added to stake, -withdrawal is generally only possible after deactivation. - -### Lock-up - -Stake accounts support the notion of lock-up, wherein the stake account balance -is unavailable for withdrawal until a specified time. Lock-up is specified as an -epoch height, i.e. the minimum epoch height that must be reached by the network -before the stake account balance is available for withdrawal, unless the -transaction is also signed by a specified custodian. This information is -gathered when the stake account is created, and stored in the Lockup field of -the stake account's state. Changing the authorized staker or withdrawer is also -subject to lock-up, as such an operation is effectively a transfer. diff --git a/docs/src/consensus/synchronization.md b/docs/src/consensus/synchronization.md deleted file mode 100644 index 3eb763e4daf..00000000000 --- a/docs/src/consensus/synchronization.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -title: Synchronization ---- - -Fast, reliable synchronization is the biggest reason Solana is able to achieve such high throughput. Traditional blockchains synchronize on large chunks of transactions called blocks. By synchronizing on blocks, a transaction cannot be processed until a duration, called "block time", has passed. In Proof of Work consensus, these block times need to be very large \(~10 minutes\) to minimize the odds of multiple validators producing a new valid block at the same time. There's no such constraint in Proof of Stake consensus, but without reliable timestamps, a validator cannot determine the order of incoming blocks. The popular workaround is to tag each block with a [wallclock timestamp](https://en.bitcoin.it/wiki/Block_timestamp). Because of clock drift and variance in network latencies, the timestamp is only accurate within an hour or two. To workaround the workaround, these systems lengthen block times to provide reasonable certainty that the median timestamp on each block is always increasing. - -Solana takes a very different approach, which it calls _Proof of History_ or _PoH_. Leader nodes "timestamp" blocks with cryptographic proofs that some duration of time has passed since the last proof. All data hashed into the proof most certainly have occurred before the proof was generated. The node then shares the new block with validator nodes, which are able to verify those proofs. The blocks can arrive at validators in any order or even could be replayed years later. With such reliable synchronization guarantees, Solana is able to break blocks into smaller batches of transactions called _entries_. Entries are streamed to validators in realtime, before any notion of block consensus. - -Solana technically never sends a _block_, but uses the term to describe the sequence of entries that validators vote on to achieve _confirmation_. In that way, Solana's confirmation times can be compared apples to apples to block-based systems. The current implementation sets block time to 800ms. - -What's happening under the hood is that entries are streamed to validators as quickly as a leader node can batch a set of valid transactions into an entry. Validators process those entries long before it is time to vote on their validity. By processing the transactions optimistically, there is effectively no delay between the time the last entry is received and the time when the node can vote. In the event consensus is **not** achieved, a node simply rolls back its state. This optimistic processing technique was introduced in 1981 and called [Optimistic Concurrency Control](https://en.wikipedia.org/wiki/Optimistic_concurrency_control). It can be applied to blockchain architecture where a cluster votes on a hash that represents the full ledger up to some _block height_. In Solana, it is implemented trivially using the last entry's PoH hash. - -## Relationship to VDFs - -The Proof of History technique was first described for use in blockchain by Solana in November of 2017. In June of the following year, a similar technique was described at Stanford and called a [verifiable delay function](https://eprint.iacr.org/2018/601.pdf) or _VDF_. - -A desirable property of a VDF is that verification time is very fast. Solana's approach to verifying its delay function is proportional to the time it took to create it. Split over a 4000 core GPU, it is sufficiently fast for Solana's needs, but if you asked the authors of the paper cited above, they might tell you \([and have](https://github.com/solana-labs/solana/issues/388)\) that Solana's approach is algorithmically slow and it shouldn't be called a VDF. We argue the term VDF should represent the category of verifiable delay functions and not just the subset with certain performance characteristics. Until that's resolved, Solana will likely continue using the term PoH for its application-specific VDF. - -Another difference between PoH and VDFs is that a VDF is used only for tracking duration. PoH's hash chain, on the other hand, includes hashes of any data the application observed. That data is a double-edged sword. On one side, the data "proves history" - that the data most certainly existed before hashes after it. On the other side, it means the application can manipulate the hash chain by changing _when_ the data is hashed. The PoH chain therefore does not serve as a good source of randomness whereas a VDF without that data could. Solana's [leader rotation algorithm](./leader-rotation.md), for example, is derived only from the VDF _height_ and not its hash at that height. - -## Relationship to Consensus Mechanisms - -Proof of History is not a consensus mechanism, but it is used to improve the performance of Solana's Proof of Stake consensus. It is also used to improve the performance of the data plane protocols. - -## More on Proof of History - -- [water clock analogy](https://medium.com/solana-labs/proof-of-history-explained-by-a-water-clock-e682183417b8) -- [Proof of History overview](https://medium.com/solana-labs/proof-of-history-a-clock-for-blockchain-cf47a61a9274) diff --git a/docs/src/consensus/turbine-block-propagation.md b/docs/src/consensus/turbine-block-propagation.md deleted file mode 100644 index f822a6a4f52..00000000000 --- a/docs/src/consensus/turbine-block-propagation.md +++ /dev/null @@ -1,124 +0,0 @@ ---- -title: Turbine Block Propagation ---- - -A Solana cluster uses a multi-layer block propagation mechanism called _Turbine_ -to broadcast ledger entries to all nodes. The cluster divides itself into layers -of nodes, and each node in a given layer is responsible for propagating any data -it receives on to a small set of nodes in the next downstream layer. This way -each node only has to communicate with a small number of nodes. - -## Layer Structure - -The leader communicates with a special root node. The root can be thought of as -layer 0 and communicates with layer 1, which is made up of at most -`DATA_PLANE_FANOUT` nodes. If the number of nodes in the cluster is greater than -layer 1, then the data plane fanout mechanism adds layers below. The number of -nodes in each additional layer grows by a factor of `DATA_PLANE_FANOUT`. - -A good way to think about this is, layer 0 starts with a single node, layer 1 -starts with fanout nodes, and layer 2 will have `fanout * number of nodes in -layer 1` and so on. - -### Layer Assignment - Weighted Selection - -In order for data plane fanout to work, the entire cluster must agree on how the -cluster is divided into layers. To achieve this, all the recognized validator -nodes \(the TVU peers\) are shuffled with a stake weighting and stored in a -list. This list is then indexed in different ways to figure out layer boundaries -and retransmit peers - referred to as the \(turbine tree\). For example, the -list is shuffled and leader selects the first node to be the root node, and the -root node selects the next `DATA_PLANE_FANOUT` nodes to make up layer 1. The -shuffle is biased towards higher staked nodes, allowing heavier votes to come -back to the leader first. Layer 2 and lower-layer nodes use the same logic to -find their next layer peers. - -To reduce the possibility of attack vectors, the list is shuffled and indexed on -every shred. The turbine tree is generated from the set of validator nodes for -each shred using a seed derived from the slot leader id, slot, shred index, and -shred type. - -### Configuration Values - -`DATA_PLANE_FANOUT` - Determines the size of layer 1. Subsequent layers grow by -a factor of `DATA_PLANE_FANOUT`. Layers will fill to capacity before new ones are -added, i.e if a layer isn't full, it _must_ be the last one. - -Currently, configuration is set when the cluster is launched. In the future, -these parameters may be hosted on-chain, allowing modification on the fly as the -cluster sizes change. - -## Shred Propagation Flow - -During its slot, the leader node makes its initial broadcasts to a special root -node \(layer 0\) sitting atop the turbine tree. This root node is rotated every -shred based on the weighted shuffle previously mentioned. The root shares data -with layer 1. Nodes in this layer then retransmit shreds to a subset of nodes in -the next layer \(layer 2\). In general, every node in layer-1 retransmits to a -unique subset of nodes in the next layer, etc, until all nodes in the cluster -have received all the shreds. - -To prevent redundant transmission, each node uses the deterministically -generated turbine tree, its own index in the tree, and `DATA_PLANE_FANOUT` to -iterate through the tree and identify downstream nodes. Each node in a layer -only has to broadcast its shreds to a maximum of `DATA_PLANE_FANOUT` nodes in -the next layer instead of to every TVU peer in the cluster. - -The following diagram shows how shreds propagate through a cluster with 15 nodes -and a fanout of 3. - -![Shred propagation through 15 node cluster with fanout of 3](/img/data-plane-propagation.png) - -## Calculating the required FEC rate - -Turbine relies on retransmission of packets between validators. Due to -retransmission, any network wide packet loss is compounded, and the probability -of the packet failing to reach its destination increases on each hop. The FEC -rate needs to take into account the network wide packet loss, and the -propagation depth. - -A shred group is the set of data and coding packets that can be used to -reconstruct each other. Each shred group has a chance of failure, based on the -likelihood of the number of packets failing that exceeds the FEC rate. If a -validator fails to reconstruct the shred group, then the block cannot be -reconstructed, and the validator has to rely on repair to fixup the blocks. - -The probability of the shred group failing can be computed using the binomial -distribution. If the FEC rate is `16:4`, then the group size is 20, and at least -5 of the shreds must fail for the group to fail. Which is equal to the sum of -the probability of 5 or more trials failing out of 20. - -Probability of a block succeeding in turbine: - -- Probability of packet failure: `P = 1 - (1 - network_packet_loss_rate)^2` -- FEC rate: `K:M` -- Number of trials: `N = K + M` -- Shred group failure rate: `S = 1 - (SUM of i=0 -> M for binomial(prob_failure = P, trials = N, failures = i))` -- Shreds per block: `G` -- Block success rate: `B = (1 - S) ^ (G / N)` -- Binomial distribution for exactly `i` results with probability of P in N trials is defined as `(N choose i) * P^i * (1 - P)^(N-i)` - -For example: - -- Network packet loss rate is 15%. -- 50k tps network generates 6400 shreds per second. -- FEC rate increases the total shreds per block by the FEC ratio. - -With a FEC rate: `16:4` - -- `G = 8000` -- `P = 1 - 0.85 * 0.85 = 1 - 0.7225 = 0.2775` -- `S = 1 - (SUM of i=0 -> 4 for binomial(prob_failure = 0.2775, trials = 20, failures = i)) = 0.689414` -- `B = (1 - 0.689) ^ (8000 / 20) = 10^-203` - -With FEC rate of `16:16` - -- `G = 12800` -- `S = 1 - (SUM of i=0 -> 16 for binomial(prob_failure = 0.2775, trials = 32, failures = i)) = 0.002132` -- `B = (1 - 0.002132) ^ (12800 / 32) = 0.42583` - -With FEC rate of `32:32` - -- `G = 12800` -- `S = 1 - (SUM of i=0 -> 32 for binomial(prob_failure = 0.2775, trials = 64, failures = i)) = 0.000048` -- `B = (1 - 0.000048) ^ (12800 / 64) = 0.99045` diff --git a/docs/src/consensus/vote-signing.md b/docs/src/consensus/vote-signing.md deleted file mode 100644 index 0d4df08b4af..00000000000 --- a/docs/src/consensus/vote-signing.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: Secure Vote Signing ---- - -A validator receives entries from the current leader and submits votes confirming those entries are valid. This vote submission presents a security challenge, because forged votes that violate consensus rules could be used to slash the validator's stake. - -The validator votes on its chosen fork by submitting a transaction that uses an asymmetric key to sign the result of its validation work. Other entities can verify this signature using the validator's public key. If the validator's key is used to sign incorrect data \(e.g. votes on multiple forks of the ledger\), the node's stake or its resources could be compromised. - -## Validators, Vote Signers, and Stakeholders - -When a validator receives multiple blocks for the same slot, it tracks all possible forks until it can determine a "best" one. A validator selects the best fork by submitting a vote to it. - -A stakeholder is an identity that has control of the staked capital. The stakeholder can delegate its stake to the vote signer. Once a stake is delegated, the vote signer's votes represent the voting weight of all the delegated stakes, and produce rewards for all the delegated stakes. - -## Validator voting - -A validator node, at startup, creates a new vote account and registers it with the cluster via gossip. The other nodes on the cluster include the new validator in the active set. Subsequently, the validator submits a "new vote" transaction signed with the validator's voting private key on each voting event. diff --git a/docs/src/css/custom.css b/docs/src/css/custom.css deleted file mode 100644 index 35cc4a0d71f..00000000000 --- a/docs/src/css/custom.css +++ /dev/null @@ -1,130 +0,0 @@ -/* stylelint-disable docusaurus/copyright-header */ -/** - * Any CSS included here will be global. The classic template - * bundles Infima by default. Infima is a CSS framework designed to - * work well for content-centric websites. - */ - -/* You can override the default Infima variables here. */ - -@import url("https://fonts.googleapis.com/css2?family=Roboto"); - -:root { - --ifm-color-primary: #25c2a0; - --ifm-color-primary-dark: #409088; - --ifm-color-primary-darker: #387462; - --ifm-color-primary-darkest: #1b4e3f; - --ifm-color-primary-light: #42ba96; - --ifm-color-primary-lighter: #86b8b6; - --ifm-color-primary-lightest: #abd5c6; - --ifm-code-font-size: 95%; - --ifm-spacing-horizontal: 1em; - --ifm-font-family-base: "Roboto", system-ui, -apple-system, Segoe UI, Roboto, - Ubuntu, Cantarell, Noto Sans, sans-serif, BlinkMacSystemFont, "Segoe UI", - Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", - "Segoe UI Symbol"; - --ifm-footer-background-color: #232323; -} - -@keyframes fadeInUp { - /* 0% { - opacity: 0; - transform: translateY(1.5rem); - } */ -} - -main { - margin: 1rem 0 5rem 0; -} - -.docusaurus-highlight-code-line { - background-color: rgb(72, 77, 91); - display: block; - margin: 0 calc(-1 * var(--ifm-pre-padding)); - padding: 0 var(--ifm-pre-padding); -} - -.button { - background-color: var(--ifm-link-color); - color: var(--ifm-background-surface-color) !important; - padding: 0.5em 0.8em; - border: 0px solid red; - font-size: 1em !important; -} - -.container__spacer { - margin: 0em 1em 3em 1em; -} - -.cards__container { - /* margin-bottom: 0; */ -} - -.cards__container .col { - margin-bottom: 3em; -} - -.card { - padding: 1rem; - margin-top: 0rem; - animation: fadeInUp 400ms backwards; - animation-delay: 150ms; - transition-property: all; - transition-duration: 200ms; - align-items: start; - height: 100%; - /* box-shadow: 0 8px 15px 2px rgba(86, 91, 115, 0.1); */ - border: 1px solid #ccc; -} - -[data-theme="dark"] .card { - border-color: #444; -} - -.card a { - text-decoration: none; -} - -.card:hover { - /* transform: translate(0px, -5px); */ - border-color: var(--ifm-color-primary); -} - -.footer--dark { - background-color: #232323 !important; -} - -footer .text--center { - padding: 2rem 0 0 0; -} - -.card__header h3 { - color: #1dd79b; -} - -.header-link-icon:before { - content: ""; - display: flex; - height: 24px; - width: 24px; - background-position: center center; -} -.header-link-icon { - padding: 0.4em !important; -} -[data-theme="dark"] .header-github-link:before { - background: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='%23fff' d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E") - no-repeat; -} -.header-github-link:before { - background: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E") - no-repeat; -} -[data-theme="dark"] .header-discord-link:before { - background: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 640 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='%23fff' d='M524.531,69.836a1.5,1.5,0,0,0-.764-.7A485.065,485.065,0,0,0,404.081,32.03a1.816,1.816,0,0,0-1.923.91,337.461,337.461,0,0,0-14.9,30.6,447.848,447.848,0,0,0-134.426,0,309.541,309.541,0,0,0-15.135-30.6,1.89,1.89,0,0,0-1.924-.91A483.689,483.689,0,0,0,116.085,69.137a1.712,1.712,0,0,0-.788.676C39.068,183.651,18.186,294.69,28.43,404.354a2.016,2.016,0,0,0,.765,1.375A487.666,487.666,0,0,0,176.02,479.918a1.9,1.9,0,0,0,2.063-.676A348.2,348.2,0,0,0,208.12,430.4a1.86,1.86,0,0,0-1.019-2.588,321.173,321.173,0,0,1-45.868-21.853,1.885,1.885,0,0,1-.185-3.126c3.082-2.309,6.166-4.711,9.109-7.137a1.819,1.819,0,0,1,1.9-.256c96.229,43.917,200.41,43.917,295.5,0a1.812,1.812,0,0,1,1.924.233c2.944,2.426,6.027,4.851,9.132,7.16a1.884,1.884,0,0,1-.162,3.126,301.407,301.407,0,0,1-45.89,21.83,1.875,1.875,0,0,0-1,2.611,391.055,391.055,0,0,0,30.014,48.815,1.864,1.864,0,0,0,2.063.7A486.048,486.048,0,0,0,610.7,405.729a1.882,1.882,0,0,0,.765-1.352C623.729,277.594,590.933,167.465,524.531,69.836ZM222.491,337.58c-28.972,0-52.844-26.587-52.844-59.239S193.056,219.1,222.491,219.1c29.665,0,53.306,26.82,52.843,59.239C275.334,310.993,251.924,337.58,222.491,337.58Zm195.38,0c-28.971,0-52.843-26.587-52.843-59.239S388.437,219.1,417.871,219.1c29.667,0,53.307,26.82,52.844,59.239C470.715,310.993,447.538,337.58,417.871,337.58Z'/%3E%3C/svg%3E") - no-repeat center; -} -.header-discord-link:before { - background: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 640 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M524.531,69.836a1.5,1.5,0,0,0-.764-.7A485.065,485.065,0,0,0,404.081,32.03a1.816,1.816,0,0,0-1.923.91,337.461,337.461,0,0,0-14.9,30.6,447.848,447.848,0,0,0-134.426,0,309.541,309.541,0,0,0-15.135-30.6,1.89,1.89,0,0,0-1.924-.91A483.689,483.689,0,0,0,116.085,69.137a1.712,1.712,0,0,0-.788.676C39.068,183.651,18.186,294.69,28.43,404.354a2.016,2.016,0,0,0,.765,1.375A487.666,487.666,0,0,0,176.02,479.918a1.9,1.9,0,0,0,2.063-.676A348.2,348.2,0,0,0,208.12,430.4a1.86,1.86,0,0,0-1.019-2.588,321.173,321.173,0,0,1-45.868-21.853,1.885,1.885,0,0,1-.185-3.126c3.082-2.309,6.166-4.711,9.109-7.137a1.819,1.819,0,0,1,1.9-.256c96.229,43.917,200.41,43.917,295.5,0a1.812,1.812,0,0,1,1.924.233c2.944,2.426,6.027,4.851,9.132,7.16a1.884,1.884,0,0,1-.162,3.126,301.407,301.407,0,0,1-45.89,21.83,1.875,1.875,0,0,0-1,2.611,391.055,391.055,0,0,0,30.014,48.815,1.864,1.864,0,0,0,2.063.7A486.048,486.048,0,0,0,610.7,405.729a1.882,1.882,0,0,0,.765-1.352C623.729,277.594,590.933,167.465,524.531,69.836ZM222.491,337.58c-28.972,0-52.844-26.587-52.844-59.239S193.056,219.1,222.491,219.1c29.665,0,53.306,26.82,52.843,59.239C275.334,310.993,251.924,337.58,222.491,337.58Zm195.38,0c-28.971,0-52.843-26.587-52.843-59.239S388.437,219.1,417.871,219.1c29.667,0,53.307,26.82,52.844,59.239C470.715,310.993,447.538,337.58,417.871,337.58Z'/%3E%3C/svg%3E") - no-repeat center; -} diff --git a/docs/src/faq.md b/docs/src/faq.md deleted file mode 100644 index be6365dba0a..00000000000 --- a/docs/src/faq.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -title: Validator Frequently Asked Questions -sidebar_label: Frequently Asked Questions ---- - -### What is Agave? How is it different from other possible Solana validators? - -Solana is an open source, decentralized, proof-of-stake blockchain. It is therefore possible for multiple distinct teams to fork and maintain their own validator software. The original Solana validator was maintained by Solana Labs. A new organization, Anza, was formed in 2024 consisting of former Solana Labs core engineering members. Anza forked the Solana validator and renamed it to Agave (this project). Agave is the version of the original Solana validator maintained by the team at Anza. As of the writing of this FAQ, Agave is the most popular validator client for the Solana network, but it is likely in the future there may be several validators running in parallel to help support the network. We recommend checking the community and doing research before making a selection. - -### What is a validator? - -A validator is a computer that runs a software program to verify transactions that are added to the Solana blockchain. A validator can be a voting validator or a non voting validator. To learn more, see [what is a validator](./what-is-a-validator.md). - -### What is an RPC node? - -An RPC node is also a computer that runs the validator software. Typically, an RPC node does not vote on the network. Instead the RPC node's job is to respond to API requests. See [what is an rpc node](./what-is-an-rpc-node.md) for more information. - -### What is a cluster? - -For a definition and an overview of the topic, see [what is a cluster?](./clusters/index.md). Solana maintains several clusters. For details on each, see [Solana clusters](./clusters/available.md). - -### What is Proof of Stake? - -Proof of Stake (PoS) is a blockchain architecture. Solana is a Proof of Stake blockchain. To read more, see [Proof of Stake](./what-is-a-validator.md#proof-of-stake). - -### What is Proof of Work? Is running a Solana validator the same as mining? - -No, a Solana validator uses Proof of Stake. It does not use Proof of Work (often called mining). See [Proof of Work: For Contrast](./what-is-a-validator.md#proof-of-stake). - -### Who can operate a validator? - -Anyone can operate a validator. All Solana clusters are permissionless. A new operator can choose to join at any time. - -### Is there a validator set or limited number of validators that can operate? - -No, all Solana clusters are permissionless. There is no limit to the number of active validators that can participate in consensus. Validators participating in consensus (voting validators) incur transaction fees for each vote. A voting validator can expect to incur up to 1.1 SOL per day in vote transaction fees. - -### What are the hardware requirements for running a validator? - -See [validator requirements](./operations/requirements.md). - -### Can I run my validator at home? - -Anyone can join the cluster including home users. You must make sure that your system can perform well and keep up with the cluster. Many home internet connections are not suitable to run a Solana validator. Most operators choose to operate their validator in a data center either by using a server provider or by supplying your own hardware at a colocation data center. - -See the [validator requirements](./operations/requirements.md) for more information. - -### What skills does a Solana validator operator need? - -See [Solana validator prerequisites](./operations/prerequisites.md). - -### What are the economics of running a validator? - -See [economics of running a validator](./operations/validator-or-rpc-node.md#economics-of-running-a-consensus-validator). diff --git a/docs/src/implemented-proposals/abi-management.md b/docs/src/implemented-proposals/abi-management.md deleted file mode 100644 index 88e27c9a1a9..00000000000 --- a/docs/src/implemented-proposals/abi-management.md +++ /dev/null @@ -1,166 +0,0 @@ ---- -title: Solana ABI management process ---- - -This document proposes the Solana ABI management process. The ABI management -process is an engineering practice and a supporting technical framework to avoid -introducing unintended incompatible ABI changes. - -# Problem - -The Solana ABI (binary interface to the cluster) is currently only defined -implicitly by the implementation and requires a very careful eye to notice -breaking changes. This makes it extremely difficult to upgrade the software -on an existing cluster without rebooting the ledger. - -# Requirements and objectives - -- Unintended ABI changes can be detected as CI failures mechanically. -- Newer implementation must be able to process the oldest data (since genesis) - once we go mainnet. -- The objective of this proposal is to protect the ABI while sustaining rather - rapid development by opting for a mechanical process rather than a very long - human-driven auditing process. -- Once signed cryptographically, data blob must be identical, so no - in-place data format update is possible regardless of inbound and outbound of - the online system. Also, considering the sheer volume of transactions we're - aiming to handle, retrospective in-place update is undesirable at best. - -# Solution - -Instead of natural human's eye due-diligence, which should be assumed to fail -regularly, we need a systematic assurance of not breaking the cluster when -changing the source code. - -For that purpose, we introduce a mechanism of marking every ABI-related things -in source code (`struct`s, `enum`s) with the new `#[frozen_abi]` attribute. This -takes hard-coded digest value derived from types of its fields via -`ser::Serialize`. And the attribute automatically generates a unit test to try -to detect any unsanctioned changes to the marked ABI-related things. - -However, the detection cannot be complete; no matter how hard we statically -analyze the source code, it's still possible to break ABI. For example, this -includes not-`derive`d hand-written `ser::Serialize`, underlying library's -implementation changes (for example `bincode`), CPU architecture differences. -The detection of these possible ABI incompatibilities is out-of-scope for this -ABI management. - -# Definitions - -ABI item/type: various types to be used for serialization, which collectively -comprises the whole ABI for any system components. For example, those types -include `struct`s and `enum`s. - -ABI item digest: Some fixed hash derived from type information of ABI item's -fields. - -# Example - -```patch -+#[frozen_abi(digest="eXSMM7b89VY72V...")] - #[derive(Serialize, Default, Deserialize, Debug, PartialEq, Eq, Clone)] - pub struct Vote { - /// A stack of votes starting with the oldest vote - pub slots: Vec, - /// signature of the bank's state at the last slot - pub hash: Hash, - } -``` - -# Developer's workflow - -To know the digest for new ABI items, developers can add `frozen_abi` with a -random digest value and run the unit tests and replace it with the correct -digest from the assertion test error message. - -Run unit tests using the following command to generate digest values: -``` -SOLANA_ABI_DUMP_DIR=. cargo +nightly test abi -``` - -In general, once we add `frozen_abi` and its change is published in the stable -release channel, its digest should never change. If such a change is needed, we -should opt for defining a new `struct` like `FooV1`. And special release flow -like hard forks should be approached. - -# Implementation remarks - -We use some degree of macro machinery to automatically generate unit tests -and calculate a digest from ABI items. This is doable by clever use of -`serde::Serialize` (`[1]`) and `any::type_name` (`[2]`). For a precedent for similar -implementation, `ink` from the Parity Technologies `[3]` could be informational. - -# Implementation details - -The implementation's goal is to detect unintended ABI changes automatically as -much as possible. To that end, the digest of structural ABI information is -calculated with best-effort accuracy and stability. - -When the ABI digest check is run, it dynamically computes an ABI digest by -recursively digesting the ABI of fields of the ABI item, by re-using the -`serde`'s serialization functionality, proc macro and generic specialization. -And then, the check `assert!`s that its finalized digest value is identical as -what is specified in the `frozen_abi` attribute. - -To realize that, it creates an example instance of the type and a custom -`Serializer` instance for `serde` to recursively traverse its fields as if -serializing the example for real. This traversing must be done via `serde` to -really capture what kinds of data actually would be serialized by `serde`, even -considering custom non-`derive`d `Serialize` trait implementations. - -# The ABI digesting process - -This part is a bit complex. There is three inter-depending parts: `AbiExample`, -`AbiDigester` and `AbiEnumVisitor`. - -First, the generated test creates an example instance of the digested type with -a trait called `AbiExample`, which should be implemented for all of digested -types like the `Serialize` and return `Self` like the `Default` trait. Usually, -it's provided via generic trait specialization for most of common types. Also -it is possible to `derive` for `struct` and `enum` and can be hand-written if -needed. - -The custom `Serializer` is called `AbiDigester`. And when it's called by `serde` -to serialize some data, it recursively collects ABI information as much as -possible. `AbiDigester`'s internal state for the ABI digest is updated -differentially depending on the type of data. This logic is specifically -redirected via with a trait called `AbiEnumVisitor` for each `enum` type. As the -name suggests, there is no need to implement `AbiEnumVisitor` for other types. - -To summarize this interplay, `serde` handles the recursive serialization control -flow in tandem with `AbiDigester`. The initial entry point in tests and child -`AbiDigester`s use `AbiExample` recursively to create an example object -hierarchical graph. And `AbiDigester` uses `AbiEnumVisitor` to inquiry the actual -ABI information using the constructed sample. - -`Default` isn't enough for `AbiExample`. Various collection's `::default()` is -empty, yet, we want to digest them with actual items. And, ABI digesting can't -be realized only with `AbiEnumVisitor`. `AbiExample` is required because an -actual instance of type is needed to actually traverse the data via `serde`. - -On the other hand, ABI digesting can't be done only with `AbiExample`, either. -`AbiEnumVisitor` is required because all variants of an `enum` cannot be -traversed just with a single variant of it as a ABI example. - -Digestible information: - -- rust's type name -- `serde`'s data type name -- all fields in `struct` -- all variants in `enum` -- `struct`: normal(`struct {...}`) and tuple-style (`struct(...)`) -- `enum`: normal variants and `struct`- and `tuple`- styles. -- attributes: `serde(serialize_with=...)` and `serde(skip)` - -Not digestible information: - -- Any custom serialize code path not touched by the sample provided by - `AbiExample`. (technically not possible) -- generics (must be a concrete type; use `frozen_abi` on concrete type - aliases) - -# References - -1. [(De)Serialization with type info · Issue #1095 · serde-rs/serde](https://github.com/serde-rs/serde/issues/1095#issuecomment-345483479) -2. [`std::any::type_name` - Rust](https://doc.rust-lang.org/std/any/fn.type_name.html) -3. [Parity's ink to write smart contracts](https://github.com/paritytech/ink) diff --git a/docs/src/implemented-proposals/bank-timestamp-correction.md b/docs/src/implemented-proposals/bank-timestamp-correction.md deleted file mode 100644 index f4ba7a0efb8..00000000000 --- a/docs/src/implemented-proposals/bank-timestamp-correction.md +++ /dev/null @@ -1,77 +0,0 @@ ---- -title: Bank Timestamp Correction ---- - -Each Bank has a timestamp that is stashed in the Clock sysvar and used to assess -time-based stake account lockups. However, since genesis, this value has been -based on a theoretical slots-per-second instead of reality, so it's quite -inaccurate. This poses a problem for lockups, since the accounts will not -register as lockup-free on (or anytime near) the date the lockup is set to -expire. - -Block times are already being estimated to cache in Blockstore and long-term -storage using a [validator timestamp oracle](validator-timestamp-oracle.md); -this data provides an opportunity to align the bank timestamp more closely with -real-world time. - -The general outline of the proposed implementation is as follows: - -- Correct each Bank timestamp using the validator-provided timestamp. -- Update the validator-provided timestamp calculation to use a stake-weighted - median, rather than a stake-weighted mean. -- Bound the timestamp correction so that it cannot deviate too far from the - expected theoretical estimate - -## Timestamp Correction - -On every new Bank, the runtime calculates a realistic timestamp estimate using -validator timestamp-oracle data. The Bank timestamp is corrected to this value -if it is greater than or equal to the previous Bank's timestamp. That is, time -should not ever go backward, so that locked up accounts may be released by the -correction, but once released, accounts can never be relocked by a time -correction. - -### Calculating Stake-Weighted Median Timestamp - -In order to calculate the estimated timestamp for a particular Bank, the runtime -first needs to get the most recent vote timestamps from the active validator -set. The `Bank::vote_accounts()` method provides the vote accounts state, and -these can be filtered to all accounts whose most recent timestamp was provided -within the last epoch. - -From each vote timestamp, an estimate for the current Bank is calculated using -the epoch's target ns_per_slot for any delta between the Bank slot and the -timestamp slot. Each timestamp estimate is associated with the stake delegated -to that vote account, and all the timestamps are collected to create a -stake-weighted timestamp distribution. - -From this set, the stake-weighted median timestamp -- that is, the timestamp at -which 50% of the stake estimates a greater-or-equal timestamp and 50% of the -stake estimates a lesser-or-equal timestamp -- is selected as the potential -corrected timestamp. - -This stake-weighted median timestamp is preferred over the stake-weighted mean -because the multiplication of stake by proposed timestamp in the mean -calculation allows a node with very small stake to still have a large effect on -the resulting timestamp by proposing a timestamp that is very large or very -small. For example, using the previous `calculate_stake_weighted_timestamp()` -method, a node with 0.00003% of the stake proposing a timestamp of `i64::MAX` -can shift the timestamp forward 97k years! - -### Bounding Timestamps - -In addition to preventing time moving backward, we can prevent malicious -activity by bounding the corrected timestamp to an acceptable level of deviation -from the theoretical expected time. - -This proposal suggests that each timestamp be allowed to deviate up to 25% from -the expected time since the start of the epoch. - -In order to calculate the timestamp deviation, each Bank needs to log the -`epoch_start_timestamp` in the Clock sysvar. This value is set to the -`Clock::unix_timestamp` on the first slot of each epoch. - -Then, the runtime compares the expected elapsed time since the start of the -epoch with the proposed elapsed time based on the corrected timestamp. If the -corrected elapsed time is within +/- 25% of expected, the corrected timestamp is -accepted. Otherwise, it is bounded to the acceptable deviation. diff --git a/docs/src/implemented-proposals/commitment.md b/docs/src/implemented-proposals/commitment.md deleted file mode 100644 index 43b6f8a7fd8..00000000000 --- a/docs/src/implemented-proposals/commitment.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -title: Commitment ---- - -The commitment metric aims to give clients a measure of the network confirmation -and stake levels on a particular block. Clients can then use this information to -derive their own [measures of commitment](../consensus/commitments.md). - -# Calculation RPC - -Clients can request commitment metrics from a validator for a signature `s` -through `get_block_commitment(s: Signature) -> BlockCommitment` over RPC. The -`BlockCommitment` struct contains an array of u64 `[u64, MAX_CONFIRMATIONS]`. This -array represents the commitment metric for the particular block `N` that -contains the signature `s` as of the last block `M` that the validator voted on. - -An entry `s` at index `i` in the `BlockCommitment` array implies that the -validator observed `s` total stake in the cluster reaching `i` confirmations on -block `N` as observed in some block `M`. There will be `MAX_CONFIRMATIONS` elements in -this array, representing all the possible number of confirmations from 1 to -`MAX_CONFIRMATIONS`. - -# Computation of commitment metric - -Building this `BlockCommitment` struct leverages the computations already being -performed for building consensus. The `collect_vote_lockouts` function in -`consensus.rs` builds a HashMap, where each entry is of the form `(b, s)` -where `s` is the amount of stake on a bank `b`. - -This computation is performed on a votable candidate bank `b` as follows. - -```text - let output: HashMap = HashMap::new(); - for vote_account in b.vote_accounts { - for v in vote_account.vote_stack { - for a in ancestors(v) { - f(*output.get_mut(a), vote_account, v); - } - } - } -``` - -Where `f` is some accumulation function that modifies the `Stake` entry -for slot `a` with some data derivable from vote `v` and `vote_account` -(stake, lockout, etc.). Note here that the `ancestors` here only includes -slots that are present in the current status cache. Signatures for banks earlier -than those present in the status cache would not be queryable anyway, so those -banks are not included in the commitment calculations here. - -Now we can naturally augment the above computation to also build a -`BlockCommitment` array for every bank `b` by: - -1. Adding a `ForkCommitmentCache` to collect the `BlockCommitment` structs -2. Replacing `f` with `f'` such that the above computation also builds this - `BlockCommitment` for every bank `b`. - -We will proceed with the details of 2) as 1) is trivial. - -Before continuing, it is noteworthy that for some validator's vote account `a`, -the number of local confirmations for that validator on slot `s` is -`v.num_confirmations`, where `v` is the smallest vote in the stack of votes -`a.votes` such that `v.slot >= s` (i.e. there is no need to look at any -votes > v as the number of confirmations will be lower). - -Now more specifically, we augment the above computation to: - -```text - let output: HashMap = HashMap::new(); - let fork_commitment_cache = ForkCommitmentCache::default(); - for vote_account in b.vote_accounts { - // vote stack is sorted from oldest vote to newest vote - for (v1, v2) in vote_account.vote_stack.windows(2) { - for a in ancestors(v1).difference(ancestors(v2)) { - f'(*output.get_mut(a), *fork_commitment_cache.get_mut(a), vote_account, v); - } - } - } -``` - -where `f'` is defined as: - -```text - fn f`( - stake: &mut Stake, - some_ancestor: &mut BlockCommitment, - vote_account: VoteAccount, - v: Vote, total_stake: u64 - ){ - f(stake, vote_account, v); - *some_ancestor.commitment[v.num_confirmations] += vote_account.stake; - } -``` diff --git a/docs/src/implemented-proposals/durable-tx-nonces.md b/docs/src/implemented-proposals/durable-tx-nonces.md deleted file mode 100644 index d2de019db5c..00000000000 --- a/docs/src/implemented-proposals/durable-tx-nonces.md +++ /dev/null @@ -1,131 +0,0 @@ ---- -title: Durable Transaction Nonces ---- - -## Problem - -To prevent replay, Solana transactions contain a nonce field populated with a -"recent" blockhash value. A transaction containing a blockhash that is too old -(~2min as of this writing) is rejected by the network as invalid. Unfortunately -certain use cases, such as custodial services, require more time to produce a -signature for the transaction. A mechanism is needed to enable these potentially -offline network participants. - -## Requirements - -1. The transaction's signature needs to cover the nonce value -2. The nonce must not be reusable, even in the case of signing key disclosure - -## A Contract-based Solution - -Here we describe a contract-based solution to the problem, whereby a client can -"stash" a nonce value for future use in a transaction's `recent_blockhash` -field. This approach is akin to the Compare and Swap atomic instruction, -implemented by some CPU ISAs. - -When making use of a durable nonce, the client must first query its value from -account data. A transaction is now constructed in the normal way, but with the -following additional requirements: - -1. The durable nonce value is used in the `recent_blockhash` field -2. An `AdvanceNonceAccount` instruction is the first issued in the transaction - -### Contract Mechanics - -TODO: svgbob this into a flowchart - -```text -Start -Create Account - state = Uninitialized -NonceInstruction - if state == Uninitialized - if account.balance < rent_exempt - error InsufficientFunds - state = Initialized - elif state != Initialized - error BadState - if sysvar.recent_blockhashes.is_empty() - error EmptyRecentBlockhashes - if !sysvar.recent_blockhashes.contains(stored_nonce) - error NotReady - stored_hash = sysvar.recent_blockhashes[0] - success -WithdrawInstruction(to, lamports) - if state == Uninitialized - if !signers.contains(owner) - error MissingRequiredSignatures - elif state == Initialized - if !sysvar.recent_blockhashes.contains(stored_nonce) - error NotReady - if lamports != account.balance && lamports + rent_exempt > account.balance - error InsufficientFunds - account.balance -= lamports - to.balance += lamports - success -``` - -A client wishing to use this feature starts by creating a nonce account under -the system program. This account will be in the `Uninitialized` state with no -stored hash, and thus unusable. - -To initialize a newly created account, an `InitializeNonceAccount` instruction must be -issued. This instruction takes one parameter, the `Pubkey` of the account's -[authority](../cli/examples/durable-nonce.md#nonce-authority). Nonce accounts -must be [rent-exempt](./rent.md#two-tiered-rent-regime) to meet the data-persistence -requirements of the feature, and as such, require that sufficient lamports be -deposited before they can be initialized. Upon successful initialization, the -cluster's most recent blockhash is stored along with specified nonce authority -`Pubkey`. - -The `AdvanceNonceAccount` instruction is used to manage the account's stored nonce -value. It stores the cluster's most recent blockhash in the account's state data, -failing if that matches the value already stored there. This check prevents -replaying transactions within the same block. - -Due to nonce accounts' [rent-exempt](./rent.md#two-tiered-rent-regime) requirement, -a custom withdraw instruction is used to move funds out of the account. -The `WithdrawNonceAccount` instruction takes a single argument, lamports to withdraw, -and enforces rent-exemption by preventing the account's balance from falling -below the rent-exempt minimum. An exception to this check is if the final balance -would be zero lamports, which makes the account eligible for deletion. This -account closure detail has an additional requirement that the stored nonce value -must not match the cluster's most recent blockhash, as per `AdvanceNonceAccount`. - -The account's [nonce authority](../cli/examples/durable-nonce.md#nonce-authority) -can be changed using the `AuthorizeNonceAccount` instruction. It takes one parameter, -the `Pubkey` of the new authority. Executing this instruction grants full -control over the account and its balance to the new authority. - -> `AdvanceNonceAccount`, `WithdrawNonceAccount` and `AuthorizeNonceAccount` all require the current [nonce authority](../cli/examples/durable-nonce.md#nonce-authority) for the account to sign the transaction. - -### Runtime Support - -The contract alone is not sufficient for implementing this feature. To enforce -an extant `recent_blockhash` on the transaction and prevent fee theft via -failed transaction replay, runtime modifications are necessary. - -Any transaction failing the usual `check_hash_age` validation will be tested -for a Durable Transaction Nonce. This is signaled by including a `AdvanceNonceAccount` -instruction as the first instruction in the transaction. - -If the runtime determines that a Durable Transaction Nonce is in use, it will -take the following additional actions to validate the transaction: - -1. The `NonceAccount` specified in the `Nonce` instruction is loaded. -2. The `NonceState` is deserialized from the `NonceAccount`'s data field and - confirmed to be in the `Initialized` state. -3. The nonce value stored in the `NonceAccount` is tested to match against the - one specified in the transaction's `recent_blockhash` field. - -If all three of the above checks succeed, the transaction is allowed to continue -validation. - -Since transactions that fail with an `InstructionError` are charged a fee and -changes to their state rolled back, there is an opportunity for fee theft if an -`AdvanceNonceAccount` instruction is reverted. A malicious validator could replay the -failed transaction until the stored nonce is successfully advanced. Runtime -changes prevent this behavior. When a durable nonce transaction fails with an -`InstructionError` aside from the `AdvanceNonceAccount` instruction, the nonce account -is rolled back to its pre-execution state as usual. Then the runtime advances -its nonce value and the advanced nonce account stored as if it succeeded. diff --git a/docs/src/implemented-proposals/ed_overview/ed_economic_sustainability.md b/docs/src/implemented-proposals/ed_overview/ed_economic_sustainability.md deleted file mode 100644 index 6e407be262d..00000000000 --- a/docs/src/implemented-proposals/ed_overview/ed_economic_sustainability.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -title: Economic Sustainability ---- - -**Subject to change.** - -Long term economic sustainability is one of the guiding principles of Solana’s economic design. While it is impossible to predict how decentralized economies will develop over time, especially economies with flexible decentralized governances, we can arrange economic components such that, under certain conditions, a sustainable economy may take shape in the long term. In the case of Solana’s network, these components take the form of token issuance \(via inflation\) and token burning. - -The dominant remittances from the Solana mining pool are validator rewards. The disinflationary mechanism is a flat, protocol-specified and adjusted, % of each transaction fee. diff --git a/docs/src/implemented-proposals/ed_overview/ed_mvp.md b/docs/src/implemented-proposals/ed_overview/ed_mvp.md deleted file mode 100644 index bb7cc87bcc7..00000000000 --- a/docs/src/implemented-proposals/ed_overview/ed_mvp.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -title: Economic Design MVP ---- - -**Subject to change.** - -The preceding sections, outlined in the -[Economic Design Overview](ed_overview.md), -describe a long-term vision of a sustainable Solana economy. -Of course, we don't expect the final implementation to perfectly match what has -been described above. We intend to fully engage with network stakeholders -throughout the implementation phases \(i.e. pre-testnet, testnet, mainnet\) -to ensure the system supports, and is representative of, the various network -participants' interests. The first step toward this goal, however, is outlining -some desired MVP economic features to be available for early pre-testnet and -testnet participants. Below is a rough sketch outlining basic economic -functionality from which a more complete and functional system can be developed. - -## MVP Economic Features - -- Faucet to deliver testnet SOLs to validators for staking and application development. -- Mechanism by which validators are rewarded via network inflation. -- Ability to delegate tokens to validator nodes -- Validator set commission fees on interest from delegated tokens. diff --git a/docs/src/implemented-proposals/ed_overview/ed_overview.md b/docs/src/implemented-proposals/ed_overview/ed_overview.md deleted file mode 100644 index 000ff2adbe6..00000000000 --- a/docs/src/implemented-proposals/ed_overview/ed_overview.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -title: Cluster Economics ---- - -**Subject to change.** - -Solana’s crypto-economic system is designed to promote a healthy, long term self-sustaining economy with participant incentives aligned to the security and decentralization of the network. The main participants in this economy are validation-clients. Their contributions to the network, state validation, and their requisite incentive mechanisms are discussed below. - -The main channels of participant remittances are referred to as protocol-based (inflationary) rewards and transaction fees. Protocol-based rewards are issuances from a global, protocol-defined, inflation rate. These rewards will constitute the total reward delivered to validation clients, the remaining sourced from transaction fees. In the early days of the network, it is likely that protocol-based rewards, deployed based on predefined issuance schedule, will drive the majority of participant incentives to participate in the network. - -These protocol-based rewards, to be distributed across the actively staked tokens on the network, are to be a result of a global supply inflation rate, calculated per Solana epoch and distributed amongst the active validator set. As discussed further below, the per annum inflation rate is based on a pre-determined disinflationary schedule. This provides the network with monetary supply predictability which supports long term economic stability and security. - -Transaction fees are market-based participant-to-participant transfers, attached to network interactions as a necessary motivation and compensation for the inclusion and execution of a proposed transaction. A mechanism for long-term economic stability and forking protection through partial burning of each transaction fee is also discussed below. - -A high-level schematic of Solana’s crypto-economic design is shown below in **Figure 1**. The specifics of validation-client economics are described in sections: [Validation-client Economics](ed_validation_client_economics/ed_vce_overview.md), [Inflation Schedule](ed_validation_client_economics/ed_vce_state_validation_protocol_based_rewards.md), and [Transaction Fees](ed_validation_client_economics/ed_vce_state_validation_transaction_fees.md). Also, the section titled [Validation Stake Delegation](ed_validation_client_economics/ed_vce_validation_stake_delegation.md) closes with a discussion of validator delegation opportunities and marketplace. Additionally, in [Storage Rent Economics](ed_storage_rent_economics.md), we describe an implementation of storage rent to account for the externality costs of maintaining the active state of the ledger. An outline of features for an MVP economic design is discussed in the [Economic Design MVP](ed_mvp.md) section. - -![](/img/economic_design_infl_230719.png) - -**Figure 1**: Schematic overview of Solana economic incentive design. diff --git a/docs/src/implemented-proposals/ed_overview/ed_references.md b/docs/src/implemented-proposals/ed_overview/ed_references.md deleted file mode 100644 index 4c9047ae381..00000000000 --- a/docs/src/implemented-proposals/ed_overview/ed_references.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: References ---- - -1. [https://blog.ethereum.org/2016/07/27/inflation-transaction-fees-cryptocurrency-monetary-policy/](https://blog.ethereum.org/2016/07/27/inflation-transaction-fees-cryptocurrency-monetary-policy/) -2. [https://medium.com/solana-labs/how-to-create-decentralized-storage-for-a-multi-petabyte-digital-ledger-2499a3a8c281](https://medium.com/solana-labs/how-to-create-decentralized-storage-for-a-multi-petabyte-digital-ledger-2499a3a8c281) -3. [https://medium.com/solana-labs/how-to-create-decentralized-storage-for-a-multi-petabyte-digital-ledger-2499a3a8c281](https://medium.com/solana-labs/how-to-create-decentralized-storage-for-a-multi-petabyte-digital-ledger-2499a3a8c281) diff --git a/docs/src/implemented-proposals/ed_overview/ed_storage_rent_economics.md b/docs/src/implemented-proposals/ed_overview/ed_storage_rent_economics.md deleted file mode 100644 index 8abd88ad2c5..00000000000 --- a/docs/src/implemented-proposals/ed_overview/ed_storage_rent_economics.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: Storage Rent Economics ---- - -Each transaction that is submitted to the Solana ledger imposes costs. Transaction fees paid by the submitter, and collected by a validator, in theory, account for the acute, transactional, costs of validating and adding that data to the ledger. Unaccounted in this process is the mid-term storage of active ledger state, necessarily maintained by the rotating validator set. This type of storage imposes costs not only to validators but also to the broader network as active state grows so does data transmission and validation overhead. To account for these costs, we describe here our preliminary design and implementation of storage rent. - -Storage rent can be paid via one of two methods: - -Method 1: Set it and forget it - -With this approach, accounts with two-years worth of rent deposits secured are exempt from network rent charges. By maintaining this minimum-balance, the broader network benefits from reduced liquidity and the account holder can rest assured that their `Account::data` will be retained for continual access/usage. - -Method 2: Pay per byte - -If an account has less than two-years worth of deposited rent the network charges rent on a per-epoch basis, in credit for the next epoch. This rent is deducted at a rate specified in genesis, in lamports per kilobyte-year. - -For information on the technical implementation details of this design, see the [Rent](../rent.md) section. diff --git a/docs/src/implemented-proposals/ed_overview/ed_validation_client_economics/ed_vce_overview.md b/docs/src/implemented-proposals/ed_overview/ed_validation_client_economics/ed_vce_overview.md deleted file mode 100644 index 89478eda517..00000000000 --- a/docs/src/implemented-proposals/ed_overview/ed_validation_client_economics/ed_vce_overview.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -title: Validation-client Economics ---- - -**Subject to change.** - -Validator-clients are eligible to charge commission on inflationary rewards distributed to staked tokens. This compensation is for providing compute resources to validate and vote on a given PoH state. These protocol-based rewards are determined through an algorithmic disinflationary schedule as a function of total token supply. The network is expected to launch with an annual inflation rate around 8%, set to decrease by 15% per year until a long-term stable rate of 1.5% is reached, however these parameters are yet to be finalized by the community. These issuances are to be split and distributed to participating validators, with around 95% of the issued tokens allocated for validator rewards initial (the remaining 5% reserved for Foundation operating expenses). Because the network will be distributing a fixed amount of inflation rewards across the stake-weighted validator set, the yield observed for staked tokens will be primarily a function of the amount of staked tokens in relation to the total token supply. - -Additionally, validator clients may earn revenue through fees via state-validation transactions. For clarity, we separately describe the design and motivation of these revenue distributions for validation-clients below: state-validation protocol-based rewards and state-validation transaction fees and rent. diff --git a/docs/src/implemented-proposals/ed_overview/ed_validation_client_economics/ed_vce_state_validation_protocol_based_rewards.md b/docs/src/implemented-proposals/ed_overview/ed_validation_client_economics/ed_vce_state_validation_protocol_based_rewards.md deleted file mode 100644 index e5c3d10380b..00000000000 --- a/docs/src/implemented-proposals/ed_overview/ed_validation_client_economics/ed_vce_state_validation_protocol_based_rewards.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -title: Inflation Schedule ---- - -**Subject to change.** - -Validator-clients have two functional roles in the Solana network: - -- Validate \(vote\) the current global state of their observed PoH. -- Be elected as ‘leader’ on a stake-weighted round-robin schedule during which time they are responsible for collecting outstanding transactions and incorporating them into their observed PoH, thus updating the global state of the network and providing chain continuity. - -Validator-client rewards for these services are to be distributed at the end of each Solana epoch. As previously discussed, compensation for validator-clients is provided via a commission charged on the protocol-based annual inflation rate dispersed in proportion to the stake-weight of each validator-node \(see below\) along with leader-claimed transaction fees available during each leader rotation. I.e. during the time a given validator-client is elected as leader, it has the opportunity to keep a portion of each transaction fee, less a protocol-specified amount that is destroyed \(see [Validation-client State Transaction Fees](ed_vce_state_validation_transaction_fees.md)\). - -The effective protocol-based annual staking yield \(%\) per epoch received by validation-clients is to be a function of: - -- the current global inflation rate, derived from the pre-determined disinflationary issuance schedule \(see [Validation-client Economics](ed_vce_overview.md)\) -- the fraction of staked SOLs out of the current total circulating supply, -- the commission charged by the validation service, -- the up-time/participation \[% of available slots that validator had opportunity to vote on\] of a given validator over the previous epoch. - -The first factor is a function of protocol parameters only \(i.e. independent of validator behavior in a given epoch\) and results in an inflation schedule designed to incentivize early participation, provide clear monetary stability and provide optimal security in the network. - -As a first step to understanding the impact of the _Inflation Schedule_ on the Solana economy, we’ve simulated the upper and lower ranges of what token issuance over time might look like given the current ranges of Inflation Schedule parameters under study. - -Specifically: - -- _Initial Inflation Rate_: 7-9% -- _Disinflation Rate_: -14-16% -- _Long-term Inflation Rate_: 1-2% - -Using these ranges to simulate a number of possible Inflation Schedules, we can explore inflation over time: - -![](/img/p_inflation_schedule_ranges_w_comments.png) - -In the above graph, the average values of the range are identified to illustrate the contribution of each parameter. -From these simulated _Inflation Schedules_, we can also project ranges for token issuance over time. - -![](/img/p_total_supply_ranges.png) - -Finally we can estimate the _Staked Yield_ on staked SOL, if we introduce an additional parameter, previously discussed, _% of Staked SOL_: - -$$ -\%~\text{SOL Staked} = \frac{\text{Total SOL Staked}}{\text{Total Current Supply}} -$$ - -In this case, because _% of Staked SOL_ is a parameter that must be estimated (unlike the _Inflation Schedule_ parameters), it is easier to use specific _Inflation Schedule_ parameters and explore a range of _% of Staked SOL_. For the below example, we’ve chosen the middle of the parameter ranges explored above: - -- _Initial Inflation Rate_: 8% -- _Disinflation Rate_: -15% -- _Long-term Inflation Rate_: 1.5% - -The values of _% of Staked SOL_ range from 60% - 90%, which we feel covers the likely range we expect to observe, based on feedback from the investor and validator communities as well as what is observed on comparable Proof-of-Stake protocols. - -![](/img/p_ex_staked_yields.png) - -Again, the above shows an example _Staked Yield_ that a staker might expect over time on the Solana network with the _Inflation Schedule_ as specified. This is an idealized _Staked Yield_ as it neglects validator uptime impact on rewards, validator commissions, potential yield throttling and potential slashing incidents. It additionally ignores that _% of Staked SOL_ is dynamic by design - the economic incentives set up by this _Inflation Schedule_. - -### Adjusted Staking Yield - -A complete appraisal of earning potential from staking tokens should take into account staked _Token Dilution_ and its impact on staking yield. For this, we define _adjusted staking yield_ as the change in fractional token supply ownership of staked tokens due to the distribution of inflation issuance. I.e. the positive dilutive effects of inflation. - -We can examine the _adjusted staking yield_ as a function of the inflation rate and the percent of staked tokens on the network. We can see this plotted for various staking fractions here: - -![](/img/p_ex_staked_dilution.png) diff --git a/docs/src/implemented-proposals/ed_overview/ed_validation_client_economics/ed_vce_state_validation_transaction_fees.md b/docs/src/implemented-proposals/ed_overview/ed_validation_client_economics/ed_vce_state_validation_transaction_fees.md deleted file mode 100644 index 9022e42d7c0..00000000000 --- a/docs/src/implemented-proposals/ed_overview/ed_validation_client_economics/ed_vce_state_validation_transaction_fees.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -title: State-validation Transaction Fees ---- - -**Subject to change.** - -Each transaction sent through the network, to be processed by the current leader validation-client and confirmed as a global state transaction, must contain a transaction fee. Transaction fees offer many benefits in the Solana economic design, for example they: - -- provide unit compensation to the validator network for the compute resources necessary to process the state transaction, -- reduce network spam by introducing real cost to transactions, -- open avenues for a transaction market to incentivize validation-client to collect and process submitted transactions in their function as leader, -- and provide potential long-term economic stability of the network through a protocol-captured minimum fee amount per transaction, as described below. - -Many current blockchain economies \(e.g. Bitcoin, Ethereum\), rely on protocol-based rewards to support the economy in the short term, with the assumption that the revenue generated through transaction fees will support the economy in the long term, when the protocol derived rewards expire. In an attempt to create a sustainable economy through protocol-based rewards and transaction fees, a fixed portion of each transaction fee is destroyed, with the remaining fee going to the current leader processing the transaction. A scheduled global inflation rate provides a source for rewards distributed to validation-clients, through the process described above. - -Transaction fees are set by the network cluster based on recent historical throughput, see [Congestion Driven Fees](../../transaction-fees.md#congestion-driven-fees). This minimum portion of each transaction fee can be dynamically adjusted depending on historical gas usage. In this way, the protocol can use the minimum fee to target a desired hardware utilization. By monitoring a protocol specified gas usage with respect to a desired, target usage amount, the minimum fee can be raised/lowered which should, in turn, lower/raise the actual gas usage per block until it reaches the target amount. This adjustment process can be thought of as similar to the difficulty adjustment algorithm in the Bitcoin protocol, however in this case it is adjusting the minimum transaction fee to guide the transaction processing hardware usage to a desired level. - -As mentioned, a fixed-proportion of each transaction fee is to be destroyed. The intent of this design is to retain leader incentive to include as many transactions as possible within the leader-slot time, while providing an inflation limiting mechanism that protects against "tax evasion" attacks \(i.e. side-channel fee payments\)[1](../ed_references.md). - -Additionally, the burnt fees can be a consideration in fork selection. In the case of a PoH fork with a malicious, censoring leader, we would expect the total fees destroyed to be less than a comparable honest fork, due to the fees lost from censoring. If the censoring leader is to compensate for these lost protocol fees, they would have to replace the burnt fees on their fork themselves, thus potentially reducing the incentive to censor in the first place. diff --git a/docs/src/implemented-proposals/ed_overview/ed_validation_client_economics/ed_vce_validation_stake_delegation.md b/docs/src/implemented-proposals/ed_overview/ed_validation_client_economics/ed_vce_validation_stake_delegation.md deleted file mode 100644 index 64ab8779260..00000000000 --- a/docs/src/implemented-proposals/ed_overview/ed_validation_client_economics/ed_vce_validation_stake_delegation.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -title: Validation Stake Delegation ---- - -**Subject to change.** - -Running a Solana validation-client required relatively modest upfront hardware capital investment. **Table 2** provides an example hardware configuration to support ~1M tx/s with estimated ‘off-the-shelf’ costs: - -| Component | Example | Estimated Cost | -| :---------------- | :----------------------------------------------- | :------------- | -| GPU | 2x 2080 Ti | \$2500 | -| or | 4x 1080 Ti | \$2800 | -| OS/Ledger Storage | Samsung 860 Evo 2TB | \$370 | -| Accounts storage | 2x Samsung 970 Pro M.2 512GB | \$340 | -| RAM | 32 Gb | \$300 | -| Motherboard | AMD x399 | \$400 | -| CPU | AMD Threadripper 2920x | \$650 | -| Case | | \$100 | -| Power supply | EVGA 1600W | \$300 | -| Network | > 500 mbps | | -| Network \(1\) | Google webpass business bay area 1gbps unlimited | \$5500/mo | -| Network \(2\) | Hurricane Electric bay area colo 1gbps | \$500/mo | - -**Table 2** example high-end hardware setup for running a Solana client. - -Despite the low-barrier to entry as a validation-client, from a capital investment perspective, as in any developing economy, there will be much opportunity and need for competent validation services as evidenced by node reliability, UX/UI, APIs and other software accessibility tools. Additionally, although Solana’s validator node startup costs are nominal when compared to similar networks, they may still be somewhat restrictive for some potential participants. In the spirit of developing a true decentralized, permissionless network, these interested parties can become involved in the Solana network/economy via delegation of previously acquired tokens with a reliable validation node to earn a portion of the interest generated. - -Delegation of tokens to validation-clients provides a way for passive Solana token holders to become part of the active Solana economy and earn interest rates proportional to the interest rate generated by the delegated validation-client. Additionally, this feature intends to create a healthy validation-client market, with potential validation-client nodes competing to build reliable, transparent and profitable delegation services. diff --git a/docs/src/implemented-proposals/epoch_accounts_hash.md b/docs/src/implemented-proposals/epoch_accounts_hash.md deleted file mode 100644 index ba16f3f869b..00000000000 --- a/docs/src/implemented-proposals/epoch_accounts_hash.md +++ /dev/null @@ -1,331 +0,0 @@ ---- -title: Epoch Accounts Hash ---- - -*Paraphrasing from https://github.com/solana-labs/solana/issues/26847* - -## Background - -Rent collection checks every account at least once per epoch. At each slot, a -deterministic set of accounts (based on the pubkey range) is loaded, checked -for rent collection, and stored back to the Accounts DB at this new slot. - -Accounts are stored (rewritten) _even if_ they are unchanged. This has a few -positive effects. - 1. Once an account is rewritten, the previous version at an older slot is now - dead. As entire slots and AppendVecs become full of only dead accounts, - they can then be dropped/recycled. - 2. Each account rewritten due to rent collection is included in that slot's - bank hash. Since the bank hash is part of what is voted on for consensus, - this means every account is verified by the network at least once per - epoch. - -However, there is a big downside to rewriting unchanged accounts: performance. -Storing accounts can be very expensive. And since accounts now are required to -be rent-exempt, the majority of accounts rewritten due to rent collection are -unchanged. What if unchanged accounts were no longer rewritten? This would -minimally be a big performance win. - - -## Problem - -If rent collection no longer is rewriting unchanged accounts, we lose the two -positive effects. Dealing with Positive Effect 1 (from above) will be handled -by _Ancient AppendVecs_, and will not be discussed here. So how do we still -get the security from Positive Effect 2? How can we still verify every account -at least once per epoch, *as part of consensus*, but without rewriting -accounts? - - -## Proposed Solution - -Perform a full accounts hash calculation once per epoch, and hash the result -into a bank's `hash`. This will be known as the _Epoch Accounts Hash_, or -_EAH_. - -This retains the Positive Effect 2 from rent collection by checking every -account at least once per epoch. Thus, any validators with missing, corrupt, -or extra accounts will identify those issues within 1-2 epochs. - - -### Implementation - -Performing a full accounts hash takes a relatively long time to complete. For -this reason, the EAH calculation must take place in the background. - -In order for all the validators to calculate the same accounts hash, the -calculation must be based on the same view of all accounts. This means the EAH -must be based on a predetermined slot. This will be known as the `start slot`. -The `start slot` is calculated as an offset into the epoch. This offset will -be known as the `start offset`. Formally, the `start slot` is the first root -*greater-than-or-equal-to* `first slot in epoch + start offset`. - -Similarly, all the validators must save the EAH into a bank at a predetermined -slot, and offset from the first slot of an epoch. This will be known as the -`stop slot` and `stop offset`, respectively. - -* The `start offset` will be set at one-quarter into the epoch. -* The `stop offset` will be set at three-quarters into the epoch. -* For epochs with 432,000 slots, the `start offset` will be 108,000 and the - `stop offset` will be 324,000. - -These constants may be changed in the future, or may be determined at runtime. -The main justifications for these values are: -1. Do not start the EAH calculation at the beginning of an epoch, as the - beginning of an epoch is already a time of contention and stress. There is - no reason to make this worse. -2. The bank to save the EAH into—the `stop offset`—should be sufficiently far - in the future to guarantee all validators are able to complete the accounts - hash calculation in time. -3. The `start offset` should be *after* the `rewarding interval` - (from [Partitioned Inflationary Rewards Distribution](https://github.com/solana-labs/solana/pull/27455)). - This ensures stake rewards have been distributed and stored into the - accounts for this epoch. - -Once the EAH calculation is complete, it must be saved somewhere. Since this -occurs in the background, there is not an associated `Bank` that would make -sense to save into. Instead, a new field will be added to `AccountsDb` that -will store the EAH. Later, the bank at slot `stop slot`†¹ will read the EAH from -`AccountsDb` and hash it into its own hash (aka _bank hash_). - -EAH calculation will use the existing _accounts background services_ (_ABS_) to -perform the actual calculation. Requests for EAH calculation will be sent from -`bank_forks::set_root()`†², with a new request type to distinguish an EAH request -from a Snapshot request. Since the EAH will be part of consensus, it is not -optional; EAH requests will have the highest priority in ABS, and will be -processed first/instead of other requests. - -†¹: More precisely, all banks where `bank slot >= stop slot` and `parent slot < - stop slot` will include the EAH in their _bank hash_. This ensures EAH - handles forking around `stop slot`, since only one of these banks will end - up rooted. - -†²: An EAH calculation will be requested when `root bank slot >= start slot` - and `root parent slot < start slot`. This handles the scenario where - validators call `bank_forks::set_root()` at different intervals. - - -#### Details - -#### Snapshots - -A snapshot contains all the state necessary to reconstruct the cluster as of a -certain slot. A snapshot may then need to contain the EAH so that the `stop -slot` can include it. Consider the following scenarios within an epoch where a -snapshot is requested for slot `X`: - - -##### 1. `X >= first slot in epoch` and `X < start slot` - -Since the `start slot` has not been reached yet, there is nothing special to do -in order to take a snapshot in this scenario. - - -##### 2. `X == start slot` - -The EAH *must* be included in the snapshot. Since the snapshot process always -calculates the accounts hash, no additional calculations are required. The -accounts hash calculation result will be used both to store in the snapshot as -the EAH, and for the snapshot hash (which is used at load-time for verification). - - -##### 3. `X > start slot` and `X < stop slot` - -If a snapshot is requested to be created *after* the `start slot` but *before* -the EAH calculation has completed, then it will be impossible to create a -snapshot with the correct EAH. The snapshot process will wait until the EAH -calculation has completed before proceeding. - - -##### 4. `X == stop slot` - -The EAH has been calculated for this epoch, and has been included in the `stop -slot` bank. No further handling is required; the snapshot does not need to -contain the EAH. - - -##### 5. `X > stop slot` and `X <= last slot in epoch` - -Same as (4). - - -#### Snapshot Verification - -If a snapshot archive includes an EAH, we want to verify the EAH is correct at -load time (instead of waiting until `stop slot`, which could be far in the -future). - -If the snapshot archive is for a slot within the `calculation window`†¹, then it -*must* include an EAH. The snapshot hash itself will now also incorporate the -EAH. In pseudo code: -```pseudo -if slot is in calculation window - let snapshot hash = hash(accounts hash, epoch accounts hash) -else - let snapshot hash = accounts hash -endif -``` -Since loading from a snapshot archive already verifies the snapshot archive's -hash against the deserialized bank, the EAH will be implicitly verified as -well. - -†¹: The `calculation window` is `[start slot, stop slot)`, based on the epoch - of the referenced `Bank`. - - -#### Corner Cases - -#### Minimum Slots per Epoch - -An EAH is requested by `BankForks::set_root()`, which happens while setting -*roots*. The EAH is stored into `Bank`s when they are *frozen*. Banks are -frozen *at least* 32 slots before they are rooted. For the expected behavior, -the EAH start slot really should be 32 slots before the stop slot. If the -number of slots per epoch is small, this can result in surprising behavior. - -Example: Assume there are 40 slots per epoch. The EAH start offset is 10, and -the EAH stop offset is 30. When Bank 30 is frozen it will include the EAH in -its hash. However, Bank 10 has not yet been rooted, so a new EAH has not been -calculated for this epoch. This means Bank 30 will have included the EAH *from -the previous epoch* in its hash. - -Later, when Bank 10 is rooted, it will request a new EAH be calculated. If a -snapshot is taken for Bank 12 (or any bank between 10 and 30), it will include -the EAH *from this epoch*. If a node loads the snapshot from Bank 12, once it -gets to freezing Bank 30, it will end up with a different bank hash since it -included the EAH from this epoch (versus the other node's Bank 30 included the -EAH from the previous epoch). Different bank hashes will result in consensus -failures. - -The above example is clearly bad. It can be observed that short epochs only -occur (1) during warmup, or (2) in tests. Real clusters have much longer -epochs (432,000 slots by default). - -Tests can be fixed as needed; that leaves fixing warmup. Since warmup is -transient, we disable EAH until slots-per-epoch is large enough. More -precisely, we disable EAH until the `calculation window` is big enough. During -warmup, slots-per-epoch doubles each epoch until reaching the desired number, -so only a few epochs will skip EAH (which also is a small total number of -slots). - - -#### Warping - -Warping introduces corner cases into EAH because many slots may be skipped, -including the entire range of `start slot` to `stop slot`. - -Now, a new EAH will always be calculated based on the parent. Thus, if the -warp slot is `stop slot` or greater (and the parent slot is less than `stop -slot`), the warp bank will include this newly-calculated EAH. This is safe -because warping cannot be used on a live cluster; only for a new cluster or -tests/debugging. Therefore _when_ the EAH was calculated is not germane. - -For specific examples, refer to Appendix A. - - -#### Implementation Alternatives - -##### Perform the EAH calculation in the foreground - -The accounts hash calculation takes around 15 seconds (median, on Mainnet-Beta -today). This is far beyond the slot time; this would be bad UX, and also -decrease network stability. - - -##### Remove `stop offset` - -Instead of having two offsets—one for `start` and one for `stop`—use a single -offset for both. This delays when the EAH is saved into a Bank and voted on. -The saved EAH is now the EAH from the previous epoch. This could work; would -reduce the number of "special" slots from two to one. No significant -advantages observed. - - -##### Send EAH requests when making a new bank, instead of a new root - -When a bank is created, we don't yet know if it will be finalized until it is -rooted, which could result in multiple EAH requests due to forking. This would -be bad for performance. - - -### Appendix A: All Warping Scenarios - -To enumerate how EAH interacts with warping, refer to the following diagram for -the scenarios below: - -```text - +---------+-----------------+-----------+---------+-----------------+-----------+ - | > < | > < | - | A > B < C | D > E < F | - | > < | > < | - +---------+-----------------+-----------+---------+-----------------+-----------+ - | | | | | | | - v v v v v v v - epoch 1 start slot 1 stop slot 1 epoch 2 start slot 2 stop slot 2 epoch 3 -``` - - -#### parent slot: `A`, warp slot: `A` - -No slots important to the EAH have been skipped, so no change in behavior. - - -#### parent slot: `A`, warp slot: `B` - -An EAH calculation will be requested when the warp slot is rooted, and then -will be included in the Bank at `slot slot 1`; behavior is unchanged. - - -#### parent slot: `A`, warp slot: `C` or `D` - -The entire EAH range has been skipped; no new EAH calculation will have been -requested for `start slot 1`. The warp slot will include the EAH calculated at -the parent slot. This is different from the normal behavior. - - -#### parent slot: `A`, warp slot: `E` - -Similar to `A -> C`, the warp slot will include the EAH calculated at the -parent slot. This behavior appears different. - -Then when the warp slot is rooted, a new EAH calculation will be requested, -which will be included in `stop slot 2`. This behavior is expected. - - -#### parent slot: `A`, warp slot: `F` - -Similar to `A -> C`, no new EAH calculation will be requested. The warp slot -will include the EAH calculated at the parent slot. This is different from the -normal behavior. - - -#### parent slot: `B`, warp slot: `B` - -A new EAH will be calculated at parent slot, which will be included in `stop -slot 1`, _not_ the previously-calculated EAH from `start slot 1`. This behavior -is different. - - -#### parent slot: `B`, warp slot: `C` or `D` - -The warp slot will include the EAH calculated at the parent slot, _not_ the -previously-calculated EAH from `start slot 1`. This is different from normal -behavior. - - -#### parent slot: `B`, warp slot: `E` - -Similar to `B -> C`, the warp slot will include the EAH calculated at the parent -slot, _not_ the previously-calculated EAH from `start slot 1`. This is -different from normal behavior. - -And similar to `A -> E`, when the warp slot is rooted, a new EAH calculation -will be requested, which will be included in `stop slot 2`. This behavior is -expected. - - -#### parent slot: `B`, warp slot: `F` - -Similar to `B -> C`, the warp slot will include the EAH calculated at the parent -slot, _not_ the previously-calculated EAH from `start slot 1`. This is -different from normal behavior. diff --git a/docs/src/implemented-proposals/index.md b/docs/src/implemented-proposals/index.md deleted file mode 100644 index 5dd1bc16fe0..00000000000 --- a/docs/src/implemented-proposals/index.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: Implemented Design Proposals -sidebar_position: 1 -sidebar_label: Overview ---- - -These architectural proposals have been accepted and implemented by the Solana maintainers. Any designs that may be subject to future change are noted in the specific proposal page. - -## Not Yet Implemented - -Design proposals that have been accepted but not yet implemented are found in [Accepted Proposals](../proposals/accepted-design-proposals.md). - -## Submit a New Proposal - -To submit a new design proposal, consult this guide on [how to submit a design proposal](../proposals.md#submit-a-design-proposal). diff --git a/docs/src/implemented-proposals/installer.md b/docs/src/implemented-proposals/installer.md deleted file mode 100644 index f61c5fab844..00000000000 --- a/docs/src/implemented-proposals/installer.md +++ /dev/null @@ -1,222 +0,0 @@ ---- -title: Cluster Software Installation and Updates ---- - -Currently users are required to build the solana cluster software themselves from the git repository and manually update it, which is error prone and inconvenient. - -This document proposes an easy to use software install and updater that can be used to deploy pre-built binaries for supported platforms. Users may elect to use binaries supplied by Solana or any other party provider. Deployment of updates is managed using an on-chain update manifest program. - -## Motivating Examples - -### Fetch and run a pre-built installer using a bootstrap curl/shell script - -The easiest install method for supported platforms: - -```bash -$ curl -sSf https://raw.githubusercontent.com/solana-labs/solana/v1.0.0/install/agave-install-init.sh | sh -``` - -This script will check github for the latest tagged release and download and run the `agave-install-init` binary from there. - -If additional arguments need to be specified during the installation, the following shell syntax is used: - -```bash -$ init_args=.... # arguments for `agave-install-init ...` -$ curl -sSf https://raw.githubusercontent.com/solana-labs/solana/v1.0.0/install/agave-install-init.sh | sh -s - ${init_args} -``` - -### Fetch and run a pre-built installer from a Github release - -With a well-known release URL, a pre-built binary can be obtained for supported platforms: - -```bash -$ curl -o agave-install-init https://github.com/solana-labs/solana/releases/download/v1.0.0/agave-install-init-x86_64-apple-darwin -$ chmod +x ./agave-install-init -$ ./agave-install-init --help -``` - -### Build and run the installer from source - -If a pre-built binary is not available for a given platform, building the installer from source is always an option: - -```bash -$ git clone https://github.com/solana-labs/solana.git -$ cd solana/install -$ cargo run -- --help -``` - -### Deploy a new update to a cluster - -Given a solana release tarball \(as created by `ci/publish-tarball.sh`\) that has already been uploaded to a publicly accessible URL, the following commands will deploy the update: - -```bash -$ solana-keygen new -o update-manifest.json # <-- only generated once, the public key is shared with users -$ agave-install deploy http://example.com/path/to/solana-release.tar.bz2 update-manifest.json -``` - -### Run a validator node that auto updates itself - -```bash -$ agave-install init --pubkey 92DMonmBYXwEMHJ99c9ceRSpAmk9v6i3RdvDdXaVcrfj # <-- pubkey is obtained from whoever is deploying the updates -$ export PATH=~/.local/share/agave-install/bin:$PATH -$ solana-keygen ... # <-- runs the latest solana-keygen -$ agave-install run agave-validator ... # <-- runs a validator, restarting it as necessary when an update is applied -``` - -## On-chain Update Manifest - -An update manifest is used to advertise the deployment of new release tarballs on a solana cluster. The update manifest is stored using the `config` program, and each update manifest account describes a logical update channel for a given target triple \(eg, `x86_64-apple-darwin`\). The account public key is well-known between the entity deploying new updates and users consuming those updates. - -The update tarball itself is hosted elsewhere, off-chain and can be fetched from the specified `download_url`. - -```text -use solana_signature::Signature; - -/// Information required to download and apply a given update -pub struct UpdateManifest { - pub timestamp_secs: u64, // When the release was deployed in seconds since UNIX EPOCH - pub download_url: String, // Download URL to the release tar.bz2 - pub download_sha256: String, // SHA256 digest of the release tar.bz2 file -} - -/// Data of an Update Manifest program Account. -#[derive(Serialize, Deserialize, Default, Debug, PartialEq)] -pub struct SignedUpdateManifest { - pub manifest: UpdateManifest, - pub manifest_signature: Signature, -} -``` - -Note that the `manifest` field itself contains a corresponding signature \(`manifest_signature`\) to guard against man-in-the-middle attacks between the `agave-install` tool and the solana cluster RPC API. - -To guard against rollback attacks, `agave-install` will refuse to install an update with an older `timestamp_secs` than what is currently installed. - -## Release Archive Contents - -A release archive is expected to be a tar file compressed with bzip2 with the following internal structure: - -- `/version.yml` - a simple YAML file containing the field `"target"` - the - - target tuple. Any additional fields are ignored. - -- `/bin/` -- directory containing available programs in the release. - - `agave-install` will symlink this directory to - - `~/.local/share/agave-install/bin` for use by the `PATH` environment - - variable. - -- `...` -- any additional files and directories are permitted - -## agave-install Tool - -The `agave-install` tool is used by the user to install and update their cluster software. - -:::info -As of v3.0 `agave-install` does not install the `agave-validator` binary, which is required to run a validator node. -Validator operators are required to [build from source](../cli/install.md#build-from-source). - -::: - -It manages the following files and directories in the user's home directory: - -- `~/.config/solana/install/config.yml` - user configuration and information about the currently installed software version -- `~/.local/share/solana/install/bin` - a symlink to the current release. eg, `~/.local/share/solana-update/-/bin` -- `~/.local/share/solana/install/releases//` - contents of a release - -### Command-line Interface - -```text -agave-install 0.16.0 -The solana cluster software installer - -USAGE: - agave-install [OPTIONS] - -FLAGS: - -h, --help Prints help information - -V, --version Prints version information - -OPTIONS: - -c, --config Configuration file to use [default: .../Library/Preferences/solana/install.yml] - -SUBCOMMANDS: - deploy deploys a new update - help Prints this message or the help of the given subcommand(s) - info displays information about the current installation - init initializes a new installation - run Runs a program while periodically checking and applying software updates - update checks for an update, and if available downloads and applies it -``` - -```text -agave-install-init -initializes a new installation - -USAGE: - agave-install init [OPTIONS] - -FLAGS: - -h, --help Prints help information - -OPTIONS: - -d, --data_dir Directory to store install data [default: .../Library/Application Support/solana] - -u, --url JSON RPC URL for the solana cluster [default: http://api.devnet.solana.com] - -p, --pubkey Public key of the update manifest [default: 9XX329sPuskWhH4DQh6k16c87dHKhXLBZTL3Gxmve8Gp] -``` - -```text -agave-install info -displays information about the current installation - -USAGE: - agave-install info [FLAGS] - -FLAGS: - -h, --help Prints help information - -l, --local only display local information, don't check the cluster for new updates -``` - -```text -agave-install deploy -deploys a new update - -USAGE: - agave-install deploy - -FLAGS: - -h, --help Prints help information - -ARGS: - URL to the solana release archive - Keypair file for the update manifest (/path/to/keypair.json) -``` - -```text -agave-install update -checks for an update, and if available downloads and applies it - -USAGE: - agave-install update - -FLAGS: - -h, --help Prints help information -``` - -```text -agave-install run -Runs a program while periodically checking and applying software updates - -USAGE: - agave-install run [program_arguments]... - -FLAGS: - -h, --help Prints help information - -ARGS: - program to run - ... arguments to supply to the program - -The program will be restarted upon a successful software update -``` diff --git a/docs/src/implemented-proposals/instruction_introspection.md b/docs/src/implemented-proposals/instruction_introspection.md deleted file mode 100644 index 8854ad675f9..00000000000 --- a/docs/src/implemented-proposals/instruction_introspection.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -title: instruction introspection ---- - -## Problem - -Some smart contract programs may want to verify that another Instruction is present in a -given Message since that Instruction could be performing a verification of certain data, -in a precompiled function. (See secp256k1_instruction for an example). - -## Solution - -Add a new sysvar Sysvar1nstructions1111111111111111111111111 that a program can reference -and received the Message's instruction data inside, and also the index of the current instruction. - -Two helper functions to extract this data can be used: - -``` -fn load_current_index_checked(instruction_data: &[u8]) -> u16; -fn load_instruction_at_checked(instruction_index: usize, instruction_sysvar_account_info: &AccountInfo) -> Result; -``` - -The runtime will recognize this special instruction, serialize the Message instruction data -for it and also write the current instruction index and then the bpf program can extract the -necessary information from there. - -Note: custom serialization of instructions is used because bincode is about 10x slower -in native code and exceeds current SBF instruction limits. diff --git a/docs/src/implemented-proposals/leader-leader-transition.md b/docs/src/implemented-proposals/leader-leader-transition.md deleted file mode 100644 index f19036d92e2..00000000000 --- a/docs/src/implemented-proposals/leader-leader-transition.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -title: Leader-to-Leader Transition ---- - -This design describes how leaders transition production of the PoH ledger between each other as each leader generates its own slot. - -## Challenges - -Current leader and the next leader are both racing to generate the final tick for the current slot. The next leader may arrive at that slot while still processing the current leader's entries. - -The ideal scenario would be that the next leader generated its own slot right after it was able to vote for the current leader. It is very likely that the next leader will arrive at their PoH slot height before the current leader finishes broadcasting the entire block. - -The next leader has to make the decision of attaching its own block to the last completed block, or wait to finalize the pending block. It is possible that the next leader will produce a block that proposes that the current leader failed, even though the rest of the network observes that block succeeding. - -The current leader has incentives to start its slot as early as possible to capture economic rewards. Those incentives need to be balanced by the leader's need to attach its block to a block that has the most commitment from the rest of the network. - -## Leader timeout - -While a leader is actively receiving entries for the previous slot, the leader can delay broadcasting the start of its block in real time. The delay is locally configurable by each leader, and can be dynamically based on the previous leader's behavior. If the previous leader's block is confirmed by the leader's TVU before the timeout, the PoH is reset to the start of the slot and this leader produces its block immediately. - -The downsides: - -- Leader delays its own slot, potentially allowing the next leader more time to - - catch up. - -The upsides compared to guards: - -- All the space in a block is used for entries. -- The timeout is not fixed. -- The timeout is local to the leader, and therefore can be clever. The leader's heuristic can take into account turbine performance. -- This design doesn't require a ledger hard fork to update. -- The previous leader can redundantly transmit the last entry in the block to the next leader, and the next leader can speculatively decide to trust it to generate its block without verification of the previous block. -- The leader can speculatively generate the last tick from the last received entry. -- The leader can speculatively process transactions and guess which ones are not going to be encoded by the previous leader. This is also a censorship attack vector. The current leader may withhold transactions that it receives from the clients so it can encode them into its own slot. Once processed, entries can be replayed into PoH quickly. - -## Alternative design options - -### Guard tick at the end of the slot - -A leader does not produce entries in its block after the _penultimate tick_, which is the last tick before the first tick of the next slot. The network votes on the _last tick_, so the time difference between the _penultimate tick_ and the _last tick_ is the forced delay for the entire network, as well as the next leader before a new slot can be generated. The network can produce the _last tick_ from the _penultimate tick_. - -If the next leader receives the _penultimate tick_ before it produces its own _first tick_, it will reset its PoH and produce the _first tick_ from the previous leader's _penultimate tick_. The rest of the network will also reset its PoH to produce the _last tick_ as the id to vote on. - -The downsides: - -- Every vote, and therefore confirmation, is delayed by a fixed timeout. 1 tick, or around 100ms. -- Average case confirmation time for a transaction would be at least 50ms worse. -- It is part of the ledger definition, so to change this behavior would require a hard fork. -- Not all the available space is used for entries. - -The upsides compared to leader timeout: - -- The next leader has received all the previous entries, so it can start processing transactions without recording them into PoH. -- The previous leader can redundantly transmit the last entry containing the _penultimate tick_ to the next leader. The next leader can speculatively generate the _last tick_ as soon as it receives the _penultimate tick_, even before verifying it. diff --git a/docs/src/implemented-proposals/leader-validator-transition.md b/docs/src/implemented-proposals/leader-validator-transition.md deleted file mode 100644 index e3281252f13..00000000000 --- a/docs/src/implemented-proposals/leader-validator-transition.md +++ /dev/null @@ -1,83 +0,0 @@ ---- -title: Leader-to-Validator Transition ---- - -A validator typically spends its time validating blocks. If, however, a staker -delegates its stake to a validator, it will occasionally be selected as a _slot -leader_. As a slot leader, the validator is responsible for producing blocks -during an assigned _slot_. A slot has a duration of some number of preconfigured -_ticks_. The duration of those ticks are estimated with a _PoH Recorder_ -described later in this document. - -## BankFork - -BankFork tracks changes to the bank state over a specific slot. Once the final -tick has been registered the state is frozen. Any attempts to write to are -rejected. - -## Validator - -A validator operates on many different concurrent forks of the bank state until -it generates a PoH hash with a height within its leader slot. - -## Slot Leader - -A slot leader builds blocks on top of only one fork, the one it last voted on. - -## PoH Recorder - -Slot leaders and validators use a PoH Recorder for both estimating slot height -and for recording transactions. - -### PoH Recorder when Validating - -The PoH Recorder acts as a simple VDF when validating. It tells the validator -when it needs to switch to the slot leader role. Every time the validator votes -on a fork, it should use the fork's latest -[blockhash](https://solana.com/docs/terminology#blockhash) to re-seed the VDF. -Re-seeding solves two problems. First, it synchronizes its VDF to the leader's, -allowing it to more accurately determine when its leader slot begins. Second, if -the previous leader goes down, all wallclock time is accounted for in the next -leader's PoH stream. For example, if one block is missing when the leader -starts, the block it produces should have a PoH duration of two blocks. The -longer duration ensures the following leader isn't attempting to snip all the -transactions from the previous leader's slot. - -### PoH Recorder when Leading - -A slot leader use the PoH Recorder to record transactions, locking their -positions in time. The PoH hash must be derived from a previous leader's last -block. If it isn't, its block will fail PoH verification and be rejected by the -cluster. - -The PoH Recorder also serves to inform the slot leader when its slot is over. -The leader needs to take care not to modify its bank if recording the -transaction would generate a PoH height outside its designated slot. The leader, -therefore, should not commit account changes until after it generates the -entry's PoH hash. When the PoH height falls outside its slot any transactions in -its pipeline may be dropped or forwarded to the next leader. Forwarding is -preferred, as it would minimize network congestion, allowing the cluster to -advertise higher TPS capacity. - -## Validator Loop - -The PoH Recorder manages the transition between modes. Once a ledger is -replayed, the validator can run until the recorder indicates it should be the -slot leader. As a slot leader, the node can then execute and record -transactions. - -The loop is synchronized to PoH and does a synchronous start and stop of the -slot leader functionality. After stopping, the validator's TVU should find -itself in the same state as if a different leader had sent it the same block. -The following is pseudocode for the loop: - -1. Query the LeaderScheduler for the next assigned slot. -2. Run the TVU over all the forks. 1. TVU will send votes to what it believes is - the "best" fork. 2. After each vote, restart the PoH Recorder to run until - the next assigned slot. -3. When time to be a slot leader, start the TPU. Point it to the last fork the - TVU voted on. -4. Produce entries until the end of the slot. 1. For the duration of the slot, - the TVU must not vote on other forks. 2. After the slot ends, the TPU freezes - its BankFork. After freezing, the TVU may resume voting. -5. Goto 1. diff --git a/docs/src/implemented-proposals/persistent-account-storage.md b/docs/src/implemented-proposals/persistent-account-storage.md deleted file mode 100644 index 038c37dc7c5..00000000000 --- a/docs/src/implemented-proposals/persistent-account-storage.md +++ /dev/null @@ -1,83 +0,0 @@ ---- -title: Persistent Account Storage ---- - -## Persistent Account Storage - -The set of accounts represent the current computed state of all the transactions that have been processed by a validator. Each validator needs to maintain this entire set. Each block that is proposed by the network represents a change to this set, and since each block is a potential rollback point, the changes need to be reversible. - -Persistent storage like NVMEs are 20 to 40 times cheaper than DDR. The problem with persistent storage is that write and read performance is much slower than DDR. Care must be taken in how data is read or written to. Both reads and writes can be split between multiple storage drives and accessed in parallel. This design proposes a data structure that allows for concurrent reads and concurrent writes of storage. Writes are optimized by using an AppendVec data structure, which allows a single writer to append while allowing access to many concurrent readers. The accounts index maintains a pointer to a spot where the account was appended to every fork, thus removing the need for explicit checkpointing of state. - -## AppendVec - -AppendVec is a data structure that allows for random reads concurrent with a single append-only writer. Growing or resizing the capacity of the AppendVec requires exclusive access. This is implemented with an atomic `offset`, which is updated at the end of a completed append. - -The underlying memory for an AppendVec is a memory-mapped file. Memory-mapped files allow for fast random access and paging is handled by the OS. - -## Account Index - -The account index is designed to support a single index for all the currently forked Accounts. - -```text -type AccountsFileId = usize; - -type Fork = u64; - -struct AccountMap(Hashmap); - -type AccountIndex = HashMap; -``` - -The index is a map of account Pubkeys to a map of Forks and the location of the Account data in an AppendVec. To get the version of an account for a specific Fork: - -```text -/// Load the account for the pubkey. -/// This function will load the account from the specified fork, falling back to the fork's parents -/// * fork - a virtual Accounts instance, keyed by Fork. Accounts keep track of their parents with Forks, -/// the persistent store -/// * pubkey - The Account's public key. -pub fn load_slow(&self, id: Fork, pubkey: &Pubkey) -> Option<&Account> -``` - -The read is satisfied by pointing to a memory-mapped location in the `AccountsFileId` at the stored offset. A reference can be returned without a copy. - -### Root Forks - -[Tower BFT](tower-bft.md) eventually selects a fork as a root fork and the fork is squashed. A squashed/root fork cannot be rolled back. - -When a fork is squashed, all accounts in its parents not already present in the fork are pulled up into the fork by updating the indexes. Accounts with zero balance in the squashed fork are removed from fork by updating the indexes. - -An account can be _garbage-collected_ when squashing makes it unreachable. - -Three possible options exist: - -- Maintain a HashSet of root forks. One is expected to be created every second. The entire tree can be garbage-collected later. Alternatively, if every fork keeps a reference count of accounts, garbage collection could occur any time an index location is updated. -- Remove any pruned forks from the index. Any remaining forks lower in number than the root are can be considered root. -- Scan the index, migrate any old roots into the new one. Any remaining forks lower than the new root can be deleted later. - -## Garbage collection - -As accounts get updated, they move to the end of the AppendVec. Once capacity has run out, a new AppendVec can be created and updates can be stored there. Eventually references to an older AppendVec will disappear because all the accounts have been updated, and the old AppendVec can be deleted. - -To speed up this process, it's possible to move Accounts that have not been recently updated to the front of a new AppendVec. This form of garbage collection can be done without requiring exclusive locks to any of the data structures except for the index update. - -The initial implementation for garbage collection is that once all the accounts in an AppendVec become stale versions, it gets reused. The accounts are not updated or moved around once appended. - -## Index Recovery - -Each bank thread has exclusive access to the accounts during append, since the accounts locks cannot be released until the data is committed. But there is no explicit order of writes between the separate AppendVec files. To create an ordering, the index maintains an atomic write version counter. Each append to the AppendVec records the index write version number for that append in the entry for the Account in the AppendVec. - -To recover the index, all the AppendVec files can be read in any order, and the latest write version for every fork should be stored in the index. - -## Snapshots - -To snapshot, the underlying memory-mapped files in the AppendVec need to be flushed to disk. The index can be written out to disk as well. - -## Performance - -- Append-only writes are fast. SSDs and NVMEs, as well as all the OS level kernel data structures, allow for appends to run as fast as PCI or NVMe bandwidth will allow \(2,700 MB/s\). -- Each replay and banking thread writes concurrently to its own AppendVec. -- Each AppendVec could potentially be hosted on a separate NVMe. -- Each replay and banking thread has concurrent read access to all the AppendVecs without blocking writes. -- Index requires an exclusive write lock for writes. Single-thread performance for HashMap updates is on the order of 10m per second. -- Banking and Replay stages should use 32 threads per NVMe. NVMes have optimal performance with 32 concurrent readers or writers. diff --git a/docs/src/implemented-proposals/readonly-accounts.md b/docs/src/implemented-proposals/readonly-accounts.md deleted file mode 100644 index 0900a5dec06..00000000000 --- a/docs/src/implemented-proposals/readonly-accounts.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -title: Read-Only Accounts ---- - -This design covers the handling of readonly and writable accounts in the [runtime](../validator/runtime.md). Multiple transactions that modify the same account must be processed serially so that they are always replayed in the same order. Otherwise, this could introduce non-determinism to the ledger. Some transactions, however, only need to read, and not modify, the data in particular accounts. Multiple transactions that only read the same account can be processed in parallel, since replay order does not matter, providing a performance benefit. - -In order to identify readonly accounts, the transaction MessageHeader structure contains `num_readonly_signed_accounts` and `num_readonly_unsigned_accounts`. Instruction `program_ids` are included in the account vector as readonly, unsigned accounts, since executable accounts likewise cannot be modified during instruction processing. - -## Runtime handling - -Runtime transaction processing rules need to be updated slightly. Programs still can't write or spend accounts that they do not own. But new runtime rules ensure that readonly accounts cannot be modified, even by the programs that own them. - -Readonly accounts have the following property: - -- Read-only access to all account fields, including lamports (cannot be credited or debited), and account data - -Instructions that credit, debit, or modify the readonly account will fail. - -## Account Lock Optimizations - -The Accounts module keeps track of current locked accounts in the runtime, which separates readonly accounts from the writable accounts. The default account lock gives an account the "writable" designation, and can only be accessed by one processing thread at one time. Readonly accounts are locked by a separate mechanism, allowing for parallel reads. - -Although not yet implemented, readonly accounts could be cached in memory and shared between all the threads executing transactions. An ideal design would hold this cache while a readonly account is referenced by any transaction moving through the runtime, and release the cache when the last transaction exits the runtime. - -Readonly accounts could also be passed into the processor as references, saving an extra copy. diff --git a/docs/src/implemented-proposals/reliable-vote-transmission.md b/docs/src/implemented-proposals/reliable-vote-transmission.md deleted file mode 100644 index c4632be4a97..00000000000 --- a/docs/src/implemented-proposals/reliable-vote-transmission.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -title: Reliable Vote Transmission ---- - -Validator votes are messages that have a critical function for consensus and continuous operation of the network. Therefore it is critical that they are reliably delivered and encoded into the ledger. - -## Challenges - -1. Leader rotation is triggered by PoH, which is clock with high drift. So many nodes are likely to have an incorrect view if the next leader is active in realtime or not. -2. The next leader may be easily be flooded. Thus a DDOS would not only prevent delivery of regular transactions, but also consensus messages. -3. UDP is unreliable, and our asynchronous protocol requires any message that is transmitted to be retransmitted until it is observed in the ledger. Retransmission could potentially cause an unintentional _thundering herd_ against the leader with a large number of validators. Worst case flood would be `(num_nodes * num_retransmits)`. -4. Tracking if the vote has been transmitted or not via the ledger does not guarantee it will appear in a confirmed block. The current observed block may be unrolled. Validators would need to maintain state for each vote and fork. - -## Design - -1. Send votes as a push message through gossip. This ensures delivery of the vote to all the next leaders, not just the next future one. -2. Leaders will read the Crds table for new votes and encode any new received votes into the blocks they propose. This allows for validator votes to be included in rollback forks by all the future leaders. -3. Validators that receive votes in the ledger will add them to their local crds table, not as a push request, but simply add them to the table. This shortcuts the push message protocol, so the validation messages do not need to be retransmitted twice around the network. -4. CrdsValue for vote should look like this `Votes(Vec)` - -Each vote transaction should maintain a `wallclock` in its data. The merge strategy for Votes will keep the last N set of votes as configured by the local client. For push/pull the vector is traversed recursively and each Transaction is treated as an individual CrdsValue with its own local wallclock and signature. - -Gossip is designed for efficient propagation of state. Messages that are sent through gossip-push are batched and propagated with a minimum spanning tree to the rest of the network. Any partial failures in the tree are actively repaired with the gossip-pull protocol while minimizing the amount of data transferred between any nodes. - -## How this design solves the Challenges - -1. Because there is no easy way for validators to be in sync with leaders on the leader's "active" state, gossip allows for eventual delivery regardless of that state. -2. Gossip will deliver the messages to all the subsequent leaders, so if the current leader is flooded the next leader would have already received these votes and is able to encode them. -3. Gossip minimizes the number of requests through the network by maintaining an efficient spanning tree, and using bloom filters to repair state. So retransmit back-off is not necessary and messages are batched. -4. Leaders that read the crds table for votes will encode all the new valid votes that appear in the table. Even if this leader's block is unrolled, the next leader will try to add the same votes without any additional work done by the validator. Thus ensuring not only eventual delivery, but eventual encoding into the ledger. - -## Performance - -1. Worst case propagation time to the next leader is Log\(N\) hops with a base depending on the fanout. With our current default fanout of 6, it is about 6 hops to 20k nodes. -2. The leader should receive 20k validation votes aggregated by gossip-push into MTU-sized shreds. Which would reduce the number of packets for 20k network to 80 shreds. -3. Each validators votes is replicated across the entire network. To maintain a queue of 5 previous votes the Crds table would grow by 25 megabytes. `(20,000 nodes * 256 bytes * 5)`. - -## Two step implementation rollout - -Initially the network can perform reliably with just 1 vote transmitted and maintained through the network with the current Vote implementation. For small networks a fanout of 6 is sufficient. With small network the memory and push overhead is minor. - -### Sub 1k validator network - -1. Crds just maintains the validators latest vote. -2. Votes are pushed and retransmitted regardless if they are appearing in the ledger. -3. Fanout of 6. -4. Worst case 256kb memory overhead per node. -5. Worst case 4 hops to propagate to every node. -6. Leader should receive the entire validator vote set in 4 push message shreds. - -### Sub 20k network - -Everything above plus the following: - -1. CRDS table maintains a vector of 5 latest validator votes. -2. Votes encode a wallclock. CrdsValue::Votes is a type that recurses into the transaction vector for all the gossip protocols. -3. Increase fanout to 20. -4. Worst case 25mb memory overhead per node. -5. Sub 4 hops worst case to deliver to the entire network. -6. 80 shreds received by the leader for all the validator messages. diff --git a/docs/src/implemented-proposals/rent.md b/docs/src/implemented-proposals/rent.md deleted file mode 100644 index 02adc0390ef..00000000000 --- a/docs/src/implemented-proposals/rent.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -title: Rent ---- - -Accounts on Solana may have owner-controlled state \(`Account::data`\) that's separate from the account's balance \(`Account::lamports`\). Since validators on the network need to maintain a working copy of this state in memory, the network charges a time-and-space based fee for this resource consumption, also known as Rent. - -## Two-tiered rent regime - -Accounts which maintain a minimum balance equivalent to 2 years of rent payments are exempt. The _2 years_ is drawn from the fact hardware cost drops by 50% in price every 2 years and the resulting convergence due to being a geometric series. Accounts whose balance falls below this threshold are charged rent at a rate specified in genesis, in lamports per byte-year. The network charges rent on a per-epoch basis, in credit for the next epoch, and `Account::rent_epoch` keeps track of the next time rent should be collected from the account. - -Currently, the rent cost is fixed at the genesis. However, it's anticipated to be dynamic, reflecting the underlying hardware storage cost at the time. So the price is generally expected to decrease as the hardware cost declines as the technology advances. - -## Timings of collecting rent - -There are two timings of collecting rent from accounts: \(1\) when referenced by a transaction, \(2\) periodically once an epoch. \(1\) includes the transaction to create the new account itself, and it happens during the normal transaction processing by the bank as part of the load phase. \(2\) exists to ensure to collect rents from stale accounts, which aren't referenced in recent epochs at all. \(2\) requires the whole scan of accounts and is spread over an epoch based on account address prefix to avoid load spikes due to this rent collection. - -On the contrary, rent collection isn't applied to accounts that are directly manipulated by any of protocol-level bookkeeping processes including: - -- The distribution of rent collection itself (Otherwise, it may cause recursive rent collection handling) -- The distribution of staking rewards at the start of every epoch (To reduce as much as processing spike at the start of new epoch) -- The distribution of transaction fee at the end of every slot - -Even if those processes are out of scope of rent collection, all of manipulated accounts will eventually be handled by the \(2\) mechanism. - -## Actual processing of collecting rent - -Rent is due for one epoch's worth of time, and accounts have `Account::rent_epoch` of `current_epoch` or `current_epoch + 1` depending on the rent regime. - -If the account is in the exempt regime, `Account::rent_epoch` is simply updated to `current_epoch`. - -If the account is non-exempt, the difference between the next epoch and `Account::rent_epoch` is used to calculate the amount of rent owed by this account \(via `Rent::due()`\). Any fractional lamports of the calculation are truncated. Rent due is deducted from `Account::lamports` and `Account::rent_epoch` is updated to `current_epoch + 1` (= next epoch). If the amount of rent due is less than one lamport, no changes are made to the account. - -Accounts whose balance is insufficient to satisfy the rent that would be due simply fail to load. - -A percentage of the rent collected is destroyed. The rest is distributed to validator accounts by stake weight, a la transaction fees, at the end of every slot. - -Finally, rent collection happens according to the protocol-level account updates like the rent distribution to validators, meaning there is no corresponding transaction for rent deductions. So, rent collection is rather invisible, only implicitly observable by a recent transaction or predetermined timing given its account address prefix. - -## Design considerations - -### Current design rationale - -Under the preceding design, it is NOT possible to have accounts that linger, never get touched, and never have to pay rent. Accounts always pay rent exactly once for each epoch, except rent-exempt, sysvar and executable accounts. - -This is an intended design choice. Otherwise, it would be possible to trigger unauthorized rent collection with `Noop` instruction by anyone who may unfairly profit from the rent (a leader at the moment) or save the rent given anticipated fluctuating rent cost. - -As another side-effect of this choice, also note that this periodic rent collection effectively forces validators not to store stale accounts into a cold storage optimistically and save the storage cost, which is unfavorable for account owners and may cause transactions on them to stall longer than others. On the flip side, this prevents malicious users from creating significant numbers of garbage accounts, burdening validators. - -As the overall consequence of this design, all accounts are stored equally as a validator's working set with the same performance characteristics, reflecting the uniform rent pricing structure. - -### Ad-hoc collection - -Collecting rent on an as-needed basis \(i.e. whenever accounts were loaded/accessed\) was considered. The issues with such an approach are: - -- accounts loaded as "credit only" for a transaction could very reasonably be expected to have rent due, - - but would not be writable during any such transaction - -- a mechanism to "beat the bushes" \(i.e. go find accounts that need to pay rent\) is desirable, - - lest accounts that are loaded infrequently get a free ride - -### System instruction for collecting rent - -Collecting rent via a system instruction was considered, as it would naturally have distributed rent to active and stake-weighted nodes and could have been done incrementally. However: - -- it would have adversely affected network throughput -- it would require special-casing by the runtime, as accounts with non-SystemProgram owners may be debited by this instruction -- someone would have to issue the transactions diff --git a/docs/src/implemented-proposals/repair-service.md b/docs/src/implemented-proposals/repair-service.md deleted file mode 100644 index 49d1ff1c9f1..00000000000 --- a/docs/src/implemented-proposals/repair-service.md +++ /dev/null @@ -1,108 +0,0 @@ ---- -title: Repair Service ---- - -## Repair Service - -The RepairService is in charge of retrieving missing shreds that failed to be -delivered by primary communication protocols like Turbine. It is in charge of -managing the protocols described below in the `Repair Protocols` section below. - -## Challenges: - -1\) Validators can fail to receive particular shreds due to network failures - -2\) Consider a scenario where blockstore contains the set of slots {1, 3, 5}. -Then Blockstore receives shreds for some slot 7, where for each of the shreds -b, b.parent == 6, so then the parent-child relation 6 -> 7 is stored in -blockstore. However, there is no way to chain these slots to any of the -existing banks in Blockstore, and thus the `Shred Repair` protocol will not -repair these slots. If these slots happen to be part of the main chain, this -will halt replay progress on this node. - -## Repair-related primitives - -Epoch Slots: -Each validator advertises separately on gossip the various parts of an -`Epoch Slots`: - -- The `stash`: An epoch-long compressed set of all completed slots. -- The `cache`: The Run-length Encoding (RLE) of the latest `N` completed - slots starting from some slot `M`, where `N` is the number of slots - that will fit in an MTU-sized packet. - -`Epoch Slots` in gossip are updated every time a validator receives a -complete slot within the epoch. Completed slots are detected by blockstore -and sent over a channel to RepairService. It is important to note that we -know that by the time a slot `X` is complete, the epoch schedule must exist -for the epoch that contains slot `X` because WindowService will reject -shreds for unconfirmed epochs. - -Every `N/2` completed slots, the oldest `N/2` slots are moved from the -`cache` into the `stash`. The base value `M` for the RLE should also -be updated. - -## Repair Request Protocols - -The repair protocol makes best attempts to progress the forking structure of -Blockstore. - -The different protocol strategies to address the above challenges: - -1. Shred Repair \(Addresses Challenge \#1\): This is the most basic repair - protocol, with the purpose of detecting and filling "holes" in the ledger. - Blockstore tracks the latest root slot. RepairService will then periodically - iterate every fork in blockstore starting from the root slot, sending repair - requests to validators for any missing shreds. It will send at most some `N` - repair requests per iteration. Shred repair should prioritize repairing - forks based on the leader's fork weight. Validators should only send repair - requests to validators who have marked that slot as completed in their - EpochSlots. Validators should prioritize repairing shreds in each slot - that they are responsible for retransmitting through turbine. Validators can - compute which shreds they are responsible for retransmitting because the - seed for turbine is based on leader id, slot, and shred index. - - Note: Validators will only accept shreds within the current verifiable - epoch \(epoch the validator has a leader schedule for\). - -2. Preemptive Slot Repair \(Addresses Challenge \#2\): The goal of this - protocol is to discover the chaining relationship of "orphan" slots that do not - currently chain to any known fork. Shred repair should prioritize repairing - orphan slots based on the leader's fork weight. - - - Blockstore will track the set of "orphan" slots in a separate column family. - - RepairService will periodically make `Orphan` requests for each of - the orphans in blockstore. - - `Orphan(orphan)` request - `orphan` is the orphan slot that the - requestor wants to know the parents of `Orphan(orphan)` response - - The highest shreds for each of the first `N` parents of the requested - `orphan` - - On receiving the responses `p`, where `p` is some shred in a parent slot, - validators will: - - - Insert an empty `SlotMeta` in blockstore for `p.slot` if it doesn't - already exist. - - If `p.slot` does exist, update the parent of `p` based on `parents` - - Note: that once these empty slots are added to blockstore, the - `Shred Repair` protocol should attempt to fill those slots. - - Note: Validators will only accept responses containing shreds within the - current verifiable epoch \(epoch the validator has a leader schedule - for\). - -Validators should try to send orphan requests to validators who have marked that -orphan as completed in their EpochSlots. If no such validators exist, then -randomly select a validator in a stake-weighted fashion. - -## Repair Response Protocol - -When a validator receives a request for a shred `S`, they respond with the -shred if they have it. - -When a validator receives a shred through a repair response, they check -`EpochSlots` to see if <= `1/3` of the network has marked this slot as -completed. If so, they resubmit this shred through its associated turbine -path, but only if this validator has not retransmitted this shred before. diff --git a/docs/src/implemented-proposals/rpc-transaction-history.md b/docs/src/implemented-proposals/rpc-transaction-history.md deleted file mode 100644 index 99e828551cd..00000000000 --- a/docs/src/implemented-proposals/rpc-transaction-history.md +++ /dev/null @@ -1,121 +0,0 @@ -# Long term RPC Transaction History - -There's a need for RPC to serve at least 6 months of transaction history. The -current history, on the order of days, is insufficient for downstream users. - -6 months of transaction data cannot be stored practically in a validator's -rocksdb ledger so an external data store is necessary. The validator's rocksdb -ledger will continue to serve as the primary data source, and then will fall -back to the external data store. - -The affected RPC endpoints are: - -- [getFirstAvailableBlock](https://solana.com/docs/rpc/http/getfirstavailableblock) -- [getConfirmedBlock](https://solana.com/docs/rpc/deprecated/getconfirmedblock) -- [getConfirmedBlocks](https://solana.com/docs/rpc/deprecated/getconfirmedblocks) -- [getConfirmedSignaturesForAddress](https://solana.com/docs/rpc/http/getconfirmedsignaturesforaddress) -- [getConfirmedTransaction](https://solana.com/docs/rpc/deprecated/getConfirmedTransaction) -- [getSignatureStatuses](https://solana.com/docs/rpc/http/getsignaturestatuses) -- [getBlockTime](https://solana.com/docs/rpc/http/getblocktime) - -Some system design constraints: - -- The volume of data to store and search can quickly jump into the terabytes, - and is immutable. -- The system should be as light as possible for SREs. For example an SQL - database cluster that requires an SRE to continually monitor and rebalance - nodes is undesirable. -- Data must be searchable in real time - batched queries that take minutes or - hours to run are unacceptable. -- Easy to replicate the data worldwide to co-locate it with the RPC endpoints - that will utilize it. -- Interfacing with the external data store should be easy and not require - depending on risky lightly-used community-supported code libraries - -Based on these constraints, Google's BigTable product is selected as the data -store. - -## Table Schema - -A BigTable instance is used to hold all transaction data, broken up into -different tables for quick searching. - -New data may be copied into the instance at anytime without affecting the -existing data, and all data is immutable. Generally the expectation is that new -data will be uploaded once a current epoch completes but there is no limitation -on the frequency of data dumps. - -Cleanup of old data is automatic by configuring the data retention policy of the -instance tables appropriately, it just disappears. Therefore the order of when -data is added becomes important. For example if data from epoch N-1 is added -after data from epoch N, the older epoch data will outlive the newer data. -However beyond producing _holes_ in query results, this kind of unordered -deletion will have no ill effect. Note that this method of cleanup effectively -allows for an unlimited amount of transaction data to be stored, restricted only -by the monetary costs of doing so. - -The table layout s supports the existing RPC endpoints only. New RPC endpoints -in the future may require additions to the schema and potentially iterating over -all transactions to build up the necessary metadata. - -## Accessing BigTable - -BigTable has a gRPC endpoint that can be accessed using the -[tonic](https://crates.io/crates/tonic) and the raw protobuf API, as currently -no higher-level Rust crate for BigTable exists. Practically this makes parsing -the results of BigTable queries more complicated but is not a significant issue. - -## Data Population - -The ongoing population of instance data will occur on an epoch cadence through -the use of a new `agave-ledger-tool` command that will convert rocksdb data for -a given slot range into the instance schema. - -The same process will be run once, manually, to backfill the existing ledger -data. - -### Block Table: `block` - -This table contains the compressed block data for a given slot. - -The row key is generated by taking the 16 digit lower case hexadecimal -representation of the slot, to ensure that the oldest slot with a confirmed -block will always be first when the rows are listed. eg, The row key for slot 42 -would be 000000000000002a. - -The row data is a compressed `StoredConfirmedBlock` struct. - -### Account Address Transaction Signature Lookup Table: `tx-by-addr` - -This table contains the transactions that affect a given address. - -The row key is -`/`. -The row data is a compressed `TransactionByAddrInfo` struct. - -Taking the one's compliment of the slot allows for listing of slots ensures that -the newest slot with transactions that affect an address will always be listed -first. - -Sysvar addresses are not indexed. However frequently used programs such as Vote -or System are, and will likely have a row for every confirmed slot. - -### Transaction Signature Lookup Table: `tx` - -This table maps a transaction signature to its confirmed block, and index within -that block. - -The row key is the base58-encoded transaction signature. -The row data is a compressed `TransactionInfo` struct. - -### Entries Table: `entries` - -> Support for the `entries` table was added in v1.18.0. - -This table contains data about the entries in a slot. - -The row key is the same as a `block` row key. - -The row data is a compressed `Entries` struct, which is a list of entry-summary -data, including hash, number of hashes since previous entry, number of -transactions, and starting transaction index. diff --git a/docs/src/implemented-proposals/snapshot-verification.md b/docs/src/implemented-proposals/snapshot-verification.md deleted file mode 100644 index 88fdc356d95..00000000000 --- a/docs/src/implemented-proposals/snapshot-verification.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -title: Snapshot Verification ---- - -## Problem - -When a validator boots up from a snapshot, it needs a way to verify the account set matches what the rest of the network sees quickly. A potential -attacker could give the validator an incorrect state, and then try to convince it to accept a transaction that would otherwise be rejected. - -## Solution - -Currently the bank hash is derived from hashing the delta state of the accounts in a slot which is then combined with the previous bank hash value. -The problem with this is that the list of hashes will grow on the order of the number of slots processed by the chain and become a burden to both -transmit and verify successfully. - -Another naive method could be to create a merkle tree of the account state. This has the downside that with each account update, the merkle tree -would have to be recomputed from the entire account state of all live accounts in the system. - -To verify the snapshot, we do the following: - -On account store of non-zero lamport accounts, we hash the following data: - -- Account owner -- Account data -- Account pubkey -- Account lamports balance -- Fork the account is stored on - -Use this resulting hash value as input to an expansion function which expands the hash value into an image value. -The function will create a 440 byte block of data where the first 32 bytes are the hash value, and the next 440 - 32 bytes are -generated from a Chacha RNG with the hash as the seed. - -The account images are then combined with xor. The previous account value will be xored into the state and the new account value also xored into the state. - -Voting and sysvar hash values occur with the hash of the resulting full image value. - -On validator boot, when it loads from a snapshot, it would verify the hash value with the accounts set. It would then -use SPV to display the percentage of the network that voted for the hash value given. - -The resulting value can be verified by a validator to be the result of xoring all current account states together. - -A snapshot must be purged of zero lamport accounts before creation and during verify since the zero lamport accounts do not affect the hash value but may cause -a validator bank to read that an account is not present when it really should be. - -An attack on the xor state could be made to influence its value: - -Thus the 440 byte image size comes from this paper, avoiding xor collision with 0 \(or thus any other given bit pattern\): \[[https://link.springer.com/content/pdf/10.1007%2F3-540-45708-9_19.pdf](https://link.springer.com/content/pdf/10.1007%2F3-540-45708-9_19.pdf)\] - -The math provides 128 bit security in this case: - -```text -O(k * 2^(n/(1+lg(k))) -k=2^40 accounts -n=440 -2^(40) * 2^(448 * 8 / 41) ~= O(2^(128)) -``` diff --git a/docs/src/implemented-proposals/staking-rewards.md b/docs/src/implemented-proposals/staking-rewards.md deleted file mode 100644 index 4ab55b5c0ea..00000000000 --- a/docs/src/implemented-proposals/staking-rewards.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: Staking Rewards ---- - -A Proof of Stake \(PoS\), \(i.e. using in-protocol asset, SOL, to provide secure consensus\) design is outlined here. Solana implements a proof of stake reward/security scheme for validator nodes in the cluster. The purpose is threefold: - -- Align validator incentives with that of the greater cluster through skin-in-the-game deposits at risk - -- Avoid 'nothing at stake' fork voting issues by implementing slashing rules aimed at promoting fork convergence - -- Provide an avenue for validator rewards provided as a function of validator participation in the cluster. - -While many of the details of the specific implementation are currently under consideration and are expected to come into focus through specific modeling studies and parameter exploration on the Solana testnet, we outline here our current thinking on the main components of the PoS system. Much of this thinking is based on the current status of Casper FFG, with optimizations and specific attributes to be modified as is allowed by Solana's Proof of History \(PoH\) blockchain data structure. - -## General Overview - -Solana's ledger validation design is based on a rotating, stake-weighted selected leader broadcasting transactions in a PoH data structure to validating nodes. These nodes, upon receiving the leader's broadcast, have the opportunity to vote on the current state and PoH height by signing a transaction into the PoH stream. - -To become a Solana validator, one must deposit/lock-up some amount of SOL in a contract. This SOL will not be accessible for a specific time period. The precise duration of the staking lockup period has not been determined. However we can consider three phases of this time for which specific parameters will be necessary: - -- _Warm-up period_: which SOL is deposited and inaccessible to the node, however PoH transaction validation has not begun. Most likely on the order of days to weeks - -- _Validation period_: a minimum duration for which the deposited SOL will be inaccessible, at risk of slashing \(see slashing rules below\) and earning rewards for the validator participation. Likely duration of months to a year. - -- _Cool-down period_: a duration of time following the submission of a 'withdrawal' transaction. During this period validation responsibilities have been removed and the funds continue to be inaccessible. Accumulated rewards should be delivered at the end of this period, along with the return of the initial deposit. - -Solana's trustless sense of time and ordering provided by its PoH data structure, along with its [turbine](https://docs.anza.xyz/consensus/turbine-block-propagation) data broadcast and transmission design, should provide sub-second transaction confirmation times that scale with the log of the number of nodes in the cluster. This means we shouldn't have to restrict the number of validating nodes with a prohibitive 'minimum deposits' and expect nodes to be able to become validators with nominal amounts of SOL staked. At the same time, Solana's focus on high-throughput should create incentive for validation clients to provide high-performant and reliable hardware. Combined with potentially a minimum network speed threshold to join as a validation-client, we expect a healthy validation delegation market to emerge. - -## Penalties - -As discussed in the [Economic Design](ed_overview/ed_overview.md) section, annual validator interest rates are to be specified as a function of total percentage of circulating supply that has been staked. The cluster rewards validators who are online and actively participating in the validation process throughout the entirety of their _validation period_. For validators that go offline/fail to validate transactions during this period, their annual reward is effectively reduced. - -Similarly, we may consider an algorithmic reduction in a validator's active staked amount in the case that they are offline. I.e. if a validator is inactive for some amount of time, either due to a partition or otherwise, the amount of their stake that is considered ‘active’ \(eligible to earn rewards\) may be reduced. This design would be structured to help long-lived partitions to eventually reach finality on their respective chains as the % of non-voting total stake is reduced over time until a supermajority can be achieved by the active validators in each partition. Similarly, upon re-engaging, the ‘active’ amount staked will come back online at some defined rate. Different rates of stake reduction may be considered depending on the size of the partition/active set. diff --git a/docs/src/implemented-proposals/testing-programs.md b/docs/src/implemented-proposals/testing-programs.md deleted file mode 100644 index 583eda7b7b4..00000000000 --- a/docs/src/implemented-proposals/testing-programs.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -title: Testing Programs ---- - -Applications send transactions to a Solana cluster and query validators to confirm the transactions were processed and to check each transaction's result. When the cluster doesn't behave as anticipated, it could be for a number of reasons: - -- The program is buggy -- The BPF loader rejected an unsafe program instruction -- The transaction was too big -- The transaction was invalid -- The Runtime tried to execute the transaction when another one was accessing - - the same account - -- The network dropped the transaction -- The cluster rolled back the ledger -- A validator responded to queries maliciously - -## The AsyncClient and SyncClient Traits - -To troubleshoot, the application should retarget a lower-level component, where fewer errors are possible. Retargeting can be done with different implementations of the AsyncClient and SyncClient traits. - -Components implement the following primary methods: - -```text -trait AsyncClient { - fn async_send_transaction(&self, transaction: Transaction) -> io::Result; -} - -trait SyncClient { - fn get_signature_status(&self, signature: &Signature) -> Result>>; -} -``` - -Users send transactions and asynchronously and synchronously await results. - -### TpuClient for the TPU - -The next level is the TPU implementation, which is not yet implemented. At the TPU level, the application sends transactions over Rust channels, where there can be no surprises from network queues or dropped packets. The TPU implements all "normal" transaction errors. It does signature verification, may report account-in-use errors, and otherwise results in the ledger, complete with proof of history hashes. - -## Low-level testing - -### BankClient for the Bank - -Below the TPU level is the Bank. The Bank doesn't do signature verification or generate a ledger. The Bank is a convenient layer at which to test new on-chain programs. It allows developers to toggle between native program implementations and BPF-compiled variants. No need for the Transact trait here. The Bank's API is synchronous. - -## Unit-testing with the Runtime - -Below the Bank is the Runtime. The Runtime is the ideal test environment for unit-testing. By statically linking the Runtime into a native program implementation, the developer gains the shortest possible edit-compile-run loop. Without any dynamic linking, stack traces include debug symbols and program errors are straightforward to troubleshoot. diff --git a/docs/src/implemented-proposals/tower-bft.md b/docs/src/implemented-proposals/tower-bft.md deleted file mode 100644 index c8a7819bbfe..00000000000 --- a/docs/src/implemented-proposals/tower-bft.md +++ /dev/null @@ -1,183 +0,0 @@ ---- -title: Tower BFT ---- - -This design describes Solana's _Tower BFT_ algorithm. It addresses the following problems: - -- Some forks may not end up accepted by the supermajority of the cluster, and voters need to recover from voting on such forks. -- Many forks may be votable by different voters, and each voter may see a different set of votable forks. The selected forks should eventually converge for the cluster. -- Reward based votes have an associated risk. Voters should have the ability to configure how much risk they take on. -- The [cost of rollback](tower-bft.md#cost-of-rollback) needs to be computable. It is important to clients that rely on some measurable form of Consistency. The costs to break consistency need to be computable, and increase super-linearly for older votes. -- ASIC speeds are different between nodes, and attackers could employ Proof of History ASICS that are much faster than the rest of the cluster. Consensus needs to be resistant to attacks that exploit the variability in Proof of History ASIC speed. - -For brevity this design assumes that a single voter with a stake is deployed as an individual validator in the cluster. - -## Time - -The Solana cluster generates a source of time via a Verifiable Delay Function we are calling [Proof of History](../consensus/synchronization.md). - -The unit of time is called a "slot". Each slot has a designated leader that can -produce a block `B`. The `slot` of block `B` is designated `slot(B)`. A leader -does not necessarily need to generate a block for its slot, in which case there -may not be blocks for some slots. - -For more details, see [fork generation](../consensus/fork-generation.md) and [leader rotation](../consensus/leader-rotation.md). - -## Votes - -Validators communicate which fork they think is the heaviest through votes. -Each vote `v` is signed by the validator that produces it, and is of the form `(i, B)`, where `i` is the public key of the validator producing the vote and `B` is a hash identifying the block being voted for. - -## Lockouts - -Making votes on a particular fork incurs a lockout on that particular fork. A lockout is a designated period of time, measured in slots, within which a validator cannot vote on another fork. The purpose of the lockout is to force a -validator to commit opportunity cost to a specific fork. Lockouts are measured -in slots, and therefore represent a real-time forced delay that a validator -needs to wait before breaking the commitment to a fork. - -Validators that violate the lockouts and vote for a diverging fork within the lockout should be punished. The proposed punishment is to slash the validator stake if a concurrent vote within a lockout for a non-descendant fork can be proven to the cluster. - -## Algorithm - -The basic idea to this approach is to stack consensus votes and double lockouts. Each vote in the stack is a confirmation of a fork. Each confirmed fork is an ancestor of the fork above it. Each vote has a `lockout` in units of slots before the validator can submit a vote that does not contain the confirmed fork as an ancestor. - -We call this stack the Vote Tower. - -When a vote is added to the tower, the lockouts of all the previous votes in the tower are doubled (more on this in [Vote Tower](#vote-tower)). With each new vote, a validator commits the previous votes to an ever-increasing lockout. At 32 votes we can consider the vote to be at `max lockout` any votes with a lockout equal to or above `1<<32` are dequeued \(FIFO\). Dequeuing a vote is the trigger for a reward. If the vote on the top of the tower expires before it is dequeued, it and subsequent expired votes are popped in a LIFO fashion from the vote tower. The validator needs to start rebuilding the tower from that point. - -### Vote Tower - -Before a vote is pushed to the tower, all the votes leading up to vote with a lower lock expiration slot than the new vote are popped. After rollback -lockouts are not doubled until the validator catches up to the rollback height of votes. - -For example, a vote tower with the following state: - -| vote | vote slot | lockout | lock expiration slot | -| ---: | --------: | ------: | -------------------: | -| 4 | 4 | 2 | 6 | -| 3 | 3 | 4 | 7 | -| 2 | 2 | 8 | 10 | -| 1 | 1 | 16 | 17 | - -_Vote 5_ is at slot 9, and the resulting state is - -| vote | vote slot | lockout | lock expiration slot | -| ---: | --------: | ------: | -------------------: | -| 5 | 9 | 2 | 11 | -| 2 | 2 | 8 | 10 | -| 1 | 1 | 16 | 17 | - -_Vote 6_ is at slot 10 - -| vote | vote slot | lockout | lock expiration slot | -| ---: | --------: | ------: | -------------------: | -| 6 | 10 | 2 | 12 | -| 5 | 9 | 4 | 13 | -| 2 | 2 | 8 | 10 | -| 1 | 1 | 16 | 17 | - -At slot 10 the new votes caught up to the previous votes. When _vote 7_ at slot 11 is applied we scan top down to pop expired votes. Although _vote 2_ has expired, since _vote 6_ has not expired, we do not continue scanning. Finally we have reached a new stack depth, lockouts are doubled - -| vote | vote slot | lockout | lock expiration slot | -| ---: | --------: | ------: | -------------------: | -| 7 | 11 | 2 | 13 | -| 6 | 10 | 4 | 14 | -| 5 | 9 | 8 | 17 | -| 2 | 2 | 16 | 18 | -| 1 | 1 | 32 | 33 | - -Finally we have _vote 8_ at slot 18, this leads to the expiry of _vote 7_, _vote 6_, and _vote 5_. - -| vote | vote slot | lockout | lock expiration slot | -| ---: | --------: | ------: | -------------------: | -| 8 | 18 | 2 | 20 | -| 2 | 2 | 16 | 18 | -| 1 | 1 | 32 | 33 | - -### Cost of Rollback - -Cost of rollback of _fork A_ is defined as the cost in terms of lockout time to the validator to confirm any other fork that does not include _fork A_ as an ancestor. - -The **Economic Finality** of _fork A_ can be calculated as the loss of all the rewards from rollback of _fork A_ and its descendants, plus the opportunity cost of reward due to the exponentially growing lockout of the votes that have confirmed _fork A_. - -### Threshold Check -In order to prevent a validator from locking itself out on the wrong fork -in the case of a partition, there also needs to be a check to ensure that the rest of the cluster is committing to the same fork. This check is called the -"threshold check", and is outlined as follows. - -In deciding whether to vote for a block `B`: - -1. Simulate a vote for `B` on your current tower -2. Simulate popping off all the votes that would be expired by `B` -3. Now index every vote in the tower from `[0, tower.length()]`, assuming that the most recent simulated vote `B` is index 0, the second most recent vote is index 1, etc. -4. Let `T` be the vote in the tower with index equal to `threshold_check_depth`, currently hardcoded to `8`. -5. Check all the blocks descended from `T`. Let `Votes` be the set of all votes in these blocks for `T` or any descendants `D_n` of `T`. Let `V` be the set of all validators that have made a vote in `V`. If the sum of the validators' stakes in `V` totals `>= 2/3` of the stake of the network, then we commit a vote to `T`. - -### Algorithm parameters - -The following parameters need to be tuned: - -- Number of votes in the stack before dequeue occurs \(32\). -- Rate of growth for lockouts in the stack \(2x\). -- Starting default lockout \(2\). -- Threshold check depth for minimum cluster commitment before committing to the fork \(8\). -- Minimum cluster commitment size at threshold depth \(50%+\). - -### Fork Choice - -Fork choice is how each validator determines which fork to vote on when multiple -concurrent forks exist. Forks are weighted based on the latest votes made by the validator set, and individual validators then vote on the "heaviest" -such fork. - -Given the view of a single validator `i`: - -Let `V` be the set of "most recent" valid votes received by `i`, i.e., `v = (j, B)` is in `V` and `i` has not also received a vote of the form `(j, B′) `such that `slot(B′) > slot(B)`. - -Now the algorithm proceeds as follows: - -1. For each vote `(j, B)` in `V`, add the stake of `j` to `B` and all of its -ancestors. -2. Now Set `B` to be the rooted block. Set `finish := 0`. -3. Perform the following loop: - -``` -*While* `finish == 0` -*Do*: - *If*: `i` has received no children of `B` then set `finish := 1` and return - `B`. - *Else*: Let `B′` be the child of `B` (amongst those received by `i`) with - most the most stake-weighted votes in `V`, breaking ties by the smallest - slot. Set `B` equal to `B'`. -``` - -### Voting Algorithm - -Each validator maintains a vote tower `T` which follows the rules described above in [Vote Tower](#vote-tower), which is a sequence of blocks it has voted for (initially empty). The variable `l` records the length of the stack. For each entry in the tower, denoted by `B = T(x)` for `x < l` where `B` is the `xth` entry in the tower, we record also a value `confcount(B)`. Define the lock expiration slot `lockexp(B) := slot(B) + 2 ^ confcount(B)`. - -The validator `i` runs a voting loop as follows. Let `B` be the heaviest -block returned by the fork choice rule above [Fork Choice](#fork-choice). If `i` has not voted for `B` before, then `i` votes for `B` so long as the following conditions are satisfied: - -1. Respecting lockouts: For any block `B′` in the tower that is not an ancestor of `B`, `lockexp(B′) ≤ slot(B)`. -2. Threshold check: Described above in [Threshold Check](#threshold-check) -3. Switching threshold: Have sufficiently many votes on other forks if switching forks. Let `Btop` denote the block at the top of the stack. If `Btop` is not an ancestor of `B`, then: - - Let `VBtop ⊆ V` be the set of votes on `Btop` or ancestors or descendents of `Btop`. - - We need `|V \ VBtop | > 38%`. More details on this can be found in [Optimistic Confirmation](../proposals/optimistic_confirmation.md) - -If all the conditions are satisfied and validator `i` votes for block `B` then it adjusts its tower as follows (same rules described above in [Vote Tower](#vote-tower)). -1. Remove expired blocks top down. Let `x := l - 1`. While `x >= 0 && lockexp(T(x)) < slot(B)`, remove `T(x)` from the tower, and set `l := l - 1` and `x := x - 1`. -2. Add block to tower. `T(l) := B`, `confcount(B) := 1`, and set `l := l + 1`. -3. Double lockouts. For each element `B = T(x)` if `l > x + confcount(B)`, then `confcount(B) := confcount(B) + 1`. - -## PoH ASIC Resistance - -Votes and lockouts grow exponentially while ASIC speed up is linear. There are possible attack vectors involving a faster ASIC outlined below. - -### ASIC Rollback - -An attacker generates a concurrent fork from an older block to try to rollback the cluster. In this attack the concurrent fork is competing with forks that have already been voted on. This attack is limited by the exponential growth of the lockouts. - -- 1 vote has a lockout of 2 slots. Concurrent fork must be at least 2 slots ahead, and be produced in 1 slot. Therefore requires an ASIC 2x faster. -- 2 votes have a lockout of 4 slots. Concurrent fork must be at least 4 slots ahead and produced in 2 slots. Therefore requires an ASIC 2x faster. -- 3 votes have a lockout of 8 slots. Concurrent fork must be at least 8 slots ahead and produced in 3 slots. Therefore requires an ASIC 2.6x faster. -- 10 votes have a lockout of 1024 slots. 1024/10, or 102.4x faster ASIC. -- 20 votes have a lockout of 2^20 slots. 2^20/20, or 52,428.8x faster ASIC. diff --git a/docs/src/implemented-proposals/transaction-fees.md b/docs/src/implemented-proposals/transaction-fees.md deleted file mode 100644 index 1163c3fcaed..00000000000 --- a/docs/src/implemented-proposals/transaction-fees.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -title: Deterministic Transaction Fees ---- - -## Calculating fees - -Before sending a transaction to the cluster, a client may query the network to -determine what the transaction's fee will be via the rpc request -[getFeeForMessage](https://solana.com/docs/rpc/http/getfeeformessage). - -## Fee Parameters - -The fee is based on the number of signatures in the transaction, the more -signatures a transaction contains, the higher the fee. In addition, a -transaction can specify an additional fee that determines how the transaction is -relatively prioritized against others. A transaction's prioritization fee is -calculated by multiplying the number of compute units by the compute unit price -(measured in micro-lamports) set by the transaction via compute budget -instructions. diff --git a/docs/src/implemented-proposals/validator-timestamp-oracle.md b/docs/src/implemented-proposals/validator-timestamp-oracle.md deleted file mode 100644 index cc0bcef54ad..00000000000 --- a/docs/src/implemented-proposals/validator-timestamp-oracle.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -title: Validator Timestamp Oracle ---- - -Third-party users of Solana sometimes need to know the real-world time a block -was produced, generally to meet compliance requirements for external auditors or -law enforcement. This proposal describes a validator timestamp oracle that -would allow a Solana cluster to satisfy this need. - -The general outline of the proposed implementation is as follows: - -- At regular intervals, each validator records its observed time for a known slot - on-chain (via a Timestamp added to a slot Vote) -- A client can request a block time for a rooted block using the `getBlockTime` - RPC method. When a client requests a timestamp for block N: - - 1. A validator determines a "cluster" timestamp for a recent timestamped slot - before block N by observing all the timestamped Vote instructions recorded on - the ledger that reference that slot, and determining the stake-weighted mean - timestamp. - - 2. This recent mean timestamp is then used to calculate the timestamp of - block N using the cluster's established slot duration - -Requirements: - -- Any validator replaying the ledger in the future must come up with the same - time for every block since genesis -- Estimated block times should not drift more than an hour or so before resolving - to real-world (oracle) data -- The block times are not controlled by a single centralized oracle, but - ideally based on a function that uses inputs from all validators -- Each validator must maintain a timestamp oracle - -The same implementation can provide a timestamp estimate for a not-yet-rooted -block. However, because the most recent timestamped slot may or may not be -rooted yet, this timestamp would be unstable (potentially failing requirement -1). Initial implementation will target rooted blocks, but if there is a use case -for recent-block timestamping, it will be trivial to add the RPC apis in the -future. - -## Recording Time - -At regular intervals as it is voting on a particular slot, each validator -records its observed time by including a timestamp in its Vote instruction -submission. The corresponding slot for the timestamp is the newest Slot in the -Vote vector (`Vote::slots.iter().max()`). It is signed by the validator's -identity keypair as a usual Vote. In order to enable this reporting, the Vote -struct needs to be extended to include a timestamp field, `timestamp: Option`, which will be set to `None` in most Votes. - -As of https://github.com/solana-labs/solana/pull/10630, validators submit a -timestamp every vote. This enables implementation of a block time caching -service that allows nodes to calculate the estimated timestamp immediately after -the block is rooted, and cache that value in Blockstore. This provides -persistent data and quick queries, while still meeting requirement 1) above. - -### Vote Accounts - -A validator's vote account will hold its most recent slot-timestamp in VoteState. - -### Vote Program - -The on-chain Vote program needs to be extended to process a timestamp sent with -a Vote instruction from validators. In addition to its current process_vote -functionality (including loading the correct Vote account and verifying that the -transaction signer is the expected validator), this process needs to compare the -timestamp and corresponding slot to the currently stored values to verify that -they are both monotonically increasing, and store the new slot and timestamp in -the account. - -## Calculating Stake-Weighted Mean Timestamp - -In order to calculate the estimated timestamp for a particular block, a -validator first needs to identify the most recently timestamped slot: - -```text -let timestamp_slot = floor(current_slot / timestamp_interval); -``` - -Then the validator needs to gather all Vote WithTimestamp transactions from the -ledger that reference that slot, using `Blockstore::get_slot_entries()`. As these -transactions could have taken some time to reach and be processed by the leader, -the validator needs to scan several completed blocks after the timestamp_slot to -get a reasonable set of Timestamps. The exact number of slots will need to be -tuned: More slots will enable greater cluster participation and more timestamp -datapoints; fewer slots will speed how long timestamp filtering takes. - -From this collection of transactions, the validator calculates the -stake-weighted mean timestamp, cross-referencing the epoch stakes from -`staking_utils::staked_nodes_at_epoch()`. - -Any validator replaying the ledger should derive the same stake-weighted mean -timestamp by processing the Timestamp transactions from the same number of -slots. - -## Calculating Estimated Time for a Particular Block - -Once the mean timestamp for a known slot is calculated, it is trivial to -calculate the estimated timestamp for subsequent block N: - -```text -let block_n_timestamp = mean_timestamp + (block_n_slot_offset * slot_duration); -``` - -where `block_n_slot_offset` is the difference between the slot of block N and -the timestamp_slot, and `slot_duration` is derived from the cluster's -`slots_per_year` stored in each Bank diff --git a/docs/src/index.mdx b/docs/src/index.mdx deleted file mode 100644 index 6314ed65581..00000000000 --- a/docs/src/index.mdx +++ /dev/null @@ -1,61 +0,0 @@ ---- -slug: / -id: home -title: Home -sidebar_label: Home -pagination_label: Agave Validator Documentation Home -description: "Agave is a validator for the Solana blockchain maintained by the Anza core engineering team \ - it is a fork of the original Solana validator software." ---- - -# Agave Validator Documentation - -Solana is a blockchain built for mass adoption. It's a high performance network -that is utilized for a range of use cases, including finance, NFTs, payments, -and gaming. Solana operates as a single global state machine, and is open, -interoperable and decentralized. Agave is a fork of the original Solana validator -previously maintained by the Solana Labs team. Agave is now under active development by the -core engineering team at Anza, one of several Solana validator clients. - -## Command Line Interface and Tool Suite - -To get started using the Solana Command Line (CLI) tools: - -- [Install the Solana CLI Tool Suite](./cli/install.md) - Quickly get setup - locally with the CLI, optionally build from source -- [Introduction to the CLI conventions](./cli/intro.md) - Understand the common - conventions used within the CLI tool suite -- [Choose a cluster](./cli/examples/choose-a-cluster.md) - Select a Solana - network cluster to connect (e.g. `devnet`, `testnet`, `mainnet-beta`) -- [Create a wallet](./cli/wallets/index.md) - Create a command line wallet for - use within the CLI and beyond - -## Understanding the Architecture - -Get to know the underlying architecture of how the proof-of-stake blockchain -works: - -- [Clusters](./clusters/index.md) - a collection of validators that work - together for consensus -- [Validators](./validator/anatomy.md) - the individual nodes that are the - backbone of the network -- [Runtime](./runtime/programs.md) - the native programs that are core to the - validator and the blockchain - -## Running a Validator - -Explore what it takes to operate an Agave validator and help secure the network. - -- [Validator vs RPC node](./operations/validator-or-rpc-node.md) - Understand - the important differences between voting and non-voting validators on the - network -- [System requirements](./operations/requirements.md) - Recommended hardware - requirements and expected SOL needed to operate a validator -- [Quick start guide](./operations/setup-a-validator.md) - Setup a validator and - get connected to a cluster for the first time - -## Learn more - -import HomeCtaLinks from "@site/components/HomeCtaLinks"; - - diff --git a/docs/src/operations/_category_.json b/docs/src/operations/_category_.json deleted file mode 100644 index 289f63ff950..00000000000 --- a/docs/src/operations/_category_.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "position": 4, - "label": "Operating a Validator", - "collapsible": true, - "collapsed": true -} diff --git a/docs/src/operations/best-practices/_category_.json b/docs/src/operations/best-practices/_category_.json deleted file mode 100644 index c26affc4d1b..00000000000 --- a/docs/src/operations/best-practices/_category_.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "position": 8, - "label": "Best Practices", - "collapsible": true, - "collapsed": true, - "link": null -} diff --git a/docs/src/operations/best-practices/general.md b/docs/src/operations/best-practices/general.md deleted file mode 100644 index 18c8d73cceb..00000000000 --- a/docs/src/operations/best-practices/general.md +++ /dev/null @@ -1,197 +0,0 @@ ---- -title: Agave Validator Operations Best Practices -sidebar_label: General Operations -pagination_label: "Best Practices: Validator Operations" ---- - -After you have successfully setup and started a -[validator on testnet](../setup-a-validator.md) (or another cluster -of your choice), you will want to become familiar with how to operate your -validator on a day-to-day basis. During daily operations, you will be -[monitoring your server](./monitoring.md), updating software regularly (both the -Solana validator software and operating system packages), and managing your vote -account and identity account. - -All of these skills are critical to practice. Maximizing your validator uptime -is an important part of being a good operator. - -## Educational Workshops - -The Solana validator community holds regular educational workshops. You can -watch past workshops through the -[Solana validator educational workshops playlist](https://www.youtube.com/watch?v=86zySQ5vGW8&list=PLilwLeBwGuK6jKrmn7KOkxRxS9tvbRa5p). - -## Community Validator calls - -The Solana validator community holds regular calls. -There is the 'Solana Foundation Validator Discussion' which is hosted by the Solana Foundation and the 'Community Led Validator Call' -which is hosted by the community itself. - -### Solana Foundation Validator Discussion - -This is a monthly call that is hosted by the Solana Foundation. -- Schedule: every second Thursday of the month 18:00 CET -- Agenda: See [validator-announcements channel in Discord](https://discord.com/channels/428295358100013066/586252910506016798). -- This call **is recorded** and past calls can be watched back on the [Community Validator Discussions playlist](https://www.youtube.com/playlist?list=PLilwLeBwGuK78yjGBZwYhTf7rao0t13Zw) - -### Community Led Validator Call - -This is also a monthly call hosted by the Solana validator community itself. -- Schedule: every fourth Thursday of the month 18:00 CET -- Agenda: See [HackMD site](https://hackmd.io/1DFauFMWTZG37-U7CXhxMg?view#Solana-Community-Validator-Call-Agendas). -- This call is **not recorded** - -***Please note that the scheduling of these calls can be changed last minute due to any circumstances. For the most up-to-date information go to the [validator-announcements channel in Discord](https://discord.com/channels/428295358100013066/586252910506016798).*** - -## Help with the validator command line - -From within the Solana CLI, you can execute the `agave-validator` command with -the `--help` flag to get a better understanding of the flags and sub commands -available. - -``` -agave-validator --help -``` - -## Restarting your validator - -There are many operational reasons you may want to restart your validator. As a -best practice, you should avoid a restart during a leader slot. A -[leader slot](https://solana.com/docs/terminology#leader-schedule) is the time -when your validator is expected to produce blocks. For the health of the cluster -and also for your validator's ability to earn transaction fee rewards, you do -not want your validator to be offline during an opportunity to produce blocks. - -To see the full leader schedule for an epoch, use the following command: - -``` -solana leader-schedule -``` - -Based on the current slot and the leader schedule, you can calculate open time -windows where your validator is not expected to produce blocks. - -Assuming you are ready to restart, you may use the `agave-validator exit` -command. The command exits your validator process when an appropriate idle time -window is reached. Assuming that you have systemd implemented for your validator -process, the validator should restart automatically after the exit. See the -below help command for details: - -``` -agave-validator exit --help -``` - -## Upgrading - -There are many ways to upgrade the -[Solana CLI software](../../cli/install.md). As an operator, you -will need to upgrade often, so it is important to get comfortable with this -process. - -> **Note** validator nodes do not need to be offline while the newest version is -> being built from source. All methods below can be done before -> the validator process is restarted. - -### Building the newest version from source - -The easiest way to upgrade the Solana CLI software is to build the newest -version from source. See the -[build from source](../../cli/install.md#build-from-source) instructions for details. - -### Restart - -The validator process will need to be restarted before -the newly installed version is in use. Use `agave-validator exit` to restart -your validator process. - -### Verifying version - -The best way to verify that your validator process has changed to the desired -version is to grep the logs after a restart. The following grep command should -show you the version that your validator restarted with: - -``` -grep -B1 'Starting validator with' -``` - -## Snapshots - -Validators operators who have not experienced significant downtime (multiple -hours of downtime), should avoid downloading snapshots. It is important for the -health of the cluster as well as your validator history to maintain the local -ledger. Therefore, you should not download a new snapshot any time your -validator is offline or experiences an issue. Downloading a snapshot should only -be reserved for occasions when you do not have local state. Prolonged downtime -or the first install of a new validator are examples of times when you may not -have state locally. In other cases, such as restarts for upgrades, a snapshot -download should be avoided. - -To avoid downloading a snapshot on restart, add the following flag to the -`agave-validator` command: - -``` ---no-snapshot-fetch -``` - -If you use this flag with the `agave-validator` command, make sure that you run -`solana catchup ` after your validator starts to make sure that the -validator is catching up in a reasonable time. After some time (potentially a -few hours), if it appears that your validator continues to fall behind, then you -may have to download a new snapshot. - -### Downloading Snapshots - -If you are starting a validator for the first time, or your validator has fallen -too far behind after a restart, then you may have to download a snapshot. - -To download a snapshot, you must **_NOT_** use the `--no-snapshot-fetch` flag. -Without the flag, your validator will automatically download a snapshot from -your known validators that you specified with the `--known-validator` flag. - -If one of the known validators is downloading slowly, you can try adding the -`--minimal-snapshot-download-speed` flag to your validator. This flag will -switch to another known validator if the initial download speed is below the -threshold that you set. - -## Regularly Check Account Balances - -It is important that you do not accidentally run out of funds in your identity -account, as your node will stop voting. It is also important to note that this -account keypair is the most vulnerable of the three keypairs in a vote account -because the keypair for the identity account is stored on your validator when -running the `agave-validator` software. How much SOL you should store there is -up to you. As a best practice, make sure to check the account regularly and -refill or deduct from it as needed. To check the account balance do: - -``` -solana balance validator-keypair.json -``` - -> **Note** `agave-watchtower` can monitor for a minimum validator identity -> balance. See [monitoring best practices](./monitoring.md) for details. - -## Withdrawing From The Vote Account - -As a reminder, your withdrawer's keypair should **_NEVER_** be stored on your -server. It should be stored on a hardware wallet, paper wallet, or multisig -mitigates the risk of hacking and theft of funds. - -To withdraw your funds from your vote account, you will need to run -`solana withdraw-from-vote-account` on a trusted computer. For example, on a -trusted computer, you could withdraw all of the funds from your vote account -(excluding the rent exempt minimum). The below example assumes you have a -separate keypair to store your funds called `person-keypair.json` - -``` -solana withdraw-from-vote-account \ - vote-account-keypair.json \ - person-keypair.json ALL \ - --authorized-withdrawer authorized-withdrawer-keypair.json -``` - -To get more information on the command, use -`solana withdraw-from-vote-account --help`. - -For a more detailed explanation of the different keypairs and other related -operations refer to -[vote account management](../guides/vote-accounts.md). diff --git a/docs/src/operations/best-practices/monitoring.md b/docs/src/operations/best-practices/monitoring.md deleted file mode 100644 index f681b55db82..00000000000 --- a/docs/src/operations/best-practices/monitoring.md +++ /dev/null @@ -1,96 +0,0 @@ ---- -title: Agave Validator Monitoring Best Practices -sidebar_label: Monitoring -pagination_label: "Best Practices: Validator Monitoring" ---- - -It is essential that you have monitoring in place on your validator. In the event that your validator is delinquent (behind the rest of the network) you want to respond immediately to fix the issue. One very useful tool to monitor your validator is [`agave-watchtower`](#agave-watchtower). - -## Agave Watchtower - -Agave Watchtower is an extremely useful monitoring tool that will regularly monitor the health of your validator. It can monitor your validator for delinquency then notify you on your application of choice: Slack, Discord, Telegram or Twilio. Additionally, `agave-watchtower` has the ability to monitor the health of the entire cluster so that you can be aware of any cluster wide problems. - -### Getting Started - -To get started with Agave Watchtower, run `agave-watchtower --help`. From the help menu, you can see the optional flags and an explanation of the command. - -Here is a sample command that will monitor a validator node with an identity public key of `2uTk98rqqwENevkPH2AHHzGHXgeGc1h6ku8hQUqWeXZp`: - -``` -agave-watchtower --monitor-active-stake --validator-identity \ - 2uTk98rqqwENevkPH2AHHzGHXgeGc1h6ku8hQUqWeXZp -``` - -The command will monitor your validator, but you will not get notifications unless you added the environment variables mentioned in `agave-watchtower --help`. Since getting each of these services setup for notifications is not straight forward, the next section will walk through [setting up watchtower notifications on Telegram](#setup-telegram-notifications). - -### Best Practices - -It is a best practice to run the `agave-watchtower` command on a separate server from your validator. - -In the case that you run `agave-watchtower` on the same computer as your `agave-validator` process, then during catastrophic events like a power outage, you will not be aware of the issue, because your `agave-watchtower` process will stop at the same time as your `agave-validator` process. - -Additionally, while running the `agave-watchtower` process manually with environment variables set in the terminal is a good way to test out the command, it is not operationally sound because the process will not be restarted when the terminal closes or during a system restart. - -Instead, you could run your `agave-watchtower` command as a system process similar to `agave-validator`. In the system process file, you can specify the environment variables for your bot. - -### Setup Telegram Notifications - -To send validator health notifications to your Telegram account, we are going to do a few things: - -1. Create a bot to send the messages. The bot will be created using BotFather on Telegram -2. Send a message to the bot -3. Create a Telegram group that will get the watchtower notifications -4. Add the environment variables to your command line environment -5. Restart the `agave-watchtower` command - -#### Create a Bot Using BotFather - -In Telegram, search for `@BotFather`. Send the following message to _@BotFather_: `/newbot`. - -Now you will have to come up with a name for the bot. The only requirement is that it cannot have dashes or spaces, and it **must** end in the word `bot`. Many names have already been taken, so you may have to try a few. Once you find an available name, you will get a response from _@BotFather_ that includes a link to chat with the bot as well as a token for the bot. Take note of the token. You will need it when you setup your environment variables. - -#### Send a Message to The Bot - -Find the bot in Telegram and send it the following message: `/start`. Messaging the bot will help you later when looking for the bot chatroom id. - -#### Create Telegram Group - -In Telegram, click on the new message icon and then select new group. Find your newly created bot and add the bot to the group. Next, name the group whatever you'd like. - -#### Set Environment Variables For Watchtower - -Now that you have a bot setup, you will need to set the environment variables for the bot so that watchtower can send notifications. - -First, recall the chat message that you got from _@BotFather_. In the message, there was an HTTP API token for your bot. The token will have this format: `389178471:MMTKMrnZB4ErUzJmuFIXTKE6DupLSgoa7h4o`. You will use that token to set the `TELEGRAM_BOT_TOKEN` environment variable. In the terminal where you plan to run `agave-watchtower`, run the following: - -``` -export TELEGRAM_BOT_TOKEN= -``` - -Next, you need the chat id for your group so that `solana-watcher` knows where to send the message. First, send a message to your bot in the chat group that you created. Something like `@newvalidatorbot hello`. - -Next, in your browser, go to `https://api.telegram.org/bot/getUpdates`. Make sure to replace `` with your API token that you got in the _@BotFather_ message. Also make sure that you include the word `bot` in the URL before the API token. Make the request in the browser. - -The response should be in JSON. Search for the string `"chat":` in the JSON. The `id` value of that chat is your `TELEGRAM_CHAT_ID`. It will be a negative number like: `-781559558`. Remember to include the negative sign! If you cannot find `"chat":` in the JSON, then you may have to remove the bot from your chat group and add it again. - -With your Telegram chat id in hand, export the environment variable where you plan to run `agave-watchtower`: - -``` -export TELEGRAM_CHAT_ID= -``` - -#### Restart agave-watchtower - -Once your environment variables are set, restart `agave-watchtower`. You should see output about your validator. - -To test that your Telegram configuration is working properly, you could stop your validator briefly until it is labeled as delinquent. Up to a minute after the validator is delinquent, you should receive a message in the Telegram group from your bot. Start the validator again and verify that you get another message in your Telegram group from the bot. The message should say `all clear`. - -## Collecting metrics - -It is important to collect metrics: it helps diagnose existing problems and allows to anticipate future ones. - -### metrics.solana.com - -There are several public dashboards available, one of them is hosted at [metrics.solana.com](https://metrics.solana.com). Reporting to the solana.com public dashboard is even required if you participate in the [Solana Foundation Delegation Program](https://solana.org/delegation-program). Using it is done by simply setting the `$SOLANA_METRICS_CONFIG` variable in your validator's environment (e.g. at the beginning of your `validator.sh` script). - -Refer to the [available Solana clusters documentation](../../clusters/available.md) to get the appropriate value of `$SOLANA_METRICS_CONFIG` for your validator. diff --git a/docs/src/operations/best-practices/security.md b/docs/src/operations/best-practices/security.md deleted file mode 100644 index bbaf120bc04..00000000000 --- a/docs/src/operations/best-practices/security.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -title: Agave Validator Security Best Practices -sidebar_label: Security -pagination_label: "Best Practices: Validator Security" ---- - -Being a system administrator for an Ubuntu computer requires technical knowledge of the system and best security practices. The following list should help you get started and is considered the bare minimum for keeping your system safe. - -## Keep Your System Up To Date - -Make sure to regularly update packages in your Ubuntu system. Out of date packages may contain known security vulnerabilities that a hacker could exploit. A good practice would be to update weekly at least. To update your system, do the following: - -``` -sudo apt update -sudo apt upgrade -``` - -## DO NOT Store Your Withdrawer Key On Your Validator Machine - -Your withdrawer key gives the operator full control of the vote account. It is highly sensitive information and should not be stored on the validator itself. - -There are a number of options for withdrawer key management. Some operators choose to use hardware wallets or paper wallets for the withdrawer keypair. Another option is a multisig where each key of the multisig is a hardware wallet or paper wallet. Whichever option you choose, make sure the authorized withdrawer key is stored securely and that it has been generated on a trusted computer (other than your validator computer). - -To reiterate, the withdrawer keypair should never be stored on your validator at any time. - -## DO NOT Run The Solana Validator as a Root User - -It may be easier to get started by running your application as root, but it is a bad practice. - -If there is an exploit in your system, a hacker could have full access if your Solana application is running as the `root` user. Instead, see the [setup instructions](../setup-a-validator.md#sol-user) for creating a user called `sol` and running the application as the `sol` user. - -## Close Ports That Are Not In Use - -Your system should close all ports that do not need to be open to the outside world. A common firewall for closing ports is `ufw` (uncomplicated firewall). You can find a guide to using `ufw` from [Digital Ocean](https://www.digitalocean.com/community/tutorials/ufw-essentials-common-firewall-rules-and-commands). - -## Eliminate Brute Force Attacks With fail2ban - -[fail2ban](https://github.com/fail2ban/fail2ban) is a network security tool that checks your logs for suspicious login attempts and bans those IP addresses after repeated attempts. This will help mitigate brute force attacks on your server. - -The default setup should work out-of-the-box by simply installing `fail2ban`: - -``` -sudo apt install fail2ban -``` - -## DO NOT Use Password Authentication for SSH - -In addition to installing `fail2ban`, it is recommended to disable password based authentication for SSH access. SSH key based authentication is preferred. diff --git a/docs/src/operations/guides/_category_.json b/docs/src/operations/guides/_category_.json deleted file mode 100644 index c399ad35754..00000000000 --- a/docs/src/operations/guides/_category_.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "position": 9, - "label": "Validator Guides", - "collapsible": true, - "collapsed": true, - "link": null -} diff --git a/docs/src/operations/guides/restart-cluster.md b/docs/src/operations/guides/restart-cluster.md deleted file mode 100644 index 0d3178b36a3..00000000000 --- a/docs/src/operations/guides/restart-cluster.md +++ /dev/null @@ -1,135 +0,0 @@ ---- -title: "Restarting a Solana Cluster" -# really high number to ensure it is listed last in the sidebar -sidebar_position: 999 -sidebar_label: Restart a Cluster -pagination_label: "Validator Guides: Restart a Cluster" ---- - -### Step 1. Identify the latest optimistically confirmed slot for the cluster - -In Agave 1.14 or greater, run the following command to output the latest -optimistically confirmed slot your validator observed: -```bash -agave-ledger-tool -l ledger latest-optimistic-slots -``` - -In Agave 1.13 or less, the latest optimistically confirmed can be found by looking for the more recent occurrence of -[this](https://github.com/solana-labs/solana/blob/0264147d42d506fb888f5c4c021a998e231a3e74/core/src/optimistic_confirmation_verifier.rs#L71) -metrics datapoint. - -Call this slot `SLOT_X` - -Note that it's possible that some validators observed an optimistically -confirmed slot that's greater than others before the outage. Survey the other -validators on the cluster to ensure that a greater optimistically confirmed slot -does not exist before proceeding. If a greater slot value is found use it -instead. - - -### Step 2. Stop the validator(s) - -### Step 3. Optionally install the new solana version - -### Step 4. Create a new snapshot for slot `SLOT_X` with a hard fork at slot `SLOT_X` - -```bash -$ agave-ledger-tool -l --snapshot-archive-path --incremental-snapshot-archive-path create-snapshot SLOT_X --hard-fork SLOT_X -``` - -The snapshots directory should now contain the new snapshot. -`agave-ledger-tool create-snapshot` will also output the new shred version, and bank hash value, -call this NEW_SHRED_VERSION and NEW_BANK_HASH respectively. - -Adjust your validator's arguments: - -```bash - --wait-for-supermajority SLOT_X - --expected-bank-hash NEW_BANK_HASH -``` - -Then restart the validator. - -Confirm with the log that the validator booted and is now in a holding pattern at `SLOT_X`, waiting for a super majority. - -Once NEW_SHRED_VERSION is determined, nudge foundation entrypoint operators to update entrypoints. - -### Step 5. Announce the restart on Discord: - -Post something like the following to #announcements (adjusting the text as appropriate): - -> Hi @Validators, -> -> We've released v1.1.12 and are ready to get testnet back up again. -> -> Steps: -> -> 1. Install the v1.1.12 release: https://github.com/solana-labs/solana/releases/tag/v1.1.12 -> 2. a. Preferred method, start from your local ledger with: -> -> ```bash -> agave-validator -> --wait-for-supermajority SLOT_X # <-- NEW! IMPORTANT! REMOVE AFTER THIS RESTART -> --expected-bank-hash NEW_BANK_HASH # <-- NEW! IMPORTANT! REMOVE AFTER THIS RESTART -> --hard-fork SLOT_X # <-- NEW! IMPORTANT! REMOVE AFTER THIS RESTART -> --no-snapshot-fetch # <-- NEW! IMPORTANT! REMOVE AFTER THIS RESTART -> --entrypoint entrypoint.testnet.solana.com:8001 -> --known-validator 5D1fNXzvv5NjV1ysLjirC4WY92RNsVH18vjmcszZd8on -> --expected-genesis-hash 4uhcVJyU9pJkvQyS88uRDiswHXSCkY3zQawwpjk2NsNY -> --only-known-rpc -> --limit-ledger-size -> ... # <-- your other --identity/--vote-account/etc arguments -> ``` -> -> b. If your validator doesn't have ledger up to slot SLOT_X or if you have deleted your ledger, have it instead download a snapshot with: -> -> ```bash -> agave-validator -> --wait-for-supermajority SLOT_X # <-- NEW! IMPORTANT! REMOVE AFTER THIS RESTART -> --expected-bank-hash NEW_BANK_HASH # <-- NEW! IMPORTANT! REMOVE AFTER THIS RESTART -> --entrypoint entrypoint.testnet.solana.com:8001 -> --known-validator 5D1fNXzvv5NjV1ysLjirC4WY92RNsVH18vjmcszZd8on -> --expected-genesis-hash 4uhcVJyU9pJkvQyS88uRDiswHXSCkY3zQawwpjk2NsNY -> --only-known-rpc -> --limit-ledger-size -> ... # <-- your other --identity/--vote-account/etc arguments -> ``` -> -> You can check for which slots your ledger has with: `agave-ledger-tool -l path/to/ledger bounds` -> -> 3. Wait until 80% of the stake comes online -> -> To confirm your restarted validator is correctly waiting for the 80%: -> a. Look for `N% of active stake visible in gossip` log messages -> b. Ask it over RPC what slot it's on: `solana --url http://127.0.0.1:8899 slot`. It should return `SLOT_X` until we get to 80% stake -> -> Thanks! - -### Step 7. Wait and listen - -Monitor the validators as they restart. Answer questions, help folks, - -## Troubleshooting - -### 80% of the stake didn't participate in the restart, now what? -If less than 80% of the stake join the restart after a reasonable amount of -time, it will be necessary to retry the restart attempt with the stake from the -non-responsive validators removed. - -The community should identify and come to social consensus on the set of -non-responsive validators. Then all participating validators return to Step 4 -and create a new snapshot with additional `--destake-vote-account ` -arguments for each of the non-responsive validator's vote account address - -```bash -$ agave-ledger-tool -l ledger create-snapshot SLOT_X ledger --hard-fork SLOT_X \ - --destake-vote-account \ - --destake-vote-account \ - . - . - --destake-vote-account \ -``` - -This will cause all stake associated with the non-responsive validators to be -immediately deactivated. All their stakers will need to re-delegate their stake -once the cluster restart is successful. diff --git a/docs/src/operations/guides/validator-failover.md b/docs/src/operations/guides/validator-failover.md deleted file mode 100644 index e9469ae61f7..00000000000 --- a/docs/src/operations/guides/validator-failover.md +++ /dev/null @@ -1,104 +0,0 @@ ---- -title: "Validator Guide: Setup Node Failover" -sidebar_position: 9 -sidebar_label: Node Failover -pagination_label: "Validator Guides: Node Failover" ---- - -A simple two machine instance failover method is described here, which allows you to: -* Upgrade your validator software with virtually no downtime, and -* Failover to the secondary instance when your monitoring detects a problem with the primary instance -without any safety issues that would otherwise be associated with running two instances of your validator. - -You will need: -* Two non-delinquent validator nodes -* Identities that are not associated with a staked vote account on both validators to use when not actively voting -* Validator startup scripts both modified to use a symbolic link as the identity -* Validator startup scripts both modified to include staked identity as authorized voter - -## Setup - -### Generating an Unstaked Secondary Identity - -Both validators need to have secondary (unstaked) identities to assume when not actively voting. -You can generate these secondary identities on each of your validators like so: -``` -solana-keygen new -s --no-bip39-passphrase -o unstaked-identity.json -``` -### Validator Startup Script Modifications - -The identity flag and authorized voter flags should be modified on both validators. - -Note that `identity.json` is not a real file but a symbolic link we will create shortly. -However, the authorized voter flag does need to point to the staked identity file (your main identity). -In this guide, the main identity is renamed to `staked-identity.json` for clarity and simplicity. -You can certainly name your main identity file however you'd like; make sure it is specified as an authorized voter as shown below: - -``` -exec /home/sol/bin/agave-validator \ - --identity /home/sol/identity.json \ - --vote-account /home/sol/vote.json \ - --authorized-voter /home/sol/staked-identity.json \ -``` - -Summary: - -* Identity is a symbolic link we will create in the next section. It may point to your staked identity or your inactive unstaked identity. -* No changed to the vote account, just shown for context. -* Authorized voter points to your main, staked identity. - -### Creating Identity Symlinks -An important part of how this system functions is the identity.json symbolic link. -This link allows us to soft link the desired identity so that the validator can restart or stop/start with the same identity we last intended it to have. - -On your actively voting validator, link this to your staked identity -``` -ln -sf /home/sol/staked-identity.json /home/sol/identity.json -``` - -On your inactive, non-voting validator, link this to your unstaked identity -``` -ln -sf /home/sol/unstaked-identity.json /home/sol/identity.json -``` - -### Transition Preparation Checklist -At this point, you should have completed the following on each validator: -* Generated an unstaked identity -* Updated your validator startup script -* Created a symbolic link to point to the respective identity file - -If you have done this - great! You're ready to transition! - -### Transition Process -#### Active Validator -* Wait for a restart window -* Set identity to unstaked identity -* Correct symbolic link to reflect this change -* Copy the tower file to the inactive validator - -``` -#!/bin/bash - -# example script of the above steps - change specifics such as user / IP / ledger path -agave-validator -l /mnt/ledger wait-for-restart-window --min-idle-time 2 --skip-new-snapshot-check -agave-validator -l /mnt/ledger set-identity /home/sol/unstaked-identity.json -ln -sf /home/sol/unstaked-identity.json /home/sol/identity.json -scp /mnt/ledger/tower-1_9-$(solana-keygen pubkey /home/sol/staked-identity.json).bin @/mnt/ledger -``` - -(At this point your primary identity is no longer voting) - -#### Inactive Validator -* Set identity to your staked identity (requiring the tower) -* Rewrite the symbolic link to reflect this - -``` -#!/bin/bash - -# example script of the above steps -agave-validator -l /mnt/ledger set-identity --require-tower /home/sol/staked-identity.json -ln -sf /home/sol/staked-identity.json /home/sol/identity.json -``` - -### Verification -Verify identities transitioned successfully using either `agave-validator monitor` or `solana catchup --our-localhost 8899` diff --git a/docs/src/operations/guides/validator-info.md b/docs/src/operations/guides/validator-info.md deleted file mode 100644 index 56b74f732c4..00000000000 --- a/docs/src/operations/guides/validator-info.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -title: "Validator Guide: Publishing Validator Info" -sidebar_position: 1 -sidebar_label: Publishing Validator Info -pagination_label: "Validator Guides: Publishing Validator Info" ---- - -You can publish your validator information to the chain to be publicly visible to other users. - -## Run solana validator-info - -Run the solana CLI to populate a validator info account: - -```bash -solana validator-info publish --keypair ~/validator-keypair.json -``` - -For details about optional fields for VALIDATOR_INFO_ARGS: - -```bash -solana validator-info publish --help -``` - -The recommended dimensions for the validator icon are 360x360px and PNG format. - -## Example Commands - -Example publish command: - -```bash -solana validator-info publish "Elvis Validator" -w "https://elvis-validates.com" -i "https://elvis-validates.com/my-icon.png" -``` - -Example query command: - -```bash -solana validator-info get -``` - -which outputs - -```text -Validator info from 8WdJvDz6obhADdxpGCiJKZsDYwTLNEDFizayqziDc9ah - Validator pubkey: 6dMH3u76qZ7XG4bVboVRnBHR2FfrxEqTTTyj4xmyDMWo - Info: {"iconUrl":"elvis","name":"Elvis Validator","website":"https://elvis-validates.com"} -``` - -For older accounts instead of `iconUrl` you might see `keybaseUsername` as those accounts used Keybase for their validator icon (see next section for further information). - -## Keybase - -Previously Keybase was used by validators to provide their validator icon, however Keybase has sunset its service and thus is no longer supported. Some old validator info accounts will still contain keybase usernames this is reflected in the serialized data returned when querying these validator info accounts. diff --git a/docs/src/operations/guides/validator-monitor.md b/docs/src/operations/guides/validator-monitor.md deleted file mode 100644 index 5e314c52e82..00000000000 --- a/docs/src/operations/guides/validator-monitor.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -title: "Validator Guide: Monitoring a Validator" -sidebar_position: 2 -sidebar_label: Monitoring a Validator -pagination_label: "Validator Guides: Monitoring a Validator" ---- - -## Check Gossip - -Confirm the IP address and **identity pubkey** of your validator is visible in -the gossip network by running: - -```bash -solana gossip -``` - -## Check Your Balance - -Your account balance should decrease by the transaction fee amount as your -validator submits votes, and increase after serving as the leader. Pass the -`--lamports` are to observe in finer detail: - -```bash -solana balance --lamports -``` - -## Check Vote Activity - -The `solana vote-account` command displays the recent voting activity from -your validator: - -```bash -solana vote-account ~/vote-account-keypair.json -``` - -## Get Cluster Info - -There are several useful JSON-RPC endpoints for monitoring your validator on the -cluster, as well as the health of the cluster: - -```bash -# Similar to solana-gossip, you should see your validator in the list of cluster nodes -curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getClusterNodes"}' http://api.devnet.solana.com -# If your validator is properly voting, it should appear in the list of `current` vote accounts. If staked, `stake` should be > 0 -curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getVoteAccounts"}' http://api.devnet.solana.com -# Returns the current leader schedule -curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getLeaderSchedule"}' http://api.devnet.solana.com -# Returns info about the current epoch. slotIndex should progress on subsequent calls. -curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getEpochInfo"}' http://api.devnet.solana.com -``` diff --git a/docs/src/operations/guides/validator-stake.md b/docs/src/operations/guides/validator-stake.md deleted file mode 100644 index 4c550cd3e8a..00000000000 --- a/docs/src/operations/guides/validator-stake.md +++ /dev/null @@ -1,164 +0,0 @@ ---- -title: "Validator Guide: Staking" -sidebar_position: 3 -sidebar_label: Staking -pagination_label: "Validator Guides: Staking" ---- - -**By default your validator will have no stake.** This means it will be -ineligible to become leader, and unable to land votes. - -## Monitoring Catch Up - -To delegate stake, first make sure your validator is running and has caught up -to the cluster. It may take some time to catch up after your validator boots. -Use the `catchup` command to monitor your validator through this process: - -```bash -solana catchup ~/validator-keypair.json -``` - -Until your validator has caught up, it will not be able to vote successfully and -stake cannot be delegated to it. - -Also if you find the cluster's slot advancing faster than yours, you will likely -never catch up. This typically implies some kind of networking issue between -your validator and the rest of the cluster. - -## Create Stake Keypair - -If you haven’t already done so, create a staking keypair. If you have completed -this step, you should see the "validator-stake-keypair.json" in your Solana -runtime directory. - -```bash -solana-keygen new -o ~/validator-stake-keypair.json -``` - -## Delegate Stake - -Now delegate 1 SOL to your validator by first creating your stake account: - -```bash -solana create-stake-account ~/validator-stake-keypair.json 1 -``` - -and then delegating that stake to your validator: - -```bash -solana delegate-stake ~/validator-stake-keypair.json ~/vote-account-keypair.json -``` - -> Don’t delegate your remaining SOL, as your validator will use those tokens to -> vote. - -Stakes can be re-delegated to another node at any time with the same command, -but only one re-delegation is permitted per epoch: - -```bash -solana delegate-stake ~/validator-stake-keypair.json ~/some-other-vote-account-keypair.json -``` - -## Validator Stake Warm-up - -To combat various attacks on consensus, new stake delegations are subject to a -[warm-up](https://solana.com/docs/references/staking/stake-accounts#delegation-warmup-and-cooldown) period. - -Monitor a validator's stake during warmup by: - -- View your vote account:`solana vote-account ~/vote-account-keypair.json` This - displays the current state of all the votes the validator has submitted to the - network. -- View your stake account, the delegation preference and details of your - stake:`solana stake-account ~/validator-stake-keypair.json` -- `solana validators` displays the current active stake of all validators, - including yours -- `solana stake-history` shows the history of stake warming up and cooling down - over recent epochs -- Look for log messages on your validator indicating your next leader slot: - `[2019-09-27T20:16:00.319721164Z INFO solana_core::replay_stage] voted and reset PoH at tick height ####. My next leader slot is ####` -- Once your stake is warmed up, you will see a stake balance listed for your - validator by running `solana validators` - -## Validator Rewards - -Once your stake is warmed up, and assuming the node is voting, you will now be -generating validator rewards. Rewards are paid automatically on epoch -boundaries. - -The rewards lamports earned are split between your stake account and the vote -account according to the commission rate set in the vote account. Rewards can -only be earned while the validator is up and running. Further, once staked, the -validator becomes an important part of the network. In order to safely remove a -validator from the network, first deactivate its stake. - -At the end of each slot, a validator is expected to send a vote transaction. -These vote transactions are paid for by lamports from a validator's identity -account. - -This is a normal transaction so the standard transaction fee will apply. The -transaction fee range is defined by the genesis block. The actual fee will -fluctuate based on transaction load. You can determine the current fee via the -[RPC API "getRecentBlockhash"](https://solana.com/docs/rpc/deprecated/getrecentblockhash) before submitting -a transaction. - -Learn more about -[transaction fees here](../../implemented-proposals/transaction-fees.md). - -## Monitor Your Staked Validator - -Confirm your validator becomes a -[leader](https://solana.com/docs/terminology#leader) - -- After your validator is caught up, use the `solana balance` command to monitor - the earnings as your validator is selected as leader and collects transaction - fees -- Solana nodes offer a number of useful JSON-RPC methods to return information - about the network and your validator's participation. Make a request by using - curl \(or another http client of your choosing\), specifying the desired - method in JSON-RPC-formatted data. For example: - -```bash - // Request - curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getEpochInfo"}' http://localhost:8899 - - // Result - {"jsonrpc":"2.0","result":{"epoch":3,"slotIndex":126,"slotsInEpoch":256},"id":1} -``` - -Helpful JSON-RPC methods: - -- `getEpochInfo`[An epoch](https://solana.com/docs/terminology#epoch) is the - time, i.e. number of [slots](https://solana.com/docs/terminology#slot), for - which a [leader schedule](https://solana.com/docs/terminology#leader-schedule) - is valid. This will tell you what the current epoch is and how far into it the - cluster is. -- `getVoteAccounts` This will tell you how much active stake your validator - currently has. A % of the validator's stake is activated on an epoch boundary. - You can learn more about staking on Solana - [here](../../consensus/stake-delegation-and-rewards.md). -- `getLeaderSchedule` At any given moment, the network expects only one - validator to produce ledger entries. The - [validator currently selected to produce ledger entries](../../consensus/leader-rotation.md#leader-rotation) - is called the "leader". This will return the complete leader schedule \(on a - slot-by-slot basis\) for currently activated stake, the identity pubkey will - show up 1 or more times here. - -## Deactivating Stake - -Before detaching your validator from the cluster, you should deactivate the -stake that was previously delegated by running: - -```bash -solana deactivate-stake ~/validator-stake-keypair.json -``` - -Stake is not deactivated immediately and instead cools down in a similar fashion -as stake warm up. Your validator should remain attached to the cluster while the -stake is cooling down. While cooling down, your stake will continue to earn -rewards. Only after stake cooldown is it safe to turn off your validator or -withdraw it from the network. Cooldown may take several epochs to complete, -depending on active stake and the size of your stake. - -Note that a stake account may only be used once, so after deactivation, use the -cli's `withdraw-stake` command to recover the previously staked lamports. diff --git a/docs/src/operations/guides/validator-start.md b/docs/src/operations/guides/validator-start.md deleted file mode 100644 index 804da5fcbac..00000000000 --- a/docs/src/operations/guides/validator-start.md +++ /dev/null @@ -1,452 +0,0 @@ ---- -title: "Validator Guide: Starting a Validator" -sidebar_position: 0 -sidebar_label: Starting a Validator -pagination_label: "Validator Guides: Starting a Validator" ---- - -## Configure Solana CLI - -The solana cli includes `get` and `set` configuration commands to automatically -set the `--url` argument for cli commands. For example: - -```bash -solana config set --url http://api.devnet.solana.com -``` - -While this section demonstrates how to connect to the Devnet cluster, the steps -are similar for the other [Solana Clusters](../../clusters/available.md). - -## Confirm The Cluster Is Reachable - -Before attaching a validator node, sanity check that the cluster is accessible -to your machine by fetching the transaction count: - -```bash -solana transaction-count -``` - -View the [metrics dashboard](https://metrics.solana.com:3000/d/monitor-edge/cluster-telemetry) for more -detail on cluster activity. - -## System Tuning - -### Linux - -If you would prefer to manage system settings on your own, you may do so with -the following commands. - -#### **Optimize sysctl knobs** - -```bash -sudo bash -c "cat >/etc/sysctl.d/21-agave-validator.conf </etc/security/limits.d/90-solana-nofiles.conf < Note: The "validator-keypair.json" file is also your \(ed25519\) private key. - -### Paper Wallet identity - -You can create a paper wallet for your identity file instead of writing the -keypair file to disk with: - -```bash -solana-keygen new --no-outfile -``` - -The corresponding identity public key can now be viewed by running: - -```bash -solana-keygen pubkey ASK -``` - -and then entering your seed phrase. - -See [Paper Wallet Usage](../../cli/wallets/paper.md) for more info. - ---- - -### Vanity Keypair - -You can generate a custom vanity keypair using solana-keygen. For instance: - -```bash -solana-keygen grind --starts-with e1v1s:1 -``` - -You may request that the generated vanity keypair be expressed as a seed phrase -which allows recovery of the keypair from the seed phrase and an optionally -supplied passphrase (note that this is significantly slower than grinding without -a mnemonic): - -```bash -solana-keygen grind --use-mnemonic --starts-with e1v1s:1 -``` - -Depending on the string requested, it may take days to find a match... - ---- - -Your validator identity keypair uniquely identifies your validator within the -network. **It is crucial to back-up this information.** - -If you don’t back up this information, you WILL NOT BE ABLE TO RECOVER YOUR -VALIDATOR if you lose access to it. If this happens, YOU WILL LOSE YOUR -ALLOCATION OF SOL TOO. - -To back-up your validator identify keypair, **back-up your -"validator-keypair.json" file or your seed phrase to a secure location.** - -## More Solana CLI Configuration - -Now that you have a keypair, set the solana configuration to use your validator -keypair for all following commands: - -```bash -solana config set --keypair ~/validator-keypair.json -``` - -You should see the following output: - -```text -Config File: /home/solana/.config/solana/cli/config.yml -RPC URL: http://api.devnet.solana.com -WebSocket URL: ws://api.devnet.solana.com/ (computed) -Keypair Path: /home/solana/validator-keypair.json -Commitment: confirmed -``` - -## Airdrop & Check Validator Balance - -Airdrop yourself some SOL to get started: - -```bash -solana airdrop 1 -``` - -Note that airdrops are only available on Devnet and Testnet. Both are limited -to 1 SOL per request. - -To view your current balance: - -```text -solana balance -``` - -Or to see in finer detail: - -```text -solana balance --lamports -``` - -Read more about the difference between SOL and lamports here: [What is SOL?](https://solana.com/docs/references/terminology#sol), [What is a lamport?](https://solana.com/docs/references/terminology#lamport). - -## Create Authorized Withdrawer Account - -If you haven't already done so, create an authorized-withdrawer keypair to be used -as the ultimate authority over your validator. This keypair will have the -authority to withdraw from your vote account, and will have the additional -authority to change all other aspects of your vote account. Needless to say, -this is a very important keypair as anyone who possesses it can make any -changes to your vote account, including taking ownership of it permanently. -So it is very important to keep your authorized-withdrawer keypair in a safe -location. It does not need to be stored on your validator, and should not be -stored anywhere from where it could be accessed by unauthorized parties. To -create your authorized-withdrawer keypair: - -```bash -solana-keygen new -o ~/authorized-withdrawer-keypair.json -``` - -## Create Vote Account - -If you haven’t already done so, create a vote-account keypair and create the -vote account on the network. If you have completed this step, you should see the -"vote-account-keypair.json" in your Solana runtime directory: - -```bash -solana-keygen new -o ~/vote-account-keypair.json -``` - -The following command can be used to create your vote account on the blockchain -with all the default options: - -```bash -solana create-vote-account ~/vote-account-keypair.json ~/validator-keypair.json ~/authorized-withdrawer-keypair.json -``` - -Remember to move your authorized withdrawer keypair into a very secure location after running the above command. - -After SIMD-0387 has been activated on your cluster, set the BLS public key for -your vote account before starting the validator. Follow the -[BLS public key instructions](./vote-accounts.md#set-the-bls-public-key). - -Read more about [creating and managing a vote account](./vote-accounts.md). - -## Known validators - -If you know and respect other validator operators, you can specify this on the -command line with the `--known-validator ` argument to -`agave-validator`. You can specify multiple ones by repeating the argument -`--known-validator --known-validator `. This has the effect -that when the validator is booting with `--only-known-rpc`, it will only ask -that set of known nodes for downloading genesis and snapshot data. - -It is highly recommended you use this option to prevent malicious snapshot -state download. - -## Connect Your Validator - -Connect to the cluster by running: - -```bash -agave-validator \ - --identity ~/validator-keypair.json \ - --vote-account ~/vote-account-keypair.json \ - --rpc-port 8899 \ - --entrypoint entrypoint.devnet.solana.com:8001 \ - --limit-ledger-size \ - --log ~/agave-validator.log -``` - -To force validator logging to the console add a `--log -` argument, otherwise -the validator will automatically log to a file. - -The ledger will be placed in the `ledger/` directory by default, use the -`--ledger` argument to specify a different location. - -> Note: You can use a -> [paper wallet seed phrase](../../cli/wallets/paper.md) -> for your `--identity` and/or -> `--authorized-voter` keypairs. To use these, pass the respective argument as -> `agave-validator --identity ASK ... --authorized-voter ASK ...` -> and you will be prompted to enter your seed phrases and optional passphrase. - -Confirm your validator is connected to the network by opening a new terminal and -running: - -```bash -solana gossip -``` - -If your validator is connected, its public key and IP address will appear in the list. - -### Controlling local network port allocation - -By default the validator will dynamically select available network ports in the -8000-10000 range, and may be overridden with `--dynamic-port-range`. For -example, `agave-validator --dynamic-port-range 11000-11020 ...` will restrict -the validator to ports 11000-11020. - -### Limiting ledger size to conserve disk space - -The `--limit-ledger-size` parameter allows you to specify how many ledger -[shreds](https://solana.com/docs/terminology#shred) your node retains on disk. -If you do not include this parameter, the validator will keep all received -ledger data until it runs out of disk space. Otherwise, the validator will -periodically purge the oldest data (FIFO) to remain under the specified -`--limit-ledger-size` value. - -The default value attempts to keep the blockstore (data within the rocksdb -directory) disk usage under 500 GB. More or less disk usage may be requested -by adding an argument to `--limit-ledger-size` if desired. More information -about selecting a custom limit value is [available -here](https://github.com/solana-labs/solana/blob/aa72aa87790277619d12c27f1ebc864d23739557/core/src/ledger_cleanup_service.rs#L26-L37). - -Note that the above target of 500 GB does not account for other items that -may reside in the `ledger` directory, depending on validator configuration. -These items may include (but are not limited to): -- Persistent accounts data -- Persistent accounts index -- Snapshots - -Additionally, specifying `--enable-rpc-transaction-history` will store extra -block and transaction metadata. The space required to store this data varies -with cluster activity, and is hard to account for. Thus, using this flag will -likely cause the 500 GB target to be exceeded. - -### Systemd Unit - -Running the validator as a systemd unit is one easy way to manage running in the -background. - -Assuming you have a user called `sol` on your machine, create the file `/etc/systemd/system/sol.service` with -the following: - -``` -[Unit] -Description=Solana Validator -After=network.target -StartLimitIntervalSec=0 - -[Service] -Type=simple -Restart=always -RestartSec=1 -User=sol -LimitNOFILE=1000000 -LimitMEMLOCK=2000000000 -LogRateLimitIntervalSec=0 -Environment="PATH=/bin:/usr/bin:/home/sol/.local/share/solana/install/active_release/bin" -ExecStart=/home/sol/bin/validator.sh - -[Install] -WantedBy=multi-user.target -``` - -Now create `/home/sol/bin/validator.sh` to include the desired -`agave-validator` command-line. Ensure that the 'exec' command is used to -start the validator process (i.e. "exec agave-validator ..."). This is -important because without it, logrotate will end up killing the validator -every time the logs are rotated. - -Ensure that running `/home/sol/bin/validator.sh` manually starts -the validator as expected. Don't forget to mark it executable with `chmod +x /home/sol/bin/validator.sh` - -Start the service with: - -```bash -sudo systemctl enable --now sol -``` - -### Logging - -#### Log output tuning - -The messages that a validator emits to the log can be controlled by the `RUST_LOG` -environment variable. Details can by found in the [documentation](https://docs.rs/env_logger/latest/env_logger/#enabling-logging) -for the `env_logger` Rust crate. - -Note that if logging output is reduced, this may make it difficult to debug issues -encountered later. Should support be sought from the team, any changes will need -to be reverted and the issue reproduced before help can be provided. - -#### Log rotation - -The validator log file, as specified by `--log ~/agave-validator.log`, can get -very large over time and it's recommended that log rotation be configured. - -The validator will re-open its log file when it receives the `USR1` signal, which is the -basic primitive that enables log rotation. - -If the validator is being started by a wrapper shell script, it is important to -launch the process with `exec` (`exec agave-validator ...`) when using logrotate. -This will prevent the `USR1` signal from being sent to the script's process -instead of the validator's, which will kill them both. - -#### Using logrotate - -An example setup for the `logrotate`, which assumes that the validator is -running as a systemd service called `sol.service` and writes a log file at -/home/sol/agave-validator.log: - -```bash -# Setup log rotation - -cat > logrotate.sol </rocksdb/ list_column_families -``` - -**Warning**: Please seek guidance on discord before modifying the validator -blockstore. - -Drop a column family: -``` -ldb --db=/rocksdb drop_column_family -``` diff --git a/docs/src/operations/guides/vote-accounts.md b/docs/src/operations/guides/vote-accounts.md deleted file mode 100644 index 75fdae01f9f..00000000000 --- a/docs/src/operations/guides/vote-accounts.md +++ /dev/null @@ -1,332 +0,0 @@ ---- -title: "Validator Guide: Vote Account Management" -sidebar_position: 5 -sidebar_label: Vote Account Management -pagination_label: "Validator Guides: Vote Account Management" ---- - -This page describes how to set up an on-chain _vote account_. Creating a vote -account is needed if you plan to run a validator node on Solana. - -## Create a Vote Account - -A vote account can be created with the -[create-vote-account](../../cli/usage.md#solana-create-vote-account) command. The -vote account can be configured when first created or after the validator is -running. All aspects of the vote account can be changed except for the -[vote account address](#vote-account-address), which is fixed for the lifetime -of the account. - -### Configure an Existing Vote Account - -- To change the [validator identity](#validator-identity), use - [vote-update-validator](../../cli/usage.md#solana-vote-update-validator). -- To change the [vote authority](#vote-authority), use - [vote-authorize-voter-checked](../../cli/usage.md#solana-vote-authorize-voter-checked). -- To set the BLS public key associated with the - [vote authority](#vote-authority), use - [vote-authorize-voter-checked](../../cli/usage.md#solana-vote-authorize-voter-checked) - after SIMD-0387 is active on the cluster. -- To change the [authorized withdrawer](#authorized-withdrawer), use - [vote-authorize-withdrawer-checked](../../cli/usage.md#solana-vote-authorize-withdrawer-checked). -- To change the [commission](#commission), use - [vote-update-commission](../../cli/usage.md#solana-vote-update-commission). - -### Set the BLS Public Key - -Voting validators must set the BLS public key associated with their authorized voter -after SIMD-0387, BLS pubkey management, is active on the target cluster. The -Solana CLI will not set the BLS public key before the feature is active. - -> **IMPORTANT** As of June 2026, SIMD-0387 is active only on testnet. Setting -> the BLS public key is a prerequisite for SIMD-0357. If the BLS public key is -> not set, the vote account will behave as unstaked once SIMD-0357 is active. - -For almost all validators, the authorized voter is the validator identity. If -you are unsure which keypair is the current authorized voter, run: - -```bash -solana vote-account | grep "Vote Authority" -``` - -Then set the BLS public key by running the following command. - -```bash -solana vote-authorize-voter-checked -``` - -This does not change the authorized voter. It fills in the BLS public key -associated with the authorized voter. - -For a new vote account, follow the normal -[create vote account](#create-a-vote-account) instructions first, then run the -same `vote-authorize-voter-checked` command above to set the BLS public key. - -To check whether the BLS public key is set on chain, run: - -```bash -solana vote-account | grep "BLS Public Key" -``` - -If the command does not print a `BLS Public Key` line, the vote account does not -have a BLS public key set. - -To view the BLS public key derived from the authorized voter keypair locally, -run: - -```bash -solana-keygen bls_pubkey -``` - -## Vote Account Structure - -### Vote Account Address - -A vote account is created at an address that is either the public key of a -keypair file, or at a derived address based on a keypair file's public key and a -seed string. - -The address of a vote account is never needed to sign any transactions, but is -just used to look up the account information. - -When someone wants to -[delegate tokens in a stake account](https://solana.com/staking), -the delegation command is pointed at the vote account address of the validator -to whom the token-holder wants to delegate. - -### Validator Identity - -The _validator identity_ is a system account that is used to pay for all the -vote transaction fees submitted to the vote account. Because the validator is -expected to vote on most valid blocks it receives, the validator identity -account is frequently (potentially multiple times per second) signing -transactions and paying fees. For this reason the validator identity keypair -must be stored as a "hot wallet" in a keypair file on the same system the -validator process is running. - -Because a hot wallet is generally less secure than an offline or "cold" wallet, -the validator operator may choose to store only enough SOL on the identity -account to cover voting fees for a limited amount of time, such as a few weeks -or months. The validator identity account could be periodically topped off from -a more secure wallet. - -This practice can reduce the risk of loss of funds if the validator node's disk -or file system becomes compromised or corrupted. - -The validator identity is required to be provided when a vote account is -created. The validator identity can also be changed after an account is created -by using the -[vote-update-validator](../../cli/usage.md#solana-vote-update-validator) command. - -### Vote Authority - -The _vote authority_ keypair is used to sign each vote transaction the validator -node wants to submit to the cluster. This doesn't necessarily have to be unique -from the validator identity, as you will see later in this document. Because the -vote authority, like the validator identity, is signing transactions frequently, -this also must be a hot keypair on the same file system as the validator -process. - -The vote authority can be set to the same address as the validator identity. If -the validator identity is also the vote authority, only one signature per vote -transaction is needed in order to both sign the vote and pay the transaction -fee. Because transaction fees on Solana are assessed per-signature, having one -signer instead of two will result in half the transaction fee paid compared to -setting the vote authority and validator identity to two different accounts. - -The vote authority can be set when the vote account is created. If it is not -provided, the default behavior is to assign it the same as the validator -identity. The vote authority can be changed later with the -[vote-authorize-voter-checked](../../cli/usage.md#solana-vote-authorize-voter-checked) -command. - -The vote authority can be changed at most once per epoch. If the authority is -changed with -[vote-authorize-voter-checked](../../cli/usage.md#solana-vote-authorize-voter-checked), -this will not take effect until the beginning of the next epoch. To support a -smooth transition of the vote signing, `agave-validator` allows the -`--authorized-voter` argument to be specified multiple times. This allows the -validator process to keep voting successfully when the network reaches an epoch -boundary at which the validator's vote authority account changes. - -### Authorized Withdrawer - -The _authorized withdrawer_ keypair is used to withdraw funds from a vote -account using the -[withdraw-from-vote-account](../../cli/usage.md#solana-withdraw-from-vote-account) -command. Any network rewards a validator earns are deposited into the vote -account and are only retrievable by signing with the authorized withdrawer -keypair. - -The authorized withdrawer is also required to sign any transaction to change a -vote account's [commission](#commission), and to change the validator identity -on a vote account. - -Because theft of an authorized withdrawer keypair can give complete control over -the operation of a validator to an attacker, it is advised to keep the withdraw -authority keypair in an offline/cold wallet in a secure location. The withdraw -authority keypair is not needed during operation of a validator and should not -stored on the validator itself. - -The authorized withdrawer must be set when the vote account is created. It must -not be set to a keypair that is the same as either the validator identity -keypair or the vote authority keypair. - -The authorized withdrawer can be changed later with the -[vote-authorize-withdrawer-checked](../../cli/usage.md#solana-vote-authorize-withdrawer-checked) -command. - -### Commission - -_Commission_ is the percent of network rewards earned by a validator that are -deposited into the validator's vote account. The remainder of the rewards are -distributed to all of the stake accounts delegated to that vote account, -proportional to the active stake weight of each stake account. - -For example, if a vote account has a commission of 10%, for all rewards earned -by that validator in a given epoch, 10% of these rewards will be deposited into -the vote account in the first block of the following epoch. The remaining 90% -will be deposited into delegated stake accounts as immediately active stake. - -A validator may choose to set a low commission to try to attract more stake -delegations as a lower commission results in a larger percentage of rewards -passed along to the delegator. As there are costs associated with setting up and -operating a validator node, a validator would ideally set a high enough -commission to at least cover their expenses. - -Commission can be set upon vote account creation with the `--commission` option. -If it is not provided, it will default to 100%, which will result in all rewards -deposited in the vote account, and none passed on to any delegated stake -accounts. - -Commission can also be changed later with the -[vote-update-commission](../../cli/usage.md#solana-vote-update-commission) command. - -When setting the commission, only integer values in the set [0-100] are -accepted. The integer represents the number of percentage points for the -commission, so creating an account with `--commission 10` will set a 10% -commission. - -Note that validators can only update their commission during the first half of -any epoch. This prevents validators from stealing delegator rewards by setting a -low commission, increasing it right before the end of the epoch, and then -changing it back after reward distribution. - -## Key Rotation - -Rotating the vote account authority keys requires special handling when dealing -with a live validator. - -Note that vote account key rotation has no effect on the stake accounts that -have been delegated to the vote account. For example it is possible to use key -rotation to transfer all authority of a vote account from one entity to another -without any impact to staking rewards. - -### Vote Account Validator Identity - -You will need access to the _authorized withdrawer_ keypair for the vote account -to change the validator identity. The following steps assume that -`~/authorized_withdrawer.json` is that keypair. - -1. Create the new validator identity keypair, - `solana-keygen new -o ~/new-validator-keypair.json`. -2. Ensure that the new identity account has been funded, - `solana transfer ~/new-validator-keypair.json 500`. -3. Run - `solana vote-update-validator ~/vote-account-keypair.json ~/new-validator-keypair.json ~/authorized_withdrawer.json` - to modify the validator identity in your vote account -4. Restart your validator with the new identity keypair for the `--identity` - argument - -**Additional steps are required if your validator has stake.** The leader -schedule is computed two epochs in advance. Therefore if your old validator -identity was in the leader schedule, it will remain in the leader schedule for -up to two epochs after the validator identity change. If extra steps are not -taken your validator will produce no blocks until your new validator identity is -added to the leader schedule. - -After your validator is restarted with the new identity keypair, per step 4, -start a second non-voting validator on a different machine with the old identity -keypair without providing the `--vote-account` argument, as well as with the -`--no-wait-for-vote-to-start-leader` argument. - -This temporary validator should be run for two full epochs. During this time it -will: - -- Produce blocks for the remaining slots that are assigned to your old validator - identity -- Receive the transaction fees and rent rewards for your old validator identity - -It is safe to stop this temporary validator when your old validator identity is -no longer listed in the `solana leader-schedule` output. - -### Vote Account Authorized Voter - -The _vote authority_ keypair may only be changed at epoch boundaries and -requires some additional arguments to `agave-validator` for a seamless -migration. - -1. Run `solana epoch-info`. If there is not much time remaining time in the - current epoch, consider waiting for the next epoch to allow your validator - plenty of time to restart and catch up. -2. Create the new vote authority keypair, - `solana-keygen new -o ~/new-vote-authority.json`. -3. Determine the current _vote authority_ keypair by running - `solana vote-account ~/vote-account-keypair.json`. It may be validator's - identity account (the default) or some other keypair. The following steps - assume that `~/validator-keypair.json` is that keypair. -4. Run - `solana vote-authorize-voter-checked ~/vote-account-keypair.json ~/validator-keypair.json ~/new-vote-authority.json`. - The new vote authority is scheduled to become active starting at the next - epoch. -5. `agave-validator` now needs to be restarted with the old and new vote - authority keypairs, so that it can smoothly transition at the next epoch. Add - the two arguments on restart: - `--authorized-voter ~/validator-keypair.json --authorized-voter ~/new-vote-authority.json` -6. After the cluster reaches the next epoch, remove the - `--authorized-voter ~/validator-keypair.json` argument and restart - `agave-validator`, as the old vote authority keypair is no longer required. - -### Vote Account Authorized Withdrawer - -No special handling or timing considerations are required. Use the -`solana vote-authorize-withdrawer-checked` command as needed. - -### Consider Durable Nonces for a Trustless Transfer of the Authorized Voter or Withdrawer - -If the Authorized Voter or Withdrawer is to be transferred to another entity -then a two-stage signing process using a -[Durable Nonce](../../cli/examples/durable-nonce.md) is recommended. - -1. Entity B creates a durable nonce using `solana create-nonce-account` -2. Entity B then runs a `solana vote-authorize-voter-checked` or - `solana vote-authorize-withdrawer-checked` command, including: - -- the `--sign-only` argument -- the `--nonce`, `--nonce-authority`, and `--blockhash` arguments to specify the - nonce particulars -- the address of the Entity A's existing authority, and the keypair for Entity - B's new authority - -3. When the `solana vote-authorize-...-checked` command successfully executes, - it will output transaction signatures that Entity B must share with Entity A -4. Entity A then runs a similar `solana vote-authorize-voter-checked` or - `solana vote-authorize-withdrawer-checked` command with the following - changes: - -- the `--sign-only` argument is removed, and replaced with a `--signer` argument - for each of the signatures provided by Entity B -- the address of Entity A's existing authority is replaced with the - corresponding keypair, and the keypair for Entity B's new authority is - replaced with the corresponding address - -On success the authority is now changed without Entity A or B having to reveal -keypairs to the other even though both entities signed the transaction. - -## Close a Vote Account - -A vote account can be closed with the -[close-vote-account](../../cli/usage.md#solana-close-vote-account) command. Closing -a vote account withdraws all remaining SOL funds to a supplied recipient address -and renders it invalid as a vote account. It is not possible to close a vote -account with active stake. diff --git a/docs/src/operations/index.md b/docs/src/operations/index.md deleted file mode 100644 index 54312ae38d8..00000000000 --- a/docs/src/operations/index.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: Operating a Validator -sidebar_position: 0 ---- - -This section describes how to run an Agave validator node. - -There are several clusters available to connect to; see [Choosing a Cluster](../cli/examples/choose-a-cluster.md) for an overview of each. diff --git a/docs/src/operations/prerequisites.md b/docs/src/operations/prerequisites.md deleted file mode 100644 index caa3e58cca6..00000000000 --- a/docs/src/operations/prerequisites.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: Agave Validator Prerequisites -sidebar_position: 2 -sidebar_label: Prerequisites -pagination_label: Prerequisites to run a Validator ---- - -Operating an Agave validator is an interesting and rewarding task. Generally speaking, it requires someone with a technical background but also involves community engagement and marketing. - -## How to be a good Validator Operator - -Here is a list of some of the requirements for being a good operator: - -- Performant computer hardware and a fast internet connection - - You can find a list of [hardware requirements here](./requirements.md) -- Knowledge of the Linux terminal -- Linux system administration - - Accessing your machine via ssh and scp - - Installing software (installing from source is encouraged) - - Keeping your Linux distribution up to date - - Managing users and system access - - Understanding computer processes - - Understanding networking basics - - Formatting and mounting drives - - Managing firewall rules (UFW/iptables) -- Hardware performance monitoring -- Cluster and node monitoring -- Quick response times in case of a validator issue -- Marketing and communications to attract delegators -- Customer support - -Whether you decide to run a [validator](../what-is-a-validator.md) or an [RPC node](../what-is-an-rpc-node.md), you should consider all of these areas of expertise. A team of people is likely necessary for you to achieve your goals. - -## Can I use my computer at home? - -While anyone can join the network, you should make sure that your home computer and network meets the specifications in the [hardware requirements](./requirements.md) doc. Most home internet service providers do not provide consistent service that would allow your validator to perform well. If your home network or personal hardware is not performant enough to keep up with the Solana cluster, your validator will not be able to participate in consensus. - -In addition to performance considerations, you will want to make sure that your home computer is resistant to outages caused by loss of power, flooding, fire, theft, etc. If you are just getting started on the testnet cluster and learning about being an operator, a home setup may be sufficient, but you will want to consider all of these factors when you start operating your validator on the mainnet-beta cluster. diff --git a/docs/src/operations/requirements.md b/docs/src/operations/requirements.md deleted file mode 100644 index 6b828adf069..00000000000 --- a/docs/src/operations/requirements.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -title: Agave Validator Requirements -sidebar_position: 3 -sidebar_label: Requirements -pagination_label: Requirements to Operate a Validator ---- - -## Minimum SOL requirements - -There is no strict minimum amount of SOL required to run an Agave validator on Solana. - -However in order to participate in consensus, a vote account is required which -has a rent-exempt reserve of 0.02685864 SOL. Voting also requires sending a vote -transaction for each block the validator agrees with, which can cost up to -1.1 SOL per day. - -## Hardware Recommendations - -The hardware recommendations below are provided as a guide. Operators are encouraged to do their own performance testing. - -| Component | Validator Requirements | Additional RPC Node Requirements | -|-----------|------------------------|----------------------------------| -| **CPU** | - 2.8GHz base clock speed, or faster
- SHA extensions instruction support
- AMD Gen 3 or newer
- Intel Ice Lake or newer
- Higher clock speed is preferable over more cores
- AVX2 instruction support (to use official release binaries, self-compile otherwise)
- Support for AVX512f is helpful
|| -| | 12 cores / 24 threads, or more | 16 cores / 32 threads, or more | -| **RAM** | Error Correction Code (ECC) memory is suggested
Motherboard with 512GB capacity suggested || -| | 256GB or more| 512 GB or more for **all [account indexes](https://docs.anza.xyz/operations/setup-an-rpc-node#account-indexing)** | -| **Disk** | PCIe Gen3 x4 NVME SSD, or better, on each of:
- **Accounts**: 1TB, or larger. High TBW (Total Bytes Written)
- **Ledger**: 1TB or larger. High TBW suggested
- **Snapshots**: 500GB or larger. High TBW suggested
- **OS**: (Optional) 500GB, or larger. SATA OK

The OS may be installed on the ledger disk, though testing has shown better performance with the ledger on its own disk

Accounts and ledger *can* be stored on the same disk, however due to high IOPS, this is not recommended

The Samsung 970 and 980 Pro series SSDs are popular with the validator community | Consider a larger ledger disk if longer transaction history is required

Accounts and ledger **should not** be stored on the same disk | - -A community maintained list of currently optimal hardware can be found here: [solanahcl.org](https://solanahcl.org/). It is updated automatically from the [solanahcl/solanahcl Github repo](https://github.com/solanahcl/solanahcl). - -## Virtual machines on Cloud Platforms - -Running an Agave node in the cloud requires significantly greater -operational expertise to achieve stability and performance. Do not -expect to find sympathetic voices should you chose this route and -find yourself in need of support. - -## Docker - -Running an Agave validator for live clusters (including mainnet-beta) inside Docker is -not recommended and generally not supported. This is due to general concerns of -Docker's containerization overhead and resultant performance degradation unless -specially configured. We use Docker only for development purposes. - -## Software - -- We build and run on Ubuntu 24.04. -- See [Installing Solana CLI](../cli/install.md) for the current Solana CLI software release. - -Prebuilt binaries are currently available targeting x86_64 with AVX2 support on Ubuntu 20.04. We -will stop publishing these in the near future (as of May 2025). -[Building from source](https://docs.anza.xyz/cli/install#build-from-source) is required for all -other supported target platforms (MacOS and Windows) today and suggested for linux as it will soon -be required there as well - -## Networking -A stable connection with a public IPv4 address is required. - -- For an unstaked node: 1 GBit/s symmetric connection is sufficient. -- For a staked node: at least 2 GBit/s symmetric connection is required, 10 GBit/s of available bandwidth is recommended for stable operation. - -### Firewall -It is not recommended to run a validator behind a NAT. Operators who choose to -do so should be comfortable configuring their networking equipment and debugging -any traversal issues on their own. Upstream agave will generally not accept -patches to support operation behind NAT. - -The following traffic needs to be allowed. Furthermore, there should not be any traffic filtering from your validator to internet. - -#### Required - -| Source | Destination | Protocol | Port(s) | Comment | -|--------|---------------------|-------------|------------|--------------------------------------------------------------------------------------------------------------------------| -| any | your validator's IP | TCP and UDP | 8000-8030 | P2P protocols (gossip, turbine, repair, etc). This can be limited to any free port range with `--dynamic-port-range` | - -#### Recommended -When you manage your validator via SSH it is recommended to limit the allowed SSH traffic to your validator management IP. - -| Source | Destination | Protocol | Port(s) | Comment | -|-----------------|--------------------------------|----------|---------|-----------------------------------------------------------------------------| -| your IP address | your validator's management IP | TCP | 22 | SSH management traffic. Port can be different depending on your SSH config. | - -***Please note that your source IP address should be a static public IP address. Please check with your service provider if you're not sure if your public IP address is static.*** - -#### Optional -For security purposes, it is not suggested that the following ports be open to -the internet on staked, mainnet-beta validators. - -| Source | Destination | Protocol | Port(s) | Comment | -|----------|---------------------|----------|---------|--------------------------------------------------------| -| RPC user | your validator's IP | TCP | 8899 | JSONRPC over HTTP. Change with `--rpc-port RPC_PORT` | -| RPC user | your validator's IP | TCP | 8900 | JSONRPC over Websockets. Derived. Uses `RPC_PORT + 1` | diff --git a/docs/src/operations/running-with-af-xdp.md b/docs/src/operations/running-with-af-xdp.md deleted file mode 100644 index 94b1f57b4a2..00000000000 --- a/docs/src/operations/running-with-af-xdp.md +++ /dev/null @@ -1,84 +0,0 @@ ---- -title: Running with AF_XDP -sidebar_label: Running with AF_XDP -sidebar_position: 7 ---- - -XDP: eXpress Data Path is a Linux kernel technology that allows developers to write high-performance networking code which bypasses the kernel’s usual packet handling path. This means fewer data copies and fewer context switches between user and kernel space. By handling packets directly with the network interface card in user space, XDP greatly reduces the overhead per packet. - -Agave version 3.0.9 and later supports XDP for Turbine packet handling. XDP works most efficiently with a NIC that has XDP driver support. Early testing indicates that 100M-CU blocks are achievable if operators adopt XDP in validator operations, providing the headroom needed to scale block propagation. - -## Configuration - -Before rolling out XDP on a production validator, you should test it on your setup and verify a few things: -* **Driver Compatibility:** No unexpected NIC driver or hardware issues when XDP is enabled on your system. -* **Performance Gain:** Confirm that performance is improved with the new configuration (e.g. lower CPU usage or higher throughput in Turbine’s retransmit stage). -* **Metric Visibility:** Verify that you can observe the retransmit-stage metrics, which show time spent sending shreds, to gauge the impact of XDP on network transmission. - -To enable XDP in Agave, add the following command-line flags to your validator startup command (using Agave v3.0.9+): - -```bash ---experimental-retransmit-xdp-cpu-cores 1 ---experimental-retransmit-xdp-zero-copy # Do NOT pass this flag when using the bnxt_en driver. ---experimental-poh-pinned-cpu-core 10 -``` - -Note that --experimental-retransmit-xdp-zero-copy will avoid using socket buffers for data, but this is only possible when talking directly to the Network Interface Card (NIC). As a result, zero copy cannot be used with the bonded interface itself. When using a bonded network interface, specify the underlying member interface to which the XDP program should be attached: - -```bash ---experimental-retransmit-xdp-interface -``` - - Also note that XDP and PoH *must* be assigned to separate (physical) cores. The ---experimental-poh-pinned-cpu-core N flag can be used to move the PoH thread. - -Next, your validator binary will need to have access to a few higher level permissions. The validator process requires the CAP_NET_RAW, CAP_NET_ADMIN, CAP_BPF, and CAP_PERFMON capabilities. These can be configured in the systemd service file by setting CapabilityBoundingSet=CAP_NET_RAW CAP_NET_ADMIN CAP_BPF CAP_PERFMON under the [Service] section or directly on the binary with the command: - -```bash -sudo setcap cap_net_raw,cap_net_admin,cap_bpf,cap_perfmon=p -#this command must be run each time the binary is replaced -``` - -The setcap stores the updated privileges on the binary file, so this command will need to be rerun any time the binary is upgraded. - -## Conclusion - -Enabling XDP in Agave allows a validator to send data out more efficiently (fewer copies and syscalls), which translates to faster block propagation and more headroom for future growth. For now, we encourage a subset validator operators, especially nodes with XDP-capable NICs, to try it out and report any issues to #validator-support in the Solana Tech Discord. Thank you for contributing to Solana and helping the cluster prepare for 100M CUs. - -## Troubleshooting - -Some driver versions seem to return non-power of 2 queue sizes, which can cause issues. Setting these explicitly resolves the problem. For example, if the queues return a size of 511, forcing to 512 resolves things. - -```bash -sudo ethtool -G enp196s0f0np0 rx 512 tx 512 -``` - -The igb driver supports zero copy starting from kernel version 6.14. For all other drivers, kernel version 6.8 or newer is recommended. - -## Debug Data Collection - -When encountering issues, understanding the kernel, NIC, and driver information will be crucial to being able to debug issues - -```bash -uname -a -sudo lshw -c network | grep 'logical name' -sudo ethtool -i -sudo ethtool -g -lspci | grep Ethernet -modinfo bnxt_en -``` - - -## XDP / Zero-Copy Driver Support Matrix - -| Driver / NIC family | AF_XDP w/o ZC | AF_XDP w/ ZC | Status | -| --- | --- | --- | --- | -| `mlx5` / Mellanox ConnectX | ✅ Works | ✅ Works | Operator reports: `mlx5` works with XDP + ZC on kernel 6.8; ConnectX-6 Lx worked after kernel 6.17 upgrade. Highest-confidence family in the discussion. | -| `i40e` / Intel 700 series | ✅ Works | ✅ Works | Operator report: `i40e` works with XDP + ZC on kernel 6.8. | -| `igb` / Intel I210 | ✅ Works | ✅ Works w/ caveat | caveat: `igb` requires kernel `>= 6.14` for ZC. Field report: I210 on 6.17 enabled ZC but had severe network degradation/high skips, so fall back to non-ZC if unstable. | -| `ixgbe` / Intel X540, X550 | ✅ Works | ⚠️ Mixed / unstable | Alessandro guidance for freeze/link-flap cases: start without ZC while `ixgbe` is debugged. Stay tuned! | -| `ice` / Intel E800 | ✅ Works | ✅ Works | `ice` supports native XDP and AF_XDP zero-copy. Caveats: XDP is blocked for frame sizes larger than 3KB | -| `bnxt_en` / Broadcom | ✅ Works | ❌ Does not work | `bnxt_en` works with XDP, but do not pass the zero-copy flag. Broadcom non-ZC can still be reasonably fast. But please get a non-broadcom NIC | -| `tg3` / Broadcom | ❌ No native/driver XDP; generic XDP only at best | ❌ Does not work | Broadcom BCM5720 uses the `tg3` driver. Treat as unsupported for Agave/AF_XDP performance work: no native XDP and no AF_XDP zero-copy. | -| `r8169` / Realtek | ❌ No native/driver XDP; generic XDP only at best | ❌ Does not work | Realtek NICs using `r8169` should be treated as unsupported for Agave/AF_XDP performance work: no native XDP and no AF_XDP zero-copy.| -| `mlx4_en` / Mellanox ConnectX-3 | ❌ Do not use | ❌ Does not work | Driver is no longer supported. Zero-copy does not work. Do not use. | diff --git a/docs/src/operations/setup-a-validator.md b/docs/src/operations/setup-a-validator.md deleted file mode 100644 index 0e801d29fb5..00000000000 --- a/docs/src/operations/setup-a-validator.md +++ /dev/null @@ -1,622 +0,0 @@ ---- -title: Setup an Agave Validator -sidebar_label: Setup an Agave Validator -sidebar_position: 5 ---- - -This is a guide for getting your validator setup on the Solana testnet cluster -for the first time. Testnet is a Solana cluster used for performance -testing of the software before the software is used on mainnet. Since testnet is -stress tested daily, it is a good cluster to practice validator operations. - -Once you have a working validator on testnet, you will want to learn about -[operational best practices](./best-practices/general.md) in the next section. -Although the guide is specific to testnet, it can be adapted to mainnet or -devnet as well. - -> Refer to the [Available Clusters](../clusters/available.md) section of the -> documentation to see example commands for each cluster. - -Now let's get started. - -## Open The Terminal Program - -To start this guide, you will be running commands on your trusted computer, not -on the remote machine that you plan to use for validator operations. First, -locate the terminal program on your _trusted computer_. - -- on Mac, you can search for the word _terminal_ in spotlight. -- on Ubuntu, you can type `CTRL + Alt + T`. -- on Windows, you will have to open the command prompt as an Administrator. - -## Install The Solana CLI Locally - -Validator operators are required to install the tools included in the Solana CLI using the [installation instructions](../cli/install.md). - -Once the Solana CLI is installed, you can return to this document once you are -able to run two commands and get an answer on your terminal. - -First, run the following command to verify that the Solana CLI is installed: - -``` -solana --version -``` - -You should see an output that looks similar to this (note your version number -may be higher): - -``` -solana-cli 1.14.17 (src:b29a37cf; feat:3488713414) -``` - -Now, run the following command to verify that the agave-validator binary is -installed: - -``` -agave-validator --version -``` - -You should see an output that looks similar to this (note your version number -may be higher): - -``` -agave-validator 2.3.1 (src:e3eca4c1; feat:3640012085, client:Agave) -``` - -Once you have successfully installed the cli and validator binary, the next step is to change your -config so that it is making requests to the `testnet` cluster: - -``` -solana config set --url https://api.testnet.solana.com -``` - -To verify that your config has changed, run: - -``` -solana config get -``` - -You should see a line that says: `RPC URL: https://api.testnet.solana.com` - -## Create Keys - -On your local computer, create the 3 keypairs that you will need to run your -validator ([docs for reference](./guides/validator-start.md#generate-identity)): - -> **NOTE** Some operators choose to make vanity keypairs for their identity and -> vote account using the `grind` sub command -> ([docs for reference](./guides/validator-start.md#vanity-keypair)). - -``` -solana-keygen new -o validator-keypair.json -``` - -``` -solana-keygen new -o vote-account-keypair.json -``` - -``` -solana-keygen new -o authorized-withdrawer-keypair.json -``` - -> **IMPORTANT** the `authorized-withdrawer-keypair.json` should be considered -> very sensitive information. Many operators choose to use a multisig, hardware -> wallet, or paper wallet for the authorized withdrawer keypair. A keypair is -> created on disk in this example for simplicity. Additionally, the withdrawer -> keypair should always be stored safely. The authorized withdrawer keypair -> should **never** be stored on the remote machine that the validator software -> runs on. For more information, see -> [validator security best practices](./best-practices/security.md#do-not-store-your-withdrawer-key-on-your-validator) - -## Create a Vote Account - -Before you can create your vote account, you need to configure the Solana -command line tool a bit more. - -The below command sets the default keypair that the Solana CLI uses to the -`validator-keypair.json` file that you just created in the terminal: - -``` -solana config set --keypair ./validator-keypair.json -``` - -Now verify your account balance of `0`: - -``` -solana balance -``` - -Next, you need to deposit some SOL into that keypair account in order create a -transaction (in this case, making your vote account): - -``` -solana airdrop 1 -``` - -> **NOTE** The `airdrop` sub command does not work on mainnet, so you will have -> to acquire SOL and transfer it into this keypair's account if you are setting -> up a mainnet validator. - -Now, use the Solana cluster to create a vote account. - -As a reminder, all commands mentioned so far **should be done on your trusted -computer** and **NOT** on a server where you intend to run your validator. It is -especially important that the following command is done on a **trusted -computer**: - -``` -solana create-vote-account -ut \ - --fee-payer ./validator-keypair.json \ - ./vote-account-keypair.json \ - ./validator-keypair.json \ - ./authorized-withdrawer-keypair.json -``` - -> Note `-ut` tells the cli command that we would like to use the testnet -> cluster. `--fee-payer` specifies the keypair that will be used to pay the -> transaction fees. Both flags are not necessary if you configured the solana -> cli properly above but they are useful to ensure you're using the intended -> cluster and keypair. - -After SIMD-0387 has been activated on your cluster, set the BLS public key for -your vote account before starting the validator. Follow the -[BLS public key instructions](./guides/vote-accounts.md#set-the-bls-public-key). - -## Save the Withdrawer Keypair Securely - -Make sure your `authorized-withdrawer-keypair.json` is stored in a safe place. -If you have chosen to create a keypair on disk, you should first backup the -keypair and then delete it from your local machine. - -**IMPORTANT**: If you lose your withdrawer key pair, you will lose control of -your vote account. You will not be able to withdraw tokens from the vote account -or update the withdrawer. Make sure to store the -`authorized-withdrawer-keypair.json` securely before you move on. - -## SSH To Your Validator - -Connect to your remote server. This is specific to your server but will look -something like this: - -``` -ssh user@ -``` - -You will have to check with your server provider to get the correct user account -and hostname that you will ssh into. - -## Update Your Ubuntu Packages - -Make sure you have the latest and greatest package versions on your server - -``` -sudo apt update -sudo apt upgrade -``` - -## Sol User - -Create a new Ubuntu user, named `sol`, for running the validator: - -``` -sudo adduser sol -``` - -It is a best practice to always run your validator as a non-root user, like the -`sol` user we just created. - -## Hard Drive Setup - -On your Ubuntu computer make sure that you have at least `2TB` of disk space -mounted. You can check disk space using the `df` command: - -``` -df -h -``` - -> If you have a drive that is not mounted/formatted, you will have to set up the -> partition and mount the drive. - -To see the hard disk devices that you have available, use the list block devices -command: - -``` -lsblk -f -``` - -You may see some devices in the list that have a name but do not have a UUID. -Any device without a UUID is unformatted. - -### Drive Formatting: Ledger - -Assuming you have an nvme drive that is not formatted, you will have to format -the drive and then mount it. - -For example, if your computer has a device located at `/dev/nvme0n1`, then you -can format the drive with the command: - -``` -sudo mkfs -t ext4 /dev/nvme0n1 -``` - -For your computer, the device name and location may be different. - -Next, check that you now have a UUID for that device: - -``` -lsblk -f -``` - -In the fourth column, next to your device name, you should see a string of -letters and numbers that look like this: `6abd1aa5-8422-4b18-8058-11f821fd3967`. -That is the UUID for the device. - -### Mounting Your Drive: Ledger - -So far we have created a formatted drive, but you do not have access to it until -you mount it. Make a directory for mounting your drive: - -``` -sudo mkdir -p /mnt/ledger -``` - -Next, change the ownership of the directory to your `sol` user: - -``` -sudo chown -R sol:sol /mnt/ledger -``` - -Now you can mount the drive: - -``` -sudo mount /dev/nvme0n1 /mnt/ledger -``` - -### Formatting And Mounting Drive: AccountsDB - -You will also want to mount the accounts db on a separate hard drive. The -process will be similar to the ledger example above. - -Assuming you have a device at `/dev/nvme1n1`, format the device and verify it -exists: - -``` -sudo mkfs -t ext4 /dev/nvme1n1 -``` - -Then verify the UUID for the device exists: - -``` -lsblk -f -``` - -Create a directory for mounting: - -``` -sudo mkdir -p /mnt/accounts -``` - -Change the ownership of that directory: - -``` -sudo chown -R sol:sol /mnt/accounts -``` - -And lastly, mount the drive: - -``` -sudo mount /dev/nvme1n1 /mnt/accounts -``` - -## System Tuning - -### Linux - -Your system will need to be tuned to run properly. Your validator may -not start without the settings below. - -#### **Optimize sysctl knobs** - -```bash -sudo bash -c "cat >/etc/sysctl.d/21-agave-validator.conf </etc/security/limits.d/90-solana-nofiles.conf <: -scp vote-account-keypair.json sol@: -``` - -> **Note**: The `vote-account-keypair.json` does not have any function other -> than identifying the vote account to potential delegators. Only the public key -> of the vote account is important once the account is created. - -## Switch to the sol User - -On the validator server, switch to the `sol` user: - -``` -su - sol -``` - -## Install agave-validator on Remote Machine - -Your remote machine will need `agave-validator` installed to run the Agave validator -software. For simplicity, install the application with user `sol`. Refer again to -[build from source](../cli/install.md#build-from-source). - -## Create A Validator Startup Script - -In your sol home directory (e.g. `/home/sol/`), create a folder called `bin`. -Inside that folder create a file called `validator.sh` and make it executable: - -``` -mkdir -p /home/sol/bin -touch /home/sol/bin/validator.sh -chmod +x /home/sol/bin/validator.sh -``` - -Next, open the `validator.sh` file for editing: - -``` -nano /home/sol/bin/validator.sh -``` - -Copy and paste the following contents into `validator.sh` then save the file: - -``` -#!/bin/bash -exec agave-validator \ - --identity /home/sol/validator-keypair.json \ - --vote-account /home/sol/vote-account-keypair.json \ - --known-validator 5D1fNXzvv5NjV1ysLjirC4WY92RNsVH18vjmcszZd8on \ - --known-validator 7XSY3MrYnK8vq693Rju17bbPkCN3Z7KvvfvJx4kdrsSY \ - --known-validator Ft5fbkqNa76vnsjYNwjDZUXoTWpP7VYm3mtsaQckQADN \ - --known-validator 9QxCLckBiJc783jnMvXZubK4wH86Eqqvashtrwvcsgkv \ - --only-known-rpc \ - --log /home/sol/agave-validator.log \ - --ledger /mnt/ledger \ - --accounts /mnt/accounts \ - --rpc-port 8899 \ - --dynamic-port-range 8000-8020 \ - --entrypoint entrypoint.testnet.solana.com:8001 \ - --entrypoint entrypoint2.testnet.solana.com:8001 \ - --entrypoint entrypoint3.testnet.solana.com:8001 \ - --expected-genesis-hash 4uhcVJyU9pJkvQyS88uRDiswHXSCkY3zQawwpjk2NsNY \ - --wal-recovery-mode skip_any_corrupted_record \ - --limit-ledger-size -``` - -Refer to `agave-validator --help` for more information on what each flag is -doing in this script. Also refer to the section on -[best practices for operating a validator](./best-practices/general.md). - -This startup script is specifically intended for testnet. For more startup script examples intended for other clusters, refer to the -[clusters section.](./../clusters/available.md). -## Verifying Your Validator Is Working - -Test that your `validator.sh` file is running properly by executing the -`validator.sh` script: - -``` -/home/sol/bin/validator.sh -``` - -The script should execute the `agave-validator` process. In a new terminal -window, ssh into your server, then verify that the process is running: - -``` -ps aux | grep agave-validator -``` - -You should see a line in the output that includes `agave-validator` with all -the flags that were added to your `validator.sh` script. - -Next, we need to look at the logs to make sure everything is operating properly. - -### Tailing The Logs - -As a spot check, you will want to make sure your validator is producing -reasonable log output (**warning**, there will be a lot of log output). - -In a new terminal window, ssh into your validator machine, switch users to the -`sol` user and `tail` the logs: - -``` -su - sol -tail -f agave-validator.log -``` - -The `tail` command will continue to display the output of a file as the file -changes. You should see a continuous stream of log output as your validator -runs. Keep an eye out for any lines that say `_ERROR_`. - -Assuming you do not see any error messages, exit out of the command. - -### Gossip Protocol - -Gossip is a protocol used in the Solana clusters to communicate between -validator nodes. For more information on gossip, see -[Gossip Service](../validator/gossip.md). To verify that your validator is -running properly, make sure that the validator has registered itself with the -gossip network. - -In a new terminal window, connect to your server via ssh. Identify your -validator's pubkey: - -``` -solana-keygen pubkey ~/validator-keypair.json -``` - -The command `solana gossip` lists all validators that have registered with the -protocol. To check that the newly setup validator is in gossip, we will `grep` -for our pubkey in the output: - -``` -solana gossip | grep -``` - -After running the command, you should see a single line that looks like this: - -``` -139.178.68.207 | 5D1fNXzvv5NjV1ysLjirC4WY92RNsVH18vjmcszZd8on | 8001 | 8004 | 139.178.68.207:80 | 1.14.17 | 3488713414 -``` - -If you do not see any output after grep-ing the output of gossip, your validator -may be having startup problems. If that is the case, start debugging by looking -through the validator log output. - -### Solana Validators - -After you have verified that your validator is in gossip, you should stake some -SOL to your validator. Once the stake has activated (which happens at the start -of the next epoch), you can verify that your validator is ready to be a voting -participant of the network with the `solana validators` command. The command -lists all validators in the network, but like before, we can `grep` the output -for the validator we care about: - -``` -solana validators | grep -``` - -You should see a line of output that looks like this: - -``` -5D1fNXzvv5NjV1ysLjirC4WY92RNsVH18vjmcszZd8on FX6NNbS5GHc2kuzgTZetup6GZX6ReaWyki8Z8jC7rbNG 100% 197434166 ( 0) 197434133 ( 0) 2.11% 323614 1.14.17 2450110.588302720 SOL (1.74%) -``` - -### Solana Catchup - -The `solana catchup` command is a useful tool for seeing how quickly your -validator is processing blocks. The Solana network has the capability to produce -many transactions per second. Since your validator is new to the network, it has -to ask another validator (listed as a `--known-validator` in your startup -script) for a recent snapshot of the ledger. By the time you receive the -snapshot, you may already be behind the network. Many transactions may have been -processed and finalized in that time. In order for your validator to participate -in consensus, it must _catchup_ to the rest of the network by asking for the -more recent transactions that it does not have. - -The `solana catchup` command is a tool that tells you how far behind the network -your validator is and how quickly you are catching up: - -``` -solana catchup -``` - -If you see a message about trying to connect, your validator may not be part of -the network yet. Make sure to check the logs and double check `solana gossip` -and `solana validators` to make sure your validator is running properly. - -Once you are happy that the validator can start up without errors, the next step -is to create a system service to run the `validator.sh` file automatically. Stop -the currently running validator by pressing `CTRL+C` in the window where -`validator.sh` is running. - -## Create a System Service - -Follow these instructions for -[running the validator as a system service](./guides/validator-start.md#systemd-unit) - -Make sure to implement log rotate as well. Once you have the system service -configured, start your validator using the newly configured service: - -``` -sudo systemctl enable --now sol -``` - -Now verify that the validator is running properly by tailing the logs and using -the commands mentioned earlier to check gossip and Solana validators: - -``` -tail -f /home/sol/agave-validator*.log -``` - -## Monitoring - -`agave-watchtower` is a command you can run on a separate machine to monitor -your server. You can read more about handling -[automatic restarts and monitoring](./best-practices/monitoring.md#agave-watchtower) -using Agave Watchtower here in the docs. - -## Common issues - -### Out of disk space - -Make sure your ledger is on drive with at least `2TB` of space. - -### Validator not catching up - -This could be a networking/hardware issue, or you may need to get the latest -snapshot from another validator node. - -### PoH hashes/second rate is slower than the cluster target - -If you are using `agave-validator` built from source, ensure that you are using a `release` build and not a `debug` build - -Ensure that your machine's CPU base clock speed is 2.8GHz or faster. Use `lscpu` to check your clock speed. `CPU(s) scaling MHz` can cause your clock speed to be underclocked. Some additional tuning: - -Set performance governor -```bash -echo performance | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor -``` - -Force minimum frequency to maximum -```bash -# Example if your maximum GHz is 2.8 -echo 2850000 | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_min_freq diff --git a/docs/src/operations/setup-an-rpc-node.md b/docs/src/operations/setup-an-rpc-node.md deleted file mode 100644 index 3aa3ba6ab86..00000000000 --- a/docs/src/operations/setup-an-rpc-node.md +++ /dev/null @@ -1,93 +0,0 @@ ---- -title: Setup an Agave RPC Node -sidebar_label: Setup an Agave RPC Node -sidebar_position: 6 ---- - -Since a Solana RPC server runs the same process as a consensus validator, first follow the instructions on -[how to setup a Solana validator](./setup-a-validator.md) to get started. -Note that you do not need to create a vote account if you are operating an RPC node. -An RPC node typically does not vote. - -After your validator is running, you can refer to this section for the RPC node specific setup instructions. - -_**WARNING:**_ The RPC service is _**NOT**_ intended to be directly exposed to the public internet. Operators who -intend to do so should be familiar with common strategies for protecting HTTP services from abuse. This topic -will not be covered in this document, nor will the team offer any guidance or technical support on the matter. - -## Sample RPC Node - -Below is an example `validator.sh` file for a `testnet` RPC server. - -You will want to be aware of the following flags: - -- `--full-rpc-api`: enables all RPC operations on this validator. -- `--no-voting`: runs the validator without participating in consensus. Typically, you do not want to run a validator as _both_ a consensus node and a full RPC node due to resource constraints. -- `--private-rpc`: does not publish the validator's open RPC port in the `solana gossip` command - -> For more explanation on the flags used in the command, refer to the `agave-validator --help` command - -``` -#!/bin/bash -exec agave-validator \ - --identity /home/sol/validator-keypair.json \ - --known-validator 5D1fNXzvv5NjV1ysLjirC4WY92RNsVH18vjmcszZd8on \ - --known-validator dDzy5SR3AXdYWVqbDEkVFdvSPCtS9ihF5kJkHCtXoFs \ - --known-validator eoKpUABi59aT4rR9HGS3LcMecfut9x7zJyodWWP43YQ \ - --known-validator 7XSY3MrYnK8vq693Rju17bbPkCN3Z7KvvfvJx4kdrsSY \ - --known-validator Ft5fbkqNa76vnsjYNwjDZUXoTWpP7VYm3mtsaQckQADN \ - --known-validator 9QxCLckBiJc783jnMvXZubK4wH86Eqqvashtrwvcsgkv \ - --only-known-rpc \ - --full-rpc-api \ - --no-voting \ - --ledger /mnt/ledger \ - --accounts /mnt/accounts \ - --log /home/sol/solana-rpc.log \ - --rpc-port 8899 \ - --private-rpc \ - --dynamic-port-range 8000-8020 \ - --entrypoint entrypoint.testnet.solana.com:8001 \ - --entrypoint entrypoint2.testnet.solana.com:8001 \ - --entrypoint entrypoint3.testnet.solana.com:8001 \ - --expected-genesis-hash 4uhcVJyU9pJkvQyS88uRDiswHXSCkY3zQawwpjk2NsNY \ - --wal-recovery-mode skip_any_corrupted_record \ - --limit-ledger-size -``` - -### Solana Bigtable - -The Solana blockchain is able to create many transactions per second. Because of the volume of transactions on the chain, it is not practical for an RPC node to store the entire blockchain on the machine. Instead, RPC operators use the `--limit-ledger-size` flag to specify how many blocks to store on the RPC node. If the user of the RPC node needs historical blockchain data, then the RPC server will have to access older blocks through a Solana bigtable instance. - -If you are interested in setting up your own bigtable instance, see these docs in the Solana GitHub repository: [solana-labs/solana-bigtable](https://github.com/solana-labs/solana-bigtable) - -### Example Known Validators - -The identities of the [known validators](./guides/validator-start.md#known-validators) supplied in these example snippets (via the `--known-validator` flag) are: - -- `5D1fNXzvv5NjV1ysLjirC4WY92RNsVH18vjmcszZd8on` - Anza -- `dDzy5SR3AXdYWVqbDEkVFdvSPCtS9ihF5kJkHCtXoFs` - MonkeDAO -- `Ft5fbkqNa76vnsjYNwjDZUXoTWpP7VYm3mtsaQckQADN` - Certus One -- `eoKpUABi59aT4rR9HGS3LcMecfut9x7zJyodWWP43YQ` - SerGo -- `9QxCLckBiJc783jnMvXZubK4wH86Eqqvashtrwvcsgkv` - Algo|Stake - -## Examples for other clusters - -Additional examples of other Solana cluster-specific validator commands can be found on the [Clusters](../clusters/available.md) page. - -Keep in mind, you will still need to customize these commands to operate as an RPC node, as well as other -operator-specific configuration settings. - -## Account indexing - -As the number of populated accounts on the cluster grows, account-data RPC -requests that scan the entire account set -- like -[`getProgramAccounts`](https://solana.com/docs/rpc/http/getprogramaccounts) and -[SPL-token-specific requests](https://solana.com/docs/rpc/http/gettokenaccountsbydelegate) -- -may perform poorly. If your validator needs to support any of these requests, -you can use the `--account-index` parameter to activate one or more in-memory -account indexes that significantly improve RPC performance by indexing accounts -by the key field. Currently, it supports the following parameter values: - -- `program-id`: each account indexed by its owning program; used by [getProgramAccounts](https://solana.com/docs/rpc/http/getprogramaccounts) -- `spl-token-mint`: each SPL token account indexed by its token Mint; used by [getTokenAccountsByDelegate](https://solana.com/docs/rpc/http/gettokenaccountsbydelegate), and [getTokenLargestAccounts](https://solana.com/docs/rpc/http/gettokenlargestaccounts) -- `spl-token-owner`: each SPL token account indexed by the token-owner address; used by [getTokenAccountsByOwner](https://solana.com/docs/rpc/http/gettokenaccountsbyowner), and [getProgramAccounts](https://solana.com/docs/rpc/http/getprogramaccounts) requests that include an spl-token-owner filter. diff --git a/docs/src/operations/validator-or-rpc-node.md b/docs/src/operations/validator-or-rpc-node.md deleted file mode 100644 index ca4ded55597..00000000000 --- a/docs/src/operations/validator-or-rpc-node.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -title: Consensus Validator or RPC Node? -sidebar_position: 1 -sidebar_label: Validator vs RPC Node -pagination_label: Consensus Validator vs RPC Node ---- - -Operators who run a [consensus validator](../what-is-a-validator.md) have much -different incentives than operators who run an -[RPC node](../what-is-an-rpc-node.md). You will have to decide which choice is -best for you based on your interests, technical background, and goals. - -## Consensus Validators - -As a validator your primary focus is maintaining the network and making sure -that your node is performing optimally so that you can fully participate in the -cluster consensus. You will want to attract a delegation of SOL to your -validator which will allow your validator the opportunity to produce more blocks -and earn rewards. - -Each staked validator earns inflation rewards from -[vote credits](https://solana.com/docs/terminology#vote-credit). Vote credits -are assigned to validators that vote on -[blocks](https://solana.com/docs/terminology#block) produced by the -[leader](https://solana.com/docs/terminology#leader). The vote credits are given -to all validators that successfully vote on blocks that are added to the -blockchain. Additionally, when the validator is the leader, it can earn -transaction fees and storage -[rent fees](https://solana.com/docs/core/accounts#rent) for each block that it -produces that is added to the blockchain. - -Since all votes in Solana happen on the blockchain, a validator incurs a -transaction cost for each vote that it makes. These transaction fees amount to -approximately 1.0 SOL per day. - -> It is important to make sure your validator always has enough SOL in its -> identity account to pay for these transactions! - -### Economics of running a consensus validator - -As an operator, it is important to understand how a consensus validator spends -and earns sol through the protocol. - -All validators who vote (consensus validators) must pay vote transaction fees -for blocks that they agree with. The cost of voting can be up to 1.1 SOL per -day. - -A voting validator can earn SOL through 2 methods: - -1. Inflationary rewards paid at the end of an epoch. See - [staking rewards](../implemented-proposals/staking-rewards.md) -2. Earning 50% of transaction fees for the blocks produced by the validator. See - [transaction fee basic economic design](https://solana.com/docs/intro/transaction_fees#basic-economic-design) - -The following links are community provided resources that discuss the economics -of running a validator: - -- Michael Hubbard wrote an - [article](https://laine-sa.medium.com/solana-staking-rewards-validator-economics-how-does-it-work-6718e4cccc4e) - that explains the economics of Solana in more depth for stakers and for - validators. -- Congent Crypto has written a - [blog post](https://medium.com/@Cogent_Crypto/how-to-become-a-validator-on-solana-9dc4288107b7) - that discusses economics and getting started. -- Cogent Crypto also provides a - [validator profit calculator](https://cogentcrypto.io/ValidatorProfitCalculator) - -## RPC Nodes - -While RPC operators **do NOT** receive rewards (because the node is not -participating in voting), there are different motivations for running an RPC -node. - -An RPC operator is providing a service to users who want to interact with the -Solana blockchain. Because your primary user is often technical, you will have -to be able to answer technical questions about performance of RPC calls. This -option may require more understanding of the -[core Solana architecture](../clusters/index.md). - -If you are operating an RPC node as a business, your job will also involve -scaling your system to meet the demands of the users. For example, some RPC -providers create dedicated servers for projects that require a high volume of -requests to the node. Someone with a background in development operations or -software engineering will be a very important part of your team. You will need a -strong understanding of the Solana architecture and the -[JSON RPC API](https://solana.com/docs/rpc/http). - -Alternatively, you may be a development team that would like to run their own -infrastructure. In this case, the RPC infrastructure could be a part of your -production stack. A development team could use the -[Geyser plugin](../validator/geyser.md), for example, to get -real time access to information about accounts or blocks in the cluster. diff --git a/docs/src/pages/styles.module.css b/docs/src/pages/styles.module.css deleted file mode 100644 index 677dc9b0686..00000000000 --- a/docs/src/pages/styles.module.css +++ /dev/null @@ -1,45 +0,0 @@ -/* stylelint-disable docusaurus/copyright-header */ - -/** - * CSS files with the .module.css suffix will be treated as CSS modules - * and scoped locally. - */ - -.heroBanner { - padding: 4rem 0; - text-align: center; - position: relative; - overflow: hidden; -} - -@media screen and (max-width: 966px) { - .heroBanner { - padding: 2rem; - } -} - -.cardTitle { - color: green; -} - -.buttons { - display: flex; - align-items: center; - justify-content: center; -} - -.features { - display: flex; - align-items: top; - padding: 2rem 0; - width: 100%; -} - -.featureImage { - height: 200px; - width: 200px; -} - -.iconExternalIcon { - margin-left: 0.5em; -} diff --git a/docs/src/proposals.md b/docs/src/proposals.md deleted file mode 100644 index 0835a338524..00000000000 --- a/docs/src/proposals.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -title: System Design Proposals -sidebar_label: Overview ---- - -Changes to the Solana architecture are performed through a public proposal process (via pull requests) on the [Solana GitHub repository](https://github.com/solana-labs/solana). New proposals should be submitted with the "[Submit a Design Proposal](#submit-a-design-proposal)" guide below. - -There are currently two different states of these design proposals: - -1. [Accepted Proposals](./proposals/accepted-design-proposals.md) -2. [Implemented Proposals](./implemented-proposals/index.md) - -## Accepted Proposals - -These architectural proposals have been accepted by the Solana team, but are not yet fully implemented. - -Each proposal may be implemented as described, implemented differently as issues in the designs become evident, or not implemented at all. If implemented, the proposal will be moved to [Implemented Proposals](./implemented-proposals/index.md) and the details will be added to relevant sections of the docs. - -## Implemented Proposals - -These architectural proposals have been accepted and implemented by the Solana team. - -Any designs that may be subject to future change are noted in their specific proposal page. - -## Submit a Design Proposal - -To submit a new design proposal for Solana: - -1. Propose a design by creating a PR that adds a markdown document to the `docs/src/proposals` directory. -2. Add any relevant Solana maintainers to the PR review. -3. Publish the PR for community review and feedback. - -> **NOTE:** All people submitting PRs to the Solana repo should consult the [CONTRIBUTING](https://github.com/solana-labs/solana/blob/master/CONTRIBUTING.md) doc in the repo. - -### After Accepted - -Once a design proposal has been accepted, the PR will be merged into the `master` branch of the Solana repo. This also signifies the maintainers support your plan of attack. - -> **NOTE:** The merging of the PR will **automatically** create a link in the "Accepted Proposals" table of contents sidebar. -> Once approved, continue to submit PRs that implement the proposal. When the implementation reveals the need for tweaks to the proposal, be sure to update the "accepted proposal" document and have these changes reviewed by the same approving maintainers. - -### After Implemented - -After a proposal has been fully implemented into the Solana architecture, a PR should be created to perform the following: - -1. Move the newly implemented proposal file from `docs/src/proposals` to `docs/src/implemented-proposals` -2. Create a new redirect in the `publish-docs.sh` to redirect the old `accepted` proposal page to the new `implemented-proposal` page -3. Publish the PR - -> **NOTE:** Moving the proposal document into the `implemented-proposals` directory will **automatically** move the link in the "Accepted Proposals" table of contents sidebar to the "Implemented Proposals" sidebar. diff --git a/docs/src/proposals/accepted-design-proposals.md b/docs/src/proposals/accepted-design-proposals.md deleted file mode 100644 index 61d2d2ef4f6..00000000000 --- a/docs/src/proposals/accepted-design-proposals.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: Accepted Design Proposals -sidebar_position: 1 -sidebar_label: Overview ---- - -These architectural proposals have been accepted by the Solana maintainers, but are not yet fully implemented. These proposals may be implemented as described, implemented differently as issues in the designs become evident, or not implemented at all. - -## After Implemented - -Once a proposal has been implemented, it will be moved to [Implemented Proposals](../implemented-proposals/index.md) and the details will be added to relevant sections of the docs. - -## Submit a New Proposal - -To submit a new design proposal, consult this guide on [how to submit a design proposal](../proposals.md#submit-a-design-proposal). diff --git a/docs/src/proposals/accounts-db-replication.md b/docs/src/proposals/accounts-db-replication.md deleted file mode 100644 index 5ff0fc4e8f9..00000000000 --- a/docs/src/proposals/accounts-db-replication.md +++ /dev/null @@ -1,185 +0,0 @@ ---- -title: AccountsDB Replication for RPC Services ---- - -## Problem - -Validators fall behind the network when bogged down by heavy RPC load. This -seems to be due to a combination of CPU load and lock contention caused by -serving RPC requests. The most expensive RPC requests involve account scans. - -## Solution Overview - -AccountsDB `replicas` that run separately from the main validator can be used to -offload account-scan requests. Replicas would only: request and pull account -updates from the validator, serve client account-state RPC requests, and manage -AccountsDb and AccountsBackgroundService clean + shrink. - -The replica communicates to the main validator via a new RPC mechanism to fetch -metadata information about the replication and the accounts update from the validator. -The main validator supports only one replica node. A replica node can relay the -information to 1 or more other replicas forming a replication tree. - -At the initial start of the replica node, it downloads the latest snapshot -from a validator and constructs the bank and AccountsDb from it. After that, it queries -its main validator for new slots and requests the validator to send the updated -accounts for that slot and update to its own AccountsDb. - -The same RPC replication mechanism can be used between a replica to another replica. -This requires the replica to serve both the client and server in the replication model. - -On the client RPC serving side, `JsonRpcAccountsService` is responsible for serving -the client RPC calls for accounts related information similar to the existing -`JsonRpcService`. - -The replica will also take snapshots periodically so that it can start quickly after -a restart if the snapshot is not too old. - -## Detailed Solution -The following sections provide more details of the design. - -### Consistency Model -The AccountsDb information is replicated asynchronously from the main validator to the replica. -When a query against the replica's AccountsDb is made, the replica may not have the latest -information of the latest slot. In this regard, it will eventually be consistent. However, for -a particular slot, the information provided is consistent with that of its peer validator -for commitment levels confirmed and finalized. For V1, we only support queries at these two -levels. - -### Solana Replica Node -A new node named solana-replica-node will be introduced whose main responsibility is to maintain -the AccountsDb replica. The RPC node or replica node is used interchangeably in this document. -It will be a separate executable from the validator. - -The replica consists of the following major components: - -The `ReplicaSlotConfirmationRequestor`: this service is responsible for periodically sending the -request `ReplicaSlotConfirmationRequest` to its peer validator or replica for the latest slots. -It specifies the latest slot (last_replicated_slot) for which the replica has already -fetched the accounts information for. This maintains the ReplWorkingSlotSet and manages -the lifecycle of BankForks, BlockCommitmentCache (for the highest confirmed slot) and -the optimistically confirmed bank. - -The `ReplicaSlotConfirmationServer`: this service is responsible for serving the -`ReplicaSlotConfirmationRequest` and sends the `ReplicaSlotConfirmationResponse` back to the requestor. -The response consists of a vector of new slots the validator knows of which is later than the -specified last_replicated_slot. This service also runs in the main validator. This service -gets the slots for replication from the BankForks, BlockCommitmentCache and OptimisticallyConfirmedBank. - -The `ReplicaAccountsRequestor`: this service is responsible for sending the request -`ReplicaAccountsRequest` to its peer validator or replica for the `ReplicaAccountInfo` for a -slot for which it has not completed accounts db replication. The `ReplicaAccountInfo` contains -the `ReplicaAccountMeta`, Hash and the AccountData. The `ReplicaAccountMeta` contains info about -the existing `AccountMeta` in addition to the account data length in bytes. - -The `ReplicaAccountsServer`: this service is responsible for serving the `ReplicaAccountsRequest` -and sends `ReplicaAccountsResponse` to the requestor. The response contains the count of the -ReplAccountInfo and the vector of ReplAccountInfo. This service runs both in the validator -and the replica relaying replication information. The server can stream the account information -from its AccountCache or from the storage if already flushed. This is similar to how a snapshot -package is created from the AccountsDb with the difference that the storage does not need to be -flushed to the disk before streaming to the client. If the account data is in the cache, it can -be directly streamed. Care must be taken to avoid account data for a slot being cleaned while -serving the streaming. When attempting a replication of a slot, if the slot is already cleaned -up with accounts data cleaned as result of update in later rooted slots, the replica should -forsake this slot and try the later uncleaned root slot. - -During replication we also need to replicate the information of accounts that have been cleaned -up due to zero lamports, i.e. we need to be able to tell the difference between an account in a -given slot which was not updated and hence has no storage entry in that slot, and one that -holds 0 lamports and has been cleaned up through the history. We may record this via some -"Tombstone" mechanism -- recording the dead accounts cleaned up for a slot. The tombstones -themselves can be removed after exceeding the retention period expressed as epochs. Any -attempt to replicate slots with tombstones removed will fail and the replica should skip -this slot and try later ones. - -The `JsonRpcAccountsService`: this is the RPC service serving client requests for account -information. The existing JsonRpcService serves other client calls than AccountsDb ones. -The replica node only serves the AccountsDb calls. - -The existing JsonRpcService requires `BankForks`, `OptimisticallyConfirmedBank` and -`BlockCommitmentCache` to load the Bank. The JsonRpcAccountsService will need to use -information obtained from ReplicaSlotConfirmationResponse to construct the AccountsDb. - -The `AccountsBackgroundService`: this service also runs in the replica which is responsible -for taking snapshots periodically and shrinking the AccountsDb and doing accounts cleaning. -The existing code also uses BankForks which we need to keep in the replica. - -### Compatibility Consideration - -For protocol compatibility considerations, all the requests have the replication version which -is initially set to 1. Alternatively, we can use the validator's version. The RPC server side -shall check the request version and fail if it is not supported. - -### Replication Setup -To limit adverse effects on the validator and the replica due to replication, they can be -configured with a list of replica nodes which can form a replication pair with it. And the -replica node is configured with the validator which can serve its requests. - - -### Fault Tolerance -The main responsibility of making sure the replication is tolerant of faults lies with the -replica. In case of request failures, the replica shall retry the requests. - - -### Interface - -Following are the client RPC APIs supported by the replica node in JsonRpcAccountsService. - -- getAccountInfo -- getBlockCommitment -- getMultipleAccounts -- getProgramAccounts -- getMinimumBalanceForRentExemption -- getInflationGovernor -- getInflationRate -- getEpochSchedule -- getRecentBlockhash -- getFees -- getFeeCalculatorForBlockhash -- getFeeRateGovernor -- getLargestAccounts -- getSupply -- getStakeActivation -- getTokenAccountBalance -- getTokenSupply -- getTokenLargestAccounts -- getTokenAccountsByOwner -- getTokenAccountsByDelegate - -Following APIs are not included: - -- getInflationReward -- getClusterNodes -- getRecentPerformanceSamples -- getGenesisHash -- getSignatureStatuses -- getMaxRetransmitSlot -- getMaxShredInsertSlot -- sendTransaction -- simulateTransaction -- getSlotLeader -- getSlotLeaders -- minimumLedgerSlot -- getBlock -- getBlockTime -- getBlocks -- getBlocksWithLimit -- getTransaction -- getSignaturesForAddress -- getFirstAvailableBlock -- getBlockProduction - - -Action Items - -1. Build the replica framework and executable -2. Integrate snapshot restore code for bootstrap the AccountsDb. -3. Develop the ReplicaSlotConfirmationRequestor and ReplicaSlotConfirmationServer interface code -4. Develop the ReplicaSlotConfirmationRequestor and ReplicaSlotConfirmationServer detailed implementations: managing the ReplEligibleSlotSet lifecycle: adding new roots and deleting root to it. And interfaces managing ReplWorkingSlotSet interface: adding and removing. Develop component synthesising information from BankForks, BlockCommitmentCache and OptimistcallyConfirmedBank on the server side and maintaining information on the client side. -5. Develop the interface code for ReplicaAccountsRequestor and ReplicaAccountsServer -6. Develop detailed implementation for ReplicaAccountsRequestor and ReplicaAccountsServer and develop the replication account storage serializer and deserializer. -7. Develop the interface code JsonRpcAccountsService -8. Detailed Implementation of JsonRpcAccountsService, refactor code to share with part of JsonRpcService. -9. Integrate with the AccountsBackgroundService in the replica for shrinking, cleaning, snapshotting. -10. Metrics and performance testing diff --git a/docs/src/proposals/bankless-leader.md b/docs/src/proposals/bankless-leader.md deleted file mode 100644 index 462b253a337..00000000000 --- a/docs/src/proposals/bankless-leader.md +++ /dev/null @@ -1,95 +0,0 @@ ---- -title: Bankless Leader ---- - -A bankless leader does the minimum amount of work to produce a valid block. The -leader is tasked with ingress transactions, sorting and filtering valid -transactions, arranging them into entries, shredding the entries and -broadcasting the shreds. While a validator only needs to reassemble the block -and replay execution of well formed entries. The leader does 3x more memory -operations before any bank execution than the validator per processed -transaction. - -## Rationale - -Normal bank operation for a spend needs to do 2 loads and 2 stores. With this -design leader just does 1 load. so 4x less account_db work before generating the -block. The store operations are likely to be more expensive than reads. - -When replay stage starts processing the same transactions, it can assume that -PoH is valid, and that all the entries are safe for parallel execution. The fee -accounts that have been loaded to produce the block are likely to still be in -memory, so the additional load should be warm and the cost is likely to be -amortized. - -## Fee Account - -The [fee account](https://solana.com/docs/terminology#fee_account) pays for the -transaction to be included in the block. The leader only needs to validate that -the fee account has the balance to pay for the fee. - -## Balance Cache - -For the duration of the leaders consecutive blocks, the leader maintains a -temporary balance cache for all the processed fee accounts. The cache is a map -of pubkeys to lamports. - -At the start of the first block the balance cache is empty. At the end of the -last block the cache is destroyed. - -The balance cache lookups must reference the same base fork for the entire -duration of the cache. At the block boundary, the cache can be reset along with -the base fork after replay stage finishes verifying the previous block. - -## Balance Check - -Prior to the balance check, the leader validates all the signatures in the -transaction. - -1. Verify the accounts are not in use and BlockHash is valid. -2. Check if the fee account is present in the cache, or load the account from - accounts_db and store the lamport balance in the cache. -3. If the balance is less than the fee, drop the transaction. -4. Subtract the fee from the balance. -5. For all the keys in the transaction that are Credit-Debit and are referenced - by an instruction, reduce their balance to 0 in the cache. The account fee is - declared as Credit-Debit, but as long as it is not used in any instruction - its balance will not be reduced to 0. - -## Leader Replay - -Leaders will need to replay their blocks as part of the standard replay stage -operation. - -## Leader Replay With Consecutive Blocks - -A leader can be scheduled to produce multiple blocks in a row. In that scenario -the leader is likely to be producing the next block while the replay stage for -the first block is playing. - -When the leader finishes the replay stage it can reset the balance cache by -clearing it, and set a new fork as the base for the cache which can become -active on the next block. - -## Resetting the Balance Cache - -1. At the start of the block, if the balance cache is uninitialized, set the - base fork for the balance cache to be the parent of the block and create an - empty cache. -2. if the cache is initialized, check if block's parents has a new frozen bank - that is newer than the current base fork for the balance cache. -3. if a parent newer than the cache's base fork exist, reset the cache to the - parent. - -## Impact on Clients - -The same fee account can be reused many times in the same block until it is used -once as Credit-Debit by an instruction. - -Clients that transmit a large number of transactions per second should use a -dedicated fee account that is not used as Credit-Debit in any instruction. - -Once an account fee is used as Credit-Debit, it will fail the balance check -until the balance cache is reset. - -### Check out the [SIMD here to contribute](https://github.com/solana-foundation/solana-improvement-documents/pull/5) diff --git a/docs/src/proposals/block-confirmation.md b/docs/src/proposals/block-confirmation.md deleted file mode 100644 index c973950a50a..00000000000 --- a/docs/src/proposals/block-confirmation.md +++ /dev/null @@ -1,85 +0,0 @@ ---- -title: Block Confirmation ---- - -A validator votes on a PoH hash for two purposes. First, the vote indicates it -believes the ledger is valid up until that point in time. Second, since many -valid forks may exist at a given height, the vote also indicates exclusive -support for the fork. This document describes only the former. The latter is -described in [Tower BFT](../implemented-proposals/tower-bft.md). - -## Current Design - -To start voting, a validator first registers an account to which it will send -its votes. It then sends votes to that account. The vote contains the tick -height of the block it is voting on. The account stores the 32 highest heights. - -### Problems - -- Only the validator knows how to find its own votes directly. - - Other components, such as the one that calculates confirmation time, needs to - be baked into the validator code. The validator code queries the bank for all - accounts owned by the vote program. - -- Voting ballots do not contain a PoH hash. The validator is only voting that - it has observed an arbitrary block at some height. - -- Voting ballots do not contain a hash of the bank state. Without that hash, - there is no evidence that the validator executed the transactions and - verified there were no double spends. - -## Proposed Design - -### No Cross-block State Initially - -At the moment a block is produced, the leader shall add a NewBlock transaction -to the ledger with a number of tokens that represents the validation reward. -It is effectively an incremental multisig transaction that sends tokens from -the mining pool to the validators. The account should allocate just enough -space to collect the votes required to achieve a supermajority. When a -validator observes the NewBlock transaction, it has the option to submit a vote -that includes a hash of its ledger state (the bank state). Once the account has -sufficient votes, the vote program should disperse the tokens to the -validators, which causes the account to be deleted. - -#### Logging Confirmation Time - -The bank will need to be aware of the vote program. After each transaction, it -should check if it is a vote transaction and if so, check the state of that -account. If the transaction caused the supermajority to be achieved, it should -log the time since the NewBlock transaction was submitted. - -### Finality and Payouts - -[Tower BFT](../implemented-proposals/tower-bft.md) is the proposed fork selection algorithm. It proposes -that payment to miners be postponed until the _stack_ of validator votes reaches -a certain depth, at which point rollback is not economically feasible. The vote -program may therefore implement Tower BFT. Vote instructions would need to -reference a global Tower account so that it can track cross-block state. - -## Challenges - -### On-chain voting - -Using programs and accounts to implement this is a bit tedious. The hardest -part is figuring out how much space to allocate in NewBlock. The two variables -are the _active set_ and the stakes of those validators. If we calculate the -active set at the time NewBlock is submitted, the number of validators to -allocate space for is known upfront. If, however, we allow new validators to -vote on old blocks, then we'd need a way to allocate space dynamically. - -Similar in spirit, if the leader caches stakes at the time of NewBlock, the -vote program doesn't need to interact with the bank when it processes votes. If -we don't, then we have the option to allow stakes to float until a vote is -submitted. A validator could conceivably reference its own staking account, but -that'd be the current account value instead of the account value of the most -recently finalized bank state. The bank currently doesn't offer a means to -reference accounts from particular points in time. - -### Voting Implications on Previous Blocks - -Does a vote on one height imply a vote on all blocks of lower heights of -that fork? If it does, we'll need a way to lookup the accounts of all -blocks that haven't yet reached supermajority. If not, the validator could -send votes to all blocks explicitly to get the block rewards. diff --git a/docs/src/proposals/cluster-test-framework.md b/docs/src/proposals/cluster-test-framework.md deleted file mode 100644 index 30c6b5bc6eb..00000000000 --- a/docs/src/proposals/cluster-test-framework.md +++ /dev/null @@ -1,103 +0,0 @@ ---- -title: Cluster Test Framework ---- - -This document proposes the Cluster Test Framework \(CTF\). CTF is a test harness that allows tests to execute against a local, in-process cluster or a deployed cluster. - -## Motivation - -The goal of CTF is to provide a framework for writing tests independent of where and how the cluster is deployed. Regressions can be captured in these tests and the tests can be run against deployed clusters to verify the deployment. The focus of these tests should be on cluster stability, consensus, fault tolerance, API stability. - -Tests should verify a single bug or scenario, and should be written with the least amount of internal plumbing exposed to the test. - -## Design Overview - -Tests are provided an entry point, which is a `contact_info::ContactInfo` structure, and a keypair that has already been funded. - -Each node in the cluster is configured with a `validator::ValidatorConfig` at boot time. At boot time this configuration specifies any extra cluster configuration required for the test. The cluster should boot with the configuration when it is run in-process or in a data center. - -Once booted, the test will discover the cluster through a gossip entry point and configure any runtime behaviors via validator RPC. - -## Test Interface - -Each CTF test starts with an opaque entry point and a funded keypair. The test should not depend on how the cluster is deployed, and should be able to exercise all the cluster functionality through the publicly available interfaces. - -```text -use crate::contact_info::ContactInfo; -use solana_keypair::Keypair; -use solana_signer::Signer; -pub fn test_this_behavior( - entry_point_info: &ContactInfo, - funding_keypair: &Keypair, - num_nodes: usize, -) -``` - -## Cluster Discovery - -At test start, the cluster has already been established and is fully connected. The test can discover most of the available nodes over a few second. - -```text -use crate::gossip_service::discover_nodes; - -// Discover the cluster over a few seconds. -let cluster_nodes = discover_nodes(&entry_point_info, num_nodes); -``` - -## Cluster Configuration - -To enable specific scenarios, the cluster needs to be booted with special configurations. These configurations can be captured in `validator::ValidatorConfig`. - -For example: - -```text -let mut validator_config = ValidatorConfig::default_for_test(); -let local = LocalCluster::new_with_config( - num_nodes, - 10_000, - 100, - &validator_config - ); -``` - -## How to design a new test - -For example, there is a bug that shows that the cluster fails when it is flooded with invalid advertised gossip nodes. Our gossip library and protocol may change, but the cluster still needs to stay resilient to floods of invalid advertised gossip nodes. - -Configure the RPC service: - -```text -let mut validator_config = ValidatorConfig::default_for_test(); -validator_config.rpc_config.enable_rpc_gossip_push = true; -validator_config.rpc_config.enable_rpc_gossip_refresh_active_set = true; -``` - -Wire the RPCs and write a new test: - -```text -pub fn test_large_invalid_gossip_nodes( - entry_point_info: &ContactInfo, - funding_keypair: &Keypair, - num_nodes: usize, -) { - let cluster = discover_nodes(&entry_point_info, num_nodes); - - // Poison the cluster. - let client = create_client(entry_point_info.client_facing_addr(), VALIDATOR_PORT_RANGE); - for _ in 0..(num_nodes * 100) { - client.gossip_push( - cluster_info::invalid_contact_info() - ); - } - sleep(Durration::from_millis(1000)); - - // Force refresh of the active set. - for node in &cluster { - let client = create_client(node.client_facing_addr(), VALIDATOR_PORT_RANGE); - client.gossip_refresh_active_set(); - } - - // Verify that spends still work. - verify_spends(&cluster); -} -``` diff --git a/docs/src/proposals/comprehensive-compute-fees.md b/docs/src/proposals/comprehensive-compute-fees.md deleted file mode 100644 index 786b553721a..00000000000 --- a/docs/src/proposals/comprehensive-compute-fees.md +++ /dev/null @@ -1,153 +0,0 @@ ---- -title: Comprehensive Compute Fees ---- - -## Motivation - -The current fee structure lacks a comprehensive account of the work required by -a validator to process a transaction. The fee structure is only based on the -number of signatures in a transaction but is meant to account for the work that -the validator must perform to validate each transaction. The validator performs -a lot more user-defined work than just signature verification. Processing a -transaction typically includes signature verification, account locking, account -loading, and instruction processing. - -## Proposed Solution - -The following solution does not specify what native token costs are to be -associated with the new fee structure. Instead, it sets the criteria and -provides the knobs that a cost model can use to determine those costs. - -### Fee - -The goal of the fees is to cover the computation cost of processing a -transaction. Each of the fee categories below will be represented as a compute -unit cost that, when added together, encompasses the entire cost of processing -the transaction. By calculating the total cost of the transaction, the runtime -can charge a more representative fee and make better transaction scheduling -decisions. - -A fee could be calculated based on: - -1. Number of signatures - - Fixed rate per signature -2. Number of write locks - - Fixed rate per writable account -3. Data byte cost - - Fixed rate per byte of the sum of the length all a transactions instruction - data -4. Account sizes - - Account sizes can't be known up-front but can account for a considerable - amount of the load the transaction incurs on the network. The payer will - be charged for a maximum account size (10m) upfront and refunded the - difference after the actual account sizes are known. -5. Compute budget - - Each transaction will be given a default transaction-wide compute budget of - 200k units with the option of requesting a larger budget via a compute - budget instruction up to a maximum of 1m units. This budget is used to - limit the time it takes to process a transaction. The compute budget - portion of the fee will be charged up-front based on the default or - requested amount. After processing, the actual number of units consumed - will be known, and the payer will be refunded the difference, so the payer - only pays for what they used. Builtin programs will have a fixed cost - while SBF program's cost will be measured at runtime. -6. Precompiled programs - - Precompiled programs are performing compute-intensive operations. The work - incurred by a precompiled program is predictable based on the instruction's - data array. Therefore a cost will be assigned per precompiled program - based on the parsing of instruction data. Because precompiled programs are - processed outside of the bank, their compute cost will not be reflected in - the compute budget and will not be used in transaction scheduling - decisions. The methods used to determine the fixed cost of the components - above are described in - [#19627](https://github.com/solana-labs/solana/issues/19627) - -### Cost model - -The cost model is used to assess what load a transaction will incur during -in-slot processing and then make decisions on how to best schedule transaction -into batches. - -The cost model's criteria are identical to the fee's criteria except for -signatures and precompiled programs. These two costs are incurred before a -transaction is scheduled and therefore do not affect how long a transaction -takes within a slot to process. - -### Cache account sizes and use them instead of the max - -https://github.com/solana-labs/solana/issues/20511 - -### Requestable compute budget caps and heap sizes - -The precompiled -[ComputeBudget](https://github.com/solana-labs/solana/blob/00929f836348d76cb3503d0ba5f76f0d275bcc66/sdk/src/compute_budget.rs#L34) -program can be used to request higher transaction-wide compute budget caps and -program heap sizes. The requested increases will be reflected in the -transaction's fee. - -### Fees for precompiled program failures - -https://github.com/solana-labs/solana/issues/20481 - -### Rate governing - -Current rate governing needs to be re-assessed. Fees are being rate -governed down to their minimums because the number of signatures in each slot is -far lower than the "target" signatures per slot. - -Instead of using the number of signatures to rate govern, the cost model will -feed back information based on the batch/queue load it is seeing. The fees will -sit at a target rate and only increase if the load goes above a specified but to -be determined threshold. The governing will be applied across all the fee -criteria. - -### Deterministic fees - -Solana's fees are currently deterministic based on a given blockhash. This -determinism is a nice feature that simplifies client interactions. An example -is when draining an account that is also the payer, the transaction issuer can -pre-compute the fee and then set the entire remaining balance to be transferred -out without worrying that the fee will change leaving a very small amount -remaining in the account. Another example is for offline signing, the payer -signer can guarantee what fee that will be charged for the transaction based on -the nonce's blockhash. - -Determinism is achieved in two ways: -- blockhash queue contains a list of recent (<=~2min) blockhashes and a - `lamports_per_signature` value. The blockhash queue is one of the snapshot's - serialized members and thus bank hash depends on it. -- Nonce accounts used for offline signing contain a `lamports_per_signature` - value in its account data - -In both cases, when a transaction is assessed a fee, the -`lamports_per_signature` to use is looked up (either in the queue or in the -nonce account's data) using the transaction's blockhash. - -This currently comes with the following challenges: -- Exposing the `FeeCalculator` object to the clients (holds the - `lamports_per_signature`) makes it hard to evolve the fee criteria due to - backward-compatibility. This issue is being solved by deprecating the - `FeeCalculator` object and instead the new apis take a message and return a - fee. -- Blockhash queue entries contain the fee criteria specifics and are part of the - bankhash so evolving the fees over time involves more work/risk -- Nonce accounts store the fee criteria directly in their account data so - evolving the fees over time requires changes to nonce account data and data - size. - -Two solutions to the latter two challenges -- Get rid of the concept of deterministic fees. Clients ask via RPC to - calculate the current fee estimate and the actual fee is assessed when the - transaction is processed. Fee changes will be governed and change slowly - based on network load so the fee differences will be small within the 2min - window. Nonce accounts no longer store the fee criteria but instead a fee - cap. If the assessed fee at the time of processing exceeds the cap then the - transaction fails. This solution removes fee criteria entirely from the - blockhash queue and nonce accounts and removes the need for either of those to - evolve if there is a need for fee criteria to evolve. -- Retain the concept of deterministic fees. Clients ask via RPC to calculate - the current fee and pass in a blockhash that fee will be associated with. - Blockhash queue and nonce accounts switch to a versioned but internal "Fee" - object (similar to "FeeCalculator"). Each time there is a need for fees to - evolve the fee object will add a new version and new blockhash queue entries - and new nonce accounts will use the new version. diff --git a/docs/src/proposals/embedding-move.md b/docs/src/proposals/embedding-move.md deleted file mode 100644 index e45730267fd..00000000000 --- a/docs/src/proposals/embedding-move.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -title: Embedding the Move Language ---- - -### This document is outdated and a new approach to support 'move' is in the works. - -## Problem - -Solana enables developers to write on-chain programs in general purpose programming languages such as C or Rust, but those programs contain Solana-specific mechanisms. For example, there isn't another chain that asks developers to create a Rust module with a `process_instruction(KeyedAccounts)` function. Whenever practical, Solana should offer application developers more portable options. - -Until just recently, no popular blockchain offered a language that could expose the value of Solana's massively parallel [runtime](../validator/runtime.md). Solidity contracts, for example, do not separate references to shared data from contract code, and therefore need to be executed serially to ensure deterministic behavior. In practice we see that the most aggressively optimized EVM-based blockchains all seem to peak out around 1,200 TPS - a small fraction of what Solana can do. The Libra project, on the other hand, designed an on-chain programming language called Move that is more suitable for parallel execution. Like Solana's runtime, Move programs depend on accounts for all shared state. - -The biggest design difference between Solana's runtime and Libra's Move VM is how they manage safe invocations between modules. Solana took an operating systems approach and Libra took the domain-specific language approach. In the runtime, a module must trap back into the runtime to ensure the caller's module did not write to data owned by the callee. Likewise, when the callee completes, it must again trap back to the runtime to ensure the callee did not write to data owned by the caller. Move, on the other hand, includes an advanced type system that allows these checks to be run by its bytecode verifier. Because Move bytecode can be verified, the cost of verification is paid just once, at the time the module is loaded on-chain. In the runtime, the cost is paid each time a transaction crosses between modules. The difference is similar in spirit to the difference between a dynamically-typed language like Python versus a statically-typed language like Java. Solana's runtime allows applications to be written in general purpose programming languages, but that comes with the cost of runtime checks when jumping between programs. - -This proposal attempts to define a way to embed the Move VM such that: - -- cross-module invocations within Move do not require the runtime's - - cross-program runtime checks - -- Move programs can leverage functionality in other Solana programs and vice - - versa - -- Solana's runtime parallelism is exposed to batches of Move and non-Move - - transactions - -## Proposed Solution - -### Move VM as a Solana loader - -The Move VM shall be embedded as a Solana loader under the identifier `MOVE_PROGRAM_ID`, so that Move modules can be marked as `executable` with the VM as its `owner`. This will allow modules to load module dependencies, as well as allow for parallel execution of Move scripts. - -All data accounts owned by Move modules must set their owners to the loader, `MOVE_PROGRAM_ID`. Since Move modules encapsulate their account data in the same way Solana programs encapsulate theirs, the Move module owner should be embedded in the account data. The runtime will grant write access to the Move VM, and Move grants access to the module accounts. - -### Interacting with Solana programs - -To invoke instructions in non-Move programs, Solana would need to extend the Move VM with a `process_instruction()` system call. It would work the same as `process_instruction()` Rust SBF programs. diff --git a/docs/src/proposals/fee_transaction_priority.md b/docs/src/proposals/fee_transaction_priority.md deleted file mode 100644 index 1b6b06de365..00000000000 --- a/docs/src/proposals/fee_transaction_priority.md +++ /dev/null @@ -1,210 +0,0 @@ ---- -title: Fee Transaction Priority ---- - -Additional fees were introduced to transactions as a method to allow users to bid for priority for -their transactions in the leader's queue. - -The fee priority of a transaction `T` can then be defined as `F(T)`, where `F(T)` is the "fee-per -compute-unit", calculated by: - -`(additional_fee + base_fee) / requested_compute_units` - -To ensure users get fair priority based on their fee, the proposed scheduler for the leader must -guarantee that given `T1` and `T2` in the pending queue, and `F(T1) > F(T2)`: - -1. `T1` should be considered for processing before `T2` -2. If `T1` cannot be processed before `T2` because there's already a transaction currently being -processed that contends on an account `A`, then `T2` should not be scheduled if it would grab -any account locks needed by `T1`. This prevents lower fee transactions like `T2` from starving -higher paying transactions like `T1`. - - -### Transaction Pipeline - -Pipeline: -1. Sigverify -2. Scheduler -3. BankingStage threads - -Transactions from sigverify are connected via a channel to the scheduler. The scheduler maintains -`N` bi-directional channels with the `N` BankingStage threads, implemented as a pair of two -unidirectional channels. - -The scheduler's job is to run an algorithm in which it determines how to schedule transactions -received from sigverify to the `N` BankingStage threads. A transaction is scheduled to be executed -on a particular BankingStage thread by sending that transaction to the thread via its associated -channel. - -Once a BankingStage thread finishes processing a transaction `T` , it sends the `T` back -to the scheduler via the same channel to signal of completion. - -### Scheduler Implementation - -The scheduler is the most complex piece of the above pipeline, its implementation is made up of a -few pieces. Note for now, all these pieces are maintained by the single scheduler thread to avoid -locking complexity. - -#### Components of the `Scheduler`: - -1. `default_transaction_queue` - A max-heap `BinaryHeap` that tracks all pending transactions. -The priority in the heap is the additional fee of the transaction. Transactions are added to this queue -from sigverify before the leader slot begins. -2. `all_transaction_queues` - A `VecDeque>` that tracks all pending queues of work. -Some pending queues have higher priority than others (as will be explained later in the `Handling Completion Signals from BankingStage Threads` section below). The list is ordered in priority from highest to lowest priority. On -initialization, `all_transaction_queues[0] = default_transaction_queue`. -3. `locked_accounts` - A `HashMap` that tracks the set of accounts needed to execute the -current set of transactions scheduled/sent to banking threads. Accounts are added to this set -*before* being sent to BankingStage threads. The `usize` represents a refcount, because multiple read -accounts could be grabbed.`LockedPubkey` is defined as: -``` -enum LockedPubkey { - Read(Pubkey), - Write(Pubkey), -} -``` -4. `blocked_transactions` - A `HashMap>` keyed by -transaction signature, and maps to a `BlockedTransactionsQueue` defined as: -``` -/// Represents a heap of transactions that cannot be scheduled because they -/// would take locks on accounts needed by a higher paying transaction -struct BlockedTransactionsQueue { - // The higher priority transaction blocking all the other transactions in - // `blocked_transactions` below - highest_priority_blocked_transaction: Transaction, - other_blocked_transactions: BinaryHeap -} -``` -5. `blocked_transaction_queues_by_accounts` - A `HashMap>` keyed by -account keys. - -#### Algorithm (Main Loop): - -Assume `N` BankingStage threads: - -The scheduler will run for each banking thread a function `find_next_highest_transaction()`: - -1. Pop off the highest priority transaction `next_highest_transaction` from `self.all_transaction_queues[0]`. -If ``self.all_transaction_queues[0]` is empty, pop off the first deque item and continue. - -2. Let `transaction_accounts` be the set of `LockedPubkey` keys needed by -`next_highest_transaction`. We run the following: - -``` - for account_key in transaction_accounts { - // Check if the `LockedPubkey` conflicts with any key in the `locked_accounts` set, which - // would indicate a transaction using this account with a conflicting lock is already - // running - if self.locked_accounts.is_conflicting(account_key) { - return Conflict; - } - - // Check if any higher fee transaction has already reserved this account. This prevents - // lower fee transactions from starving higher fee transactions. - if self.blocked_transaction_queues_by_accounts.contains_key(account_key) { - return Conflict; - } - return NoConflict; - } -``` - -3. In the case of a `NoConflict` we run: - -``` - for account_key in transaction_accounts { - self.locked_accounts.insert_reference(account_key.key()); - } - - banking_thread_channel.send(next_highest_transaction); -``` - -4. In the case of a `Conflict` we run: - -``` -for locked_account_key in transaction_accounts { - let account_key = locked_account_key.key() - let blocked_transaction_entry = self.blocked_transaction_queues_by_accounts.entry(account_key); - match blocked_transaction_entry { - Occupied(existing_blocked_transaction) => { - // If there is already a set of transactions blocked on this account, add - // this transaction to the priority queue. - existing_blocked_transaction.insert_transaction(next_highest_transaction); - } - - Vacant(vacant_entry) => { - // Create a new queue blocked on this transaction - let new_blocked_transaction_queue = - Rc::new(BlockedTransactionsQueue { - highest_priority_blocked_transaction: next_highest_transaction, - other_blocked_transactions: BinaryHeap::new(), - }); - // Insert into the hashmap for this `account_key` - vacant_entry.insert(new_blocked_transaction_queue.clone()); - // Insert into the `blocked_transactions` hashmap to indicate this set of transactions - // is blocked by `next_highest_transaction` - self.blocked_transactions.insert( - next_highest_transaction.signature(), - new_blocked_transaction_queue - ); - } - } -} -``` - -5. Run until all `N` BankingStage threads have been sent `processing_batch` transactions (i.e. hit step 3 above). - -#### Banking Threads -1. Banking threads maintain a queue of transactions sent to them by the scheduler, sorted by priority. -2. Because the scheduler has guaranteed that there are no locking conflicts, the banking thread can process -some `M` of these transactions at a time and pack them into entries - -#### Handling Completion Signals from BankingStage Threads - -Outside of the main loop above, we rely on BankingThreads threads signaling us they've finished their -task to schedule the next transactions. - -1. Once a BankingStage thread finishes processing a batch of transactions `completed_transactions_batch` , -it sends the `completed_transactions_batch` back to the scheduler via the same channel to signal of completion. - -2. Upon receiving this signal, the BankingStage thread processes the locked accounts -`transaction_accounts` for each `completed_transaction` in `completed_transactions_batch`: -``` -let mut unlocked_accounts = vec![]; -// First remove all the locks from the tracking list -for locked_account in transaction_accounts { - if self.locked_accounts.remove_reference(locked_account) { - unlocked_accounts.push(locked_account.key()); - } -} - -// Check if freeing up these accounts has now allowed any new -// blocked transactions to run -for account_key in unlocked_accounts { - if let Some(blocked_transaction_queue) = self.blocked_transaction_queues_by_accounts.get(account_key) { - // Check if the transaction blocking this queue can be run now, thereby unblocking this queue - if blocked_transaction_queue.highest_priority_blocked_transaction.can_get_locks() { - // Schedule the transaction to the banking thread - banking_thread_channel.send(blocked_transaction_queue.highest_priority_blocked_transaction); - - return; - } - } - - // If no higher priority transactions were unblocked, continue scheduling from the main queue, - // described in the main loop section above - find_next_highest_transaction(); -} -``` - -3. Check if the finished transaction was the blocking transaction for any queue: - -``` -if let Some(blocked_transaction_queue) = self.blocked_transactions.get(completed_transaction.signature) { - // Now push the rest of the queue to the head of `all_transaction_queues`, since we know - // everything in this blocked queue must be of higher priority, (since they were - // added to this queue earlier, this means they must have been popped off the main - // `transaction_accounts` queue earlier, hence higher priority) - self.all_transaction_queues.push_front(blocked_transaction_queue.other_blocked_transactions); - self.blocked_transactions.remove(completed_transaction.signature); -} -``` \ No newline at end of file diff --git a/docs/src/proposals/handle-duplicate-block.md b/docs/src/proposals/handle-duplicate-block.md deleted file mode 100644 index 2941f17d270..00000000000 --- a/docs/src/proposals/handle-duplicate-block.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -title: Handle Duplicate Block ---- - -# Leader Duplicate Block Slashing - -This design describes how the cluster slashes leaders that produce duplicate -blocks. - -Leaders that produce multiple blocks for the same slot increase the number of -potential forks that the cluster has to resolve. - -## Primitives -1. gossip_root: Nodes now gossip their current root -2. gossip_duplicate_slots: Nodes can gossip up to `N` duplicate slot proofs. -3. `DUPLICATE_THRESHOLD`: The minimum percentage of stake that needs to vote on a fork with version `X` of a duplicate slot, in order for that fork to become votable. - -## Protocol -1. When WindowStage detects a duplicate slot proof `P`, it checks the new `gossip_root` to see if `<= 1/3` of the nodes have rooted a slot `S >= P`. If so, it pushes a proof to `gossip_duplicate_slots` to gossip. WindowStage then signals ReplayStage about this duplicate slot `S`. These proofs can be purged from gossip once the validator sees > 2/3 of people gossiping roots `R > S`. - -2. When ReplayStage receives the signal for a duplicate slot `S` from `1)` above, the validator monitors gossip and replay waiting for`>= DUPLICATE_THRESHOLD` votes for the same hash which implies the same version of the slot. If this condition is met for some version with hash `H` of slot `S`, this is then known as the `duplicate_confirmed` version of the slot. - -Before a duplicate slot `S` is `duplicate_confirmed`, it's first excluded from the vote candidate set in the fork choice rules. In addition, ReplayStage also resets PoH to the *latest* ancestor of the *earliest* `non-duplicate/confirmed_duplicate_slot`, so that block generation can start happening on the earliest known *safe* block. - -Some notes about the `DUPLICATE_THRESHOLD`. In the cases below, assume `DUPLICATE_THRESHOLD = 52`: - -a) If less than `2 * DUPLICATE_THRESHOLD - 1` percentage of the network is malicious, then there can only be one such `duplicate_confirmed` version of the slot. With `DUPLICATE_THRESHOLD = 52`, this is -a malicious tolerance of `4%` - -b) The liveness of the network is at most `1 - DUPLICATE_THRESHOLD - SWITCH_THRESHOLD`. This is because if you need at least `SWITCH_THRESHOLD` percentage of the stake voting on a different fork in order to switch off of a duplicate fork that has `< DUPLICATE_THRESHOLD` stake voting on it, and is *not* `duplicate_confirmed`. For `DUPLICATE_THRESHOLD = 52` and `DUPLICATE_THRESHOLD = 38`, this implies a liveness tolerance of `10%`. - -For example in the situation below, validators that voted on `2` can't vote any further on fork `2` because it's been removed from fork choice. Now slot 6 better have enough stake for a switching proof, or the network halts. - -```text - |-------- 2 (51% voted, then detected this slot was a duplicate and removed this slot from fork choice) -0---| - |---------- 6 (39%) - -``` - -3. Switching proofs need to be extended to allow including vote hashes from different versions of the same slot (detected through 1). Right now this is not supported since switching proofs can -only be built using votes from banks in BankForks, and two different versions of the same slot cannot -simultaneously exist in BankForks. For instance: - -```text - |-------- 2 - | -0------------- 1 ------ 2' - | - |---------- 6 - -``` - -Imagine each version of slot 2 and 2' have `DUPLICATE_THRESHOLD / 2` of the votes on them, so neither duplicate can be confirmed. At most slot 6 has `1 - DUPLICATE_THRESHOLD / 2` of the votes -on it, which is less than the switching threshold. Thus, in order for validators voting on `2` or `2'` to switch to slot 6, and make progress, they need to incorporate votes from the other version of the slot into their switching proofs. - - -### The repair problem. -Now what happens if one of the following occurs: - -1) Due to network blips/latencies, some validators fail to observe the gossip votes before they are overwritten by newer votes? Then some validators may conclude a slot `S` is `duplicate_confirmed` while others don't. - -2) Due to lockouts, no version of duplicate slot `S` reaches `duplicate_confirmed` status, but one of its descendants may reach `duplicate_confirmed` after those lockouts expire, which by definition, means `S` is also `duplicate_confirmed`. - -3) People who are catching up and don't see the votes in gossip encounter a dup block and can't make progress. - -We assume that given a network is eventually stable, if at least one correct validator observed `S` is `duplicate_confirmed`, then if `S` is part of the heaviest fork, then eventually all validators will observe some descendant of `S` is duplicate confirmed. - -This problem we need to solve is modeled simply by the below scenario: - -```text -1 -> 2 (duplicate) -> 3 -> 4 (duplicate) -``` -Assume the following: - -1. Due to gossiping duplicate proofs, we assume everyone will eventually see duplicate proofs for 2 and 4, so everyone agrees to remove them from fork choice until they are `duplicate_confirmed`. - -2. Due to lockouts, `> DUPLICATE_THRESHOLD` of the stake votes on 4, but not 2. This means at least `DUPLICATE_THRESHOLD` of people have the "correct" version of both slots 2 and 4. - -3. However, the remaining `1-DUPLICATE_THRESHOLD` of people have wrong version of 2. This means in replay, their slot 3 will be marked dead, *even though the faulty slot is 2*. The goal is to get these people on the right fork again. - -Possible solution: - -1. Change `EpochSlots` to signal when a bank is frozen, not when a slot is complete. If we see > `DUPLICATE_THRESHOLD` have frozen the dead slot 3, then we attempt recovery. Note this does not mean that all `DUPLICATE_THRESHOLD` have frozen the same version of the bank, it's just a signal to us that something may be wrong with our version of the bank. - -2. Recovery takes the form of a special repair request, `RepairDuplicateConfirmed(dead_slot, Vec<(Slot, Hash)>)`, which specifies a dead slot, and then a vector of `(slot, hash)` of `N` of its latest parents. - -3. The repairer sees this request and responds with the correct hash only if any element of the `(slot, hash)` vector is both `duplicate_confirmed` and the hash doesn't match the requester's hash in the vector. - -4. Once the requester sees the "correct" hash is different than their frozen hash, they dump the block so that they can accept a new block, and ask the network for the block with the correct hash. - -Of course the repairer might lie to you, and you'll get the wrong version of the block, in which case you'll end up with another dead block and repeat the procedure. diff --git a/docs/src/proposals/interchain-transaction-verification.md b/docs/src/proposals/interchain-transaction-verification.md deleted file mode 100644 index 7a50042227f..00000000000 --- a/docs/src/proposals/interchain-transaction-verification.md +++ /dev/null @@ -1,105 +0,0 @@ ---- -title: Inter-chain Transaction Verification ---- - -## Problem - -Inter-chain applications are not new to the digital asset ecosystem; in fact, even the smaller centralized exchanges still categorically dwarf all single chain applications put together in terms of users and volume. They command massive valuations and have spent years effectively optimizing their core products for a broad range of end users. However, their basic operations center around mechanisms that require their users to unilaterally trust them, typically with little to no recourse or protection from accidental loss. This has led to the broader digital asset ecosystem being fractured along network lines because interoperability solutions typically: - -- Are technically complex to fully implement -- Create unstable network scale incentive structures -- Require consistent and high level cooperation between stakeholders - -## Proposed Solution - -Simple Payment Verification \(SPV\) is a generic term for a range of different methodologies used by light clients on most major blockchain networks to verify aspects of the network state without the burden of fully storing and maintaining the chain itself. In most cases, this means relying on a form of hash tree to supply a proof of the presence of a given transaction in a certain block by comparing against a root hash in that block’s header or equivalent. This allows a light client or wallet to reach a probabilistic level of certainty about on-chain events by itself with a minimum of trust required with regard to network nodes. - -Traditionally the process of assembling and validating these proofs is carried out off chain by nodes, wallets, or other clients, but it also offers a potential mechanism for inter-chain state verification. However, by moving the capability to validate SPV proofs on-chain as a smart contract while leveraging the archival properties inherent to the blockchain, it is possible to construct a system for programmatically detecting and verifying transactions on other networks without the involvement of any type of trusted oracle or complex multi-stage consensus mechanism. This concept is broadly generalisable to any network with an SPV mechanism and can even be operated bilaterally on other smart contract platforms, opening up the possibility of cheap, fast, inter-chain transfer of value without relying on collateral, hashlocks, or trusted intermediaries. - -Opting to take advantage of well established and developmentally stable mechanisms already common to all major blockchains allows SPV based interoperability solutions to be dramatically simpler than orchestrated multi-stage approaches. As part of this, they dispense with the need for widely agreed upon cross chain communication standards and the large multi-party organizations that write them in favor of a set of discrete contract-based services that can be easily utilized by caller contracts through a common abstraction format. This will set the groundwork for a broad range of applications and contracts able to interoperate across the variegated and every growing platform ecosystem. - -## Terminology - -SPV Program - Client-facing interface for the inter-chain SPV system, manages participant roles. SPV Engine - Validates transaction proofs, subset of the SPV Program. Client - The caller to the SPV Program, typically another solana contract. Prover - Party who generates proofs for transactions and submits them to the SPV Program. Transaction Proof - Created by Provers, contains a merkle proof, transaction, and blockheader reference. Merkle Proof - Basic SPV proof that validates the presence of a transaction in a certain block. Block Header - Represents the basic parameters and relative position of a given block. Proof Request - An order placed by a client for verification of transaction\(s\) by provers. Header Store - A data structure for storing and referencing ranges of block headers in proofs. Client Request - Transaction from the client to the SPV Program to trigger creation of a Proof Request. Sub-account - A Solana account owned by another contract account, without its own private key. - -## Service - -SPV Programs run as contracts deployed on the Solana network and maintain a type of public marketplace for SPV proofs that allows any party to submit both requests for proofs as well as proofs themselves for verification in response to requests. There will be multiple SPV Program instances active at any given time, at least one for each connected external network and potentially multiple instances per network. SPV program instances will be relatively consistent in their high level API and feature sets with some variation between currency platforms \(Bitcoin, Litecoin\) and smart contract platforms owing to the potential for verification of network state changes beyond simply transactions. In every case regardless of network, the SPV Program relies on an internal component called an SPV engine to provide stateless verification of the actual SPV proofs upon which the higher level client facing features and api are built. The SPV engine requires a network specific implementation, but allows easy extension of the larger inter-chain ecosystem by any team who chooses to carry out that implementation and drop it into the standard SPV program for deployment. - -For purposes of Proof Requests, the requester is referred to as the program client, which in most if not all cases will be another Solana Contract. The client can choose to submit a request pertaining to a specific transaction or to include a broader filter that can apply to any of a range of parameters of a transaction including its inputs, outputs, and amount. For example, A client could submit a request for any transaction sent from a given address A to address B with the amount X after a certain time. This structure can be used in a range of applications, such as verifying a specific intended payment in the case of an atomic swap or detecting the movement of collateral assets for a loan. - -Following submission of a Client Request, assuming that it is successfully validated, a proof request account is created by the SPV program to track the progress of the request. Provers use the account to specify the request they intend to fill in the proofs they submit for validation, at which point the SPV program validates those proofs and if successful, saves them to the account data of the request account. Clients can monitor the status of their requests and see any applicable transactions alongside their proofs by querying the account data of the request account. In future iterations when supported by Solana, this process will be simplified by contracts publishing events rather than requiring a polling style process as described. - -## Implementation - -The Solana Inter-chain SPV mechanism consists of the following components and participants: - -### SPV engine - -A contract deployed on Solana which statelessly verifies SPV proofs for the caller. It takes as arguments for validation: - -- An SPV proof in the correct format of the blockchain associated with the program -- Reference\(s\) to the relevant block headers to compare that proof against -- The necessary parameters of the transaction to verify - - If the proof in question is successfully validated, the SPV program saves proof - - of that verification to the request account, which can be saved by the caller to - - its account data or otherwise handled as necessary. SPV programs also expose - - utilities and structs used for representation and validation of headers, - - transactions, hashes, etc. on a chain by chain basis. - -### SPV program - -A contract deployed on Solana which coordinates and intermediates the interaction between Clients and Provers and manages the validation of requests, headers, proofs, etc. It is the primary point of access for Client contracts to access the inter-chain. SPV mechanism. It offers the following core features: - -- Submit Proof Request - allows client to place a request for a specific proof or set of proofs -- Cancel Proof Request - allows client to invalidate a pending request -- Fill Proof Request - used by Provers to submit for validation a proof corresponding to a given Proof Request - - The SPV program maintains a publicly available listing of valid pending Proof - - Requests in its account data for the benefit of the Provers, who monitor it and - - enclose references to target requests with their submitted proofs. - -### Proof Request - -A message sent by the Client to the SPV engine denoting a request for a proof of a specific transaction or set of transactions. Proof Requests can either manually specify a certain transaction by its hash or can elect to submit a filter that matches multiple transactions or classes of transactions. For example, a filter matching "any transaction from address xxx to address yyy" could be used to detect payment of a debt or settlement of an inter-chain swap. Likewise, a filter matching "any transaction from address xxx" could be used by a lending or synthetic token minting contract to monitor and react to changes in collateralization. Proof Requests are sent with a fee, which is disbursed by the SPV engine contract to the appropriate Prover once a proof matching that request is validated. - -### Request Book - -The public listing of valid, open Proof Requests available to provers to fill or for clients to cancel. Roughly analogous to an orderbook in an exchange, but with a single type of listing rather than two separate sides. It is stored in the account data of the SPV program. - -### Proof - -A proof of the presence of a given transaction in the blockchain in question. Proofs encompass both the actual merkle proof and reference\(s\) to a chain of valid sequential block headers. They are constructed and submitted by Provers in accordance with the specifications of the publicly available Proof Requests hosted on the request book by the SPV program. Upon Validation, they are saved to the account data of the relevant Proof Request, which can be used by the Client to monitor the state of the request. - -### Client - -The originator of a request for a transaction proof. Clients will most often be other contracts as parts of applications or specific financial products like loans, swaps, escrow, etc. The client in any given verification process cycle initially submits a ClientRequest which communicates the parameters and fee and if successfully validated, results in the creation of a Proof Request account by the SPV program. The Client may also submit a CancelRequest referencing an active Proof Request in order to denote it as invalid for purposes of proof submission. - -### Prover - -The submitter of a proof that fills a Proof Request. Provers monitor the request book of the SPV program for outstanding Proof Requests and generate matching proofs, which they submit to the SPV program for validation. If the proof is accepted, the fee associated with the Proof Request in question is disbursed to the Prover. Provers typically operate as Solana Blockstreamer nodes that also have access to a Bitcoin node, which they use for purposes of constructing proofs and accessing block headers. - -### Header Store - -An account-based data structure used to maintain block headers for the purpose of inclusion in submitted proofs by reference to the header store account. header stores can be maintained by independent entities, since header chain validation is a component of the SPV program proof validation mechanism. Fees that are paid out by Proof Requests to Provers are split between the submitter of the merkle proof itself and the header store that is referenced in the submitted proof. Due to the current inability to grow already allocated account data capacity, the use case necessitates a data structure that can grow indefinitely without rebalancing. Sub-accounts are accounts owned by the SPV program without their own private keys that are used for storage by allocating blockheaders to their account data. Multiple potential approaches to the implementation of the header store system are feasible: - -Store Headers in program sub-accounts indexed by Public address: - -- Each sub-account holds one header and has a public key matching the blockhash -- Requires same number of account data lookups as confirmations per verification -- Limit on number of confirmations \(15-20\) via max transaction data ceiling -- No network-wide duplication of individual headers - -Linked List of multiple sub-accounts storing headers: - -- Maintain sequential index of storage accounts, many headers per storage account -- Max 2 account data lookups for >99.9% of verification \(1 for most\) -- Compact sequential data address format allows any number of confirmations and fast lookups -- Facilitates network-wide header duplication inefficiencies diff --git a/docs/src/proposals/ledger-replication-to-implement.md b/docs/src/proposals/ledger-replication-to-implement.md deleted file mode 100644 index 1d68fdf1c25..00000000000 --- a/docs/src/proposals/ledger-replication-to-implement.md +++ /dev/null @@ -1,415 +0,0 @@ ---- -title: Ledger Replication ---- - -Note: this ledger replication solution was partially implemented, but not -completed. The partial implementation was removed by -https://github.com/solana-labs/solana/pull/9992 in order to prevent the security -risk of unused code. The first part of this design document reflects the -once-implemented parts of ledger replication. The -[second part of this document](#ledger-replication-not-implemented) describes the -parts of the solution never implemented. - -## Proof of Replication - -At full capacity on a 1gbps network solana will generate 4 petabytes of data per year. To prevent the network from centralizing around validators that have to store the full data set this protocol proposes a way for mining nodes to provide storage capacity for pieces of the data. - -The basic idea to Proof of Replication is encrypting a dataset with a public symmetric key using CBC encryption, then hash the encrypted dataset. The main problem with the naive approach is that a dishonest storage node can stream the encryption and delete the data as it's hashed. The simple solution is to periodically regenerate the hash based on a signed PoH value. This ensures that all the data is present during the generation of the proof and it also requires validators to have the entirety of the encrypted data present for verification of every proof of every identity. So the space required to validate is `number_of_proofs * data_size` - -## Optimization with PoH - -Our improvement on this approach is to randomly sample the encrypted segments faster than it takes to encrypt, and record the hash of those samples into the PoH ledger. Thus the segments stay in the exact same order for every PoRep and verification can stream the data and verify all the proofs in a single batch. This way we can verify multiple proofs concurrently. The total space required for verification is `1_ledger_segment + 2_cbc_blocks * number_of_identities` with core count equal to `number_of_identities`. We use a 64-byte chacha CBC block size. - -## Network - -Validators for PoRep are the same validators that are verifying transactions. If an archiver can prove that a validator verified a fake PoRep, then the validator will not receive a reward for that storage epoch. - -Archivers are specialized _light clients_. They download a part of the ledger \(a.k.a Segment\) and store it, and provide PoReps of storing the ledger. For each verified PoRep archivers earn a reward of sol from the mining pool. - -## Constraints - -We have the following constraints: - -- Verification requires generating the CBC blocks. That requires space of 2 - - blocks per identity. So as many identities at once should be batched with as - - many proofs for those identities verified concurrently for the same dataset. - -- Validators will randomly sample the set of storage proofs to the set that - - they can handle, and only the creators of those chosen proofs will be - - rewarded. The validator can run a benchmark whenever its hardware configuration - - changes to determine what rate it can validate storage proofs. - -## Validation and Replication Protocol - -### Constants - -1. SLOTS_PER_SEGMENT: Number of slots in a segment of ledger data. The - - unit of storage for an archiver. - -2. NUM_KEY_ROTATION_SEGMENTS: Number of segments after which archivers - - regenerate their encryption keys and select a new dataset to store. - -3. NUM_STORAGE_PROOFS: Number of storage proofs required for a storage proof - - claim to be successfully rewarded. - -4. RATIO_OF_FAKE_PROOFS: Ratio of fake proofs to real proofs that a storage - - mining proof claim has to contain to be valid for a reward. - -5. NUM_STORAGE_SAMPLES: Number of samples required for a storage mining - - proof. - -6. NUM_CHACHA_ROUNDS: Number of encryption rounds performed to generate - - encrypted state. - -7. NUM_SLOTS_PER_TURN: Number of slots that define a single storage epoch or - - a "turn" of the PoRep game. - -### Validator behavior - -1. Validators join the network and begin looking for archiver accounts at each - - storage epoch/turn boundary. - -2. Every turn, Validators sign the PoH value at the boundary and use that signature - - to randomly pick proofs to verify from each storage account found in the turn boundary. - - This signed value is also submitted to the validator's storage account and will be used by - - archivers at a later stage to cross-verify. - -3. Every `NUM_SLOTS_PER_TURN` slots the validator advertises the PoH value. This is value - - is also served to Archivers via RPC interfaces. - -4. For a given turn N, all validations get locked out until turn N+3 \(a gap of 2 turn/epoch\). - - At which point all validations during that turn are available for reward collection. - -5. Any incorrect validations will be marked during the turn in between. - -### Archiver behavior - -1. Since an archiver is somewhat of a light client and not downloading all the - - ledger data, they have to rely on other validators and archivers for information. - - Any given validator may or may not be malicious and give incorrect information, although - - there are not any obvious attack vectors that this could accomplish besides having the - - archiver do extra wasted work. For many of the operations there are a number of options - - depending on how paranoid an archiver is: - - - \(a\) archiver can ask a validator - - \(b\) archiver can ask multiple validators - - \(c\) archiver can ask other archivers - - \(d\) archiver can subscribe to the full transaction stream and generate - - the information itself \(assuming the slot is recent enough\) - - - \(e\) archiver can subscribe to an abbreviated transaction stream to - - generate the information itself \(assuming the slot is recent enough\) - -2. An archiver obtains the PoH hash corresponding to the last turn with its slot. -3. The archiver signs the PoH hash with its keypair. That signature is the - - seed used to pick the segment to replicate and also the encryption key. The - - archiver mods the signature with the slot to get which segment to - - replicate. - -4. The archiver retrieves the ledger by asking peer validators and - - archivers. See 6.5. - -5. The archiver then encrypts that segment with the key with chacha algorithm - - in CBC mode with `NUM_CHACHA_ROUNDS` of encryption. - -6. The archiver initializes a chacha rng with the signed recent PoH value as - - the seed. - -7. The archiver generates `NUM_STORAGE_SAMPLES` samples in the range of the - - entry size and samples the encrypted segment with sha256 for 32-bytes at each - - offset value. Sampling the state should be faster than generating the encrypted - - segment. - -8. The archiver sends a PoRep proof transaction which contains its sha state - - at the end of the sampling operation, its seed and the samples it used to the - - current leader and it is put onto the ledger. - -9. During a given turn the archiver should submit many proofs for the same segment - - and based on the `RATIO_OF_FAKE_PROOFS` some of those proofs must be fake. - -10. As the PoRep game enters the next turn, the archiver must submit a - - transaction with the mask of which proofs were fake during the last turn. This - - transaction will define the rewards for both archivers and validators. - -11. Finally for a turn N, as the PoRep game enters turn N + 3, archiver's proofs for - - turn N will be counted towards their rewards. - -### The PoRep Game - -The Proof of Replication game has 4 primary stages. For each "turn" multiple PoRep games can be in progress but each in a different stage. - -The 4 stages of the PoRep Game are as follows: - -1. Proof submission stage - - Archivers: submit as many proofs as possible during this stage - - Validators: No-op -2. Proof verification stage - - Archivers: No-op - - Validators: Select archivers and verify their proofs from the previous turn -3. Proof challenge stage - - Archivers: Submit the proof mask with justifications \(for fake proofs submitted 2 turns ago\) - - Validators: No-op -4. Reward collection stage - - Archivers: Collect rewards for 3 turns ago - - Validators: Collect rewards for 3 turns ago - -For each turn of the PoRep game, both Validators and Archivers evaluate each stage. The stages are run as separate transactions on the storage program. - -### Finding who has a given block of ledger - -1. Validators monitor the turns in the PoRep game and look at the rooted bank - - at turn boundaries for any proofs. - -2. Validators maintain a map of ledger segments and corresponding archiver public keys. - - The map is updated when a Validator processes an archiver's proofs for a segment. - - The validator provides an RPC interface to access this map. Using this API, clients - - can map a segment to an archiver's network address \(correlating it via cluster_info table\). - - The clients can then send repair requests to the archiver to retrieve segments. - -3. Validators would need to invalidate this list every N turns. - -## Sybil attacks - -For any random seed, we force everyone to use a signature that is derived from a PoH hash at the turn boundary. Everyone uses the same count, so the same PoH hash is signed by every participant. The signatures are then each cryptographically tied to the keypair, which prevents a leader from grinding on the resulting value for more than 1 identity. - -Since there are many more client identities than encryption identities, we need to split the reward for multiple clients, and prevent Sybil attacks from generating many clients to acquire the same block of data. To remain BFT we want to avoid a single human entity from storing all the replications of a single chunk of the ledger. - -Our solution to this is to force the clients to continue using the same identity. If the first round is used to acquire the same block for many client identities, the second round for the same client identities will force a redistribution of the signatures, and therefore PoRep identities and blocks. Thus to get a reward for archivers need to store the first block for free and the network can reward long lived client identities more than new ones. - -## Validator attacks - -- If a validator approves fake proofs, archiver can easily out them by - - showing the initial state for the hash. - -- If a validator marks real proofs as fake, no on-chain computation can be done - - to distinguish who is correct. Rewards would have to rely on the results from - - multiple validators to catch bad actors and archivers from being denied rewards. - -- Validator stealing mining proof results for itself. The proofs are derived - - from a signature from an archiver, since the validator does not know the - - private key used to generate the encryption key, it cannot be the generator of - - the proof. - -## Reward incentives - -Fake proofs are easy to generate but difficult to verify. For this reason, PoRep proof transactions generated by archivers may require a higher fee than a normal transaction to represent the computational cost required by validators. - -Some percentage of fake proofs are also necessary to receive a reward from storage mining. - -## Notes - -- We can reduce the costs of verification of PoRep by using PoH, and actually - - make it feasible to verify a large number of proofs for a global dataset. - -- We can eliminate grinding by forcing everyone to sign the same PoH hash and - - use the signatures as the seed - -- The game between validators and archivers is over random blocks and random - - encryption identities and random data samples. The goal of randomization is - - to prevent colluding groups from having overlap on data or validation. - -- Archiver clients fish for lazy validators by submitting fake proofs that - - they can prove are fake. - -- To defend against Sybil client identities that try to store the same block we - - force the clients to store for multiple rounds before receiving a reward. - -- Validators should also get rewarded for validating submitted storage proofs - - as incentive for storing the ledger. They can only validate proofs if they - - are storing that slice of the ledger. - -# Ledger Replication Not Implemented - -Replication behavior yet to be implemented. - -## Storage epoch - -The storage epoch should be the number of slots which results in around 100GB-1TB of ledger to be generated for archivers to store. Archivers will start storing ledger when a given fork has a high probability of not being rolled back. - -## Validator behavior - -1. Every NUM_KEY_ROTATION_TICKS it also validates samples received from - - archivers. It signs the PoH hash at that point and uses the following - - algorithm with the signature as the input: - - - The low 5 bits of the first byte of the signature creates an index into - - another starting byte of the signature. - - - The validator then looks at the set of storage proofs where the byte of - - the proof's sha state vector starting from the low byte matches exactly - - with the chosen byte\(s\) of the signature. - - - If the set of proofs is larger than the validator can handle, then it - - increases to matching 2 bytes in the signature. - - - Validator continues to increase the number of matching bytes until a - - workable set is found. - - - It then creates a mask of valid proofs and fake proofs and sends it to - - the leader. This is a storage proof confirmation transaction. - -2. After a lockout period of NUM_SECONDS_STORAGE_LOCKOUT seconds, the - - validator then submits a storage proof claim transaction which then causes the - - distribution of the storage reward if no challenges were seen for the proof to - - the validators and archivers party to the proofs. - -## Archiver behavior - -1. The archiver then generates another set of offsets which it submits a fake - - proof with an incorrect sha state. It can be proven to be fake by providing the - - seed for the hash result. - - - A fake proof should consist of an archiver hash of a signature of a PoH - - value. That way when the archiver reveals the fake proof, it can be - - verified on chain. - -2. The archiver monitors the ledger, if it sees a fake proof integrated, it - - creates a challenge transaction and submits it to the current leader. The - - transaction proves the validator incorrectly validated a fake storage proof. - - The archiver is rewarded and the validator's staking balance is slashed or - - frozen. - -## Storage proof contract logic - -Each archiver and validator will have their own storage account. The validator's account would be separate from their gossip id similar to their vote account. These should be implemented as two programs one which handles the validator as the keysigner and one for the archiver. In that way when the programs reference other accounts, they can check the program id to ensure it is a validator or archiver account they are referencing. - -### SubmitMiningProof - -```text -SubmitMiningProof { - slot: u64, - sha_state: Hash, - signature: Signature, -}; -keys = [archiver_keypair] -``` - -Archivers create these after mining their stored ledger data for a certain hash value. The slot is the end slot of the segment of ledger they are storing, the sha_state the result of the archiver using the hash function to sample their encrypted ledger segment. The signature is the signature that was created when they signed a PoH value for the current storage epoch. The list of proofs from the current storage epoch should be saved in the account state, and then transferred to a list of proofs for the previous epoch when the epoch passes. In a given storage epoch a given archiver should only submit proofs for one segment. - -The program should have a list of slots which are valid storage mining slots. This list should be maintained by keeping track of slots which are rooted slots in which a significant portion of the network has voted on with a high lockout value, maybe 32-votes old. Every SLOTS_PER_SEGMENT number of slots would be added to this set. The program should check that the slot is in this set. The set can be maintained by receiving a AdvertiseStorageRecentBlockHash and checking with its bank/Tower BFT state. - -The program should do a signature verify check on the signature, public key from the transaction submitter and the message of the previous storage epoch PoH value. - -### ProofValidation - -```text -ProofValidation { - proof_mask: Vec, -} -keys = [validator_keypair, archiver_keypair(s) (unsigned)] -``` - -A validator will submit this transaction to indicate that a set of proofs for a given segment are valid/not-valid or skipped where the validator did not look at it. The keypairs for the archivers that it looked at should be referenced in the keys so the program logic can go to those accounts and see that the proofs are generated in the previous epoch. The sampling of the storage proofs should be verified ensuring that the correct proofs are skipped by the validator according to the logic outlined in the validator behavior of sampling. - -The included archiver keys will indicate the storage samples which are being referenced; the length of the proof_mask should be verified against the set of storage proofs in the referenced archiver account\(s\), and should match with the number of proofs submitted in the previous storage epoch in the state of said archiver account. - -### ClaimStorageReward - -```text -ClaimStorageReward { -} -keys = [validator_keypair or archiver_keypair, validator/archiver_keypairs (unsigned)] -``` - -Archivers and validators will use this transaction to get paid tokens from a program state where SubmitStorageProof, ProofValidation and ChallengeProofValidations are in a state where proofs have been submitted and validated and there are no ChallengeProofValidations referencing those proofs. For a validator, it should reference the archiver keypairs to which it has validated proofs in the relevant epoch. And for an archiver it should reference validator keypairs for which it has validated and wants to be rewarded. - -### ChallengeProofValidation - -```text -ChallengeProofValidation { - proof_index: u64, - hash_seed_value: Vec, -} -keys = [archiver_keypair, validator_keypair] -``` - -This transaction is for catching lazy validators who are not doing the work to validate proofs. An archiver will submit this transaction when it sees a validator has approved a fake SubmitMiningProof transaction. Since the archiver is a light client not looking at the full chain, it will have to ask a validator or some set of validators for this information maybe via RPC call to obtain all ProofValidations for a certain segment in the previous storage epoch. The program will look in the validator account state see that a ProofValidation is submitted in the previous storage epoch and hash the hash_seed_value and see that the hash matches the SubmitMiningProof transaction and that the validator marked it as valid. If so, then it will save the challenge to the list of challenges that it has in its state. - -### AdvertiseStorageRecentBlockhash - -```text -AdvertiseStorageRecentBlockhash { - hash: Hash, - slot: u64, -} -``` - -Validators and archivers will submit this to indicate that a new storage epoch has passed and that the storage proofs which are current proofs should now be for the previous epoch. Other transactions should check to see that the epoch that they are referencing is accurate according to current chain state. diff --git a/docs/src/proposals/log_data.md b/docs/src/proposals/log_data.md deleted file mode 100644 index 2744a6c5673..00000000000 --- a/docs/src/proposals/log_data.md +++ /dev/null @@ -1,146 +0,0 @@ -# Program log binary data - -## Problem - -There is no support for logging binary data in Solidity. - -### Events in Solidity - -In Solidity, events can be reported. These look like structures with zero or -more fields, and can be emitted with specific values. For example: - -``` -event PaymentReceived { - address sender; - uint amount; -} - -contract c { - function pay() public payable { - emit PaymentReceived(msg.sender, msg.value); - } -} -``` - -Events are write-only from a Solidity/VM perspective and are written to -the blocks in the tx records. - -Some of these fields can be marked `indexed`, which affects how the data is -encoded. All non-indexed fields are eth abi encoded into a variable length -byte array. All indexed fields go into so-called topics. - -Topics are fixed length fields of 32 bytes. There are a maximum of 4 topics; -if a type does not always fit into 32 bytes (e.g. string types), then the topic -is keccak256 hashed. - -The first topic is a keccak256 hash of the event signature, in this case -`keccak256('PaymentReceived(address,uint)')`. The four remaining are available -for `indexed` fields. The event may be declared `anonymous`, in which case -the first field is not a hash of the signature, and it is permitted to have -4 indexed fields. - -### Listening to events in a client - -The reason for the distinction between topics/indexed and regular fields is -that it easier to filter on topics. - -``` -const Web3 = require('web3'); -const url = 'ws://127.0.0.1:8546'; -const web3 = new Web3(url); - -var options = { - address: '0xfbBE8f06FAda977Ea1E177da391C370EFbEE3D25', - topics: [ - '0xdf50c7bb3b25f812aedef81bc334454040e7b27e27de95a79451d663013b7e17', - //'0x0000000000000000000000000d8a3f5e71560982fb0eb5959ecf84412be6ae3e' - ] -}; - -var subscription = web3.eth.subscribe('logs', options, function(error, result){ - if (!error) console.log('got result'); - else console.log(error); -}).on("data", function(log){ - console.log('got data', log); -}).on("changed", function(log){ - console.log('changed'); -}); -``` - -In order to decode the non-indexed fields (the data), the abi of the contract -is needed. So, the topic is first used to discover what event was used, and -then the data can be decoded. - -### Ethereum Tx in block - -The transaction calls event logs. Here is a tx with a single event, with 3 -topics and some data. - -``` -{ - "tx": { - "nonce": "0x2", - "gasPrice": "0xf224d4a00", - "gas": "0xc350", - "to": "0x6B175474E89094C44Da98b954EedeAC495271d0F", - "value": "0x0", - "input": "0xa9059cbb000000000000000000000000a12431d0b9db640034b0cdfceef9cce161e62be40000000000000000000000000000000000000000000000a030dcebbd2f4c0000", - "hash": "0x98a67f0a35ebc0ac068acf0885d38419c632ffa4354e96641d6d5103a7681910", - "blockNumber": "0xc96431", - "from": "0x82f890D638478d211eF2208f3c1466B5Abf83551", - "transactionIndex": "0xe1" - }, - "receipt": { - "gasUsed": "0x74d2", - "status": "0x1", - "logs": [ - { - "address": "0x6B175474E89094C44Da98b954EedeAC495271d0F", - "topics": [ - "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", - "0x00000000000000000000000082f890d638478d211ef2208f3c1466b5abf83551", - "0x000000000000000000000000a12431d0b9db640034b0cdfceef9cce161e62be4" - ], - "data": "0x0000000000000000000000000000000000000000000000a030dcebbd2f4c0000" - } - ] - } -} -``` - -### Further considerations - -In Ethereum, events are stored in blocks. Events mark certain state changes in -smart contracts. This serves two purposes: - - - Listen to events (i.e. state changes) as they happen by reading new blocks - as they are published - - Re-read historical events by reading old blocks - -So for example, smart contracts may emit changes as they happen but never the -complete state, so the only way to recover the entire state of the contract -is by re-reading all events from the chain. So an application will read events -from block 1 or whatever block the application was deployed at and then use -that state for local processing. This is a local cache and may re-populated -from the chain at any point. - -## Proposed Solution - -Binary logging should be added to the program log. The program log should include the base64 encoded data (zero or more one permitted). - -So if we encoding the topics first, followed by the data then the event in the -tx above would look like: -``` -program data: 3fJSrRviyJtpwrBo/DeNqpUrpFjxKEWKPVaTfUjs8AAAAAAAAAAAAAAACC+JDWOEeNIR7yII88FGa1q/g1UQAAAAAAAAAAAAAAAKEkMdC522QANLDN/O75zOFh5ivk AAAAAAAAAAAAAAAAAAAAAAAAAAAAAACgMNzrvS9MAAA= -``` - -This requires a new system call: - -``` -void sol_log_data(SolBytes *fields, uint64_t length); -``` - -### Considerations - -- Should there be text field in the program log so we can have a little bit of - metadata on the binary data, to make it more human readable diff --git a/docs/src/proposals/off-chain-message-signing.md b/docs/src/proposals/off-chain-message-signing.md deleted file mode 100644 index 9ef92ac6f76..00000000000 --- a/docs/src/proposals/off-chain-message-signing.md +++ /dev/null @@ -1,232 +0,0 @@ -# Off-chain message signing - -There is ecosystem demand for a method of signing non-transaction messages with -a Solana wallet. Typically this is some kind of "proof of wallet ownership" for -entry into a whitelisted system. - -This document specifies **version 1** of the off-chain message format, the -current recommended format. The original **version 0** format remains supported -and is documented in [Appendix: version 0 (legacy)](#appendix-version-0-legacy). - -Some inspiration can be gleaned from relevant portions of Ethereum's -[EIP-712](https://eips.ethereum.org/EIPS/eip-712). - -## Motivation - -* Security - * Off-chain message signatures must not be valid transaction message signatures -* Future-proofing - * Versioning - * Co-exist with versioned transaction messages and extended length transactions -* Compatibility - * Hardware wallet signing - * Determinism - * The same logical message must always produce the same bytes, and therefore - the same signature, regardless of the order in which signers are declared - -### What changed in version 1 - -Version 1 is a deliberate simplification of version 0. It removes three fields -that added complexity or ambiguity without proportional benefit: - -* **Application domain** (32 bytes) — provided limited utility and created a - spoofing surface. Removed. -* **Message format** (1 byte) — whether a given message is signable or - clear-signable by a hardware wallet depends on that wallet's implementation, - not on a byte in the message. Removed; clients may impose their own character - set or length constraints out of band. -* **Message length** (2-byte prefix) — the 16-bit length prefix capped messages - at 65,535 bytes and created a class of length-mismatch validation failures. - Removed; the message content is now a trailing, variable-length field that - consumes the remainder of the buffer. - -Version 1 also adds an ordering and uniqueness constraint on signer addresses so -that identical messages always serialize identically (see -[Required signers](#required-signers)). - -## Message format - -A version 1 off-chain message is the byte string that is ed25519-signed. It is -composed of a preamble followed by the message content. - -| Field | Start offset | Length (bytes) | Description | -| :------------- | :----------------------: | :-------------------: | :--------------------------------------------------------------------------- | -| Signing domain | 0x00 | 16 | \[[1](#signing-domain)\] | -| Version | 0x10 | 1 | An 8-bit unsigned integer. **MUST** equal `1`. \[[2](#version)\] | -| Signer count | 0x11 | 1 | Number of required signers. An 8-bit unsigned integer. **MUST NOT** be zero. | -| Signers | 0x12 | `SIGNER_COUNT` * 32 | `SIGNER_COUNT` ed25519 public keys. \[[3](#required-signers)\] | -| Content | 0x12 + `SIGNER_COUNT`*32 | remainder of buffer | The message content. UTF-8. **MUST NOT** be empty. \[[4](#content)\] | - -### Signing domain - -The signing domain is a fixed 16-byte prefix that gives common structure to all -off-chain message signatures: - -``` -b"\xffsolana offchain" -``` - -In hexadecimal: `ff 73 6f 6c 61 6e 61 20 6f 66 66 63 68 61 69 6e`. - -The first byte, `\xff`, was chosen for the following reasons: - -1. It corresponds to a value that is illegal as the first byte in a transaction - `MessageHeader` (see [Runtime considerations](#runtime-considerations)). -1. It avoids unintentional misuse in languages with C-like, null-terminated - strings. - -The remaining bytes, `b"solana offchain"`, were chosen to be descriptive and -reasonably long, but are otherwise arbitrary. - -This field **SHOULD NOT** be displayed to users. - -### Version - -The version is an 8-bit unsigned integer. For messages conforming to this -document it **MUST** equal `1`. - -This field **SHOULD NOT** be displayed to users. - -### Required signers - -The list of `SIGNER_COUNT` ed25519 public keys that **MUST** sign the message -for it to be valid. The list: - -* **MUST NOT** be empty. -* **MUST NOT** contain duplicates. -* **MUST** be sorted in ascending lexicographical (byte-wise) order. - -The ordering and uniqueness constraints exist so that a given logical message -always serializes to the same bytes — and therefore the same signature — no -matter the order in which an application declares its signers. Signers and -verifiers **MUST** enforce both constraints. - -These addresses **SHOULD** be displayed to users as base58-encoded strings. - -### Content - -The message content occupies the remainder of the buffer after the signers. It -is a UTF-8 encoded string and **MUST NOT** be empty. Unlike version 0, there is -no length prefix and no upper bound imposed by the format itself. - -Hardware-wallet signability and clear-signability are properties of the signing -device, not of the message format. Clients that need to target a specific -hardware wallet **MAY** impose their own additional restrictions — for example -limiting content to printable ASCII (`0x20..=0x7e`) or to a maximum byte length -— but the format does not require or encode these choices. - -## Signing - -Solana off-chain messages **MUST** only be signed using the ed25519 digital -signature scheme. Before signing, the message **MUST** be strictly checked to -conform to this specification, including the signer ordering and uniqueness -constraints. The full message byte string (signing domain, version, signers, and -content) is then ed25519-signed by each required signer. - -## Verification - -A message is valid only if, for **every** required signer in the preamble, a -corresponding ed25519 signature is present and verifies against the full message -byte string. The message **MUST** also be strictly checked to conform to this -specification — a message that does not conform is invalid regardless of the -validity of any signatures. - -## Envelope - -When passing around signed off-chain messages a common format is helpful. The -recommended binary representation is as follows: - -| Field | Start offset | Length (bytes) | Description | -| :-------------- | :-----------------------: | :---------------: | :----------------------------------------------------------------------- | -| Signature count | 0x00 | 1 | Number of signatures. An 8-bit unsigned integer. **MUST NOT** be zero. | -| Signatures | 0x01 | `SIG_COUNT` * 64 | `SIG_COUNT` ed25519 signatures. | -| Message | 0x01 + `SIG_COUNT` * 64 | remainder | The signed [message](#message-format) (preamble and content). | - -* The signature count **MUST** equal the signer count from the message preamble. -* Signatures **MUST** be ordered to match their corresponding public keys as they - appear in the (sorted) signers list. -* A missing signature in a partially-signed envelope is represented by 64 zero - bytes. - -## Runtime considerations - -To prevent social attacks by which the signer is tricked into signing a -transaction, the runtime **MUST NOT** accept signed off-chain messages as -transactions under any circumstances. The first byte of the -[signing domain](#signing-domain) is chosen such that it corresponds to a value -(`0xff`) which is implicitly illegal as the first byte in a transaction -`MessageHeader` today. The property is implicit because the top bit in the first -byte of a `MessageHeader` being set signals a versioned transaction, but only a -value of -[zero is supported](https://github.com/solana-labs/solana/blob/b6ae6c1fe17e4b64c5051c651ca2585e4f55468c/sdk/program/src/message/versions/mod.rs#L269-L281) -at this time. The runtime reserves `127` as an illegal version number, making -this property explicit. - -## Reference - -* Specification discussion: - [SRFC #3](https://github.com/solana-foundation/SRFCs/discussions/3) -* Offchain messages SDK: - [`@solana/offchain-messages`](https://github.com/anza-xyz/kit/tree/main/packages/offchain-messages) - (exported via `@solana/kit`) -* The runtime check that reserves transaction version `127` (rejecting - `0xff`-prefixed bytes as transactions) was implemented in PR - [#29807](https://github.com/solana-labs/solana/pull/29807) - ---- - -## Appendix: version 0 (legacy) - -Version 0 is the original off-chain message format. It remains supported for -backwards compatibility, but new applications **SHOULD** prefer -[version 1](#message-format). The differences are summarized in -[What changed in version 1](#what-changed-in-version-1). - -### Message preamble (v0) - -| Field | Start offset | Length (bytes) | Description | -| :----------------- | :----------------------: | :------------------: | :------------------------------------------------------------------------------------------------ | -| Signing Domain | 0x00 | 16 | Same as [v1](#signing-domain). | -| Header version | 0x10 | 1 | An 8-bit unsigned integer. `0` for this format. | -| Application domain | 0x11 | 32 | A 32-byte application identifier (e.g. a program address, DAO, etc.). \[[1](#application-domain-v0)\] | -| Message format | 0x31 | 1 | \[[2](#message-format-v0)\] | -| Signer count | 0x32 | 1 | Number of signers. An 8-bit unsigned integer. **MUST NOT** be zero. | -| Signers | 0x33 | `SIGNER_COUNT` * 32 | `SIGNER_COUNT` ed25519 public keys of signers. | -| Message length | 0x33 + `SIGNER_CNT` * 32 | 2 | Length of message in bytes. A 16-bit, unsigned, little-endian integer. **MUST NOT** be zero. | - -#### Application domain (v0) - -A 32-byte array identifying the application requesting off-chain message signing. -This may be any arbitrary bytes. This field **SHOULD** be displayed to users as a -base58-encoded ASCII string rather than interpreted otherwise. - -#### Message format (v0) - -Version `0` headers specify three message formats allowing for trade-offs between -compatibility and composition of messages. - -| ID | Encoding | Maximum Length \* | Hardware Wallet Support | -| :-: | :-------------------: | :---------------: | :---------------------: | -| 0 | Restricted ASCII \*\* | 1232 | Yes | -| 1 | UTF-8 | 1232 | Blind sign only | -| 2 | UTF-8 | 65535 | No | - -\* Combined length of the message preamble and message body
-\*\* Those characters for which [`isprint(3)`](https://linux.die.net/man/3/isprint) -returns true. That is, `0x20..=0x7e`. - -Both the message encoding and maximum message length **MUST** be enforced by -signer and verifier. Formats `0` and `1` are motivated by hardware wallet support -where both RAM to store the payload and font character support are limited. - -### Envelope (v0) - -| Field | Start offset | Length (bytes) | Description | -| :---------------- | :-----------------------------------: | :---------------: | :--------------------------------------------------- | -| Signature Count | 0x00 | 1 | Number of signatures. An 8-bit, unsigned integer. | -| Signatures | 0x01 | `SIG_COUNT` * 64 | `SIG_COUNT` ed25519 signatures. | -| Message Preamble | 0x01 + `SIG_COUNT` * 64 | `PREAMBLE_LEN` | The message preamble. | -| Message Body | 0x01 + `SIG_COUNT` * 64 + `PREAMBLE_LEN` | `MESSAGE_LEN` | The message content. | - -The signature count **MUST** match the signer count from the message preamble, -and signatures **MUST** be ordered to match their corresponding public keys. diff --git a/docs/src/proposals/optimistic-confirmation-and-slashing.md b/docs/src/proposals/optimistic-confirmation-and-slashing.md deleted file mode 100644 index 0c6de6eb57b..00000000000 --- a/docs/src/proposals/optimistic-confirmation-and-slashing.md +++ /dev/null @@ -1,89 +0,0 @@ ---- -title: Optimistic Confirmation and Slashing ---- - -Progress on optimistic confirmation can be tracked here - -https://github.com/solana-labs/solana/projects/52 - -At the end of May, the mainnet-beta is moving to 1.1, and testnet is -moving to 1.2. With 1.2, testnet will behave as if it has optimistic -finality as long as at least no more than 4.66% of the validators are -acting maliciously. Applications can assume that 2/3+ votes observed in -gossip confirm a block or that at least 4.66% of the network is violating -the protocol. - -## How does it work? - -The general idea is that validators must continue voting following their -last fork, unless the validator can construct a proof that their current -fork may not reach finality. The way validators construct this proof is -by collecting votes for all the forks excluding their own. If the set -of valid votes represents over 1/3+X of the epoch stake weight, there -may not be a way for the validators current fork to reach 2/3+ finality. -The validator hashes the proof (creates a witness) and submits it with -their vote for the alternative fork. But if 2/3+ votes for the same -block, it is impossible for any of the validators to construct this proof, -and therefore no validator is able to switch forks and this block will -be eventually finalized. - -## Tradeoffs - -The safety margin is 1/3+X, where X represents the minimum amount of stake -that will be slashed in case the protocol is violated. The tradeoff is -that liveness is now reduced by 2X in the worst case. If more than 1/3 - -2X of the network is unavailable, the network may stall and will only -resume finalizing blocks after the network recovers below 1/3 - 2X of -failing nodes. So far, we haven’t observed a large unavailability hit -on our mainnet, cosmos, or tezos. For our network, which is primarily -composed of high availability systems, this seems unlikely. Currently, -we have set the threshold percentage to 4.66%, which means that if 23.68% -have failed the network may stop finalizing blocks. For our network, -which is primarily composed of high availability systems, a 23.68% drop -in availability seems unlikely. 1:10^12 odds, assuming five 4.7% staked -nodes with 0.995 uptime. - -## Security - -Long term average votes per slot has been 670,000,000 votes / 12,000,000 -slots, or 55 out of 64 voting validators. This includes missed blocks due -to block producer failures. When a client sees 55/64, or ~86% confirming -a block, it can expect that ~24% or `(86 - 66.666.. + 4.666..)%` of -the network must be slashed for this block to fail full finalization. - -## Why Solana? - -This approach can be built on other networks, but the implementation -complexity is significantly reduced on Solana because our votes -have provable VDF-based timeouts. It’s not clear if switching proofs -can be easily constructed in networks with weak assumptions about -time. - -## Slashing roadmap - -Slashing is a hard problem, and it becomes harder when the goal of -the network is to have the lowest possible latency. The tradeoffs are -especially apparent when optimizing for latency. For example, ideally -validators should cast and propagate their votes before the -memory has been synced to disk, which means that the risk of local state -corruption is much higher. - -Fundamentally, our goal for slashing is to slash 100% in cases where -the node is maliciously trying to violate safety rules and 0% during -routine operation. How we aim to achieve that is to first implement -slashing proofs without any automatic slashing whatsoever. - -Right now, for regular consensus, after a safety violation, the -network will halt. We can analyze the data and figure out who was -responsible and propose that the stake should be slashed after -restart. A similar approach will be used with a optimistic conf. -An optimistic conf safety violation is easily observable, but under -normal circumstances, an optimistic confirmation safety violation -may not halt the network. Once the violation has been observed, the -validators will freeze the affected stake in the next epoch and -will decide on the next upgrade if the violation requires slashing. - -In the long term, transactions should be able to recover a portion -of the slashing collateral if the optimistic safety violation is -proven. In that scenario, each block is effectively insured by the -network. diff --git a/docs/src/proposals/optimistic-transaction-propagation-signal.md b/docs/src/proposals/optimistic-transaction-propagation-signal.md deleted file mode 100644 index b6c18d2d0ce..00000000000 --- a/docs/src/proposals/optimistic-transaction-propagation-signal.md +++ /dev/null @@ -1,109 +0,0 @@ ---- -title: Optimistic Transaction Propagation Signal ---- - -## Current Retransmit behavior - -The retransmission tree currently considers: -1. epoch staked nodes -2. tvu peers (filtered by contact info and shred version) -3. current validator - -concatenating (1), (2), and (3) -deduplicating this list of entries by pubkey favoring entries with contact info -filtering this list by entries with contact info - -This list is then randomly shuffled by stake weight. - -Shreds are then retransmitted to up to FANOUT neighbors and up to FANOUT -children. - -## Deterministic retransmission tree - -`weighted_shuffle` will use a deterministic seed when -`enable_deterministic_seed` has been enabled based on the triple (shred slot, -shred index, leader pubkey): - -``` -if enable_deterministic_seed(self.slot(), root_bank) { - hashv(&[ - &self.slot().to_le_bytes(), - &self.index().to_le_bytes(), - &leader_pubkey.to_bytes(), - ]) -``` - -First, only epoch staked nodes will be considered regardless of presence of -contact info (and possibly including the validator node itself). - -A deterministic ordering of the epoch staked nodes will be created based on the -deterministic shred seed using weighted_shuffle. - -Let `neighbor_set` be selected from up to FANOUT neighbors of the current node. -Let `child_set` be selected from up to FANOUT children of the current node. - -Filter `neighbor_set` by contact info. -Filter `child_set` by contact info. - -Let `epoch_set` be the union of `neighbor_set` and `child_set`. - -Let `remaining_set` be all other nodes with contact info not contained in -`epoch_set`. - -If `epoch_set.len < 2*FANOUT` then we may randomly select up to -`2*FANOUT - epoch_set.len` nodes to retransmit to from `remaining_set`. - -## Receiving retransmitted shred - -If the current validator node is not in the set of epoch staked nodes for the -shred epoch then no early retransmission information can be obtained. - -Compute the deterministic shred seed. - -Run the deterministic epoch_stakes shuffle. - -Find position of self in the neighbor or child sets. - -Calculate the sum of the stakes of all nodes in the current and prior -distribution levels. - -### Stake summation considerations: - -- Stake sum could include stakes of nodes which had been skipped in prior -distribution levels because of lack of contact info. -- Current node was part of original epoch staked shuffle from retransmitter -but was filtered out because of missing contact info. Current node subsequently -receives retransmission of shred and assumes that the retransmit was a result -of the deterministic tree calculation and not from subsequent random selection. -This should be benign because the current node will underestimate prior stake -weight in the retransmission tree. - -### General considerations: - -attack by leader (level 0): -- transmits shred for distribution through the tree as normal -- additionally transmits shred (or fake shred) directly to node(s) at level >=2 -leading the node(s) to believe a greater percentage of the tree retransmission -tree had been processed - -attack by node at level n: -- retransmits shred to node(s) at level >=n+2 leading the node(s) to believe a -greater percentage of the tree retransmission tree had been processed - -### Questions - -- Should receiving nodes attempt to verify that the origin of the shred was -retransmitted from the expected node? If so, consideration of spoofing? -- How is this information consumed? - -## Notes - -Practically, signals should fall into the following buckets: -1. current leader (can signal layer 1 when broadcast is sent) -2. layer 1 -1.1. can signal layer 1 when shred is received -1.2. can signal layer 1 + subset of layer 2 when retransmit is sent -3. layer 2 -3.1. can signal layer 2 when shred is received -3.2. can signal layer 2 + subset of layer 3 when retransmit is sent -4. current node not a member of epoch staked nodes, no signal can be sent diff --git a/docs/src/proposals/optimistic_confirmation.md b/docs/src/proposals/optimistic_confirmation.md deleted file mode 100644 index 3cb553cdcb9..00000000000 --- a/docs/src/proposals/optimistic_confirmation.md +++ /dev/null @@ -1,408 +0,0 @@ ---- -title: Optimistic Confirmation ---- - -## Primitives - -`vote(X, S)` - Votes will be augmented with a "reference" slot, `X` -which is the **latest** ancestor of this fork that this validator voted on -with a proof of switching. As long as the validator makes consecutive votes -that are all descended from each other, the same `X` should be used for all -those votes. When the validator makes a vote for a slot `s` that is not -descended from the previous, `X` will be set to the new slot `s`. All votes -will then be of the form `vote(X, S)`, where `S` is the sorted list of slots -`(s, s.lockout)` being voted for. - -Given a vote `vote(X, S)`, let `S.last == vote.last` be the last slot in `S`. - -Now we define some "Optimistic Slashing" slashing conditions. The intuition -for these is described below: - -- `Intuition`: If a validator submits `vote(X, S)`, the same validator - should not have voted on a different fork that "overlaps" this fork. - More concretely, this validator should not have cast another vote - `vote(X', S')` where the range `[X, S.last]` overlaps the range - `[X', S'.last]`, `X != X'`, as shown below: - -```text - +-------+ - | | - +---------+ +--------+ - | | | | - | +-------+ | - | | - | | - | | - +---+---+ | - | | | - X | | | - | | | - +---+---+ | - | | - | +---+---+ - | | | - | | | X' - | | | - | +---+---+ - | | - | | - | | - | | - | +---+---+ - | | | - | | | S'.last - | | | - | +-------+ - | - +---+---+ - | | - s | | - | | - +---+---+ - | - | - | - | - +---+---+ - | | - S.last | | - | | - +-------+ -``` - -(Example of slashable votes vote(X', S') and vote(X, S)) - -In the diagram above, note that the vote for `S.last` must have been sent after -the vote for `S'.last` (due to lockouts, the higher vote must have been sent -later). Thus, the sequence of votes must have been: `X ... S'.last ... S.last`. -This means after the vote on `S'.last`, the validator must have switched back -to the other fork at some slot `s > S'.last > X`. Thus, the vote for `S.last` -should have used `s` as the "reference" point, not `X`, because that was the -last "switch" on the fork. - -To enforce this, we define the "Optimistic Slashing" slashing conditions. Given -any two distinct votes `vote(X, S)`and `vote(X', S')` by the same validator, -the votes must satisfy: - -- `X <= S.last`, `X' <= S'.last` -- All `s` in `S` are ancestors/descendants of one another, - all `s'` in `S'` are ancestors/descendants of one another, -- -- `X == X'` implies `S` is parent of `S'` or `S'` is a parent of `S` -- `X' > X` implies `X' > S.last` and `S'.last > S.last` - and for all `s` in `S`, `s + lockout(s) < X'` -- `X > X'` implies `X > S'.last` and `S.last > S'.last` - and for all `s` in `S'`, `s + lockout(s) < X` - -(The last two rules imply the ranges cannot overlap): -Otherwise the validator is slashed. - -`Range(vote)` - Given a vote `v = vote(X, S)`, define `Range(v)` to be the range -of slots `[X, S.last]`. - -`SP(old_vote, new_vote)` - This is the "Switching Proof" for `old_vote`, the -validator's latest vote. Such a proof is necessary anytime a validator switches -their "reference" slot (see vote section above). The switching proof includes -a reference to `old_vote`, so that there's a record of what the "range" of that -`old_vote` was (to make other conflicting switches in this range slashable). -Such a switch must still respect lockouts. - -A switching proof shows that `> 1/3` of the network is locked out at slot -`old_vote.last`. - -The proof is a list of elements `(validator_id, validator_vote(X, S))`, where: - -1. The sum of the stakes of all the validator id's `> 1/3` - -2. For each `(validator_id, validator_vote(X, S))`, there exists some slot `s` - in `S` where: - _ a.`s` is not a common ancestor of both `validator_vote.last` and - `old_vote.last` and `new_vote.last`. - _ b. `s` is not a descendant of `validator_vote.last`. \* c. `s + s.lockout() >= old_vote.last` (implies validator is still locked - out on slot `s` at slot `old_vote.last`). - -Switching forks without a valid switching proof is slashable. - -## Definitions: - -Optimistic Confirmation - A block `B` is then said to have achieved -"optimistic confirmation" if `>2/3` of stake have voted with votes `v` -where `Range(v)` for each such `v` includes `B.slot`. - -Finalized - A block `B` is said to be finalized if at least one -correct validator has rooted `B` or a descendant of `B`. - -Reverted - A block `B` is said to be reverted if another block `B'` that -is not a parent or descendant of `B` was finalized. - -## Guarantees: - -A block `B` that has reached optimistic confirmation will not be reverted -unless at least one validator is slashed. - -## Proof: - -Assume for the sake of contradiction, a block `B` has achieved -`optimistic confirmation` at some slot `B + n` for some `n`, and: - -- Another block `B'` that is not a parent or descendant of `B` - was finalized. -- No validators violated any slashing conditions. - -By the definition of `optimistic confirmation`, this means `> 2/3` of validators -have each shown some vote `v` of the form `Vote(X, S)` where `X <= B <= v.last`. -Call this set of validators the `Optimistic Validators`. - -Now given a validator `v` in `Optimistic Validators`, given two votes made by -`v`, `Vote(X, S)` and `Vote(X', S')` where `X <= B <= S.last`, and -`X' <= B <= S'.last`, then `X == X'` otherwise an "Optimistic Slashing" condition -is violated (the "ranges" of each vote would overlap at `B`). - -Thus define the `Optimistic Votes` to be the set of votes made by -`Optimistic Validators`, where for each optimistic validator `v`, the vote made -by `v` included in the set is the `maximal` vote `Vote(X, S)` with the -greatest `S.last` out of any votes made by `v` that satisfy `X <= B <= S.last`. -Because we know from above `X` for all such votes made by `v` is unique, we know -there is such a unique `maximal` vote. - -### Lemma 1: - -`Claim:` Given a vote `Vote(X, S)` made by a validator `V` in the -`Optimistic Validators` set, and `S` contains a vote for a slot `s` -for which: - -- `s + s.lockout > B`, -- `s` is not an ancestor or descendant of `B`, - -then `X > B`. - -```text - +-------+ - | | - +---------+ +--------+ - | | | | - | +-------+ | - | | - | | - | | - | +---+---+ - | | | - | | | X' - | | | - | +---+---+ - | | - | | - | +---+---+ - | | | - | | | B (Optimistically Confirmed) - | | | - | +---+---+ - | | - | | - | | - | +---+---+ - | | | - | | | S'.last - | | | - | +-------+ - | - +---+---+ - | | - X | | - | | - +---+---+ - | - | - | - | - | - | - +---+---+ - | | - S.last | | - | | - +---+---+ - | - | - | - | - +---+---+ - | | - s + s.lockout | | - +-------+ -``` - -`Proof`: Assume for the sake of contradiction a validator `V` from the -"Optimistic Validators" set made such a vote `Vote(X, S)` where `S` contains -a vote for a slot `s` not an ancestor or descendant of `B`, where -`s + s.lockout > B`, but `X <= B`. - -Let `Vote(X', S')` be the vote in `Optimistic Votes` set made by validator `V`. -By definition of that set (all votes optimistically confirmed `B`), -`X' <= B <= S'.last` (see diagram above). - -This implies that because it's assumed above `X <= B`, then `X <= S'.last`, -so by the slashing rules, either `X == X'` or `X < X'` (otherwise would -overlap the range `(X', S'.last)`). - -`Case X == X'`: - -Consider `s`. We know `s != X` because it is assumed `s` is not an ancestor -or descendant of `B`, and `X` is an ancestor of `B`. Because `S'.last` is a -descendant of `B`, this means `s` is also not an ancestor or descendant of -`S'.last`. Then because `S.last` is descended from `s`, then `S'.last` cannot -be an ancestor or descendant of `S.last` either. This implies `X != X'` by the -"Optimistic Slashing" rules. - -`Case X < X'`: - -Intuitively, this implies that `Vote(X, S)` was made "before" `Vote(X', S')`. - -From the assumption above, `s + s.lockout > B > X'`. Because `s` is not an -ancestor of `X'`, lockouts would have been violated when this validator -first attempted to submit a switching vote to `X'` with some vote of the -form `Vote(X', S'')`. - -Since none of these cases are valid, the assumption must have been invalid, -and the claim is proven. - -### Lemma 2: - -Recall `B'` was the block finalized on a different fork than -"optimistically" confirmed" block `B`. - -`Claim`: For any vote `Vote(X, S)` in the `Optimistic Votes` set, it must be -true that `B' > X` - -```text - +-------+ - | | - +--------+ +---------+ - | | | | - | +-------+ | - | | - | | - | | - | +---+---+ - | | | - | | | X - | | | - | +---+---+ - | | - | | - | +---+---+ - | | | - | | | B (Optimistically Confirmed) - | | | - | +---+---+ - | | - | | - | | - | +---+---+ - | | | - | | | S.last - | | | - | +-------+ - | - +---+---+ - | | - B'(Finalized) | | - | | - +-------+ -``` - -`Proof`: Let `Vote(X, S)` be a vote in the `Optimistic Votes` set. Then by -definition, given the "optimistically confirmed" block `B`, `X <= B <= S.last`. - -Because `X` is a parent of `B`, and `B'` is not a parent or ancestor of `B`, -then: - -- `B' != X` -- `B'` is not a parent of `X` - -Now consider if `B'` < `X`: - -`Case B' < X`: We will show this is a violation of lockouts. -From above, we know `B'` is not a parent of `X`. Then because `B'` was rooted, -and `B'` is not a parent of `X`, then the validator should not have been able -to vote on the higher slot `X` that does not descend from `B'`. - -### Proof of Safety: - -We now aim to show at least one of the validators in the -`Optimistic Validators` set violated a slashing rule. - -First note that in order for `B'` to have been rooted, there must have been -`> 2/3` stake that voted on `B'` or a descendant of `B'`. Given that the -`Optimistic Validator` set also contains `> 2/3` of the staked validators, -it follows that `> 1/3` of the staked validators: - -- Rooted `B'` or a descendant of `B'` -- Also submitted a vote `v` of the form `Vote(X, S)` where `X <= B <= v.last`. - -Let the `Delinquent` set be the set of validators that meet the above -criteria. - -By definition, in order to root `B'`, each validator `V` in `Delinquent` -must have each made some "switching vote" of the form `Vote(X_v, S_v)` where: - -- `S_v.last > B'` -- `S_v.last` is a descendant of `B'`, so it can't be a descendant of `B` -- Because `S_v.last` is not a descendant of `B`, then `X_v` cannot be a - descendant or ancestor of `B`. - -By definition, this delinquent validator `V` also made some vote `Vote(X, S)` -in the `Optimistic Votes` where by definition of that set (optimistically -confirmed `B`), we know `S.last >= B >= X`. - -By `Lemma 2` we know `B' > X`, and from above `S_v.last > B'`, so then -`S_v.last > X`. Because `X_v != X` (cannot be a descendant or ancestor of -`B` from above), then by the slashing rules then, we know `X_v > S.last`. -From above, `S.last >= B >= X` so for all such "switching votes", `X_v > B`. - -Now ordering all these "switching votes" in time, let `V` to be the validator -in `Optimistic Validators` that first submitted such a "switching vote" -`Vote(X', S')`, where `X' > B`. We know that such a validator exists because -we know from above that all delinquent validators must have submitted such -a vote, and the delinquent validators are a subset of the -`Optimistic Validators`. - -Let `Vote(X, S)` be the unique vote in `Optimistic Votes` made by -validator `V` (maximizing `S.last`). - -Given `Vote(X, S)` because `X' > B >= X`, then `X' > X`, so -by the "Optimistic Slashing" rules, `X' > S.last`. - -In order to perform such a "switching vote" to `X'`, a switching proof -`SP(Vote(X, S), Vote(X', S'))` must show `> 1/3` of stake being locked -out at this validator's latest vote, `S.last`. Combine this `>1/3` with the -fact that the set of validators in the `Optimistic Voters` set consists of -`> 2/3` of the stake, implies at least one optimistic validator `W` from the -`Optimistic Voters` set must have submitted a vote (recall the definition of -a switching proof),`Vote(X_w, S_w)` that was included in validator `V`'s -switching proof for slot `X'`, where `S_w` contains a slot `s` such that: - -- `s` is not a common ancestor of `S.last` and `X'` -- `s` is not a descendant of `S.last`. -- `s' + s'.lockout > S.last` - -Because `B` is an ancestor of `S.last`, it is also true then: - -- `s` is not a common ancestor of `B` and `X'` -- `s' + s'.lockout > B` - -which was included in `V`'s switching proof. - -Now because `W` is also a member of `Optimistic Voters`, then by the `Lemma 1` -above, given a vote by `W`, `Vote(X_w, S_w)`, where `S_w` contains a vote for -a slot `s` where `s + s.lockout > B`, and `s` is not an ancestor of `B`, then -`X_w > B`. - -Because validator `V` included vote `Vote(X_w, S_w)` in its proof of switching -for slot `X'`, then his implies validator `V'` submitted vote `Vote(X_w, S_w)` -**before** validator `V` submitted its switching vote for slot `X'`, -`Vote(X', S')`. - -But this is a contradiction because we chose `Vote(X', S')` to be the first vote -made by any validator in the `Optimistic Voters` set where `X' > B` and `X'` is -not a descendant of `B`. diff --git a/docs/src/proposals/partitioned-inflationary-rewards-distribution.md b/docs/src/proposals/partitioned-inflationary-rewards-distribution.md deleted file mode 100644 index e5694e7b70e..00000000000 --- a/docs/src/proposals/partitioned-inflationary-rewards-distribution.md +++ /dev/null @@ -1,151 +0,0 @@ ---- -title: Partitioned Inflationary Rewards Distribution ---- - -## Problem - -With the increase of number of stake accounts, computing and redeeming the stake -rewards at the start block of the epoch boundary becomes very expensive. -Currently, with 550K stake accounts, the stake reward time has already taken -more than 10 seconds. This prolonged computation slows down the network, and can -cause large number of forks at the epoch boundary, which makes the matter even -worse. - -## Proposed Solutions - -Instead of computing and reward stake accounts at epoch boundary, we will -decouple reward computation and reward credit into two phases. - -A separate service, "EpochRewardCalculationService" will be created. The service -will listen to a channel for any incoming rewards calculation requests, and -perform the calculation for the rewards. For each block that cross the epoch -boundary, the bank will send a request to the `EpochRewardCalculationService`. -This marks the start of the reward computation phase. - -``` -N-1 -- N -- N+1 - \ - \ - N+2 -``` - -In the above example, N is the start of the new epoch. Two rewards calculation -requests will be sent out at slot N and slot N+2 because they both cross the -epoch boundary and are on different forks. To avoid repeated computation with -the same input, the signature of the computation requests, `hash(epoch_number, -hash(stake_accounts_data), hash(vote_accounts), hash(delegation_map))`, are -calculated. Duplicated computation requests will be discard. For the above -example, if there are no stake/vote accounts changes between slot N and slot -N+2, the 2nd computation request will be discarded. - -When reaching block height `N` after the start of the `reward computation -phase`, the bank starts the second phase - reward credit, in which, the bank -first query the `epoch calc service` with the request signature to get the -rewards result, which will be resented as a map from accounts_pubkey->rewards, -then credit the rewards to the stake accounts for the next `M` blocks. If the -rewards result is not available, the bank will wait until the results are -available. - -We call them:
-(a) calculating interval: `[epoch_start, epoch_start+N]`
-(b) credit interval: `[epoch_start+N+1, epoch_start+N+M]`, respectively.
-And the combined interval `[epoch_start, epoch_start+N+M]` is called -`rewarding interval`. - -For `calculating interval`, `N` is chosen to be sufficiently large so that the -background computation should have completed and the result of the reward -computation is available at the end of `calculating interval`. `N` can be fixed -such as 100 (roughly equivalent to 50 seconds), or chosen as a function of the -number of stake accounts, `f(num_stake_accounts)`. - -In `credit interval`, the bank will fetch the reward computation results from -the background thread and start credit the rewards during the next `M` blocks. -The idea is partition the accounts into `M` partitions. And each block, the bank -credit `1/M` accounts. The partition is required to be deterministic for the -current epoch, but must also be random across different epochs. One way to -achieve these properties is to hash the account's pubkey with some epoch -dependent values, sort the results, and divide them into `M` bins. The epoch -dependent value can be the epoch number, total rewards for the epoch, the leader -pubkey for the epoch block, etc. `M` can be choses based on 50K account per -block, which equal to `ceil(num_stake_accounts/50,000)`. - -`num_stake_account` is extracted from `leader_schedule_epoch` block, so we don't -run into discrepancy where new transactions right before an epoch boundary -creates one fork with `X` stake accounts and another fork with `Y` stake accounts. - -In order to avoid putting extra burden of computing and credit the stake reward -for blocks produced during the `rewarding interval`, we can reduce the compute -budget limits on those blocks in `rewarding interval`, and reserve some computing -and read/write capacity to perform stake rewarding. - -### Challenges - -1. stake accounts reads/writes during the `rewarding interval` - -`epoch_start..epoch_start+N+M` Because of the delayed credit of the rewards, -Reads to those stake accounts will not return the value that the user are -expecting (viz. not include the recent epoch stake rewards). Writes to those -stake accounts will be lost once the reward are credited on block -`epoch_start+N+M`. We will need to modify the runtime to restrict read/writes to -stake accounts during the `rewarding interval`. Any transactions, which involves -stake accounts, will result in a new execution error, i.e. "stake rewards -pending, account access is restricted". However, normal rpc queries, such as -'getBalance', will return the current lamport of the account. The user can -expect the rewards to be credit as some time point during the 'rewarding -interval'. - -2. voting during `reward interval` - -During reward interval, vote transactions must be processed normally for -achieving consensus and making progress for rooted blocks. However, those vote -transactions may potentially change the vote accounts balance (i.e. pay for the -voting transaction fee if vote_account and block reward recipient accounts -are the same), before the epoch rewards are paid. When the epoch rewards are -paid, those block rewards will be wiped out by the stale cached value. To -prevent this, we will enforce that the vote_account and authorized_voter -authority must be different. - -3. snapshot taken during the `rewarding interval` - -If a snapshot is taken during the `rewarding interval`, it would miss the -rewards for the stake accounts. Any plain restart from those snapshots will be -wrong, unless we reconstruct the rewards from the recent epoch boundary. This -will add some complexity to validator restart. In the first implementation, we -will force *not* taking any snapshot and *not* performing accounts hash -calculation during the `rewarding interval`. Incremental snapshot request will -be skipped. Full snapshot request will be re-queued be picked up later at the -end of the `reward interval`. - -In future, if needed, we can -revisit to enable taking snapshots and perform hash calculation during reward -interval. - -4. account-db related action during the `rewarding interval` - -Account-db related action such as flush, clean, squash, shrink etc. may touch -and evict the stake accounts from account db's cache during the `rewarding -interval`. This will slow down the credit in the future at bank `epoch_start+N`. -We may need to exclude such accounts_db actions for stake_accounts during -`rewarding interval`. This is going to be a performance tuning problem. In the -first implementation, for simplicity, we will keep the account-db action as it -is, and make the `credit interval` larger to accommodate the performance hit -when writing back those accounts. In future, we can continue tuning account db -actions during 'rewarding interval'. - -5. view of total epoch capitalization change - -The view of total epoch capitalization, instead of being available at every -epoch boundary, is only available after the `rewarding interval`. Any third -party application logic, which depends on total epoch capitalization, need to -wait after `rewarding interval`. - -6. `getInflationReward` JSONRPC API method call - -Today, the `getInflationReward` JSONRPC API method call can simply grab the -first block in the target epoch and lookup the target stake account's rewards -entry. With these changes, the call will need updated to derive the target -stake account's credit block, grab _that_ block, then lookup rewards. -Additionally we'll need to return more informative errors for queries made -during the lockout period, so users can know that their rewards are pending for -the target epoch. A new rpc API, i.e. `getRewardInterval`, will be added for -querying the `rewarding interval` for the current epoch. diff --git a/docs/src/proposals/program-instruction-macro.md b/docs/src/proposals/program-instruction-macro.md deleted file mode 100644 index e87b0930f50..00000000000 --- a/docs/src/proposals/program-instruction-macro.md +++ /dev/null @@ -1,226 +0,0 @@ -# Program Instruction Macro - -## Problem - -Currently, inspecting an on-chain transaction requires depending on a -client-side, language-specific decoding library to parse the instruction. If -rpc methods could return decoded instruction details, these custom solutions -would be unnecessary. - -We can deserialize instruction data using a program's Instruction enum, but -decoding the account-key list into human-readable identifiers requires manual -parsing. Our current Instruction enums have that account information, but only -in variant docs. - -Similarly, we have instruction constructor functions that duplicate nearly all -the information in the enum, but we can't generate that constructor from the -enum definition because the list of account references is in code comments. - -Also, Instruction docs can vary between implementations, as there is no -mechanism to ensure consistency. - -## Proposed Solution - -Move the data from code comments to attributes, such that the constructors -can be generated, and include all the documentation from the enum definition. - -Here is an example of an Instruction enum using the new accounts format: - -```rust,ignore -#[instructions(test_program::id())] -pub enum TestInstruction { - /// Transfer lamports - #[accounts( - from_account(SIGNER, WRITABLE, desc = "Funding account"), - to_account(WRITABLE, desc = "Recipient account"), - )] - Transfer { - lamports: u64, - }, - - /// Provide M of N required signatures - #[accounts( - data_account(WRITABLE, desc = "Data account"), - signers(SIGNER, multiple, desc = "Signer"), - )] - Multisig, - - /// Consumes a stored nonce, replacing it with a successor - #[accounts( - nonce_account(SIGNER, WRITABLE, desc = "Nonce account"), - recent_blockhashes_sysvar(desc = "RecentBlockhashes sysvar"), - nonce_authority(SIGNER, optional, desc = "Nonce authority"), - )] - AdvanceNonceAccount, -} -``` - -An example of the generated TestInstruction with docs: - -```rust,ignore -pub enum TestInstruction { - /// Transfer lamports - /// - /// * Accounts expected by this instruction: - /// 0. `[WRITABLE, SIGNER]` Funding account - /// 1. `[WRITABLE]` Recipient account - Transfer { - lamports: u64, - }, - - /// Provide M of N required signatures - /// - /// * Accounts expected by this instruction: - /// 0. `[WRITABLE]` Data account - /// * (Multiple) `[SIGNER]` Signers - Multisig, - - /// Consumes a stored nonce, replacing it with a successor - /// - /// * Accounts expected by this instruction: - /// 0. `[WRITABLE, SIGNER]` Nonce account - /// 1. `[]` RecentBlockhashes sysvar - /// 2. (Optional) `[SIGNER]` Nonce authority - AdvanceNonceAccount, -} -``` - -Generated constructors: - -```rust,ignore -/// Transfer lamports -/// -/// * `from_account` - `[WRITABLE, SIGNER]` Funding account -/// * `to_account` - `[WRITABLE]` Recipient account -pub fn transfer(from_account: Pubkey, to_account: Pubkey, lamports: u64) -> Instruction { - let account_metas = vec![ - AccountMeta::new(from_pubkey, true), - AccountMeta::new(to_pubkey, false), - ]; - Instruction::new_with_bincode( - test_program::id(), - &SystemInstruction::Transfer { lamports }, - account_metas, - ) -} - -/// Provide M of N required signatures -/// -/// * `data_account` - `[WRITABLE]` Data account -/// * `signers` - (Multiple) `[SIGNER]` Signers -pub fn multisig(data_account: Pubkey, signers: &[Pubkey]) -> Instruction { - let mut account_metas = vec![ - AccountMeta::new(nonce_pubkey, false), - ]; - for pubkey in signers.iter() { - account_metas.push(AccountMeta::new_readonly(pubkey, true)); - } - - Instruction::new_with_bincode( - test_program::id(), - &TestInstruction::Multisig, - account_metas, - ) -} - -/// Consumes a stored nonce, replacing it with a successor -/// -/// * nonce_account - `[WRITABLE, SIGNER]` Nonce account -/// * recent_blockhashes_sysvar - `[]` RecentBlockhashes sysvar -/// * nonce_authority - (Optional) `[SIGNER]` Nonce authority -pub fn advance_nonce_account( - nonce_account: Pubkey, - recent_blockhashes_sysvar: Pubkey, - nonce_authority: Option, -) -> Instruction { - let mut account_metas = vec![ - AccountMeta::new(nonce_account, false), - AccountMeta::new_readonly(recent_blockhashes_sysvar, false), - ]; - if let Some(pubkey) = authorized_pubkey { - account_metas.push(AccountMeta::new_readonly*nonce_authority, true)); - } - Instruction::new_with_bincode( - test_program::id(), - &TestInstruction::AdvanceNonceAccount, - account_metas, - ) -} - -``` - -Generated TestInstructionVerbose enum: - -```rust,ignore -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -pub enum TestInstruction { - /// Transfer lamports - Transfer { - /// Funding account - funding_account: u8 - - /// Recipient account - recipient_account: u8 - - lamports: u64, - }, - - /// Provide M of N required signatures - Multisig { - data_account: u8, - signers: Vec, - }, - - /// Consumes a stored nonce, replacing it with a successor - AdvanceNonceAccount { - nonce_account: u8, - recent_blockhashes_sysvar: u8, - nonce_authority: Option, - } -} - -impl TestInstructionVerbose { - pub fn from_instruction(instruction: TestInstruction, account_keys: Vec) -> Self { - match instruction { - TestInstruction::Transfer { lamports } => TestInstructionVerbose::Transfer { - funding_account: account_keys[0], - recipient_account: account_keys[1], - lamports, - } - TestInstruction::Multisig => TestInstructionVerbose::Multisig { - data_account: account_keys[0], - signers: account_keys[1..], - } - TestInstruction::AdvanceNonceAccount => TestInstructionVerbose::AdvanceNonceAccount { - nonce_account: account_keys[0], - recent_blockhashes_sysvar: account_keys[1], - nonce_authority: &account_keys.get(2), - } - } - } -} - -``` - -## Considerations - -1. **Named fields** - Since the resulting Verbose enum constructs variants with - named fields, any unnamed fields in the original Instruction variant will need - to have names generated. As such, it would be considerably more straightforward - if all Instruction enum fields are converted to named types, instead of unnamed - tuples. This seems worth doing anyway, adding more precision to the variants and - enabling real documentation (so developers don't have to do - [this](https://github.com/solana-labs/solana/blob/3aab13a1679ba2b7846d9ba39b04a52f2017d3e0/sdk/src/system_instruction.rs#L140) - This will cause a little churn in our current code base, but not a lot. -2. **Variable account lists** - This approach offers a couple options for - variable account lists. First, optional accounts may be added and tagged with - the `optional` keyword. However, currently only one optional account is - supported per instruction. Additional data will need to be added to the - instruction to support multiples, enabling identification of which accounts are - present when some but not all are included. Second, accounts that share the same - features may be added as a set, tagged with the `multiple` keyword. Like - optional accounts, only one multiple account set is supported per instruction - (and optional and multiple may not coexist). More complex instructions that - cannot be accommodated by `optional` or `multiple`, requiring logic to figure - out account order/representation, should probably be made into separate - instructions. diff --git a/docs/src/proposals/return-data.md b/docs/src/proposals/return-data.md deleted file mode 100644 index 3e70e87195e..00000000000 --- a/docs/src/proposals/return-data.md +++ /dev/null @@ -1,144 +0,0 @@ -# Return data from SBF programs - -## Problem - -In the Solidity language it is permitted to return any number of values from a function, -for example a variable length string can be returned: - -``` -function foo1() public returns (string) { - return "Hello, world!\n"; -} -``` - -Multiple values, arrays and structs are permitted too. - -``` -struct S { - int f1; - bool f2 -}; - -function foo2() public returns (string, int[], S) { - return (a, b, c); -} -``` - -All the return values are eth abi encoded to a variable-length byte array. - -On ethereum errors can be returned too: - -``` -function withdraw() public { - require(msg.sender == owner, "Permission denied"); -} - -function failure() public { - revert("I afraid I can't do that dave"); -} -``` -These errors help the developer debug any issue they are having, and can -also be caught in a Solidity `try` .. `catch` block. Outside of a `try` .. `catch` -block, any of these would cause the transaction or rpc to fail. - -## Existing solution - -The existing solution that Solang uses, writes the return data to the callee account data. -The caller's account cannot be used, since the callee may not be the same SBF program, so -it will not have permission to write to the callee's account data. - -Another solution would be to have a single return data account which is passed -around through CPI. Again this does not work for CPI as the callee may not have -permission to write to it. - -The problem with this solution is: - -- It does not work for RPC calls -- It is very racey; a client has to submit the Tx and then retrieve the account - data. This is not atomic so the return data can be overwritten by another transaction. - -## Requirements for Solution - -It must work for: - -- RPC: An RPC should be able to return any number of values without writing to account data -- Transaction: An transaction should be able to return any number of values without needing to write them account data -- CPI: The callee must "set" return value, and the caller must be able to retrieve it. - -## Review of other chains - -### Ethereum (EVM) - -The `RETURN` opcode allows a contract to set a buffer as a returndata. This opcode takes a pointer to memory and a size. The `REVERT` opcode works similarly but signals that the call failed, and all account data changes must be reverted. - -For CPI, the caller can retrieve the returned data of the callee using the `RETURNDATASIZE` opcode which returns the length, and the `RETURNDATACOPY` opcode, which takes a memory destination pointer, offset into the returndata, and a length argument. - -Ethereum stores the returndata in blocks. - -### Parity Substrate - -The return data can be set using the `seal_return(u32 flags, u32 pointer, u32 size)` syscall. -- Flags can be 1 for revert, 0 for success (nothing else defined) -- Function does not return - -CPI: The `seal_call()` syscall takes pointer to buffer and pointer to buffer size where return data goes - - There is a 32KB limit for return data. - -Parity Substrate does not write the return data to blocks. - -## Rejected Solution - -The concept of ephemeral accounts has been proposed a solution for this. This would -certainly work for the CPI case, but this would not work RPC or Transaction case. - -## Proposed Solution - -The callee can set the return data using a new system call `sol_set_return_data(buf: *const u8, length: u64)`. -There is a limit of 1024 bytes for the returndata. This function can be called multiple times, and -will simply overwrite what was written in the last call. - -The return data can be retrieved with `sol_get_return_data(buf: *mut u8, length: u64, program_id: *mut Pubkey) -> u64`. -This function copies the return buffer, and the program_id that set the return data, and -returns the length of the return data, or `0` if no return data is set. In this case, program_id is not set. - -When an instruction calls `sol_invoke()`, the return data of the callee is copied into the return data -of the current instruction. This means that any return data is automatically passed up the call stack, -to the callee of the current instruction (or the RPC call). - -Note that `sol_invoke()` clears the returns data before invoking the callee, so that any return data from -a previous invoke is not reused if the invoked fails to set a return data. For example: - - - A invokes B - - Before entry to B, return data is cleared.0 - - B sets some return data and returns - - A invokes C - - Before entry to C, return data is cleared. - - C does not set return data and returns - - A checks return data and finds it empty - -Another scenario to consider: - - - A invokes B - - B invokes C - - C sets return data and returns - - B does not touch return data and returns - - A gets return data from C - - A does not touch return data - - Return data from transaction is what C set. - -The compute costs are calculated for getting and setting the return data using -the syscalls. - -For a normal RPC or Transaction, the returndata is base64-encoded and stored along side the sol_log -strings in the [stable log](https://github.com/solana-labs/solana/blob/95292841947763bdd47ef116b40fc34d0585bca8/sdk/src/process_instruction.rs#L275-L281). - -## Note on returning errors - -Solidity on Ethereum allows the contract to return an error in the return data. In this case, all -the account data changes for the account should be reverted. On Solana, any non-zero exit code -for a SBF program means the entire transaction fails. We do not wish to support an error return -by returning success and then returning an error in the return data. This would mean we would have -to support reverting the account data changes; this too expensive both on the VM side and the SBF -contract side. - -Errors will be reported via sol_log. diff --git a/docs/src/proposals/rip-curl.md b/docs/src/proposals/rip-curl.md deleted file mode 100644 index c562f01bd13..00000000000 --- a/docs/src/proposals/rip-curl.md +++ /dev/null @@ -1,56 +0,0 @@ -# RiP Curl: low-latency, transaction-oriented RPC - -## Problem - -Solana's initial RPC implementation was created for the purpose of allowing -users to confirm transactions that had just recently been sent to the cluster. -It was designed with memory usage in mind such that any validator should be -able to support the API without concern of DoS attacks. - -Later down the line, it became desirable to use that same API to support the -Solana explorer. The original design only supported minutes of history, so we -changed it to instead store transaction statuses in a local RocksDB instance -and offer days of history. We then extended that to 6 months via BigTable. - -With each modification, the API became more suitable for applications serving -static content and less appealing for transaction processing. The clients poll -for transaction status instead of being notified, giving the false impression -of higher confirmation times. Furthermore, what clients can poll for is -limited, preventing them from making reasonable real-time decisions, such as -recognizing a transaction is confirmed as soon as particular, known -validators vote on it. - -## Proposed Solution - -A web-friendly, transaction-oriented, streaming API built around the -validator's ReplayStage. - -Improved client experience: - -- Support connections directly from WebAssembly apps. -- Clients can be notified of confirmation progress in real-time, including votes - and voter stake weight. -- Clients can be notified when the heaviest fork changes, if it affects the - transactions confirmation count. - -Easier for validators to support: - -- Each validator supports some number of concurrent connections and otherwise - has no significant resource constraints. -- Transaction status is never stored in memory and cannot be polled for. -- Signatures are only stored in memory until the desired commitment level or - until the blockhash expires, whichever is later. - -How it works: - -1. The client connects to a validator using a reliable communication channel, - such as a web socket. -2. The validator registers the signature with ReplayStage. -3. The validator sends the transaction into the Gulf Stream and retries all - known forks until the blockhash expires (not until the transaction is - accepted on only the heaviest fork). If the blockhash expires, the - signature is unregistered, the client is notified, and connection is closed. -4. As ReplayStage detects events affecting the transaction's status, it - notifies the client in real-time. -5. After confirmation that the transaction is rooted (`CommitmentLevel::Max`), - the signature is unregistered and the server closes the upstream channel. diff --git a/docs/src/proposals/simple-payment-and-state-verification.md b/docs/src/proposals/simple-payment-and-state-verification.md deleted file mode 100644 index 6a561cd96a1..00000000000 --- a/docs/src/proposals/simple-payment-and-state-verification.md +++ /dev/null @@ -1,258 +0,0 @@ ---- -title: Simple Payment and State Verification ---- - -It is often useful to allow low resourced clients to participate in a Solana -cluster. Be this participation economic or contract execution, verification -that a client's activity has been accepted by the network is typically -expensive. This proposal lays out a mechanism for such clients to confirm that -their actions have been committed to the ledger state with minimal resource -expenditure and third-party trust. - -## A Naive Approach - -Validators store the signatures of recently confirmed transactions for a short -period of time to ensure that they are not processed more than once. Validators -provide a JSON RPC endpoint, which clients can use to query the cluster if a -transaction has been recently processed. Validators also provide a PubSub -notification, whereby a client registers to be notified when a given signature -is observed by the validator. While these two mechanisms allow a client to -verify a payment, they are not a proof and rely on completely trusting a -validator. - -We will describe a way to minimize this trust using Merkle Proofs to anchor the -validator's response in the ledger, allowing the client to confirm on their own -that a sufficient number of their preferred validators have confirmed a -transaction. Requiring multiple validator attestations further reduces trust in -the validator, as it increases both the technical and economic difficulty of -compromising several other network participants. - -## Light Clients - -A 'light client' is a cluster participant that does not itself run a validator. -This light client would provide a level of security greater than trusting a -remote validator, without requiring the light client to spend a lot of resources -verifying the ledger. - -Rather than providing transaction signatures directly to a light client, the -validator instead generates a Merkle Proof from the transaction of interest to -the root of a Merkle Tree of all transactions in the including block. This -Merkle Root is stored in a ledger entry which is voted on by validators, -providing it consensus legitimacy. The additional level of security for a light -client depends on an initial canonical set of validators the light client -considers to be the stakeholders of the cluster. As that set is changed, the -client can update its internal set of known validators with -[receipts](simple-payment-and-state-verification.md#receipts). This may become -challenging with a large number of delegated stakes. - -Validators themselves may want to use light client APIs for performance reasons. -For example, during the initial launch of a validator, the validator may use a -cluster provided checkpoint of the state and verify it with a receipt. - -## Receipts - -A receipt is a minimal proof that; a transaction has been included in a block, -that the block has been voted on by the client's preferred set of validators -and that the votes have reached the desired confirmation depth. - -### Transaction Inclusion Proof - -A transaction inclusion proof is a data structure that contains a Merkle Path -from a transaction, through an Entry-Merkle to a Block-Merkle, which is included -in a Bank-Hash with the required set of validator votes. A chain of PoH Entries -containing subsequent validator votes, deriving from the Bank-Hash, is the proof -of confirmation. - -#### Transaction Merkle - -An Entry-Merkle is a Merkle Root including all transactions in a given entry, -sorted by signature. Each transaction in an entry is already merkled here: -https://github.com/solana-labs/solana/blob/b6bfed64cb159ee67bb6bdbaefc7f833bbed3563/ledger/src/entry.rs#L205. -This means we can show a transaction `T` was included in an entry `E`. - -A Block-Merkle is the Merkle Root of all the Entry-Merkles sequenced in the -block. - -![Block Merkle Diagram](/img/spv-block-merkle.svg) - -Together the two merkle proofs show a transaction `T` was included in a block -with bank hash `B`. - -An Accounts-Hash is the hash of the concatenation of the state hashes of -each account modified during the current slot. - -Transaction status is necessary for the receipt because the state receipt is -constructed for the block. Two transactions over the same state can appear in -the block, and therefore, there is no way to infer from just the state whether -a transaction that is committed to the ledger has succeeded or failed in -modifying the intended state. It may not be necessary to encode the full status -code, but a single status bit to indicate the transaction's success. - -Currently, the Block-Merkle is not implemented, so to verify `E` was an entry -in the block with bank hash `B`, we would need to provide all the entry hashes -in the block. Ideally this Block-Merkle would be implemented, as the alternative -is very inefficient. - -#### Block Headers - -In order to verify transaction inclusion proofs, light clients need to be able -to infer the topology of the forks in the network - -More specifically, the light client will need to track incoming block headers -such that given two bank hashes for blocks `A` and `B`, they can determine -whether `A` is an ancestor of `B` (Below section on -`Optimistic Confirmation Proof` explains why!). Contents of header are the -fields necessary to compute the bank hash. - -A Bank-Hash is the hash of the concatenation of the Block-Merkle and -Accounts-Hash described in the `Transaction Merkle` section above. - -![Bank Hash Diagram](/img/spv-bank-hash.svg) - -In the code: - -https://github.com/solana-labs/solana/blob/b6bfed64cb159ee67bb6bdbaefc7f833bbed3563/runtime/src/bank.rs#L3468-L3473 - -``` - let mut hash = hashv(&[ - // bank hash of the parent block - self.parent_hash.as_ref(), - // hash of all the modified accounts - accounts_delta_hash.hash.as_ref(), - // Number of signatures processed in this block - &signature_count_buf, - // Last PoH hash in this block - self.latest_blockhash().as_ref(), - ]); -``` - -A good place to implement this logic along existing streaming logic in the -validator's replay logic: https://github.com/solana-labs/solana/blob/b6bfed64cb159ee67bb6bdbaefc7f833bbed3563/core/src/replay_stage.rs#L1092-L1096 - -#### Optimistic Confirmation Proof - -Currently optimistic confirmation is detected via a listener that monitors -gossip and the replay pipeline for votes: -https://github.com/solana-labs/solana/blob/b6bfed64cb159ee67bb6bdbaefc7f833bbed3563/core/src/cluster_info_vote_listener.rs#L604-L614. - -Each vote is a signed transaction that includes the bank hash of the block the -validator voted for, i.e. the `B` from the `Transaction Merkle` section above. -Once a certain threshold `T` of the network has voted on a block, the block is -considered optimistically confirmed. The votes made by this group of `T` -validators is needed to show the block with bank hash `B` was optimistically -confirmed. - -However other than some metadata, the signed votes themselves are not -currently stored anywhere, so they can't be retrieved on demand. These votes -probably need to be persisted in Rocksdb database, indexed by a key -`(Slot, Hash, Pubkey)` which represents the slot of the vote, bank hash of the -vote, and vote account pubkey responsible for the vote. - -Together, the transaction merkle and optimistic confirmation proofs can be -provided over RPC to subscribers by extending the existing signature -subscription logic. Clients who subscribe to the "Confirmed" confirmation -level are already notified when optimistic confirmation is detected, a flag -can be provided to signal the two proofs above should also be returned. - -It is important to note that optimistically confirming `B` also implies that all -ancestor blocks of `B` are also optimistically confirmed, and also that not -all blocks will be optimistically confirmed. - -``` - -B -> B' - -``` - -So in the example above if a block `B'` is optimistically confirmed, then so is -`B`. Thus if a transaction was in block `B`, the transaction merkle in the -proof will be for block `B`, but the votes presented in the proof will be for -block `B'`. This is why the headers in the `Block headers` section above are -important, the client will need to verify that `B` is indeed an ancestor of -`B'`. - -#### Proof of Stake Distribution - -Once presented with the transaction merkle and optimistic confirmation proofs -above, a client can verify a transaction `T` was optimistically confirmed in a -block with bank hash `B`. The last missing piece is how to verify that the -votes in the optimistic proofs above actually constitute the valid `T` -percentage of the stake necessary to uphold the safety guarantees of -"optimistic confirmation". - -One way to approach this might be for every epoch, when the stake set changes, -to write all the stakes to a system account, and then have validators subscribe -to that system account. Full nodes can then provide a merkle proving that the -system account state was updated in some block `B`, and then show that the -block `B` was optimistically confirmed/rooted. - -### Account State Verification - -An account's state (balance or other data) can be verified by submitting a -transaction with a **_TBD_** Instruction to the cluster. The client can then -use a [Transaction Inclusion Proof](#transaction-inclusion-proof) to verify -whether the cluster agrees that the account has reached the expected state. - -### Validator Votes - -Leaders should coalesce the validator votes by stake weight into a single entry. -This will reduce the number of entries necessary to create a receipt. - -### Chain of Entries - -A receipt has a PoH link from the payment or state Merkle Path root to a list -of consecutive validation votes. - -It contains the following: - -- Transaction -> Entry-Merkle -> Block-Merkle -> Bank-Hash - -And a vector of PoH entries: - -- Validator vote entries -- Ticks -- Light entries - -```text -/// This Entry definition skips over the transactions and only contains the -/// hash of the transactions used to modify PoH. -LightEntry { - /// The number of hashes since the previous Entry ID. - pub num_hashes: u64, - /// The SHA-256 hash `num_hashes` after the previous Entry ID. - hash: Hash, - /// The Merkle Root of the transactions encoded into the Entry. - entry_hash: Hash, -} -``` - -The light entries are reconstructed from Entries and simply show the entry -Merkle Root that was mixed in to the PoH hash, instead of the full transaction -set. - -Clients do not need the starting vote state. The -[fork selection](../implemented-proposals/tower-bft.md) algorithm is defined -such that only votes that appear after the transaction provide finality for the -transaction, and finality is independent of the starting state. - -### Verification - -A light client that is aware of the supermajority set validators can verify a -receipt by following the Merkle Path to the PoH chain. The Block-Merkle is the -Merkle Root and will appear in votes included in an Entry. The light client can -simulate [fork selection](../implemented-proposals/tower-bft.md) for the -consecutive votes and verify that the receipt is confirmed at the desired -lockout threshold. - -### Synthetic State - -Synthetic state should be computed into the Bank-Hash along with the bank -generated state. - -For example: - -- Epoch validator accounts and their stakes and weights. -- Computed fee rates - -These values should have an entry in the Bank-Hash. They should live under known -accounts, and therefore have an index into the hash concatenation. diff --git a/docs/src/proposals/slashing.md b/docs/src/proposals/slashing.md deleted file mode 100644 index 10d4fe988f7..00000000000 --- a/docs/src/proposals/slashing.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -title: Slashing rules ---- - -Unlike Proof of Work \(PoW\) where off-chain capital expenses are already -deployed at the time of block construction/voting, PoS systems require -capital-at-risk to prevent a logical/optimal strategy of multiple chain voting. -We intend to implement slashing rules which, if broken, result some amount of -the offending validator's deposited stake to be removed from circulation. Given -the ordering properties of the PoH data structure, we believe we can simplify -our slashing rules to the level of a voting lockout time assigned per vote. - -I.e. Each vote has an associated lockout time \(PoH duration\) that represents -a duration by any additional vote from that validator must be in a PoH that -contains the original vote, or a portion of that validator's stake is -slashable. This duration time is a function of the initial vote PoH count and -all additional vote PoH counts. It will likely take the form: - -```text -Lockouti\(PoHi, PoHj\) = PoHj + K \* exp\(\(PoHj - PoHi\) / K\) -``` - -Where PoHi is the height of the vote that the lockout is to be applied to and -PoHj is the height of the current vote on the same fork. If the validator -submits a vote on a different PoH fork on any PoHk where k > j > i and -PoHk < Lockout\(PoHi, PoHj\), then a portion of that validator's stake is at -risk of being slashed. - -In addition to the functional form lockout described above, early -implementation may be a numerical approximation based on a First In, First Out -\(FIFO\) data structure and the following logic: - -- FIFO queue holding 32 votes per active validator -- new votes are pushed on top of queue \(`push_front`\) -- expired votes are popped off top \(`pop_front`\) -- as votes are pushed into the queue, the lockout of each queued vote doubles -- votes are removed from back of queue if `queue.len() > 32` -- the earliest and latest height that has been removed from the back of the - queue should be stored - -It is likely that a reward will be offered as a % of the slashed amount to any -node that submits proof of this slashing condition being violated to the PoH. - -### Partial Slashing - -In the schema described so far, when a validator votes on a given PoH stream, -they are committing themselves to that fork for a time determined by the vote -lockout. An open question is whether validators will be hesitant to begin -voting on an available fork if the penalties are perceived too harsh for an -honest mistake or flipped bit. - -One way to address this concern would be a partial slashing design that results -in a slashable amount as a function of either: - -1. the fraction of validators, out of the total validator pool, that were also - slashed during the same time period \(ala Casper\) -2. the amount of time since the vote was cast \(e.g. a linearly increasing % of - total deposited as slashable amount over time\), or both. - -This is an area currently under exploration. diff --git a/docs/src/proposals/snapshot-verification.md b/docs/src/proposals/snapshot-verification.md deleted file mode 100644 index 8955fb51ed2..00000000000 --- a/docs/src/proposals/snapshot-verification.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Snapshot Verification ---- - -## Problem - -Snapshot verification of the account states is implemented, but the bank hash of the snapshot which is used to verify is falsifiable. - -## Solution - -While a validator is processing transactions to catch up to the cluster from the snapshot, use incoming vote transactions and the commitment calculator to confirm that the cluster is indeed building on the snapshotted bank hash. Once a threshold commitment level is reached, accept the snapshot as valid and start voting. diff --git a/docs/src/proposals/tick-verification.md b/docs/src/proposals/tick-verification.md deleted file mode 100644 index a84c42c985d..00000000000 --- a/docs/src/proposals/tick-verification.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -title: Tick Verification ---- - -This design the criteria and validation of ticks in a slot. It also describes -error handling and slashing conditions encompassing how the system handles -transmissions that do not meet these requirements. - -# Slot structure - -Each slot must contain an expected `ticks_per_slot` number of ticks. The last -shred in a slot must contain only the entirety of the last tick, and nothing -else. The leader must also mark this shred containing the last tick with the -`LAST_SHRED_IN_SLOT` flag. Between ticks, there must be `hashes_per_tick` -number of hashes. - -# Handling bad transmissions - -Malicious transmissions `T` are handled in two ways: - -1. If a leader can generate some erroneous transmission `T` and also some - alternate transmission `T'` for the same slot without violating any slashing - rules for duplicate transmissions (for instance if `T'` is a subset of `T`), - then the cluster must handle the possibility of both transmissions being live. - -Thus this means we cannot mark the erroneous transmission `T` as dead because -the cluster may have reached consensus on `T'`. These cases necessitate a -slashing proof to punish this bad behavior. - -2. Otherwise, we can simply mark the slot as dead and not playable. A slashing - proof may or may not be necessary depending on feasibility. - -# Blockstore receiving shreds - -When blockstore receives a new shred `s`, there are two cases: - -1. `s` is marked as `LAST_SHRED_IN_SLOT`, then check if there exists a shred - `s'` in blockstore for that slot where `s'.index > s.index` If so, together `s` - and `s'` constitute a slashing proof. - -2. Blockstore has already received a shred `s'` marked as `LAST_SHRED_IN_SLOT` - with index `i`. If `s.index > i`, then together `s` and `s'`constitute a - slashing proof. In this case, blockstore will also not insert `s`. - -3. Duplicate shreds for the same index are ignored. Non-duplicate shreds for - the same index are a slashable condition. Details for this case are covered - in the `Leader Duplicate Block Slashing` section. - -# Replaying and validating ticks - -1. Replay stage replays entries from blockstore, keeping track of the number of - ticks it has seen per slot, and verifying there are `hashes_per_tick` number of - hashes between ticks. After the tick from this last shred has been played, - replay stage then checks the total number of ticks. - -Failure scenario 1: If ever there are two consecutive ticks between which the -number of hashes is `!= hashes_per_tick`, mark this slot as dead. - -Failure scenario 2: If the number of ticks != `ticks_per_slot`, mark slot as -dead. - -Failure scenario 3: If the number of ticks reaches `ticks_per_slot`, but we still -haven't seen the `LAST_SHRED_IN_SLOT`, mark this slot as dead. - -2. When ReplayStage reaches a shred marked as the last shred, it checks if this - last shred is a tick. - -Failure scenario: If the signed shred with the `LAST_SHRED_IN_SLOT` flag cannot -be deserialized into a tick (either fails to deserialize or deserializes into -an entry), mark this slot as dead. diff --git a/docs/src/proposals/timely-vote-credits.md b/docs/src/proposals/timely-vote-credits.md deleted file mode 100644 index 39c496ff9df..00000000000 --- a/docs/src/proposals/timely-vote-credits.md +++ /dev/null @@ -1,190 +0,0 @@ ---- -title: Timely Vote Credits ---- - -## Timely Vote Credits - -This design describes a modification to the method that is used to calculate -vote credits earned by validator votes. - -Vote credits are the accounting method used to determine what percentage of -inflation rewards a validator earns on behalf of its stakers. Currently, when -a slot that a validator has previously voted on is "rooted", it earns 1 vote -credit. A "rooted" slot is one which has received full commitment by the -validator (i.e. has been finalized). - -One problem with this simple accounting method is that it awards one credit -regardless of how "old" the slot that was voted on at the time that it was -voted on. This means that a validator can delay its voting for many slots in -order to survey forks and wait to make votes that are less likely to be -expired, and without incurring any penalty for doing so. This is not just a -theoretical concern: there are known and documented instances of validators -using this technique to significantly delay their voting while earning more -credits as a result. - - -### Proposed Change - -The proposal is to award a variable number of vote credits per voted on slot, -with more credits being given for votes that have "less latency" than votes -that have "more latency". - -In this context, "latency" is the number of slots in between the slot that is -being voted on and the slot in which the vote has landed. Because a slot -cannot be voted on until after it has been completed, the minimum possible -latency is 1, which would occur when a validator voted as quickly as possible, -transmitting its vote on that slot in time for it to be included in the very -next slot. - -Credits awarded would become a function of this latency, with lower latencies -awarding more credits. This will discourage intentional "lagging", because -delaying a vote for any slots decreases the number of credits that vote will -earn, because it will necessarily land in a later slot if it is delayed, and -then earn a lower number of credits than it would have earned had it been -transmitted immediately and landed in an earlier slot. - -### Grace Period - -If landing a vote with 1 slot latency awarded more credit than landing that -same vote in 2 slots latency, then validators who could land votes -consistently within 1 slot would have a credits earning advantage over those -who could not. Part of the latency when transmitting votes is unavoidable as -it's a function of geographical distance between the sender and receiver of -the vote. The Solana network is spread around the world but it is not evenly -distributed over the whole planet; there are some locations which are, on -average, more distant from the network than others are. - -It would likely be harmful to the network to encourage tight geographical -concentration - if, for example, the only way to achieve 1 slot latency was to -be within a specific country - then a very strict credit rewards schedule -would encourage all validators to move to the same country in order to -maximize their credit earnings. - -For this reason, the credits reward schedule should have a built-in "grace -period" that gives all validators a "reasonable" amount of time to land their -votes. This will reduce the credits earning disadvantage that comes from -being more distant from the network. A balance needs to be struck between the -strictest rewards schedule, which most strongly discourages intentional -lagging, and more lenient rewards schedules, which improves credit earnings -for distant validators who are not artificially lagging. - -Historical voting data has been analyzed over many epochs and the data -suggests that the smallest grace period that allows for very minimal impact on -well behaved distant validators is 3 slots, which means that all slots voted -on within 3 slots will award maximum vote credits to the voting validator. -This gives validators nearly 2 seconds to land their votes without penalty. -The maximum latency between two points on Earth is about 100 ms, so allowing a -full 1,500 ms to 2,000 ms latency without penalty should not have adverse -impact on distant validators. - -### Maximum Vote Credits - -Another factor to consider is what the maximum vote credits to award for a -vote should be. Assuming linear reduction in vote credits awarded (where 1 -slot of additional lag reduces earned vote credits by 1), the maximum vote -credits value determines how much "penalty" there is for each additional slot -of latency. For example, a value of 10 would mean that after the grace period -slots, every additional slot of latency would result in a 10% reduction in -vote credits earned as each subsequent slot earns 1 credit less out of a -maximum possible 10 credits. - -Again, historical voting data was analyzed over many epochs and the conclusion -drawn was that a maximum credits of 10 is the largest value that can be used -and still have a noticeable effect on known laggers. Values higher than that -result in such a small penalty for each slot of lagging that intentional -lagging is still too profitable. Lower values are even more punishing to -intentional lagging; but an attempt has been made to conservatively choose the -highest value that produces noticeable results. - -The selection of these values is partially documented here: - -https://www.shinobi-systems.com/timely_voting_proposal - -The above document is somewhat out of date with more recent analysis, which -occurred in this github issue: - -https://github.com/solana-labs/solana/issues/19002 - -To summarize the findings of these documents: analysis over many epochs showed -that almost all validators from all regions have an average vote latency of 1 -slot or less. The validators with higher average latency are either known -laggers, or are not representative of their region since many other validators -in the same region achieve low latency voting. With a maximum vote credit of -10, there is almost no change in vote credits earned relative to the highest vote -earner by the majority of validators, aside from a general uplift of about 0.4%. -Additionally, data centers were analyzed to ensure that there aren't regions of -the world that would be adversely affected, and none were found. - - -### Method of Implementation - -When a Vote or VoteStateUpdate instruction is received by a validator, it will -use the Clock sysvar to identify the slot in which that instruction has -landed. For any newly voted on slot within that Vote or VoteStateUpdate -transaction, the validator will record the vote latency of that slot as -(voted_in_slot - voted_on_slot). - -These vote latencies will be stored a new vector of u8 latency values appended -to the end of the VoteState. VoteState currently has ~200 bytes of free space -at the end that is unused, so this new vector of u8 values should easily fit -within this available space. Because VoteState is an ABI frozen structure, -utilizing the mechanisms for updating frozen ABI will be required, which will -complicate the change. Furthermore, because VoteState is embedded in the -Tower data structure and it is frozen ABI as well, updates to the frozen ABI -mechanisms for Tower will be needed also. These are almost entirely -mechanical changes though, that involve ensuring that older versions of these -data structures can be updated to the new version as they are read in, and the -new version written out when the data structure is next persisted. - -The credits to award for a rooted slot will be calculated using the latency -value stored in latency vector for the slot, and a formula that awards -latencies of 1 - 3 slots ten credits, with a 1 credit reduction for each vote -latency after 3. Rooted slots will always be awarded a minimum credit of 1 -(never 0) so that very old votes, possibly necessary in times of network -stress, are not discouraged. - -To summarize the above: latency is recorded in a new Vector at the end of -VoteState when a vote first lands, but the credits for that slot are not -awarded until the slot becomes rooted, at which point the latency that was -recorded is used to compute the credits to award for that newly rooted slot. - -When a Vote instruction is processed, the changes are fairly easy to implement -as Vote can only add new slots to the end of Lockouts and pop existing slots -off of the back (which become rooted), so the logic merely has to compute -rewards for the new roots, and new latencies for the newly added slots, both -of which can be processed in the fairly simple existing logic for Vote -processing. - -When a VoteStateUpdate instruction is processed: - -1. For each slot that was in the previous VoteState but are not in the new -VoteState because they have been rooted in the transition from the old -VoteState to the new VoteState, credits to award are calculated based on the -latency that was recorded for them and still available in the old VoteState. - -2. For each slot that was in both the previous VoteState and the new -VoteState, the latency that was previously recorded for that slot is copied -from the old VoteState to the new VoteState. - -3. For each slot that is in the new VoteState but wasn't in the old VoteState, -the latency value is calculated for this new slot according to what slot the -vote is for and what slot is in the Clock (i.e. the slot this VoteStateUpdate -tx landed in) and this latency is stored in VoteState for that slot. - -The code to handle this is more complex, because VoteStateUpdate may include -removal of slots that expired as performed by the voting validator, in -addition to slots that have been rooted and new slots added. However, the -assumptions that are needed to handle VoteStateUpdate with timely vote credits -are already guaranteed by existing VoteStateUpdate correctness checking code: - -The existing VoteStateUpdate processing code already ensures that (1) only -roots slots that could actually have been rooted in the transition from the -old VoteState to the new VoteState, so there is no danger of over-counting -credits (i.e. imagine that a 'cheating' validator "pretended" that slots were -rooted by dropping them off of the back of the new VoteState before they have -actually achieved 32 confirmations; the existing logic prevents this). - -The existing VoteStateUpdate processing code already ensures that (2) new -slots included in the new VoteState are only slots after slots that have -already been voted on in the old VoteState (i.e. can't inject new slots in the -middle of slots already voted on). diff --git a/docs/src/proposals/validator-proposal.md b/docs/src/proposals/validator-proposal.md deleted file mode 100644 index 74bdfe3a066..00000000000 --- a/docs/src/proposals/validator-proposal.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -title: Validator ---- - -## History - -When we first started Solana, the goal was to de-risk our TPS claims. We knew -that between optimistic concurrency control and sufficiently long leader slots, -that PoS consensus was not the biggest risk to TPS. It was GPU-based signature -verification, software pipelining and concurrent banking. Thus, the TPU was -born. After topping 100k TPS, we split the team into one group working toward -710k TPS and another to flesh out the validator pipeline. Hence, the TVU was -born. The current architecture is a consequence of incremental development with -that ordering and project priorities. It is not a reflection of what we ever -believed was the most technically elegant cross-section of those technologies. -In the context of leader rotation, the strong distinction between leading and -validating is blurred. - -## Difference between validating and leading - -The fundamental difference between the pipelines is when the PoH is present. In -a leader, we process transactions, removing bad ones, and then tag the result -with a PoH hash. In the validator, we verify that hash, peel it off, and -process the transactions in exactly the same way. The only difference is that -if a validator sees a bad transaction, it can't simply remove it like the -leader does, because that would cause the PoH hash to change. Instead, it -rejects the whole block. The other difference between the pipelines is what -happens _after_ banking. The leader broadcasts entries to downstream validators -whereas the validator will have already done that in RetransmitStage, which is -a confirmation time optimization. The validation pipeline, on the other hand, -has one last step. Any time it finishes processing a block, it needs to weigh -any forks it's observing, possibly cast a vote, and if so, reset its PoH hash -to the block hash it just voted on. - -## Proposed Design - -We unwrap the many abstraction layers and build a single pipeline that can -toggle leader mode on whenever the validator's ID shows up in the leader -schedule. - -![Validator block diagram](/img/validator-proposal.svg) - -## Notable changes - -- Hoist FetchStage and BroadcastStage out of TPU -- BankForks renamed to Banktree -- TPU moves to new socket-free crate called solana-tpu. -- TPU's BankingStage absorbs ReplayStage -- TVU goes away -- New RepairStage absorbs Shred Fetch Stage and repair requests -- JSON RPC Service is optional - used for debugging. It should instead be part - of a separate `solana-blockstreamer` executable. -- New MulticastStage absorbs retransmit part of RetransmitStage -- MulticastStage downstream of Blockstore diff --git a/docs/src/proposals/versioned-transactions.md b/docs/src/proposals/versioned-transactions.md deleted file mode 100644 index b51c772afbc..00000000000 --- a/docs/src/proposals/versioned-transactions.md +++ /dev/null @@ -1,338 +0,0 @@ -# Versioned Transactions - v0: Address Lookup Tables - -## Problem - -Messages transmitted to Solana validators must not exceed the IPv6 MTU size to -ensure fast and reliable network transmission of cluster info over UDP. -Solana's networking stack uses a conservative MTU size of 1280 bytes which, -after accounting for headers, leaves 1232 bytes for packet data like serialized -transactions. - -Developers building applications on Solana must design their on-chain program -interfaces within the above transaction size limit constraint. One common -work-around is to store state temporarily on-chain and consume that state in -later transactions. This is the approach used by the BPF loader program for -deploying Solana programs. - -However, this workaround doesn't work well when developers compose many on-chain -programs in a single atomic transaction. With more composition comes more -account inputs, each of which takes up 32 bytes. There is currently no available -workaround for increasing the number of accounts used in a single transaction -since each transaction must list all accounts that it needs to properly lock -accounts for parallel execution. Therefore the current cap is about 35 accounts -after accounting for signatures and other transaction metadata. - -## Proposed Solution - -1. Introduce a new program which manages on-chain address lookup tables -2. Add a new transaction format which can make use of on-chain - address lookup tables to efficiently load more accounts in a single transaction. - -### Address Lookup Table Program - -Here we describe a program-based solution to the problem, whereby a protocol -developer or end-user can create collections of related addresses on-chain for -concise use in a transaction's account inputs. - -After addresses are stored on-chain in an address lookup table account, they may be -succinctly referenced in a transaction using a 1-byte u8 index rather than a -full 32-byte address. This will require a new transaction format to make use of -these succinct references as well as runtime handling for looking up and loading -addresses from the on-chain lookup tables. - -#### State - -Address lookup tables must be rent-exempt when initialized and after -each time new addresses are appended. Lookup tables can either be extended -from an on-chain buffered list of addresses or directly by appending -addresses through instruction data. Newly appended addresses require -one slot to warmup before being available to transactions for lookups. - -Since transactions use a `u8` index to look up addresses, address tables can -store up to 256 addresses each. In addition to stored addresses, address table -accounts also tracks various metadata explained below. - -```rust -/// The maximum number of addresses that a lookup table can hold -pub const LOOKUP_TABLE_MAX_ADDRESSES: usize = 256; - -/// The serialized size of lookup table metadata -pub const LOOKUP_TABLE_META_SIZE: usize = 56; - -pub struct LookupTableMeta { - /// Lookup tables cannot be closed until the deactivation slot is - /// no longer "recent" (not accessible in the `SlotHashes` sysvar). - pub deactivation_slot: Slot, - /// The slot that the table was last extended. Address tables may - /// only be used to lookup addresses that were extended before - /// the current bank's slot. - pub last_extended_slot: Slot, - /// The start index where the table was last extended from during - /// the `last_extended_slot`. - pub last_extended_slot_start_index: u8, - /// Authority address which must sign for each modification. - pub authority: Option, - // Raw list of addresses follows this serialized structure in - // the account's data, starting from `LOOKUP_TABLE_META_SIZE`. -} -``` - -#### Cleanup - -Once an address lookup table is no longer needed, it can be deactivated and closed -to have its rent balance reclaimed. Address lookup tables may not be recreated -at the same address because each new lookup table must be initialized at an address -derived from a recent slot. - -Address lookup tables can be deactivated at any time but can continue to be used -by transactions until the deactivation slot is no longer present in the slot hashes -sysvar. This cool-down period ensures that in-flight transactions cannot be -censored and that address lookup tables cannot be closed and recreated for the same -slot. - -### Versioned Transactions - -In order to support address table lookups, the structure of serialized -transactions must be modified. The new transaction format should not -affect transaction processing in the Solana program runtime beyond the -increased capacity for accounts and program invocations. Invoked -programs will be unaware of which transaction format was used. - -The new transaction format must be distinguished from the current transaction -format. Current transactions can fit at most 19 signatures (64-bytes each) but -the message header encodes `num_required_signatures` as a `u8`. Since the upper -bit of the `u8` will never be set for a valid transaction, we can enable it to -denote whether a transaction should be decoded with the versioned format or not. - -The original transaction format will be referred to as the legacy transaction -version and the first versioned transaction format will start at version 0. - -#### New Transaction Format - -```rust -#[derive(Serialize, Deserialize)] -pub struct VersionedTransaction { - /// List of signatures - #[serde(with = "short_vec")] - pub signatures: Vec, - /// Message to sign. - pub message: VersionedMessage, -} - -// Uses custom serialization. If the first bit is set, the remaining bits -// in the first byte will encode a version number. If the first bit is not -// set, the first byte will be treated as the first byte of an encoded -// legacy message. -pub enum VersionedMessage { - Legacy(LegacyMessage), - V0(v0::Message), -} - -// The structure of the new v0 Message -#[derive(Serialize, Deserialize)] -pub struct Message { - // unchanged - pub header: MessageHeader, - - // unchanged - #[serde(with = "short_vec")] - pub account_keys: Vec, - - // unchanged - pub recent_blockhash: Hash, - - // unchanged - // - // # Notes - // - // Account and program indexes will index into the list of addresses - // constructed from the concatenation of three key lists: - // 1) message `account_keys` - // 2) ordered list of keys loaded from address table `writable_indexes` - // 3) ordered list of keys loaded from address table `readonly_indexes` - #[serde(with = "short_vec")] - pub instructions: Vec, - - /// List of address table lookups used to load additional accounts - /// for this transaction. - #[serde(with = "short_vec")] - pub address_table_lookups: Vec, -} - -/// Address table lookups describe an on-chain address lookup table to use -/// for loading more readonly and writable accounts in a single tx. -#[derive(Serialize, Deserialize)] -pub struct MessageAddressTableLookup { - /// Address lookup table account key - pub account_key: Pubkey, - /// List of indexes used to load writable account addresses - #[serde(with = "short_vec")] - pub writable_indexes: Vec, - /// List of indexes used to load readonly account addresses - #[serde(with = "short_vec")] - pub readonly_indexes: Vec, -} -``` - -#### Size changes - -- 1 extra byte for the `version` field -- 1 extra byte for the `address_table_lookups` length -- 34 extra bytes for each address table lookup which includes 32 bytes for the - address of the table and a byte each for the lengths of the `writable_indexes` - and `readonly_indexes` fields -- 1 extra byte for each lookup table index - -#### Cost - -Address lookups require extra computational overhead during transaction -processing, but also reduce network bandwidth due to smaller transactions and -therefore smaller blocks. - -#### Metadata changes - -Each resolved address from an address lookup table should be stored in -the transaction metadata for quick reference. This will avoid the need for -clients to make multiple RPC round trips to fetch all accounts loaded by a -v0 transaction. It will also make it easier to use the ledger tool to -analyze account access patterns. - -#### RPC changes - -Fetched transaction responses will likely require a new version field to -indicate to clients which transaction structure to use for deserialization. -Clients using pre-existing RPC methods will receive error responses when -attempting to fetch a versioned transaction which will indicate that they -must upgrade. - -The RPC API should also support an option for returning fully expanded -transactions to abstract away the address lookup table details from -downstream clients. - -### Limitations - -- Max of 256 unique accounts may be loaded by a transaction because `u8` - is used by compiled instructions to index into transaction message `account_keys`. -- Address lookup tables can hold up to 256 entries because lookup table indexes are also `u8`. -- Transaction signers may not be loaded through an address lookup table, the full - address of each signer must be serialized in the transaction. This ensures that - the performance of transaction signature checks is not affected. -- Hardware wallets will not be able to display details about accounts referenced - through address lookup tables due to inability to verify on-chain data. -- Only single level address lookup tables can be used. Recursive lookups will not be supported. - -## Security Concerns - -### Lookup table re-initialization - -If an address lookup table can be closed and re-initialized with new addresses, -any client which is unaware of the change could inadvertently lookup unexpected -addresses. To avoid this, all address lookup tables must be initialized at an -address derived from a recent slot and they cannot be closed until the slot -used for deactivation is no longer in the slot hashes sysvar. - -### Resource consumption - -Enabling more account inputs in a transaction allows for more program -invocations, write-locks, and data reads / writes. Before address tables are -enabled, transaction-wide compute limits and increased costs for write locks and -data reads are required. - -### Front running - -If the addresses listed within an address lookup table are mutable, front -running attacks could modify which addresses are resolved for a later -transaction. For this reason, address lookup tables are append-only and may -only be closed if it's no longer possible to create a new lookup table at the -same derived address. - -Additionally, a malicious actor could try to fork the chain immediately after a -new address lookup table account is added to a block. If successful, they could -add a different unexpected table entry in the fork. In order to deter this attack, -clients should wait for address lookup tables to be finalized before using them in a -transaction. Clients may also append integrity check instructions to the -transaction which verify that the correct accounts are looked up. - -### Denial of service - -Address lookup table accounts may be read very frequently and will therefore -be a more high profile target for denial of service attacks through write locks -similar to sysvar accounts. - -For this reason, special handling should be given to address lookup tables. -When an address lookup table is used to lookup addresses for a transaction, -it can be loaded without waiting for a read lock. To avoid race conditions, -only the addresses appended in previous blocks can be used for lookups and -deactivation requires a cool-down period. - -### Duplicate accounts - -Transactions may not load an account more than once whether directly through -`account_keys` or indirectly through `address_table_lookups`. - -## Other Proposals - -1. Account prefixes - -Needing to pre-register accounts in an on-chain address lookup table is cumbersome -because it adds an extra step for transaction processing. Instead, Solana -transactions could use variable length address prefixes to specify accounts. -These prefix shortcuts can save on data usage without needing to setup on-chain -state. - -However, this model requires nodes to keep a mapping of prefixes to active account -addresses. Attackers can create accounts with the same prefix as a popular account -to disrupt transactions. - -2. Transaction builder program - -Solana can provide a new on-chain program which allows "Big" transactions to be -constructed on-chain by normal transactions. Once the transaction is -constructed, a final "Execute" transaction can trigger a node to process the big -transaction as a normal transaction without needing to fit it into an MTU sized -packet. - -The UX of this approach is tricky. A user could in theory sign a big transaction -but it wouldn't be great if they had to use their wallet to sign multiple -transactions to build that transaction that they already signed and approved. This -could be a use-case for transaction relay services, though. A user could pay a -relayer to construct the large pre-signed transaction on-chain for them. - -In order to prevent the large transaction from being reconstructed and replayed, -its message hash will need to be added to the status cache when executed. - -3. Epoch account indexes - -Similarly to leader schedule calculation, validators could create a global index -of the most accessed accounts in the previous epoch and make that index -available to transactions in the following epoch. - -This approach has a downside of only updating the index at epoch boundaries -which means there would be a few day delay before popular new accounts could be -referenced. It also needs to be consistently generated by all validators by -using some criteria like adding accounts in order by access count. - -4. Address lists - -Extend the transaction structure to support addresses that, when loaded, expand -to a list of addresses. After expansion, all account inputs are concatenated to -form a single list of account keys which can be indexed into by instructions. -Address lists would likely need to be immutable to prevent attacks. They would -also need to be limited in length to limit resource consumption. - -This proposal can be thought of a special case of the proposed index account -approach. Since the full account list would be expanded, there's no need to add -additional offsets that use up the limited space in a serialized transaction. -However, the expected size of an address list may need to be encoded into the -transaction to aid the sanitization of account indexes. We would also need to -encode how many addresses in the list should be loaded as readonly vs -read-write. Lastly, special attention must be given to watch out for addresses -that exist in multiple account lists. - -5. Increase transaction size - -Significantly larger serialized transactions have an increased likelihood of being -dropped over the wire but this might not be a big issue since clients can retry -transactions anyways. The only time validators need to send individual transactions -over the network is when a leader forwards unprocessed transactions to the next -leader. diff --git a/docs/src/proposals/vote-signing-to-implement.md b/docs/src/proposals/vote-signing-to-implement.md deleted file mode 100644 index 52f32d6ab20..00000000000 --- a/docs/src/proposals/vote-signing-to-implement.md +++ /dev/null @@ -1,116 +0,0 @@ ---- -title: Secure Vote Signing ---- - -## Secure Vote Signing - -This design describes additional vote signing behavior that will make the process more secure. - -Currently, Solana implements a vote-signing service that evaluates each vote to ensure it does not violate a slashing condition. The service could potentially have different variations, depending on the hardware platform capabilities. In particular, it could be used in conjunction with a secure enclave \(such as SGX\). The enclave could generate an asymmetric key, exposing an API for user \(untrusted\) code to sign the vote transactions, while keeping the vote-signing private key in its protected memory. - -The following sections outline how this architecture would work: - -### Message Flow - -1. The node initializes the enclave at startup - - - The enclave generates an asymmetric key and returns the public key to the - - node - - - The keypair is ephemeral. A new keypair is generated on node bootup. A - - new keypair might also be generated at runtime based on some to be determined - - criteria. - - - The enclave returns its attestation report to the node - -2. The node performs attestation of the enclave \(e.g using Intel's IAS APIs\) - - - The node ensures that the Secure Enclave is running on a TPM and is - - signed by a trusted party - -3. The stakeholder of the node grants ephemeral key permission to use its stake. - - This process is to be determined. - -4. The node's untrusted, non-enclave software calls trusted enclave software - - using its interface to sign transactions and other data. - - - In case of vote signing, the node needs to verify the PoH. The PoH - - verification is an integral part of signing. The enclave would be - - presented with some verifiable data to check before signing the vote. - - - The process of generating the verifiable data in untrusted space is to be determined - -### PoH Verification - -1. When the node votes on an en entry `X`, there's a lockout period `N`, for - - which it cannot vote on a fork that does not contain `X` in its history. - -2. Every time the node votes on the derivative of `X`, say `X+y`, the lockout - - period for `X` increases by a factor `F` \(i.e. the duration node cannot vote on - - a fork that does not contain `X` increases\). - - - The lockout period for `X+y` is still `N` until the node votes again. - -3. The lockout period increment is capped \(e.g. factor `F` applies maximum 32 - - times\). - -4. The signing enclave must not sign a vote that violates this policy. This - - means - - - Enclave is initialized with `N`, `F` and `Factor cap` - - Enclave stores `Factor cap` number of entry IDs on which the node had - - previously voted - - - The sign request contains the entry ID for the new vote - - Enclave verifies that new vote's entry ID is on the correct fork - - \(following the rules \#1 and \#2 above\) - -### Ancestor Verification - -This is alternate, albeit, less certain approach to verifying voting fork. 1. The validator maintains an active set of nodes in the cluster 2. It observes the votes from the active set in the last voting period 3. It stores the ancestor/last_tick at which each node voted 4. It sends new vote request to vote-signing service - -- It includes previous votes from nodes in the active set, and their - - corresponding ancestors - - 1. The signer checks if the previous votes contains a vote from the validator, - - and the vote ancestor matches with majority of the nodes - -- It signs the new vote if the check is successful -- It asserts \(raises an alarm of some sort\) if the check is unsuccessful - -The premise is that the validator can be spoofed at most once to vote on incorrect data. If someone hijacks the validator and submits a vote request for bogus data, that vote will not be included in the PoH \(as it'll be rejected by the cluster\). The next time the validator sends a request to sign the vote, the signing service will detect that validator's last vote is missing \(as part of - -## 5 above\). - -### Fork determination - -Due to the fact that the enclave cannot process PoH, it has no direct knowledge of fork history of a submitted validator vote. Each enclave should be initiated with the current _active set_ of public keys. A validator should submit its current vote along with the votes of the active set \(including itself\) that it observed in the slot of its previous vote. In this way, the enclave can surmise the votes accompanying the validator's previous vote and thus the fork being voted on. This is not possible for the validator's initial submitted vote, as it will not have a 'previous' slot to reference. To account for this, a short voting freeze should apply until the second vote is submitted containing the votes within the active set, along with it's own vote, at the height of the initial vote. - -### Enclave configuration - -A staking client should be configurable to prevent voting on inactive forks. This mechanism should use the client's known active set `N_active` along with a threshold vote `N_vote` and a threshold depth `N_depth` to determine whether or not to continue voting on a submitted fork. This configuration should take the form of a rule such that the client will only vote on a fork if it observes more than `N_vote` at `N_depth`. Practically, this represents the client from confirming that it has observed some probability of economic finality of the submitted fork at a depth where an additional vote would create a lockout for an undesirable amount of time if that fork turns out not to be live. - -### Challenges - -1. Generation of verifiable data in untrusted space for PoH verification in the - - enclave. - -2. Need infrastructure for granting stake to an ephemeral key. diff --git a/docs/src/runtime/programs.md b/docs/src/runtime/programs.md deleted file mode 100644 index f1a1280b9a8..00000000000 --- a/docs/src/runtime/programs.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: "Native Programs in the Solana Runtime" -pagination_label: Runtime Native Programs -sidebar_label: Native Programs ---- - -[Redirect](https://solana.com/docs/core/accounts) diff --git a/docs/src/runtime/sysvars.md b/docs/src/runtime/sysvars.md deleted file mode 100644 index 9f21203e3d8..00000000000 --- a/docs/src/runtime/sysvars.md +++ /dev/null @@ -1,186 +0,0 @@ ---- -title: Solana Sysvar Cluster Data -pagination_label: Runtime Sysvar Cluster Data -sidebar_label: Sysvar Cluster Data ---- - -Solana exposes a variety of cluster state data to programs via -[`sysvar`](https://solana.com/docs/terminology#sysvar) accounts. These accounts -are populated at known addresses published along with the account layouts in the -[`solana-program` crate](https://docs.rs/solana-program/VERSION_FOR_DOCS_RS/solana_program/sysvar/index.html), -and outlined below. - -There are two ways for a program to access a sysvar. - -The first is to query the sysvar at runtime via the sysvar's `get()` function: - -``` -let clock = Clock::get() -``` - -The following sysvars support `get`: - -- Clock -- EpochSchedule -- Fees -- Rent -- EpochRewards - -The second is to pass the sysvar to the program as an account by including its -address as one of the accounts in the `Instruction` and then deserializing the -data during execution. Access to sysvars accounts is always _readonly_. - -``` -let clock_sysvar_info = next_account_info(account_info_iter)?; -let clock = Clock::from_account_info(&clock_sysvar_info)?; -``` - -The first method incurs a base cost of 100 CU plus the size of the sysvar in -bytes (e.g., ~140 CU for Clock), but does not require passing the sysvar account -in the instruction. The second method avoids this syscall overhead and can be -more efficient for programs that read the same sysvar multiple times, since the -data is already in program memory — however, it requires including the sysvar -account in the transaction. - -## Clock - -The Clock sysvar contains data on cluster time, including the current slot, -epoch, and estimated wall-clock Unix timestamp. It is updated every slot. - -- Address: `SysvarC1ock11111111111111111111111111111111` -- Layout: - [Clock](https://docs.rs/solana-program/VERSION_FOR_DOCS_RS/solana_program/clock/struct.Clock.html) -- Fields: - - - `slot`: the current slot - - `epoch_start_timestamp`: the Unix timestamp of the first slot in this epoch. - In the first slot of an epoch, this timestamp is identical to the - `unix_timestamp` (below). - - `epoch`: the current epoch - - `leader_schedule_epoch`: the most recent epoch for which the leader schedule - has already been generated - - `unix_timestamp`: the Unix timestamp of this slot. - - Each slot has an estimated duration based on Proof of History. But in reality, - slots may elapse faster and slower than this estimate. As a result, the Unix - timestamp of a slot is generated based on oracle input from voting validators. - This timestamp is calculated as the stake-weighted median of timestamp - estimates provided by votes, bounded by the expected time elapsed since the - start of the epoch. - - More explicitly: for each slot, the most recent vote timestamp provided by - each validator is used to generate a timestamp estimate for the current slot - (the elapsed slots since the vote timestamp are assumed to be - Bank::ns_per_slot). Each timestamp estimate is associated with the stake - delegated to that vote account to create a distribution of timestamps by - stake. The median timestamp is used as the `unix_timestamp`, unless the - elapsed time since the `epoch_start_timestamp` has deviated from the expected - elapsed time by more than 25%. - -## EpochSchedule - -The EpochSchedule sysvar contains epoch scheduling constants that are set in -genesis, and enables calculating the number of slots in a given epoch, the epoch -for a given slot, etc. (Note: the epoch schedule is distinct from the -[`leader schedule`](https://solana.com/docs/terminology#leader-schedule)) - -- Address: `SysvarEpochSchedu1e111111111111111111111111` -- Layout: - [EpochSchedule](https://docs.rs/solana-program/VERSION_FOR_DOCS_RS/solana_program/epoch_schedule/struct.EpochSchedule.html) - -## Fees - -The Fees sysvar contains the fee calculator for the current slot. It is updated -every slot, based on the fee-rate governor. - -- Address: `SysvarFees111111111111111111111111111111111` -- Layout: - [Fees](https://docs.rs/solana-program/VERSION_FOR_DOCS_RS/solana_program/sysvar/fees/struct.Fees.html) - -## Instructions - -The Instructions sysvar contains the serialized instructions in a Message while -that Message is being processed. This allows program instructions to reference -other instructions in the same transaction. Read more information on -[instruction introspection](implemented-proposals/instruction_introspection.md). - -- Address: `Sysvar1nstructions1111111111111111111111111` -- Layout: - [Instructions](https://docs.rs/solana-program/VERSION_FOR_DOCS_RS/solana_program/sysvar/instructions/struct.Instructions.html) - -## RecentBlockhashes - -The RecentBlockhashes sysvar contains the active recent blockhashes as well as -their associated fee calculators. It is updated every slot. Entries are ordered -by descending block height, so the first entry holds the most recent block hash, -and the last entry holds an old block hash. - -- Address: `SysvarRecentB1ockHashes11111111111111111111` -- Layout: - [RecentBlockhashes](https://docs.rs/solana-program/VERSION_FOR_DOCS_RS/solana_program/sysvar/recent_blockhashes/struct.RecentBlockhashes.html) - -## Rent - -The Rent sysvar contains the rental rate. Currently, the rate is static and set -in genesis. The Rent burn percentage is modified by manual feature activation. - -- Address: `SysvarRent111111111111111111111111111111111` -- Layout: - [Rent](https://docs.rs/solana-program/VERSION_FOR_DOCS_RS/solana_program/rent/struct.Rent.html) - -## SlotHashes - -The SlotHashes sysvar contains the most recent hashes of the slot's parent -banks. It is updated every slot. - -- Address: `SysvarS1otHashes111111111111111111111111111` -- Layout: - [SlotHashes](https://docs.rs/solana-program/VERSION_FOR_DOCS_RS/solana_program/slot_hashes/struct.SlotHashes.html) - -## SlotHistory - -The SlotHistory sysvar contains a bitvector of slots present over the last -epoch. It is updated every slot. - -- Address: `SysvarS1otHistory11111111111111111111111111` -- Layout: - [SlotHistory](https://docs.rs/solana-program/VERSION_FOR_DOCS_RS/solana_program/slot_history/struct.SlotHistory.html) - -## StakeHistory - -The StakeHistory sysvar contains the history of cluster-wide stake activations -and de-activations per epoch. It is updated at the start of every epoch. - -- Address: `SysvarStakeHistory1111111111111111111111111` -- Layout: - [StakeHistory](https://docs.rs/solana-program/VERSION_FOR_DOCS_RS/solana_program/stake_history/struct.StakeHistory.html) - -## EpochRewards - -The EpochRewards sysvar tracks whether the rewards period (including calculation -and distribution) is in progress, as well as the details needed to resume -distribution when starting from a snapshot during the rewards period. The sysvar -is repopulated at the start of the first block of each epoch. - - -- Address: `SysvarEpochRewards1111111111111111111111111` -- Layout: - [EpochRewards](https://docs.rs/solana-program/VERSION_FOR_DOCS_RS/solana_program/epoch_rewards/struct.EpochRewards.html) -- Fields: - - - `distribution_starting_block_height` - starting block height for distribution for the current epoch - - `num_partitions` - the number of partitions in the distribution - - `parent_blockhash` - the blockhash seed used to generate the partition hasher, ie. the blockhash of the parent of the first block in the epoch - - `total_points` - the total rewards points calculated for the epoch - - `total_rewards` - total rewards for epoch, in lamports - - `distributed_rewards` - rewards for the epoch distributed so far, in lamports - - `active` - whether the rewards period is currently active - -## LastRestartSlot - -The LastRestartSlot sysvar contains the slot number of the last restart or _0_ -(zero) if none ever happened. - -- Address: `SysvarLastRestartS1ot1111111111111111111111` -- Layout: - [LastRestartSlot](https://docs.rs/solana-program/VERSION_FOR_DOCS_RS/solana_program/last_restart_slot/struct.LastRestartSlot.html) diff --git a/docs/src/runtime/zk-docs/ciphertext_ciphertext_equality.pdf b/docs/src/runtime/zk-docs/ciphertext_ciphertext_equality.pdf deleted file mode 100644 index 458be4b634fe07054489459850393637e3423523..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 268270 zcmce-W2`XEwk^DD+qP}nwr$(SvuxY8ZQHhOJd5AEPtHwla!>M|KYLfwNY(60jf~l| zdQ{CORS*%QWu#+=BAs6zS%+dFU?8wJvV!8_fudLObTB2L*N``|GBtLAqE~h`a`~?s zF?%OlD0(3wdk+F_hJWlC8UE|gg`)pg%YQw^2t_Yz=;CDgj|JmD7XP&W>->*}lBu)3 ztCO**GXd*AHA+zQGNyLsE*1ogtOWmE{^`Zi#>Lc$fL_eT(8W~5)Y#s{6pD`z%Gt%q z)X)~nV{-|9R)J!69|!>a8}J^=)XwC;TmHHG*U;Jj>G(ebf#cr=f%88Gf$?AD{99;X z{1@f_86Fs!{%s)sbI!#0Z}TRme{uYuVS?#jr2kvoiHZ3?2jX8r@ZST1?LPwkJe7oRx6_an|{G@L}o664{)rzK8;WX9k&Xf&@U6E{)$heLP(&bf?El^z4_v_tSOO9jPvZ zFo&{}qjR4@CQ`+W5Vc^4*0v4WT~l959K7fbTHe;mU&julWOM)DoA%w5o5ZW@D0@#$ z`?$}-!Bn@u2dDT=LOxGDfRH4JO3uj!rZq_hrl1p`Z$$Mck&LyC_*X{iw^LTSX`;bP zz%3dKQcju7#bM;oOM}tSL+Db+AzEGS8$&pIwONmFA;~;da5tT_@Td>U5KDQ7-%?VU zzJ1M9+{;s_bFkGRTi3`xIQ0W&CyA!0y=^0{<=m1k{#aw&@UBqTZ7PFeo{n$pRkiS63;*?)ic#JLK=fci}1O=-uKnI!mH2facEL z%f9n2e)Qf)eXRoTnr@=gBc+Vi4cNz>NJVqMpA}S;6N!ceodq@rs0N}CCx<+y{)9du zE$AG9onQ(JNP<%Y2Oue#$aeT|`##J8>UemO4U9O=cN#FpeMjri%te%V$~+G+i)j$9 z!Ku~W6eKxdsqvr(JbP?@Vrl}^h+twO%N~mM!!a2eNAhDZ@B$j{h%^q+5gAkqRE=b> z2*uP;Z%@_x_bHH7EurPUr1AjprkAKpakC|MB=n2U(Osh*mYDkK^!%RIJn9;l2h z`N&m5(c}k(N?v-`=wKvbUvhDNH25?a4FjY&6WJ@-I1^EL#6OjHm{JKsVjH0fK&g0C z6Tb~%u)HIZzeAz!Q=7bM6wNem=q|AIUOC+TX0hV*G;V%4)+g1rtfS7hZQ3|}wlj)Y zqdwp-5U_iWCb_M;_TVz=nY#Ui>Rlgx|0I6tb^DFlwQi4D(i5*yt)lA6jo?Ta8w&q;%?14NtT{5fPIRWw<7+f zPp;x*2F)+j5;L*LjW=%VOT_H!TVF?UAHy0Vc86&=an!AQ6V+bM2M0d?wzTvlcWJtX zwZT`g=fJk(Ypr(Q0+xBLf2%V+>Vj5C@1^F}Xm5Swi?;ms_pKg0Z|s~W2`46x7+E%2nMIDnmX&=oXGW>?|^} zoB`04>WnDQSlWs#Pqr|-nSSY&7^gpCBQ3uhoZ`MBhbhvM zVHgn_(8!;YopjB%98G*6xPJymD+h7^$Em(%)@AAXw15w%4L;oXN*%Qub0%_bKw%#U zekH}8x5E#^i)kvgM6Enub|FwHc(Ffgdc4|0u30q=BO?+aC4hGgT?Dt5tQ`^-PM#cK zKi2e40?0*{?hC*nkP=jYggAt|ZX(3R1@)*fw;$qXMRcCwmlFh$SH}p0srioYbe%@t zC}$d?+nufbq0V*TotEmBOJhh+kOnK!dis`-O<~gX%>{4+>;R$nqdV6nGGhPW$)1biG4nr?1B*^XTW{8bBAt-#a0jTV> zU2y1QdZvf{p9qHad(Df?XY07&spcZ`GgtA}K9;L3Q{Om<34#m6gIrj^0E$yC{<|*f z;ZO}!)|mMy6bV^4kSt2=%bMoobc>Yw=bQ?{4+BW!80paKMjW;XHat0I@3m8NO`nJ2 z)@K-NF;*4>{obWq%~sbBMgm#zAza|#mozhljGz|%m5qdo3U-R$QHK}+j1wO2sf(ys zAQo9n6r$JV8=UK5{Dq;yv!8^=CH-3NQ{m6shQc^BSp4_5g2DB&oGsm4OcU(_m)=ZC zIKmcHrBUG(C0h`)5g=S&9Bc?Q&|v7Fa`Uo9IvELpt9UcETQnrS^zQwUFTd@N<$*qe zaTab|N()DFxF7s0C;j(b;hv1wmc<})EQ@1Dwvc6n^GBEJ`2*#X<3@Q>S(Pk6Iw=`$&@Zbl4=Lor4 zFC0Q0SQq7(WJ(ZksA<$t27Df47E2U}T2)@X<_(ssO?pHRKY1iL4 z#y@zk#c8W?f|*CSb`Qa6LxKQng(M>SFh)Sa9Bq7L?+x4;Y>P<%I}|X>4ey6we8tVM zmf1m&pGSH>yP%+dq<8Ab83kD7t`mD5C=>EihXqC#uwO!i_Nzq1fGG1!J%{N%YXNW9 z9q?A3q#}yMG9eYn$ks-fBtUS%o>}{+iHL(7^L)Br>J(tl=|%yR z9btTB$D<9SASl$egPfsAR(*JI+J;1H?~#}C*xndihbQB4k~(1ZL!|T6%U`G<7QJ*! z5yq8r@iAWr1+8sWHS>$w!YAWdt07Tom^4f-%cL?)8ArM1u+5KO$}`MMW};I9-HSv@ zNSGY}8eD{aFMF1FTMdasRUxOli0 zeiCZDkCLfxjfQ_7*`0N@(oIyQ)=I!R_Xq##4T^U$tfp!56mWhTJ)>qm?tvcC z-QPvO`l;XQ(EUUaDf-!TL!btN8nAPwg2X`V+l`Y3m20P4D-*yb9^OPJ6?wfWpu&?| zlc#}}{Mrgvs6yB+=NAS`J{RrLb&0dr+n8%d=yECc08!`z%mybs;CP zj15(p1^EaD1oHM(xU9&_tsaZOJe7_Aq(_}Ay_4x!4A~0Yw^#1ado~RRteP__%$N?= zhR{0>SeU^n+D^o2Z2WV+S_38Bbn3$Ib7H?+FQF_@dQP==`xHa=?9mT2Vs{FMY2?jk z;KBbk5e5csuPXEG^PwH{m3Vj~0Ry4|AkXi#6Ok9X9j&v+036ul+~-WK08`T&dy-Kd zA%Gwef@O0SwE>045t<$%o7O>UV3uEc17lW?5Ex{6g{uI9d2xGuUnfnq5NrLKTolyC zxmxdWnrB`-FogsB!$8!TX<{t&HDBhzJ~j$PWXorAG(ELFV3Iz4Ch4b`Y!fkXxcIj* zVm3!_e28QHZlPu!e~UO(xnRT_>RBr5Z~-`4> zW4I`_<7k{##ldJUX&;qNU8sH=gRLwcO}Zhi8+__Q(pKPUW+`vWtsniUfWw$ z{=aKrod37b{689vo#TJ4(OR^(VvpF6eDmrYc9#XR4pd2yj|nI28aHqaz?ow-4a^_& z*PYss+P33QIddk)ZDjLx}x{0F^{M|?Kf~%e=FhHgG}+?PF{PvHKA)7@3KCZj5Uvr zR{8&G|IKC6RJV04k7MuK{eHU23(;egqG^E%HwbaUzzZ8$a3YqpyS}_d6#Fa*<8#Wo zk^AiF>!iPGqYOriYRF4G;PO3=(vfBQT#20pxfDTDbA>)If_L_w!N; zA^hdm6qZ`BusWq<_kA-~>3gp(3wfjQdd$Du{A1}2VZ5S8wfEG^5he)7o&Dxfn2AXC z&C_;Lx{20zW9TvA*-C%;sqCnaH!-8-ocTR82lY$M-~Nl8-jX~S6HObtaHF$_ zBYb{-5f(U3@xWM~-*4&=^E6NBH_?gt>6Wv|LNx=6kP^l&htEN*A;O%VuqYCm0xVjP zD5HRcswbIj&fn|Tw43_AXlB|oqG*HGiz%lgmD&^Gt3t6;x_)Okk)rJ+*5te4<-J(J z3mO4}9-^4^=LHLW*5s~Y|Hq2mG$Gg$awI~5=prOc@o7TwU}$@@c%V%>_&A%QB8lz# zaM?*VTDGL~4OXRhaoFmzS6zgv18MkB{Ji&$5>*-Zj6t$ht4zPJZi-sP1oeR7dW9-6 zIS~iMCpMB~q(i5U5OS_rRoznJdj%_T5TW#%nS}&YQH4)lcrjb^(iD{vFxBcOI`T9W z5ZILF=9B$yYIh0u8VHNv`Z$X!AGy5^coMKw;v#qsf-Dq3gfgqs0>Lg6q<;gr6>t~V z>agMAwxIeuJoL9%91k~P3>?%0Jzn0>cM2Mxw=%wGwDGS5H^;rr-Ojtq8F&oQ&hLl)Hx*K4NwKpnF zkc=@Nk|t=#Jo=a@j!5*Sga~l_#ESL(zRQ#Ju5`sC(=Kr7V`px5mbNMcfU^TEX!1G> zLt=UQ;~=}Y$MMS5$^WV2wjZkiyVZc7%$wIXfM7D3-pwQ!k@7?eP)@nFoBA94sJ!Vl zR{4hibE|YaC8jSV-50PV9W42f?Z(57HXMj?C_*v|b&TOq7{SWf!-r=XIAYQZS>{8%09}=TT)>JW?nY|?ysYy z0x3npwN5DUy$YBb-l%I`Z~)MTwl0?y6j`hUJG6C~csGDcqh=DmH~1lfSeSGRF+n*mq2=H-wq#MmK?v=F&7 z@uxKe=471xF~B%C(E#H*FdTsOgs6mNuRpW-FzjwhV{u3U(#}mq5^~GJvJLfxLWfbNa2Rl#dXw zx$o}teQz9He)9xOI5J2_0fqhK>$6LOQn9b6)I%kw4@KSv)!Z%@v=zgh&OBB%y`jJu zvSN;AS40>Hb}f)p4ktMummeJGb(y`G*<5+%BK&@0zhK44cQgZqxy!N_53A^fLQGQ^ zYBcq^e(Xm$t1)tQSrgYEne6}t83e(u{E3xC10*CYx>>L;i3dW?z~$-UnBGh9l|IwpP1e&>WYTyBd3 z!$(Z{hae_tbUo3|gE6&p5iGHc5E7VS-nDD=oY9AG}19f8@L zDw)rdI1S@R+vRvi5E0EuJIk&xG5`M14>xh52Dpj{OhqUfP~)UiF_cvS#tc>|7NN|e ziG%$DN%LZF2x3xv(2W+u2P)HQ12)CGDQ38JlmpF^7z!2FRAQWV0`LWz-Ub^+PXniR zP!BU5)zi4__wna424P5beevNNVp?Ry%s_{lomw#jKWkv7vUT;_YV6Ge@upY`PwxxL zU^Nf*0<19vj5i46r4}Fb)3Ykn6swj;e=WK-x=_15#}Q?eE&x`<#c^$}o=gIb1}2Q^ z(@IjoaNKzK?SgSpYCr)imTcMKRgA}9?}Vw}pd3PEA*Y1boFJ9zjM8q+l=@f=y@Gwb z28>5Uj}!}=@GTAJ9PLfz^EmDXjDj`p30}Abn7PaG(Ozdin=<#YlQ)y7OxW4Wd4(%) zTwsVZCZ)!}o@Fo|BuOwrjgX!zyuaTGnkP#PY z>rg=cW!YC;v;Y7FmJIuBql!np+h+WLseA^4fueS-?L&+U#2GGZayqLPNJ`B{jWV1k zHY>K-P|tW%^&LrbSL({VU&kSfnj6RUG=kHYy_+=5DYr!f zWou7kS?qdpwDZP4b1VbFVYl z03m@>tC;N*!r~WK{xX}Wy0CSbP$5Aff<7~$+mDG`wmAr>#dUT z3@eY6%*8c}qN=xcRf|*zRtka2h?KHUois;?sZ$tpDCv}HQ9W{1H&}q8{jk(;HSME! zkXsM1z3OijOl-9p z@ENJOH5^T{#cX8WFgIty^LTeC67c=B$_kryRpPdhFQ_C;V?;ZIIpab-lGZ55Pt+J4 z!`0mdLytx-wcVspTR|5e#;x@s@e07?q)^5|u1l*}3pXYyIMZhrw>c z)`lmG`(r(O^-#;N9GoHV&qQ|LGBfaZA-r-cJom^r*o-l>Q|Chlp7}9bP_49#I*;fm zWKNZ@qbUa;JolZvGKXqTFSQ(NJRUtVl?S{V^u!4iXLwUubZS5)5E)pV;lk&$sEC1v zEB<))_n|_V-DBGjqkHRsvSd^r{pW@CMT(N;z>MYf3N+BsSPn9QO-N;l4KF_E;7C$| zoHEE0u@E2EeiW zcfW?R3Ep^3UM(y}-$vO18#)*78A}104K8ho>eD0Pjbwc5>x@Vx`F$YpTv_6vB^+~4Ma zlkDS$^PBP9*D^(y+z#Py@-ZTzZL2SD^a>Y=4Jh?8@zFY&TmP_v2ZyktJ+3d8{rRs0 zKOaMHo25=~)Z0OMP_pNOhTK|;#QUUQU7@ z0Qu|?<3%Ja=_h$vOn*2K<|G3#JbE+)o!2fC+IP< z@MRFZuMeD$5baVbwA8WZ2w-2Nw2P&z*;*Y4{dm=A)1IF9o+~9k!7S&TFHEbl#WiEJooU2`1F!5QZnwbQhJL;e`?^CnfjOtq#X$6qoUmj0i{^wQ zH?!C+&%Rp*RTvor56~FXgL;4Ji^NP!3K)O%KvpHZHpkPrpNQJxN<&u}uJiauS>mwb zv-y5pmSZYvbcetVfWC+L@;9_gP2w-uD>1^mozE!RTsg1m%gc4OtonOAjMu!YA!F5$ zyUojn?)1YW!G?rLec9|)_SyH^uaeO7Ss-kKcVgY$(%Lbw*O(FJ8Dlt}uj@N^`XBg0 z1p2uDR~0id{NLO8|3?3XZdGrnPWkKp9A(|T>~mnn8u5@$ZytEke??jXWJp9{32WZ1d7RdO=XcBi zQLF(3hP!v=fN^Yf543exHdT&aDKkJ*MVt_)=gke!q?1jSA5@x8p)y-OdQ&YWkweL~ z7cL-!+DKN*?Ene0=vGrG>d?}$Gem^hs`_gSvYbWjR!6NL)TygnoKq$vQ{L?3uzCB^ zN?M)VLpj)gxJuVC#tnqw0tKv2N)gdKK{IDhBr)}Eps|mvtk$4Ry!c5bIqjfyEe;u< z--~fTqL1YJ?vMv#`5QVp!M^lA|CRUndGgNqf!cScW!aYlsz?{dt1og`lSQW%EslRM zd#eI*AHZesvpigIUB{uhD-v#Fjik_$j(bLNHU;~M<}AOo8;f-uh{hykX^deSl--m z5Xa+DnG60S8msjThuI(|0^oIRVCQfX2M^#f=x)ehuhcR#njqnE)&wm$RGNJLm_VT1 zXp%@mP!7hM>m;gNMI%jp(jqoszH65HKSRST*@OL#UkFrrzC0=XsaT^){D)l(qj-*N z=v))4S6{u9lUeT>k&Ady_vMpZ@)Fqs-8q})V)RULdHVRHL(5eA+${z$7^%PT94x{x z1uv;sBNL7AhN@3y{)4GOB{_^`r#8-3c&2jdP{8>1gvZ%oIcilOimj@1>uQjjGFwm4 z)W7GWuF)KF=?lVJT=cg^J&G$liZJjLK8Ov?`}xGy&oJk64bCix2>raj=9XhMOT?7!-YDMV77xFjY5OHG!jPl!f)PbC3C{u`rD1pXCz z5>paTxqqTA_QGHS93}!vnC-f#wC#rs6h9kaOYKE6?7tk`nAC>?RLjkVlKgT5V*&3Z z`%euPn#uz&k(6-gM9`^5>Tla)@leJC*h&aB01Qytlz2 zU_7ASFoi|r1&0A6aAYe zLO9HdI6IvU_|3%GNv2Zji8^p57Ashcp=w$E)2ks;6J2NwHr%?jgBZQE00;~R>l^wCPGkfp=81u$i82e+x-7%+;kPI$G! z=DGi{KP6pyHV>au&D^r>(CawQF~-*pSUw8%I_e)Rpd=?$aZN zN%5Z@)KJ^mrvl#~)P%>sd~*e^KF)Pt`$^`Po^#sraYlx#ee}PBZm-D0paQzB=rK$2 zmX;RqfZHCRp{NOHYTX}*c5q-EGOPyF6hd;5#a-Lc{Qw{hBK+w|58d)QFDa zVK)9gN!Hq*3{pA)?w8-1%}7>+$KQ5HgliLng`#oYC=C;tN#>w*z#K6jW~;NUtIc}x11^zl{c z%ZF0XB8b>_0iBcFHCnfxvgHtftd~5<%2yq@Q~O47Cdl~=#WIMYUJPY)6zh^QxSVFK zWTGE&4c%q?C9%I>_r0~!3;ERcLU(?ECmcGXbrq>OQY^@7YJ z;=&vK(MVYB@2y>}*t8u>tt14!FSfcO(PM>sVZweIS}*2@#H3-uz5>`eR~D)2L#5=F z*+ucqc3`n!jH*&vfGdg$JxDq=L3Ds4gYGY z-eD&jssfSEwtPN3fFtprM8K^**l7}srw~NO0`3^;jk3N2T)*qR0`$BGLm^|Bbq{$$ z*GFYMI?xYl6Pf7qHrZxDMvp}e)q}9WRjAzOCv|09l$)50Kn<<*gBEZq8f(jd%RTFF zYyM@<>GRLK$^jRH(Cc|d#+87?&f1uPjlNkynh;mZxq-@HL>REke9Zn>lN!707KLc< zB#!;K(#n`v`NK^CiSc4*9L^@^ay$420D4?p72rgxu~NbVlauxS3SjGC;de8HbWshl zKah+{l8(G9sLWfQs_5pU>p)9AI&k)!QCJ22UqSlZ!!w*U>cp;tC1D&&;O-CSM2Fe2 zG22Dq~AiDzCgf4O`x?P z%`)$h$&buamW~gr>-vOw%OUrOTfXUJgO~8jw?%o&w@>G>>ERT~b30|ujEYO3@A3`# z=`>gx!)1}|?*WluQ@2k6C?XA46^4-nIi+_fH(w^2r}a1 zJ6=Z{H56$?(+w~nOFARuAFqJE3NWkNQ~}}fPx~D9N%Q59+kLsrSOk)aCheT5=d~{q z72HB%9(h(p9o2a)`n8Sk9|Dl79~eX6+ZZOjHJnDbJ0J`x46o*kc0EXS1;n@x_4Er@ z<8eXv)3u390s}^vJ~O;S-%8uanJONd>TCzcvv_oumEb2vTCN9?ZXs%(84k4y!cAHD zQTklS%9&f(>c?9J2J5puZJ=2DOZa(-7i%|VOP``0vd3nR_&Ou>!>+IH$vt#~?9@Yu zInNE&R(Hwp=I~ITBiys~#<3uU8vHKShAMD8FRQmgS3)AXcuF9x&y>cumzH*Q`R%Li zbW8pr3~8wn#)neBHOpvr$ei6UpX0+C8TEC( zzJB&L%QwY-6G@aGe|7SuU2BAU!~LXFP+N$4+ZOfA>nxKw-fmB9Nf-bcgf^)2my*-) zhceM%NhIhoF&yskYY#E%nM+f2b&xd6U3NIJ3z*+q#o7m3oKso)d1fogf?>kGVLM`=()!MirVpwtn5(>EqEk#tpTWxSjc&b!Pu zPdsk~e$YSsiy=W%Ui8yE1;c}}0tG3$`HxkcB2s`9Z*o3M4&P-0#jzsgna1<}6UDL! zwI3K*Lf{^6>L|T2w0g4Upjg5p{*s`e3V#r$>`FJCH$ssNs5O%>2(_@yh{^LE)>wDp z2LN9@Ib|v52oD(_X^*l0&&L%DHb2%KzlgE6Xfs;6Y+<*w!9RI2W*7C7Pd5`p?~#HM z@5$@ET+%cg2=^A*O!W130mSks5ZsM3xG^PRYIlRzud|~~=`J3Y z=8v3!#^m{XyFQ$_528dPiQ`i1_0aB54svVN5B`(HND1Q_BIVIRQ9G&Lwqh|&N`01e zGV^_0iuL_!Ch24 z>1cMV%Bjm=Jp@h4MQ_2_I=tTq@$gu6H~57ammHw z%N%4L=OH`As^{O&UvbTOar6TRqW=GWL600AiO%x-o*#2e3Vz!yQcgEq% ziZ+?T^B7uM*Uori3coUFF^DRkrn4uv3ixydb)l=4Fsk-Gcb1kkVuDwpEq686^a6^+K-bk_=Dw19pD*>$6`YT(Z-eYDXXR!y~Nq9)Siez7&%*hPTV^aU=oBQS%Hu! zVW(shD1vsffhDR3Cgf}s!yyS)y{66Ce=UN^A&G-e0h}a!mCbZsq~au7-YRb@AZA=) zabIYG<^BS7bJ)ZF*MhAri41GmMT>Pn1+$%Cr9stppl{rwiW+A&n7Kdds?zo101pR> z<6_L=$xZbRMttu8Hyokxh#0O@&RP(QXryR@u_TJaJqD6z~0sIc5;CmssK!B7RBcA9JUvJx9@ZbSkzOHDvk%uBHZBwTBL+6)Ozx9fYx-Z<*v; zbTd6^m>`2#=u&yOf0ihWSW*>5X680Y2u7zUqFHQN25sP4bL^OO7&X5WuOH}Z@^!z+ zebA9`DjY{|LCe&-2I#Ob%Nu`100k|VC2pouE9C0j)HEuc!{e~?k=V(cy4C^MFz>_# zbPN+321x`86Ku9n+|Mib5yui}!e0I9?_^$CW*@=!jHl}x@;U*Ocy^dC?kcvk93(K# zD9%KS;S9b(IEeiZ-mi+bcvp^F%BAnyh_K2ye1|0h61XiCv*;rn4x95ypSavmVc?|k z+AjMxy~o4X%1Bq#jbs*bG+Mx4uwfou9i4BPC~J@Kt+asp!(jXlihiGgARL{!s5EqX zAt(Y(&OxPmbPHV7Q^rFfXjm!&8wNdVRIFkb#m*Tq*0%Y6`yo;Dd3lw^m{Qw3kuy%bK%wxw7M|1X? z!^UIb#csEvQvzr>+$V(Q=$!|HEfl=8`PDqW2PL2nkN3P1~BE1dq+bDRS4e*$zSUCAI_< zVIeqI^82-1g=VXpjL~AVzT$4K|4B7RR53>8g6qy6xtV?E@xu3j3z=H#dA! zM`)3(6?k=v>x(LKKtOLTT8_YS6%$p6C;>I;k<9{Ro7u*U&g>XayGhAA!9;L|FaR5F zdG?>qTscvTAC~xD`6#XxNvYiVn1HUHAf`+*7)bn7;)v6QntfZLV~50-}sh)6py6; zVo4v}*;<-LB+{4=7s5|}8dv;)z<7s}k0Zvsqfz%N0?-z&5P0W?-Tp*)Rhf{lB@C`7 zfl{|8$_5{S@XA?a{G%MA!+7A6*knMkXNG_?m)5?89oHZaZ_$8hbvGa@^nHW>$9fcV zkR_ay4r=fk1H;GHj{M7a6%v{Xp5tJ90EQ(Z^XKS%U4vtV=n7U*3y-*9E|KFiT|V2A z&OfW#ypk7a;uDv8jvcc&kXt4)o9IuDiZ;yPVgAu_45d-=?Tl(PW-zvS+#)-xz}~X? zKNk|ksxUGu_YA16pm|WMXv|WLQ)bH4%^E8SRO*BU$Vk&fRS&%e78|Wg6!BL}=DA(% zKYPJ#;~gA;1w|95IByUyI=ez7BS;xSTxN@dHgJlFMUBeowDnX+JKrXJ+Qx>H#vV@U zbwUFUoVqhZMH)qhlkl=043PLUUX9(k%WDsIZcXXep$pYlpBEp!u~&ZWHJekeQBm~v zEDha=heM5Ub6qEsLEP1w9a-vGO5(jWI|G>rB}O-L2r)wU>^@HtNAzIl( zsOM6+5fdVX)>Q;}a3W*akpek@AGREAhs_>lYkq$L-;hJ|9gEpR%9glmh z6sT(eC2tizUkFW5O89yns8vV=!Jb4U6Lb#a7h~bcwfQrX z9S!GE7mr5MpQ}!LLZ}TRLS_gEk2VMg7uGE}^l=FTdZjF?4(e-^$HD*J-CsGS%3+ND zg!5jn0e8$<+V{a`!$gFdB477%oI8Tpjh4kHt&nNtb{;(YWG#_7XidV+mlk(P{bgzn zi8c@CrF|;NCND?16CMqe({vA!JCL9;`tqehMK4$Rf+#seN|uUHKCgJvI$3t``p|l4 zSNvgVFQPFf)X|qt&Cj-AR0a|ES7_)$VePS#PbKHPCV#1z1)gi|^+F#yL zgwvpyM{PO^^udm#_+4=;0zFt>Y0m6slc`^iORmDgZ3ACX1;Oiff~$hTagV{n?Aq{|u9}{nzR7~Z`l(^eHc8=IW)AdO z{{h9pX-Kk>!#I_ABcVDIlLG};Kwan!X-q5^gJq+ggVtbV@lCgyewkXO3h<$z^fh1 zg5>7QC=*E}SamB3U8n-wj6*02?j3w;o@hiL6luufAA*4D9^&j5iSU_#7|YOGq*Tn? z)g)Jk%M3>_jJP+GTJOjcOY?>_X1BlZPT6Um)Na0Kv|XKb@5Ow3bII3h>wR+gM{;TS zc-RKXJIT`F9^7vapsSmLOukD4?Xet{e>*9lmyDBsPw$&?}x{Q0d4tB-+;=r4Y-q7LP&=27T=DLBm8p1_ZA#t_clH0TF0D|C$ zpY{((MR`ANAncF>QswPDjV?;59$Ce(J-7h!EIY2Nu8a0TJFaP-&{(eayly~XchfRm zu4$Woc$Qs97Q|&AJhSSplP`FBnYD%@jBt;cp^25}UO9w70;i`a=q5CiYk^k}z6&~& zeK=1TwWk=zCewExNC^Xfdx@t*_l3@K`^eYFv8gButXz|zwwdr>Oie|#EV!Z2x3U6a zd)&`l)2xi4GWm!NgWw~v>S{Ob*Fx?Gv5v|rQDL{n#r4O;84gS_6uTRb*~G1479h+R z)RGbcP2dhJ=`$bv9d^PE4{(o}DgvKm&BfxjrKF6*u(EYJ15sB6uIrAySNAyXn%y5& z)*yaCa{Qqr$(T3+@&xfI4jM&PcqH%|2G^e2(F#2SG2{sp4NauN0^UiJ$tQL6-+va+F-QLgEy?mf{K3h@$o9Y92EEaibUtE3>-|)} zmrxaAc{NqyB+0RKmvxdT@y%#=Rv=kd5=BEzw3_<9dIC_OO#ma6N?oq4;7k~P3~2L% zhjw(z+duw*YRl!{)#>pp(-2}{T8}?Ir3++-b>$$8Ad-;qFbHM{pA0`{>&aN}u7?kf zR?KA*0r%hjI-B7xZI#^qn2gidhyUxum%G&%0*{o*Cf+5RK>fp1zKLT=BzZAEfs^2I#Zz^jyD6?x?X}sETsJoV7I>R;hW*cQ+(no(WrJ$>;8D zcM&3yD?yCF&NA(&kvbI~YM-N6)P3yVh|hTNbdfolauu5(6ZvWK{jv3e3bFnIPzcKk zpjgMK&pBa!gapepo1*N``^S8=C%QB$f|jbQvT??`9Lo!FhaUgBn~pNDVsU0VwP4>- zi}Q0lKQ1S(=E&#TlMFkoK`~+(1alT>=+#hDn%r2Z>|}G&jd?OeiqgndYzwS?4!4*= zf@;AHe7YZ3Bb-~HM0xVAb-XF>>~Fy3v!mJA+bgIaAd7tvoJbp-h*b{Mj~<-)(j{dw zOpmW^^Wc^i;>Do0f#cZ=Vi~)jd~s*N^ z-jb&#>=|*FohJGk9X6j%HuRu!gTQpM&_07KnBPC?GV&-)P@iy6kdw%OirS!_TYbXR zbZuHYrJBWewCU%Sqrd~LR05$`lzP_pA6;*ZZ`+A%;YMWHPP}l(Dog zv&lMv?M`5NkTY(nNlbvrVo8evXr$>oR_YllnEwRV*6g*eE~GR;vnw7H~mF z1c6NqsS~jXizSL<6~SY}ss$Na95;$ipoCvH@e;}S?LbNQMoD*VVwR@bUq?_i?DB#K zF1ywhh69pSPT4yUSqWjo;B<=tI8VMS(A*8*35oREZIp5S?_T;6g#`b&rLnWh9+Kt8 zZ~(=q0JE7EhiiVGpwM{e?%X07VY0QY3znLSOSQfRf5W<(qemLJ#0>||pBeak-Gp4L zm2Ywk*gJ&E=FWDli{F(_+?*{d*2@6M-Z*)MBs<)P^4M}QoUGBW_0VS*D9-0-Et5T^ zAS#Lca(R?xLxp?J(P{y)_;mWV&`Hia%H@TI3M)@0?bCZHulkmWkk_%8@^=WU8dCL# zkT5JqyNen*a^7GD;?IrEW4}jDtEG$0yrY7#>b5_}vBL_41#@&`g~GA%zy3=*)Kuaz z3^xO4^JoykMd8dxD4Dt~$cc4;cSepsr*#&=w5lj!`16EUt zubRv+a1|ypI<^m`dn^BVcjAD%e6jJ@-7uVIkw z8{1BHY}>YN?bx<$+qP}n&Q7vp+cv&;Po0@t=iW0_XMTI1Kj7`^Uft`n5>rlwDAAi` zrSk?&c?*yW6CZT$_2J9Z$A^}gn1b>B)iIKr(;AL27(_?;9wLp3g+-^P#SGmjJwAI; ze!AKR{>nrCZru!VTS}E=^ySy}|0Mv-6!IdJ_}B0_Ff(9t`DAf>-Hluj_;6bYaIPF! zpALN{bG^!B*auNZaFrRwyE*zrF#5c7tpZd>)9ardL_#D_-t_9&V^-Cd>o_F&Ty_6p z3VFTg`^@&%bU2U>bRbEqKZ#R8@samhVgxHqHjvrR@XTG1q6;N$i6))PxJ4E?ocQ#I zf?D5k)~kz?ZtGs4+o9m5Q)bPxdelRP`!Vc~)a{i+93@|4wks~HvST3_y|cA@d%pio z2YN5;1k!#}zTk??A;z}9@YYA=FpwwlS1YT(|0kq>dv3|_I&Dvg|8KOgB=1jirkpro zPbEN!N?kU5^+Mf^;00*fEFyB9&5_-fLQOy4r^fcmxACF<<)fUwd;2claYr!zE8jPB zt%SrN!5(kzv^Yt3#noEwJ7Z2CJOF6zS%E2=fZZT80{TyYLEcKY4?Apsamp-#JPM3~ z$nS+Ra#r_0XtC^ozRnNGB}5gt|E7Uq`ww`~e-Tao^Nx#^g_HAtEQae|+LN>+1M>QX zuglpSHtxR9_7$HJ%cx*jEf81raxfMqt#txuxi-{%ECKIx30rKP2mB_VNkDW6oH8{0 zW?{pbyKvz$7#y%oZx1XeKR++m=J^1jWbo3`)FiqcJqA`C3nLlgvxDoEgC-=hZcXJW zllx~w|Hh-1HT_`%y6%2^`|9t)uI{(fO@YCIyL#1V(70QHpXcd$jPyF9@IoIFfzMO_ zJue|C%g9nrHlL=U=VDkn8L=l}Y!#S74Ex)Fly*#UG)Yj6Q9h($*?YbpjeU~e%b$lg zRvIhMG`mNIpK|Dz-*t8f_O@U=@j3C0Z#23c7L;j-_dP*>Fy>2|r@I{oR;#nDTN13{CO z0iPIkO6XcBZTqWoe_C~K{YsZeDqHMi4dDiEp}-SPhI*fLo`lq>2{O!>s?&VcP_&#w zSEODqWFw8)KMe=XLPK;KJIP}F!_Ps^5bMdE$u&2~{EKnsKH~|O*}O@9zR)?rHS5=- zi-If9Uu5B>W60FPT4YTKPy~H-!&RGsxO%tCJJxNHJA>|xZCnkGm5jnY>2%e}yr~cT zhF#;@WLbKXNyBumxT;&`9DR&w+1+WQZ6BMU=lw-QlSbB3yWY$6dH^RGnVqd=rrabQ zLz?iz!7ZkE8;Z1U&NE2xxbyFvqrJRT`SQuGj#8lLV7c^$Gi5N6jSn_G+{-HbCOTs4 z#8TSK6KB~bs?H!hjMgXlq;~$XU%hJ9we4t*UpnHGly!wFN1OBjS`spcyKWEX!BluY=M0vI&D! zpsR`JP+`jrj0~tCMw!{DGz?yj2~V~7Kn_*4N9CU^toAz_D7+_r0Us_fa>rJaTv>%@ z4(({a3m6c)J%2QG+B;otDU+)TPEMkQLwAMzEk))V+5zH9lcWD9YrB~D&2#=z3%d5= z9`eYSz*^=7HH3@HECObCmK{l#8Fa0*nIE1>h7^#%w|>Kbc)XAfW0 z;g1SJi6Rw9eDBB=T2c=-Ib^$71)J&U5>$P(>CoH}HR$r_ifV@(iTUalGjF#!j#pu; zQNXz6-akqn!MPxb;<9uH89-9;Z2<^UTq zpr-t#BH&V+&7o$3!abAyebIrmwO*G2+klz6`-SfFu<8FfY^g^kpWdQPK0~tAA)vb3 z-pQIrZXt>l(GHD+UvB)eNwo=`4-b@YE)J8?Kr--UTtdj|%#>?a4EAc$D`9>31lrGT zJG}vpj)bEZnio%dpkd&MR)`Ii!o)g~gZWvRzz@j@p`8+8gtBOb)W7^K&j9)a4m?sN z4^?#Njcj7!1;z(@DgvhNWHpx@74RLNnJ-i+D>p^-^Vcomu?7QrP+D#|5<`xzJ3w=D z0zVuGk92bX3*Kj7rg{OlmW0~9A&vqfU?`5%mfeq)@NRv9_o3fq0RO(%dDwICRH%BN zOyldka{RRD&0G}X3N_}PcBD>u#Ljgtk&NF$Y5;;R4XJU;Qgi+v{$_Y@a)goe?0OwF zJ^-M=b8irl;J`~EYG441ZJ=;_dL%304&9xBrvKagHD=RABqQCBQSKod-tX8m0JMgR zfjl`P5|Jopf!4~q_$-^4XWfrz8|J*m;7_iTd=u59&>c3B!&aN zDfKT03S*i8EOL0$aP*+#S9no`D86;&r&qaoWjI6NdJOV2#o;=3!T9 z#189C$d9~2!IA`@Znl|Pl&a${j@3u|J_7b;a4K$S5pXW1or zEN@zuX>Y7uq2nQG`SrRY6kDQwE!#%CYIPzzc3R)XRI9T73;>ENj2iflp1xbyxJ>Wy z9@rbN461ZUn72n`K~s^&Kug4??&2{h){tOHeOX{41fNpAyyHZho315USG3lb7XmV^Blp@T=203JZ9MNUxMh zB?DPexlpERd6G`j2F6YV)Z)OFvT*$5I5TVdlxj$&pLlv@lR~tyRCASCv&afDjC?nX z&#<*n#AB-WIb~=y(}LJIf{ik>Qr4Am*d{!GRC9-eRQ!r0lE?Q7{3)!V7K6al?B}&p zszaCOa3p^(xA(0LR=Zbh1}Jpg){nb)y0+R+>Z*uAW<;v6ULU@6ym;f1?_7g|R~uv_ z#FFT=0jHBRKM)A#O1P$0hX&`K1?Xk}^{0KsYP>Minr}SI?xJB#tQ#nSGky$2MJ`Za zzG1H7E*bE91Rl4j&GcgJp9Sar*&n(nnn@<^cx31&&OuREL?c3h>C508GaO2r z*(gy!**^IC#3SEf!h8vcW=l&xh9f3RhHKB`)EG zl}`>-l7VLK(>f&H@`|*vVu)WNfdSwq->%i&%S;@kx|`NxLyiu*wuED-gv()%^j2sghZ&L_H5SCC^q*1dKVla$Fu)aSfEu(`6kXi*($Cx9=n6iywp3N^|!Ni52WW~MO zp~MuND_NG$+AR}xANX#0jpUlngS%aXPH$mB_OBbeqYN_As&o7+Q>rHric1YDQD+|e zcr=w~N>8{3<%3{?tiE}gErI4%nzYH_eodkkYy%#8u?=3Bh7Jm`z+} zD-6~QZ6%92HYE6n6pVHihheMoOL6{A-F*1K)<}Czt69EXek&f2eo~Gm0x4yT{}cjOyPWDmm!Mxe*sK% zwzq4^Z*&KtTHH^o_+%1BcW-(hHM^TCo?+w~_r)X$Y^oKdw=pDi)z)k@o}s#5T9=S? z-9^Kx%b^Gf*sBpYt*Sb*Kc|=r6~<&0`=i4=V$_Y(#XGfwZVablCl{@c?>NZFMVL!QVrD&3l2&Y6CHp;3?X}c$J zjdWsnl?7-1?ZcO&>j}Bb4+@tj&*t2UuX`&rG~27bL#fGsfDYrj5wOgN?XF0C@)Q_m zRIyr;&p0B;mK4@B)p9G^v0ExARUrdoV$0YLLzfo!>sb=t2!Q7gscsmr!k2g^lvpHj zr4AhGa3f1Uod84bV`k=A1Y*yoLM0H(Iu&=q?z+s%$Uou4@rh+TZJmm)mwvlrOYx9Ih1EG44FrkF%JnJVP1tobl|rO z@%Ccfgc?uIwG0soB~xLEbJVUJnJ$GoBNGwawDk0X z6C7|6qljcLbpVT%l<3UGvS{`Y|063bYwUvcC&T4P*AZwWI;;PSjQHgtJ>u+%tcVlS zNK(qn8e>jE(Cf$K_X9QuVxTk0gOlu`9o0KPh9_V10+}Mspnd=MY+x$vj%NFgCZs)_ z^?8qDk2#Ki3|3F$m8UV+ajZ`Ch^ZwdKnb;m-|IRx3N8nce|kI)eI@6Tnjt-Gw!}Fp z#YCvZCD4p?i%36gdLYR(qX#0m6OKOL5s};iI`DV(nNPo|?Z&Csl*S$#C@ZXExrF7Z zJ&f_lAac@pF1bC7O5CfJyjhV-`Znd|>=SIhNw$fcp?=m2UOn94qstQN*yZFZ>>gk4 zj&dxEsiu^T0tyEUj0_UwxG%nMmsP#5bavk3`|d$@!KGTqCHQ=Oe7EZxxvd7OVeu5B zh3I$l?DN^NqfaLIv3V7k+4j8)*ZFZ3oQX#&lvA2I?O4av+oFC`xhH4@Ek9NS^ zWm|vfUHv}HxRBQ~ua_%3pAfL57P%t7XxvedNTSIyEnF7pRs-#ude9P6b7;C!b6&+v zL&aJ~#m=d>p>6$CcLxV(FfcS%W#=0q4Xg810VP*J7=GQ^YW$&$R$3%J3zzH?gL9@W zkh1|Mah7J7Q~}r6k@alO?kN!tpqA$bEL&@JyxW`gy56Vp474nP8gO_`?Hf)=MP7yaEmL>k zxg)zBY7SP<6xFhn0cyCwcxCUD*G3pzr@{o@4(XGi9!Ib?pCbjr{WU_kEKajniIbqWhkhRcNC| z3s76m=B_~SB{HEdC$wZNS-zjWC8gsile;+O+vqSnBW$9Nh&<_c-8}}Q<6(O|zCkzu z{aqgS>olVnU?jotTDrP)ds4&p9zqyVgk#UbhX0mF1ojxWP5Dt>Gu2j|MN#Jcw51b& zHxHUB(qqi>)5Og!-shwHeP5y~?MiRea}KUlM>7ZGNk{b9DOG)vvbt{<&NB zJJv7d-h5|x`1JV))$4A0u)nGE5s`NPMI3t;<1C65N>5p`k52LFEo-4dnIKO{JYiIt z2vNyEzdy^-DDNU31|6Dm1wZRe>!8vbGeru`toj1W^fBrrRx)+768DppaX~p1q)lwEK5b4gFMnTi`&>neY&;gNU!~v)^y^Y+pwm z4LuAwBRoR1DaJ(x9%6n&6s6fAW&NOntXO$eO;Ba+FjM~*GR`vwW|(de6s9pu8bt~6 z-hI)_W63}&w;ii89(6LJ2n(ELsk{Pt?(#V~yy~TZ zY^c%Rcr_}d)Pp1HaWp6!h^w&>X@l774`MQW54#Qg-&is9a_KD_8q zBqWdig;IEL?rEA`x6bPC;g8@?zjk|pU>A0jBzdgmfDlgDVCo*{fzte5Vok7vU_>Gul||Oi|rG$=B3Zmx;ei6G~j$8XIx2%H@pC3iXG3^ zHxNo&t;J}kirHgWNINo1^GdAv zZv_r+0bwo=1o+~b{G7InL65M2ObJO~$ta4w#e$mB0W<2BO+9crrQ_gB)Pj{|Bo0Xb z^)47!CzeUE#}l!`;kw&P{{bJbat2&JZ}`29`lANFjU=ft(ps_b#}iAzB{<1niQT5j5kiuZcTyx7IOqXZAsIX)0Xf0`CbQ3|Q5n9(CBB*Vz*9G@ZkERw zD;WFg*(Y6;kz;Prb({lM-9p=Z_WQJ~)N&Kngs1>HkvrJwBS`aPUC%UNOCEZ-1fvei zPtuDQJ2qy&teB7c)fYL-iq)sCfMR)4eX!aH1%#ZlKm~$Q*ab2d!wNRUEkI{_Vb~P9 z;exTems|jWy|OT=c<6{jEdtLbs&F1^dYNkncXoedNBXSE5B4$bJo1v>PHYpW3U@*q4Xd^`G%z)ehoxPy8%2Ne$DB}&?Zxn5L^UjnJduDGDd2oVyg|Cd#d|55fA?_n@FnWl``ybMQ8UMFb^iE#O9M|>97Vx=M$R!uLh>W zyds*qlf6N?wTF>ZIf%1_B|QyPq1`B6RvG3CQz|jeuBddq+M>V57oNlOK)Mb?CvbpO z$F{oC47M*>&9GhTbz|SVAj=wmP37Ugh%a#8ehx7B`=D3&SX7_%29msS$Tx^Ku2mjP zspVtCI&_sgiIOFM-FjwB+N+3N7$aS@xWh7 zk+PEG5GhVh4EUufh76oZ0aoM}3Yv1VNT`WJU9!m(2ILT3P*M0Uu4orV4Y1n`$*KZpe>hnh zql0K`P%@wyZyn7*?Nv$c-GHJfj>Xfohzw!e?cz*zI+(2Uz`dEi80V^)f0PDR7YxKnWz* z9GW`_%aIVe!@*wRPW&7oB)uTSp&{W=Rl4jqf@44zdgW$ZWk}r`1r0n)=ymq&NY5`H z2-ATcZ^m;ib%SD@puM9?R$Zsb!DjW)kG-rhRYhNzw~)_=2|@9RO@=YeYy%fSs0;RQ zS`!`0$@kxzX|t+@EdJ1&R+Q^ZML4QQaD`M;fwZkj@!{~Wu{9W8l?=(Df?&f!cSRqW z>rQe)y5x0O27Lvl0g>0u@nM>qlo#7Vor9?chv1+E*!%!Bt)F|-p|w1CZ}M@1=dND| z&-bneed%%NG??$$Bu_HHJo3y)$2!!j8KK43|<4oJ9r_Dpxh{T&@e6crzu$B`9_kn@P zA|qcRPMOPI*)~hoSXs#pm9KlBt#c{bu&ocrGm4$j`|-WUN6jl|mml^PD06$CxUy=0 zb*+`DmlxAoxfF`+Qn(&uKNQDRq3&jrhej=c;6-NZXqJjhMV>r9clAR(<=0lIP7)h{ zPsh#7lNXPLGn_HjyiP18DqES9$B$*|GN2Pt>NFrk2<^i6sMhiRXsxLKR{7@;H2rbZ z@uB6n*^UC6w0rGFyhI#rbD&84r3c)<=4{}(UUzc(L)72Mux-2gY$qTYMFx5=N4dE6 z&RAA(n%3wJ`wX`_<)DANn%%W-(eMKB(}Fp^LE>~--)2C%BY_JBW5Vf3-8}MV=`ioN zGCS+zYRz8zk09d{^;wTcJs+*2;hMDOVYm(U|t_0 z*6VBbgEBCWc%4ebl}+&GbTw0W(WN^azE*X-b}qY~w~im!Vd(8#`^w~?;w|G1_+?jMV+=M0H{hq?(5Xb7zW?7VU8Tx)<;{fwYzLdJ&%$V$+J}*%DUczBT*w2)Jet2OuyMhFUB`%WI_*|5tW)Q)V zpr?Fu5o5w7DLQrr?dvOz7^VdPS;;}bZJ2vdGN+8rZAa(DCuqvs>Vm~4pW5!JyseMN(uwE51-z?`)4b@h#2r797# z9ze{aL!sm&%DP1T8VXVAI3Eg~wIQ^Cnt_t4xx}FK#!tG@t_72xhXW zpHc3ubm3TZ+4+*uwWX!5QR(Ceum?z^HPf<>?!(zWT8!6Lj+~p?xvPfK0MoLAc`rs^Qv#t9}rN#zRn$Cmjd5DkLFNup-m2#=jEy{sWcBAd;w&|$J`?D-H~dtjXk zR~W9_zx%a+xS!i!ZgTfKLHivSdpA)*$Rb=Ow?$2FdF3=w^}gtLR`QV$Oc&&mbK_1F0K_Bd+^(`dQ2H~#K=$)}I5+QB3kP}*Ej{9v#hGj(58 zMaB!t+a(pWx*`Wz^~y%oY@d`Q+FXV(-d-NtYaHzRr}g19i5@ia)u$Wy3t{jcL?!;x&O2Hd8A6u$ohkvCST0we7UIgMebgfg8K%^mh<+`Ntkf|Yohi~9tC>` z?1SE|{7b=E=rmwl8#OTpBb`Pf;0AC!ktf)pEqxd{!W%$NtfQ&&p5m|9RQUW1Dl^7x9F-zEE zoiFM{Xe()8xf4tv=%ebF!borEIB5{ICkjJsG33RXR{)tdC?BAh3MDR505c=#Lac>M zODZxIy+;>~ujvvCB@IO+SacTIs*ForAbuov6a+?qxa*Z6G=O!pP_zwbo6wx1?li~)ZcvR;Om%0by^{LUAIRmNlT)#pg<-pJ-Yh1 zitiE`#+SkFWXVc>?$RKb{ogOJ8TboMOQCH zcAf9FG)>hvm*KUk=yW8mOr9+oj(*-7t8}4a)1LwzZyG$t>&&j(LG{||QFO?38+|&+ z=ya{r;{x`dJMJ1>o8>XbKP!s}-90oSn7Tf;uDY(r-Lo%Yz;ic72-a;X+(+`JIz97T zYj&L$o>^@)zg@)Mp9|y;zFbhm^&IaTkISyS%q8N&U?;(!8^rzbt-He6zE2y?+BCfn z`G&r^Q08k6*7WRgE{<6EvwbMVCEuKn0<5TfjP@*i^k}EEM_V)>R^z&kPYsqCNaRm))Wp$hl(uMdBRB>M>vUur1o)u4?X)MW3EcLkLGlATpQDx zpV{*3b9FVSAJh`%G5hjz+861^uK0tH9>P+WU@D8$m|%ibPnUu2Iu@=nJxd)d(KUT- z9VrX_%SKsk)F|#-@;mDH;B>T=GWxUnzHZDIg8^yvb+nqShpAGafDxN3Nzm_AIix|h z>o2bA%3{8n%f%?9Z0@rX*KKTMwIzAtKV5k_5!bp(BWCoMVm2pE74;_@je*$x)P1Vi z?4A4njEr4^O|Q;@E!ra5jrr;*GAAj1LeT2zZ`PT~5KXHvenY#}x+&ZppnkyClvVT5 zO+NiI+lP`IWOS&@lG@W}MGKrCoF3rV6vMy_zVF3R6h|OB`qTNJIP^%gC~)QRunQ7* z|Kf+p3C-Sh*#BXNTo`_Rs)DY{*Ks(x5_XfULX3D)6g&Ac;z@lh@caYR$ncuBOp?e_ zRGB1_#xu4f7yOcXI9rb7)e704zo5E?Vrnlu%`*&DU5laNUB?5NuAO-I6U@hRVVdBL zfSc^n%lSK@=&+AU?U4WHp&^%sQA4sWSU0mXDe^7Y_d>~;!qbG@HQVE+T5IDkZRckx zT;IrG%^hA`@gJ}i2HUKFXo>@Ef6M|n2 zvDitmpBV9T+L>)UZuDwG)BmkUZ$@ygs2aSnLrOy^QNs<<0m7z`hPENIa4EsAQrbL7 zu;C7VYa6t4I7|q`MhfKbe#?Y5teiK9$-(!e?@ddDv@h6U5P!%R*o-caYR2-C|1`9o zBa!W1b4S2G$2JIYA`pEbO38a(oaHb);ECn6G(E~t1Rk3c_X)4!=3KyAajw4%+2wA% z<8SgMCumU=q96LC+C95a$VmqpgG1*-c>*a*xvU2?s(0D5m=i{FV89awb zDgl;pW#P1owZEkv<8YG4G*tI?aM?f+10DMfj$YbS*Vr$J{Nld$H3Z!&NVH_#haAU( z3fPGFE$7fSm$ZYA0AsfVn{)jdgkbi$@B@ik@c5`|=h|s{f|0Frz9ZtC2gyU9*qLxh zv$9Xw{L*6k*D^sXxjozM`ry`ZenaBC#{Md^U2O4>Vek(3qets*(huY;0|os5*)K7% z|3}{3|L=y9iHV){f7}si$vAJaBmFahG;mlc+ctO@Gjp|AqA1AciKhRRH%E(XV_O*( znRHCjClD-9PSRFRq3sMm5siboqIc`}^+A%cLrLr5Gj2V-4Q@WQ50Od;G)71sZ1r~~ z43UiSXFm7Gp-86+-x(k5CfH1x?lYqOPN*H4pWns$o#wFq`ne=-c;jKi*S+nB<9*0vlrB`;}6HC9In+_FLFwcQ>8+6Sp*HBFUU!F&#yZ#|_hAyiqL ztqlZ~6>w0vF;zp|8i_)f@(*gMlB<02aweamm%NAsqbJUcBZ~7M;J*m%u+CF~FI*^0 z=7;#MO>Go;71wNGPj9w}fax{~bcuKevUWL}K_EmP7j)39k%3Pe*Zlcn<>*aD&13k; zi;nmU?jQ}>#XLeL6Ea4jIQf-rl$br!@cy%RFDuE+h=8J}sKafl#g3RJ)l_jo2 z!LBO(^cI`Qqy)K1mpx93^7$x9aNX))u_U16f|RPVZ0&H?do81ZFi1*TegwPb9h}dRy1(|?UeOs2xE?M)lfvvztR4^At5Q` zi-iMJKHS9nU5+F>jZ(No|viK(r+cp-SMrd-TiHlUGXuWL`b{GK_2rYg58FBH1?>kBxo4?3oS**1uMsZB)rwTF zCYq{nAr~&UjxMwh@|q#JnzRt6h@;6ZGvyRS;g40pg+k0oz?80-pw_hK$O{%E8319C z^aA8a#M9Qs^Y&vio9P3fm(Fq*?iq?s08zXR&9XvJiTg}_U zBOU?{F7ZDapeUT`$yqTu;Dt1<-Wiz9pB7Br*=E%?EJE?$-u&4!&5mC`R5J_u*~4sB zhIsqRBh+GZxbRmKE=9~POOn%lIJgR9l`ra6eHa#f0*IcmzfwKEG=^ z&^soI+0soqEsUha+Zz0;hM;f3eMEP9imJ=f@JyYA$+;L|8ZZ#kJ%vt-)V}V%Zamz0 z2iF^O_cN$DD7s?pMf(@hW%ie<0*jpsm->ITz0WQBP=sd3t(D28BHB89Qy5M_y%+mN zpBTT&&nokBQ&Lo)kB)^0-FN{mj0XK#_`|KQn;A^6P7~wf6pik zRg?R-t3>L(tzSq4A*L4)t86dHhDT`;RQyF3b%n2*-4?o0A}zIT_PWr&LliD=#9>SS zV!_F>eCX|CG}>k`I?Iy=s874y@n@~si;`B0HtM8`p@g%hQt8pGi-vqi-d;1FJ1{fz&Ema~}F%G=ghgu*Bad*!v4KR@L;tN&L(HH*pVICuMZN-8V-yP)$zrz;$s z*40!Ls=gC5qUW_a&Ul{vk0Mp+1X+rtR9m6W{IQ*qVa)GOZz1ek(<0`k1eJj2iYC(Zi4cK zq6!)Z1#+>y=fTv9s9A{ zDQQdKo=-+G80`lmWYk{-MhifVH>ihJw!cekFS_|KfKWKxsw-dV+Y9mvj`xB~J4S1V zUu~Z&1`^xv}OzQ z@e8F0T5I7u5x?`dsm%qLW-(sxru8-2S!q{BIHJ`sD^B0Mw5o;$nG$sUgIihF)qCXa z-Kq87!#s$r-l=?XKU|~F?1ZWIM9W+Pf5kB;$4(FuIrH3f@l*JJ&u4EYjDG2~-sWDT zw6nci1=stz^D}>%+OKZ91j3&Wx$&C}F*O4_3yNVWibA=uaq+}u-2K8`-9OBre}k)H zl7eWszhn-$99w3wtGg6#H;BY_)K3#eJv}Mm{5_m739%|Ril?&c4Oxdh33)2(Khfph z!GG;!)G32Ah%Kv{qg1lL(y~vY4O{50g)h%i5zTtZwz>4TlrOLR_p<+WGTjV0 zYuV7He*EiMNo7i zoEQxH`cV&QD_ZadP59p#ch-p%1Yw@i|6Jg{WPOWxK2b%&bGpHp1_fwyh{(kKawqP$ z#g{hC3U?oBJ*1z3qI%5E%*^g%kwEeX=EcfmdufR&FC}MQ?>7V_XaJx8^N(j{{ErOa z|8G0PM99R+`cL`!UvdvKArlJ|3(J2!|HaAv*C!zpEAxLcfdB86A@&7aQFrq%I1K3E zHj%~Q&CLzU79Ng8N#a;;!XCyJT}L-0EIw-oggXdP2Y=j__uThS|3&Xb)eT?SeCL>M zwsiZc*6>IMM_fgT95o)9vhkcTHAIrsOTK$v`85Rj!aVgkBg zD0;ye7=&?{6XYHoJPypGT;Ib`u~6D5yIoz&S#K16WrO?d%{Bx-*AP!J)Od zj5B0FSuz>a*~zy6(OIbN_Erw~`Wi1EpVllMQH^{ZdlB3WkU~N>oggR_aF-WQEs%c; zoFbb$*v~c&4KuvpGzQqaOyRl7^@-&T3|J2gu3(xVn0QUIlXe{sroA&aAdrgdAEEDk_v-5A1fkjA2K)r6 z1*-3#;7fFZFBp(r&9kukx*G)$o4~~dqDd(IH2`7(3PJ)v{Z@l(`GM`v>!+E3HJJF3 z7C->{B*+KwWA`)9OhI4meaFA&OU#s!R#%diPCdC#1JtCYxjlh>v^+e3X>)k5fDjP$ z?0|MJg8_gsB^Jo<-{bv0=>KeKK!N>JzPU^O(l2k>pilk`P@sGM#+BT?%$QIGfbcW% zE8eTLdVO5Lzns_dX@Ec)0Qne@dIo4G)^djo7kGa1$NmDG&Eg3m?D*^SFt2W3yTYpG z=(z)b`OCQm`q$Q#BnPhmR;W5M`)`84*2ebxjlqOWg!BYy*b=f!uk~HtW$M1ii?{*? z1lcH{&f=@V2Zjdce*9hPH%%=8UcQ2QT<_&DZ(Y2AY-I?WbCc)sG{?TYhR*8hPL795 zqJCoh0EY2PZf)S+9}}j4SoEu-mkSUN+c^S*>!YZz@yLe}#zpaJ1@X*YFs_3kJ6pI_WPfIR^Q3&I6h zi^v|U}j~Pg=knN->@-nl7_UvV9_-ge3Is zm~hlYgzrr#lea5ha4yfgaNS!Z^g4hvta@VAAeiOFZqB?`IU4oPW7blN6U7WrzyoE)|C7uSWx z998gzKZzWKAATiNsm<~=WjIu9v*f_wJA6usR^(?wI!wOjWS3m;mD{st=|~ck6Ufa1 z`;u;SLCq_vcmtmvkn`$>{B-4T={E4K@~t%=2!PKHv%dbAF(}fq#)VafJ+l1Px3JNh z*xwoYjoHPET?PVCm;!+tBf+8Che8MI=PF9>J=YP2Ds(sKdJ|}6mg$0TFDbuHSu~7T zcIy#mU~<5c&dSl(zlNhM1=da-@Q6q^7`s$1Ue&!Q&`6g+jm}sU)_<2D8_`?{G%9o` z1mY+?Fj0=NB-WhI1(vBCk{W3Z@_Y$6gXJQC>$F5}!Iq-J!{IH-t%c2Io2kEooOY@p zXXDa$hNGU`xo@Y0f?7oY)IpcrxED$FE-}G4 z>GI+;`K@7or;*xJwXl)-@}kWGpcJZ+u30-^^zWGt&0tb;}K9;)OG!$lwB z`)8eV>q0BQKPO#M*O<@DwV}RMF$pOayvI6)8b&Wp^Lzu;=~t0Ux}D7jVfFE%aFaLweN z@m%H(;)24ZwY!uZR&+?3y^NnL5Q%~y&vdM9eR~k>2d^+d3ggCdPx~|C*VAO8W1cEK zBjGZGT1|;7A#{q>9(HpVisk+`OAFXhuiB)d;{0Z^i`xBgSWHD*k|%q_@aH_0{T&*A zRTgMmq|i6hfXG};k@9mtqb>lKY@-m*mxT1oJU{GQH~7ZP&DM$X{h2?;!MbivlAm)= zl3@I)>q%LTQ5=Ymew4YXXdDC#Z&<)qa=>)Q+1_f{ohP0Ht=kKPIWeO2 zfe7zBO+ELx`tj4Z3u_%pKth&4^DT|>_5@Hw_|A+a-)duBKM|%*1Kj__|E(Jp-ofZni%MLxsT{uV(*P zE1K<9i0R;T>7qBp8p!c0TY{K;IhXR=N^STzg$X8;WDC(@p|!bC{^HHHA%9#K=eROp`80cq_6=&}ALgyO{DY1wu&vd777x$=zSV=k za`vMcGAG#eV+KL1ar9Dqa@z>s=|u9##wZ4E4+p|r`l6Cp+@fW;Xi`+d&EDy>|8;5` zxR>&iM2DTDkc#F$s2m@rXMCu)vd2$_Sw&7wr^rKdLUyL^9(avuk;8W3Rn=gDEd_46E`jL*iW}u^nA}MnA$lE?{02FmuMKklK9}0S(~-n0oBVSG6R8g zIX(y4&{+26;NeFTtpiSFyQRxNK=hu;Hi9^%JMezR>99&7T-@bR85c9BpuykgZgLh! zX?t;%x13#Z_?4mc)o^sjc-~-c9&@)HT!LH$BYJ^P2C~J-kH>(#1Qr0s}LGO!;hisaJ5OfTc-)t>SE=~W1V0p zcRYVkEIS0(9l(i@B%-5ZQQ24$zqMG^&ky3}bu)awDJs@27SKtidl9KZi|ul?vm_ut z4drZHS%-$~3VanI@OLQkJqDlOs5|#jwZ88-N(zN2{+oO8-K`NXtpKgqerH~h#JE{L zg+h}E;#9=5&3YQW3bV|%wU%SFt#^)NcHgRx5j;hQZqv}Yc7$e&t^6=?FB`G(X{Dk| z9mzISCz0jwGt)&Dy&t-6geYgCTZNZ7rHiXN}P0bS<`pGK^@onR&SB>tk&eye%T)IgSVT!}s-3d40K*^_whWu)aPgF?zITT5|)c1Z%UzE6zNR?d3=91+vCDp%;(gNA=dp+D1 zIuo{Mb+A;Fh7dOFb|wCv5*D23d5nfuq!`tgFLEAlB-x5Oa8{_tc@R>mhuZqCcH|<@ zuGn^EvNeg94fK#!w=*t5D$0v!2q~{GJ|if>N18{SO6hM+a(9MLB}Ew|f@7nmROiEF z0_~GuPiGk7uPrWiERpZUKm-?VhSu{b_A8Q71%nJnMEWP#;#b_|EJ2#W#(!u$XAohX+5&H-O`~5}O-767y5VTcA58%H5uC zdi=zgF7`*i@w0x@TOVc}#+noORo`l+ z$M>QAy;ru^!An6FnGWY`M?)Fa=45@ybOBDwg;FexKk z{VGM&K`aClumTD`zR#2GQ?!|^B@DDcA~yp7YHa~in(@+@+_$j^&Oj5pR=F`wQ0bH? zd<z>!rMtR|qTvzS9)-z<`@9f8cqe+skWyOf;-5G>>16d`q2Xt4UHkpz%+{aWcCqguFLcjb4Q8 zT2mCr$fkWe*D*dw#-FYYS;4VFCav<}W0fXK334tBRzlxvJmqpbAg?No)>YTsyzDOt zr~aG>9^x6&oVUsuKmJn7z+4>;MuhnGXD<4&YJ3ejR(6Wq=80XzF{h1vd?wMCgUoi{ zeL@-{o6p9hFeSni`Zv%9acrgd=l#u^QDzzy?Fn9)7mt?$8Em3me>g%Ur%4|(95pA! zL(I~6E0w=@`Y*j=HdhGC4}Odko~t7s+9hn(MY{3YV7T=WA1k$x+Gyr?$l3Cm%ywR= zM_rV5GA%};5(Djg=&NPSZwFTrFE7ce1xHkBSeF0VIqT@ec8#-$pZ|rhh)+aNi1Thb z+?x+S@Q~{9;j^{T^gOt&N63u`g6xlY4gvIavmj>5$YcO}dOMiMPb18unSb$h>R70? z(EuVqwus&6B&R=v+d+-chpIE^ivLicn1AGj^-Bl#@k}OmqO> z8*+!olovUsv*N}NGB)<7J>u;9kLjax2L;6|_sQTWJ`Hy`xe=|Hl&=D zW3#;VOeK*IE9Q8yl#oh}ZG@NkTt(pAeDDmUc)57Ho6k=!HI}=&wS2FHXi|z`-ns_e zgksw@C0wh~a7Yo%>)ejs-%s~ho+;*TUWFA<)tS2F2M6*tVb7Ijyqw;$UlHvC+Tg}u z$!9aDiJhHS$t@&rnrIa_C)bt-!IxNEs@aNLZ$hvpTC-CDS*(9v%2pY-Lh7c+)|VZl zWI`MQl?{6{3cuHO2|j!5RcO8=KeUt!g|@JME<8>GTj;;t*CX%k0cF!BmX%lX+OPDVD8P9 zygoY1m95nhe>NA^IT;Ca^xDO}d3rfws@|J)HtEe=*Ss=~&4KxsBb+Sr{N0m!cO)+GjBeoQvv7L-y|S&IXhi5YZKjh5GCnQcanu)zK%sT-;J(6HD89oZ zSRX5Vy6!JV2AgERjaHNtfFD;fi0L)-H0{woX{bx6;8Z(Ll8Yn6d$nG6aOZDfFwT$> zhEW?$3F76(30eyr#t*{~1$}6hqPs4o2n96Z88Ii?IqDsSWLS z{X-I7p6xxdis`>sus5vW;*j~!gaTJdJg$#^_&J1t{aB%h+TeQk%K_I?3A)=Bb@-VA z_&=(wY?&+N+UrcNzKmVeA{nQa(3S^xNzCtduM(;1<~trZN^xG8#>aeR#2J|@1A#!` zp<-k(^gVj0xq9^q`N_+6t_?nRo{L-L{*u*0tEoX^wBckZ9Viw(&hpySm?WIOR?nMO z{e#S>liX1FvCw@e;u}F6Mete+8h+=@ddCUmKq{EQR^}Yp#_PYumj%kfPp~imV1}yAQF^`JrJptYfBSbl=7hT_FRv;q{H{2| z95R`UcivM@e0m2DI&CrkWJ<$I!W#u8XjKJxDdO5-MHf%QK0I#W#8GDumzwTXRrfzk z2RNmOS88$Vab zrp-Vyr>7knOP*(e&2q~hm+WMABH$CYr6a)URkepJe~1F}Fz$Fc`p=Fgg)nW#{e>MO zyvT~-n6lRR)tBYy3&bnxx8$F{(&4@xG(`_3KvU)U)~re_urqCv07Zv^qc2-|euuS| z#HDWx&<@aL)wnV2&;%tQtSvJ7*$hlLdeu}OLu7^VFfYCu?R)rwI7!-W=QAu9 z-BBb;6^>fDW-GjHOror8wi8Cbm>NDq#2l3zZkLPp{iLU9{^@o$nR}MBB(E07VI7kI z)oK~<2$F?thot|4A*84aA$@9``Lqs?8yiKzRrd^*0+(0T6U9>IBDG7?(>o5-@aUqa z&FqqbQYzEXvu0y~aj;CEA+qNovy}FI73`IIc~Bx#D=x6xLdxm&X+B({Q)q9WqEf9W z&K+2497QWQ*Oqr54EV86s?vm6iY?=wST73MIN<9&1Z#}drrml4jX^AnUT&mMEq&iLJ`eZw-jG6|~r zVmYODP0dj9%+nV&m6owlx=6$6wcoLL4AS{}-G37Mqk`oYA|ZVQ!aeXHYpVc;*uw9IC@<((Px|5mZztPx+x(;wRQ zuDb3S-|)-{b!$Y?WF?d=8V8r|VWL$#-^GNtFSHENo6);U-OJ7pS)z5uFjsURc9k?o zKJdai1B=}PoWR$-TE4Ijv-NH%QPu@T5hAh?iaW< z!Tn1u(&Q-vPXC_``Z+j z!fL{1;CaKMhpjp!5NEW2AI;mlLa*lxC+n||nRBuhso(1Fk+Ymp7X`=eL*R`>jUsQE|km!ot&LuG|Ii(69 zER|>;m6vEG(hLWbYqI{`5%D6KAEL#_jBBjh{x7?zR;$%kk#+`dBsM=$e=z(-q^dM; z5c0M*%vx{BRZL^y27O!HGN|e?V|+18d>NAG&@jKRw_ah1Rr4@-REWUp_&%CZer0f4 zQz$YKYdSY9eMvlHED@k7ZJ;5xtH3yIgc0e@F~Pz>41v$s0upKHprQJ6)NBXj-$+irG7Eahz$C zW#T&PlMj??oIAN;tDMG5%qMKQV91^MIi<9H`MmexfMbE%VBvGf<^);JN@tMoh3mGn z!&?Wb>Wg?yw6uOSlo|Ma;zf7Tw37q{u5G;6mXAf#D&iOMIB3!o3d~J|0nuHI7Ea+M&+S3XUXu}#tZzDFb%gf&mdOp;r@?nx{UP_+G-Ukj!M|}Z3^;p- z-?jK!0mZU24sdDpG$3WaWgv<8pY85}+By4+2)l;nmKjm-(sGnm+t(PBc7VVBe*bT{ zSgQa_$CbdNqk`>VTCNG-p6PPL$N1m$I{+cWUgvpg&c~foAKtp1a6k;{aOLv`(Z9&C>>X3;IjYT=rwlBa=R%Q11W=60Nd^jS!cCd(suqFl5^Ux49;7O-kXF*?TfMGlBo4kn{_g zR6AMnx&kHjk~QoLmzu{FbaecK64A-~!#R18bYiozWf=4p%Ye6C#?&mWq3>D}V;t@N zYnsl`B(;QrK)_G2>A?PpQ|7up-!So0c=2XZib&WB-wm3jL`V? zkC;i4%tkbE^F*WVU$;N1lw2OguH$2vTyBr2ReU-#Yza;eS^!7qj~k;XK(Wk~OrW@U zPd7KNwknS%ZHJ=glNt~!DHzU14iz>voT|%3K}2sqYd>5=;4yT0=vNiOz)C_Lkaue9 z)r>w6rW}WYFlEnxW*YR8NU?;$8-bks#n^x{I;Fyc&&JPFB%fjAtPqJ~6rJ!rPk%L* z8-Zc`TD=^qFP+`;n}7Toi(p!5M(|wJdAH>KEVks9qXxLbf9a*yF+L ziHYe5y<7#IC-vs&-7+m{BHoHdPt1kpPa!Gnp8L{6)Gol1Px21ksJTbL8Vd@=_1u1# zc_?-W(XJH)@z3sO%dU()iYx+5Yv&bV{0dYJYLpa2=)rjCAnYDED&KYwiE7V-?^$@0 zPjtNBLgr!Yy2tgMx>Yjs)r?VE*MY3UNdnVEjc{(JG8Fu$f49``SA3E8UwP0<=7vet z6}%xuq3TI!JMx+8Ge?|ZJvLYt>^`VG^LPP@GS+f20b;lxUH6#==GYd z1}U2U(Y&Pjq}++gKrerz=j(#RBkq}ORS&p>ew0!v)&HJi6S7RrDPYE3i+Y$J&WgWt z6@=^PL30Yw_&o>IkYw}HHPYurpi^1oakGEeviOxHEnM#%7-htS82ZTZa-d6UV1x98 z4u1a_M=rNQO0FO=wnGii;3EGcxv80`uRje#9}{^+U@WFcwj9e3Zza8~fTeL0Mg4{U+n0yg zp21%aF>rz%HDKl$Qm&ydMZaQmueqk;X1pdaSB|k91YHRv-%d2n>;mQR1)Ch9k!x4i zm83HEwX>Ajt3*u;AxPol-@J+M+V_J<<_cV~eeQ|m5Vs`qy|0*x|qR}NuRu_hY(S}s5 zoQ}eeAr+^pSEox3YvJdim*p%NJu|bPz?3OMnugSovGbM54f`*{xxJ$TV=tHQicN5-S#Q+G;7f}9qZat}ZL(tlv;0;{4YYuW83xHj?xa_+eW%Q_E z+CNL+Lzs>)xz$!@qk>7-x6cTGacs)SjLY+2({7Xi@@8FY9l7?VrYQ4@FBq-zyV`6T za5X(MafYT*f~`ia$Ko^9eQ)ITdoL6w&Zx__VB6(XBil!(xOuar z`_8OXE`%aoK>v`5IoMi=S^Aqx%UDT2rP5^rB>wt8Sjy2t0+s)MLlI}4-k6vTB};o| zwOn3hxtabAjh7who;R8u^CXWeVR?w~0UOv3F`LJCdJ70FEj%i4PfSBF+Er=La6-hN$vNS*ez;(UER+Ak?Vn z8Gl~qs*dhQ^oE{^Tqm&xU^e|sz1>AyMcEAoTgxA#c0Qm6d1OAj(Tt2F?a0*h1-7N7 zUE#P5Bns-ZF!mbfiSE|J_}sw52ZVvYW~!kd#n4sLvY~{XX~<{&9?4_|A=xPsEvK)+ z_rrkShd=#8(;PkgbCZ^>guq5>c3^$29JO2>BD#a@*Y~<=mlJ|dXRofEot5Dykh5f) zU{g`-eDz|MG;dwV5hVl2xgx@Fn_w^xdq;Pi?dS;tmM)XMD)35D)pdUgvP=MipT=pz zZ&f_ajGC5(`n3AlOaloezb?W^yD!+FM@v_&5O_(kaj`%_v45eB!uS|frp>OR3@MDY zc*%xK@uqCr{s{{SF!*IXmWaAbYt!Juv2?kI0P`8xpJ?~adu)kCl04-La_SiIOj~pt zI(n6BT(hQ7#0lH|V_F)x=G}*A#>~6`45$|-HF$`GK_bY3#*9jlefyreaW=-JP{38) z8(ieKO@%^KW!9_r#(g1;{B|kfw`aot5|t1_W?wpi_2#_EVI7C;Y`nTKmTNN77?mHQ z!9DO!t|e8SnzO9}-A#5`Mq_9+P<^20!c6FQy99@>sP4=p(Sp*oeTFkR@eN*MgJP)` zLtzC8S>C9sbR?Esz)r$#r#cHQcpwddmj&^@UM`r`FN1+%+N^#rfm$wJt@m5V0loxj z*`Y=ZLH=)A_cG{q(yieVWL5A!kJsDxq%Q}ZSssn0+R1`_-sCDvea$R)lxu?;zf?h) zE+79?Fx0^1F&cNUVi)%D>20Ew_=)q(%Wd_tj~8HY?OKjd0KZ7IPv-KCHLc!7FUT z@b={nj=dtuKJf!C?+v1IO!A*vM}YUJcWW>psy?0Q(-TeKOSX0}QymIR3M@%Mz`X*7 z_O4QCZg9(@1f_nj@fBM`t{pgH6!DW8QL52i(nSrUjw!$Dqvwqmm%7QGbM|p+}z)S>%i1I~sbPY^3{3B+O2G?he#}FT; zl^a~%^XE3-#?c^fBVG_C@DNf{wJjaK4%nX9*Xir+_xeoU=Njnrn)f7vzyIxcRzA&G5!Yn}`V8~<#HM3wi>BeKx@ zYv7WseDKqc$>+fA7NR5jIY%U%^SW^5Ei^xsV?zz!d@YLGi%Ja~Mg#edn#*m-&8YQx zg7xueD}MbomIr1-73cQ0l(d4LXXiYihlm1~k$K`Y5jb}GKbILrcP5@!6v`-r;>0v3 zfMJUCHnKRl?X>d@oaFNr99<gQJEMylrYz!L5bEq-d2A^UO&At7cSn_3n{*V;Wu?5=x8;T8G$J!&;f}fc7oeoAe%F5;GS^PMFm#BSZ@! z_cOl4p27scjbcmdS z_~?0I89hHYy+#l3&S=!HVGeNDcm`J!S&| zBZgCGRcvjy#PxN{XPR~Toij?+J-pm6G`!*NT<#cZ8 z#uOM^Xl=~{94(5^1Cj9X<~Q_ho*T7CRr}d%KBnx-#)e@9$#jnG?QxS|8fGncCuK?5 zl}8e%s66d=t@nW_Tnds1Xo_}7lG;?wW{2^T_oZw=+H#>2dzLaFKd{P#BUe3C-2UsX zITDhgc+xH_+va7#&VBCHo|2#Q7$|mP54iiTesJCuX-c%XnwH5D`?Rd5i>QdC@lF>s z3~!`up2pK&g=*qfXYz@q|#h2Q0RTP6eMP&;JHsv;hCa9os1O-}~1O z#)Xx0V%Bm5yM>iJ{z=`3lqQHyD9Wz>L-t3ZygCu4Ru+v8GA)wY^Rh`r5MX>BHULqN zrGVLB%wXdcf`fa5HN5PFOU9>H2l?Igk5g7&5kPS<%HCr$;h1J7*OqCQGsc0daq0pM zx%~0XoJU1C3KasZLTio?M}FId>;T9JI)CDRQC4vNkQtX8&QIBGr!~2p<+)XD=y$<( zT8XQ;zHGLh3TXW2fHekYjyiD&qq6xag~%UXCIbO>9ARsgEarKV5?uJ?16*Rk{D553 z+hLJ1*G}9h_9be9W$a3_;tgKwxF zu=XW!n4U_ChEfPDMmAXYJuZM+EVRGRDHakOlP-+Hi7X_;w(o{K-!>ifNA~f6fwSQs z%JS|rBFFd>uc@v(7JJbpeFUL+g-f1e$m!C-0w#Q^;GD`k+VJn*V^gcN2l2nF$V2q@ z!zH-;|6=)N!?KENf6zDeBxC8>?7vA_-52X;U-$@O;Nl_szUCi=kQ&hY4v2TUqW3p{ zre>w*^MOh+g!gR;&K(8rL=q`=u3{^xa=fT{m z+7(M_84G{kA9I#k_F3k1bZQQegSJSPX|~(l>D&5Y)0|(b? z?T_mB(@D*o5;gz3{=K>D+4Y@XpVcf#lUN=hCwN%Ki4{5rVnI`p?3*&B#zoO3+EYXr z?w@tU*^8=G3t%0*#X$S_(8v;J(vLU7_gQiGSiZu^m7=z@7OAxGf)HG7ix|`qsR+;E&lRJXR8?Z zS$X4T~Vh465T0#N=#ur7@yn%EA}2qDe9tX6d`sHy|9X*q>>{-RL~Ax%5}+Q z@AMtA|m94*DU z)d{~GH#?a4$%r}(Dq422DzDdB*eMWMTVBdOBJn$9J@sqtm|V`s6P$IQHTPM86CAZ^ zNtMX_36af2+<;WU-tMlq6;2e=RQ0Fj{C#g~8;IohThWEu0ntQ*-s4)#^R;MTJF#; zY3C@Ep~{d!QMG4ZLkC2B>#@@D-(u6O!)`szp|XD^1%NP%usxi;{5T0 zeTE1~x$m>~{!>W3f>R^W7Z7QFM@LoM68h*;u|MOv?o=h)zL&lCOx|@7J*D!(?CM1p zJ{YJ?K&CjWd3Cc)1ypo%S5pT4?zYb=xIJ#*YK+Zn9Eq>8H>23HwN}TbLt*^Sd-?_& z9bNTisdM!$Zr%lz;Ob6ES7^+uJY=QB|Uo!x$Xe<-bsY*P)I?txG zJqhOuJ&5ldaOntbsd@sJ5>!KS32_6+&&O7pDZE!4D(CL2;Yi2lYuZEQWqJ)Gs}t-- zJ+aHMKAW@Q zKh|JWkdFhk?H7OPm5Guv;gq!`G6!xbgg5g&V;N8<1y51qb91NE5^vxtSN3|?0SQ25vj!rn8+EaNqM5j&dbs4JVO zyZqgdRVay4;~T6wwP7MMJvc0d`ab-M79lb+a(ViC<;uTE!o+=XRr)EJy7r|B%!ACW z9HKet!E@76ByFtuSh+VunDS~rjOn$)wu)LyTOvr%-5`hbLd@IaKB>W8GVQ)1MN7UTTH!!VjL^6Td7|u& z@+xvs`k-(=)+950chRfkk-reQ#m(>MN2IE*Xon>3f)as?Tm#bGnEs^7%PCX+<+D86;5BjQwj&~>C;uja< zW4m>d7bo``b&5nSSgj~R*+GC7EL(F55>Q{^$w6sZeit-?{m5tC&~Y3)f3HSA)t2j4~&1PKzUFvK4o2krec@y9a+$e>+@Y}_YwJ8<7 zX!gMgcwVE=@t4~z{5^xDAG*xY(X53=6>)9Hd=-SiE_YlWIGaW5&T0)#Egn8{15y=x z-Za>biNQu`hCY%8l8ykwn#V@?_yPG=J$+UC=D^1zx*7s=K{hvPxG?Vy*-1JQ+?Qnj z7dZOeoXjg`(wMBxr^+~2X$pj#BZ41ARCp~L^HDVNJ62|Q7&l&SZ9}zquJ+6w^xser zic)=|9F`uZ=F~kW)P}O(D=DIDe3dasa89-!Fj<$=;!ZN6N_QigP}>n2f)`jD>L{m< z8?VhkCJ{uDIO@u|5^9LU;y&WPX?abXEY=F7v^>x;qdPzZ@ZLzERjxR{tB81-XhlJU z4UWg2u&f*#`50Hl+ThBisKi7@K`8NDYL(Ys!a>OwMc_xOF}Gob-5S9{g@N9xQ!@&c z4c$-l*3K0Theio`Eeu<0aAlHCQkzoSK}4}b9tB0OR|alO-=z=tInLxI;m`9tD2V@1 zh|;EPkVij*iUTt4J&B;R3rZP4irNQ$5o65S2mfCvA?yDWCFErNA7Jo5l#q#wjqQJq z{})Qg!o>W4YN-A{P{KBF<*ajbdN|RjE-8D8b}0w9R|pA!y&I*h1LbfO2E$G5aF2Tc zI_1sH@j?a{i_^?+oy+Zn=J>1i?x)X7R&XppCrV~;VE~u*QvY^hWPFAQEM75T>GTN1 z-rl~+$=<$TZr(yz$7bL+p=jp^*h=>%SY(BiAm)^50pz{k+@kM)7UyF97TjLm+HSw1+FCIxznp@D4GLquF0qOqi)6}ti>lZW8Blw zhiw7c?BeZeU)KuE0iIn4)`Xb=ViT|b&KsHV!>$MU!AAHGfqcBR_w)Eg0BiaEhh=QA zuYU^F_!_v;2P}}^s_&m!&D>R0MneN+Z1=v2WPEvWcZKK(DQF{jG7I6Ci~uqQFb5Kt z#rs*#iwa>`nqLmt2Dkj|65qRJoHa^qXiIjiZvx%y>dgPDmBYS(Yjw}z{9yWf}pSM`fSX#WUIU5HK_ z_+*bm-YeO!@Nd*W{NMY>cOGFrU@_d;r$aFNFW>KPgkWtY6l*-!nm1d9AuW+T4uGlaFq2>i_z-^?&7 zeZFEA&^OA<)t7KDEe~GEb$(H;zW127%6rv!2EjhPE`#VF9__smY(Khctnum*(Ek@Z zCk*bZoBJzEWn{On|L!%}?*0x$Xl53V0W->y((dj7%%44bX%7AJ6*LS)*wqts*#x`S z!SxqtXaMW@HR9nO#2=+k)Q=b#NdCia=(k$w1M(i&f5MLh7y(HBn_vhW$nqzs9ax~o z4+#;d{F?*C`-SBT@*dPb-!GmJ$o!t;gAYFcxQ~zm2p&-2)u{#I|;iGTIrf#L0H|3j{Bc3itAd*GMU{RC!i z`QN@C!mN?&Cx$ou*r8sL7sB2%z13dqfq_A|9knI2_w_B_#3*ESxwV$#xyIKkbVI?}{Y&-zpjV@w7;ItBiZ zD~?s7kTz1w=ABRebTV^Uo7O^FX_O~9?~SzbZ=s*WXqnID)Z3z@I@^4BZ5Hb?Vx8Ee zETB+N{F@l@ylk`Uv<15&1E(x(trTxSIbzA8H52#zNPsAdbH_hfO*hV8K4aDMi~J2Rej-YX}L=ftIC?4-nDT|-zog|7))lB%TH zuT=EWN^nus!SPu$Y>_QSf!Xer72Fu20CDmav2fREkP-kBN9o`JE$hlTX_%``OCwLX zy`$xOR4D=1^n-#nD|agaba&B&89z5<)azzDex*}L*>i7O2~>yiAMvX-=v~R&Q!T=o z%U|NNzti%sKj+s~!Qk|d+G<6%1o0Ff2BWY)SxS8o&|F?ev_-HsLBdSel9-J|*mI0! zXezCY2bce7dj@`BvMF$w)E5%dD=+{nMa#QrJUvXgo+?c*bb-TZ(xR=ldIddiOt-~7 z=je_48kS(Wkuq)QidU^+1PYyT#rq2rRm>mtzhp`axZ>VVO)%E+!V>U)CQMLIR9wVy z0mW2b)1vb&+#Pf7+sfllJWD#00LjVMUL}~4B=LfWQv6$GMw$uoe%6JB(d-P9|GIA8 zb&#)$l1_6XufV(?(O8a%D>jCxg&~)o4T)w%qKEQSf*|gi66aTfESQI|;cL3;svklx zG!$KYJ(T%#Z1Z`S=0|?7aT*Mg-Cmkp4EY{c}90 zSAbTs>!ugI4u1|b_du(gcFFwh$G`NUI}^LX-1IBy<;T=3xj5k+EvCINRYBpwVdbOb z>rLNWsNqL7(L54gS`&$wTV@M-NUCw)(hBpq!U2ChwfU`re+NMv)W4O1FTiLLmZ(X_ zbh0wKA}GD1;l>S6#F6KJ~D_C9;H7$iifHE+QdZbo5Fk!G9W-PC&V*k1&|ZZ)E1 zx8gOnK;HQu?r}P1KoQ<#>z~YE&Ar(YjlY2HD*JD5zKKii0$exxWVxrNm&Lv9xQAG# zUxGebNBa{M-9FkM`y@oMjG`En2a7ZXagWr=$^W@x(IpF z-C#uIFGgHBSw>+k@u9(dYKMhsvhtXJb*@bGltcJ0b%kWdaj&iZ0Hc+_M4xN_%friq z6RB*K3HE6j6OsNN-X1&}%?=RYvr$i)K6He05;~Evtrjt#M3Q90)mE>zu-5=DhaeP? z{s3N`hB~>Sx=!`cY^bxK72yLml%{pFx*~8ozFrbH5PEc>F3qbF&xOt=?$hK}`vbIN zT*N6OtchHOTcHxS)erN( zXaIs)#M!?b&Cxtm4KQ-CbrF%5sE^8gwBuSXTj5RtG*i~8N(k!8H1#Mvvp*L|PWLW< z%Q%yYVNE3!-b50H*APsy6QvX0a^uX~M?*0=TxaOzNL23dJTkc!Rz(f@kAVu8W*rq~B+=CIm6FR%{65boL;uQ5X|Y(J zFrq$Gdm5cVqy5wzU_KqzFG^JA5DQeG16j;^oDNsJz((!6p#i5X9zO!H4WiV}Z!8CQ zMCp6rSjJ~k&)Pbb-&k;$=qdOZoe(%GsaoM+u=(+v_&w>j7QQ_wP)i7E2md!20juCl zZ3FirGP8v{Z;u9&w3B zlE!%3BeSVi467ld?6WUKUS!WYhwF%^VfJA>Ifj=jTvvA<<$z&JFE<+8C71uGP|*jd zp4#nZtJVq?`pr@cT2T> zGiWtPAAV}tRoWv$#VdFSL=lOBbt)mY|AzEea4D6g=uNCa_*%O8V(0mof|qHkUZ>IA z97z?Z4%ruv`Px@&RvD-uY(eb#HY^P5bL{;g;1cMVqWMhzf1njD{Q& zjKtp+m&6-vD;jkvTD~JvvB%PuuU0`U5EqwBbc(c&rJulQ$v<@l30Zxwz%qxaGQ<4Q zd6ZiZf180*R?cC zi3@39bb5*dlS4BMxqK){B%FRY5dmjyU#urjPJ7oNRg_xSar_0-Y!K0gwUFj`c-3VN zAv3HyBy&2&bX0G`IsjP>K|8tQ>|S1OF{%dWqAZOyk8fStc7gmP?CD0dUe&!e@7}AN zBa^YROQZw-=Vb|0tyxWb`Z=e^wMP=8V79Pjv7t2lwu;(a1QHUejgkY5pP zrHxXhZWf&<*pS(<-=5-4XmnA%6{w_RK<0a$xy>$a1hL{NkvhZOrQU-O@>ssSnQc{G zsL7zO5;81X!BpJg2Q&!ifjfGcy{-Dw+XpzA1$}T!i;Cr}x3O9P7qR`Wmm^n7wkGs4 z=>z{9q8Mn{G+GWRqoPqyso@JdL7hz(&+`=e|pbYR~A zIS1D1zJe9njxQ^5bG!_?i4hDxsA~!Ky@I6*#AEvu#|dS$+o^>$O&$}iwO9opm zsPKxPSb{~@k)5k-4W*4Z;zR%2Q`EX5Zocdm;HB(9uCbO>QOz@!t>T-%Q^2hsiqv2% zKX4%-LMjc{Ct!qjI&!{ocO=JuTtsy34Wf`E9@)E@k2TceCxQpScBCPUW}dYIA@@zY ztkx+79;?#BNyF@8*V{}l>2XBA*2hMTgG5i%h8>AC=G+5Z=wDwmJgC*5-lbY2NwQm; zQv)qn!h#7diF7Kr<2)VZu4UV`M!x*zDcK;6AP>V6X#$4h+8i{b-c&t`iKSWan+V*~ zY~Iq7ITd!p;cYGJF~_G{%+q~}#tk!=?|}1ron;IK-4)yNW;2N9(-lJA;01MP@kYrT z)X{x`o?hRIpUj;U@>L;O%Cq`>zlnl1m(n+D5Uw@RQEC}CD?b>S4W2^+CdFXtn zOqKaMTM$XV_Ra%Rn30Q01v!dx?K$yh-_<`HvA;?PcvjCiK!YObO2clvSkoF64Oq=y zJ1y()))c3tVk0^s%dItK2aB2;NeX-N75vO*bLXRPsZw3@+|#;ZLdocu3E z8z>f9wRt*!`4aQc+KfZ0;kImWbwhu@g#s$JJ)2}i3U(}-az)r7lIFIhUt`32sysgYpWpV6IK-9q=+F6_>< z^?MdRX9H0xm9hZFqadI;WL=SD18XXGphQplMyf3-gPyWry}rJdHxz5UlaYnoH48bQ zQ_lb;b?F8b6qK5NGkvFDz>lpcd%mGZe(8LcDNxzs@^KbuT~df{5V{Z~!DVl^L09XZ z0Ji+-UivDb=V=G|&jx0VPScOc>6LFrfiJRD5d8~33nE@>)7xLvu@_R zaA(Tz6NVH0nUM}dW)NScr$#M|?9RMk(dmF)oR*#1<%g^vmvYRT817>fbgs=i7DtX1y7 zPE6V0XD&C9nPZNdgH2#w@0PEC2Q_H0PvyRuOBDwEuA0Y{W%seD&3km7DYabozigxZ zohK8y9yvD4wz|0s1fSVn=9mqbV${8}PL@Ij7P?*`p4|p!vme`$X8sCPb4k8M*Z}qH-{gv5G%69xg;&YM$|u-@N_T%^-=ll>f8Ai8DKcqs;UD^P zk#FR5bG&vzGeS-DjMme=F)+DY?uWE|YDkE3go%zCXp7U*JWdQbO~5gUKjB!wUy{qo zy<;*1uMh0O#}=_g#kv1M+i2`NiSn@L#og6mbwZgmDI^J|P}lKQKTn{TR4ryfUkRWPR3 z*>c}#Li)yruvhe^vWwf3B-GOyUeODDQb8cA5DjobdTgaX_T)nq@MMwXOv2$w9 z1nRbK*s(h9q+{DpY}>YN+qP}nwr$(Cozth@st;A?;{1TUuhz|~HOCw{NbXvAeYB`2 zH8c9nN$YuHgf9yGEi+3*^6pE&y9%PukFZ60*&T8>Tmo&j(T{ncD*WXuL|-N{;fmp5 zEEEYV>ak9zOG0<>1~TE|Pfr&#qPh+ubYw49xqCk73jotfR_%h0$OsX8)GBVTK)&V2 zk})Y<-0pnqQ;pfB=tI|QJDuGGafb;T!fx1`DU$iF7>$cO;WZ;2kygtocL8PJt{8vX z!IuZjm^a3no0iD>jywbd*-=8kx>HcgqDc3$#XRT&Kbq>R#YJk%{Y=m4r5k@^FI^IX7D&sp#%k@L)ZTlD{cKfW9DMjqZt zSfvcK_JG4>Zu@ zqbGHs&wC!;sSSR6fpwItC)7u9rM`bkf7?VMjNnO>RbwpVa7*%L2JV8>eO^;7h>@Vg zh?6Ht@d~aqim=B9H|ehqyn7Mq-60i}C%HJm$1Etdz7 zviVGNRYYjX^__VVtTfbLPl+v`_mB~@^ykWTo7|f#r=lNC&}9+4{|d+u)z$Ja`*Hzm za%M==y4Hsht)8j?rL(l>_LK(8ZbcHx{etqm0$FvZ&$!<7ZoQjTDc-!x8q%r#3kJ(F z(h(K8gP|}(?ck8YnSfj(65Hm<1f#2B{{CDJni@P7N<+=U`84ro^;Z{?W7O;E{ws@4 zB=n^V>TSEbLh2549-d{uhF+7@wSx%h6|EYc8qCU2*?_ zJ_C8i#;@Xv)IV$c0Px5zge&kbqEp`z!O5EkP;0LRQo%p>txa1%@)E*|ulTtP-lntm zE?eU4ANu8#dMfRp>xC7pEM=C)28?oWe+M%-tV2askR3f2Ec8n<1%6)Jt`UoMDRgg~ zy3JU6;01~m0}Wa=bAO(ymNcxVyW*N^NFUR?F&1t6w`4TB6%tE?tP{PYUGYVOj~aiM z`w1;<4^Gd&-hDWtN5}-X(@G{6K>OR1n>{(4#w+FH=?(Wf=D!?GHl^3u2#jswQTu^G z3o6Ii2*AR}9DFu5lycj#(2-A7G!!6&mF=!w$rQ1B^tT{oRjcUiIETE>W?{FcA%*C% zEl>7i#Tn!+w(LC|fAk!0pP)E=dPA<$wsf~R&f?&%N)+hOz0HMHnlimsm=Bqf?k@3? zaz#}FIUW?1=?Vvk>N8n}j-dQbB~GatZLZT|Sh+U{=D4m~l^Oe8=t?uf3{7ax@9#ix z;U?>2WIi6uvuX0)JYEBMD5qg?&=kf79lcj_5t z2#zSq0UVuhBVxiu8=CKitP7V!nd<{>iKhbdw`J)#24eeGcb`C_D{tiYUb;R%OGgyE zj5QwE;hiYk&yzAZJ{77r;b1hI#wLu4EHf9;r%;LIFY22?Dvp}WuW>K+59oy{g%>Et zEmLxxZZ|AKUQ=ns>eG{}qcBcA#lDQgEpDinlD;y}XD5)1rFM7oLM@Ftu0av+P0UQw zD++AIRABHtOlJHI_-t*ewS-tJt0YmEPkcC4bw9vzv8s@y^p|!Of&|xj8*Q@=`9M;Z z2CgcJa;5M>y`ZYI{UmeP+lF>8cc+$dqiDA3i zDtEr)(hWy4PFG>~R2@ZQTxEgWuvu6--pZC4AWJGd$oLB@2SDqNULNI*ISk_3v!D`j z`oAP9bd5MD4gYYxId_YN1#`sUE5V7<>XYZuOM#eQ!>kbCJC;*CL?=_AiCF2Y&S}E# zmztEAS^}ON4sxc+Z@ZD`tMeAwt8V-@NN@)SP%ot~T6#t9#3N^H%>#3?-oYF$B5K-1>6BW1H(-+x zl;eK(UFpUgBzJG@z9`Hy9TM^Q>VL?>nuhO_ehw9y087+{yVRsU%!bL9qq*R^!fNJE ztw5D-wuJ5#xll!72G~DjU4w=E9IEM=ro()>x?&$dtZd;B%vt!ZdieF*ba|_NLp@wm zeIzupdqBWP)l@Ckzi7ER>OaotU>`?8=FM?1a-PtRD0VRsBK)zCQtoJ@)0{?ZULH^B z-le$Wcm!d6SnbB%_g;TgG?o};QD_Sj&YXs)<9{fYcTCW9WtWv=4I^B-0Vjl!68+D4Ew@GbMARP@tn#nUHz=!oLH%Y0w@h+`iO+ zGeU?Ug*;!59)?Q0O`AHQt!3@?B;ZPg2KX6G@e0+tn-hlbaf2SEUW3!yUWiSQ;jIzP ze??=bzZFi6N77qvJF1cw45^+LEo?_agO=j87?t6?RdyX98xNN7S3xUg@!|kNMEPH|I+57joQK4W{N5yyQpocPwY28r`M4RABuxyFW7BpqAQE;{-ZKhP zg$1uZG5B6+7g};5P@fr1YhV{)Sap3EW=Bwn>c-ER-#bP*s}VzhQNLkFi@ z8GaFXixkN`z-Z@mPW0?hQE}Ga6Y6d9VFUrcjT|KtXQ4Qe^n$W62$msJa<@Tfpw?myw?A=6^e+s60LTv?WGVF}L*ZwHcaFL5uveHtX{K#MS6wJwL4orTMJ*Fux+gvVl$4up1iX zv##E63WmckiJ;3vYVE#^DDi7xG+b8CPzRj7cHz4Rp^g^#<9+LvuPwW@)=d@44fFuPYx+2Uq9#dl3}}YKF_PBznDjuc_HC zBm&8_?1!{KCoEb8Fgtv-GOwGEk_^n{`;6)>z1&>aZ`IavY{$$E@FCi?KEt=>Gxb8A zg=^eZgUxFo8^eT5w4Im&u_laaqK?b+)KQKb_e1Kv2At}{xk;lvEz3Xd#peupLxK(8 z@oF5-hrm275^;8G>tn1QSRpERMt9Rv4kUjhGs!Iw$Xjq|vgwtoj^>Qnof-2W!%ToN zSm#vRp*4QeUAkTOBxd`s&8slpCJnVcHkD2h6dT&#S;O2}U(|}acdNYvWE2sm5h0wk-~hvNX$=6GR_jFbAZBV-$)1Mbz=W=k=2)NbU&R(egF9 z4I(LeD~GZ$;D1AVFetmLc(x2&#;r_Rg7Et3>h-gOc1=zx+^Q5wx3v%Rss9Gi5$+ae zDXQfsB{>V$aggkZTQeIir^?QfaZw^F!Ye-WaGbOg`mG+qMzK_&r!TsBxB}jWW?&1c zS?X=^Xjq?K(!gfH2fK^JyVEC)siVuw6qVYUYspqX=|x@4$jd zUfPCGPd%X+lDa?^0ohqH0`SA7lJGtix`{|$McocmiXD~ti_|K7Gd^xsz`62~cA%mm zZaV(cLU_fS|Hfc&M^NEhT=Bq!JaN#8w1ddl^< zE}zoC-FI%)hE%>Lk{p<5sZ6}KMl!AL%wE7!brqZ=UuKQCGA&oHGj&re?pYd zv2?pZJBP&B`dG&EpQWhF_+&h#`@z1kD;*}j<;$$p*C>{Ao&>wc4vr&rMEve*N{6&u zjcE6AikS?2g2|_)U5-qas3DSE@(785*7gVHV+JfELO01Z9LkD{O^3CU5@+a=_V*=l z9ud(Klf91f;1M-7+plFtF-Zo23cPX)GGnUd^om99q1tULu|iNXQsAyAgX%eZq{1)ifnOJE@of|FegevQERjm9V6O)y!Z3 z*oOx&&T}#|>`_qKYW0hAH4D~FwUET0-i7vDPW~{)K;z0GJm|3QLB7gj&peRqj+|6n z+p0K#TQ#wup|AbX%EfRffrIxj}%4%!Dd|;Cb7%S}x_>kBX z!2&~NDUzPhUN)MIz64o;J7KKzzdjzP;=u&St@O_9+_T{_8)!tQD|zaeHUw0j3Nb{l zqYVwn8VWD%+7RI~!mJV4zj??xNhBAX;0h=8RoUq7ZWD5g$FG#r&O6PPOz>bGV->SOgMp1$d)`%h~_kA%*T~SGzyYfmDgaA-s7on9A7+yrXNSUJwOYCMa(I zOlL_UC(S58mOs4TYY+}K(Ir))=KLCKo$^2=^{oLjmka&Q-OrCQWcqUY&YuE3 zIGP8u>XyAN9gpTaRFg|vH{=Ifs4%6q4$)vhuj#ku_rDdf)|#!s1|oe@5{B5y!R@=Y z45mMkDZgLZ1U7J1nKt0sT3=sh(g*XXH)uBDJt>j#!W8zkS+2f2K zo|eXXwN~`*H;I$x+NKXVeSmD!2XgKa{6#+deWejfPnXi^UgP~QOP;Y6(xgT}|GIfW zpOb@-XTfr1A75ZWlg0uP;)8chWu?yXQm!rg6xDgSwq`=TOYvA8cD&9=Uoq?O9HS&gOMx-;hb_NzP<5}~=ET-euNQJE`z(eJ3)m9JIWi~E+fTocYPy!J(o^a^l zr8fRg|4D;}J75DAg8RV`8LyIM{{e>gX}O<4LBPd_T^#n;xfQ$MzF@TC?QH zM*MTh2Q%w>iA;i#1O|ElA+@tsMkKtYe+T~oRLYmug@SBx%ci34flaEZKY%Pio5GP` z1+Ij5B%WU%e2$&;u=(~G4u#ey?Ag0s-U#QOl^Y$OZfGn>uZZr{vpc7gDF5)_MFhNL*9K)$LBdeY;kP}ayRggmoP zjT;P8`2VY$O;Xmx!$n;xn zf%v$!q_-Xx?h}Q9VR51I&`wunOX;J^>i+NtyxKQsLk;bbSGj}_ch}h#HGY3~us?t_ zc0OqWW3ntg1{K2iJg!lAe;VTl?qh2x_65Kw3DUCHr!W{+CD=Yl;3v@n+5^^NeX9IjU`A9PuiaBbx^e`{=ar9#2x2@XoUN02Y>GoP}4 z^D5=3V=e#gOSmcT9s^dXpT-T+i*Nj6{$)D!&041_EQvvK5URkc%W*Ft;6ib;4}R*k zl6g(w?}?e`cWkRvDB{U>ShOh-oKq)L^=llA`?mDCDqGV{Obipabu9$Cu#!W;z)L)z zRdZo$=Y9J8*#}eMHob0HuLim6Vkq4m=*u3ZX<|sdjZ~GV z@I$(~xGu&~ZeZ0*Hk-kO4&U(FBTh-2Sw50gsSNb}Z6Q1uQHUb z!Ac{m+W5sCcyQ+nXq@xjB)I-`CD{Ix=& zpjXi_6K?4&t%M;P@Ae)SKa{uGa zS1f+JP^1VNm9sK+J)+2zqMHycV*Iv4Xmt4Kg})leN{RNVaB9xfCV5*WHH@#=18%5~ zQarQihEAB*#hxKBUM~cOKGr?J+bK$!-~t&_NZrK&rrTlu3hRWER$b26#2 z9*D6a7s3`ggz9}=hDo||08#ey=i|!LVpgZq1{N)cDQ^W%fHPB-2?5IHm5<%H!$ONm7=7 z(mLF^%66mjN%HLOyKKqRch+PiaY`O+goQLhX{5P|5Ec|66;khpWgO!LzfW?bap#1H zS%d2=&OVu&Wc%_fye>coxxevxfmrIt??_r*Bn^%^{~{6zavJAKb9&6YWQU!s1Nm|i z^!fRWdfqc!*N6mpMt5Zs)=fv#Pza8hF4444bs9<)`1wjte8lqUHn@y%@amC32 z)z4v#qR9`LW=L{mr`yC(av4_(Es4BDqPZ*a>L}%EQ30mfNVGG(iDcG&kf5d_k*Hxo z4v~8_zK?QJ>!s7?+vDrRPp= zdlo4t2O&mtZeCm@Lg!8^KS%tsBRZBrp)47en%S|z_XDku=Ea;jWt{i>?EIyE z`1!~kpAS4V$#`GBEOS@q4VFQ#%g>cl)ZBeJWVsh!qS7ApCD1En2bQlBUnd`RcKB&k zXP1e`Qb6|XnDkY#HBGSA+}~I~$M0KGZCo!104{bahXvShrFy0Q(gH@bo8lM>RZpqaMWAwgOnSdd!0uVKP@^u zUUZKc)2v84`_PPgNa>=;$hz+$aI=4PRSFQF&{}aIT|H|P1WB>YZ z7uf$*zyAYx80eV)j|u+&2Y49h|6_&!UjRH$;Idg8^Wfq8Xqo{*K?1Qs9UUDXAZXTo zTg0s$;4Up-P}Y4~{v5#m9a{AL zbc_slK!Qg?NBqKS#-$>dE{DLOewx%$!1s|rm>wLp$iIt9y8=QA zirRu6Hu!$tz-CitK~me=*v@{SAs{#W|58iwIs>`IHh`}_UkM_|!tpL`5B|QqyyGD^ zeWl9G`s?b;3jhFwW2=x0x(eEWsJVKl=GQ=SL7YK2+Wn<~ebRrC`C9haz#2t@b!A*tP4~UZ+}78;lf(iq0u#~^GVp26!qDd?3)I1 zCZUmLAwdIrMFzUG)(8G{2uu$FeWfLRz70ik2Gvj<__;r9Fn#QYw; z-*NgRR9v5)#JMG1$#LKL^^}vY^+OGs?n4Vjz3Y=ezS>iU4x9U?ap^93DM@0}llsFW z@m&-9<0Jl)aP*^f`O`uKg^rcc1IhGb{P}~(+nmgT^Hbb8wlef)g?f0kgVuljBhTFT zqps^#TUHct_46do5wNYyzdF5n+|$tL7+>oMkwKxxo}v0F!T3{O@v{cNlHjO~?@Bnz zA^p|cJ3091<7p#dtpD{K$=U_=phFY8urbW`;}qdT_i9!0-2!IEf$XusGcnRX4GhG_ z?#~&!ZT$^b*8{a@B_Y*!?IVT-WZ2l?;td1b*>nL)*We=f?l6IU36y(_yU>FO0Yr8W z*ara``i@{t1Lhs;LAuC*{t>nW%AMRfrHZPpusd9eA7jmLCxz8xCm4PY6$rOWY*- z9wF9$ZkP-i_egi=Lg7|qd4CuSoMJ_CI z^FZF#t$5#k?oij`2Lx}~ajz~ka!{iM-|>Tjop|4w4NQS8T~->c*@qQz8$*FsNQ1}8 zxUgBkq-b;eJAUF=0$_=LLmU$g+db3(VjIG=TLcXsOm*<@nG^)MZxdOc1;~&@@?G$u z)6-un{Waj;hj(N0P$RkbuSbC}S9-S?!(~li?Y8j==viL>L?wAHmTdlB zDS(xvH4vl9BV72lcdyj4p@Z8k7;Yb)C5+L|!Ltf=eHYt9b>+GeA(Z4aO-CYFL@ogt zR(-SRUl$oik+d^SYry?L!~f0x{qcc=`KVJ@?T|u2D7Dop?y}`dR0N29xb^39Yg{$O z^Vw~t>JwaNhRHcTpyYiJr&ohq*?_=4E>PBB@P{kLnIG(eRF-0Wtjh-2%b;W6rU_=tclvfxgJ-Z!DQ4`AjSpZJ$`5*xk=tGK^%Wp z=C7yN3c@rAMs}j;L#0YJ;tMj>`V#9*0j>}uilp&VyT#sz-rYlF;@=lc!mK;-Q0v*i zBw3OuQ@vf68T_;1=lA#ZAjNeI_j--jwj%|bm6x4h)luX}7xUck=22eFh@QwpiJU*{%M0Tqx=kaWRc+x`5~!y6SJ+8y{h1j?={^>cGl8Z~ zQ_D&W9_^W&O)VJhB6Ei3LKEckfbO%J}+ zberTaCjI5dy5oc8j!~a(*wMwyfFv6T;wv^>H>KDtKEK+c0t3d7bYaV3ECh~=o04o; z9b=BVr1fh&KVd0>?GXr76;#6S}tTC`?b zHmfI>T0IJm{ebtES;8O2IiONWc%hOhV)T%m=ISR5KhRh7e$M+DZO*tkdm%isf|^0Q0i1Mhd_ zFgQ{Bgl;WqMDIY&Yco(U6_}M0Jo^*IvIN9WHsi2=3o_tZWVi>jn9j93@;f7ny1lFZ z)IF~9%e|2N8VrpMkJQl%U(Guhl6IK)=vk0x6Gh2#^}^-yZH?s!=K~0WMt#DLDo3@l zau7@I-sZ$L5JX`>O?6~l^1)h3P|mjV|CZXc4}yl)r6iJUSdb3fy7%QwB8a_DFKjzN zP1GSItJQPu8P(TjM$LSqFfn;&j^FP(l3IH4oX^u4YUP1df|va zQ<)Jl=tmo;{c5F$CSr9!lN9ajk#{Qrrre6_(Y|Dj3s7de6HnRaLBMT{{ z=o+~BHk#n+%i(LF#wJZMGE%Xvk(utZ#WKNv-C8DkY-n^U()1QFuZOh@e5BvCv0V3u zWDi->9*)`mbIHSEu9ur{NU_>6J(K_J9yJg5_;Fh9q~djvOBznWWn!u=132AA==^Qz zHtkdDe2emZmG^wEZLyV~;|88c6PD-O5-2AJPbFidGqNbnstx5-KShz|6Dn=n2(*&q z@aaKc7G}whIhZEU{!pT>j>kMAuiUwQ5=F&M^Me_%-J)DAl?F-UQi)S*?M%rPLVw@` zPtP!Qp{DzJ(N%vza@Sb75@`VIOt!ZOJ=!f2^MHhsD^X7p-y2=TY2KwMCZ zqAr?1{^AxLTr!(32`y9BTa~G`pag^2JT76PL;pxOs=+&*`5d3j|~v% zCzIXJqp4}~_e;F#pZBRoVj8*MOsx~=qZ!Mx?x6Met`NlWB5Zj9E6wF>6|xAYz3tmj z)7p}TA-dY%%VgiJf<>OK$N3^Fjp3sZGOAKNqBD2kBP^t=(7m2K OIJYCVF%!Wv{ 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Ð>C!bt&HiZw)SFN=Aeqj?qtD6)(iz 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%0DSH_~JQ;ABEUI(lUNIKw0)1Q#HRMF}!bNo*g~ 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

n+W`=fVOO$@tvWm8 zyd^4kqE@|uHu=qV+^ffdUH>I2Q`D}1L69nGKx8Wpo=f%39*QEs#p~&_b!69uEcdlm zUsF&xOL9-jh+uMyh6cUDi)GM^SduS7sHrg}FKU@dmLQeU#q}Yw$<(84HiAlP6TB&e zGt9SI*Eymo+kz87<~-fWw+;+FG^$>{U4X$4bOoJzjgzqUD@KvYw;4AVbPI#yX419L zfP>w14oy6h2}-#j!SDaEJP*a_gpLz2SPr8$0K&R#lPDgXu#nTs-x7{N7_)N3sFHuw zJ5l1JS6ETHczFff#Xs!I>J&@Soy0m=rxxS!b5gn9#j=%Po6~f=Jje9!1JkOPJb8_i zulEId)p`1$HaSxZle1kD{!`7a>m*+~v8wXTvy4zuvTPwjXy>TG&<}i2of6qsv>GyP z`qYiZ4p+749)t7AIj&MNXtr!;s23ehJi2HN#gw_X{RK3SI=`^H^kfkLpR6On{gvL% zOB!-*mQ9a&i8ww`$Na#9xr;(vH(tk~{Fl3Hvqr&mNEb!RNSk7HyqdeWLEX^dFPq_= zzF{q&#K6UL$Qhi@!U=Y*1fed5((gP1InE>rS0d`tZ{x-O0`9C$`-s7t=S6MpetdeUecLq>K&dU)$j1btCLphqTp$bRl_k;MIa86BuH10HOLa9tk zBIKX-IQC-8o?Q)A0AlIjaquvsA`SCX`GbQWImveEyL82CiKQguZWGoVR~PY+dAL<8 z4|^U7b{cFIX3ICFM0#OuV0ufztNJQ%w`dUg5(XB!_p{*kvq*Ib<&2y1pyts`v%B%R zR6tWLb<2ZO5c+eOH?fksA`~YD@i=4&Q2k`LxeKNBR(-tSZ7Z-!=E|x=zKCS@wq~ur zT7QR@L8v4_K3ll4L*3-~xuW-Nra@zMEjFd}!|z}2hv=M(&ig0-`O>7}VGzBUlqH^3 zK=3k`U#dqaG8r4_d@YT>Mk3#f4SUUM;{G9+T%CB+Kev={(=QdqQx1x*wWbu~N?LNb zw$n)*x#~mwu<7V(sC)0Dj^4*fK?f+hD2*vi$+}@SZ~aLb=`5p`m2Uzz-;*6hF(t)r zP(4N#G%%b$X`k{<^k1iu4X-MdFH<$AE1tvD>yqEx>C1br*W+W*8fw`xcLtX)Bk2)7Nj8O}^kLx? z_U^^2z`}xPKA~+jWuNtGZlPL9knGrO!k>sF0%~iQhvuQCX+DHG?GX)Q%M3QCeg9>fH~y4I zU``&>9s${9-5jbD#uZGgf~9}_e&xRp;0h@A8VzP#`x&C#bB$23L05#Dsm-^Wbq1~Z z+RX&>NMAWob%f8Jus-CNGXJhnW5$#I)iS==TCe5;9nO`@99JtU>+0-13|S+i*@cc= z!vWO}ebd2sHTB)}V%PGL)_oZj)&J77^adQh#tE7sc*{xYER>JRxSsaFFPL_HefH5uLL;%0kGx-^kpote@u~>j;0@bx^$`LtU-v_~Lq(WfjUll;Z|p?;i@XAK1XrcFFzS}&{-RpPx4 z-8nS`cW&2l7tx7%lJkN!d)2xnqECU(6ege-@SBH-zVKSFjkbleC6z^-6>! zmy3}%ZsSY&w|QINtA#-#!?WiZlZm;|nRx-RN7QNdXD zFQ6u7!zHEHJch^KBz<9x*Y`K?##^%BTlZ=x17FG4jHwG2Q9Z*sy2>APY4=gRoC+Hpc zAAi|?5QAmF=v!cNVsZB`)gj^3HqyId*Px6JYfIV;4xci6T!`z7O@xG#*$e2NKr%`) zMJpm|$39r&MjbNRatk&MGNz9J(Mdx7+CelbHawa9l=N{mY@gc&`n2ylOx9VuKm{M9 zdsYvnyp`VPP$z_Gs(TEy&Zg@=_7|6rx`TwJFzd4sI$Uj)zX!_pX%L^i$AZ=1?ixc2 zh<(xcK|5W9^*_!vGpj~OiU>hH&-X*JsGcRtT1|B3QSRHd9W1itn5>hF{d8wox5Q^i z4J#5Sd6>C1-t}8#ir$o;4{|r9En*$-#ZE|qt#T~5S>}SxgLzy7(@|ZiPqYx->q
  • lXK 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 diff --git a/docs/src/runtime/zk-docs/ciphertext_validity.pdf b/docs/src/runtime/zk-docs/ciphertext_validity.pdf deleted file mode 100644 index 9256f3bc438b4a2523d983a3d9004a7fdcdab197..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 277913 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 sp_&|*zOuIGdx1x#T&NZzl3IkpKVy diff --git a/docs/src/runtime/zk-docs/percentage_with_cap.pdf b/docs/src/runtime/zk-docs/percentage_with_cap.pdf deleted file mode 100644 index 78e2f6f54a12aa1e669c43caee9a6965b5e590b6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 265177 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
  • VL0 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 diff --git a/docs/src/runtime/zk-docs/ciphertext_commitment_equality.pdf b/docs/src/runtime/zk-docs/ciphertext_commitment_equality.pdf deleted file mode 100644 index ac6036a3cff065450f726540cfc5f61d2e913ea3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 266927 zcmce+bBr(D)-Bw&ZFleXuWj45ZQHhOTf2L=ZQH%uwr$+!yx+aaP41g~??2~MQkBY@ zbFI|Km>HF^<|0=R5u;#zt!n~%QrI%BisL{-~a#e-L0jWyupUz_fn^DFR~nmveA zB-3RO=1_KWbnZ2XO3aB4RSSY@Zd;?>Hr+{!M-<({$X{P>4gcz~c@b?snuJ!0NV8PF3%;(GY)9GWfCc!@}o4l&Ta=Z$hShosiD1T{LjPQ1V^djUAmAF3}2 zh1L{C-Q735pQaz?tsU5%HhH>R-IiQrWkpFi+$#vp+LE!xe8MOzDWwn&3j_&n4RQ}g zAx|uO?0N@1k}T`Z0v=;1$}=ESBn6?U8mgE0&e?Cy09g51vNjC_Elk?u`uxP`BhGr3 z_{eSV5h$9&_5O)wUKbR(!Kf3E1Oj<)ep1+iR7z6gBFQeORY3|HnY&-0W<*3a*%EB) zqF^!T7R?!rS{3!Kq2AcfH|i56%l3Pc_GhR-8YWXiwA)i-g#uHgcd~2o71S>@uYk z21Yc%6F|@jF2=qX94ZV&5cLZ|d_t!PteY~~I%%>--0`o!{2RlH)z!555!@De|y|)g^Nmq&W}(o-|Tl!o6;2A)&hMzDUkpn>jC$ zD;CUtYTOK`nS%M#c#CMJqB>CmwQPde1eBjf8>nh?ydXObu;!O&$?((|mg8*A+sPK4 zv4X!yRW>3%)hCv7GC>p+=}4Ja@HL zes?u>p>}8@LbM=Ou;;+D6Kbz?UI9~huYUY(xZ47!l-Wter`6H+Bphk_?&VW6bkWSU zOd3o~9@@8Z6bu;Oc(QG$$;KEYm$Y2X9=vvTT}{8pwYOn}SU*@H&gc(LQ9X624iDE= zwB3OGxpasPULRpkSw5%-C%0f#0BL%W>YEpYp6hz{e;GYT0Bw~7H7a@dn6Emn>1m4k z!#Mw7pl*>QI}?e)Rpyb(XNm%H08mPPt5t8L2uEd2`fbw@3#RR|F3Evuup`W(VB>z& z(zUe*afheK=t|L7Yq*Io>nLf$vz~G8l@!145MdNFNQ-M~&=gteR~jY9Wp>-V zn<1{%j0ʦuTfW13lDLb(4#TvGiteu?zP*erA<0tAUQ+ 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