Remove credit-only account handling (#6726)

* Renaming
- credit-only/credit-debit to read-only/read-write
- debitable to writable

* Remove credit handling, making credit-only accounts read-only

* Update programs to remove deprecated credit-only account designation

* Use readonly and writable instead of underscored types
This commit is contained in:
Tyera Eulberg
2019-11-05 09:38:35 -07:00
committed by GitHub
parent cea13e964c
commit c6931dcb07
20 changed files with 344 additions and 621 deletions

View File

@ -19,14 +19,12 @@ use solana_sdk::transaction::{Transaction, TransactionError};
use std::collections::{HashMap, HashSet};
use std::io::{BufReader, Error as IOError, Read};
use std::path::Path;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex, RwLock};
use crate::transaction_utils::OrderedIterator;
#[derive(Default, Debug)]
struct CreditOnlyLock {
credits: AtomicU64,
struct ReadonlyLock {
lock_count: Mutex<u64>,
}
@ -39,27 +37,19 @@ pub struct Accounts {
/// Single global AccountsDB
pub accounts_db: Arc<AccountsDB>,
/// set of credit-debit accounts which are currently in the pipeline
/// set of writable accounts which are currently in the pipeline
account_locks: Mutex<HashSet<Pubkey>>,
/// Set of credit-only accounts which are currently in the pipeline, caching account balance
/// and number of locks. On commit_credits(), we do a take() on the option so that the hashmap
/// is no longer available to be written to.
credit_only_locks: Arc<RwLock<Option<HashMap<Pubkey, CreditOnlyLock>>>>,
/// Set of read-only accounts which are currently in the pipeline, caching number of locks.
readonly_locks: Arc<RwLock<Option<HashMap<Pubkey, ReadonlyLock>>>>,
}
// for the load instructions
pub type TransactionAccounts = Vec<Account>;
pub type TransactionCredits = Vec<u64>;
pub type TransactionRents = Vec<u64>;
pub type TransactionLoaders = Vec<Vec<(Pubkey, Account)>>;
pub type TransactionLoadResult = (
TransactionAccounts,
TransactionLoaders,
TransactionCredits,
TransactionRents,
);
pub type TransactionLoadResult = (TransactionAccounts, TransactionLoaders, TransactionRents);
impl Accounts {
pub fn new(paths: Option<String>) -> Self {
@ -69,7 +59,7 @@ impl Accounts {
slot: 0,
accounts_db,
account_locks: Mutex::new(HashSet::new()),
credit_only_locks: Arc::new(RwLock::new(Some(HashMap::new()))),
readonly_locks: Arc::new(RwLock::new(Some(HashMap::new()))),
}
}
pub fn new_from_parent(parent: &Accounts, slot: Slot, parent_slot: Slot) -> Self {
@ -79,7 +69,7 @@ impl Accounts {
slot,
accounts_db,
account_locks: Mutex::new(HashSet::new()),
credit_only_locks: Arc::new(RwLock::new(Some(HashMap::new()))),
readonly_locks: Arc::new(RwLock::new(Some(HashMap::new()))),
}
}
@ -259,8 +249,7 @@ impl Accounts {
tx,
error_counters,
)?;
let credits = vec![0; accounts.len()];
Ok((accounts, loaders, credits, rents))
Ok((accounts, loaders, rents))
}
(_, Err(e)) => Err(e),
})
@ -273,13 +262,11 @@ impl Accounts {
ancestors: &HashMap<Slot, usize>,
pubkey: &Pubkey,
) -> Option<(Account, Slot)> {
let (mut account, slot) = self
let (account, slot) = self
.accounts_db
.load_slow(ancestors, pubkey)
.unwrap_or((Account::default(), self.slot));
account.lamports += self.credit_only_pending_credits(pubkey);
if account.lamports > 0 {
Some((account, slot))
} else {
@ -358,15 +345,8 @@ impl Accounts {
self.accounts_db.store(slot, &[(pubkey, account)]);
}
fn take_credit_only(&self) -> Result<HashMap<Pubkey, CreditOnlyLock>> {
let mut w_credit_only_locks = self.credit_only_locks.write().unwrap();
w_credit_only_locks
.take()
.ok_or(TransactionError::AccountInUse)
}
fn is_locked_credit_only(&self, key: &Pubkey) -> bool {
self.credit_only_locks
fn is_locked_readonly(&self, key: &Pubkey) -> bool {
self.readonly_locks
.read()
.unwrap()
.as_ref()
@ -377,32 +357,16 @@ impl Accounts {
})
}
fn credit_only_pending_credits(&self, key: &Pubkey) -> u64 {
self.credit_only_locks
.read()
.unwrap()
.as_ref()
.map_or(0, |locks| {
locks
.get(key)
.map_or(0, |lock| lock.credits.load(Ordering::Relaxed))
})
fn unlock_readonly(&self, key: &Pubkey) {
self.readonly_locks.read().unwrap().as_ref().map(|locks| {
locks
.get(key)
.map(|lock| *lock.lock_count.lock().unwrap() -= 1)
});
}
fn unlock_credit_only(&self, key: &Pubkey) {
self.credit_only_locks
.read()
.unwrap()
.as_ref()
.map(|locks| {
locks
.get(key)
.map(|lock| *lock.lock_count.lock().unwrap() -= 1)
});
}
fn lock_credit_only(&self, key: &Pubkey) -> bool {
self.credit_only_locks
fn lock_readonly(&self, key: &Pubkey) -> bool {
self.readonly_locks
.read()
.unwrap()
.as_ref()
@ -414,8 +378,8 @@ impl Accounts {
})
}
fn insert_credit_only(&self, key: &Pubkey, lock: CreditOnlyLock) -> bool {
self.credit_only_locks
fn insert_readonly(&self, key: &Pubkey, lock: ReadonlyLock) -> bool {
self.readonly_locks
.write()
.unwrap()
.as_mut()
@ -432,16 +396,16 @@ impl Accounts {
message: &Message,
error_counters: &mut ErrorCounters,
) -> Result<()> {
let (credit_debit_keys, credit_only_keys) = message.get_account_keys_by_lock_type();
let (writable_keys, readonly_keys) = message.get_account_keys_by_lock_type();
for k in credit_debit_keys.iter() {
if locks.contains(k) || self.is_locked_credit_only(k) {
for k in writable_keys.iter() {
if locks.contains(k) || self.is_locked_readonly(k) {
error_counters.account_in_use += 1;
debug!("CD Account in use: {:?}", k);
return Err(TransactionError::AccountInUse);
}
}
for k in credit_only_keys.iter() {
for k in readonly_keys.iter() {
if locks.contains(k) {
error_counters.account_in_use += 1;
debug!("CO Account in use: {:?}", k);
@ -449,20 +413,19 @@ impl Accounts {
}
}
for k in credit_debit_keys {
for k in writable_keys {
locks.insert(*k);
}
let credit_only_writes: Vec<&&Pubkey> = credit_only_keys
let readonly_writes: Vec<&&Pubkey> = readonly_keys
.iter()
.filter(|k| !self.lock_credit_only(k))
.filter(|k| !self.lock_readonly(k))
.collect();
for k in credit_only_writes.iter() {
self.insert_credit_only(
for k in readonly_writes.iter() {
self.insert_readonly(
*k,
CreditOnlyLock {
credits: AtomicU64::new(0),
ReadonlyLock {
lock_count: Mutex::new(1),
},
);
@ -472,15 +435,15 @@ impl Accounts {
}
fn unlock_account(&self, tx: &Transaction, result: &Result<()>, locks: &mut HashSet<Pubkey>) {
let (credit_debit_keys, credit_only_keys) = &tx.message().get_account_keys_by_lock_type();
let (writable_keys, readonly_keys) = &tx.message().get_account_keys_by_lock_type();
match result {
Err(TransactionError::AccountInUse) => (),
_ => {
for k in credit_debit_keys {
for k in writable_keys {
locks.remove(k);
}
for k in credit_only_keys {
self.unlock_credit_only(k);
for k in readonly_keys {
self.unlock_readonly(k);
}
}
}
@ -569,63 +532,6 @@ impl Accounts {
self.accounts_db.add_root(slot)
}
/// Commit remaining credit-only changes, regardless of reference count
///
/// We do a take() on `self.credit_only_locks` so that the hashmap is no longer
/// available to be written to. This prevents any transactions from reinserting into the hashmap.
/// Then there are then only 2 cases for interleaving with commit_credits and lock_accounts.
/// Either:
// 1) Any transactions that tries to lock after commit_credits will find the HashMap is None
// so will fail the lock
// 2) Any transaction that grabs a lock and then commit_credits clears the HashMap will find
// the HashMap is None on unlock_accounts, and will perform a no-op.
pub fn commit_credits(&self, ancestors: &HashMap<Slot, usize>, slot: Slot) {
// Clear the credit only hashmap so that no further transactions can modify it
let credit_only_locks = self
.take_credit_only()
.expect("Credit only locks didn't exist in commit_credits");
self.store_credit_only_credits(credit_only_locks, ancestors, slot);
}
/// Used only for tests to store credit-only accounts after every transaction
pub fn commit_credits_unsafe(&self, ancestors: &HashMap<Slot, usize>, slot: Slot) {
// Clear the credit only hashmap so that no further transactions can modify it
let mut credit_only_locks = self.credit_only_locks.write().unwrap();
let credit_only_locks = credit_only_locks
.as_mut()
.expect("Credit only locks didn't exist in commit_credits");
self.store_credit_only_credits(credit_only_locks.drain(), ancestors, slot);
}
fn store_credit_only_credits<I>(
&self,
credit_only_locks: I,
ancestors: &HashMap<Slot, usize>,
slot: Slot,
) where
I: IntoIterator<Item = (Pubkey, CreditOnlyLock)>,
{
for (pubkey, lock) in credit_only_locks {
let lock_count = *lock.lock_count.lock().unwrap();
if lock_count != 0 {
warn!(
"dropping credit-only lock on {}, still has {} locks",
pubkey, lock_count
);
}
let credit = lock.credits.load(Ordering::Relaxed);
if credit > 0 {
let (mut account, _slot) = self
.accounts_db
.load_slow(ancestors, &pubkey)
.unwrap_or_default();
account.lamports += credit;
self.store_slow(slot, &pubkey, &account);
}
}
}
fn collect_accounts_to_store<'a>(
&self,
txs: &'a [Transaction],
@ -645,28 +551,10 @@ impl Accounts {
let message = &tx.message();
let acc = raccs.as_mut().unwrap();
for (((i, key), account), credit) in message
.account_keys
.iter()
.enumerate()
.zip(acc.0.iter())
.zip(acc.2.iter())
{
if message.is_debitable(i) {
for ((i, key), account) in message.account_keys.iter().enumerate().zip(acc.0.iter()) {
if message.is_writable(i) {
accounts.push((key, account));
}
if *credit > 0 {
// Increment credit-only account balance Atomic
self.credit_only_locks
.read()
.unwrap()
.as_ref()
.expect("Collect accounts should only be called before a commit, and credit only account locks should exist before a commit")
.get(key)
.unwrap()
.credits
.fetch_add(*credit, Ordering::Relaxed);
}
}
}
accounts
@ -704,7 +592,7 @@ mod tests {
use solana_sdk::sysvar;
use solana_sdk::transaction::Transaction;
use std::io::Cursor;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::{thread, time};
use tempfile::TempDir;
@ -917,18 +805,11 @@ mod tests {
assert_eq!(error_counters.account_not_found, 0);
assert_eq!(loaded_accounts.len(), 1);
match &loaded_accounts[0] {
Ok((
transaction_accounts,
transaction_loaders,
transaction_credits,
_transaction_rents,
)) => {
Ok((transaction_accounts, transaction_loaders, _transaction_rents)) => {
assert_eq!(transaction_accounts.len(), 2);
assert_eq!(transaction_accounts[0], accounts[0].1);
assert_eq!(transaction_loaders.len(), 1);
assert_eq!(transaction_loaders[0].len(), 0);
assert_eq!(transaction_credits.len(), 2);
assert_eq!(transaction_credits, &vec![0, 0]);
}
Err(e) => Err(e).unwrap(),
}
@ -1105,19 +986,12 @@ mod tests {
assert_eq!(error_counters.account_not_found, 0);
assert_eq!(loaded_accounts.len(), 1);
match &loaded_accounts[0] {
Ok((
transaction_accounts,
transaction_loaders,
transaction_credits,
_transaction_rents,
)) => {
Ok((transaction_accounts, transaction_loaders, _transaction_rents)) => {
assert_eq!(transaction_accounts.len(), 1);
assert_eq!(transaction_accounts[0], accounts[0].1);
assert_eq!(transaction_loaders.len(), 2);
assert_eq!(transaction_loaders[0].len(), 1);
assert_eq!(transaction_loaders[1].len(), 2);
assert_eq!(transaction_credits.len(), 1);
assert_eq!(transaction_credits, &vec![0]);
for loaders in transaction_loaders.iter() {
for (i, accounts_subset) in loaders.iter().enumerate() {
// +1 to skip first not loader account
@ -1294,7 +1168,7 @@ mod tests {
assert!(results0[0].is_ok());
assert_eq!(
*accounts
.credit_only_locks
.readonly_locks
.read()
.unwrap()
.as_ref()
@ -1330,11 +1204,11 @@ mod tests {
let txs = vec![tx0, tx1];
let results1 = accounts.lock_accounts(&txs, None);
assert!(results1[0].is_ok()); // Credit-only account (keypair1) can be referenced multiple times
assert!(results1[1].is_err()); // Credit-only account (keypair1) cannot also be locked as credit-debit
assert!(results1[0].is_ok()); // Read-only account (keypair1) can be referenced multiple times
assert!(results1[1].is_err()); // Read-only account (keypair1) cannot also be locked as writable
assert_eq!(
*accounts
.credit_only_locks
.readonly_locks
.read()
.unwrap()
.as_ref()
@ -1362,12 +1236,12 @@ mod tests {
let tx = Transaction::new(&[&keypair1], message, Hash::default());
let results2 = accounts.lock_accounts(&[tx], None);
assert!(results2[0].is_ok()); // Now keypair1 account can be locked as credit-debit
assert!(results2[0].is_ok()); // Now keypair1 account can be locked as writable
// Check that credit-only credits are still cached in accounts struct
let credit_only_locks = accounts.credit_only_locks.read().unwrap();
let credit_only_locks = credit_only_locks.as_ref().unwrap();
let keypair1_lock = credit_only_locks.get(&keypair1.pubkey());
// Check that read-only locks are still cached in accounts struct
let readonly_locks = accounts.readonly_locks.read().unwrap();
let readonly_locks = readonly_locks.as_ref().unwrap();
let keypair1_lock = readonly_locks.get(&keypair1.pubkey());
assert!(keypair1_lock.is_some());
assert_eq!(*keypair1_lock.unwrap().lock_count.lock().unwrap(), 0);
}
@ -1393,7 +1267,7 @@ mod tests {
let accounts_arc = Arc::new(accounts);
let instructions = vec![CompiledInstruction::new(2, &(), vec![0, 1])];
let credit_only_message = Message::new_with_compiled_instructions(
let readonly_message = Message::new_with_compiled_instructions(
1,
0,
2,
@ -1401,10 +1275,10 @@ mod tests {
Hash::default(),
instructions,
);
let credit_only_tx = Transaction::new(&[&keypair0], credit_only_message, Hash::default());
let readonly_tx = Transaction::new(&[&keypair0], readonly_message, Hash::default());
let instructions = vec![CompiledInstruction::new(2, &(), vec![0, 1])];
let credit_debit_message = Message::new_with_compiled_instructions(
let writable_message = Message::new_with_compiled_instructions(
1,
0,
2,
@ -1412,7 +1286,7 @@ mod tests {
Hash::default(),
instructions,
);
let credit_debit_tx = Transaction::new(&[&keypair1], credit_debit_message, Hash::default());
let writable_tx = Transaction::new(&[&keypair1], writable_message, Hash::default());
let counter_clone = counter.clone();
let accounts_clone = accounts_arc.clone();
@ -1421,7 +1295,7 @@ mod tests {
let counter_clone = counter_clone.clone();
let exit_clone = exit_clone.clone();
loop {
let txs = vec![credit_debit_tx.clone()];
let txs = vec![writable_tx.clone()];
let results = accounts_clone.clone().lock_accounts(&txs, None);
for result in results.iter() {
if result.is_ok() {
@ -1436,7 +1310,7 @@ mod tests {
});
let counter_clone = counter.clone();
for _ in 0..5 {
let txs = vec![credit_only_tx.clone()];
let txs = vec![readonly_tx.clone()];
let results = accounts_arc.clone().lock_accounts(&txs, None);
if results[0].is_ok() {
let counter_value = counter_clone.clone().load(Ordering::SeqCst);
@ -1449,110 +1323,6 @@ mod tests {
exit.store(true, Ordering::Relaxed);
}
#[test]
fn test_commit_credits() {
let pubkey0 = Pubkey::new_rand();
let pubkey1 = Pubkey::new_rand();
let pubkey2 = Pubkey::new_rand();
let account0 = Account::new(1, 0, &Pubkey::default());
let account1 = Account::new(2, 0, &Pubkey::default());
let accounts = Accounts::new(None);
accounts.store_slow(0, &pubkey0, &account0);
accounts.store_slow(0, &pubkey1, &account1);
{
let mut credit_only_locks = accounts.credit_only_locks.write().unwrap();
let credit_only_locks = credit_only_locks.as_mut().unwrap();
credit_only_locks.insert(
pubkey0,
CreditOnlyLock {
credits: AtomicU64::new(0),
lock_count: Mutex::new(1),
},
);
credit_only_locks.insert(
pubkey1,
CreditOnlyLock {
credits: AtomicU64::new(5),
lock_count: Mutex::new(1),
},
);
credit_only_locks.insert(
pubkey2,
CreditOnlyLock {
credits: AtomicU64::new(10),
lock_count: Mutex::new(1),
},
);
}
let ancestors = vec![(0, 0)].into_iter().collect();
accounts.commit_credits(&ancestors, 0);
// No change when CreditOnlyLock credits are 0
assert_eq!(
accounts.load_slow(&ancestors, &pubkey0).unwrap().0.lamports,
1
);
// New balance should equal previous balance plus CreditOnlyLock credits
assert_eq!(
accounts.load_slow(&ancestors, &pubkey1).unwrap().0.lamports,
7
);
// New account should be created
assert_eq!(
accounts.load_slow(&ancestors, &pubkey2).unwrap().0.lamports,
10
);
// Account locks should be cleared
assert!(accounts.credit_only_locks.read().unwrap().is_none());
}
#[test]
fn test_credit_only_pending_credits() {
let pubkey = Pubkey::new_rand();
let account = Account::new(1, 0, &Pubkey::default());
let accounts = Accounts::new(None);
accounts.store_slow(0, &pubkey, &account);
accounts.insert_credit_only(
&pubkey,
CreditOnlyLock {
credits: AtomicU64::new(10),
lock_count: Mutex::new(1),
},
);
let ancestors = vec![(0, 0)].into_iter().collect();
assert_eq!(
accounts.load_slow(&ancestors, &pubkey).unwrap().0.lamports,
11
);
assert_eq!(
accounts
.accounts_db
.load_slow(&ancestors, &pubkey)
.unwrap()
.0
.lamports,
1
);
accounts.commit_credits(&ancestors, 0);
assert_eq!(
accounts
.accounts_db
.load_slow(&ancestors, &pubkey)
.unwrap()
.0
.lamports,
11
);
}
#[test]
fn test_collect_accounts_to_store() {
let keypair0 = Keypair::new();
@ -1590,23 +1360,19 @@ mod tests {
let transaction_accounts0 = vec![account0, account2.clone()];
let transaction_loaders0 = vec![];
let transaction_credits0 = vec![0, 2];
let transaction_rents0 = vec![0, 0];
let loaded0 = Ok((
transaction_accounts0,
transaction_loaders0,
transaction_credits0,
transaction_rents0,
));
let transaction_accounts1 = vec![account1, account2.clone()];
let transaction_loaders1 = vec![];
let transaction_credits1 = vec![0, 3];
let transaction_rents1 = vec![0, 0];
let loaded1 = Ok((
transaction_accounts1,
transaction_loaders1,
transaction_credits1,
transaction_rents1,
));
@ -1614,12 +1380,11 @@ mod tests {
let accounts = Accounts::new(None);
{
let mut credit_only_locks = accounts.credit_only_locks.write().unwrap();
let credit_only_locks = credit_only_locks.as_mut().unwrap();
credit_only_locks.insert(
let mut readonly_locks = accounts.readonly_locks.write().unwrap();
let readonly_locks = readonly_locks.as_mut().unwrap();
readonly_locks.insert(
pubkey,
CreditOnlyLock {
credits: AtomicU64::new(0),
ReadonlyLock {
lock_count: Mutex::new(1),
},
);
@ -1636,16 +1401,17 @@ mod tests {
.find(|(pubkey, _account)| *pubkey == &keypair1.pubkey())
.is_some());
// Ensure credit_only_lock reflects credits from both accounts: 2 + 3 = 5
let credit_only_locks = accounts.credit_only_locks.read().unwrap();
let credit_only_locks = credit_only_locks.as_ref().unwrap();
// Ensure readonly_lock reflects lock
let readonly_locks = accounts.readonly_locks.read().unwrap();
let readonly_locks = readonly_locks.as_ref().unwrap();
assert_eq!(
credit_only_locks
*readonly_locks
.get(&pubkey)
.unwrap()
.credits
.load(Ordering::Relaxed),
5
.lock_count
.lock()
.unwrap(),
1
);
}
}

View File

@ -594,7 +594,6 @@ impl Bank {
if *hash == Hash::default() {
// finish up any deferred changes to account state
self.commit_credits();
self.collect_fees();
// freeze is a one-way trip, idempotent
@ -807,15 +806,10 @@ impl Bank {
}
/// Process a Transaction. This is used for unit tests and simply calls the vector
/// Bank::process_transactions method, and commits credit-only credits.
/// Bank::process_transactions method
pub fn process_transaction(&self, tx: &Transaction) -> Result<()> {
let txs = vec![tx.clone()];
self.process_transactions(&txs)[0].clone()?;
// Call this instead of commit_credits(), so that the credit-only locks hashmap on this
// bank isn't deleted
self.rc
.accounts
.commit_credits_unsafe(&self.ancestors, self.slot());
tx.signatures
.get(0)
.map_or(Ok(()), |sig| self.get_signature_status(sig).unwrap())
@ -1061,10 +1055,10 @@ impl Bank {
.zip(OrderedIterator::new(txs, batch.iteration_order()))
.map(|(accs, tx)| match accs {
Err(e) => Err(e.clone()),
Ok((accounts, loaders, credits, _rents)) => {
Ok((accounts, loaders, _rents)) => {
signature_count += u64::from(tx.message().header.num_required_signatures);
self.message_processor
.process_message(tx.message(), loaders, accounts, credits)
.process_message(tx.message(), loaders, accounts)
}
})
.collect();
@ -1595,12 +1589,6 @@ impl Bank {
);
}
fn commit_credits(&self) {
self.rc
.accounts
.commit_credits(&self.ancestors, self.slot());
}
pub fn purge_zero_lamport_accounts(&self) {
self.rc
.accounts
@ -1646,19 +1634,18 @@ mod tests {
epoch_schedule::MINIMUM_SLOTS_PER_EPOCH,
genesis_block::create_genesis_block,
hash,
instruction::{AccountMeta, Instruction, InstructionError},
instruction::InstructionError,
message::{Message, MessageHeader},
poh_config::PohConfig,
rent::Rent,
signature::{Keypair, KeypairUtil},
system_instruction::{self, SystemInstruction},
system_program, system_transaction,
system_instruction,
sysvar::{fees::Fees, rewards::Rewards},
};
use solana_stake_api::stake_state::Stake;
use solana_vote_api::{
vote_instruction,
vote_state::{VoteInit, VoteState, MAX_LOCKOUT_HISTORY},
vote_state::{self, Vote, VoteInit, VoteState, MAX_LOCKOUT_HISTORY},
};
use std::{io::Cursor, time::Duration};
use tempfile::TempDir;
@ -1881,7 +1868,6 @@ mod tests {
let t1 = system_transaction::transfer(&mint_keypair, &key1, 1, genesis_block.hash());
let t2 = system_transaction::transfer(&mint_keypair, &key2, 1, genesis_block.hash());
let res = bank.process_transactions(&vec![t1.clone(), t2.clone()]);
bank.commit_credits();
assert_eq!(res.len(), 2);
assert_eq!(res[0], Ok(()));
@ -2234,60 +2220,78 @@ mod tests {
assert_eq!(bank.transaction_count(), 1);
}
fn transfer_credit_only(
from: &Keypair,
to: &Pubkey,
lamports: u64,
recent_blockhash: Hash,
) -> Transaction {
Transaction::new_signed_instructions(
&[from],
vec![Instruction::new(
system_program::id(),
&SystemInstruction::Transfer { lamports },
vec![
AccountMeta::new(from.pubkey(), true),
AccountMeta::new_credit_only(*to, false),
],
)],
recent_blockhash,
)
}
#[test]
fn test_credit_only_accounts() {
let (genesis_block, mint_keypair) = create_genesis_block(100);
fn test_readonly_accounts() {
let GenesisBlockInfo {
genesis_block,
mint_keypair,
..
} = create_genesis_block_with_leader(500, &Pubkey::new_rand(), 0);
let bank = Bank::new(&genesis_block);
let vote_pubkey0 = Pubkey::new_rand();
let vote_pubkey1 = Pubkey::new_rand();
let vote_pubkey2 = Pubkey::new_rand();
let authorized_voter = Keypair::new();
let payer0 = Keypair::new();
let payer1 = Keypair::new();
let recipient = Keypair::new();
// Fund additional payers
bank.transfer(3, &mint_keypair, &payer0.pubkey()).unwrap();
bank.transfer(3, &mint_keypair, &payer1.pubkey()).unwrap();
let tx0 = transfer_credit_only(&mint_keypair, &recipient.pubkey(), 1, genesis_block.hash());
let tx1 = transfer_credit_only(&payer0, &recipient.pubkey(), 1, genesis_block.hash());
let tx2 = transfer_credit_only(&payer1, &recipient.pubkey(), 1, genesis_block.hash());
let txs = vec![tx0, tx1, tx2];
let results = bank.process_transactions(&txs);
// If multiple transactions attempt to deposit into the same account, they should succeed,
// since System Transfer `To` accounts are given credit-only handling
assert_eq!(results[0], Ok(()));
assert_eq!(results[1], Ok(()));
assert_eq!(results[2], Ok(()));
assert_eq!(bank.get_balance(&recipient.pubkey()), 3);
// Create vote accounts
let vote_account0 =
vote_state::create_account(&vote_pubkey0, &authorized_voter.pubkey(), 0, 100);
let vote_account1 =
vote_state::create_account(&vote_pubkey1, &authorized_voter.pubkey(), 0, 100);
let vote_account2 =
vote_state::create_account(&vote_pubkey2, &authorized_voter.pubkey(), 0, 100);
bank.store_account(&vote_pubkey0, &vote_account0);
bank.store_account(&vote_pubkey1, &vote_account1);
bank.store_account(&vote_pubkey2, &vote_account2);
let tx0 = system_transaction::transfer(
&mint_keypair,
&recipient.pubkey(),
2,
genesis_block.hash(),
// Fund payers
bank.transfer(10, &mint_keypair, &payer0.pubkey()).unwrap();
bank.transfer(10, &mint_keypair, &payer1.pubkey()).unwrap();
bank.transfer(1, &mint_keypair, &authorized_voter.pubkey())
.unwrap();
let vote = Vote::new(vec![1], Hash::default());
let ix0 = vote_instruction::vote(&vote_pubkey0, &authorized_voter.pubkey(), vote.clone());
let tx0 = Transaction::new_signed_with_payer(
vec![ix0],
Some(&payer0.pubkey()),
&[&payer0, &authorized_voter],
bank.last_blockhash(),
);
let ix1 = vote_instruction::vote(&vote_pubkey1, &authorized_voter.pubkey(), vote.clone());
let tx1 = Transaction::new_signed_with_payer(
vec![ix1],
Some(&payer1.pubkey()),
&[&payer1, &authorized_voter],
bank.last_blockhash(),
);
let tx1 =
system_transaction::transfer(&recipient, &payer0.pubkey(), 1, genesis_block.hash());
let txs = vec![tx0, tx1];
let results = bank.process_transactions(&txs);
// However, an account may not be locked as credit-only and credit-debit at the same time.
// If multiple transactions attempt to read the same account, they should succeed.
// Vote authorized_voter and sysvar accounts are given read-only handling
assert_eq!(results[0], Ok(()));
assert_eq!(results[1], Ok(()));
let ix0 = vote_instruction::vote(&vote_pubkey2, &authorized_voter.pubkey(), vote.clone());
let tx0 = Transaction::new_signed_with_payer(
vec![ix0],
Some(&payer0.pubkey()),
&[&payer0, &authorized_voter],
bank.last_blockhash(),
);
let tx1 = system_transaction::transfer(
&authorized_voter,
&Pubkey::new_rand(),
1,
bank.last_blockhash(),
);
let txs = vec![tx0, tx1];
let results = bank.process_transactions(&txs);
// However, an account may not be locked as read-only and writable at the same time.
assert_eq!(results[0], Ok(()));
assert_eq!(results[1], Err(TransactionError::AccountInUse));
}
@ -2326,7 +2330,7 @@ mod tests {
}
#[test]
fn test_credit_only_relaxed_locks() {
fn test_readonly_relaxed_locks() {
let (genesis_block, _) = create_genesis_block(3);
let bank = Bank::new(&genesis_block);
let key0 = Keypair::new();
@ -2337,8 +2341,8 @@ mod tests {
let message = Message {
header: MessageHeader {
num_required_signatures: 1,
num_credit_only_signed_accounts: 0,
num_credit_only_unsigned_accounts: 1,
num_readonly_signed_accounts: 0,
num_readonly_unsigned_accounts: 1,
},
account_keys: vec![key0.pubkey(), key3],
recent_blockhash: Hash::default(),
@ -2350,13 +2354,13 @@ mod tests {
let batch0 = bank.prepare_batch(&txs, None);
assert!(batch0.lock_results()[0].is_ok());
// Try locking accounts, locking a previously credit-only account as credit-debit
// Try locking accounts, locking a previously read-only account as writable
// should fail
let message = Message {
header: MessageHeader {
num_required_signatures: 1,
num_credit_only_signed_accounts: 0,
num_credit_only_unsigned_accounts: 0,
num_readonly_signed_accounts: 0,
num_readonly_unsigned_accounts: 0,
},
account_keys: vec![key1.pubkey(), key3],
recent_blockhash: Hash::default(),
@ -2368,12 +2372,12 @@ mod tests {
let batch1 = bank.prepare_batch(&txs, None);
assert!(batch1.lock_results()[0].is_err());
// Try locking a previously credit-only account a 2nd time; should succeed
// Try locking a previously read-only account a 2nd time; should succeed
let message = Message {
header: MessageHeader {
num_required_signatures: 1,
num_credit_only_signed_accounts: 0,
num_credit_only_unsigned_accounts: 1,
num_readonly_signed_accounts: 0,
num_readonly_unsigned_accounts: 1,
},
account_keys: vec![key2.pubkey(), key3],
recent_blockhash: Hash::default(),

View File

@ -1,9 +1,7 @@
use crate::native_loader;
use crate::system_instruction_processor;
use serde::{Deserialize, Serialize};
use solana_sdk::account::{
create_keyed_credit_only_accounts, Account, KeyedAccount, LamportCredit,
};
use solana_sdk::account::{create_keyed_readonly_accounts, Account, KeyedAccount};
use solana_sdk::instruction::{CompiledInstruction, InstructionError};
use solana_sdk::instruction_processor_utils;
use solana_sdk::loader_instruction::LoaderInstruction;
@ -59,7 +57,7 @@ fn get_subset_unchecked_mut<'a, T>(
}
pub fn verify_instruction(
is_debitable: bool,
is_writable: bool,
program_id: &Pubkey,
pre: &Account,
post: &Account,
@ -67,10 +65,10 @@ pub fn verify_instruction(
// Verify the transaction
// Only the owner of the account may change owner and
// only if the account is credit-debit and
// only if the account is writable and
// only if the data is zero-initialized or empty
if pre.owner != post.owner
&& (!is_debitable // line coverage used to get branch coverage
&& (!is_writable // line coverage used to get branch coverage
|| *program_id != pre.owner // line coverage used to get branch coverage
|| !is_zeroed(&post.data))
{
@ -84,11 +82,11 @@ pub fn verify_instruction(
return Err(InstructionError::ExternalAccountLamportSpend);
}
// The balance of credit-only accounts may only increase.
if !is_debitable // line coverage used to get branch coverage
&& pre.lamports > post.lamports
// The balance of read-only accounts may not change.
if !is_writable // line coverage used to get branch coverage
&& pre.lamports != post.lamports
{
return Err(InstructionError::CreditOnlyLamportSpend);
return Err(InstructionError::ReadonlyLamportChange);
}
// Only the system program can change the size of the data
@ -126,16 +124,16 @@ pub fn verify_instruction(
return Err(InstructionError::ExternalAccountDataModified);
}
// Credit-only account data may not change.
if !is_debitable // line coverage used to get branch coverage
// Read-only account data may not change.
if !is_writable // line coverage used to get branch coverage
&& data_changed()
{
return Err(InstructionError::CreditOnlyDataModified);
return Err(InstructionError::ReadonlyDataModified);
}
// executable is one-way (false->true) and only the account owner may set it.
if pre.executable != post.executable
&& (!is_debitable // line coverage used to get branch coverage
&& (!is_writable // line coverage used to get branch coverage
|| pre.executable // line coverage used to get branch coverage
|| *program_id != pre.owner)
{
@ -227,26 +225,26 @@ impl MessageProcessor {
&mut loader_ix_data,
);
let mut keyed_accounts = create_keyed_credit_only_accounts(executable_accounts);
let mut keyed_accounts = create_keyed_readonly_accounts(executable_accounts);
let mut keyed_accounts2: Vec<_> = instruction
.accounts
.iter()
.map(|&index| {
let index = index as usize;
let key = &message.account_keys[index];
let is_debitable = message.is_debitable(index);
let is_writable = message.is_writable(index);
(
key,
index < message.header.num_required_signatures as usize,
is_debitable,
is_writable,
)
})
.zip(program_accounts.iter_mut())
.map(|((key, is_signer, is_debitable), account)| {
if is_debitable {
.map(|((key, is_signer, is_writable), account)| {
if is_writable {
KeyedAccount::new(key, is_signer, account)
} else {
KeyedAccount::new_credit_only(key, is_signer, account)
KeyedAccount::new_readonly(key, is_signer, account)
}
})
.collect();
@ -282,7 +280,6 @@ impl MessageProcessor {
instruction: &CompiledInstruction,
executable_accounts: &mut [(Pubkey, Account)],
program_accounts: &mut [&mut Account],
credits: &mut [&mut LamportCredit],
) -> Result<(), InstructionError> {
let program_id = instruction.program_id(&message.account_keys);
assert_eq!(instruction.accounts.len(), program_accounts.len());
@ -300,21 +297,17 @@ impl MessageProcessor {
self.process_instruction(message, instruction, executable_accounts, program_accounts)?;
// Verify the instruction
for (pre_account, (i, post_account, is_debitable)) in
for (pre_account, (post_account, is_writable)) in
pre_accounts
.iter()
.zip(program_accounts.iter().enumerate().map(|(i, account)| {
(
i,
account,
message.is_debitable(instruction.accounts[i] as usize),
message.is_writable(instruction.accounts[i] as usize),
)
}))
{
verify_instruction(is_debitable, &program_id, pre_account, post_account)?;
if !is_debitable {
*credits[i] += post_account.lamports - pre_account.lamports;
}
verify_instruction(is_writable, &program_id, pre_account, post_account)?;
}
// The total sum of all the lamports in all the accounts cannot change.
let post_total: u128 = program_accounts
@ -336,7 +329,6 @@ impl MessageProcessor {
message: &Message,
loaders: &mut [Vec<(Pubkey, Account)>],
accounts: &mut [Account],
credits: &mut [LamportCredit],
) -> Result<(), TransactionError> {
for (instruction_index, instruction) in message.instructions.iter().enumerate() {
let executable_index = message
@ -348,14 +340,11 @@ impl MessageProcessor {
// TODO: `get_subset_unchecked_mut` 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 instruction_credits = get_subset_unchecked_mut(credits, &instruction.accounts)
.map_err(|err| TransactionError::InstructionError(instruction_index as u8, err))?;
self.execute_instruction(
message,
instruction,
executable_accounts,
&mut program_accounts,
&mut instruction_credits,
)
.map_err(|err| TransactionError::InstructionError(instruction_index as u8, err))?;
}
@ -440,10 +429,10 @@ mod tests {
ix: &Pubkey,
pre: &Pubkey,
post: &Pubkey,
is_debitable: bool,
is_writable: bool,
) -> Result<(), InstructionError> {
verify_instruction(
is_debitable,
is_writable,
&ix,
&Account::new(0, 0, pre),
&Account::new(0, 0, post),
@ -465,9 +454,14 @@ mod tests {
"system program should be able to change the account owner"
);
assert_eq!(
change_owner(&system_program_id, &system_program_id, &alice_program_id, false),
change_owner(
&system_program_id,
&system_program_id,
&alice_program_id,
false
),
Err(InstructionError::ModifiedProgramId),
"system program should not be able to change the account owner of a credit only account"
"system program should not be able to change the account owner of a read-only account"
);
assert_eq!(
change_owner(
@ -517,7 +511,7 @@ mod tests {
fn test_verify_instruction_change_executable() {
let owner = Pubkey::new_rand();
let change_executable = |program_id: &Pubkey,
is_debitable: bool,
is_writable: bool,
pre_executable: bool,
post_executable: bool|
-> Result<(), InstructionError> {
@ -532,7 +526,7 @@ mod tests {
executable: post_executable,
..Account::default()
};
verify_instruction(is_debitable, &program_id, &pre, &post)
verify_instruction(is_writable, &program_id, &pre, &post)
};
let mallory_program_id = Pubkey::new_rand();
@ -551,7 +545,7 @@ mod tests {
assert_eq!(
change_executable(&owner, false, false, true),
Err(InstructionError::ExecutableModified),
"system program can't modify executable of credit-only accounts"
"system program can't modify executable of read-only accounts"
);
assert_eq!(
change_executable(&owner, true, true, false),
@ -596,10 +590,10 @@ mod tests {
let alice_program_id = Pubkey::new_rand();
let change_data =
|program_id: &Pubkey, is_debitable: bool| -> Result<(), InstructionError> {
|program_id: &Pubkey, is_writable: bool| -> Result<(), InstructionError> {
let pre = Account::new_data(0, &[0], &alice_program_id).unwrap();
let post = Account::new_data(0, &[42], &alice_program_id).unwrap();
verify_instruction(is_debitable, &program_id, &pre, &post)
verify_instruction(is_writable, &program_id, &pre, &post)
};
let mallory_program_id = Pubkey::new_rand();
@ -617,7 +611,7 @@ mod tests {
assert_eq!(
change_data(&alice_program_id, false),
Err(InstructionError::CreditOnlyDataModified),
Err(InstructionError::ReadonlyDataModified),
"alice isn't allowed to touch a CO account"
);
}
@ -670,7 +664,7 @@ mod tests {
);
assert_eq!(
verify_instruction(false, &alice_program_id, &pre, &post,),
Err(InstructionError::CreditOnlyLamportSpend),
Err(InstructionError::ReadonlyLamportChange),
"debit should fail, even if owning program"
);
@ -715,12 +709,12 @@ mod tests {
}
#[test]
fn test_process_message_credit_only_handling() {
fn test_process_message_readonly_handling() {
#[derive(Serialize, Deserialize)]
enum MockSystemInstruction {
Correct { lamports: u64 },
AttemptDebit { lamports: u64 },
Misbehave { lamports: u64 },
Correct,
AttemptCredit { lamports: u64 },
AttemptDataChange { data: u8 },
}
fn mock_system_process_instruction(
@ -730,20 +724,15 @@ mod tests {
) -> Result<(), InstructionError> {
if let Ok(instruction) = bincode::deserialize(data) {
match instruction {
MockSystemInstruction::Correct { lamports } => {
MockSystemInstruction::Correct => Ok(()),
MockSystemInstruction::AttemptCredit { lamports } => {
keyed_accounts[0].account.lamports -= lamports;
keyed_accounts[1].account.lamports += lamports;
Ok(())
}
MockSystemInstruction::AttemptDebit { lamports } => {
keyed_accounts[0].account.lamports += lamports;
keyed_accounts[1].account.lamports -= lamports;
Ok(())
}
// Credit a credit-only account for more lamports than debited
MockSystemInstruction::Misbehave { lamports } => {
keyed_accounts[0].account.lamports -= lamports;
keyed_accounts[1].account.lamports = 2 * lamports;
// Change data in a read-only account
MockSystemInstruction::AttemptDataChange { data } => {
keyed_accounts[1].account.data = vec![data];
Ok(())
}
}
@ -771,53 +760,46 @@ mod tests {
let to_pubkey = Pubkey::new_rand();
let account_metas = vec![
AccountMeta::new(from_pubkey, true),
AccountMeta::new_credit_only(to_pubkey, false),
AccountMeta::new_readonly(to_pubkey, false),
];
let message = Message::new(vec![Instruction::new(
mock_system_program_id,
&MockSystemInstruction::Correct { lamports: 50 },
&MockSystemInstruction::Correct,
account_metas.clone(),
)]);
let mut deltas = vec![0, 0];
let result =
message_processor.process_message(&message, &mut loaders, &mut accounts, &mut deltas);
let result = message_processor.process_message(&message, &mut loaders, &mut accounts);
assert_eq!(result, Ok(()));
assert_eq!(accounts[0].lamports, 50);
assert_eq!(accounts[1].lamports, 50);
assert_eq!(deltas, vec![0, 50]);
assert_eq!(accounts[0].lamports, 100);
assert_eq!(accounts[1].lamports, 0);
let message = Message::new(vec![Instruction::new(
mock_system_program_id,
&MockSystemInstruction::AttemptDebit { lamports: 50 },
&MockSystemInstruction::AttemptCredit { lamports: 50 },
account_metas.clone(),
)]);
let mut deltas = vec![0, 0];
let result =
message_processor.process_message(&message, &mut loaders, &mut accounts, &mut deltas);
let result = message_processor.process_message(&message, &mut loaders, &mut accounts);
assert_eq!(
result,
Err(TransactionError::InstructionError(
0,
InstructionError::CreditOnlyLamportSpend
InstructionError::ReadonlyLamportChange
))
);
let message = Message::new(vec![Instruction::new(
mock_system_program_id,
&MockSystemInstruction::Misbehave { lamports: 50 },
&MockSystemInstruction::AttemptDataChange { data: 50 },
account_metas,
)]);
let mut deltas = vec![0, 0];
let result =
message_processor.process_message(&message, &mut loaders, &mut accounts, &mut deltas);
let result = message_processor.process_message(&message, &mut loaders, &mut accounts);
assert_eq!(
result,
Err(TransactionError::InstructionError(
0,
InstructionError::UnbalancedInstruction
InstructionError::ReadonlyDataModified
))
);
}

View File

@ -423,7 +423,7 @@ mod tests {
let mut to_account = Account::new(1, 0, &Pubkey::new(&[3; 32])); // account owner should not matter
transfer_lamports(
&mut KeyedAccount::new(&from, true, &mut from_account),
&mut KeyedAccount::new_credit_only(&to, false, &mut to_account),
&mut KeyedAccount::new(&to, false, &mut to_account),
50,
)
.unwrap();
@ -435,7 +435,7 @@ mod tests {
// Attempt to move more lamports than remaining in from_account
let result = transfer_lamports(
&mut KeyedAccount::new(&from, true, &mut from_account),
&mut KeyedAccount::new_credit_only(&to, false, &mut to_account),
&mut KeyedAccount::new(&to, false, &mut to_account),
100,
);
assert_eq!(result, Err(SystemError::ResultWithNegativeLamports.into()));
@ -445,7 +445,7 @@ mod tests {
// test unsigned transfer of zero
assert!(transfer_lamports(
&mut KeyedAccount::new(&from, false, &mut from_account),
&mut KeyedAccount::new_credit_only(&to, false, &mut to_account),
&mut KeyedAccount::new(&to, false, &mut to_account),
0,
)
.is_ok(),);