diff --git a/Cargo.lock b/Cargo.lock index 21c1d44c24..1b6deec664 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5654,6 +5654,7 @@ dependencies = [ "dashmap", "dir-diff", "ed25519-dalek", + "enum-iterator", "flate2", "fnv", "index_list", diff --git a/accountsdb-plugin-postgres/scripts/create_schema.sql b/accountsdb-plugin-postgres/scripts/create_schema.sql index e4d8d87be8..2e72215a54 100644 --- a/accountsdb-plugin-postgres/scripts/create_schema.sql +++ b/accountsdb-plugin-postgres/scripts/create_schema.sql @@ -54,7 +54,8 @@ Create TYPE "TransactionErrorCode" AS ENUM ( 'AddressLookupTableNotFound', 'InvalidAddressLookupTableOwner', 'InvalidAddressLookupTableData', - 'InvalidAddressLookupTableIndex' + 'InvalidAddressLookupTableIndex', + 'InvalidRentPayingAccount' ); CREATE TYPE "TransactionError" AS ( diff --git a/accountsdb-plugin-postgres/src/postgres_client/postgres_client_transaction.rs b/accountsdb-plugin-postgres/src/postgres_client/postgres_client_transaction.rs index ea095a0b2f..c06f9eaf4f 100644 --- a/accountsdb-plugin-postgres/src/postgres_client/postgres_client_transaction.rs +++ b/accountsdb-plugin-postgres/src/postgres_client/postgres_client_transaction.rs @@ -336,6 +336,7 @@ pub enum DbTransactionErrorCode { InvalidAddressLookupTableOwner, InvalidAddressLookupTableData, InvalidAddressLookupTableIndex, + InvalidRentPayingAccount, } impl From<&TransactionError> for DbTransactionErrorCode { @@ -376,6 +377,7 @@ impl From<&TransactionError> for DbTransactionErrorCode { TransactionError::InvalidAddressLookupTableIndex => { Self::InvalidAddressLookupTableIndex } + TransactionError::InvalidRentPayingAccount => Self::InvalidRentPayingAccount, } } } diff --git a/cli/tests/nonce.rs b/cli/tests/nonce.rs index 855ab2b774..6d32b223a4 100644 --- a/cli/tests/nonce.rs +++ b/cli/tests/nonce.rs @@ -14,6 +14,7 @@ use { solana_sdk::{ commitment_config::CommitmentConfig, hash::Hash, + native_token::sol_to_lamports, pubkey::Pubkey, signature::{keypair_from_seed, Keypair, Signer}, system_program, @@ -73,10 +74,14 @@ fn full_battery_tests( &rpc_client, &config_payer, &config_payer.signers[0].pubkey(), - 2000, + sol_to_lamports(2000.0), ) .unwrap(); - check_recent_balance(2000, &rpc_client, &config_payer.signers[0].pubkey()); + check_recent_balance( + sol_to_lamports(2000.0), + &rpc_client, + &config_payer.signers[0].pubkey(), + ); let mut config_nonce = CliConfig::recent_for_tests(); config_nonce.json_rpc_url = json_rpc_url; @@ -108,12 +113,16 @@ fn full_battery_tests( seed, nonce_authority: optional_authority, memo: None, - amount: SpendAmount::Some(1000), + amount: SpendAmount::Some(sol_to_lamports(1000.0)), }; process_command(&config_payer).unwrap(); - check_recent_balance(1000, &rpc_client, &config_payer.signers[0].pubkey()); - check_recent_balance(1000, &rpc_client, &nonce_account); + check_recent_balance( + sol_to_lamports(1000.0), + &rpc_client, + &config_payer.signers[0].pubkey(), + ); + check_recent_balance(sol_to_lamports(1000.0), &rpc_client, &nonce_account); // Get nonce config_payer.signers.pop(); @@ -161,12 +170,16 @@ fn full_battery_tests( nonce_authority: index, memo: None, destination_account_pubkey: payee_pubkey, - lamports: 100, + lamports: sol_to_lamports(100.0), }; process_command(&config_payer).unwrap(); - check_recent_balance(1000, &rpc_client, &config_payer.signers[0].pubkey()); - check_recent_balance(900, &rpc_client, &nonce_account); - check_recent_balance(100, &rpc_client, &payee_pubkey); + check_recent_balance( + sol_to_lamports(1000.0), + &rpc_client, + &config_payer.signers[0].pubkey(), + ); + check_recent_balance(sol_to_lamports(900.0), &rpc_client, &nonce_account); + check_recent_balance(sol_to_lamports(100.0), &rpc_client, &payee_pubkey); // Show nonce account config_payer.command = CliCommand::ShowNonceAccount { @@ -208,12 +221,16 @@ fn full_battery_tests( nonce_authority: 1, memo: None, destination_account_pubkey: payee_pubkey, - lamports: 100, + lamports: sol_to_lamports(100.0), }; process_command(&config_payer).unwrap(); - check_recent_balance(1000, &rpc_client, &config_payer.signers[0].pubkey()); - check_recent_balance(800, &rpc_client, &nonce_account); - check_recent_balance(200, &rpc_client, &payee_pubkey); + check_recent_balance( + sol_to_lamports(1000.0), + &rpc_client, + &config_payer.signers[0].pubkey(), + ); + check_recent_balance(sol_to_lamports(800.0), &rpc_client, &nonce_account); + check_recent_balance(sol_to_lamports(200.0), &rpc_client, &payee_pubkey); } #[test] @@ -241,18 +258,26 @@ fn test_create_account_with_seed() { &rpc_client, &CliConfig::recent_for_tests(), &offline_nonce_authority_signer.pubkey(), - 42, + sol_to_lamports(42.0), ) .unwrap(); request_and_confirm_airdrop( &rpc_client, &CliConfig::recent_for_tests(), &online_nonce_creator_signer.pubkey(), - 4242, + sol_to_lamports(4242.0), ) .unwrap(); - check_recent_balance(42, &rpc_client, &offline_nonce_authority_signer.pubkey()); - check_recent_balance(4242, &rpc_client, &online_nonce_creator_signer.pubkey()); + check_recent_balance( + sol_to_lamports(42.0), + &rpc_client, + &offline_nonce_authority_signer.pubkey(), + ); + check_recent_balance( + sol_to_lamports(4242.0), + &rpc_client, + &online_nonce_creator_signer.pubkey(), + ); check_recent_balance(0, &rpc_client, &to_address); check_ready(&rpc_client); @@ -273,12 +298,20 @@ fn test_create_account_with_seed() { seed: Some(seed), nonce_authority: Some(authority_pubkey), memo: None, - amount: SpendAmount::Some(241), + amount: SpendAmount::Some(sol_to_lamports(241.0)), }; process_command(&creator_config).unwrap(); - check_recent_balance(241, &rpc_client, &nonce_address); - check_recent_balance(42, &rpc_client, &offline_nonce_authority_signer.pubkey()); - check_recent_balance(4000, &rpc_client, &online_nonce_creator_signer.pubkey()); + check_recent_balance(sol_to_lamports(241.0), &rpc_client, &nonce_address); + check_recent_balance( + sol_to_lamports(42.0), + &rpc_client, + &offline_nonce_authority_signer.pubkey(), + ); + check_recent_balance( + sol_to_lamports(4000.999999999), + &rpc_client, + &online_nonce_creator_signer.pubkey(), + ); check_recent_balance(0, &rpc_client, &to_address); // Fetch nonce hash @@ -299,7 +332,7 @@ fn test_create_account_with_seed() { authority_config.command = CliCommand::ClusterVersion; process_command(&authority_config).unwrap_err(); authority_config.command = CliCommand::Transfer { - amount: SpendAmount::Some(10), + amount: SpendAmount::Some(sol_to_lamports(10.0)), to: to_address, from: 0, sign_only: true, @@ -325,7 +358,7 @@ fn test_create_account_with_seed() { submit_config.json_rpc_url = test_validator.rpc_url(); submit_config.signers = vec![&authority_presigner]; submit_config.command = CliCommand::Transfer { - amount: SpendAmount::Some(10), + amount: SpendAmount::Some(sol_to_lamports(10.0)), to: to_address, from: 0, sign_only: false, @@ -344,8 +377,16 @@ fn test_create_account_with_seed() { derived_address_program_id: None, }; process_command(&submit_config).unwrap(); - check_recent_balance(241, &rpc_client, &nonce_address); - check_recent_balance(31, &rpc_client, &offline_nonce_authority_signer.pubkey()); - check_recent_balance(4000, &rpc_client, &online_nonce_creator_signer.pubkey()); - check_recent_balance(10, &rpc_client, &to_address); + check_recent_balance(sol_to_lamports(241.0), &rpc_client, &nonce_address); + check_recent_balance( + sol_to_lamports(31.999999999), + &rpc_client, + &offline_nonce_authority_signer.pubkey(), + ); + check_recent_balance( + sol_to_lamports(4000.999999999), + &rpc_client, + &online_nonce_creator_signer.pubkey(), + ); + check_recent_balance(sol_to_lamports(10.0), &rpc_client, &to_address); } diff --git a/cli/tests/request_airdrop.rs b/cli/tests/request_airdrop.rs index 8a724d4862..8b41525c63 100644 --- a/cli/tests/request_airdrop.rs +++ b/cli/tests/request_airdrop.rs @@ -4,6 +4,7 @@ use { solana_faucet::faucet::run_local_faucet, solana_sdk::{ commitment_config::CommitmentConfig, + native_token::sol_to_lamports, signature::{Keypair, Signer}, }, solana_streamer::socket::SocketAddrSpace, @@ -22,7 +23,7 @@ fn test_cli_request_airdrop() { bob_config.json_rpc_url = test_validator.rpc_url(); bob_config.command = CliCommand::Airdrop { pubkey: None, - lamports: 50, + lamports: sol_to_lamports(50.0), }; let keypair = Keypair::new(); bob_config.signers = vec![&keypair]; @@ -36,5 +37,5 @@ fn test_cli_request_airdrop() { let balance = rpc_client .get_balance(&bob_config.signers[0].pubkey()) .unwrap(); - assert_eq!(balance, 50); + assert_eq!(balance, sol_to_lamports(50.0)); } diff --git a/cli/tests/stake.rs b/cli/tests/stake.rs index da9239d3d2..5fa0581a74 100644 --- a/cli/tests/stake.rs +++ b/cli/tests/stake.rs @@ -1572,7 +1572,7 @@ fn test_offline_nonced_create_stake_account_and_withdraw() { config_offline.command = CliCommand::WithdrawStake { stake_account_pubkey: stake_pubkey, destination_account_pubkey: recipient_pubkey, - amount: SpendAmount::Some(42), + amount: SpendAmount::Some(50_000), withdraw_authority: 0, custodian: None, sign_only: true, @@ -1591,7 +1591,7 @@ fn test_offline_nonced_create_stake_account_and_withdraw() { config.command = CliCommand::WithdrawStake { stake_account_pubkey: stake_pubkey, destination_account_pubkey: recipient_pubkey, - amount: SpendAmount::Some(42), + amount: SpendAmount::Some(50_000), withdraw_authority: 0, custodian: None, sign_only: false, @@ -1607,7 +1607,7 @@ fn test_offline_nonced_create_stake_account_and_withdraw() { fee_payer: 0, }; process_command(&config).unwrap(); - check_recent_balance(42, &rpc_client, &recipient_pubkey); + check_recent_balance(50_000, &rpc_client, &recipient_pubkey); // Fetch nonce hash let nonce_hash = nonce_utils::get_account_with_commitment( diff --git a/cli/tests/transfer.rs b/cli/tests/transfer.rs index 2fd8b09cbb..c78fbf2625 100644 --- a/cli/tests/transfer.rs +++ b/cli/tests/transfer.rs @@ -14,6 +14,7 @@ use { solana_faucet::faucet::run_local_faucet, solana_sdk::{ commitment_config::CommitmentConfig, + native_token::sol_to_lamports, nonce::State as NonceState, pubkey::Pubkey, signature::{keypair_from_seed, Keypair, NullSigner, Signer}, @@ -49,15 +50,16 @@ fn test_transfer() { let sender_pubkey = config.signers[0].pubkey(); let recipient_pubkey = Pubkey::new(&[1u8; 32]); - request_and_confirm_airdrop(&rpc_client, &config, &sender_pubkey, 50_000).unwrap(); - check_recent_balance(50_000, &rpc_client, &sender_pubkey); + request_and_confirm_airdrop(&rpc_client, &config, &sender_pubkey, sol_to_lamports(5.0)) + .unwrap(); + check_recent_balance(sol_to_lamports(5.0), &rpc_client, &sender_pubkey); check_recent_balance(0, &rpc_client, &recipient_pubkey); check_ready(&rpc_client); // Plain ole transfer config.command = CliCommand::Transfer { - amount: SpendAmount::Some(10), + amount: SpendAmount::Some(sol_to_lamports(1.0)), to: recipient_pubkey, from: 0, sign_only: false, @@ -73,12 +75,12 @@ fn test_transfer() { derived_address_program_id: None, }; process_command(&config).unwrap(); - check_recent_balance(49_989, &rpc_client, &sender_pubkey); - check_recent_balance(10, &rpc_client, &recipient_pubkey); + check_recent_balance(sol_to_lamports(4.0) - 1, &rpc_client, &sender_pubkey); + check_recent_balance(sol_to_lamports(1.0), &rpc_client, &recipient_pubkey); // Plain ole transfer, failure due to InsufficientFundsForSpendAndFee config.command = CliCommand::Transfer { - amount: SpendAmount::Some(49_989), + amount: SpendAmount::Some(sol_to_lamports(4.0)), to: recipient_pubkey, from: 0, sign_only: false, @@ -94,8 +96,8 @@ fn test_transfer() { derived_address_program_id: None, }; assert!(process_command(&config).is_err()); - check_recent_balance(49_989, &rpc_client, &sender_pubkey); - check_recent_balance(10, &rpc_client, &recipient_pubkey); + check_recent_balance(sol_to_lamports(4.0) - 1, &rpc_client, &sender_pubkey); + check_recent_balance(sol_to_lamports(1.0), &rpc_client, &recipient_pubkey); let mut offline = CliConfig::recent_for_tests(); offline.json_rpc_url = String::default(); @@ -105,13 +107,14 @@ fn test_transfer() { process_command(&offline).unwrap_err(); let offline_pubkey = offline.signers[0].pubkey(); - request_and_confirm_airdrop(&rpc_client, &offline, &offline_pubkey, 50).unwrap(); - check_recent_balance(50, &rpc_client, &offline_pubkey); + request_and_confirm_airdrop(&rpc_client, &offline, &offline_pubkey, sol_to_lamports(1.0)) + .unwrap(); + check_recent_balance(sol_to_lamports(1.0), &rpc_client, &offline_pubkey); // Offline transfer let blockhash = rpc_client.get_latest_blockhash().unwrap(); offline.command = CliCommand::Transfer { - amount: SpendAmount::Some(10), + amount: SpendAmount::Some(sol_to_lamports(0.5)), to: recipient_pubkey, from: 0, sign_only: true, @@ -133,7 +136,7 @@ fn test_transfer() { let offline_presigner = sign_only.presigner_of(&offline_pubkey).unwrap(); config.signers = vec![&offline_presigner]; config.command = CliCommand::Transfer { - amount: SpendAmount::Some(10), + amount: SpendAmount::Some(sol_to_lamports(0.5)), to: recipient_pubkey, from: 0, sign_only: false, @@ -149,8 +152,8 @@ fn test_transfer() { derived_address_program_id: None, }; process_command(&config).unwrap(); - check_recent_balance(39, &rpc_client, &offline_pubkey); - check_recent_balance(20, &rpc_client, &recipient_pubkey); + check_recent_balance(sol_to_lamports(0.5) - 1, &rpc_client, &offline_pubkey); + check_recent_balance(sol_to_lamports(1.5), &rpc_client, &recipient_pubkey); // Create nonce account let nonce_account = keypair_from_seed(&[3u8; 32]).unwrap(); @@ -166,7 +169,11 @@ fn test_transfer() { amount: SpendAmount::Some(minimum_nonce_balance), }; process_command(&config).unwrap(); - check_recent_balance(49_987 - minimum_nonce_balance, &rpc_client, &sender_pubkey); + check_recent_balance( + sol_to_lamports(4.0) - 3 - minimum_nonce_balance, + &rpc_client, + &sender_pubkey, + ); // Fetch nonce hash let nonce_hash = nonce_utils::get_account_with_commitment( @@ -181,7 +188,7 @@ fn test_transfer() { // Nonced transfer config.signers = vec![&default_signer]; config.command = CliCommand::Transfer { - amount: SpendAmount::Some(10), + amount: SpendAmount::Some(sol_to_lamports(1.0)), to: recipient_pubkey, from: 0, sign_only: false, @@ -200,8 +207,12 @@ fn test_transfer() { derived_address_program_id: None, }; process_command(&config).unwrap(); - check_recent_balance(49_976 - minimum_nonce_balance, &rpc_client, &sender_pubkey); - check_recent_balance(30, &rpc_client, &recipient_pubkey); + check_recent_balance( + sol_to_lamports(3.0) - 4 - minimum_nonce_balance, + &rpc_client, + &sender_pubkey, + ); + check_recent_balance(sol_to_lamports(2.5), &rpc_client, &recipient_pubkey); let new_nonce_hash = nonce_utils::get_account_with_commitment( &rpc_client, &nonce_account.pubkey(), @@ -221,7 +232,11 @@ fn test_transfer() { new_authority: offline_pubkey, }; process_command(&config).unwrap(); - check_recent_balance(49_975 - minimum_nonce_balance, &rpc_client, &sender_pubkey); + check_recent_balance( + sol_to_lamports(3.0) - 5 - minimum_nonce_balance, + &rpc_client, + &sender_pubkey, + ); // Fetch nonce hash let nonce_hash = nonce_utils::get_account_with_commitment( @@ -236,7 +251,7 @@ fn test_transfer() { // Offline, nonced transfer offline.signers = vec![&default_offline_signer]; offline.command = CliCommand::Transfer { - amount: SpendAmount::Some(10), + amount: SpendAmount::Some(sol_to_lamports(0.4)), to: recipient_pubkey, from: 0, sign_only: true, @@ -257,7 +272,7 @@ fn test_transfer() { let offline_presigner = sign_only.presigner_of(&offline_pubkey).unwrap(); config.signers = vec![&offline_presigner]; config.command = CliCommand::Transfer { - amount: SpendAmount::Some(10), + amount: SpendAmount::Some(sol_to_lamports(0.4)), to: recipient_pubkey, from: 0, sign_only: false, @@ -276,8 +291,8 @@ fn test_transfer() { derived_address_program_id: None, }; process_command(&config).unwrap(); - check_recent_balance(28, &rpc_client, &offline_pubkey); - check_recent_balance(40, &rpc_client, &recipient_pubkey); + check_recent_balance(sol_to_lamports(0.1) - 2, &rpc_client, &offline_pubkey); + check_recent_balance(sol_to_lamports(2.9), &rpc_client, &recipient_pubkey); } #[test] @@ -305,18 +320,26 @@ fn test_transfer_multisession_signing() { &rpc_client, &CliConfig::recent_for_tests(), &offline_from_signer.pubkey(), - 43, + sol_to_lamports(43.0), ) .unwrap(); request_and_confirm_airdrop( &rpc_client, &CliConfig::recent_for_tests(), &offline_fee_payer_signer.pubkey(), - 3, + sol_to_lamports(1.0) + 3, ) .unwrap(); - check_recent_balance(43, &rpc_client, &offline_from_signer.pubkey()); - check_recent_balance(3, &rpc_client, &offline_fee_payer_signer.pubkey()); + check_recent_balance( + sol_to_lamports(43.0), + &rpc_client, + &offline_from_signer.pubkey(), + ); + check_recent_balance( + sol_to_lamports(1.0) + 3, + &rpc_client, + &offline_fee_payer_signer.pubkey(), + ); check_recent_balance(0, &rpc_client, &to_pubkey); check_ready(&rpc_client); @@ -331,7 +354,7 @@ fn test_transfer_multisession_signing() { fee_payer_config.command = CliCommand::ClusterVersion; process_command(&fee_payer_config).unwrap_err(); fee_payer_config.command = CliCommand::Transfer { - amount: SpendAmount::Some(42), + amount: SpendAmount::Some(sol_to_lamports(42.0)), to: to_pubkey, from: 1, sign_only: true, @@ -362,7 +385,7 @@ fn test_transfer_multisession_signing() { from_config.command = CliCommand::ClusterVersion; process_command(&from_config).unwrap_err(); from_config.command = CliCommand::Transfer { - amount: SpendAmount::Some(42), + amount: SpendAmount::Some(sol_to_lamports(42.0)), to: to_pubkey, from: 1, sign_only: true, @@ -390,7 +413,7 @@ fn test_transfer_multisession_signing() { config.json_rpc_url = test_validator.rpc_url(); config.signers = vec![&fee_payer_presigner, &from_presigner]; config.command = CliCommand::Transfer { - amount: SpendAmount::Some(42), + amount: SpendAmount::Some(sol_to_lamports(42.0)), to: to_pubkey, from: 1, sign_only: false, @@ -407,9 +430,17 @@ fn test_transfer_multisession_signing() { }; process_command(&config).unwrap(); - check_recent_balance(1, &rpc_client, &offline_from_signer.pubkey()); - check_recent_balance(1, &rpc_client, &offline_fee_payer_signer.pubkey()); - check_recent_balance(42, &rpc_client, &to_pubkey); + check_recent_balance( + sol_to_lamports(1.0), + &rpc_client, + &offline_from_signer.pubkey(), + ); + check_recent_balance( + sol_to_lamports(1.0) + 1, + &rpc_client, + &offline_fee_payer_signer.pubkey(), + ); + check_recent_balance(sol_to_lamports(42.0), &rpc_client, &to_pubkey); } #[test] @@ -551,17 +582,19 @@ fn test_transfer_with_seed() { ) .unwrap(); - request_and_confirm_airdrop(&rpc_client, &config, &sender_pubkey, 1).unwrap(); - request_and_confirm_airdrop(&rpc_client, &config, &derived_address, 50_000).unwrap(); - check_recent_balance(1, &rpc_client, &sender_pubkey); - check_recent_balance(50_000, &rpc_client, &derived_address); + request_and_confirm_airdrop(&rpc_client, &config, &sender_pubkey, sol_to_lamports(1.0)) + .unwrap(); + request_and_confirm_airdrop(&rpc_client, &config, &derived_address, sol_to_lamports(5.0)) + .unwrap(); + check_recent_balance(sol_to_lamports(1.0), &rpc_client, &sender_pubkey); + check_recent_balance(sol_to_lamports(5.0), &rpc_client, &derived_address); check_recent_balance(0, &rpc_client, &recipient_pubkey); check_ready(&rpc_client); // Transfer with seed config.command = CliCommand::Transfer { - amount: SpendAmount::Some(50_000), + amount: SpendAmount::Some(sol_to_lamports(5.0)), to: recipient_pubkey, from: 0, sign_only: false, @@ -577,7 +610,7 @@ fn test_transfer_with_seed() { derived_address_program_id: Some(derived_address_program_id), }; process_command(&config).unwrap(); - check_recent_balance(0, &rpc_client, &sender_pubkey); - check_recent_balance(50_000, &rpc_client, &recipient_pubkey); + check_recent_balance(sol_to_lamports(1.0) - 1, &rpc_client, &sender_pubkey); + check_recent_balance(sol_to_lamports(5.0), &rpc_client, &recipient_pubkey); check_recent_balance(0, &rpc_client, &derived_address); } diff --git a/cli/tests/vote.rs b/cli/tests/vote.rs index ab52ec0247..c799b4864c 100644 --- a/cli/tests/vote.rs +++ b/cli/tests/vote.rs @@ -74,7 +74,7 @@ fn test_vote_authorize_and_withdraw() { // Transfer in some more SOL config.signers = vec![&default_signer]; config.command = CliCommand::Transfer { - amount: SpendAmount::Some(1_000), + amount: SpendAmount::Some(10_000), to: vote_account_pubkey, from: 0, sign_only: false, @@ -90,7 +90,7 @@ fn test_vote_authorize_and_withdraw() { derived_address_program_id: None, }; process_command(&config).unwrap(); - let expected_balance = expected_balance + 1_000; + let expected_balance = expected_balance + 10_000; check_recent_balance(expected_balance, &rpc_client, &vote_account_pubkey); // Authorize vote account withdrawal to another signer @@ -169,7 +169,7 @@ fn test_vote_authorize_and_withdraw() { config.command = CliCommand::WithdrawFromVoteAccount { vote_account_pubkey, withdraw_authority: 1, - withdraw_amount: SpendAmount::Some(100), + withdraw_amount: SpendAmount::Some(1_000), destination_account_pubkey: destination_account, sign_only: false, dump_transaction_message: false, @@ -180,9 +180,9 @@ fn test_vote_authorize_and_withdraw() { fee_payer: 0, }; process_command(&config).unwrap(); - let expected_balance = expected_balance - 100; + let expected_balance = expected_balance - 1_000; check_recent_balance(expected_balance, &rpc_client, &vote_account_pubkey); - check_recent_balance(100, &rpc_client, &destination_account); + check_recent_balance(1_000, &rpc_client, &destination_account); // Re-assign validator identity let new_identity_keypair = Keypair::new(); @@ -293,7 +293,7 @@ fn test_offline_vote_authorize_and_withdraw() { // Transfer in some more SOL config_payer.signers = vec![&default_signer]; config_payer.command = CliCommand::Transfer { - amount: SpendAmount::Some(1_000), + amount: SpendAmount::Some(10_000), to: vote_account_pubkey, from: 0, sign_only: false, @@ -309,7 +309,7 @@ fn test_offline_vote_authorize_and_withdraw() { derived_address_program_id: None, }; process_command(&config_payer).unwrap(); - let expected_balance = expected_balance + 1_000; + let expected_balance = expected_balance + 10_000; check_recent_balance(expected_balance, &rpc_client, &vote_account_pubkey); // Authorize vote account withdrawal to another signer, offline @@ -367,7 +367,7 @@ fn test_offline_vote_authorize_and_withdraw() { config_offline.command = CliCommand::WithdrawFromVoteAccount { vote_account_pubkey, withdraw_authority: 1, - withdraw_amount: SpendAmount::Some(100), + withdraw_amount: SpendAmount::Some(1_000), destination_account_pubkey: destination_account, sign_only: true, dump_transaction_message: false, @@ -387,7 +387,7 @@ fn test_offline_vote_authorize_and_withdraw() { config_payer.command = CliCommand::WithdrawFromVoteAccount { vote_account_pubkey, withdraw_authority: 1, - withdraw_amount: SpendAmount::Some(100), + withdraw_amount: SpendAmount::Some(1_000), destination_account_pubkey: destination_account, sign_only: false, dump_transaction_message: false, @@ -398,9 +398,9 @@ fn test_offline_vote_authorize_and_withdraw() { fee_payer: 0, }; process_command(&config_payer).unwrap(); - let expected_balance = expected_balance - 100; + let expected_balance = expected_balance - 1_000; check_recent_balance(expected_balance, &rpc_client, &vote_account_pubkey); - check_recent_balance(100, &rpc_client, &destination_account); + check_recent_balance(1_000, &rpc_client, &destination_account); // Re-assign validator identity offline let blockhash = rpc_client.get_latest_blockhash().unwrap(); @@ -483,9 +483,7 @@ fn test_offline_vote_authorize_and_withdraw() { memo: None, fee_payer: 0, }; - let result = process_command(&config_payer).unwrap(); - println!("{:?}", result); + process_command(&config_payer).unwrap(); check_recent_balance(0, &rpc_client, &vote_account_pubkey); - println!("what"); check_recent_balance(expected_balance, &rpc_client, &destination_account); } diff --git a/core/src/banking_stage.rs b/core/src/banking_stage.rs index b2485a527f..538a253c20 100644 --- a/core/src/banking_stage.rs +++ b/core/src/banking_stage.rs @@ -2542,30 +2542,47 @@ mod tests { fn test_write_persist_transaction_status() { solana_logger::setup(); let GenesisConfigInfo { - genesis_config, + mut genesis_config, mint_keypair, .. - } = create_slow_genesis_config(10_000); + } = create_slow_genesis_config(solana_sdk::native_token::sol_to_lamports(1000.0)); + genesis_config.rent.lamports_per_byte_year = 50; + genesis_config.rent.exemption_threshold = 2.0; let bank = Arc::new(Bank::new_no_wallclock_throttle_for_tests(&genesis_config)); let pubkey = solana_sdk::pubkey::new_rand(); let pubkey1 = solana_sdk::pubkey::new_rand(); let keypair1 = Keypair::new(); - let success_tx = - system_transaction::transfer(&mint_keypair, &pubkey, 1, genesis_config.hash()); + let rent_exempt_amount = bank.get_minimum_balance_for_rent_exemption(0); + + let success_tx = system_transaction::transfer( + &mint_keypair, + &pubkey, + rent_exempt_amount, + genesis_config.hash(), + ); let success_signature = success_tx.signatures[0]; let entry_1 = next_entry(&genesis_config.hash(), 1, vec![success_tx.clone()]); - let ix_error_tx = - system_transaction::transfer(&keypair1, &pubkey1, 10, genesis_config.hash()); + let ix_error_tx = system_transaction::transfer( + &keypair1, + &pubkey1, + 2 * rent_exempt_amount, + genesis_config.hash(), + ); let ix_error_signature = ix_error_tx.signatures[0]; let entry_2 = next_entry(&entry_1.hash, 1, vec![ix_error_tx.clone()]); - let fail_tx = - system_transaction::transfer(&mint_keypair, &pubkey1, 1, genesis_config.hash()); + let fail_tx = system_transaction::transfer( + &mint_keypair, + &pubkey1, + rent_exempt_amount, + genesis_config.hash(), + ); let entry_3 = next_entry(&entry_2.hash, 1, vec![fail_tx.clone()]); let entries = vec![entry_1, entry_2, entry_3]; let transactions = sanitize_transactions(vec![success_tx, ix_error_tx, fail_tx]); - bank.transfer(4, &mint_keypair, &keypair1.pubkey()).unwrap(); + bank.transfer(rent_exempt_amount, &mint_keypair, &keypair1.pubkey()) + .unwrap(); let ledger_path = get_tmp_ledger_path!(); { diff --git a/core/src/replay_stage.rs b/core/src/replay_stage.rs index 6b4115dacc..e0d5cf7f8c 100644 --- a/core/src/replay_stage.rs +++ b/core/src/replay_stage.rs @@ -3754,10 +3754,12 @@ pub mod tests { #[test] fn test_write_persist_transaction_status() { let GenesisConfigInfo { - genesis_config, + mut genesis_config, mint_keypair, .. - } = create_genesis_config(1000); + } = create_genesis_config(solana_sdk::native_token::sol_to_lamports(1000.0)); + genesis_config.rent.lamports_per_byte_year = 50; + genesis_config.rent.exemption_threshold = 2.0; let (ledger_path, _) = create_new_tmp_ledger!(&genesis_config); { let blockstore = Blockstore::open(&ledger_path) @@ -3770,7 +3772,11 @@ pub mod tests { let bank0 = Arc::new(Bank::new_for_tests(&genesis_config)); bank0 - .transfer(4, &mint_keypair, &keypair2.pubkey()) + .transfer( + bank0.get_minimum_balance_for_rent_exemption(0), + &mint_keypair, + &keypair2.pubkey(), + ) .unwrap(); let bank1 = Arc::new(Bank::new_from_parent(&bank0, &Pubkey::default(), 1)); diff --git a/program-test/tests/warp.rs b/program-test/tests/warp.rs index 78496aca06..e3c392788f 100644 --- a/program-test/tests/warp.rs +++ b/program-test/tests/warp.rs @@ -72,7 +72,7 @@ async fn setup_vote(context: &mut ProgramTestContext) -> Pubkey { instructions.push(system_instruction::create_account( &context.payer.pubkey(), &validator_keypair.pubkey(), - 42, + Rent::default().minimum_balance(0), 0, &system_program::id(), )); @@ -182,57 +182,6 @@ async fn clock_sysvar_updated_from_warp() { ); } -#[tokio::test] -async fn rent_collected_from_warp() { - let program_id = Pubkey::new_unique(); - // Initialize and start the test network - let program_test = ProgramTest::default(); - - let mut context = program_test.start_with_context().await; - let account_size = 100; - let keypair = Keypair::new(); - let account_lamports = Rent::default().minimum_balance(account_size) - 100; // not rent exempt - let instruction = system_instruction::create_account( - &context.payer.pubkey(), - &keypair.pubkey(), - account_lamports, - account_size as u64, - &program_id, - ); - let transaction = Transaction::new_signed_with_payer( - &[instruction], - Some(&context.payer.pubkey()), - &[&context.payer, &keypair], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - let account = context - .banks_client - .get_account(keypair.pubkey()) - .await - .expect("account exists") - .unwrap(); - assert_eq!(account.lamports, account_lamports); - - // Warp forward and see that rent has been collected - // This test was a bit flaky with one warp, but two warps always works - let slots_per_epoch = context.genesis_config().epoch_schedule.slots_per_epoch; - context.warp_to_slot(slots_per_epoch).unwrap(); - context.warp_to_slot(slots_per_epoch * 2).unwrap(); - - let account = context - .banks_client - .get_account(keypair.pubkey()) - .await - .expect("account exists") - .unwrap(); - assert!(account.lamports < account_lamports); -} - #[tokio::test] async fn stake_rewards_from_warp() { // Initialize and start the test network diff --git a/programs/bpf/Cargo.lock b/programs/bpf/Cargo.lock index 6e4fd22cea..e06472519b 100644 --- a/programs/bpf/Cargo.lock +++ b/programs/bpf/Cargo.lock @@ -806,6 +806,26 @@ dependencies = [ "cfg-if 0.1.10", ] +[[package]] +name = "enum-iterator" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eeac5c5edb79e4e39fe8439ef35207780a11f69c52cbe424ce3dfad4cb78de6" +dependencies = [ + "enum-iterator-derive", +] + +[[package]] +name = "enum-iterator-derive" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c134c37760b27a871ba422106eedbb8247da973a09e82558bf26d619c882b159" +dependencies = [ + "proc-macro2 1.0.24", + "quote 1.0.6", + "syn 1.0.67", +] + [[package]] name = "enum-ordinalize" version = "3.1.10" @@ -3330,6 +3350,7 @@ dependencies = [ "crossbeam-channel", "dashmap", "dir-diff", + "enum-iterator", "flate2", "fnv", "index_list", diff --git a/rpc-test/tests/rpc.rs b/rpc-test/tests/rpc.rs index 1c9fb9fdcb..f050242cef 100644 --- a/rpc-test/tests/rpc.rs +++ b/rpc-test/tests/rpc.rs @@ -19,6 +19,7 @@ use { commitment_config::CommitmentConfig, hash::Hash, pubkey::Pubkey, + rent::Rent, signature::{Keypair, Signer}, system_transaction, transaction::Transaction, @@ -79,7 +80,12 @@ fn test_rpc_send_tx() { .unwrap(); info!("blockhash: {:?}", blockhash); - let tx = system_transaction::transfer(&alice, &bob_pubkey, 20, blockhash); + let tx = system_transaction::transfer( + &alice, + &bob_pubkey, + Rent::default().minimum_balance(0), + blockhash, + ); let serialized_encoded_tx = bs58::encode(serialize(&tx).unwrap()).into_string(); let req = json_req!("sendTransaction", json!([serialized_encoded_tx])); @@ -242,7 +248,7 @@ fn test_rpc_subscriptions() { system_transaction::transfer( &alice, &solana_sdk::pubkey::new_rand(), - 1, + Rent::default().minimum_balance(0), recent_blockhash, ) }) @@ -380,7 +386,7 @@ fn test_rpc_subscriptions() { let timeout = deadline.saturating_duration_since(Instant::now()); match account_receiver.recv_timeout(timeout) { Ok(result) => { - assert_eq!(result.value.lamports, 1); + assert_eq!(result.value.lamports, Rent::default().minimum_balance(0)); account_notifications -= 1; } Err(_err) => { diff --git a/rpc/src/rpc.rs b/rpc/src/rpc.rs index a4327a2a67..c15e841030 100644 --- a/rpc/src/rpc.rs +++ b/rpc/src/rpc.rs @@ -4277,23 +4277,32 @@ pub fn create_test_transactions_and_populate_blockstore( let keypair3 = keypairs[3]; let slot = bank.slot(); let blockhash = bank.confirmed_last_blockhash(); + let rent_exempt_amount = bank.get_minimum_balance_for_rent_exemption(0); // Generate transactions for processing // Successful transaction - let success_tx = - solana_sdk::system_transaction::transfer(mint_keypair, &keypair1.pubkey(), 2, blockhash); + let success_tx = solana_sdk::system_transaction::transfer( + mint_keypair, + &keypair1.pubkey(), + rent_exempt_amount, + blockhash, + ); let success_signature = success_tx.signatures[0]; let entry_1 = solana_entry::entry::next_entry(&blockhash, 1, vec![success_tx]); // Failed transaction, InstructionError - let ix_error_tx = - solana_sdk::system_transaction::transfer(keypair2, &keypair3.pubkey(), 10, blockhash); + let ix_error_tx = solana_sdk::system_transaction::transfer( + keypair2, + &keypair3.pubkey(), + 2 * rent_exempt_amount, + blockhash, + ); let ix_error_signature = ix_error_tx.signatures[0]; let entry_2 = solana_entry::entry::next_entry(&entry_1.hash, 1, vec![ix_error_tx]); // Failed transaction let fail_tx = solana_sdk::system_transaction::transfer( mint_keypair, &keypair2.pubkey(), - 2, + rent_exempt_amount, Hash::default(), ); let entry_3 = solana_entry::entry::next_entry(&entry_2.hash, 1, vec![fail_tx]); @@ -4421,6 +4430,7 @@ pub mod tests { ) -> RpcHandler { let (bank_forks, alice, leader_vote_keypair) = new_bank_forks(); let bank = bank_forks.read().unwrap().working_bank(); + let rent_exempt_amount = bank.get_minimum_balance_for_rent_exemption(0); let vote_pubkey = leader_vote_keypair.pubkey(); let mut vote_account = bank.get_account(&vote_pubkey).unwrap_or_default(); @@ -4440,7 +4450,8 @@ pub mod tests { let keypair1 = Keypair::new(); let keypair2 = Keypair::new(); let keypair3 = Keypair::new(); - bank.transfer(4, &alice, &keypair2.pubkey()).unwrap(); + bank.transfer(rent_exempt_amount, &alice, &keypair2.pubkey()) + .unwrap(); let max_complete_transaction_status_slot = Arc::new(AtomicU64::new(blockstore.max_root())); let confirmed_block_signatures = create_test_transactions_and_populate_blockstore( vec![&alice, &keypair1, &keypair2, &keypair3], @@ -4510,10 +4521,14 @@ pub mod tests { let validator_exit = create_validator_exit(&exit); let blockhash = bank.confirmed_last_blockhash(); - let tx = system_transaction::transfer(&alice, pubkey, 20, blockhash); + let tx = system_transaction::transfer(&alice, pubkey, rent_exempt_amount, blockhash); bank.process_transaction(&tx).expect("process transaction"); - let tx = - system_transaction::transfer(&alice, &non_circulating_accounts()[0], 20, blockhash); + let tx = system_transaction::transfer( + &alice, + &non_circulating_accounts()[0], + rent_exempt_amount, + blockhash, + ); bank.process_transaction(&tx).expect("process transaction"); let tx = system_transaction::transfer(&alice, pubkey, std::u64::MAX, blockhash); @@ -4821,13 +4836,16 @@ pub mod tests { #[test] fn test_get_supply() { let bob_pubkey = solana_sdk::pubkey::new_rand(); - let RpcHandler { io, meta, .. } = start_rpc_handler_with_tx(&bob_pubkey); + let RpcHandler { io, meta, bank, .. } = start_rpc_handler_with_tx(&bob_pubkey); let req = r#"{"jsonrpc":"2.0","id":1,"method":"getSupply"}"#; let res = io.handle_request_sync(req, meta); let json: Value = serde_json::from_str(&res.unwrap()).unwrap(); let supply: RpcSupply = serde_json::from_value(json["result"]["value"].clone()) .expect("actual response deserialization"); - assert_eq!(supply.non_circulating, 20); + assert_eq!( + supply.non_circulating, + bank.get_minimum_balance_for_rent_exemption(0) + ); assert!(supply.circulating >= TEST_MINT_LAMPORTS); assert!(supply.total >= TEST_MINT_LAMPORTS + 20); let expected_accounts: Vec = non_circulating_accounts() @@ -4846,13 +4864,16 @@ pub mod tests { #[test] fn test_get_supply_exclude_account_list() { let bob_pubkey = solana_sdk::pubkey::new_rand(); - let RpcHandler { io, meta, .. } = start_rpc_handler_with_tx(&bob_pubkey); + let RpcHandler { io, meta, bank, .. } = start_rpc_handler_with_tx(&bob_pubkey); let req = r#"{"jsonrpc":"2.0","id":1,"method":"getSupply","params":[{"excludeNonCirculatingAccountsList":true}]}"#; let res = io.handle_request_sync(req, meta); let json: Value = serde_json::from_str(&res.unwrap()).unwrap(); let supply: RpcSupply = serde_json::from_value(json["result"]["value"].clone()) .expect("actual response deserialization"); - assert_eq!(supply.non_circulating, 20); + assert_eq!( + supply.non_circulating, + bank.get_minimum_balance_for_rent_exemption(0) + ); assert!(supply.circulating >= TEST_MINT_LAMPORTS); assert!(supply.total >= TEST_MINT_LAMPORTS + 20); assert!(supply.non_circulating_accounts.is_empty()); @@ -5174,7 +5195,7 @@ pub mod tests { "context":{"slot":0}, "value":{ "owner": "11111111111111111111111111111111", - "lamports": 20, + "lamports": bank.get_minimum_balance_for_rent_exemption(0), "data": "", "executable": false, "rentEpoch": 0 @@ -5245,9 +5266,11 @@ pub mod tests { let bob_pubkey = solana_sdk::pubkey::new_rand(); let RpcHandler { io, meta, bank, .. } = start_rpc_handler_with_tx(&bob_pubkey); + let rent_exempt_amount = bank.get_minimum_balance_for_rent_exemption(0); + let address = Pubkey::new(&[9; 32]); let data = vec![1, 2, 3, 4, 5]; - let mut account = AccountSharedData::new(42, 5, &Pubkey::default()); + let mut account = AccountSharedData::new(rent_exempt_amount + 1, 5, &Pubkey::default()); account.set_data(data.clone()); bank.store_account(&address, &account); @@ -5265,7 +5288,7 @@ pub mod tests { "context":{"slot":0}, "value":[{ "owner": "11111111111111111111111111111111", - "lamports": 20, + "lamports": rent_exempt_amount, "data": ["", "base64"], "executable": false, "rentEpoch": 0 @@ -5273,7 +5296,7 @@ pub mod tests { null, { "owner": "11111111111111111111111111111111", - "lamports": 42, + "lamports": rent_exempt_amount + 1, "data": [base64::encode(&data), "base64"], "executable": false, "rentEpoch": 0 @@ -5385,7 +5408,7 @@ pub mod tests { "pubkey": "{}", "account": {{ "owner": "{}", - "lamports": 20, + "lamports": {}, "data": "", "executable": false, "rentEpoch": 0 @@ -5395,7 +5418,8 @@ pub mod tests { "id":1}} "#, bob.pubkey(), - new_program_id + new_program_id, + bank.get_minimum_balance_for_rent_exemption(0), ); let expected: Response = serde_json::from_str(&expected).expect("expected response deserialization"); @@ -5588,8 +5612,10 @@ pub mod tests { .. } = start_rpc_handler_with_tx(&solana_sdk::pubkey::new_rand()); + let rent_exempt_amount = bank.get_minimum_balance_for_rent_exemption(0); let bob_pubkey = solana_sdk::pubkey::new_rand(); - let mut tx = system_transaction::transfer(&alice, &bob_pubkey, 1234, blockhash); + let mut tx = + system_transaction::transfer(&alice, &bob_pubkey, rent_exempt_amount, blockhash); let tx_serialized_encoded = bs58::encode(serialize(&tx).unwrap()).into_string(); tx.signatures[0] = Signature::default(); let tx_badsig_serialized_encoded = bs58::encode(serialize(&tx).unwrap()).into_string(); @@ -5630,7 +5656,7 @@ pub mod tests { "data": ["", "base64"], "executable": false, "owner": "11111111111111111111111111111111", - "lamports": 1234, + "lamports": rent_exempt_amount, "rentEpoch": 0 } ], @@ -5880,6 +5906,7 @@ pub mod tests { blockhash, alice, confirmed_block_signatures, + bank, .. } = start_rpc_handler_with_tx(&bob_pubkey); @@ -5898,7 +5925,12 @@ pub mod tests { assert_eq!(None, result.confirmations); // Test getSignatureStatus request on unprocessed tx - let tx = system_transaction::transfer(&alice, &bob_pubkey, 10, blockhash); + let tx = system_transaction::transfer( + &alice, + &bob_pubkey, + bank.get_minimum_balance_for_rent_exemption(0) + 10, + blockhash, + ); let req = format!( r#"{{"jsonrpc":"2.0","id":1,"method":"getSignatureStatuses","params":[["{}"]]}}"#, tx.signatures[0] diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 999dbeadf6..3f1b1f653d 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -19,6 +19,7 @@ bzip2 = "0.4.3" dashmap = { version = "4.0.2", features = ["rayon", "raw-api"] } crossbeam-channel = "0.5" dir-diff = "0.3.2" +enum-iterator = "0.7.0" flate2 = "1.0.22" fnv = "1.0.7" index_list = "0.2.7" diff --git a/runtime/src/account_rent_state.rs b/runtime/src/account_rent_state.rs new file mode 100644 index 0000000000..a7b7d3ce51 --- /dev/null +++ b/runtime/src/account_rent_state.rs @@ -0,0 +1,131 @@ +use { + enum_iterator::IntoEnumIterator, + log::*, + solana_sdk::{ + account::{AccountSharedData, ReadableAccount}, + pubkey::Pubkey, + rent::Rent, + transaction::{Result, TransactionError}, + }, +}; + +#[derive(Debug, PartialEq, IntoEnumIterator)] +pub(crate) enum RentState { + Uninitialized, // account.lamports == 0 + RentPaying, // 0 < account.lamports < rent-exempt-minimum + RentExempt, // account.lamports >= rent-exempt-minimum +} + +impl RentState { + pub(crate) fn from_account(account: &AccountSharedData, rent: &Rent) -> Self { + if account.lamports() == 0 { + Self::Uninitialized + } else if !rent.is_exempt(account.lamports(), account.data().len()) { + Self::RentPaying + } else { + Self::RentExempt + } + } + + pub(crate) fn transition_allowed_from(&self, pre_rent_state: &RentState) -> bool { + // Only a legacy RentPaying account may end in the RentPaying state after message processing + !(self == &Self::RentPaying && pre_rent_state != &Self::RentPaying) + } +} + +pub(crate) fn submit_rent_state_metrics(pre_rent_state: &RentState, post_rent_state: &RentState) { + match (pre_rent_state, post_rent_state) { + (&RentState::Uninitialized, &RentState::RentPaying) => { + inc_new_counter_info!("rent_paying_err-new_account", 1); + } + (&RentState::RentPaying, &RentState::RentPaying) => { + inc_new_counter_info!("rent_paying_ok-legacy", 1); + } + (_, &RentState::RentPaying) => { + inc_new_counter_info!("rent_paying_err-other", 1); + } + _ => {} + } +} + +pub(crate) fn check_rent_state( + pre_rent_state: Option<&RentState>, + post_rent_state: Option<&RentState>, + pubkey: &Pubkey, + account: &AccountSharedData, +) -> Result<()> { + if let Some((pre_rent_state, post_rent_state)) = pre_rent_state.zip(post_rent_state) { + submit_rent_state_metrics(pre_rent_state, post_rent_state); + if !post_rent_state.transition_allowed_from(pre_rent_state) { + debug!("Account {:?} not rent exempt, state {:?}", pubkey, account,); + return Err(TransactionError::InvalidRentPayingAccount); + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use {super::*, solana_sdk::pubkey::Pubkey}; + + #[test] + fn test_from_account() { + let program_id = Pubkey::new_unique(); + let uninitialized_account = AccountSharedData::new(0, 0, &Pubkey::default()); + + let account_data_size = 100; + + let rent = Rent::free(); + let rent_exempt_account = AccountSharedData::new(1, account_data_size, &program_id); // if rent is free, all accounts with non-zero lamports and non-empty data are rent-exempt + + assert_eq!( + RentState::from_account(&uninitialized_account, &rent), + RentState::Uninitialized + ); + assert_eq!( + RentState::from_account(&rent_exempt_account, &rent), + RentState::RentExempt + ); + + let rent = Rent::default(); + let rent_minimum_balance = rent.minimum_balance(account_data_size); + let rent_paying_account = AccountSharedData::new( + rent_minimum_balance.saturating_sub(1), + account_data_size, + &program_id, + ); + let rent_exempt_account = AccountSharedData::new( + rent.minimum_balance(account_data_size), + account_data_size, + &program_id, + ); + + assert_eq!( + RentState::from_account(&uninitialized_account, &rent), + RentState::Uninitialized + ); + assert_eq!( + RentState::from_account(&rent_paying_account, &rent), + RentState::RentPaying + ); + assert_eq!( + RentState::from_account(&rent_exempt_account, &rent), + RentState::RentExempt + ); + } + + #[test] + fn test_transition_allowed_from() { + for post_rent_state in RentState::into_enum_iter() { + for pre_rent_state in RentState::into_enum_iter() { + if post_rent_state == RentState::RentPaying + && pre_rent_state != RentState::RentPaying + { + assert!(!post_rent_state.transition_allowed_from(&pre_rent_state)); + } else { + assert!(post_rent_state.transition_allowed_from(&pre_rent_state)); + } + } + } + } +} diff --git a/runtime/src/accounts_db.rs b/runtime/src/accounts_db.rs index 54d279c5ec..16d998fbee 100644 --- a/runtime/src/accounts_db.rs +++ b/runtime/src/accounts_db.rs @@ -218,6 +218,7 @@ pub struct ErrorCounters { pub invalid_program_for_execution: usize, pub not_allowed_during_cluster_maintenance: usize, pub invalid_writable_account: usize, + pub invalid_rent_paying_account: usize, } #[derive(Debug, Default, Clone, Copy)] diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index dce23aa559..c0abce392c 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -163,6 +163,8 @@ use { }, }; +mod transaction_account_state_info; + pub const SECONDS_PER_YEAR: f64 = 365.25 * 24.0 * 60.0 * 60.0; pub const MAX_LEADER_SCHEDULE_STAKES: Epoch = 5; @@ -217,7 +219,7 @@ impl RentDebits { } type BankStatusCache = StatusCache>; -#[frozen_abi(digest = "2r36f5cfgP7ABq7D3kRkRfQZWdggGFUnnhwTrVEWhoTC")] +#[frozen_abi(digest = "Gr2MTwWyUdkbF6FxM6TSwGaC3c5buUirHmh64oAPgg7Z")] pub type BankSlotDelta = SlotDelta>; // Eager rent collection repeats in cyclic manner. @@ -3722,6 +3724,9 @@ impl Bank { let account_refcells = Self::accounts_to_refcells(&mut loaded_transaction.accounts); + let pre_account_state_info = + self.get_transaction_account_state_info(&account_refcells, tx.message()); + let instruction_recorders = if enable_cpi_recording { let ix_count = tx.message().instructions().len(); let mut recorders = Vec::with_capacity(ix_count); @@ -3772,11 +3777,28 @@ impl Bank { ); let status = process_result + .and_then(|info| { + let post_account_state_info = + self.get_transaction_account_state_info(&account_refcells, tx.message()); + self.verify_transaction_account_state_changes( + &pre_account_state_info, + &post_account_state_info, + &account_refcells, + ) + .map(|_| info) + }) .map(|info| { self.store_accounts_data_len(info.accounts_data_len); }) .map_err(|err| { - error_counters.instruction_error += 1; + match err { + TransactionError::InvalidRentPayingAccount => { + error_counters.invalid_rent_paying_account += 1; + } + _ => { + error_counters.instruction_error += 1; + } + } err }); @@ -6313,7 +6335,7 @@ impl Bank { // Adjust capitalization.... it has been wrapping, reducing the real capitalization by 1-lamport self.capitalization.fetch_add(1, Relaxed); info!( - "purged rewards pool accont: {}, new capitalization: {}", + "purged rewards pool account: {}, new capitalization: {}", reward_pubkey, self.capitalization() ); @@ -7203,6 +7225,12 @@ pub(crate) mod tests { bootstrap_validator_stake_lamports, ) .genesis_config; + // While we are preventing new accounts left in a rent-paying state, not quite ready to rip + // out all the rent assessment tests. Just deactivate the feature for now. + genesis_config + .accounts + .remove(&feature_set::require_rent_exempt_accounts::id()) + .unwrap(); genesis_config.epoch_schedule = EpochSchedule::custom( MINIMUM_SLOTS_PER_EPOCH, @@ -15603,4 +15631,283 @@ pub(crate) mod tests { 7 ); } + + #[derive(Serialize, Deserialize)] + enum MockTransferInstruction { + Transfer(u64), + } + + fn mock_transfer_process_instruction( + first_instruction_account: usize, + data: &[u8], + invoke_context: &mut InvokeContext, + ) -> result::Result<(), InstructionError> { + let keyed_accounts = invoke_context.get_keyed_accounts()?; + if let Ok(instruction) = bincode::deserialize(data) { + match instruction { + MockTransferInstruction::Transfer(amount) => { + keyed_account_at_index(keyed_accounts, first_instruction_account + 1)? + .account + .borrow_mut() + .checked_sub_lamports(amount)?; + keyed_account_at_index(keyed_accounts, first_instruction_account + 2)? + .account + .borrow_mut() + .checked_add_lamports(amount)?; + Ok(()) + } + } + } else { + Err(InstructionError::InvalidInstructionData) + } + } + + fn create_mock_transfer( + payer: &Keypair, + from: &Keypair, + to: &Keypair, + amount: u64, + mock_program_id: Pubkey, + recent_blockhash: Hash, + ) -> Transaction { + let account_metas = vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new(from.pubkey(), true), + AccountMeta::new(to.pubkey(), true), + ]; + let transfer_instruction = Instruction::new_with_bincode( + mock_program_id, + &MockTransferInstruction::Transfer(amount), + account_metas, + ); + Transaction::new_signed_with_payer( + &[transfer_instruction], + Some(&payer.pubkey()), + &[payer, from, to], + recent_blockhash, + ) + } + + #[test] + fn test_invalid_rent_state_changes_existing_accounts() { + let GenesisConfigInfo { + mut genesis_config, + mint_keypair, + .. + } = create_genesis_config_with_leader(sol_to_lamports(100.), &Pubkey::new_unique(), 42); + genesis_config.rent = Rent::default(); + + let mock_program_id = Pubkey::new_unique(); + let account_data_size = 100; + let rent_exempt_minimum = genesis_config.rent.minimum_balance(account_data_size); + + // Create legacy accounts of various kinds + let rent_paying_account = Keypair::new(); + genesis_config.accounts.insert( + rent_paying_account.pubkey(), + Account::new(rent_exempt_minimum - 1, account_data_size, &mock_program_id), + ); + let rent_exempt_account = Keypair::new(); + genesis_config.accounts.insert( + rent_exempt_account.pubkey(), + Account::new(rent_exempt_minimum, account_data_size, &mock_program_id), + ); + // Activate features, including require_rent_exempt_accounts + activate_all_features(&mut genesis_config); + + let mut bank = Bank::new_for_tests(&genesis_config); + bank.add_builtin( + "mock_program", + &mock_program_id, + mock_transfer_process_instruction, + ); + let recent_blockhash = bank.last_blockhash(); + + let check_account_is_rent_exempt = |pubkey: &Pubkey| -> bool { + let account = bank.get_account(pubkey).unwrap(); + Rent::default().is_exempt(account.lamports(), account.data().len()) + }; + + // RentPaying account can be left as Uninitialized, in other RentPaying states, or RentExempt + let tx = create_mock_transfer( + &mint_keypair, // payer + &rent_paying_account, // from + &mint_keypair, // to + 1, + mock_program_id, + recent_blockhash, + ); + let result = bank.process_transaction(&tx); + assert!(result.is_ok()); + assert!(!check_account_is_rent_exempt(&rent_paying_account.pubkey())); + let tx = create_mock_transfer( + &mint_keypair, // payer + &rent_paying_account, // from + &mint_keypair, // to + rent_exempt_minimum - 2, + mock_program_id, + recent_blockhash, + ); + let result = bank.process_transaction(&tx); + assert!(result.is_ok()); + assert!(bank.get_account(&rent_paying_account.pubkey()).is_none()); + + bank.store_account( + // restore program-owned account + &rent_paying_account.pubkey(), + &AccountSharedData::new(rent_exempt_minimum - 1, account_data_size, &mock_program_id), + ); + let result = bank.transfer(1, &mint_keypair, &rent_paying_account.pubkey()); + assert!(result.is_ok()); + assert!(check_account_is_rent_exempt(&rent_paying_account.pubkey())); + + // RentExempt account can only remain RentExempt or be Uninitialized + let tx = create_mock_transfer( + &mint_keypair, // payer + &rent_exempt_account, // from + &mint_keypair, // to + 1, + mock_program_id, + recent_blockhash, + ); + let result = bank.process_transaction(&tx); + assert!(result.is_err()); + assert!(check_account_is_rent_exempt(&rent_exempt_account.pubkey())); + let result = bank.transfer(1, &mint_keypair, &rent_exempt_account.pubkey()); + assert!(result.is_ok()); + assert!(check_account_is_rent_exempt(&rent_exempt_account.pubkey())); + let tx = create_mock_transfer( + &mint_keypair, // payer + &rent_exempt_account, // from + &mint_keypair, // to + rent_exempt_minimum + 1, + mock_program_id, + recent_blockhash, + ); + let result = bank.process_transaction(&tx); + assert!(result.is_ok()); + assert!(bank.get_account(&rent_exempt_account.pubkey()).is_none()); + } + + #[test] + fn test_invalid_rent_state_changes_new_accounts() { + let GenesisConfigInfo { + mut genesis_config, + mint_keypair, + .. + } = create_genesis_config_with_leader(sol_to_lamports(100.), &Pubkey::new_unique(), 42); + genesis_config.rent = Rent::default(); + + let mock_program_id = Pubkey::new_unique(); + let account_data_size = 100; + let rent_exempt_minimum = genesis_config.rent.minimum_balance(account_data_size); + + // Activate features, including require_rent_exempt_accounts + activate_all_features(&mut genesis_config); + + let mut bank = Bank::new_for_tests(&genesis_config); + bank.add_builtin( + "mock_program", + &mock_program_id, + mock_transfer_process_instruction, + ); + let recent_blockhash = bank.last_blockhash(); + + let check_account_is_rent_exempt = |pubkey: &Pubkey| -> bool { + let account = bank.get_account(pubkey).unwrap(); + Rent::default().is_exempt(account.lamports(), account.data().len()) + }; + + // Try to create RentPaying account + let rent_paying_account = Keypair::new(); + let tx = system_transaction::create_account( + &mint_keypair, + &rent_paying_account, + recent_blockhash, + rent_exempt_minimum - 1, + account_data_size as u64, + &mock_program_id, + ); + let result = bank.process_transaction(&tx); + assert!(result.is_err()); + assert!(bank.get_account(&rent_paying_account.pubkey()).is_none()); + + // Try to create RentExempt account + let rent_exempt_account = Keypair::new(); + let tx = system_transaction::create_account( + &mint_keypair, + &rent_exempt_account, + recent_blockhash, + rent_exempt_minimum, + account_data_size as u64, + &mock_program_id, + ); + let result = bank.process_transaction(&tx); + assert!(result.is_ok()); + assert!(check_account_is_rent_exempt(&rent_exempt_account.pubkey())); + } + + #[test] + fn test_rent_state_changes_sysvars() { + let GenesisConfigInfo { + mut genesis_config, + mint_keypair, + .. + } = create_genesis_config_with_leader(sol_to_lamports(100.), &Pubkey::new_unique(), 42); + genesis_config.rent = Rent::default(); + // Activate features, including require_rent_exempt_accounts + activate_all_features(&mut genesis_config); + + let validator_pubkey = solana_sdk::pubkey::new_rand(); + let validator_stake_lamports = sol_to_lamports(1.); + let validator_staking_keypair = Keypair::new(); + let validator_voting_keypair = Keypair::new(); + + let validator_vote_account = vote_state::create_account( + &validator_voting_keypair.pubkey(), + &validator_pubkey, + 0, + validator_stake_lamports, + ); + + let validator_stake_account = stake_state::create_account( + &validator_staking_keypair.pubkey(), + &validator_voting_keypair.pubkey(), + &validator_vote_account, + &genesis_config.rent, + validator_stake_lamports, + ); + + genesis_config.accounts.insert( + validator_pubkey, + Account::new( + genesis_config.rent.minimum_balance(0), + 0, + &system_program::id(), + ), + ); + genesis_config.accounts.insert( + validator_staking_keypair.pubkey(), + Account::from(validator_stake_account), + ); + genesis_config.accounts.insert( + validator_voting_keypair.pubkey(), + Account::from(validator_vote_account), + ); + + let bank = Bank::new_for_tests(&genesis_config); + + // Ensure transactions with sysvars succeed, even though sysvars appear RentPaying by balance + let tx = Transaction::new_signed_with_payer( + &[stake_instruction::deactivate_stake( + &validator_staking_keypair.pubkey(), + &validator_staking_keypair.pubkey(), + )], + Some(&mint_keypair.pubkey()), + &[&mint_keypair, &validator_staking_keypair], + bank.last_blockhash(), + ); + let result = bank.process_transaction(&tx); + assert!(result.is_ok()); + } } diff --git a/runtime/src/bank/transaction_account_state_info.rs b/runtime/src/bank/transaction_account_state_info.rs new file mode 100644 index 0000000000..bf04906705 --- /dev/null +++ b/runtime/src/bank/transaction_account_state_info.rs @@ -0,0 +1,76 @@ +use { + crate::{ + account_rent_state::{check_rent_state, RentState}, + bank::Bank, + }, + solana_program_runtime::invoke_context::TransactionAccountRefCell, + solana_sdk::{ + account::ReadableAccount, feature_set, message::SanitizedMessage, native_loader, + transaction::Result, + }, +}; + +pub(crate) struct TransactionAccountStateInfo { + rent_state: Option, // None: readonly account +} + +impl Bank { + pub(crate) fn get_transaction_account_state_info( + &self, + transaction_account_refcells: &[TransactionAccountRefCell], + message: &SanitizedMessage, + ) -> Vec { + transaction_account_refcells + .iter() + .enumerate() + .map(|(i, (_pubkey, account_refcell))| { + let account = account_refcell.borrow(); + + let rent_state = if message.is_writable(i) { + // Native programs appear to be RentPaying because they carry low lamport + // balances; however they will never be loaded as writable + debug_assert!(!native_loader::check_id(account.owner())); + + Some(RentState::from_account( + &account, + &self.rent_collector().rent, + )) + } else { + None + }; + TransactionAccountStateInfo { rent_state } + }) + .collect() + } + + pub(crate) fn verify_transaction_account_state_changes( + &self, + pre_state_infos: &[TransactionAccountStateInfo], + post_state_infos: &[TransactionAccountStateInfo], + transaction_account_refcells: &[TransactionAccountRefCell], + ) -> Result<()> { + let require_rent_exempt_accounts = self + .feature_set + .is_active(&feature_set::require_rent_exempt_accounts::id()); + for ((pre_state_info, post_state_info), (pubkey, account_refcell)) in pre_state_infos + .iter() + .zip(post_state_infos) + .zip(transaction_account_refcells) + { + if let Err(err) = check_rent_state( + pre_state_info.rent_state.as_ref(), + post_state_info.rent_state.as_ref(), + pubkey, + &account_refcell.borrow(), + ) { + // Feature gate only wraps the actual error return so that the metrics and debug + // logging generated by `check_rent_state()` can be examined before feature + // activation + if require_rent_exempt_accounts { + return Err(err); + } + } + } + Ok(()) + } +} diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 81a5ada5fc..b59f47101b 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -1,5 +1,6 @@ #![cfg_attr(RUSTC_WITH_SPECIALIZATION, feature(min_specialization))] #![allow(clippy::integer_arithmetic)] +pub mod account_rent_state; pub mod accounts; pub mod accounts_background_service; pub mod accounts_cache; diff --git a/sdk/src/feature_set.rs b/sdk/src/feature_set.rs index af10f1a38d..a86504506d 100644 --- a/sdk/src/feature_set.rs +++ b/sdk/src/feature_set.rs @@ -283,6 +283,10 @@ pub mod max_tx_account_locks { solana_sdk::declare_id!("CBkDroRDqm8HwHe6ak9cguPjUomrASEkfmxEaZ5CNNxz"); } +pub mod require_rent_exempt_accounts { + solana_sdk::declare_id!("BkFDxiJQWZXGTZaJQxH7wVEHkAmwCgSEVkrvswFfRJPD"); +} + lazy_static! { /// Map of feature identifiers to user-visible description pub static ref FEATURE_NAMES: HashMap = [ @@ -348,6 +352,7 @@ lazy_static! { (reject_all_elf_rw::id(), "reject all read-write data in program elfs"), (cap_accounts_data_len::id(), "cap the accounts data len"), (max_tx_account_locks::id(), "enforce max number of locked accounts per transaction"), + (require_rent_exempt_accounts::id(), "require all new transaction accounts with data to be rent-exempt"), /*************** ADD NEW FEATURES HERE ***************/ ] .iter() diff --git a/sdk/src/transaction/error.rs b/sdk/src/transaction/error.rs index a207b91951..72812bbacf 100644 --- a/sdk/src/transaction/error.rs +++ b/sdk/src/transaction/error.rs @@ -125,6 +125,12 @@ pub enum TransactionError { /// Address table lookup uses an invalid index #[error("Transaction address table lookup uses an invalid index")] InvalidAddressLookupTableIndex, + + /// Transaction leaves an account with a lower balance than rent-exempt minimum + #[error( + "Transaction leaves an account with data with a lower balance than rent-exempt minimum" + )] + InvalidRentPayingAccount, } impl From for TransactionError { diff --git a/storage-proto/proto/transaction_by_addr.proto b/storage-proto/proto/transaction_by_addr.proto index 6b467f2694..cc63498876 100644 --- a/storage-proto/proto/transaction_by_addr.proto +++ b/storage-proto/proto/transaction_by_addr.proto @@ -51,6 +51,7 @@ enum TransactionErrorType { INVALID_ADDRESS_LOOKUP_TABLE_OWNER = 24; INVALID_ADDRESS_LOOKUP_TABLE_DATA = 25; INVALID_ADDRESS_LOOKUP_TABLE_INDEX = 26; + INVALID_RENT_PAYING_ACCOUNT = 27; } message InstructionError { diff --git a/storage-proto/src/convert.rs b/storage-proto/src/convert.rs index 05d1c4be2e..e5b1807f1a 100644 --- a/storage-proto/src/convert.rs +++ b/storage-proto/src/convert.rs @@ -574,6 +574,7 @@ impl TryFrom for TransactionError { 24 => TransactionError::InvalidAddressLookupTableOwner, 25 => TransactionError::InvalidAddressLookupTableData, 26 => TransactionError::InvalidAddressLookupTableIndex, + 27 => TransactionError::InvalidRentPayingAccount, _ => return Err("Invalid TransactionError"), }) } @@ -662,6 +663,10 @@ impl From for tx_by_addr::TransactionError { TransactionError::InvalidAddressLookupTableIndex => { tx_by_addr::TransactionErrorType::InvalidAddressLookupTableIndex } + + TransactionError::InvalidRentPayingAccount => { + tx_by_addr::TransactionErrorType::InvalidRentPayingAccount + } } as i32, instruction_error: match transaction_error { TransactionError::InstructionError(index, ref instruction_error) => {