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
This commit is contained in:
Tyera Eulberg 2022-01-11 11:32:25 -07:00 committed by GitHub
parent a49ef49f87
commit 637e366b18
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 819 additions and 179 deletions

1
Cargo.lock generated
View File

@ -5706,6 +5706,7 @@ dependencies = [
"dashmap",
"dir-diff",
"ed25519-dalek",
"enum-iterator",
"flate2",
"fnv",
"index_list",

View File

@ -53,7 +53,8 @@ Create TYPE "TransactionErrorCode" AS ENUM (
'AddressLookupTableNotFound',
'InvalidAddressLookupTableOwner',
'InvalidAddressLookupTableData',
'InvalidAddressLookupTableIndex'
'InvalidAddressLookupTableIndex',
'InvalidRentPayingAccount'
);
CREATE TYPE "TransactionError" AS (

View File

@ -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,
}
}
}

View File

@ -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);
}

View File

@ -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));
}

View File

@ -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(

View File

@ -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);
}

View File

@ -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);
}

View File

@ -2425,30 +2425,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!();
{

View File

@ -3821,10 +3821,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)
@ -3837,7 +3839,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));

View File

@ -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

View File

@ -869,6 +869,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"
@ -3451,6 +3471,7 @@ dependencies = [
"crossbeam-channel",
"dashmap",
"dir-diff",
"enum-iterator",
"flate2",
"fnv",
"index_list",

View File

@ -20,6 +20,7 @@ use {
commitment_config::CommitmentConfig,
hash::Hash,
pubkey::Pubkey,
rent::Rent,
signature::{Keypair, Signer},
system_transaction,
transaction::Transaction,
@ -80,7 +81,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]));
@ -243,7 +249,7 @@ fn test_rpc_subscriptions() {
system_transaction::transfer(
&alice,
&solana_sdk::pubkey::new_rand(),
1,
Rent::default().minimum_balance(0),
recent_blockhash,
)
})
@ -381,7 +387,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) => {

View File

@ -4273,23 +4273,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]);
@ -4412,6 +4421,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();
@ -4431,7 +4441,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],
@ -4501,10 +4512,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);
@ -4812,13 +4827,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<String> = non_circulating_accounts()
@ -4837,13 +4855,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());
@ -5165,7 +5186,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
@ -5236,9 +5257,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);
@ -5256,7 +5279,7 @@ pub mod tests {
"context":{"slot":0},
"value":[{
"owner": "11111111111111111111111111111111",
"lamports": 20,
"lamports": rent_exempt_amount,
"data": ["", "base64"],
"executable": false,
"rentEpoch": 0
@ -5264,7 +5287,7 @@ pub mod tests {
null,
{
"owner": "11111111111111111111111111111111",
"lamports": 42,
"lamports": rent_exempt_amount + 1,
"data": [base64::encode(&data), "base64"],
"executable": false,
"rentEpoch": 0
@ -5376,7 +5399,7 @@ pub mod tests {
"pubkey": "{}",
"account": {{
"owner": "{}",
"lamports": 20,
"lamports": {},
"data": "",
"executable": false,
"rentEpoch": 0
@ -5386,7 +5409,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");
@ -5579,8 +5603,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();
@ -5621,7 +5647,7 @@ pub mod tests {
"data": ["", "base64"],
"executable": false,
"owner": "11111111111111111111111111111111",
"lamports": 1234,
"lamports": rent_exempt_amount,
"rentEpoch": 0
}
],
@ -5871,6 +5897,7 @@ pub mod tests {
blockhash,
alice,
confirmed_block_signatures,
bank,
..
} = start_rpc_handler_with_tx(&bob_pubkey);
@ -5889,7 +5916,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]

View File

@ -19,6 +19,7 @@ bzip2 = "0.4.3"
dashmap = { version = "5.0.0", 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"

View File

@ -0,0 +1,135 @@
use {
enum_iterator::IntoEnumIterator,
log::*,
solana_sdk::{
account::{AccountSharedData, ReadableAccount},
rent::Rent,
transaction::{Result, TransactionError},
transaction_context::TransactionContext,
},
};
#[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>,
transaction_context: &TransactionContext,
index: usize,
) -> 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 {:?}",
transaction_context.get_key_of_account_at_index(index),
transaction_context.get_account_at_index(index).borrow(),
);
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));
}
}
}
}
}

