Skip to content

Commit b0a2859

Browse files
committed
local-cluster: add migration test and VoteStateV4 hack
1 parent 19a1abe commit b0a2859

File tree

10 files changed

+214
-10
lines changed

10 files changed

+214
-10
lines changed

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

feature-set/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ readme = false
1111

1212
[features]
1313
frozen-abi = ["dep:solana-frozen-abi", "dep:solana-frozen-abi-macro"]
14+
dev-context-only-utils = []
1415

1516
[dependencies]
1617
ahash = { workspace = true }
@@ -22,6 +23,7 @@ solana-frozen-abi-macro = { workspace = true, optional = true, features = [
2223
"frozen-abi",
2324
] }
2425
solana-hash = { workspace = true }
26+
solana-keypair = { workspace = true }
2527
solana-pubkey = { workspace = true, default-features = false }
2628
solana-sha256-hasher = { workspace = true }
2729
solana-svm-feature-set = { workspace = true }

feature-set/src/lib.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1099,7 +1099,17 @@ pub mod formalize_loaded_transaction_data_size {
10991099
}
11001100

11011101
pub mod alpenglow {
1102+
use {solana_keypair::Keypair, std::sync::LazyLock};
1103+
1104+
pub static TEST_KEYPAIR: LazyLock<Keypair> = LazyLock::new(|| {
1105+
Keypair::from_base58_string("2Vzd6oTWU4RtM5UmsSyBH3tAhPSi1sKqMeMC8bF1jzHHLBMRhEWtrfmBV4EmwQbGSwkunk5Wy67kXNAL1ZL1xQhR")
1106+
});
1107+
1108+
#[cfg(not(feature = "dev-context-only-utils"))]
11021109
solana_pubkey::declare_id!("mustRekeyVm2QHYB3JPefBiU4BY3Z6JkW2k3Scw5GWP");
1110+
1111+
#[cfg(feature = "dev-context-only-utils")]
1112+
solana_pubkey::declare_id!("8KpruRFrT59jQ9NfFX9DU6j8a1hW7y6xchvZNQ5rxD4P");
11031113
}
11041114

11051115
pub mod disable_zk_elgamal_proof_program {

local-cluster/Cargo.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,14 @@ edition = { workspace = true }
1313
targets = ["x86_64-unknown-linux-gnu"]
1414

1515
[features]
16-
dev-context-only-utils = ["solana-core/dev-context-only-utils"]
16+
dev-context-only-utils = [
17+
"solana-core/dev-context-only-utils",
18+
"agave-feature-set/dev-context-only-utils",
19+
"solana-votor-messages/dev-context-only-utils",
20+
]
1721

1822
[dependencies]
23+
agave-feature-set = { workspace = true }
1924
bincode = { workspace = true }
2025
crossbeam-channel = { workspace = true }
2126
itertools = { workspace = true }
@@ -34,6 +39,7 @@ solana-connection-cache = { workspace = true }
3439
solana-core = { workspace = true }
3540
solana-entry = { workspace = true }
3641
solana-epoch-schedule = { workspace = true }
42+
solana-feature-gate-interface = { workspace = true }
3743
solana-genesis-config = { workspace = true }
3844
solana-gossip = { workspace = true }
3945
solana-hard-forks = { workspace = true }

local-cluster/src/integration_tests.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,9 @@ pub const RUST_LOG_FILTER: &str =
6969
pub const AG_DEBUG_LOG_FILTER: &str =
7070
"error,solana_core::replay_stage=info,solana_local_cluster=info,local_cluster=info,\
7171
solana_core::block_creation_loop=trace,solana_votor=trace,\
72-
solana_votor::vote_history_storage=info,solana_core::validator=info";
72+
solana_votor::vote_history_storage=info,solana_votor::voting_service=info,\
73+
solana_core::validator=info, solana_core::consensus=info, \
74+
solana_ledger::blockstore_processor=info";
7375
pub const DEFAULT_NODE_STAKE: u64 = 10 * LAMPORTS_PER_SOL;
7476

7577
pub fn last_vote_in_tower(tower_path: &Path, node_pubkey: &Pubkey) -> Option<(Slot, Hash)> {

local-cluster/src/local_cluster.rs

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ use {
5858
solana_transaction_error::TransportError,
5959
solana_vote_program::{
6060
vote_instruction,
61-
vote_state::{self, VoteInit},
61+
vote_state::{self, VoteInit, VoteStateTargetVersion},
6262
},
6363
solana_votor::vote_history_storage::FileVoteHistoryStorage,
6464
std::{
@@ -75,6 +75,16 @@ use {
7575
pub const DEFAULT_MINT_LAMPORTS: u64 = 10_000_000 * LAMPORTS_PER_SOL;
7676
const DUMMY_SNAPSHOT_CONFIG_PATH_MARKER: &str = "dummy";
7777

78+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79+
pub enum AlpenglowMode {
80+
/// No alpenglow - creates regular vote accounts (V3)
81+
Disabled,
82+
/// Full alpenglow - creates V4 vote accounts and activates alpenglow feature
83+
Enabled,
84+
/// Pre-migration mode - creates V4 vote accounts but does NOT activate alpenglow feature
85+
PreMigration,
86+
}
87+
7888
pub struct ClusterConfig {
7989
/// The validator config that should be applied to every node in the cluster
8090
pub validator_configs: Vec<ValidatorConfig>,
@@ -200,17 +210,33 @@ impl LocalCluster {
200210
}
201211

202212
pub fn new(config: &mut ClusterConfig, socket_addr_space: SocketAddrSpace) -> Self {
203-
Self::init(config, socket_addr_space, false)
213+
*solana_vote_program::vote_state::TEMP_HARDCODED_TARGET_VERSION
214+
.lock()
215+
.unwrap() = VoteStateTargetVersion::V3;
216+
Self::init(config, socket_addr_space, AlpenglowMode::Disabled)
204217
}
205218

206219
pub fn new_alpenglow(config: &mut ClusterConfig, socket_addr_space: SocketAddrSpace) -> Self {
207-
Self::init(config, socket_addr_space, true)
220+
*solana_vote_program::vote_state::TEMP_HARDCODED_TARGET_VERSION
221+
.lock()
222+
.unwrap() = VoteStateTargetVersion::V4;
223+
Self::init(config, socket_addr_space, AlpenglowMode::Enabled)
224+
}
225+
226+
pub fn new_pre_migration_alpenglow(
227+
config: &mut ClusterConfig,
228+
socket_addr_space: SocketAddrSpace,
229+
) -> Self {
230+
*solana_vote_program::vote_state::TEMP_HARDCODED_TARGET_VERSION
231+
.lock()
232+
.unwrap() = VoteStateTargetVersion::V4;
233+
Self::init(config, socket_addr_space, AlpenglowMode::PreMigration)
208234
}
209235

210236
pub fn init(
211237
config: &mut ClusterConfig,
212238
socket_addr_space: SocketAddrSpace,
213-
is_alpenglow: bool,
239+
alpenglow_mode: AlpenglowMode,
214240
) -> Self {
215241
assert_eq!(config.validator_configs.len(), config.node_stakes.len());
216242

@@ -318,6 +344,12 @@ impl LocalCluster {
318344
let leader_pubkey = leader_keypair.pubkey();
319345
let leader_node = Node::new_localhost_with_pubkey(&leader_pubkey);
320346

347+
// For PreMigration mode, we need to create V4 vote accounts but not activate the feature
348+
let create_v4_accounts = matches!(
349+
alpenglow_mode,
350+
AlpenglowMode::Enabled | AlpenglowMode::PreMigration
351+
);
352+
321353
let GenesisConfigInfo {
322354
mut genesis_config,
323355
mint_keypair,
@@ -327,8 +359,16 @@ impl LocalCluster {
327359
&keys_in_genesis,
328360
stakes_in_genesis,
329361
config.cluster_type,
330-
is_alpenglow,
362+
create_v4_accounts,
331363
);
364+
365+
// Remove the alpenglow feature for PreMigration mode
366+
if alpenglow_mode == AlpenglowMode::PreMigration {
367+
genesis_config
368+
.accounts
369+
.remove(&agave_feature_set::alpenglow::id());
370+
}
371+
332372
genesis_config.accounts.extend(
333373
config
334374
.additional_accounts

local-cluster/tests/local_cluster.rs

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ use {
102102
consensus_message::{
103103
CertificateType, ConsensusMessage, VoteMessage, BLS_KEYPAIR_DERIVE_SEED,
104104
},
105+
migration::MIGRATION_SLOT_OFFSET,
105106
vote::Vote,
106107
},
107108
std::{
@@ -6211,6 +6212,140 @@ fn test_alpenglow_imbalanced_stakes_catchup() {
62116212
);
62126213
}
62136214

6215+
fn test_alpenglow_migration(num_nodes: usize) {
6216+
solana_logger::setup_with_default(AG_DEBUG_LOG_FILTER);
6217+
let test_name = &format!("test_alpenglow_migration_{num_nodes}");
6218+
6219+
let vote_listener_socket = solana_net_utils::bind_to_localhost().unwrap();
6220+
let vote_listener_addr = vote_listener_socket.try_clone().unwrap();
6221+
let mut validator_config = ValidatorConfig::default_for_test();
6222+
validator_config.voting_service_test_override = Some(VotingServiceOverride {
6223+
additional_listeners: vec![vote_listener_addr.local_addr().unwrap()],
6224+
alpenglow_port_override: AlpenglowPortOverride::default(),
6225+
});
6226+
// Since we don't skip warmup slots and have very small epochs + tight MIGRATION_SLOT_OFFSET
6227+
// we can't afford any forking due to rolling start
6228+
validator_config.wait_for_supermajority = Some(0);
6229+
6230+
let validator_keys = (0..num_nodes)
6231+
.map(|i| (Arc::new(keypair_from_seed(&[i as u8; 32]).unwrap()), true))
6232+
.collect::<Vec<_>>();
6233+
let node_stakes = vec![DEFAULT_NODE_STAKE; num_nodes];
6234+
6235+
let mut cluster_config = ClusterConfig {
6236+
validator_configs: make_identical_validator_configs(&validator_config, num_nodes),
6237+
validator_keys: Some(validator_keys),
6238+
node_stakes: node_stakes.clone(),
6239+
slots_per_epoch: 2 * MINIMUM_SLOTS_PER_EPOCH,
6240+
stakers_slot_offset: 2 * MINIMUM_SLOTS_PER_EPOCH,
6241+
// So we don't have to wait so long
6242+
skip_warmup_slots: false,
6243+
..ClusterConfig::default()
6244+
};
6245+
6246+
// Create local cluster with alpenglow accounts but feature not activated
6247+
let cluster = LocalCluster::new_pre_migration_alpenglow(
6248+
&mut cluster_config,
6249+
SocketAddrSpace::Unspecified,
6250+
);
6251+
6252+
let validator_keys: Vec<Arc<Keypair>> = cluster
6253+
.validators
6254+
.values()
6255+
.map(|v| v.info.keypair.clone())
6256+
.collect();
6257+
6258+
// Send feature activation transaction
6259+
info!("Sending feature activation transaction");
6260+
let client = RpcClient::new_socket_with_commitment(
6261+
cluster.entry_point_info.rpc().unwrap(),
6262+
CommitmentConfig::processed(),
6263+
);
6264+
let faucet_keypair = &cluster.funding_keypair;
6265+
let feature_keypair = &*agave_feature_set::alpenglow::TEST_KEYPAIR;
6266+
let blockhash = client.get_latest_blockhash().unwrap();
6267+
let lamports = client
6268+
.get_minimum_balance_for_rent_exemption(solana_feature_gate_interface::Feature::size_of())
6269+
.unwrap();
6270+
6271+
let activation_message = solana_message::Message::new(
6272+
&solana_feature_gate_interface::activate_with_lamports(
6273+
&agave_feature_set::alpenglow::id(),
6274+
&faucet_keypair.pubkey(),
6275+
lamports,
6276+
),
6277+
Some(&faucet_keypair.pubkey()),
6278+
);
6279+
let activation_tx = solana_transaction::Transaction::new(
6280+
&[&feature_keypair, &faucet_keypair],
6281+
activation_message,
6282+
blockhash,
6283+
);
6284+
6285+
client.send_and_confirm_transaction(&activation_tx).unwrap();
6286+
info!("Feature activation transaction confirmed");
6287+
6288+
// Monitor for feature activation
6289+
let activation_slot;
6290+
loop {
6291+
if let Ok(account) = client.get_account(&agave_feature_set::alpenglow::id()) {
6292+
if let Some(feature) = solana_feature_gate_interface::from_account(&account) {
6293+
if let Some(slot) = feature.activated_at {
6294+
activation_slot = slot;
6295+
info!("Feature activated at slot {slot}");
6296+
break;
6297+
}
6298+
}
6299+
}
6300+
std::thread::sleep(std::time::Duration::from_millis(100));
6301+
}
6302+
6303+
// The migration happens at a fixed offset from feature activation
6304+
let migration_slot = activation_slot + MIGRATION_SLOT_OFFSET;
6305+
info!("Waiting for migration slot {migration_slot}");
6306+
6307+
loop {
6308+
let slot = client.get_slot().unwrap();
6309+
if slot >= migration_slot - 1 {
6310+
break;
6311+
}
6312+
std::thread::sleep(std::time::Duration::from_millis(100));
6313+
}
6314+
6315+
info!("Migration slot reached, checking for notarized votes");
6316+
6317+
// Check for new notarized votes
6318+
cluster.check_for_new_notarized_votes(
6319+
4,
6320+
test_name,
6321+
SocketAddrSpace::Unspecified,
6322+
vote_listener_addr,
6323+
&validator_keys,
6324+
&node_stakes,
6325+
);
6326+
6327+
// Additionally ensure that roots are being made
6328+
cluster.check_for_new_roots(8, test_name, SocketAddrSpace::Unspecified);
6329+
}
6330+
6331+
#[test]
6332+
#[serial]
6333+
fn test_alpenglow_migration_1() {
6334+
test_alpenglow_migration(1)
6335+
}
6336+
6337+
#[test]
6338+
#[serial]
6339+
fn test_alpenglow_migration_2() {
6340+
test_alpenglow_migration(2)
6341+
}
6342+
6343+
#[test]
6344+
#[serial]
6345+
fn test_alpenglow_migration_4() {
6346+
test_alpenglow_migration(4)
6347+
}
6348+
62146349
fn broadcast_vote(
62156350
message: ConsensusMessage,
62166351
tpu_socket_addrs: &[std::net::SocketAddr],

programs/vote/src/vote_processor.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ declare_process_instruction!(Entrypoint, DEFAULT_COMPUTE_UNITS, |invoke_context|
6767
}
6868

6969
// Determine the target vote state version to use for all operations.
70-
let target_version = vote_state::TEMP_HARDCODED_TARGET_VERSION;
70+
let target_version = *vote_state::TEMP_HARDCODED_TARGET_VERSION.lock().unwrap();
7171

7272
let signers = instruction_context.get_signers()?;
7373
match limited_deserialize(data, solana_packet::PACKET_DATA_SIZE as u64)? {

programs/vote/src/vote_state/mod.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,12 @@ use {
2323
std::{
2424
cmp::Ordering,
2525
collections::{HashSet, VecDeque},
26+
sync::{LazyLock, Mutex},
2627
},
2728
};
2829

29-
// TODO: Change me once the program has full v4 feature gate support.
30-
pub(crate) const TEMP_HARDCODED_TARGET_VERSION: VoteStateTargetVersion = VoteStateTargetVersion::V3;
30+
pub static TEMP_HARDCODED_TARGET_VERSION: LazyLock<Mutex<VoteStateTargetVersion>> =
31+
LazyLock::new(|| Mutex::new(VoteStateTargetVersion::V3));
3132

3233
// utility function, used by Stakes, tests
3334
pub fn from<T: ReadableAccount>(account: &T) -> Option<VoteStateV3> {

votor-messages/src/migration.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,13 @@ use {solana_bls_signatures::Signature as BLSSignature, solana_hash::Hash};
4444
/// The slot offset post feature flag activation to begin the migration.
4545
/// Epoch boundaries induce heavy computation often resulting in forks. It's best to decouple the migration period
4646
/// from the boundary. We require that a root is made between the epoch boundary and this migration slot offset.
47+
#[cfg(not(feature = "dev-context-only-utils"))]
4748
pub const MIGRATION_SLOT_OFFSET: Slot = 5000;
4849

50+
/// Small offset for tests
51+
#[cfg(feature = "dev-context-only-utils")]
52+
pub const MIGRATION_SLOT_OFFSET: Slot = 32;
53+
4954
/// We match Alpenglow's 20 + 20 model, by allowing a maximum of 20% malicious stake during the migration.
5055
pub const MIGRATION_MALICIOUS_THRESHOLD: f64 = 20.0 / 100.0;
5156

0 commit comments

Comments
 (0)