Skip to main content

solana_runtime/
genesis_utils.rs

1#[expect(deprecated)]
2use solana_stake_interface::config::Config as StakeConfig;
3use {
4    crate::{
5        bank::DEFAULT_VAT_TO_BURN_PER_EPOCH,
6        block_component_processor::vote_reward::epoch_inflation_account_state::EpochInflationAccountState,
7        stake_utils,
8    },
9    agave_feature_set::{FEATURE_NAMES, FeatureSet},
10    agave_votor_messages::{
11        self,
12        consensus_message::{BLS_KEYPAIR_DERIVE_SEED, Block},
13        migration::GENESIS_CERTIFICATE_ACCOUNT,
14        wire::{WireBlockCertMessage, WireCertSignature},
15    },
16    bincode::serialize,
17    bitvec::vec::BitVec,
18    log::*,
19    solana_account::{
20        Account, AccountSharedData, ReadableAccount, state_traits::StateMutWincode as _,
21    },
22    solana_bls_signatures::{
23        BLS_SIGNATURE_AFFINE_SIZE, Pubkey as BLSPubkey, Signature as BLSSignature,
24        keypair::Keypair as BLSKeypair, pubkey::PubkeyCompressed as BLSPubkeyCompressed,
25    },
26    solana_clock::Epoch,
27    solana_cluster_type::ClusterType,
28    solana_config_interface::state::ConfigKeys,
29    solana_feature_gate_interface::{self as feature, Feature},
30    solana_fee_calculator::FeeRateGovernor,
31    solana_genesis_config::GenesisConfig,
32    solana_hash::Hash,
33    solana_keypair::Keypair,
34    solana_native_token::LAMPORTS_PER_SOL,
35    solana_pubkey::Pubkey,
36    solana_rent::Rent,
37    solana_sdk_ids::{stake as stake_program, sysvar},
38    solana_seed_derivable::SeedDerivable,
39    solana_signer::Signer,
40    solana_signer_store::encode_base2,
41    solana_stake_interface::state::{Authorized, Lockup, Meta, StakeStateV2},
42    solana_system_interface::program as system_program,
43    solana_sysvar::epoch_rewards,
44    solana_vote_interface::state::{BLS_PUBLIC_KEY_COMPRESSED_SIZE, VoteStateV4},
45    solana_vote_program::vote_state,
46    std::{borrow::Borrow, sync::Arc},
47};
48
49// Default amount received by the validator
50const VALIDATOR_LAMPORTS: u64 = 890_880;
51const MINT_KEYPAIR_SEED: [u8; 32] = [
52    0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,
53    26, 27, 28, 29, 30, 31,
54];
55const VALIDATOR_STAKE_KEYPAIR_SEED: [u8; 32] = [
56    64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87,
57    88, 89, 90, 91, 92, 93, 94, 95,
58];
59
60// Default minimum vote account balance used by tests/genesis helpers. This is
61// conservative once shorter slot-time regimes lower the live bank VAT burn.
62pub fn minimum_vote_account_balance_for_vat(num_epochs: Epoch) -> u64 {
63    DEFAULT_VAT_TO_BURN_PER_EPOCH * num_epochs
64        + Rent::default().minimum_balance(VoteStateV4::size_of())
65}
66
67// Minimum stake lamports required for a valid stake account with non-zero stake.
68// This is rent_exempt_reserve + 1 lamport of actual stake.
69pub fn minimum_stake_lamports_for_vat(rent: &Rent) -> u64 {
70    rent.minimum_balance(StakeStateV2::size_of()) + 1
71}
72
73pub fn bootstrap_validator_stake_lamports() -> u64 {
74    minimum_stake_lamports_for_vat(&Rent::default())
75}
76
77// Number of lamports automatically used for genesis accounts
78pub const fn genesis_sysvar_and_builtin_program_lamports() -> u64 {
79    const NUM_BUILTIN_PROGRAMS: u64 = 6;
80    const NUM_PRECOMPILES: u64 = 3;
81    const STAKE_HISTORY_MIN_BALANCE: u64 = 114_979_200;
82    const CLOCK_SYSVAR_MIN_BALANCE: u64 = 1_169_280;
83    const RENT_SYSVAR_MIN_BALANCE: u64 = 1_009_200;
84    const EPOCH_SCHEDULE_SYSVAR_MIN_BALANCE: u64 = 1_120_560;
85    const RECENT_BLOCKHASHES_SYSVAR_MIN_BALANCE: u64 = 42_706_560;
86    const LAST_RESTART_SLOT_SYSVAR_MIN_BALANCE: u64 = 946_560;
87
88    STAKE_HISTORY_MIN_BALANCE
89        + CLOCK_SYSVAR_MIN_BALANCE
90        + RENT_SYSVAR_MIN_BALANCE
91        + EPOCH_SCHEDULE_SYSVAR_MIN_BALANCE
92        + RECENT_BLOCKHASHES_SYSVAR_MIN_BALANCE
93        + LAST_RESTART_SLOT_SYSVAR_MIN_BALANCE
94        + NUM_BUILTIN_PROGRAMS
95        + NUM_PRECOMPILES
96}
97
98#[derive(Debug)]
99pub struct ValidatorVoteKeypairs {
100    pub node_keypair: Keypair,
101    pub vote_keypair: Keypair,
102    pub stake_keypair: Keypair,
103    pub bls_keypair: BLSKeypair,
104}
105
106impl ValidatorVoteKeypairs {
107    pub fn new(node_keypair: Keypair, vote_keypair: Keypair, stake_keypair: Keypair) -> Self {
108        let bls_keypair =
109            BLSKeypair::derive_from_signer(&vote_keypair, BLS_KEYPAIR_DERIVE_SEED).unwrap();
110        Self {
111            node_keypair,
112            vote_keypair,
113            stake_keypair,
114            bls_keypair,
115        }
116    }
117
118    pub fn new_rand() -> Self {
119        let node_keypair = Keypair::new();
120        let vote_keypair = Keypair::new();
121        let stake_keypair = Keypair::new();
122        Self::new(node_keypair, vote_keypair, stake_keypair)
123    }
124}
125
126pub struct GenesisConfigInfo {
127    pub genesis_config: GenesisConfig,
128    pub mint_keypair: Keypair,
129    pub voting_keypair: Keypair,
130    pub validator_pubkey: Pubkey,
131}
132
133pub fn create_genesis_config(mint_lamports: u64) -> GenesisConfigInfo {
134    // Note that zero lamports for validator stake will result in stake account
135    // not being stored in accounts-db but still cached in bank stakes. This
136    // causes discrepancy between cached stakes accounts in bank and
137    // accounts-db which in particular will break snapshots test.
138    create_genesis_config_with_leader(
139        mint_lamports,
140        &solana_pubkey::new_rand(), // validator_pubkey
141        0,                          // validator_stake_lamports
142    )
143}
144
145pub fn create_genesis_config_with_vote_accounts(
146    mint_lamports: u64,
147    voting_keypairs: &[impl Borrow<ValidatorVoteKeypairs>],
148    stakes: Vec<u64>,
149) -> GenesisConfigInfo {
150    create_genesis_config_with_vote_accounts_and_cluster_type(
151        mint_lamports,
152        voting_keypairs,
153        stakes,
154        ClusterType::Development,
155        &FeatureSet::all_enabled(),
156        false,
157    )
158}
159
160#[cfg(feature = "dev-context-only-utils")]
161pub fn create_genesis_config_with_alpenglow_vote_accounts(
162    mint_lamports: u64,
163    voting_keypairs: &[impl Borrow<ValidatorVoteKeypairs>],
164    stakes: Vec<u64>,
165) -> GenesisConfigInfo {
166    create_genesis_config_with_vote_accounts_and_cluster_type(
167        mint_lamports,
168        voting_keypairs,
169        stakes,
170        ClusterType::Development,
171        &FeatureSet::all_enabled(),
172        true,
173    )
174}
175
176pub fn create_genesis_config_with_vote_accounts_and_cluster_type(
177    mint_lamports: u64,
178    voting_keypairs: &[impl Borrow<ValidatorVoteKeypairs>],
179    stakes: Vec<u64>,
180    cluster_type: ClusterType,
181    feature_set: &FeatureSet,
182    is_alpenglow: bool,
183) -> GenesisConfigInfo {
184    assert!(!voting_keypairs.is_empty());
185    assert_eq!(voting_keypairs.len(), stakes.len());
186
187    // Use deterministic keypair so we don't get confused by randomness in tests
188    let mint_keypair = Keypair::from_seed(&MINT_KEYPAIR_SEED).unwrap();
189    let voting_keypair = voting_keypairs[0].borrow().vote_keypair.insecure_clone();
190
191    let validator_pubkey = voting_keypairs[0].borrow().node_keypair.pubkey();
192    let validator_bls_pubkey = Some(
193        voting_keypairs[0]
194            .borrow()
195            .bls_keypair
196            .public
197            .to_bytes_compressed(),
198    );
199    let mut genesis_config = create_genesis_config_with_leader_ex(
200        mint_lamports,
201        &mint_keypair.pubkey(),
202        &validator_pubkey,
203        &voting_keypairs[0].borrow().vote_keypair.pubkey(),
204        &voting_keypairs[0].borrow().stake_keypair.pubkey(),
205        validator_bls_pubkey,
206        stakes[0],
207        VALIDATOR_LAMPORTS,
208        FeeRateGovernor::new(0, 0), // most tests can't handle transaction fees
209        Rent::free(),               // most tests don't expect rent
210        cluster_type,
211        feature_set,
212        vec![],
213    );
214
215    if is_alpenglow {
216        activate_all_features_alpenglow(&mut genesis_config);
217    }
218
219    let mut genesis_config_info = GenesisConfigInfo {
220        genesis_config,
221        mint_keypair,
222        voting_keypair,
223        validator_pubkey,
224    };
225
226    for (validator_voting_keypairs, &stake) in voting_keypairs[1..].iter().zip(&stakes[1..]) {
227        let node_pubkey = validator_voting_keypairs.borrow().node_keypair.pubkey();
228        let vote_pubkey = validator_voting_keypairs.borrow().vote_keypair.pubkey();
229        let stake_pubkey = validator_voting_keypairs.borrow().stake_keypair.pubkey();
230        let bls_pubkey = validator_voting_keypairs
231            .borrow()
232            .bls_keypair
233            .public
234            .to_bytes_compressed();
235
236        // Ensure minimum lamports for VAT filtering, but only when stake > 0.
237        // When stake is explicitly 0, respect that (e.g., for testing unstaked validator filtering).
238        let rent = &genesis_config_info.genesis_config.rent;
239        let (vote_account_lamports, stake_lamports) = if stake > 0 {
240            (
241                stake.max(minimum_vote_account_balance_for_vat(100)),
242                stake.max(minimum_stake_lamports_for_vat(rent)),
243            )
244        } else {
245            // Zero stake - just need rent exemption, no VAT minimums
246            (
247                rent.minimum_balance(VoteStateV4::size_of()),
248                rent.minimum_balance(StakeStateV2::size_of()),
249            )
250        };
251
252        let accounts = create_validator(
253            rent,
254            node_pubkey,
255            VALIDATOR_LAMPORTS,
256            vote_pubkey,
257            vote_account_lamports,
258            stake_pubkey,
259            stake_lamports,
260            Some(bls_pubkey),
261        )
262        .into_iter()
263        .map(|(pubkey, account)| (pubkey, Account::from(account)));
264        genesis_config_info.genesis_config.accounts.extend(accounts);
265    }
266
267    genesis_config_info
268}
269
270pub fn create_genesis_config_with_leader(
271    mint_lamports: u64,
272    validator_pubkey: &Pubkey,
273    validator_stake_lamports: u64,
274) -> GenesisConfigInfo {
275    // Use deterministic keypair so we don't get confused by randomness in tests
276    let mint_keypair = Keypair::from_seed(&MINT_KEYPAIR_SEED).unwrap();
277
278    create_genesis_config_with_leader_with_mint_keypair(
279        mint_keypair,
280        mint_lamports,
281        validator_pubkey,
282        validator_stake_lamports,
283    )
284}
285
286pub fn create_genesis_config_with_leader_with_mint_keypair(
287    mint_keypair: Keypair,
288    mint_lamports: u64,
289    validator_pubkey: &Pubkey,
290    validator_stake_lamports: u64,
291) -> GenesisConfigInfo {
292    // Use deterministic keypair so we don't get confused by randomness in tests
293    let voting_keypair = Keypair::from_seed(&[
294        32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54,
295        55, 56, 57, 58, 59, 60, 61, 62, 63,
296    ])
297    .unwrap();
298
299    let bls_keypair =
300        BLSKeypair::derive_from_signer(&voting_keypair, BLS_KEYPAIR_DERIVE_SEED).unwrap();
301    let validator_bls_pubkey = Some(bls_keypair.public.to_bytes_compressed());
302    let stake_pubkey = Keypair::from_seed(&VALIDATOR_STAKE_KEYPAIR_SEED)
303        .unwrap()
304        .pubkey();
305
306    let genesis_config = create_genesis_config_with_leader_ex(
307        mint_lamports,
308        &mint_keypair.pubkey(),
309        validator_pubkey,
310        &voting_keypair.pubkey(),
311        &stake_pubkey,
312        validator_bls_pubkey,
313        validator_stake_lamports,
314        VALIDATOR_LAMPORTS,
315        FeeRateGovernor::new(0, 0), // most tests can't handle transaction fees
316        Rent::free(),               // most tests don't expect rent
317        ClusterType::Development,
318        &FeatureSet::all_enabled(),
319        vec![],
320    );
321
322    GenesisConfigInfo {
323        genesis_config,
324        mint_keypair,
325        voting_keypair,
326        validator_pubkey: *validator_pubkey,
327    }
328}
329
330pub fn activate_all_features_alpenglow(genesis_config: &mut GenesisConfig) {
331    do_activate_all_features::<true>(genesis_config);
332    configure_alpenglow_at_genesis(genesis_config);
333}
334
335pub fn activate_alpenglow_at_genesis(genesis_config: &mut GenesisConfig) {
336    activate_feature(genesis_config, agave_feature_set::alpenglow::id());
337    configure_alpenglow_at_genesis(genesis_config);
338}
339
340fn configure_alpenglow_at_genesis(genesis_config: &mut GenesisConfig) {
341    // PoH is in low power mode
342    genesis_config.poh_config.hashes_per_tick = None;
343
344    // This is a dev cluster with alpenglow enabled at genesis. We don't want to test the migration pathway
345    // so we add a fake genesis certificate.
346    let cert = WireBlockCertMessage {
347        block: Block {
348            slot: 0,
349            block_id: Hash::default(),
350        },
351        signature: WireCertSignature {
352            signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]),
353            bitmap: encode_base2(&BitVec::new()).unwrap(),
354        },
355    };
356    let cert_size = bincode::serialized_size(&cert).unwrap();
357    let lamports = Rent::default().minimum_balance(cert_size as usize);
358    let certificate_account = Account::new_data(lamports, &cert, &system_program::ID).unwrap();
359
360    genesis_config
361        .accounts
362        .insert(*GENESIS_CERTIFICATE_ACCOUNT, certificate_account);
363    EpochInflationAccountState::insert_into_genesis_config(genesis_config);
364}
365
366pub fn activate_all_features(genesis_config: &mut GenesisConfig) {
367    do_activate_all_features::<false>(genesis_config);
368}
369
370fn do_activate_all_features<const IS_ALPENGLOW: bool>(genesis_config: &mut GenesisConfig) {
371    // Activate all features at genesis in development mode
372    for feature_id in FeatureSet::default().inactive() {
373        if IS_ALPENGLOW || *feature_id != agave_feature_set::alpenglow::id() {
374            activate_feature(genesis_config, *feature_id);
375        }
376    }
377}
378
379pub fn deactivate_features(
380    genesis_config: &mut GenesisConfig,
381    features_to_deactivate: &Vec<Pubkey>,
382) {
383    // Remove all features in `features_to_skip` from genesis
384    for deactivate_feature_pk in features_to_deactivate {
385        if FEATURE_NAMES.contains_key(deactivate_feature_pk) {
386            genesis_config.accounts.remove(deactivate_feature_pk);
387        } else {
388            warn!(
389                "Feature {deactivate_feature_pk:?} set for deactivation is not a known Feature \
390                 public key"
391            );
392        }
393    }
394}
395
396pub fn activate_feature(genesis_config: &mut GenesisConfig, feature_id: Pubkey) {
397    genesis_config.accounts.insert(
398        feature_id,
399        Account::from(feature::create_account(
400            &Feature {
401                activated_at: Some(0),
402            },
403            std::cmp::max(genesis_config.rent.minimum_balance(Feature::size_of()), 1),
404        )),
405    );
406}
407
408pub fn bls_pubkey_to_compressed_bytes(
409    bls_pubkey: &BLSPubkey,
410) -> [u8; BLS_PUBLIC_KEY_COMPRESSED_SIZE] {
411    let key = BLSPubkeyCompressed::try_from(bls_pubkey).unwrap();
412    bincode::serialize(&key).unwrap().try_into().unwrap()
413}
414
415pub(crate) fn create_validator(
416    rent: &Rent,
417    node_pubkey: Pubkey,
418    node_lamports: u64,
419    vote_pubkey: Pubkey,
420    vote_lamports: u64,
421    stake_pubkey: Pubkey,
422    stake_lamports: u64,
423    bls_pubkey: Option<[u8; BLS_PUBLIC_KEY_COMPRESSED_SIZE]>,
424) -> Vec<(Pubkey, AccountSharedData)> {
425    let vote_account = vote_state::create_v4_account_with_authorized(
426        &node_pubkey,
427        &vote_pubkey,
428        bls_pubkey.unwrap_or([0u8; BLS_PUBLIC_KEY_COMPRESSED_SIZE]),
429        &vote_pubkey,
430        0,
431        &vote_pubkey,
432        10_000,
433        &node_pubkey,
434        vote_lamports,
435    );
436
437    let stake_account = stake_utils::create_stake_account(
438        &stake_pubkey,
439        &vote_pubkey,
440        &vote_account,
441        rent,
442        stake_lamports,
443    );
444
445    let node_account = AccountSharedData::new(node_lamports, 0, &system_program::id());
446
447    vec![
448        (vote_pubkey, vote_account),
449        (stake_pubkey, stake_account),
450        (node_pubkey, node_account),
451    ]
452}
453
454#[expect(clippy::too_many_arguments)]
455pub fn create_genesis_config_with_leader_ex_no_features(
456    mint_lamports: u64,
457    mint_pubkey: &Pubkey,
458    validator_pubkey: &Pubkey,
459    validator_vote_account_pubkey: &Pubkey,
460    validator_stake_account_pubkey: &Pubkey,
461    validator_bls_pubkey: Option<[u8; BLS_PUBLIC_KEY_COMPRESSED_SIZE]>,
462    validator_stake_lamports: u64,
463    validator_lamports: u64,
464    fee_rate_governor: FeeRateGovernor,
465    rent: Rent,
466    cluster_type: ClusterType,
467    mut initial_accounts: Vec<(Pubkey, AccountSharedData)>,
468) -> GenesisConfig {
469    // Ensure minimum lamports for VAT filtering, but only when stake > 0.
470    // VAT requires non-zero stake, a BLS pubkey, and lamports >= the bank's
471    // current VAT burn plus rent-exempt minimum. This helper funds with the
472    // conservative default burn amount.
473    let (vote_account_lamports, stake_lamports) = if validator_stake_lamports > 0 {
474        (
475            validator_stake_lamports.max(minimum_vote_account_balance_for_vat(100)),
476            validator_stake_lamports.max(minimum_stake_lamports_for_vat(&rent)),
477        )
478    } else {
479        // Zero stake - just need rent exemption, no VAT minimums
480        (
481            rent.minimum_balance(VoteStateV4::size_of()),
482            rent.minimum_balance(StakeStateV2::size_of()),
483        )
484    };
485
486    initial_accounts.push((
487        *mint_pubkey,
488        AccountSharedData::new(mint_lamports, 0, &system_program::id()),
489    ));
490    let mut validator_accounts = create_validator(
491        &rent,
492        *validator_pubkey,
493        validator_lamports,
494        *validator_vote_account_pubkey,
495        vote_account_lamports,
496        *validator_stake_account_pubkey,
497        stake_lamports,
498        validator_bls_pubkey,
499    );
500    initial_accounts.append(&mut validator_accounts);
501
502    let native_mint_account = solana_account::AccountSharedData::from(Account {
503        owner: spl_generic_token::token::id(),
504        data: spl_generic_token::token::native_mint::ACCOUNT_DATA.to_vec(),
505        lamports: LAMPORTS_PER_SOL,
506        executable: false,
507        rent_epoch: 1,
508    });
509    initial_accounts.push((
510        spl_generic_token::token::native_mint::id(),
511        native_mint_account,
512    ));
513
514    let mut genesis_config = GenesisConfig {
515        accounts: initial_accounts
516            .iter()
517            .cloned()
518            .map(|(key, account)| (key, Account::from(account)))
519            .collect(),
520        fee_rate_governor,
521        rent,
522        cluster_type,
523        ..GenesisConfig::default()
524    };
525
526    add_genesis_stake_config_account(&mut genesis_config);
527    add_genesis_epoch_rewards_account(&mut genesis_config);
528
529    genesis_config
530}
531
532#[expect(clippy::too_many_arguments)]
533pub fn create_genesis_config_with_leader_ex(
534    mint_lamports: u64,
535    mint_pubkey: &Pubkey,
536    validator_pubkey: &Pubkey,
537    validator_vote_account_pubkey: &Pubkey,
538    validator_stake_account_pubkey: &Pubkey,
539    validator_bls_pubkey: Option<[u8; BLS_PUBLIC_KEY_COMPRESSED_SIZE]>,
540    validator_stake_lamports: u64,
541    validator_lamports: u64,
542    fee_rate_governor: FeeRateGovernor,
543    rent: Rent,
544    cluster_type: ClusterType,
545    feature_set: &FeatureSet,
546    initial_accounts: Vec<(Pubkey, AccountSharedData)>,
547) -> GenesisConfig {
548    let mut genesis_config = create_genesis_config_with_leader_ex_no_features(
549        mint_lamports,
550        mint_pubkey,
551        validator_pubkey,
552        validator_vote_account_pubkey,
553        validator_stake_account_pubkey,
554        validator_bls_pubkey,
555        validator_stake_lamports,
556        validator_lamports,
557        fee_rate_governor,
558        rent,
559        cluster_type,
560        initial_accounts,
561    );
562
563    for feature_id in feature_set.active().keys() {
564        // Skip alpenglow (existing behavior)
565        if *feature_id == agave_feature_set::alpenglow::id() {
566            continue;
567        }
568        activate_feature(&mut genesis_config, *feature_id);
569    }
570
571    genesis_config
572}
573
574#[expect(deprecated)]
575pub fn add_genesis_stake_config_account(genesis_config: &mut GenesisConfig) -> u64 {
576    let mut data = serialize(&ConfigKeys { keys: vec![] }).unwrap();
577    data.extend_from_slice(&serialize(&StakeConfig::default()).unwrap());
578    let lamports = std::cmp::max(genesis_config.rent.minimum_balance(data.len()), 1);
579    let account = AccountSharedData::from(Account {
580        lamports,
581        data,
582        owner: solana_sdk_ids::config::id(),
583        ..Account::default()
584    });
585
586    genesis_config.add_account(solana_stake_interface::config::id(), account);
587
588    lamports
589}
590
591pub fn add_genesis_epoch_rewards_account(genesis_config: &mut GenesisConfig) -> u64 {
592    let data = vec![0; epoch_rewards::SIZE];
593    let lamports = std::cmp::max(genesis_config.rent.minimum_balance(data.len()), 1);
594
595    let account = AccountSharedData::create_from_existing_shared_data(
596        lamports,
597        Arc::new(data),
598        sysvar::id(),
599        false,
600        u64::MAX,
601    );
602
603    genesis_config.add_account(epoch_rewards::id(), account);
604
605    lamports
606}
607
608// genesis investor accounts
609pub fn create_lockup_stake_account(
610    authorized: &Authorized,
611    lockup: &Lockup,
612    rent: &Rent,
613    lamports: u64,
614) -> AccountSharedData {
615    let mut stake_account =
616        AccountSharedData::new(lamports, StakeStateV2::size_of(), &stake_program::id());
617
618    let rent_exempt_reserve = rent.minimum_balance(stake_account.data().len());
619    assert!(
620        lamports >= rent_exempt_reserve,
621        "lamports: {lamports} is less than rent_exempt_reserve {rent_exempt_reserve}"
622    );
623
624    stake_account
625        .set_state(&StakeStateV2::Initialized(Meta {
626            authorized: *authorized,
627            lockup: *lockup,
628            #[expect(deprecated)]
629            rent_exempt_reserve,
630        }))
631        .expect("set_state");
632
633    stake_account
634}