View File

@ -213,6 +213,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)]

View File

@ -160,6 +160,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;
@ -214,7 +216,7 @@ impl RentDebits {
}
type BankStatusCache = StatusCache<Result<()>>;
#[frozen_abi(digest = "2r36f5cfgP7ABq7D3kRkRfQZWdggGFUnnhwTrVEWhoTC")]
#[frozen_abi(digest = "Gr2MTwWyUdkbF6FxM6TSwGaC3c5buUirHmh64oAPgg7Z")]
pub type BankSlotDelta = SlotDelta<Result<()>>;
// Eager rent collection repeats in cyclic manner.
@ -3697,6 +3699,9 @@ impl Bank {
compute_budget.max_invoke_depth.saturating_add(1),
);
let pre_account_state_info =
self.get_transaction_account_state_info(&transaction_context, tx.message());
let instruction_recorder = if enable_cpi_recording {
Some(InstructionRecorder::new_ref(
tx.message().instructions().len(),
@ -3746,11 +3751,28 @@ impl Bank {
);
let status = process_result
.and_then(|info| {
let post_account_state_info =
self.get_transaction_account_state_info(&transaction_context, tx.message());
self.verify_transaction_account_state_changes(
&pre_account_state_info,
&post_account_state_info,
&transaction_context,
)
.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
});
@ -6254,7 +6276,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()
);
@ -7085,6 +7107,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,
@ -15526,4 +15554,282 @@ 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 transaction_context = &invoke_context.transaction_context;
let instruction_context = transaction_context.get_current_instruction_context()?;
if let Ok(instruction) = bincode::deserialize(data) {
match instruction {
MockTransferInstruction::Transfer(amount) => {
instruction_context
.try_borrow_instruction_account(transaction_context, 1)?
.checked_sub_lamports(amount)?;
instruction_context
.try_borrow_instruction_account(transaction_context, 2)?
.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());
}
}

View File

@ -0,0 +1,71 @@
use {
crate::{
account_rent_state::{check_rent_state, RentState},
bank::Bank,
},
solana_sdk::{
account::ReadableAccount, feature_set, message::SanitizedMessage, native_loader,
transaction::Result, transaction_context::TransactionContext,
},
};
pub(crate) struct TransactionAccountStateInfo {
rent_state: Option<RentState>, // None: readonly account
}
impl Bank {
pub(crate) fn get_transaction_account_state_info(
&self,
transaction_context: &TransactionContext,
message: &SanitizedMessage,
) -> Vec<TransactionAccountStateInfo> {
(0..transaction_context.get_number_of_accounts())
.map(|i| {
let rent_state = if message.is_writable(i) {
let account = transaction_context.get_account_at_index(i).borrow();
// 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_context: &TransactionContext,
) -> Result<()> {
let require_rent_exempt_accounts = self
.feature_set
.is_active(&feature_set::require_rent_exempt_accounts::id());
for (i, (pre_state_info, post_state_info)) in
pre_state_infos.iter().zip(post_state_infos).enumerate()
{
if let Err(err) = check_rent_state(
pre_state_info.rent_state.as_ref(),
post_state_info.rent_state.as_ref(),
transaction_context,
i,
) {
// 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(())
}
}

View File

@ -1,6 +1,7 @@
#![cfg_attr(RUSTC_WITH_SPECIALIZATION, feature(min_specialization))]
#![allow(clippy::integer_arithmetic)]
pub mod account_info;
pub mod account_rent_state;
pub mod accounts;
pub mod accounts_background_service;
pub mod accounts_cache;

View File

@ -295,6 +295,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<Pubkey, &'static str> = [
@ -363,6 +367,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()

View File

@ -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<SanitizeError> for TransactionError {

View File

@ -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 {

View File

@ -574,6 +574,7 @@ impl TryFrom<tx_by_addr::TransactionError> for TransactionError {
24 => TransactionError::InvalidAddressLookupTableOwner,
25 => TransactionError::InvalidAddressLookupTableData,
26 => TransactionError::InvalidAddressLookupTableIndex,
27 => TransactionError::InvalidRentPayingAccount,
_ => return Err("Invalid TransactionError"),
})
}
@ -662,6 +663,10 @@ impl From<TransactionError> 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) => {