* Prevent rent-paying account creation (#22292)
* Fixup typo
* Add new feature
* Add new TransactionError
* Add framework for checking account state before and after transaction processing
* Fail transactions that leave new rent-paying accounts
* Only check rent-state of writable tx accounts
* Review comments: combine process_result success behavior; log and metrics before feature activation
* Fix tests that assume rent-exempt accounts are okay
* Remove test no longer relevant
* Remove native/sysvar special case
* Move metrics submission to report legacy->legacy rent paying transitions as well
(cherry picked from commit 637e366b18)
# Conflicts:
#	runtime/src/bank.rs
#	runtime/src/lib.rs
* Fix conflicts and rework for TransactionRefCells
Co-authored-by: Tyera Eulberg <teulberg@gmail.com>
Co-authored-by: Tyera Eulberg <tyera@solana.com>
		
	
		
			
				
	
	
		
			394 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			394 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
| #![allow(clippy::integer_arithmetic)]
 | |
| use {
 | |
|     bincode::deserialize,
 | |
|     solana_banks_client::BanksClient,
 | |
|     solana_program_test::{
 | |
|         processor, ProgramTest, ProgramTestBanksClientExt, ProgramTestContext, ProgramTestError,
 | |
|     },
 | |
|     solana_sdk::{
 | |
|         account_info::{next_account_info, AccountInfo},
 | |
|         clock::Clock,
 | |
|         entrypoint::ProgramResult,
 | |
|         instruction::{AccountMeta, Instruction, InstructionError},
 | |
|         program_error::ProgramError,
 | |
|         pubkey::Pubkey,
 | |
|         rent::Rent,
 | |
|         signature::{Keypair, Signer},
 | |
|         stake::{
 | |
|             instruction as stake_instruction,
 | |
|             state::{Authorized, Lockup, StakeActivationStatus, StakeState},
 | |
|         },
 | |
|         system_instruction, system_program,
 | |
|         sysvar::{
 | |
|             clock,
 | |
|             stake_history::{self, StakeHistory},
 | |
|             Sysvar,
 | |
|         },
 | |
|         transaction::{Transaction, TransactionError},
 | |
|     },
 | |
|     solana_vote_program::{
 | |
|         vote_instruction,
 | |
|         vote_state::{VoteInit, VoteState},
 | |
|     },
 | |
|     std::convert::TryInto,
 | |
| };
 | |
| 
 | |
| // Use a big number to be sure that we get the right error
 | |
| const WRONG_SLOT_ERROR: u32 = 123456;
 | |
| 
 | |
| async fn setup_stake(
 | |
|     context: &mut ProgramTestContext,
 | |
|     user: &Keypair,
 | |
|     vote_address: &Pubkey,
 | |
|     stake_lamports: u64,
 | |
| ) -> Pubkey {
 | |
|     let stake_keypair = Keypair::new();
 | |
|     let transaction = Transaction::new_signed_with_payer(
 | |
|         &stake_instruction::create_account_and_delegate_stake(
 | |
|             &context.payer.pubkey(),
 | |
|             &stake_keypair.pubkey(),
 | |
|             vote_address,
 | |
|             &Authorized::auto(&user.pubkey()),
 | |
|             &Lockup::default(),
 | |
|             stake_lamports,
 | |
|         ),
 | |
|         Some(&context.payer.pubkey()),
 | |
|         &vec![&context.payer, &stake_keypair, user],
 | |
|         context.last_blockhash,
 | |
|     );
 | |
|     context
 | |
|         .banks_client
 | |
|         .process_transaction(transaction)
 | |
|         .await
 | |
|         .unwrap();
 | |
|     stake_keypair.pubkey()
 | |
| }
 | |
| 
 | |
| async fn setup_vote(context: &mut ProgramTestContext) -> Pubkey {
 | |
|     // warp once to make sure stake config doesn't get rent-collected
 | |
|     context.warp_to_slot(100).unwrap();
 | |
|     let mut instructions = vec![];
 | |
|     let validator_keypair = Keypair::new();
 | |
|     instructions.push(system_instruction::create_account(
 | |
|         &context.payer.pubkey(),
 | |
|         &validator_keypair.pubkey(),
 | |
|         Rent::default().minimum_balance(0),
 | |
|         0,
 | |
|         &system_program::id(),
 | |
|     ));
 | |
|     let vote_lamports = Rent::default().minimum_balance(VoteState::size_of());
 | |
|     let vote_keypair = Keypair::new();
 | |
|     let user_keypair = Keypair::new();
 | |
|     instructions.append(&mut vote_instruction::create_account(
 | |
|         &context.payer.pubkey(),
 | |
|         &vote_keypair.pubkey(),
 | |
|         &VoteInit {
 | |
|             node_pubkey: validator_keypair.pubkey(),
 | |
|             authorized_voter: user_keypair.pubkey(),
 | |
|             ..VoteInit::default()
 | |
|         },
 | |
|         vote_lamports,
 | |
|     ));
 | |
| 
 | |
|     let transaction = Transaction::new_signed_with_payer(
 | |
|         &instructions,
 | |
|         Some(&context.payer.pubkey()),
 | |
|         &vec![&context.payer, &validator_keypair, &vote_keypair],
 | |
|         context.last_blockhash,
 | |
|     );
 | |
|     context
 | |
|         .banks_client
 | |
|         .process_transaction(transaction)
 | |
|         .await
 | |
|         .unwrap();
 | |
| 
 | |
|     vote_keypair.pubkey()
 | |
| }
 | |
| 
 | |
| fn process_instruction(
 | |
|     _program_id: &Pubkey,
 | |
|     accounts: &[AccountInfo],
 | |
|     input: &[u8],
 | |
| ) -> ProgramResult {
 | |
|     let account_info_iter = &mut accounts.iter();
 | |
|     let clock_info = next_account_info(account_info_iter)?;
 | |
|     let clock = &Clock::from_account_info(clock_info)?;
 | |
|     let expected_slot = u64::from_le_bytes(input.try_into().unwrap());
 | |
|     if clock.slot == expected_slot {
 | |
|         Ok(())
 | |
|     } else {
 | |
|         Err(ProgramError::Custom(WRONG_SLOT_ERROR))
 | |
|     }
 | |
| }
 | |
| 
 | |
| #[tokio::test]
 | |
| async fn clock_sysvar_updated_from_warp() {
 | |
|     let program_id = Pubkey::new_unique();
 | |
|     // Initialize and start the test network
 | |
|     let program_test = ProgramTest::new(
 | |
|         "program-test-warp",
 | |
|         program_id,
 | |
|         processor!(process_instruction),
 | |
|     );
 | |
| 
 | |
|     let mut context = program_test.start_with_context().await;
 | |
|     let expected_slot = 100_000;
 | |
|     let instruction = Instruction::new_with_bincode(
 | |
|         program_id,
 | |
|         &expected_slot,
 | |
|         vec![AccountMeta::new_readonly(clock::id(), false)],
 | |
|     );
 | |
| 
 | |
|     // Fail transaction
 | |
|     let transaction = Transaction::new_signed_with_payer(
 | |
|         &[instruction.clone()],
 | |
|         Some(&context.payer.pubkey()),
 | |
|         &[&context.payer],
 | |
|         context.last_blockhash,
 | |
|     );
 | |
|     assert_eq!(
 | |
|         context
 | |
|             .banks_client
 | |
|             .process_transaction(transaction)
 | |
|             .await
 | |
|             .unwrap_err()
 | |
|             .unwrap(),
 | |
|         TransactionError::InstructionError(0, InstructionError::Custom(WRONG_SLOT_ERROR))
 | |
|     );
 | |
| 
 | |
|     // Warp to success!
 | |
|     context.warp_to_slot(expected_slot).unwrap();
 | |
|     let instruction = Instruction::new_with_bincode(
 | |
|         program_id,
 | |
|         &expected_slot,
 | |
|         vec![AccountMeta::new_readonly(clock::id(), false)],
 | |
|     );
 | |
|     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();
 | |
| 
 | |
|     // Try warping again to the same slot
 | |
|     assert_eq!(
 | |
|         context.warp_to_slot(expected_slot).unwrap_err(),
 | |
|         ProgramTestError::InvalidWarpSlot,
 | |
|     );
 | |
| }
 | |
| 
 | |
| #[tokio::test]
 | |
| async fn stake_rewards_from_warp() {
 | |
|     // Initialize and start the test network
 | |
|     let program_test = ProgramTest::default();
 | |
|     let mut context = program_test.start_with_context().await;
 | |
|     let vote_address = setup_vote(&mut context).await;
 | |
| 
 | |
|     let user_keypair = Keypair::new();
 | |
|     let stake_lamports = 1_000_000_000_000;
 | |
|     let stake_address =
 | |
|         setup_stake(&mut context, &user_keypair, &vote_address, stake_lamports).await;
 | |
| 
 | |
|     let account = context
 | |
|         .banks_client
 | |
|         .get_account(stake_address)
 | |
|         .await
 | |
|         .expect("account exists")
 | |
|         .unwrap();
 | |
|     assert_eq!(account.lamports, stake_lamports);
 | |
| 
 | |
|     // warp one epoch forward for normal inflation, no rewards collected
 | |
|     let first_normal_slot = context.genesis_config().epoch_schedule.first_normal_slot;
 | |
|     context.warp_to_slot(first_normal_slot).unwrap();
 | |
|     let account = context
 | |
|         .banks_client
 | |
|         .get_account(stake_address)
 | |
|         .await
 | |
|         .expect("account exists")
 | |
|         .unwrap();
 | |
|     assert_eq!(account.lamports, stake_lamports);
 | |
| 
 | |
|     context.increment_vote_account_credits(&vote_address, 100);
 | |
| 
 | |
|     // go forward and see that rewards have been distributed
 | |
|     let slots_per_epoch = context.genesis_config().epoch_schedule.slots_per_epoch;
 | |
|     context
 | |
|         .warp_to_slot(first_normal_slot + slots_per_epoch)
 | |
|         .unwrap();
 | |
| 
 | |
|     let account = context
 | |
|         .banks_client
 | |
|         .get_account(stake_address)
 | |
|         .await
 | |
|         .expect("account exists")
 | |
|         .unwrap();
 | |
|     assert!(account.lamports > stake_lamports);
 | |
| 
 | |
|     // check that stake is fully active
 | |
|     let stake_history_account = context
 | |
|         .banks_client
 | |
|         .get_account(stake_history::id())
 | |
|         .await
 | |
|         .expect("account exists")
 | |
|         .unwrap();
 | |
| 
 | |
|     let clock_account = context
 | |
|         .banks_client
 | |
|         .get_account(clock::id())
 | |
|         .await
 | |
|         .expect("account exists")
 | |
|         .unwrap();
 | |
| 
 | |
|     let stake_state: StakeState = deserialize(&account.data).unwrap();
 | |
|     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!(
 | |
|         stake
 | |
|             .delegation
 | |
|             .stake_activating_and_deactivating(clock.epoch, Some(&stake_history)),
 | |
|         StakeActivationStatus::with_effective(stake.delegation.stake),
 | |
|     );
 | |
| }
 | |
| 
 | |
| async fn check_credits_observed(
 | |
|     banks_client: &mut BanksClient,
 | |
|     stake_address: Pubkey,
 | |
|     expected_credits: u64,
 | |
| ) {
 | |
|     let stake_account = banks_client
 | |
|         .get_account(stake_address)
 | |
|         .await
 | |
|         .unwrap()
 | |
|         .unwrap();
 | |
|     let stake_state: StakeState = deserialize(&stake_account.data).unwrap();
 | |
|     assert_eq!(
 | |
|         stake_state.stake().unwrap().credits_observed,
 | |
|         expected_credits
 | |
|     );
 | |
| }
 | |
| 
 | |
| #[tokio::test]
 | |
| async fn stake_merge_immediately_after_activation() {
 | |
|     let program_test = ProgramTest::default();
 | |
|     let mut context = program_test.start_with_context().await;
 | |
|     let vote_address = setup_vote(&mut context).await;
 | |
|     context.increment_vote_account_credits(&vote_address, 100);
 | |
| 
 | |
|     let first_normal_slot = context.genesis_config().epoch_schedule.first_normal_slot;
 | |
|     let slots_per_epoch = context.genesis_config().epoch_schedule.slots_per_epoch;
 | |
|     let mut current_slot = first_normal_slot + slots_per_epoch;
 | |
|     context.warp_to_slot(current_slot).unwrap();
 | |
| 
 | |
|     // this is annoying, but if no stake has earned rewards, the bank won't
 | |
|     // iterate through the stakes at all, which means we can only test the
 | |
|     // behavior of advancing credits observed if another stake is earning rewards
 | |
| 
 | |
|     // make a base stake which receives rewards
 | |
|     let user_keypair = Keypair::new();
 | |
|     let stake_lamports = 1_000_000_000_000;
 | |
|     let base_stake_address =
 | |
|         setup_stake(&mut context, &user_keypair, &vote_address, stake_lamports).await;
 | |
|     check_credits_observed(&mut context.banks_client, base_stake_address, 100).await;
 | |
|     context.increment_vote_account_credits(&vote_address, 100);
 | |
| 
 | |
|     current_slot += slots_per_epoch;
 | |
|     context.warp_to_slot(current_slot).unwrap();
 | |
| 
 | |
|     // make another stake which will just have its credits observed advanced
 | |
|     let absorbed_stake_address =
 | |
|         setup_stake(&mut context, &user_keypair, &vote_address, stake_lamports).await;
 | |
|     // the new stake is at the right value
 | |
|     check_credits_observed(&mut context.banks_client, absorbed_stake_address, 200).await;
 | |
|     // the base stake hasn't been moved forward because no rewards were earned
 | |
|     check_credits_observed(&mut context.banks_client, base_stake_address, 100).await;
 | |
| 
 | |
|     context.increment_vote_account_credits(&vote_address, 100);
 | |
|     current_slot += slots_per_epoch;
 | |
|     context.warp_to_slot(current_slot).unwrap();
 | |
| 
 | |
|     // check that base stake has earned rewards and credits moved forward
 | |
|     let stake_account = context
 | |
|         .banks_client
 | |
|         .get_account(base_stake_address)
 | |
|         .await
 | |
|         .unwrap()
 | |
|         .unwrap();
 | |
|     let stake_state: StakeState = deserialize(&stake_account.data).unwrap();
 | |
|     assert_eq!(stake_state.stake().unwrap().credits_observed, 300);
 | |
|     assert!(stake_account.lamports > stake_lamports);
 | |
| 
 | |
|     // check that new stake hasn't earned rewards, but that credits_observed have been advanced
 | |
|     let stake_account = context
 | |
|         .banks_client
 | |
|         .get_account(absorbed_stake_address)
 | |
|         .await
 | |
|         .unwrap()
 | |
|         .unwrap();
 | |
|     let stake_state: StakeState = deserialize(&stake_account.data).unwrap();
 | |
|     assert_eq!(stake_state.stake().unwrap().credits_observed, 300);
 | |
|     assert_eq!(stake_account.lamports, stake_lamports);
 | |
| 
 | |
|     // sanity-check that the activation epoch was actually last epoch
 | |
|     let clock_account = context
 | |
|         .banks_client
 | |
|         .get_account(clock::id())
 | |
|         .await
 | |
|         .unwrap()
 | |
|         .unwrap();
 | |
|     let clock: Clock = deserialize(&clock_account.data).unwrap();
 | |
|     assert_eq!(
 | |
|         clock.epoch,
 | |
|         stake_state.delegation().unwrap().activation_epoch + 1
 | |
|     );
 | |
| 
 | |
|     // sanity-check that it's possible to merge the just-activated stake with the older stake!
 | |
|     let transaction = Transaction::new_signed_with_payer(
 | |
|         &stake_instruction::merge(
 | |
|             &base_stake_address,
 | |
|             &absorbed_stake_address,
 | |
|             &user_keypair.pubkey(),
 | |
|         ),
 | |
|         Some(&context.payer.pubkey()),
 | |
|         &vec![&context.payer, &user_keypair],
 | |
|         context.last_blockhash,
 | |
|     );
 | |
|     context
 | |
|         .banks_client
 | |
|         .process_transaction(transaction)
 | |
|         .await
 | |
|         .unwrap();
 | |
| }
 | |
| 
 | |
| #[tokio::test]
 | |
| async fn get_blockhash_post_warp() {
 | |
|     let program_test = ProgramTest::default();
 | |
|     let mut context = program_test.start_with_context().await;
 | |
| 
 | |
|     let new_blockhash = context
 | |
|         .banks_client
 | |
|         .get_new_latest_blockhash(&context.last_blockhash)
 | |
|         .await
 | |
|         .unwrap();
 | |
|     let mut tx = Transaction::new_with_payer(&[], Some(&context.payer.pubkey()));
 | |
|     tx.sign(&[&context.payer], new_blockhash);
 | |
|     context.banks_client.process_transaction(tx).await.unwrap();
 | |
| 
 | |
|     context.warp_to_slot(10).unwrap();
 | |
| 
 | |
|     let new_blockhash = context
 | |
|         .banks_client
 | |
|         .get_new_latest_blockhash(&context.last_blockhash)
 | |
|         .await
 | |
|         .unwrap();
 | |
| 
 | |
|     let mut tx = Transaction::new_with_payer(&[], Some(&context.payer.pubkey()));
 | |
|     tx.sign(&[&context.payer], new_blockhash);
 | |
|     context.banks_client.process_transaction(tx).await.unwrap();
 | |
| }
 |