Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions interface/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,11 @@ impl Authorized {
derive(serde_derive::Deserialize, serde_derive::Serialize)
)]
pub struct Meta {
#[deprecated(
since = "3.0.1",
note = "Stake account rent must be calculated via the `Rent` sysvar. \
This value will cease to be correct once lamports-per-byte is adjusted."
)]
Comment on lines +418 to +422
Copy link
Member Author

@2501babe 2501babe Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

youll notice we use lamports_per_byte_year in interface.rs. the lamports_per_byte rename comes in solana-rent v4 which landed in agave v4 so i chose to use this term in comments. we may or may not bump solana-rent before release, but its out of scope for this pr

it not unsafe to keep using solana-rent v3 because the underlying onchain bytes just change to a lamports_per_byte_year twice the historical value and an exemption_threshold of 1

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that's totally fine, we've been adopting that language everywhere already

pub rent_exempt_reserve: u64,
pub authorized: Authorized,
pub lockup: Lockup,
Expand Down
1 change: 1 addition & 0 deletions program/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ agave-feature-set = "3.0.0"
arbitrary = { version = "1.4.2", features = ["derive"] }
assert_matches = "1.5.0"
mollusk-svm = { version = "0.7.2", features = ["all-builtins"] }
mollusk-svm-result = "0.7.2"
proptest = "1.10.0"
rand = "0.10.0"
solana-account = { version = "3.2.0", features = ["bincode"] }
Expand Down
6 changes: 3 additions & 3 deletions program/src/helpers/delegate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use {
solana_pubkey::Pubkey,
solana_stake_interface::{
error::StakeError,
state::{Delegation, Meta, Stake},
state::{Delegation, Stake},
},
};

