Skip to content

Conversation

@rustopian
Copy link
Contributor

Part 2 of 9 in this project.

(diff is cumulative)

Problem

solana-program-test is on its slow way out.

Solution

Convert these tests to Mollusk, including simple but perfectly equivalent stake tracking.

This part migrates Deactivate tests and updates to full StakeLifecycle management. It does not yet add stake tracking when warping across slots; that is added in the next PR.

Included:

  • Full StakeLifecycle management
  • Deactivate tests moved from program_test.rs to deactivate.rs
  • Some additional common helpers needed by the above
    This does NOT migrate the tests from the newer stake_instruction.rs, which already uses Mollusk.

@rustopian rustopian changed the title tests: Mollusk Deactivate tests (2/9) tests: Mollusk StakeLifecycle, Deactivate tests (2/9) Dec 18, 2025
@rustopian rustopian force-pushed the mollusk2-deactivate branch 2 times, most recently from ac97793 to 80abfd2 Compare December 19, 2025 11:32
Comment on lines +120 to +127
let stake = Pubkey::new_unique();
let stake_account = AccountSharedData::new_data_with_space(
ctx.rent_exempt_reserve / 2,
&StakeStateV2::Uninitialized,
StakeStateV2::size_of(),
&id(),
)
.unwrap();
Copy link
Member

Choose a reason for hiding this comment

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

What if we had an API like:

let (addr, account) = ctx.stake_account()
                         .rent(ctx.rent_exempt_reserve / 2)
                         .build()

.vote_account
.as_ref()
.map(|(pk, _)| pk)
.expect("vote_account required for this lifecycle")
Copy link
Member

Choose a reason for hiding this comment

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

This sort of gives me a feeling that we aren't using the type system enough if it's possible to run a test with an incorrect setup state. Some ideas...

What about instead of choosing a lifecycle up front, you start uninitialized and transition via methods:

let stake = ctx
  .stake()                 // -> StakeBuilder<Uninitialized>
  .fund(amount)
  .initialize(auth, lockup) // -> StakeBuilder<Initialized>
  .delegate(vote)           // requires vote here
  .activate_epoch();        // -> StakeBuilder<Active>

or have builders for the different lifecycles:

ctx.uninitialized_stake()
ctx.initialized_stake().lockup(...)
ctx.activating_stake(vote).staked_amount(...)
ctx.active_stake(vote).staked_amount(...)

But in general I feel like we may be trying to serve all instruction case parameters at once in the current design.

Comment on lines +217 to +218
/// Verify that removing any signer from the instruction causes MissingRequiredSignature error
fn verify_all_signers_required(
Copy link
Member

Choose a reason for hiding this comment

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

Nice, this is cool that it runs by default

};

// Initialize if needed
if self >= StakeLifecycle::Initialized {
Copy link
Member

@grod220 grod220 Dec 19, 2025

Choose a reason for hiding this comment

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

Will the >= comparisons sort of guarantee that a stake account goes through the entire lifecycle from the beginning? Hitting many state transitions along the way?

Activating,
Active,
Deactivating,
Deactive,
Copy link
Member

Choose a reason for hiding this comment

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

Inactive?

///
/// This function re-serializes the stake history sysvar from mollusk.sysvars.stake_history
/// every time it's called, ensuring that any updates to the stake history are reflected in the accounts.
pub fn add_sysvars(
Copy link
Member

Choose a reason for hiding this comment

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

I'm on the fence with this helper. Is it better to automatically populate needed accounts or have the test writer enumerate them at the site of the instruction builder? Hmm.

Comment on lines +68 to +76
/// Create a vote account with VoteStateV4
pub fn create_vote_account() -> AccountSharedData {
let space = VoteStateV4::size_of();
let lamports = Rent::default().minimum_balance(space);
let vote_state = VoteStateVersions::new_v4(VoteStateV4::default());
let data = bincode::serialize(&vote_state).unwrap();

Account::create(lamports, data, solana_sdk_ids::vote::id(), false, u64::MAX).into()
}
Copy link
Member

Choose a reason for hiding this comment

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

I kinda think it would help to put all account builders in a separate dir


#[test_case(false; "activating")]
#[test_case(true; "active")]
fn test_deactivate(activate: bool) {
Copy link
Member

Choose a reason for hiding this comment

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

Not for this PR, but when we later migrate this instruction, I think it would help to revisit these tests and likely break them up into specific cases versus a single test with multiple inside.

@@ -0,0 +1,128 @@
#![allow(clippy::arithmetic_side_effects)]
Copy link
Member

Choose a reason for hiding this comment

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

Is this not covered via the parent mod.s allow?

Comment on lines +36 to +40
.execute(
DeactivateBuilder::new()
.stake(stake)
.stake_authority(staker)
.instruction(),
Copy link
Member

Choose a reason for hiding this comment

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

If we had per-instruction builders, we could essentially could pull all of the work passing ctx defaults into the instructions to be internal to the builder. If we did that, I think we could make individual tests like 50% smaller. Maybe I am still enamored by the token-wrap pattern of having tests like this small:

#[test]
fn test_incorrect_escrow_owner() {
    let incorrect_escrow_owner = Pubkey::new_unique();
    WrapBuilder::default()
        .unwrapped_escrow_owner(incorrect_escrow_owner)
        .check(Check::err(TokenWrapError::EscrowOwnerMismatch.into()))
        .execute();
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants