Allow the same account to be passed multiple times to a single instruction (#7795)
This commit is contained in:
@ -9,7 +9,9 @@ use solana_sdk::message::Message;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use solana_sdk::system_program;
|
||||
use solana_sdk::transaction::TransactionError;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
use std::sync::RwLock;
|
||||
|
||||
#[cfg(unix)]
|
||||
@ -17,44 +19,6 @@ use libloading::os::unix::*;
|
||||
#[cfg(windows)]
|
||||
use libloading::os::windows::*;
|
||||
|
||||
/// Return true if the slice has any duplicate elements
|
||||
pub fn has_duplicates<T: PartialEq>(xs: &[T]) -> bool {
|
||||
// Note: This is an O(n^2) algorithm, but requires no heap allocations. The benchmark
|
||||
// `bench_has_duplicates` in benches/message_processor.rs shows that this implementation is
|
||||
// ~50 times faster than using HashSet for very short slices.
|
||||
for i in 1..xs.len() {
|
||||
if xs[i..].contains(&xs[i - 1]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Get mut references to a subset of elements.
|
||||
fn get_subset_unchecked_mut<'a, T>(
|
||||
xs: &'a mut [T],
|
||||
indexes: &[u8],
|
||||
) -> Result<Vec<&'a mut T>, InstructionError> {
|
||||
// Since the compiler doesn't know the indexes are unique, dereferencing
|
||||
// multiple mut elements is assumed to be unsafe. If, however, all
|
||||
// indexes are unique, it's perfectly safe. The returned elements will share
|
||||
// the liftime of the input slice.
|
||||
|
||||
// Make certain there are no duplicate indexes. If there are, return an error
|
||||
// because we can't return multiple mut references to the same element.
|
||||
if has_duplicates(indexes) {
|
||||
return Err(InstructionError::DuplicateAccountIndex);
|
||||
}
|
||||
|
||||
Ok(indexes
|
||||
.iter()
|
||||
.map(|i| {
|
||||
let ptr = &mut xs[*i as usize] as *mut T;
|
||||
unsafe { &mut *ptr }
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
// The relevant state of an account before an Instruction executes, used
|
||||
// to verify account integrity after the Instruction completes
|
||||
pub struct PreInstructionAccount {
|
||||
@ -204,8 +168,8 @@ impl MessageProcessor {
|
||||
&self,
|
||||
message: &Message,
|
||||
instruction: &CompiledInstruction,
|
||||
executable_accounts: &mut [(Pubkey, Account)],
|
||||
program_accounts: &mut [&mut Account],
|
||||
executable_accounts: &mut [(Pubkey, RefCell<Account>)],
|
||||
program_accounts: &mut [Rc<RefCell<Account>>],
|
||||
) -> Result<(), InstructionError> {
|
||||
let program_id = instruction.program_id(&message.account_keys);
|
||||
let mut keyed_accounts = create_keyed_readonly_accounts(executable_accounts);
|
||||
@ -234,7 +198,7 @@ impl MessageProcessor {
|
||||
keyed_accounts.append(&mut keyed_accounts2);
|
||||
|
||||
assert!(
|
||||
keyed_accounts[0].account.executable,
|
||||
keyed_accounts[0].try_account_ref()?.executable,
|
||||
"loader not executable"
|
||||
);
|
||||
|
||||
@ -257,8 +221,38 @@ impl MessageProcessor {
|
||||
)
|
||||
}
|
||||
|
||||
fn sum_account_lamports(accounts: &mut [&mut Account]) -> u128 {
|
||||
accounts.iter().map(|a| u128::from(a.lamports)).sum()
|
||||
pub fn verify_account_references(
|
||||
executable_accounts: &mut [(Pubkey, RefCell<Account>)],
|
||||
program_accounts: &mut [Rc<RefCell<Account>>],
|
||||
) -> Result<(), InstructionError> {
|
||||
for account in program_accounts.iter() {
|
||||
account
|
||||
.try_borrow_mut()
|
||||
.map_err(|_| InstructionError::AccountBorrowOutstanding)?;
|
||||
}
|
||||
for (_, account) in executable_accounts.iter() {
|
||||
account
|
||||
.try_borrow_mut()
|
||||
.map_err(|_| InstructionError::AccountBorrowOutstanding)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn sum_account_lamports(accounts: &[Rc<RefCell<Account>>]) -> u128 {
|
||||
// Note: This is an O(n^2) algorithm,
|
||||
// but performed on a very small slice and requires no heap allocations
|
||||
accounts
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, a)| {
|
||||
for account in accounts.iter().skip(i + 1) {
|
||||
if Rc::ptr_eq(a, account) {
|
||||
return 0; // don't double count duplicates
|
||||
}
|
||||
}
|
||||
u128::from(a.borrow().lamports)
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
||||
/// Execute an instruction
|
||||
@ -269,8 +263,8 @@ impl MessageProcessor {
|
||||
&self,
|
||||
message: &Message,
|
||||
instruction: &CompiledInstruction,
|
||||
executable_accounts: &mut [(Pubkey, Account)],
|
||||
program_accounts: &mut [&mut Account],
|
||||
executable_accounts: &mut [(Pubkey, RefCell<Account>)],
|
||||
program_accounts: &mut [Rc<RefCell<Account>>],
|
||||
) -> Result<(), InstructionError> {
|
||||
assert_eq!(instruction.accounts.len(), program_accounts.len());
|
||||
let program_id = instruction.program_id(&message.account_keys);
|
||||
@ -280,8 +274,9 @@ impl MessageProcessor {
|
||||
.enumerate()
|
||||
.map(|(i, account)| {
|
||||
let is_writable = message.is_writable(instruction.accounts[i] as usize);
|
||||
let account = account.borrow();
|
||||
PreInstructionAccount::new(
|
||||
account,
|
||||
&account,
|
||||
is_writable,
|
||||
need_account_data_checked(&account.owner, program_id, is_writable),
|
||||
)
|
||||
@ -292,11 +287,16 @@ impl MessageProcessor {
|
||||
|
||||
self.process_instruction(message, instruction, executable_accounts, program_accounts)?;
|
||||
|
||||
// Verify all accounts have zero outstanding refs
|
||||
Self::verify_account_references(executable_accounts, program_accounts)?;
|
||||
// Verify the instruction
|
||||
for (pre_account, post_account) in pre_accounts.iter().zip(program_accounts.iter()) {
|
||||
verify_account_changes(&program_id, pre_account, post_account)?;
|
||||
let post_account = post_account
|
||||
.try_borrow()
|
||||
.map_err(|_| InstructionError::AccountBorrowFailed)?;
|
||||
verify_account_changes(&program_id, pre_account, &post_account)?;
|
||||
}
|
||||
// The total sum of all the lamports in all the accounts cannot change.
|
||||
// Verify total sum of all the lamports did not change
|
||||
let post_total = Self::sum_account_lamports(program_accounts);
|
||||
if pre_total != post_total {
|
||||
return Err(InstructionError::UnbalancedInstruction);
|
||||
@ -310,19 +310,24 @@ impl MessageProcessor {
|
||||
pub fn process_message(
|
||||
&self,
|
||||
message: &Message,
|
||||
loaders: &mut [Vec<(Pubkey, Account)>],
|
||||
accounts: &mut [Account],
|
||||
loaders: &mut [Vec<(Pubkey, RefCell<Account>)>],
|
||||
accounts: &mut [Rc<RefCell<Account>>],
|
||||
) -> Result<(), TransactionError> {
|
||||
for (instruction_index, instruction) in message.instructions.iter().enumerate() {
|
||||
let executable_index = message
|
||||
.program_position(instruction.program_id_index as usize)
|
||||
.ok_or(TransactionError::InvalidAccountIndex)?;
|
||||
let executable_accounts = &mut loaders[executable_index];
|
||||
let mut program_accounts = get_subset_unchecked_mut(accounts, &instruction.accounts)
|
||||
.map_err(|err| TransactionError::InstructionError(instruction_index as u8, err))?;
|
||||
// TODO: `get_subset_unchecked_mut` panics on an index out of bounds if an executable
|
||||
|
||||
// TODO: panics on an index out of bounds if an executable
|
||||
// account is also included as a regular account for an instruction, because the
|
||||
// executable account is not passed in as part of the accounts slice
|
||||
let mut program_accounts: Vec<_> = instruction
|
||||
.accounts
|
||||
.iter()
|
||||
.map(|i| accounts[*i as usize].clone())
|
||||
.collect();
|
||||
|
||||
self.execute_instruction(
|
||||
message,
|
||||
instruction,
|
||||
@ -373,37 +378,72 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_has_duplicates() {
|
||||
assert!(!has_duplicates(&[1, 2]));
|
||||
assert!(has_duplicates(&[1, 2, 1]));
|
||||
fn test_verify_account_references() {
|
||||
let mut executable_accounts = vec![(Pubkey::new_rand(), RefCell::new(Account::default()))];
|
||||
let mut program_accounts = vec![Rc::new(RefCell::new(Account::default()))];
|
||||
|
||||
assert!(MessageProcessor::verify_account_references(
|
||||
&mut executable_accounts,
|
||||
&mut program_accounts,
|
||||
)
|
||||
.is_ok());
|
||||
|
||||
let cloned = program_accounts[0].clone();
|
||||
let _borrowed = cloned.borrow();
|
||||
assert_eq!(
|
||||
MessageProcessor::verify_account_references(
|
||||
&mut executable_accounts,
|
||||
&mut program_accounts,
|
||||
),
|
||||
Err(InstructionError::AccountBorrowOutstanding)
|
||||
);
|
||||
|
||||
// TODO when the `&mut`s go away test outstanding executable_account refs
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_subset_unchecked_mut() {
|
||||
assert_eq!(
|
||||
get_subset_unchecked_mut(&mut [7, 8], &[0]).unwrap(),
|
||||
vec![&mut 7]
|
||||
);
|
||||
assert_eq!(
|
||||
get_subset_unchecked_mut(&mut [7, 8], &[0, 1]).unwrap(),
|
||||
vec![&mut 7, &mut 8]
|
||||
);
|
||||
}
|
||||
fn test_sum_account_lamports() {
|
||||
let owner_pubkey = Pubkey::new_rand();
|
||||
let account1 = Rc::new(RefCell::new(Account::new(1, 1, &owner_pubkey)));
|
||||
let account2 = Rc::new(RefCell::new(Account::new(2, 1, &owner_pubkey)));
|
||||
let account3 = Rc::new(RefCell::new(Account::new(3, 1, &owner_pubkey)));
|
||||
|
||||
#[test]
|
||||
fn test_get_subset_unchecked_mut_duplicate_index() {
|
||||
// This panics, because it assumes duplicate detection is done elsewhere.
|
||||
assert_eq!(0, MessageProcessor::sum_account_lamports(&mut vec![]));
|
||||
assert_eq!(
|
||||
get_subset_unchecked_mut(&mut [7, 8], &[0, 0]).unwrap_err(),
|
||||
InstructionError::DuplicateAccountIndex
|
||||
6,
|
||||
MessageProcessor::sum_account_lamports(&mut vec![
|
||||
account1.clone(),
|
||||
account2.clone(),
|
||||
account3.clone()
|
||||
])
|
||||
);
|
||||
assert_eq!(
|
||||
3,
|
||||
MessageProcessor::sum_account_lamports(&mut vec![
|
||||
account1.clone(),
|
||||
account2.clone(),
|
||||
account1.clone()
|
||||
])
|
||||
);
|
||||
assert_eq!(
|
||||
1,
|
||||
MessageProcessor::sum_account_lamports(&mut vec![
|
||||
account1.clone(),
|
||||
account1.clone(),
|
||||
account1.clone()
|
||||
])
|
||||
);
|
||||
assert_eq!(
|
||||
6,
|
||||
MessageProcessor::sum_account_lamports(&mut vec![
|
||||
account1.clone(),
|
||||
account2.clone(),
|
||||
account3.clone(),
|
||||
account1.clone(),
|
||||
account2.clone(),
|
||||
account3.clone(),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_get_subset_unchecked_mut_out_of_bounds() {
|
||||
// This panics, because it assumes bounds validation is done elsewhere.
|
||||
get_subset_unchecked_mut(&mut [7, 8], &[2]).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -772,13 +812,13 @@ mod tests {
|
||||
match instruction {
|
||||
MockSystemInstruction::Correct => Ok(()),
|
||||
MockSystemInstruction::AttemptCredit { lamports } => {
|
||||
keyed_accounts[0].account.lamports -= lamports;
|
||||
keyed_accounts[1].account.lamports += lamports;
|
||||
keyed_accounts[0].account.borrow_mut().lamports -= lamports;
|
||||
keyed_accounts[1].account.borrow_mut().lamports += lamports;
|
||||
Ok(())
|
||||
}
|
||||
// Change data in a read-only account
|
||||
MockSystemInstruction::AttemptDataChange { data } => {
|
||||
keyed_accounts[1].account.data = vec![data];
|
||||
keyed_accounts[1].account.borrow_mut().data = vec![data];
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@ -792,14 +832,14 @@ mod tests {
|
||||
message_processor
|
||||
.add_instruction_processor(mock_system_program_id, mock_system_process_instruction);
|
||||
|
||||
let mut accounts: Vec<Account> = Vec::new();
|
||||
let account = Account::new(100, 1, &mock_system_program_id);
|
||||
let mut accounts: Vec<Rc<RefCell<Account>>> = Vec::new();
|
||||
let account = Account::new_ref(100, 1, &mock_system_program_id);
|
||||
accounts.push(account);
|
||||
let account = Account::new(0, 1, &mock_system_program_id);
|
||||
let account = Account::new_ref(0, 1, &mock_system_program_id);
|
||||
accounts.push(account);
|
||||
|
||||
let mut loaders: Vec<Vec<(Pubkey, Account)>> = Vec::new();
|
||||
let account = create_loadable_account("mock_system_program");
|
||||
let mut loaders: Vec<Vec<(Pubkey, RefCell<Account>)>> = Vec::new();
|
||||
let account = RefCell::new(create_loadable_account("mock_system_program"));
|
||||
loaders.push(vec![(mock_system_program_id, account)]);
|
||||
|
||||
let from_pubkey = Pubkey::new_rand();
|
||||
@ -816,8 +856,8 @@ mod tests {
|
||||
|
||||
let result = message_processor.process_message(&message, &mut loaders, &mut accounts);
|
||||
assert_eq!(result, Ok(()));
|
||||
assert_eq!(accounts[0].lamports, 100);
|
||||
assert_eq!(accounts[1].lamports, 0);
|
||||
assert_eq!(accounts[0].borrow().lamports, 100);
|
||||
assert_eq!(accounts[1].borrow().lamports, 0);
|
||||
|
||||
let message = Message::new(vec![Instruction::new(
|
||||
mock_system_program_id,
|
||||
@ -849,4 +889,124 @@ mod tests {
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_message_duplicate_accounts() {
|
||||
#[derive(Serialize, Deserialize)]
|
||||
enum MockSystemInstruction {
|
||||
BorrowFail,
|
||||
MultiBorrowMut,
|
||||
DoWork { lamports: u64, data: u8 },
|
||||
}
|
||||
|
||||
fn mock_system_process_instruction(
|
||||
_program_id: &Pubkey,
|
||||
keyed_accounts: &mut [KeyedAccount],
|
||||
data: &[u8],
|
||||
) -> Result<(), InstructionError> {
|
||||
if let Ok(instruction) = bincode::deserialize(data) {
|
||||
match instruction {
|
||||
MockSystemInstruction::BorrowFail => {
|
||||
let from_account = keyed_accounts[0].try_account_ref_mut()?;
|
||||
let dup_account = keyed_accounts[2].try_account_ref_mut()?;
|
||||
if from_account.lamports != dup_account.lamports {
|
||||
return Err(InstructionError::InvalidArgument);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
MockSystemInstruction::MultiBorrowMut => {
|
||||
let from_lamports = {
|
||||
let from_account = keyed_accounts[0].try_account_ref_mut()?;
|
||||
from_account.lamports
|
||||
};
|
||||
let dup_lamports = {
|
||||
let dup_account = keyed_accounts[2].try_account_ref_mut()?;
|
||||
dup_account.lamports
|
||||
};
|
||||
if from_lamports != dup_lamports {
|
||||
return Err(InstructionError::InvalidArgument);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
MockSystemInstruction::DoWork { lamports, data } => {
|
||||
{
|
||||
let mut to_account = keyed_accounts[1].try_account_ref_mut()?;
|
||||
let mut dup_account = keyed_accounts[2].try_account_ref_mut()?;
|
||||
dup_account.lamports -= lamports;
|
||||
to_account.lamports += lamports;
|
||||
dup_account.data = vec![data];
|
||||
}
|
||||
keyed_accounts[0].try_account_ref_mut()?.lamports -= lamports;
|
||||
keyed_accounts[1].try_account_ref_mut()?.lamports += lamports;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Err(InstructionError::InvalidInstructionData)
|
||||
}
|
||||
}
|
||||
|
||||
let mock_program_id = Pubkey::new(&[2u8; 32]);
|
||||
let mut message_processor = MessageProcessor::default();
|
||||
message_processor
|
||||
.add_instruction_processor(mock_program_id, mock_system_process_instruction);
|
||||
|
||||
let mut accounts: Vec<Rc<RefCell<Account>>> = Vec::new();
|
||||
let account = Account::new_ref(100, 1, &mock_program_id);
|
||||
accounts.push(account);
|
||||
let account = Account::new_ref(0, 1, &mock_program_id);
|
||||
accounts.push(account);
|
||||
|
||||
let mut loaders: Vec<Vec<(Pubkey, RefCell<Account>)>> = Vec::new();
|
||||
let account = RefCell::new(create_loadable_account("mock_system_program"));
|
||||
loaders.push(vec![(mock_program_id, account)]);
|
||||
|
||||
let from_pubkey = Pubkey::new_rand();
|
||||
let to_pubkey = Pubkey::new_rand();
|
||||
let dup_pubkey = from_pubkey.clone();
|
||||
let account_metas = vec![
|
||||
AccountMeta::new(from_pubkey, true),
|
||||
AccountMeta::new(to_pubkey, false),
|
||||
AccountMeta::new(dup_pubkey, false),
|
||||
];
|
||||
|
||||
// Try to borrow mut the same account
|
||||
let message = Message::new(vec![Instruction::new(
|
||||
mock_program_id,
|
||||
&MockSystemInstruction::BorrowFail,
|
||||
account_metas.clone(),
|
||||
)]);
|
||||
let result = message_processor.process_message(&message, &mut loaders, &mut accounts);
|
||||
assert_eq!(
|
||||
result,
|
||||
Err(TransactionError::InstructionError(
|
||||
0,
|
||||
InstructionError::AccountBorrowFailed
|
||||
))
|
||||
);
|
||||
|
||||
// Try to borrow mut the same account in a safe way
|
||||
let message = Message::new(vec![Instruction::new(
|
||||
mock_program_id,
|
||||
&MockSystemInstruction::MultiBorrowMut,
|
||||
account_metas.clone(),
|
||||
)]);
|
||||
let result = message_processor.process_message(&message, &mut loaders, &mut accounts);
|
||||
assert_eq!(result, Ok(()));
|
||||
|
||||
// Do work on the same account but at different location in keyed_accounts[]
|
||||
let message = Message::new(vec![Instruction::new(
|
||||
mock_program_id,
|
||||
&MockSystemInstruction::DoWork {
|
||||
lamports: 10,
|
||||
data: 42,
|
||||
},
|
||||
account_metas,
|
||||
)]);
|
||||
let result = message_processor.process_message(&message, &mut loaders, &mut accounts);
|
||||
assert_eq!(result, Ok(()));
|
||||
assert_eq!(accounts[0].borrow().lamports, 80);
|
||||
assert_eq!(accounts[1].borrow().lamports, 20);
|
||||
assert_eq!(accounts[0].borrow().data, vec![42]);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user