Expand All @@ -32,9 +32,9 @@ pub(crate) fn new_stake(
/// an error.
pub(crate) fn validate_delegated_amount(
account: &AccountInfo,
meta: &Meta,
rent_exempt_reserve: u64,
) -> Result<ValidatedDelegatedInfo, ProgramError> {
let stake_amount = account.lamports().saturating_sub(meta.rent_exempt_reserve); // can't stake the rent
let stake_amount = account.lamports().saturating_sub(rent_exempt_reserve); // can't stake the rent

// Stake accounts may be initialized with a stake amount below the minimum
// delegation so check that the minimum is met before delegation.
Expand Down
85 changes: 57 additions & 28 deletions program/src/helpers/merge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,23 @@ use {
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum MergeKind {
Inactive(Meta, u64, StakeFlags),
ActivationEpoch(Meta, Stake, StakeFlags),
ActivationEpoch(Meta, Stake, u64, StakeFlags),
FullyActive(Meta, Stake),
}

impl MergeKind {
pub(crate) fn meta(&self) -> &Meta {
match self {
Self::Inactive(meta, _, _) => meta,
Self::ActivationEpoch(meta, _, _) => meta,
Self::ActivationEpoch(meta, _, _, _) => meta,
Self::FullyActive(meta, _) => meta,
}
}

pub(crate) fn active_stake(&self) -> Option<&Stake> {
match self {
Self::Inactive(_, _, _) => None,
Self::ActivationEpoch(_, stake, _) => Some(stake),
Self::ActivationEpoch(_, stake, _, _) => Some(stake),
Self::FullyActive(_, stake) => Some(stake),
}
}
Expand All @@ -51,7 +51,12 @@ impl MergeKind {

match (status.effective, status.activating, status.deactivating) {
(0, 0, 0) => Ok(Self::Inactive(*meta, stake_lamports, *stake_flags)),
(0, _, _) => Ok(Self::ActivationEpoch(*meta, *stake, *stake_flags)),
(0, _, _) => Ok(Self::ActivationEpoch(
*meta,
*stake,
stake_lamports,
*stake_flags,
)),
(_, 0, 0) => Ok(Self::FullyActive(*meta, *stake)),
_ => {
let err = StakeError::MergeTransientStake;
Expand Down Expand Up @@ -114,9 +119,9 @@ impl MergeKind {
.unwrap_or(Ok(()))?;
let merged_state = match (self, source) {
(Self::Inactive(_, _, _), Self::Inactive(_, _, _)) => None,
(Self::Inactive(_, _, _), Self::ActivationEpoch(_, _, _)) => None,
(Self::Inactive(_, _, _), Self::ActivationEpoch(_, _, _, _)) => None,
(
Self::ActivationEpoch(meta, mut stake, stake_flags),
Self::ActivationEpoch(meta, mut stake, _, stake_flags),
Self::Inactive(_, source_lamports, source_stake_flags),
) => {
stake.delegation.stake = checked_add(stake.delegation.stake, source_lamports)?;
Expand All @@ -127,13 +132,9 @@ impl MergeKind {
))
}
(
Self::ActivationEpoch(meta, mut stake, stake_flags),
Self::ActivationEpoch(source_meta, source_stake, source_stake_flags),
Self::ActivationEpoch(meta, mut stake, _, stake_flags),
Self::ActivationEpoch(_, source_stake, source_lamports, source_stake_flags),
) => {
let source_lamports = checked_add(
source_meta.rent_exempt_reserve,
source_stake.delegation.stake,
)?;
Comment on lines -133 to -136
Copy link
Member Author

@2501babe 2501babe Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as a nice little side effect, we now merge all lamports from an activating source into the delegation of an activating destination instead of leaving out non-rent non-stake lamports

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice! I hadn't even considered that part

merge_delegation_stake_and_credits_observed(
&mut stake,
source_lamports,
Expand Down Expand Up @@ -525,7 +526,7 @@ mod tests {
&stake_history
)
.unwrap(),
MergeKind::ActivationEpoch(meta, stake, StakeFlags::empty()),
MergeKind::ActivationEpoch(meta, stake, stake_account.lamports(), StakeFlags::empty()),
);

// all paritially activated, transient epochs fail
Expand Down Expand Up @@ -653,20 +654,31 @@ mod tests {
#[test]
fn test_merge_kind_merge() {
let clock = Clock::default();
let lamports = 424242;
let rent_exempt_reserve = 42;
let activating_stake = 4242;
let inactive_total_lamports = 424242;
let meta = Meta {
rent_exempt_reserve: 42,
rent_exempt_reserve,
..Meta::default()
};
let stake = Stake {
delegation: Delegation {
stake: 4242,
stake: activating_stake,
..Delegation::default()
},
..Stake::default()
};
let inactive = MergeKind::Inactive(Meta::default(), lamports, StakeFlags::empty());
let activation_epoch = MergeKind::ActivationEpoch(meta, stake, StakeFlags::empty());
let inactive = MergeKind::Inactive(
Meta::default(),
inactive_total_lamports,
StakeFlags::empty(),
);
let activation_epoch = MergeKind::ActivationEpoch(
meta,
stake,
activating_stake + rent_exempt_reserve,
StakeFlags::empty(),
);
let fully_active = MergeKind::FullyActive(meta, stake);

assert_eq!(
Expand Down Expand Up @@ -703,26 +715,23 @@ mod tests {
.unwrap()
.unwrap();
let delegation = new_state.delegation().unwrap();
assert_eq!(delegation.stake, stake.delegation.stake + lamports);
assert_eq!(delegation.stake, activating_stake + inactive_total_lamports);

let new_state = activation_epoch
.clone()
.merge(activation_epoch, &clock)
.unwrap()
.unwrap();
let delegation = new_state.delegation().unwrap();
assert_eq!(
delegation.stake,
2 * stake.delegation.stake + meta.rent_exempt_reserve
);
assert_eq!(delegation.stake, 2 * activating_stake + rent_exempt_reserve);

let new_state = fully_active
.clone()
.merge(fully_active, &clock)
.unwrap()
.unwrap();
let delegation = new_state.delegation().unwrap();
assert_eq!(delegation.stake, 2 * stake.delegation.stake);
assert_eq!(delegation.stake, 2 * activating_stake);
}

#[test]
Expand Down Expand Up @@ -752,8 +761,18 @@ mod tests {
};

// activating stake merge, match credits observed
let activation_epoch_a = MergeKind::ActivationEpoch(meta, stake_a, StakeFlags::empty());
let activation_epoch_b = MergeKind::ActivationEpoch(meta, stake_b, StakeFlags::empty());
let activation_epoch_a = MergeKind::ActivationEpoch(
meta,
stake_a,
delegation_a + rent_exempt_reserve,
StakeFlags::empty(),
);
let activation_epoch_b = MergeKind::ActivationEpoch(
meta,
stake_b,
delegation_b + rent_exempt_reserve,
StakeFlags::empty(),
);
let new_stake = activation_epoch_a
.merge(activation_epoch_b, &clock)
.unwrap()
Expand Down Expand Up @@ -787,8 +806,18 @@ mod tests {
},
credits_observed: credits_b,
};
let activation_epoch_a = MergeKind::ActivationEpoch(meta, stake_a, StakeFlags::empty());
let activation_epoch_b = MergeKind::ActivationEpoch(meta, stake_b, StakeFlags::empty());
let activation_epoch_a = MergeKind::ActivationEpoch(
meta,
stake_a,
delegation_a + rent_exempt_reserve,
StakeFlags::empty(),
);
let activation_epoch_b = MergeKind::ActivationEpoch(
meta,
stake_b,
delegation_b + rent_exempt_reserve,
StakeFlags::empty(),
);
let new_stake = activation_epoch_a
.merge(activation_epoch_b, &clock)
.unwrap()
Expand Down
9 changes: 9 additions & 0 deletions program/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ solana_pubkey::declare_id!("Stake11111111111111111111111111111111111111");
// we can pretend the rate has always beein 9% without issue. so we do that
const PERPETUAL_NEW_WARMUP_COOLDOWN_RATE_EPOCH: Option<u64> = Some(0);

// Historically, `Meta.rent_exempt_reserve` contained the canonical rent
// reservation for a stake account. This implicitly depended on
// lamports-per-byte remaining fixed over time. This value will be allowed
// to fluctuate, which means the stake program must calculate rent from the
// `Rent` sysvar directly. However, downstream programs may still rely on the
// `Meta` value being set. For maximum predictability, we set `rent_exempt_reserve`
// to its historical value unconditionally, but ignore it in the stake program.
const PSEUDO_RENT_EXEMPT_RESERVE: u64 = 2_282_880;

/// The minimum stake amount that can be delegated, in lamports.
/// NOTE: This is also used to calculate the minimum balance of a delegated
/// stake account, which is the rent exempt reserve _plus_ the minimum stake
Expand Down
Loading