From 1ffd6b4b4d43e28eab010914427efa1d67b5595c Mon Sep 17 00:00:00 2001 From: Trent Nelson Date: Sat, 7 Dec 2019 12:54:10 -0700 Subject: [PATCH] Add program and runtime support for Durable Transaction Nonces (#6845) * Rework transaction processing result forwarding Durable nonce prereq * Add Durable Nonce program API * Add runtime changes for Durable Nonce program * Register Durable Nonce program * Concise comments and bad math * Fix c/p error * Add rent sysvar to withdraw ix * Remove rent exempt required balance from Meta struct * Use the helper --- Cargo.lock | 1 + core/src/banking_stage.rs | 57 ++- core/src/transaction_status_service.rs | 11 +- genesis-programs/src/lib.rs | 11 +- ledger/src/blocktree_processor.rs | 6 +- runtime/src/accounts.rs | 169 +++++-- runtime/src/bank.rs | 433 ++++++++++++++--- runtime/src/genesis_utils.rs | 2 + runtime/src/lib.rs | 1 + runtime/src/message_processor.rs | 13 +- runtime/src/nonce_utils.rs | 195 ++++++++ sdk/Cargo.toml | 1 + sdk/src/genesis_config.rs | 3 +- sdk/src/lib.rs | 3 + sdk/src/nonce_instruction.rs | 432 +++++++++++++++++ sdk/src/nonce_program.rs | 5 + sdk/src/nonce_state.rs | 618 +++++++++++++++++++++++++ sdk/src/sysvar/recent_blockhashes.rs | 14 +- 18 files changed, 1832 insertions(+), 143 deletions(-) create mode 100644 runtime/src/nonce_utils.rs create mode 100644 sdk/src/nonce_instruction.rs create mode 100644 sdk/src/nonce_program.rs create mode 100644 sdk/src/nonce_state.rs diff --git a/Cargo.lock b/Cargo.lock index 553f476c0f..300dffd1cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3877,6 +3877,7 @@ dependencies = [ "solana-crate-features 0.22.0", "solana-logger 0.22.0", "solana-sdk-macro 0.22.0", + "thiserror 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", "tiny-bip39 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/core/src/banking_stage.rs b/core/src/banking_stage.rs index 96a683b173..43aa7a788a 100644 --- a/core/src/banking_stage.rs +++ b/core/src/banking_stage.rs @@ -20,7 +20,11 @@ use solana_ledger::{ use solana_measure::measure::Measure; use solana_metrics::{inc_new_counter_debug, inc_new_counter_info, inc_new_counter_warn}; use solana_perf::{cuda_runtime::PinnedVec, perf_libs}; -use solana_runtime::{accounts_db::ErrorCounters, bank::Bank, transaction_batch::TransactionBatch}; +use solana_runtime::{ + accounts_db::ErrorCounters, + bank::{Bank, TransactionProcessResult}, + transaction_batch::TransactionBatch, +}; use solana_sdk::{ clock::{ Slot, DEFAULT_TICKS_PER_SECOND, DEFAULT_TICKS_PER_SLOT, MAX_PROCESSING_AGE, @@ -443,7 +447,7 @@ impl BankingStage { fn record_transactions( bank_slot: Slot, txs: &[Transaction], - results: &[transaction::Result<()>], + results: &[TransactionProcessResult], poh: &Arc>, ) -> (Result, Vec) { let mut processed_generation = Measure::start("record::process_generation"); @@ -451,7 +455,7 @@ impl BankingStage { .iter() .zip(txs.iter()) .enumerate() - .filter_map(|(i, (r, x))| { + .filter_map(|(i, ((r, _h), x))| { if Bank::can_commit(r) { Some((x.clone(), i)) } else { @@ -678,13 +682,13 @@ impl BankingStage { // This function returns a vector containing index of all valid transactions. A valid // transaction has result Ok() as the value fn filter_valid_transaction_indexes( - valid_txs: &[transaction::Result<()>], + valid_txs: &[TransactionProcessResult], transaction_indexes: &[usize], ) -> Vec { let valid_transactions = valid_txs .iter() .enumerate() - .filter_map(|(index, x)| if x.is_ok() { Some(index) } else { None }) + .filter_map(|(index, (x, _h))| if x.is_ok() { Some(index) } else { None }) .collect_vec(); valid_transactions @@ -1022,6 +1026,7 @@ mod tests { entry::{next_entry, Entry, EntrySlice}, get_tmp_ledger_path, }; + use solana_runtime::bank::HashAgeKind; use solana_sdk::{ instruction::InstructionError, signature::{Keypair, KeypairUtil}, @@ -1369,7 +1374,10 @@ mod tests { system_transaction::transfer(&keypair2, &pubkey2, 1, genesis_config.hash()), ]; - let mut results = vec![Ok(()), Ok(())]; + let mut results = vec![ + (Ok(()), Some(HashAgeKind::Extant)), + (Ok(()), Some(HashAgeKind::Extant)), + ]; let _ = BankingStage::record_transactions( bank.slot(), &transactions, @@ -1380,10 +1388,13 @@ mod tests { assert_eq!(entry.transactions.len(), transactions.len()); // InstructionErrors should still be recorded - results[0] = Err(TransactionError::InstructionError( - 1, - InstructionError::new_result_with_negative_lamports(), - )); + results[0] = ( + Err(TransactionError::InstructionError( + 1, + InstructionError::new_result_with_negative_lamports(), + )), + Some(HashAgeKind::Extant), + ); let (res, retryable) = BankingStage::record_transactions( bank.slot(), &transactions, @@ -1396,7 +1407,7 @@ mod tests { assert_eq!(entry.transactions.len(), transactions.len()); // Other TransactionErrors should not be recorded - results[0] = Err(TransactionError::AccountNotFound); + results[0] = (Err(TransactionError::AccountNotFound), None); let (res, retryable) = BankingStage::record_transactions( bank.slot(), &transactions, @@ -1559,12 +1570,12 @@ mod tests { assert_eq!( BankingStage::filter_valid_transaction_indexes( &vec![ - Err(TransactionError::BlockhashNotFound), - Err(TransactionError::BlockhashNotFound), - Ok(()), - Err(TransactionError::BlockhashNotFound), - Ok(()), - Ok(()) + (Err(TransactionError::BlockhashNotFound), None), + (Err(TransactionError::BlockhashNotFound), None), + (Ok(()), Some(HashAgeKind::Extant)), + (Err(TransactionError::BlockhashNotFound), None), + (Ok(()), Some(HashAgeKind::Extant)), + (Ok(()), Some(HashAgeKind::Extant)), ], &vec![2, 4, 5, 9, 11, 13] ), @@ -1574,12 +1585,12 @@ mod tests { assert_eq!( BankingStage::filter_valid_transaction_indexes( &vec![ - Ok(()), - Err(TransactionError::BlockhashNotFound), - Err(TransactionError::BlockhashNotFound), - Ok(()), - Ok(()), - Ok(()) + (Ok(()), Some(HashAgeKind::Extant)), + (Err(TransactionError::BlockhashNotFound), None), + (Err(TransactionError::BlockhashNotFound), None), + (Ok(()), Some(HashAgeKind::Extant)), + (Ok(()), Some(HashAgeKind::Extant)), + (Ok(()), Some(HashAgeKind::Extant)), ], &vec![1, 6, 7, 9, 31, 43] ), diff --git a/core/src/transaction_status_service.rs b/core/src/transaction_status_service.rs index 58ea8b09c2..c3c4b9fba5 100644 --- a/core/src/transaction_status_service.rs +++ b/core/src/transaction_status_service.rs @@ -2,7 +2,7 @@ use crate::result::{Error, Result}; use crossbeam_channel::{Receiver, RecvTimeoutError}; use solana_client::rpc_request::RpcTransactionStatus; use solana_ledger::{blocktree::Blocktree, blocktree_processor::TransactionStatusBatch}; -use solana_runtime::bank::Bank; +use solana_runtime::bank::{Bank, HashAgeKind}; use std::{ sync::{ atomic::{AtomicBool, Ordering}, @@ -56,10 +56,15 @@ impl TransactionStatusService { } = write_transaction_status_receiver.recv_timeout(Duration::from_secs(1))?; let slot = bank.slot(); - for (transaction, status) in transactions.iter().zip(statuses) { + for (transaction, (status, hash_age_kind)) in transactions.iter().zip(statuses) { if Bank::can_commit(&status) && !transaction.signatures.is_empty() { + let fee_hash = if let Some(HashAgeKind::DurableNonce) = hash_age_kind { + bank.last_blockhash() + } else { + transaction.message().recent_blockhash + }; let fee_calculator = bank - .get_fee_calculator(&transaction.message().recent_blockhash) + .get_fee_calculator(&fee_hash) .expect("FeeCalculator must exist"); let fee = fee_calculator.calculate_fee(transaction.message()); blocktree diff --git a/genesis-programs/src/lib.rs b/genesis-programs/src/lib.rs index 2e0fcc7442..a50660da36 100644 --- a/genesis-programs/src/lib.rs +++ b/genesis-programs/src/lib.rs @@ -1,6 +1,7 @@ use solana_sdk::{ clock::Epoch, genesis_config::OperatingMode, inflation::Inflation, - move_loader::solana_move_loader_program, pubkey::Pubkey, system_program::solana_system_program, + move_loader::solana_move_loader_program, nonce_program::solana_nonce_program, pubkey::Pubkey, + system_program::solana_system_program, }; #[macro_use] @@ -58,6 +59,7 @@ pub fn get_programs(operating_mode: OperatingMode, epoch: Epoch) -> Option Option { if epoch == 0 { - // Voting, Staking and System Program only at epoch 0 + // Nonce, Voting, Staking and System Program only at epoch 0 Some(vec![ + solana_nonce_program(), solana_stake_program!(), solana_system_program(), solana_vote_program!(), @@ -124,6 +127,7 @@ pub fn get_entered_epoch_callback(operating_mode: OperatingMode) -> EnteredEpoch #[cfg(test)] mod tests { use super::*; + use solana_sdk::nonce_program::solana_nonce_program; use std::collections::HashSet; #[test] @@ -146,7 +150,7 @@ mod tests { fn test_development_programs() { assert_eq!( get_programs(OperatingMode::Development, 0).unwrap().len(), - 10 + 11 ); assert_eq!(get_programs(OperatingMode::Development, 1), None); } @@ -169,6 +173,7 @@ mod tests { assert_eq!( get_programs(OperatingMode::SoftLaunch, 0), Some(vec![ + solana_nonce_program(), solana_stake_program!(), solana_system_program(), solana_vote_program!(), diff --git a/ledger/src/blocktree_processor.rs b/ledger/src/blocktree_processor.rs index 5cc34a3e1d..c58b6553f2 100644 --- a/ledger/src/blocktree_processor.rs +++ b/ledger/src/blocktree_processor.rs @@ -14,7 +14,7 @@ use rayon::{prelude::*, ThreadPool}; use solana_metrics::{datapoint, datapoint_error, inc_new_counter_debug}; use solana_rayon_threadlimit::get_thread_count; use solana_runtime::{ - bank::{Bank, TransactionResults}, + bank::{Bank, TransactionProcessResult, TransactionResults}, transaction_batch::TransactionBatch, }; use solana_sdk::{ @@ -550,14 +550,14 @@ fn process_pending_slots( pub struct TransactionStatusBatch { pub bank: Arc, pub transactions: Vec, - pub statuses: Vec>, + pub statuses: Vec, } pub type TransactionStatusSender = Sender; pub fn send_transaction_status_batch( bank: Arc, transactions: &[Transaction], - statuses: Vec>, + statuses: Vec, transaction_status_sender: TransactionStatusSender, ) { let slot = bank.slot(); diff --git a/runtime/src/accounts.rs b/runtime/src/accounts.rs index 1470fc3a31..d3075e5dff 100644 --- a/runtime/src/accounts.rs +++ b/runtime/src/accounts.rs @@ -1,6 +1,7 @@ use crate::accounts_db::{AccountInfo, AccountStorage, AccountsDB, AppendVecId, ErrorCounters}; use crate::accounts_index::AccountsIndex; use crate::append_vec::StoredAccount; +use crate::bank::{HashAgeKind, TransactionProcessResult}; use crate::blockhash_queue::BlockhashQueue; use crate::message_processor::has_duplicates; use crate::rent_collector::RentCollector; @@ -224,11 +225,11 @@ impl Accounts { ancestors: &HashMap, txs: &[Transaction], txs_iteration_order: Option<&[usize]>, - lock_results: Vec>, + lock_results: Vec, hash_queue: &BlockhashQueue, error_counters: &mut ErrorCounters, rent_collector: &RentCollector, - ) -> Vec> { + ) -> Vec<(Result, Option)> { //PERF: hold the lock to scan for the references, but not to clone the accounts //TODO: two locks usually leads to deadlocks, should this be one structure? let accounts_index = self.accounts_db.accounts_index.read().unwrap(); @@ -236,13 +237,20 @@ impl Accounts { OrderedIterator::new(txs, txs_iteration_order) .zip(lock_results.into_iter()) .map(|etx| match etx { - (tx, Ok(())) => { - let fee_calculator = hash_queue - .get_fee_calculator(&tx.message().recent_blockhash) - .ok_or(TransactionError::BlockhashNotFound)?; + (tx, (Ok(()), hash_age_kind)) => { + let fee_hash = if let Some(HashAgeKind::DurableNonce) = hash_age_kind { + hash_queue.last_hash() + } else { + tx.message().recent_blockhash + }; + let fee = if let Some(fee_calculator) = hash_queue.get_fee_calculator(&fee_hash) + { + fee_calculator.calculate_fee(tx.message()) + } else { + return (Err(TransactionError::BlockhashNotFound), hash_age_kind); + }; - let fee = fee_calculator.calculate_fee(tx.message()); - let (accounts, rents) = self.load_tx_accounts( + let load_res = self.load_tx_accounts( &storage, ancestors, &accounts_index, @@ -250,17 +258,27 @@ impl Accounts { fee, error_counters, rent_collector, - )?; - let loaders = Self::load_loaders( + ); + let (accounts, rents) = match load_res { + Ok((a, r)) => (a, r), + Err(e) => return (Err(e), hash_age_kind), + }; + + let load_res = Self::load_loaders( &storage, ancestors, &accounts_index, tx, error_counters, - )?; - Ok((accounts, loaders, rents)) + ); + let loaders = match load_res { + Ok(loaders) => loaders, + Err(e) => return (Err(e), hash_age_kind), + }; + + (Ok((accounts, loaders, rents)), hash_age_kind) } - (_, Err(e)) => Err(e), + (_, (Err(e), hash_age_kind)) => (Err(e), hash_age_kind), }) .collect() } @@ -520,8 +538,8 @@ impl Accounts { slot: Slot, txs: &[Transaction], txs_iteration_order: Option<&[usize]>, - res: &[Result<()>], - loaded: &mut [Result], + res: &[TransactionProcessResult], + loaded: &mut [(Result, Option)], rent_collector: &RentCollector, ) { let accounts_to_store = @@ -543,17 +561,18 @@ impl Accounts { &self, txs: &'a [Transaction], txs_iteration_order: Option<&'a [usize]>, - res: &'a [Result<()>], - loaded: &'a mut [Result], + res: &'a [TransactionProcessResult], + loaded: &'a mut [(Result, Option)], rent_collector: &RentCollector, ) -> Vec<(&'a Pubkey, &'a Account)> { let mut accounts = Vec::with_capacity(loaded.len()); - for (i, (raccs, tx)) in loaded + for (i, ((raccs, _hash_age_kind), tx)) in loaded .iter_mut() .zip(OrderedIterator::new(txs, txs_iteration_order)) .enumerate() { - if res[i].is_err() || raccs.is_err() { + let (res, _hash_age_kind) = &res[i]; + if res.is_err() || raccs.is_err() { continue; } @@ -599,6 +618,7 @@ mod tests { use super::*; use crate::accounts_db::tests::copy_append_vecs; use crate::accounts_db::{get_temp_accounts_paths, AccountsDBSerialize}; + use crate::bank::HashAgeKind; use bincode::serialize_into; use rand::{thread_rng, Rng}; use solana_sdk::account::Account; @@ -618,7 +638,7 @@ mod tests { ka: &Vec<(Pubkey, Account)>, fee_calculator: &FeeCalculator, error_counters: &mut ErrorCounters, - ) -> Vec> { + ) -> Vec<(Result, Option)> { let mut hash_queue = BlockhashQueue::new(100); hash_queue.register_hash(&tx.message().recent_blockhash, &fee_calculator); let accounts = Accounts::new(Vec::new()); @@ -632,7 +652,7 @@ mod tests { &ancestors, &[tx], None, - vec![Ok(())], + vec![(Ok(()), Some(HashAgeKind::Extant))], &hash_queue, error_counters, &rent_collector, @@ -644,7 +664,7 @@ mod tests { tx: Transaction, ka: &Vec<(Pubkey, Account)>, error_counters: &mut ErrorCounters, - ) -> Vec> { + ) -> Vec<(Result, Option)> { let fee_calculator = FeeCalculator::default(); load_accounts_with_fee(tx, ka, &fee_calculator, error_counters) } @@ -667,7 +687,13 @@ mod tests { assert_eq!(error_counters.account_not_found, 1); assert_eq!(loaded_accounts.len(), 1); - assert_eq!(loaded_accounts[0], Err(TransactionError::AccountNotFound)); + assert_eq!( + loaded_accounts[0], + ( + Err(TransactionError::AccountNotFound), + Some(HashAgeKind::Extant) + ) + ); } #[test] @@ -690,7 +716,13 @@ mod tests { assert_eq!(error_counters.account_not_found, 1); assert_eq!(loaded_accounts.len(), 1); - assert_eq!(loaded_accounts[0], Err(TransactionError::AccountNotFound)); + assert_eq!( + loaded_accounts[0], + ( + Err(TransactionError::AccountNotFound), + Some(HashAgeKind::Extant) + ), + ); } #[test] @@ -723,7 +755,10 @@ mod tests { assert_eq!(loaded_accounts.len(), 1); assert_eq!( loaded_accounts[0], - Err(TransactionError::ProgramAccountNotFound) + ( + Err(TransactionError::ProgramAccountNotFound), + Some(HashAgeKind::Extant) + ) ); } @@ -757,7 +792,10 @@ mod tests { assert_eq!(loaded_accounts.len(), 1); assert_eq!( loaded_accounts[0].clone(), - Err(TransactionError::InsufficientFundsForFee) + ( + Err(TransactionError::InsufficientFundsForFee), + Some(HashAgeKind::Extant) + ), ); } @@ -787,7 +825,10 @@ mod tests { assert_eq!(loaded_accounts.len(), 1); assert_eq!( loaded_accounts[0], - Err(TransactionError::InvalidAccountForFee) + ( + Err(TransactionError::InvalidAccountForFee), + Some(HashAgeKind::Extant) + ), ); } @@ -822,13 +863,16 @@ 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_rents)) => { + ( + Ok((transaction_accounts, transaction_loaders, _transaction_rents)), + _hash_age_kind, + ) => { 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); } - Err(e) => Err(e).unwrap(), + (Err(e), _hash_age_kind) => Err(e).unwrap(), } } @@ -892,7 +936,13 @@ mod tests { assert_eq!(error_counters.call_chain_too_deep, 1); assert_eq!(loaded_accounts.len(), 1); - assert_eq!(loaded_accounts[0], Err(TransactionError::CallChainTooDeep)); + assert_eq!( + loaded_accounts[0], + ( + Err(TransactionError::CallChainTooDeep), + Some(HashAgeKind::Extant) + ) + ); } #[test] @@ -925,7 +975,13 @@ mod tests { assert_eq!(error_counters.account_not_found, 1); assert_eq!(loaded_accounts.len(), 1); - assert_eq!(loaded_accounts[0], Err(TransactionError::AccountNotFound)); + assert_eq!( + loaded_accounts[0], + ( + Err(TransactionError::AccountNotFound), + Some(HashAgeKind::Extant) + ) + ); } #[test] @@ -957,7 +1013,13 @@ mod tests { assert_eq!(error_counters.account_not_found, 1); assert_eq!(loaded_accounts.len(), 1); - assert_eq!(loaded_accounts[0], Err(TransactionError::AccountNotFound)); + assert_eq!( + loaded_accounts[0], + ( + Err(TransactionError::AccountNotFound), + Some(HashAgeKind::Extant) + ) + ); } #[test] @@ -1003,7 +1065,10 @@ 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_rents)) => { + ( + Ok((transaction_accounts, transaction_loaders, _transaction_rents)), + _hash_age_kind, + ) => { assert_eq!(transaction_accounts.len(), 1); assert_eq!(transaction_accounts[0], accounts[0].1); assert_eq!(transaction_loaders.len(), 2); @@ -1016,7 +1081,7 @@ mod tests { } } } - Err(e) => Err(e).unwrap(), + (Err(e), _hash_age_kind) => Err(e).unwrap(), } } @@ -1046,7 +1111,10 @@ mod tests { assert_eq!(loaded_accounts.len(), 1); assert_eq!( loaded_accounts[0], - Err(TransactionError::AccountLoadedTwice) + ( + Err(TransactionError::AccountLoadedTwice), + Some(HashAgeKind::Extant) + ) ); } @@ -1378,7 +1446,10 @@ mod tests { let tx1 = Transaction::new(&[&keypair1], message, Hash::default()); let txs = vec![tx0, tx1]; - let loaders = vec![Ok(()), Ok(())]; + let loaders = vec![ + (Ok(()), Some(HashAgeKind::Extant)), + (Ok(()), Some(HashAgeKind::Extant)), + ]; let account0 = Account::new(1, 0, &Pubkey::default()); let account1 = Account::new(2, 0, &Pubkey::default()); @@ -1387,20 +1458,26 @@ mod tests { let transaction_accounts0 = vec![account0, account2.clone()]; let transaction_loaders0 = vec![]; let transaction_rent0 = 0; - let loaded0 = Ok(( - transaction_accounts0, - transaction_loaders0, - transaction_rent0, - )); + let loaded0 = ( + Ok(( + transaction_accounts0, + transaction_loaders0, + transaction_rent0, + )), + Some(HashAgeKind::Extant), + ); let transaction_accounts1 = vec![account1, account2.clone()]; let transaction_loaders1 = vec![]; let transaction_rent1 = 0; - let loaded1 = Ok(( - transaction_accounts1, - transaction_loaders1, - transaction_rent1, - )); + let loaded1 = ( + Ok(( + transaction_accounts1, + transaction_loaders1, + transaction_rent1, + )), + Some(HashAgeKind::Extant), + ); let mut loaded = vec![loaded0, loaded1]; diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 6c2b598b0c..96d0d533f2 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -7,6 +7,7 @@ use crate::{ accounts_db::{AccountStorageEntry, AccountsDBSerialize, AppendVecId, ErrorCounters}, blockhash_queue::BlockhashQueue, message_processor::{MessageProcessor, ProcessInstruction}, + nonce_utils, rent_collector::RentCollector, serde_utils::{ deserialize_atomicbool, deserialize_atomicu64, serialize_atomicbool, serialize_atomicu64, @@ -154,9 +155,16 @@ impl StatusCacheRc { pub type EnteredEpochCallback = Box () + Sync + Send>; +pub type TransactionProcessResult = (Result<()>, Option); pub struct TransactionResults { pub fee_collection_results: Vec>, - pub processing_results: Vec>, + pub processing_results: Vec, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum HashAgeKind { + Extant, + DurableNonce, } /// Manager for the state of all accounts and programs after processing its entries. @@ -791,16 +799,17 @@ impl Bank { &self, txs: &[Transaction], iteration_order: Option<&[usize]>, - res: &[Result<()>], + res: &[TransactionProcessResult], ) { let mut status_cache = self.src.status_cache.write().unwrap(); for (i, tx) in OrderedIterator::new(txs, iteration_order).enumerate() { - if Self::can_commit(&res[i]) && !tx.signatures.is_empty() { + let (res, _hash_age_kind) = &res[i]; + if Self::can_commit(res) && !tx.signatures.is_empty() { status_cache.insert( &tx.message().recent_blockhash, &tx.signatures[0], self.slot(), - res[i].clone(), + res.clone(), ); } } @@ -868,9 +877,9 @@ impl Bank { &self, txs: &[Transaction], iteration_order: Option<&[usize]>, - results: Vec>, + results: Vec, error_counters: &mut ErrorCounters, - ) -> Vec> { + ) -> Vec<(Result, Option)> { self.rc.accounts.load_accounts( &self.ancestors, txs, @@ -907,19 +916,23 @@ impl Bank { lock_results: Vec>, max_age: usize, error_counters: &mut ErrorCounters, - ) -> Vec> { + ) -> Vec { let hash_queue = self.blockhash_queue.read().unwrap(); OrderedIterator::new(txs, iteration_order) .zip(lock_results.into_iter()) - .map(|(tx, lock_res)| { - if lock_res.is_ok() - && !hash_queue.check_hash_age(&tx.message().recent_blockhash, max_age) - { - error_counters.reserve_blockhash += 1; - Err(TransactionError::BlockhashNotFound) - } else { - lock_res + .map(|(tx, lock_res)| match lock_res { + Ok(()) => { + let message = tx.message(); + if hash_queue.check_hash_age(&message.recent_blockhash, max_age) { + (Ok(()), Some(HashAgeKind::Extant)) + } else if self.check_tx_durable_nonce(&tx) { + (Ok(()), Some(HashAgeKind::DurableNonce)) + } else { + error_counters.reserve_blockhash += 1; + (Err(TransactionError::BlockhashNotFound), None) + } } + Err(e) => (Err(e), None), }) .collect() } @@ -927,9 +940,9 @@ impl Bank { &self, txs: &[Transaction], iteration_order: Option<&[usize]>, - lock_results: Vec>, + lock_results: Vec, error_counters: &mut ErrorCounters, - ) -> Vec> { + ) -> Vec { let rcache = self.src.status_cache.read().unwrap(); OrderedIterator::new(txs, iteration_order) .zip(lock_results.into_iter()) @@ -937,20 +950,25 @@ impl Bank { if tx.signatures.is_empty() { return lock_res; } - if lock_res.is_ok() - && rcache - .get_signature_status( - &tx.signatures[0], - &tx.message().recent_blockhash, - &self.ancestors, - ) - .is_some() { - error_counters.duplicate_signature += 1; - Err(TransactionError::DuplicateSignature) - } else { - lock_res + let (lock_res, hash_age_kind) = &lock_res; + if lock_res.is_ok() + && rcache + .get_signature_status( + &tx.signatures[0], + &tx.message().recent_blockhash, + &self.ancestors, + ) + .is_some() + { + error_counters.duplicate_signature += 1; + return ( + Err(TransactionError::DuplicateSignature), + hash_age_kind.clone(), + ); + } } + lock_res }) .collect() } @@ -962,6 +980,18 @@ impl Bank { .check_hash_age(hash, max_age) } + pub fn check_tx_durable_nonce(&self, tx: &Transaction) -> bool { + nonce_utils::transaction_uses_durable_nonce(&tx) + .and_then(|nonce_ix| nonce_utils::get_nonce_pubkey_from_instruction(&nonce_ix, &tx)) + .and_then(|nonce_pubkey| self.get_account(&nonce_pubkey)) + .map_or_else( + || false, + |nonce_account| { + nonce_utils::verify_nonce(&nonce_account, &tx.message().recent_blockhash) + }, + ) + } + pub fn check_transactions( &self, txs: &[Transaction], @@ -969,7 +999,7 @@ impl Bank { lock_results: &[Result<()>], max_age: usize, mut error_counters: &mut ErrorCounters, - ) -> Vec> { + ) -> Vec { let refs_results = self.check_refs(txs, iteration_order, lock_results, &mut error_counters); let age_results = self.check_age( txs, @@ -1032,8 +1062,8 @@ impl Bank { batch: &TransactionBatch, max_age: usize, ) -> ( - Vec>, - Vec>, + Vec<(Result, Option)>, + Vec, Vec, u64, u64, @@ -1071,15 +1101,18 @@ impl Bank { let mut execution_time = Measure::start("execution_time"); let mut signature_count: u64 = 0; - let executed: Vec> = loaded_accounts + let executed: Vec = loaded_accounts .iter_mut() .zip(OrderedIterator::new(txs, batch.iteration_order())) .map(|(accs, tx)| match accs { - Err(e) => Err(e.clone()), - Ok((accounts, loaders, _rents)) => { + (Err(e), hash_age_kind) => (Err(e.clone()), hash_age_kind.clone()), + (Ok((accounts, loaders, _rents)), hash_age_kind) => { signature_count += u64::from(tx.message().header.num_required_signatures); - self.message_processor - .process_message(tx.message(), loaders, accounts) + ( + self.message_processor + .process_message(tx.message(), loaders, accounts), + hash_age_kind.clone(), + ) } }) .collect(); @@ -1094,7 +1127,7 @@ impl Bank { ); let mut tx_count: u64 = 0; let mut err_count = 0; - for (r, tx) in executed.iter().zip(txs.iter()) { + for ((r, _hash_age_kind), tx) in executed.iter().zip(txs.iter()) { if r.is_ok() { tx_count += 1; } else { @@ -1127,17 +1160,22 @@ impl Bank { &self, txs: &[Transaction], iteration_order: Option<&[usize]>, - executed: &[Result<()>], + executed: &[TransactionProcessResult], ) -> Vec> { let hash_queue = self.blockhash_queue.read().unwrap(); let mut fees = 0; let results = OrderedIterator::new(txs, iteration_order) .zip(executed.iter()) - .map(|(tx, res)| { - let fee_calculator = hash_queue - .get_fee_calculator(&tx.message().recent_blockhash) - .ok_or(TransactionError::BlockhashNotFound)?; - let fee = fee_calculator.calculate_fee(tx.message()); + .map(|(tx, (res, hash_age_kind))| { + let fee_hash = if let Some(HashAgeKind::DurableNonce) = hash_age_kind { + self.last_blockhash() + } else { + tx.message().recent_blockhash + }; + let fee = hash_queue + .get_fee_calculator(&fee_hash) + .ok_or(TransactionError::BlockhashNotFound)? + .calculate_fee(tx.message()); let message = tx.message(); match *res { @@ -1166,8 +1204,8 @@ impl Bank { &self, txs: &[Transaction], iteration_order: Option<&[usize]>, - loaded_accounts: &mut [Result], - executed: &[Result<()>], + loaded_accounts: &mut [(Result, Option)], + executed: &[TransactionProcessResult], tx_count: u64, signature_count: u64, ) -> TransactionResults { @@ -1182,7 +1220,10 @@ impl Bank { inc_new_counter_info!("bank-process_transactions-txs", tx_count as usize); inc_new_counter_info!("bank-process_transactions-sigs", signature_count as usize); - if executed.iter().any(|res| Self::can_commit(res)) { + if executed + .iter() + .any(|(res, _hash_age_kind)| Self::can_commit(res)) + { self.is_delta.store(true, Ordering::Relaxed); } @@ -1256,10 +1297,15 @@ impl Bank { self.distribute_rent_to_validators(&self.vote_accounts(), rent_to_be_distributed); } - fn collect_rent(&self, res: &[Result<()>], loaded_accounts: &[Result]) { + fn collect_rent( + &self, + res: &[TransactionProcessResult], + loaded_accounts: &[(Result, Option)], + ) { let mut collected_rent: u64 = 0; - for (i, raccs) in loaded_accounts.iter().enumerate() { - if res[i].is_err() || raccs.is_err() { + for (i, (raccs, _hash_age_kind)) in loaded_accounts.iter().enumerate() { + let (res, _hash_age_kind) = &res[i]; + if res.is_err() || raccs.is_err() { continue; } @@ -1543,15 +1589,16 @@ impl Bank { &self, txs: &[Transaction], iteration_order: Option<&[usize]>, - res: &[Result<()>], - loaded: &[Result], + res: &[TransactionProcessResult], + loaded: &[(Result, Option)], ) { - for (i, (raccs, tx)) in loaded + for (i, ((raccs, _load_hash_age_kind), tx)) in loaded .iter() .zip(OrderedIterator::new(txs, iteration_order)) .enumerate() { - if res[i].is_err() || raccs.is_err() { + let (res, _res_hash_age_kind) = &res[i]; + if res.is_err() || raccs.is_err() { continue; } @@ -1706,11 +1753,13 @@ mod tests { use solana_sdk::system_program::solana_system_program; use solana_sdk::{ account::KeyedAccount, + account_utils::State, clock::DEFAULT_TICKS_PER_SLOT, epoch_schedule::MINIMUM_SLOTS_PER_EPOCH, genesis_config::create_genesis_config, instruction::{Instruction, InstructionError}, message::{Message, MessageHeader}, + nonce_instruction, nonce_state, poh_config::PohConfig, rent::Rent, signature::{Keypair, KeypairUtil}, @@ -2943,11 +2992,14 @@ mod tests { system_transaction::transfer(&mint_keypair, &key.pubkey(), 5, genesis_config.hash()); let results = vec![ - Ok(()), - Err(TransactionError::InstructionError( - 1, - InstructionError::new_result_with_negative_lamports(), - )), + (Ok(()), Some(HashAgeKind::Extant)), + ( + Err(TransactionError::InstructionError( + 1, + InstructionError::new_result_with_negative_lamports(), + )), + Some(HashAgeKind::Extant), + ), ]; let initial_balance = bank.get_balance(&leader); @@ -4233,4 +4285,267 @@ mod tests { } } } + + fn get_nonce(bank: &Bank, nonce_pubkey: &Pubkey) -> Option { + bank.get_account(&nonce_pubkey) + .and_then(|acc| match acc.state() { + Ok(nonce_state::NonceState::Initialized(_meta, hash)) => Some(hash), + _ => None, + }) + } + + fn nonce_setup( + bank: &mut Arc, + mint_keypair: &Keypair, + custodian_lamports: u64, + nonce_lamports: u64, + ) -> Result<(Keypair, Keypair)> { + let custodian_keypair = Keypair::new(); + let nonce_keypair = Keypair::new(); + /* Setup accounts */ + let mut setup_ixs = vec![system_instruction::transfer( + &mint_keypair.pubkey(), + &custodian_keypair.pubkey(), + custodian_lamports, + )]; + setup_ixs.extend_from_slice(&nonce_instruction::create_nonce_account( + &custodian_keypair.pubkey(), + &nonce_keypair.pubkey(), + nonce_lamports, + )); + let setup_tx = Transaction::new_signed_instructions( + &[mint_keypair, &custodian_keypair, &nonce_keypair], + setup_ixs, + bank.last_blockhash(), + ); + bank.process_transaction(&setup_tx)?; + Ok((custodian_keypair, nonce_keypair)) + } + + fn setup_nonce_with_bank( + supply_lamports: u64, + mut genesis_cfg_fn: F, + custodian_lamports: u64, + nonce_lamports: u64, + ) -> Result<(Arc, Keypair, Keypair, Keypair)> + where + F: FnMut(&mut GenesisConfig), + { + let (mut genesis_config, mint_keypair) = create_genesis_config(supply_lamports); + genesis_cfg_fn(&mut genesis_config); + let mut bank = Arc::new(Bank::new(&genesis_config)); + + let (custodian_keypair, nonce_keypair) = + nonce_setup(&mut bank, &mint_keypair, custodian_lamports, nonce_lamports)?; + Ok((bank, mint_keypair, custodian_keypair, nonce_keypair)) + } + + #[test] + fn test_check_tx_durable_nonce_ok() { + let (bank, _mint_keypair, custodian_keypair, nonce_keypair) = + setup_nonce_with_bank(10_000_000, |_| {}, 5_000_000, 250_000).unwrap(); + let custodian_pubkey = custodian_keypair.pubkey(); + let nonce_pubkey = nonce_keypair.pubkey(); + + let nonce_hash = get_nonce(&bank, &nonce_pubkey).unwrap(); + let tx = Transaction::new_signed_with_payer( + vec![ + nonce_instruction::nonce(&nonce_pubkey), + system_instruction::transfer(&custodian_pubkey, &nonce_pubkey, 100_000), + ], + Some(&custodian_pubkey), + &[&custodian_keypair, &nonce_keypair], + nonce_hash, + ); + assert!(bank.check_tx_durable_nonce(&tx)); + } + + #[test] + fn test_check_tx_durable_nonce_not_durable_nonce_fail() { + let (bank, _mint_keypair, custodian_keypair, nonce_keypair) = + setup_nonce_with_bank(10_000_000, |_| {}, 5_000_000, 250_000).unwrap(); + let custodian_pubkey = custodian_keypair.pubkey(); + let nonce_pubkey = nonce_keypair.pubkey(); + + let nonce_hash = get_nonce(&bank, &nonce_pubkey).unwrap(); + let tx = Transaction::new_signed_with_payer( + vec![ + system_instruction::transfer(&custodian_pubkey, &nonce_pubkey, 100_000), + nonce_instruction::nonce(&nonce_pubkey), + ], + Some(&custodian_pubkey), + &[&custodian_keypair, &nonce_keypair], + nonce_hash, + ); + assert!(!bank.check_tx_durable_nonce(&tx)); + } + + #[test] + fn test_check_tx_durable_nonce_missing_ix_pubkey_fail() { + let (bank, _mint_keypair, custodian_keypair, nonce_keypair) = + setup_nonce_with_bank(10_000_000, |_| {}, 5_000_000, 250_000).unwrap(); + let custodian_pubkey = custodian_keypair.pubkey(); + let nonce_pubkey = nonce_keypair.pubkey(); + + let nonce_hash = get_nonce(&bank, &nonce_pubkey).unwrap(); + let mut tx = Transaction::new_signed_with_payer( + vec![ + nonce_instruction::nonce(&nonce_pubkey), + system_instruction::transfer(&custodian_pubkey, &nonce_pubkey, 100_000), + ], + Some(&custodian_pubkey), + &[&custodian_keypair, &nonce_keypair], + nonce_hash, + ); + tx.message.instructions[0].accounts.clear(); + assert!(!bank.check_tx_durable_nonce(&tx)); + } + + #[test] + fn test_check_tx_durable_nonce_nonce_acc_does_not_exist_fail() { + let (bank, _mint_keypair, custodian_keypair, nonce_keypair) = + setup_nonce_with_bank(10_000_000, |_| {}, 5_000_000, 250_000).unwrap(); + let custodian_pubkey = custodian_keypair.pubkey(); + let nonce_pubkey = nonce_keypair.pubkey(); + let missing_keypair = Keypair::new(); + let missing_pubkey = missing_keypair.pubkey(); + + let nonce_hash = get_nonce(&bank, &nonce_pubkey).unwrap(); + let tx = Transaction::new_signed_with_payer( + vec![ + nonce_instruction::nonce(&missing_pubkey), + system_instruction::transfer(&custodian_pubkey, &nonce_pubkey, 100_000), + ], + Some(&custodian_pubkey), + &[&custodian_keypair, &missing_keypair], + nonce_hash, + ); + assert!(!bank.check_tx_durable_nonce(&tx)); + } + + #[test] + fn test_check_tx_durable_nonce_bad_tx_hash_fail() { + let (bank, _mint_keypair, custodian_keypair, nonce_keypair) = + setup_nonce_with_bank(10_000_000, |_| {}, 5_000_000, 250_000).unwrap(); + let custodian_pubkey = custodian_keypair.pubkey(); + let nonce_pubkey = nonce_keypair.pubkey(); + + let tx = Transaction::new_signed_with_payer( + vec![ + nonce_instruction::nonce(&nonce_pubkey), + system_instruction::transfer(&custodian_pubkey, &nonce_pubkey, 100_000), + ], + Some(&custodian_pubkey), + &[&custodian_keypair, &nonce_keypair], + Hash::default(), + ); + assert!(!bank.check_tx_durable_nonce(&tx)); + } + + #[test] + fn test_durable_nonce_transaction() { + let (mut bank, _mint_keypair, custodian_keypair, nonce_keypair) = setup_nonce_with_bank( + 10_000_000, + |gc| { + gc.rent.lamports_per_byte_year; + }, + 5_000_000, + 250_000, + ) + .unwrap(); + let alice_keypair = Keypair::new(); + let alice_pubkey = alice_keypair.pubkey(); + let custodian_pubkey = custodian_keypair.pubkey(); + let nonce_pubkey = nonce_keypair.pubkey(); + + assert_eq!(bank.get_balance(&custodian_pubkey), 4_750_000); + assert_eq!(bank.get_balance(&nonce_pubkey), 250_000); + + /* Grab the hash stored in the nonce account */ + let nonce_hash = get_nonce(&bank, &nonce_pubkey).unwrap(); + + /* Kick nonce hash off the blockhash_queue */ + for _ in 0..MAX_RECENT_BLOCKHASHES + 1 { + goto_end_of_slot(Arc::get_mut(&mut bank).unwrap()); + bank = Arc::new(new_from_parent(&bank)); + } + + /* Expect a non-Durable Nonce transfer to fail */ + assert_eq!( + bank.process_transaction(&system_transaction::transfer( + &custodian_keypair, + &alice_pubkey, + 100_000, + nonce_hash + ),), + Err(TransactionError::BlockhashNotFound), + ); + /* Check fee not charged */ + assert_eq!(bank.get_balance(&custodian_pubkey), 4_750_000); + + /* Durable Nonce transfer */ + let durable_tx = Transaction::new_signed_with_payer( + vec![ + nonce_instruction::nonce(&nonce_pubkey), + system_instruction::transfer(&custodian_pubkey, &alice_pubkey, 100_000), + ], + Some(&custodian_pubkey), + &[&custodian_keypair, &nonce_keypair], + nonce_hash, + ); + assert_eq!(bank.process_transaction(&durable_tx), Ok(())); + + /* Check balances */ + assert_eq!(bank.get_balance(&custodian_pubkey), 4_640_000); + assert_eq!(bank.get_balance(&nonce_pubkey), 250_000); + assert_eq!(bank.get_balance(&alice_pubkey), 100_000); + + /* Confirm stored nonce has advanced */ + let new_nonce = get_nonce(&bank, &nonce_pubkey).unwrap(); + assert_ne!(nonce_hash, new_nonce); + + /* Durable Nonce re-use fails */ + let durable_tx = Transaction::new_signed_with_payer( + vec![ + nonce_instruction::nonce(&nonce_pubkey), + system_instruction::transfer(&custodian_pubkey, &alice_pubkey, 100_000), + ], + Some(&custodian_pubkey), + &[&custodian_keypair, &nonce_keypair], + nonce_hash, + ); + assert_eq!( + bank.process_transaction(&durable_tx), + Err(TransactionError::BlockhashNotFound) + ); + /* Check fee not charged */ + assert_eq!(bank.get_balance(&custodian_pubkey), 4_640_000); + + let nonce_hash = get_nonce(&bank, &nonce_pubkey).unwrap(); + + /* Kick nonce hash off the blockhash_queue */ + for _ in 0..MAX_RECENT_BLOCKHASHES + 1 { + goto_end_of_slot(Arc::get_mut(&mut bank).unwrap()); + bank = Arc::new(new_from_parent(&bank)); + } + + let durable_tx = Transaction::new_signed_with_payer( + vec![ + nonce_instruction::nonce(&nonce_pubkey), + system_instruction::transfer(&custodian_pubkey, &alice_pubkey, 100_000_000), + ], + Some(&custodian_pubkey), + &[&custodian_keypair, &nonce_keypair], + nonce_hash, + ); + assert_eq!( + bank.process_transaction(&durable_tx), + Err(TransactionError::InstructionError( + 1, + system_instruction::SystemError::ResultWithNegativeLamports.into() + )) + ); + /* Check fee charged */ + assert_eq!(bank.get_balance(&custodian_pubkey), 4_630_000); + } } diff --git a/runtime/src/genesis_utils.rs b/runtime/src/genesis_utils.rs index d3312ba618..05f8a0d1ae 100644 --- a/runtime/src/genesis_utils.rs +++ b/runtime/src/genesis_utils.rs @@ -2,6 +2,7 @@ use solana_sdk::{ account::Account, fee_calculator::FeeCalculator, genesis_config::GenesisConfig, + nonce_program::solana_nonce_program, pubkey::Pubkey, rent::Rent, signature::{Keypair, KeypairUtil}, @@ -74,6 +75,7 @@ pub fn create_genesis_config_with_leader( // Bare minimum program set let native_instruction_processors = vec![ solana_system_program(), + solana_nonce_program(), solana_bpf_loader_program!(), solana_vote_program!(), solana_stake_program!(), diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index f51259262d..197c3f27c8 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -10,6 +10,7 @@ pub mod genesis_utils; pub mod loader_utils; pub mod message_processor; mod native_loader; +mod nonce_utils; pub mod rent_collector; mod serde_utils; pub mod stakes; diff --git a/runtime/src/message_processor.rs b/runtime/src/message_processor.rs index 580d4afc64..ec19d9eac0 100644 --- a/runtime/src/message_processor.rs +++ b/runtime/src/message_processor.rs @@ -7,6 +7,8 @@ use solana_sdk::instruction::{CompiledInstruction, InstructionError}; use solana_sdk::instruction_processor_utils; use solana_sdk::loader_instruction::LoaderInstruction; use solana_sdk::message::Message; +use solana_sdk::nonce_instruction; +use solana_sdk::nonce_program; use solana_sdk::pubkey::Pubkey; use solana_sdk::system_program; use solana_sdk::transaction::TransactionError; @@ -198,10 +200,13 @@ pub struct MessageProcessor { impl Default for MessageProcessor { fn default() -> Self { - let instruction_processors: Vec<(Pubkey, ProcessInstruction)> = vec![( - system_program::id(), - system_instruction_processor::process_instruction, - )]; + let instruction_processors: Vec<(Pubkey, ProcessInstruction)> = vec![ + ( + system_program::id(), + system_instruction_processor::process_instruction, + ), + (nonce_program::id(), nonce_instruction::process_instruction), + ]; Self { instruction_processors, diff --git a/runtime/src/nonce_utils.rs b/runtime/src/nonce_utils.rs new file mode 100644 index 0000000000..792d9eaa70 --- /dev/null +++ b/runtime/src/nonce_utils.rs @@ -0,0 +1,195 @@ +use solana_sdk::{ + account::Account, account_utils::State, hash::Hash, instruction::CompiledInstruction, + instruction_processor_utils::limited_deserialize, nonce_instruction::NonceInstruction, + nonce_program, nonce_state::NonceState, pubkey::Pubkey, transaction::Transaction, +}; + +pub fn transaction_uses_durable_nonce(tx: &Transaction) -> Option<&CompiledInstruction> { + let message = tx.message(); + message + .instructions + .get(0) + .filter(|maybe_ix| { + let prog_id_idx = maybe_ix.program_id_index as usize; + match message.account_keys.get(prog_id_idx) { + Some(program_id) => nonce_program::check_id(&program_id), + _ => false, + } + }) + .filter(|maybe_ix| match limited_deserialize(&maybe_ix.data) { + Ok(NonceInstruction::Nonce) => true, + _ => false, + }) +} + +pub fn get_nonce_pubkey_from_instruction<'a>( + ix: &CompiledInstruction, + tx: &'a Transaction, +) -> Option<&'a Pubkey> { + ix.accounts.get(0).and_then(|idx| { + let idx = *idx as usize; + tx.message().account_keys.get(idx) + }) +} + +pub fn verify_nonce(acc: &Account, hash: &Hash) -> bool { + match acc.state() { + Ok(NonceState::Initialized(_meta, ref nonce)) => hash == nonce, + _ => false, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use solana_sdk::{ + hash::Hash, + nonce_instruction, + nonce_state::{with_test_keyed_account, NonceAccount}, + pubkey::Pubkey, + signature::{Keypair, KeypairUtil}, + system_instruction, + sysvar::{recent_blockhashes::create_test_recent_blockhashes, rent::Rent}, + }; + use std::collections::HashSet; + + fn nonced_transfer_tx() -> (Pubkey, Pubkey, Transaction) { + let from_keypair = Keypair::new(); + let from_pubkey = from_keypair.pubkey(); + let nonce_keypair = Keypair::new(); + let nonce_pubkey = nonce_keypair.pubkey(); + let tx = Transaction::new_signed_instructions( + &[&from_keypair, &nonce_keypair], + vec![ + nonce_instruction::nonce(&nonce_pubkey), + system_instruction::transfer(&from_pubkey, &nonce_pubkey, 42), + ], + Hash::default(), + ); + (from_pubkey, nonce_pubkey, tx) + } + + #[test] + fn tx_uses_nonce_ok() { + let (_, _, tx) = nonced_transfer_tx(); + assert!(transaction_uses_durable_nonce(&tx).is_some()); + } + + #[test] + fn tx_uses_nonce_empty_ix_fail() { + let tx = + Transaction::new_signed_instructions(&[&Keypair::new(); 0], vec![], Hash::default()); + assert!(transaction_uses_durable_nonce(&tx).is_none()); + } + + #[test] + fn tx_uses_nonce_bad_prog_id_idx_fail() { + let (_, _, mut tx) = nonced_transfer_tx(); + tx.message.instructions.get_mut(0).unwrap().program_id_index = 255u8; + assert!(transaction_uses_durable_nonce(&tx).is_none()); + } + + #[test] + fn tx_uses_nonce_first_prog_id_not_nonce_fail() { + let from_keypair = Keypair::new(); + let from_pubkey = from_keypair.pubkey(); + let nonce_keypair = Keypair::new(); + let nonce_pubkey = nonce_keypair.pubkey(); + let tx = Transaction::new_signed_instructions( + &[&from_keypair, &nonce_keypair], + vec![ + system_instruction::transfer(&from_pubkey, &nonce_pubkey, 42), + nonce_instruction::nonce(&nonce_pubkey), + ], + Hash::default(), + ); + assert!(transaction_uses_durable_nonce(&tx).is_none()); + } + + #[test] + fn tx_uses_nonce_wrong_first_nonce_ix_fail() { + let from_keypair = Keypair::new(); + let from_pubkey = from_keypair.pubkey(); + let nonce_keypair = Keypair::new(); + let nonce_pubkey = nonce_keypair.pubkey(); + let tx = Transaction::new_signed_instructions( + &[&from_keypair, &nonce_keypair], + vec![ + nonce_instruction::withdraw(&nonce_pubkey, &from_pubkey, 42), + system_instruction::transfer(&from_pubkey, &nonce_pubkey, 42), + ], + Hash::default(), + ); + assert!(transaction_uses_durable_nonce(&tx).is_none()); + } + + #[test] + fn get_nonce_pub_from_ix_ok() { + let (_, nonce_pubkey, tx) = nonced_transfer_tx(); + let nonce_ix = transaction_uses_durable_nonce(&tx).unwrap(); + assert_eq!( + get_nonce_pubkey_from_instruction(&nonce_ix, &tx), + Some(&nonce_pubkey), + ); + } + + #[test] + fn get_nonce_pub_from_ix_no_accounts_fail() { + let (_, _, tx) = nonced_transfer_tx(); + let nonce_ix = transaction_uses_durable_nonce(&tx).unwrap(); + let mut nonce_ix = nonce_ix.clone(); + nonce_ix.accounts.clear(); + assert_eq!(get_nonce_pubkey_from_instruction(&nonce_ix, &tx), None,); + } + + #[test] + fn get_nonce_pub_from_ix_bad_acc_idx_fail() { + let (_, _, tx) = nonced_transfer_tx(); + let nonce_ix = transaction_uses_durable_nonce(&tx).unwrap(); + let mut nonce_ix = nonce_ix.clone(); + nonce_ix.accounts[0] = 255u8; + assert_eq!(get_nonce_pubkey_from_instruction(&nonce_ix, &tx), None,); + } + + #[test] + fn verify_nonce_ok() { + with_test_keyed_account(42, true, |nonce_account| { + let mut signers = HashSet::new(); + signers.insert(nonce_account.signer_key().unwrap().clone()); + let state: NonceState = nonce_account.state().unwrap(); + // New is in Uninitialzed state + assert_eq!(state, NonceState::Uninitialized); + let recent_blockhashes = create_test_recent_blockhashes(0); + nonce_account + .nonce(&recent_blockhashes, &Rent::default(), &signers) + .unwrap(); + assert!(verify_nonce(&nonce_account.account, &recent_blockhashes[0])); + }); + } + + #[test] + fn verify_nonce_bad_acc_state_fail() { + with_test_keyed_account(42, true, |nonce_account| { + assert!(!verify_nonce(&nonce_account.account, &Hash::default())); + }); + } + + #[test] + fn verify_nonce_bad_query_hash_fail() { + with_test_keyed_account(42, true, |nonce_account| { + let mut signers = HashSet::new(); + signers.insert(nonce_account.signer_key().unwrap().clone()); + let state: NonceState = nonce_account.state().unwrap(); + // New is in Uninitialzed state + assert_eq!(state, NonceState::Uninitialized); + let recent_blockhashes = create_test_recent_blockhashes(0); + nonce_account + .nonce(&recent_blockhashes, &Rent::default(), &signers) + .unwrap(); + assert!(!verify_nonce( + &nonce_account.account, + &recent_blockhashes[1] + )); + }); + } +} diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index 9902067f7a..6f19d56aa6 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -45,6 +45,7 @@ serde_bytes = "0.11" serde_derive = "1.0.103" serde_json = { version = "1.0.42", optional = true } sha2 = "0.8.0" +thiserror = "1.0" ed25519-dalek = { version = "1.0.0-pre.1", optional = true } solana-crate-features = { path = "../crate-features", version = "0.22.0", optional = true } solana-logger = { path = "../logger", version = "0.22.0", optional = true } diff --git a/sdk/src/genesis_config.rs b/sdk/src/genesis_config.rs index b2a9e17276..74dc913b00 100644 --- a/sdk/src/genesis_config.rs +++ b/sdk/src/genesis_config.rs @@ -7,6 +7,7 @@ use crate::{ fee_calculator::FeeCalculator, hash::{hash, Hash}, inflation::Inflation, + nonce_program::solana_nonce_program, poh_config::PohConfig, pubkey::Pubkey, rent::Rent, @@ -52,7 +53,7 @@ pub fn create_genesis_config(lamports: u64) -> (GenesisConfig, Keypair) { faucet_keypair.pubkey(), Account::new(lamports, 0, &system_program::id()), )], - &[solana_system_program()], + &[solana_nonce_program(), solana_system_program()], ), faucet_keypair, ) diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index 1806ea1dcd..f4d8c2a2ad 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -16,6 +16,9 @@ pub mod message; pub mod move_loader; pub mod native_loader; pub mod native_token; +pub mod nonce_instruction; +pub mod nonce_program; +pub mod nonce_state; pub mod poh_config; pub mod pubkey; pub mod rent; diff --git a/sdk/src/nonce_instruction.rs b/sdk/src/nonce_instruction.rs new file mode 100644 index 0000000000..d742f0490b --- /dev/null +++ b/sdk/src/nonce_instruction.rs @@ -0,0 +1,432 @@ +use crate::{ + account::{get_signers, KeyedAccount}, + instruction::{AccountMeta, Instruction, InstructionError}, + instruction_processor_utils::{limited_deserialize, next_keyed_account, DecodeError}, + nonce_program::id, + nonce_state::{NonceAccount, NonceState}, + pubkey::Pubkey, + system_instruction, + sysvar::{ + recent_blockhashes::{self, RecentBlockhashes}, + rent::{self, Rent}, + Sysvar, + }, +}; +use num_derive::{FromPrimitive, ToPrimitive}; +use serde_derive::{Deserialize, Serialize}; +use thiserror::Error; + +#[derive(Error, Debug, Clone, PartialEq, FromPrimitive, ToPrimitive)] +pub enum NonceError { + #[error("recent blockhash list is empty")] + NoRecentBlockhashes, + #[error("stored nonce is still in recent_blockhashes")] + NotExpired, + #[error("specified nonce does not match stored nonce")] + UnexpectedValue, + #[error("cannot handle request in current account state")] + BadAccountState, +} + +impl DecodeError for NonceError { + fn type_of() -> &'static str { + "NonceError" + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] +pub enum NonceInstruction { + /// `Nonce` consumes a stored nonce, replacing it with a successor + /// + /// Expects 3 Accounts: + /// 0 - A NonceAccount + /// 1 - RecentBlockhashes sysvar + /// 2 - Rent sysvar + /// + Nonce, + + /// `Withdraw` transfers funds out of the nonce account + /// + /// Expects 4 Accounts: + /// 0 - A NonceAccount + /// 1 - A system account to which the lamports will be transferred + /// 2 - RecentBlockhashes sysvar + /// 3 - Rent sysvar + /// + /// The `u64` parameter is the lamports to withdraw, which must leave the + /// account balance above the rent exempt reserve or at zero. + Withdraw(u64), +} + +pub fn create_nonce_account( + from_pubkey: &Pubkey, + nonce_pubkey: &Pubkey, + lamports: u64, +) -> Vec { + vec![ + system_instruction::create_account( + from_pubkey, + nonce_pubkey, + lamports, + NonceState::size() as u64, + &id(), + ), + nonce(nonce_pubkey), + ] +} + +pub fn nonce(nonce_pubkey: &Pubkey) -> Instruction { + Instruction::new( + id(), + &NonceInstruction::Nonce, + vec![ + AccountMeta::new(*nonce_pubkey, true), + AccountMeta::new_readonly(recent_blockhashes::id(), false), + AccountMeta::new_readonly(rent::id(), false), + ], + ) +} + +pub fn withdraw(nonce_pubkey: &Pubkey, to_pubkey: &Pubkey, lamports: u64) -> Instruction { + Instruction::new( + id(), + &NonceInstruction::Withdraw(lamports), + vec![ + AccountMeta::new(*nonce_pubkey, true), + AccountMeta::new(*to_pubkey, false), + AccountMeta::new_readonly(recent_blockhashes::id(), false), + AccountMeta::new_readonly(rent::id(), false), + ], + ) +} + +pub fn process_instruction( + _program_id: &Pubkey, + keyed_accounts: &mut [KeyedAccount], + data: &[u8], +) -> Result<(), InstructionError> { + let signers = get_signers(keyed_accounts); + + let keyed_accounts = &mut keyed_accounts.iter_mut(); + let me = &mut next_keyed_account(keyed_accounts)?; + + match limited_deserialize(data)? { + NonceInstruction::Nonce => me.nonce( + &RecentBlockhashes::from_keyed_account(next_keyed_account(keyed_accounts)?)?, + &Rent::from_keyed_account(next_keyed_account(keyed_accounts)?)?, + &signers, + ), + NonceInstruction::Withdraw(lamports) => { + let to = &mut next_keyed_account(keyed_accounts)?; + me.withdraw( + lamports, + to, + &RecentBlockhashes::from_keyed_account(next_keyed_account(keyed_accounts)?)?, + &Rent::from_keyed_account(next_keyed_account(keyed_accounts)?)?, + &signers, + ) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{account::Account, hash::Hash, nonce_state, system_program, sysvar}; + use bincode::serialize; + + fn process_instruction(instruction: &Instruction) -> Result<(), InstructionError> { + let mut accounts: Vec<_> = instruction + .accounts + .iter() + .map(|meta| { + if sysvar::recent_blockhashes::check_id(&meta.pubkey) { + sysvar::recent_blockhashes::create_account_with_data( + 1, + vec![(0u64, &Hash::default()); 32].into_iter(), + ) + } else if sysvar::rent::check_id(&meta.pubkey) { + sysvar::rent::create_account(1, &Rent::default()) + } else { + Account::default() + } + }) + .collect(); + + { + let mut keyed_accounts: Vec<_> = instruction + .accounts + .iter() + .zip(accounts.iter_mut()) + .map(|(meta, account)| KeyedAccount::new(&meta.pubkey, meta.is_signer, account)) + .collect(); + super::process_instruction(&Pubkey::default(), &mut keyed_accounts, &instruction.data) + } + } + + #[test] + fn test_create_account() { + let from_pubkey = Pubkey::new_rand(); + let nonce_pubkey = Pubkey::new_rand(); + let ixs = create_nonce_account(&from_pubkey, &nonce_pubkey, 42); + assert_eq!(ixs.len(), 2); + let ix = &ixs[0]; + assert_eq!(ix.program_id, system_program::id()); + let pubkeys: Vec<_> = ix.accounts.iter().map(|am| am.pubkey).collect(); + assert!(pubkeys.contains(&from_pubkey)); + assert!(pubkeys.contains(&nonce_pubkey)); + } + + #[test] + fn test_process_nonce_ix_no_acc_data_fail() { + assert_eq!( + process_instruction(&nonce(&Pubkey::default(),)), + Err(InstructionError::InvalidAccountData), + ); + } + + #[test] + fn test_process_nonce_ix_no_keyed_accs_fail() { + assert_eq!( + super::process_instruction( + &Pubkey::default(), + &mut [], + &serialize(&NonceInstruction::Nonce).unwrap() + ), + Err(InstructionError::NotEnoughAccountKeys), + ); + } + + #[test] + fn test_process_nonce_ix_only_nonce_acc_fail() { + assert_eq!( + super::process_instruction( + &Pubkey::default(), + &mut [KeyedAccount::new( + &Pubkey::default(), + true, + &mut Account::default(), + ),], + &serialize(&NonceInstruction::Nonce).unwrap(), + ), + Err(InstructionError::NotEnoughAccountKeys), + ); + } + + #[test] + fn test_process_nonce_ix_bad_recent_blockhash_state_fail() { + assert_eq!( + super::process_instruction( + &Pubkey::default(), + &mut [ + KeyedAccount::new(&Pubkey::default(), true, &mut Account::default(),), + KeyedAccount::new( + &sysvar::recent_blockhashes::id(), + false, + &mut Account::default(), + ), + ], + &serialize(&NonceInstruction::Nonce).unwrap(), + ), + Err(InstructionError::InvalidArgument), + ); + } + + #[test] + fn test_process_nonce_ix_bad_rent_state_fail() { + assert_eq!( + super::process_instruction( + &Pubkey::default(), + &mut [ + KeyedAccount::new(&Pubkey::default(), true, &mut Account::default(),), + KeyedAccount::new( + &sysvar::recent_blockhashes::id(), + false, + &mut sysvar::recent_blockhashes::create_account(1), + ), + KeyedAccount::new(&sysvar::rent::id(), false, &mut Account::default(),), + ], + &serialize(&NonceInstruction::Nonce).unwrap(), + ), + Err(InstructionError::InvalidArgument), + ); + } + + #[test] + fn test_process_nonce_ix_ok() { + assert_eq!( + super::process_instruction( + &Pubkey::default(), + &mut [ + KeyedAccount::new( + &Pubkey::default(), + true, + &mut nonce_state::create_account(1_000_000), + ), + KeyedAccount::new( + &sysvar::recent_blockhashes::id(), + false, + &mut sysvar::recent_blockhashes::create_account_with_data( + 1, + vec![(0u64, &Hash::default()); 32].into_iter(), + ), + ), + KeyedAccount::new( + &sysvar::rent::id(), + false, + &mut sysvar::rent::create_account(1, &Rent::default()), + ), + ], + &serialize(&NonceInstruction::Nonce).unwrap(), + ), + Ok(()), + ); + } + + #[test] + fn test_process_withdraw_ix_no_acc_data_fail() { + assert_eq!( + process_instruction(&withdraw(&Pubkey::default(), &Pubkey::default(), 1,)), + Err(InstructionError::InvalidAccountData), + ); + } + + #[test] + fn test_process_withdraw_ix_no_keyed_accs_fail() { + assert_eq!( + super::process_instruction( + &Pubkey::default(), + &mut [], + &serialize(&NonceInstruction::Withdraw(42)).unwrap(), + ), + Err(InstructionError::NotEnoughAccountKeys), + ); + } + + #[test] + fn test_process_withdraw_ix_only_nonce_acc_fail() { + assert_eq!( + super::process_instruction( + &Pubkey::default(), + &mut [KeyedAccount::new( + &Pubkey::default(), + true, + &mut Account::default(), + ),], + &serialize(&NonceInstruction::Withdraw(42)).unwrap(), + ), + Err(InstructionError::NotEnoughAccountKeys), + ); + } + + #[test] + fn test_process_withdraw_ix_bad_recent_blockhash_state_fail() { + assert_eq!( + super::process_instruction( + &Pubkey::default(), + &mut [ + KeyedAccount::new(&Pubkey::default(), true, &mut Account::default(),), + KeyedAccount::new(&Pubkey::default(), false, &mut Account::default(),), + KeyedAccount::new( + &sysvar::recent_blockhashes::id(), + false, + &mut Account::default(), + ), + ], + &serialize(&NonceInstruction::Withdraw(42)).unwrap(), + ), + Err(InstructionError::InvalidArgument), + ); + } + + #[test] + fn test_process_withdraw_ix_bad_rent_state_fail() { + assert_eq!( + super::process_instruction( + &Pubkey::default(), + &mut [ + KeyedAccount::new(&Pubkey::default(), true, &mut Account::default(),), + KeyedAccount::new(&Pubkey::default(), false, &mut Account::default(),), + KeyedAccount::new( + &sysvar::recent_blockhashes::id(), + false, + &mut Account::default(), + ), + KeyedAccount::new(&sysvar::rent::id(), false, &mut Account::default(),), + ], + &serialize(&NonceInstruction::Withdraw(42)).unwrap(), + ), + Err(InstructionError::InvalidArgument), + ); + } + + #[test] + fn test_process_withdraw_ix_ok() { + assert_eq!( + super::process_instruction( + &Pubkey::default(), + &mut [ + KeyedAccount::new( + &Pubkey::default(), + true, + &mut nonce_state::create_account(1_000_000), + ), + KeyedAccount::new(&Pubkey::default(), true, &mut Account::default(),), + KeyedAccount::new( + &sysvar::recent_blockhashes::id(), + false, + &mut sysvar::recent_blockhashes::create_account_with_data( + 1, + vec![(0u64, &Hash::default()); 32].into_iter(), + ), + ), + KeyedAccount::new( + &sysvar::rent::id(), + false, + &mut sysvar::rent::create_account(1, &Rent::default()) + ), + ], + &serialize(&NonceInstruction::Withdraw(42)).unwrap(), + ), + Ok(()), + ); + } + + #[test] + fn test_custom_error_decode() { + use num_traits::FromPrimitive; + fn pretty_err(err: InstructionError) -> String + where + T: 'static + std::error::Error + DecodeError + FromPrimitive, + { + if let InstructionError::CustomError(code) = err { + let specific_error: T = T::decode_custom_error_to_enum(code).unwrap(); + format!( + "{:?}: {}::{:?} - {}", + err, + T::type_of(), + specific_error, + specific_error, + ) + } else { + "".to_string() + } + } + assert_eq!( + "CustomError(0): NonceError::NoRecentBlockhashes - recent blockhash list is empty", + pretty_err::(NonceError::NoRecentBlockhashes.into()) + ); + assert_eq!( + "CustomError(1): NonceError::NotExpired - stored nonce is still in recent_blockhashes", + pretty_err::(NonceError::NotExpired.into()) + ); + assert_eq!( + "CustomError(2): NonceError::UnexpectedValue - specified nonce does not match stored nonce", + pretty_err::(NonceError::UnexpectedValue.into()) + ); + assert_eq!( + "CustomError(3): NonceError::BadAccountState - cannot handle request in current account state", + pretty_err::(NonceError::BadAccountState.into()) + ); + } +} diff --git a/sdk/src/nonce_program.rs b/sdk/src/nonce_program.rs new file mode 100644 index 0000000000..7e92a9e559 --- /dev/null +++ b/sdk/src/nonce_program.rs @@ -0,0 +1,5 @@ +crate::declare_id!("Nonce11111111111111111111111111111111111111"); + +pub fn solana_nonce_program() -> (String, crate::pubkey::Pubkey) { + ("solana_nonce_program".to_string(), id()) +} diff --git a/sdk/src/nonce_state.rs b/sdk/src/nonce_state.rs new file mode 100644 index 0000000000..ccc229827f --- /dev/null +++ b/sdk/src/nonce_state.rs @@ -0,0 +1,618 @@ +use crate::{ + account::{Account, KeyedAccount}, + account_utils::State, + hash::Hash, + instruction::InstructionError, + nonce_instruction::NonceError, + nonce_program, + pubkey::Pubkey, + sysvar::recent_blockhashes::RecentBlockhashes, + sysvar::rent::Rent, +}; +use serde_derive::{Deserialize, Serialize}; +use std::collections::HashSet; + +#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Clone, Copy)] +pub struct Meta {} + +impl Meta { + pub fn new() -> Self { + Self {} + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Copy)] +pub enum NonceState { + Uninitialized, + Initialized(Meta, Hash), +} + +impl Default for NonceState { + fn default() -> Self { + NonceState::Uninitialized + } +} + +impl NonceState { + pub fn size() -> usize { + bincode::serialized_size(&NonceState::Initialized(Meta::default(), Hash::default())) + .unwrap() as usize + } +} + +pub trait NonceAccount { + fn nonce( + &mut self, + recent_blockhashes: &RecentBlockhashes, + rent: &Rent, + signers: &HashSet, + ) -> Result<(), InstructionError>; + fn withdraw( + &mut self, + lamports: u64, + to: &mut KeyedAccount, + recent_blockhashes: &RecentBlockhashes, + rent: &Rent, + signers: &HashSet, + ) -> Result<(), InstructionError>; +} + +impl<'a> NonceAccount for KeyedAccount<'a> { + fn nonce( + &mut self, + recent_blockhashes: &RecentBlockhashes, + rent: &Rent, + signers: &HashSet, + ) -> Result<(), InstructionError> { + if recent_blockhashes.is_empty() { + return Err(NonceError::NoRecentBlockhashes.into()); + } + + if !signers.contains(self.unsigned_key()) { + return Err(InstructionError::MissingRequiredSignature); + } + + let meta = match self.state()? { + NonceState::Initialized(meta, ref hash) => { + if *hash == recent_blockhashes[0] { + return Err(NonceError::NotExpired.into()); + } + meta + } + NonceState::Uninitialized => { + let min_balance = rent.minimum_balance(self.account.data.len()); + if self.account.lamports < min_balance { + return Err(InstructionError::InsufficientFunds); + } + Meta::new() + } + }; + + self.set_state(&NonceState::Initialized(meta, recent_blockhashes[0])) + } + + fn withdraw( + &mut self, + lamports: u64, + to: &mut KeyedAccount, + recent_blockhashes: &RecentBlockhashes, + rent: &Rent, + signers: &HashSet, + ) -> Result<(), InstructionError> { + if !signers.contains(self.unsigned_key()) { + return Err(InstructionError::MissingRequiredSignature); + } + + match self.state()? { + NonceState::Uninitialized => { + if lamports > self.account.lamports { + return Err(InstructionError::InsufficientFunds); + } + } + NonceState::Initialized(_meta, ref hash) => { + if lamports == self.account.lamports { + if *hash == recent_blockhashes[0] { + return Err(NonceError::NotExpired.into()); + } + self.set_state(&NonceState::Uninitialized)?; + } else { + let min_balance = rent.minimum_balance(self.account.data.len()); + if lamports + min_balance > self.account.lamports { + return Err(InstructionError::InsufficientFunds); + } + } + } + } + + self.account.lamports -= lamports; + to.account.lamports += lamports; + + Ok(()) + } +} + +pub fn create_account(lamports: u64) -> Account { + Account::new_data_with_space( + lamports, + &NonceState::Uninitialized, + NonceState::size(), + &nonce_program::id(), + ) + .expect("nonce_account") +} + +/// Convenience function for working with keyed accounts in tests +#[cfg(not(feature = "program"))] +pub fn with_test_keyed_account(lamports: u64, signer: bool, mut f: F) +where + F: FnMut(&mut KeyedAccount), +{ + let pubkey = Pubkey::new_rand(); + let mut account = create_account(lamports); + let mut keyed_account = KeyedAccount::new(&pubkey, signer, &mut account); + f(&mut keyed_account) +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{ + account::KeyedAccount, + nonce_instruction::NonceError, + sysvar::recent_blockhashes::{create_test_recent_blockhashes, RecentBlockhashes}, + }; + use std::iter::FromIterator; + + #[test] + fn default_is_uninitialized() { + assert_eq!(NonceState::default(), NonceState::Uninitialized) + } + + #[test] + fn new_meta() { + assert_eq!(Meta::new(), Meta {}); + } + + #[test] + fn keyed_account_expected_behavior() { + let rent = Rent { + lamports_per_byte_year: 42, + ..Rent::default() + }; + let min_lamports = rent.minimum_balance(NonceState::size()); + let meta = Meta::new(); + with_test_keyed_account(min_lamports + 42, true, |keyed_account| { + let mut signers = HashSet::new(); + signers.insert(keyed_account.signer_key().unwrap().clone()); + let state: NonceState = keyed_account.state().unwrap(); + // New is in Uninitialzed state + assert_eq!(state, NonceState::Uninitialized); + let recent_blockhashes = create_test_recent_blockhashes(95); + keyed_account + .nonce(&recent_blockhashes, &rent, &signers) + .unwrap(); + let state: NonceState = keyed_account.state().unwrap(); + let stored = recent_blockhashes[0]; + // First nonce instruction drives state from Uninitialized to Initialized + assert_eq!(state, NonceState::Initialized(meta, stored)); + let recent_blockhashes = create_test_recent_blockhashes(63); + keyed_account + .nonce(&recent_blockhashes, &rent, &signers) + .unwrap(); + let state: NonceState = keyed_account.state().unwrap(); + let stored = recent_blockhashes[0]; + // Second nonce instruction consumes and replaces stored nonce + assert_eq!(state, NonceState::Initialized(meta, stored)); + let recent_blockhashes = create_test_recent_blockhashes(31); + keyed_account + .nonce(&recent_blockhashes, &rent, &signers) + .unwrap(); + let state: NonceState = keyed_account.state().unwrap(); + let stored = recent_blockhashes[0]; + // Third nonce instruction for fun and profit + assert_eq!(state, NonceState::Initialized(meta, stored)); + with_test_keyed_account(42, false, |mut to_keyed| { + let recent_blockhashes = create_test_recent_blockhashes(0); + let withdraw_lamports = keyed_account.account.lamports; + let expect_nonce_lamports = keyed_account.account.lamports - withdraw_lamports; + let expect_to_lamports = to_keyed.account.lamports + withdraw_lamports; + keyed_account + .withdraw( + withdraw_lamports, + &mut to_keyed, + &recent_blockhashes, + &rent, + &signers, + ) + .unwrap(); + let state: NonceState = keyed_account.state().unwrap(); + // Withdraw instruction... + // Deinitializes NonceAccount state + assert_eq!(state, NonceState::Uninitialized); + // Empties NonceAccount balance + assert_eq!(keyed_account.account.lamports, expect_nonce_lamports); + // NonceAccount balance goes to `to` + assert_eq!(to_keyed.account.lamports, expect_to_lamports); + }) + }) + } + + #[test] + fn nonce_inx_uninitialized_account_not_signer_fail() { + let rent = Rent { + lamports_per_byte_year: 42, + ..Rent::default() + }; + let min_lamports = rent.minimum_balance(NonceState::size()); + with_test_keyed_account(min_lamports + 42, false, |nonce_account| { + let signers = HashSet::new(); + let recent_blockhashes = create_test_recent_blockhashes(0); + let result = nonce_account.nonce(&recent_blockhashes, &rent, &signers); + assert_eq!(result, Err(InstructionError::MissingRequiredSignature),); + }) + } + + #[test] + fn nonce_inx_initialized_account_not_signer_fail() { + let rent = Rent { + lamports_per_byte_year: 42, + ..Rent::default() + }; + let min_lamports = rent.minimum_balance(NonceState::size()); + let meta = Meta::new(); + with_test_keyed_account(min_lamports + 42, true, |nonce_account| { + let mut signers = HashSet::new(); + signers.insert(nonce_account.signer_key().unwrap().clone()); + let recent_blockhashes = create_test_recent_blockhashes(31); + let stored = recent_blockhashes[0]; + nonce_account + .nonce(&recent_blockhashes, &rent, &signers) + .unwrap(); + let pubkey = nonce_account.account.owner.clone(); + let mut nonce_account = KeyedAccount::new(&pubkey, false, nonce_account.account); + let state: NonceState = nonce_account.state().unwrap(); + assert_eq!(state, NonceState::Initialized(meta, stored)); + let signers = HashSet::new(); + let recent_blockhashes = create_test_recent_blockhashes(0); + let result = nonce_account.nonce(&recent_blockhashes, &rent, &signers); + assert_eq!(result, Err(InstructionError::MissingRequiredSignature),); + }) + } + + #[test] + fn nonce_inx_with_empty_recent_blockhashes_fail() { + let rent = Rent { + lamports_per_byte_year: 42, + ..Rent::default() + }; + let min_lamports = rent.minimum_balance(NonceState::size()); + with_test_keyed_account(min_lamports + 42, true, |keyed_account| { + let mut signers = HashSet::new(); + signers.insert(keyed_account.signer_key().unwrap().clone()); + let recent_blockhashes = RecentBlockhashes::from_iter(vec![].into_iter()); + let result = keyed_account.nonce(&recent_blockhashes, &rent, &signers); + assert_eq!(result, Err(NonceError::NoRecentBlockhashes.into())); + }) + } + + #[test] + fn nonce_inx_too_early_fail() { + let rent = Rent { + lamports_per_byte_year: 42, + ..Rent::default() + }; + let min_lamports = rent.minimum_balance(NonceState::size()); + with_test_keyed_account(min_lamports + 42, true, |keyed_account| { + let mut signers = HashSet::new(); + signers.insert(keyed_account.signer_key().unwrap().clone()); + let recent_blockhashes = create_test_recent_blockhashes(63); + keyed_account + .nonce(&recent_blockhashes, &rent, &signers) + .unwrap(); + let result = keyed_account.nonce(&recent_blockhashes, &rent, &signers); + assert_eq!(result, Err(NonceError::NotExpired.into())); + }) + } + + #[test] + fn nonce_inx_uninitialized_acc_insuff_funds_fail() { + let rent = Rent { + lamports_per_byte_year: 42, + ..Rent::default() + }; + let min_lamports = rent.minimum_balance(NonceState::size()); + with_test_keyed_account(min_lamports - 42, true, |keyed_account| { + let mut signers = HashSet::new(); + signers.insert(keyed_account.signer_key().unwrap().clone()); + let recent_blockhashes = create_test_recent_blockhashes(63); + let result = keyed_account.nonce(&recent_blockhashes, &rent, &signers); + assert_eq!(result, Err(InstructionError::InsufficientFunds)); + }) + } + + #[test] + fn withdraw_inx_unintialized_acc_ok() { + let rent = Rent { + lamports_per_byte_year: 42, + ..Rent::default() + }; + let min_lamports = rent.minimum_balance(NonceState::size()); + with_test_keyed_account(min_lamports + 42, true, |nonce_keyed| { + let state: NonceState = nonce_keyed.state().unwrap(); + assert_eq!(state, NonceState::Uninitialized); + with_test_keyed_account(42, false, |mut to_keyed| { + let mut signers = HashSet::new(); + signers.insert(nonce_keyed.signer_key().unwrap().clone()); + let recent_blockhashes = create_test_recent_blockhashes(0); + let withdraw_lamports = nonce_keyed.account.lamports; + let expect_nonce_lamports = nonce_keyed.account.lamports - withdraw_lamports; + let expect_to_lamports = to_keyed.account.lamports + withdraw_lamports; + nonce_keyed + .withdraw( + withdraw_lamports, + &mut to_keyed, + &recent_blockhashes, + &rent, + &signers, + ) + .unwrap(); + let state: NonceState = nonce_keyed.state().unwrap(); + // Withdraw instruction... + // Deinitializes NonceAccount state + assert_eq!(state, NonceState::Uninitialized); + // Empties NonceAccount balance + assert_eq!(nonce_keyed.account.lamports, expect_nonce_lamports); + // NonceAccount balance goes to `to` + assert_eq!(to_keyed.account.lamports, expect_to_lamports); + }) + }) + } + + #[test] + fn withdraw_inx_unintialized_acc_unsigned_fail() { + let rent = Rent { + lamports_per_byte_year: 42, + ..Rent::default() + }; + let min_lamports = rent.minimum_balance(NonceState::size()); + with_test_keyed_account(min_lamports + 42, false, |nonce_keyed| { + let state: NonceState = nonce_keyed.state().unwrap(); + assert_eq!(state, NonceState::Uninitialized); + with_test_keyed_account(42, false, |mut to_keyed| { + let signers = HashSet::new(); + let recent_blockhashes = create_test_recent_blockhashes(0); + let result = nonce_keyed.withdraw( + nonce_keyed.account.lamports, + &mut to_keyed, + &recent_blockhashes, + &rent, + &signers, + ); + assert_eq!(result, Err(InstructionError::MissingRequiredSignature),); + }) + }) + } + + #[test] + fn withdraw_inx_unintialized_acc_insuff_funds_fail() { + let rent = Rent { + lamports_per_byte_year: 42, + ..Rent::default() + }; + let min_lamports = rent.minimum_balance(NonceState::size()); + with_test_keyed_account(min_lamports + 42, true, |nonce_keyed| { + let state: NonceState = nonce_keyed.state().unwrap(); + assert_eq!(state, NonceState::Uninitialized); + with_test_keyed_account(42, false, |mut to_keyed| { + let mut signers = HashSet::new(); + signers.insert(nonce_keyed.signer_key().unwrap().clone()); + let recent_blockhashes = create_test_recent_blockhashes(0); + let result = nonce_keyed.withdraw( + nonce_keyed.account.lamports + 1, + &mut to_keyed, + &recent_blockhashes, + &rent, + &signers, + ); + assert_eq!(result, Err(InstructionError::InsufficientFunds)); + }) + }) + } + + #[test] + fn withdraw_inx_uninitialized_acc_two_withdraws_ok() { + let rent = Rent { + lamports_per_byte_year: 42, + ..Rent::default() + }; + let min_lamports = rent.minimum_balance(NonceState::size()); + with_test_keyed_account(min_lamports + 42, true, |nonce_keyed| { + with_test_keyed_account(42, false, |mut to_keyed| { + let mut signers = HashSet::new(); + signers.insert(nonce_keyed.signer_key().unwrap().clone()); + let recent_blockhashes = create_test_recent_blockhashes(0); + let withdraw_lamports = nonce_keyed.account.lamports / 2; + let nonce_expect_lamports = nonce_keyed.account.lamports - withdraw_lamports; + let to_expect_lamports = to_keyed.account.lamports + withdraw_lamports; + nonce_keyed + .withdraw( + withdraw_lamports, + &mut to_keyed, + &recent_blockhashes, + &rent, + &signers, + ) + .unwrap(); + let state: NonceState = nonce_keyed.state().unwrap(); + assert_eq!(state, NonceState::Uninitialized); + assert_eq!(nonce_keyed.account.lamports, nonce_expect_lamports); + assert_eq!(to_keyed.account.lamports, to_expect_lamports); + let withdraw_lamports = nonce_keyed.account.lamports; + let nonce_expect_lamports = nonce_keyed.account.lamports - withdraw_lamports; + let to_expect_lamports = to_keyed.account.lamports + withdraw_lamports; + nonce_keyed + .withdraw( + withdraw_lamports, + &mut to_keyed, + &recent_blockhashes, + &rent, + &signers, + ) + .unwrap(); + let state: NonceState = nonce_keyed.state().unwrap(); + assert_eq!(state, NonceState::Uninitialized); + assert_eq!(nonce_keyed.account.lamports, nonce_expect_lamports); + assert_eq!(to_keyed.account.lamports, to_expect_lamports); + }) + }) + } + + #[test] + fn withdraw_inx_initialized_acc_two_withdraws_ok() { + let rent = Rent { + lamports_per_byte_year: 42, + ..Rent::default() + }; + let min_lamports = rent.minimum_balance(NonceState::size()); + let meta = Meta::new(); + with_test_keyed_account(min_lamports + 42, true, |nonce_keyed| { + let mut signers = HashSet::new(); + signers.insert(nonce_keyed.signer_key().unwrap().clone()); + let recent_blockhashes = create_test_recent_blockhashes(31); + nonce_keyed + .nonce(&recent_blockhashes, &rent, &signers) + .unwrap(); + let state: NonceState = nonce_keyed.state().unwrap(); + let stored = recent_blockhashes[0]; + assert_eq!(state, NonceState::Initialized(meta, stored)); + with_test_keyed_account(42, false, |mut to_keyed| { + let withdraw_lamports = nonce_keyed.account.lamports - min_lamports; + let nonce_expect_lamports = nonce_keyed.account.lamports - withdraw_lamports; + let to_expect_lamports = to_keyed.account.lamports + withdraw_lamports; + nonce_keyed + .withdraw( + withdraw_lamports, + &mut to_keyed, + &recent_blockhashes, + &rent, + &signers, + ) + .unwrap(); + let state: NonceState = nonce_keyed.state().unwrap(); + let stored = recent_blockhashes[0]; + assert_eq!(state, NonceState::Initialized(meta, stored)); + assert_eq!(nonce_keyed.account.lamports, nonce_expect_lamports); + assert_eq!(to_keyed.account.lamports, to_expect_lamports); + let recent_blockhashes = create_test_recent_blockhashes(0); + let withdraw_lamports = nonce_keyed.account.lamports; + let nonce_expect_lamports = nonce_keyed.account.lamports - withdraw_lamports; + let to_expect_lamports = to_keyed.account.lamports + withdraw_lamports; + nonce_keyed + .withdraw( + withdraw_lamports, + &mut to_keyed, + &recent_blockhashes, + &rent, + &signers, + ) + .unwrap(); + let state: NonceState = nonce_keyed.state().unwrap(); + assert_eq!(state, NonceState::Uninitialized); + assert_eq!(nonce_keyed.account.lamports, nonce_expect_lamports); + assert_eq!(to_keyed.account.lamports, to_expect_lamports); + }) + }) + } + + #[test] + fn withdraw_inx_initialized_acc_nonce_too_early_fail() { + let rent = Rent { + lamports_per_byte_year: 42, + ..Rent::default() + }; + let min_lamports = rent.minimum_balance(NonceState::size()); + with_test_keyed_account(min_lamports + 42, true, |nonce_keyed| { + let mut signers = HashSet::new(); + signers.insert(nonce_keyed.signer_key().unwrap().clone()); + let recent_blockhashes = create_test_recent_blockhashes(0); + nonce_keyed + .nonce(&recent_blockhashes, &rent, &signers) + .unwrap(); + with_test_keyed_account(42, false, |mut to_keyed| { + let mut signers = HashSet::new(); + signers.insert(nonce_keyed.signer_key().unwrap().clone()); + let withdraw_lamports = nonce_keyed.account.lamports; + let result = nonce_keyed.withdraw( + withdraw_lamports, + &mut to_keyed, + &recent_blockhashes, + &rent, + &signers, + ); + assert_eq!(result, Err(NonceError::NotExpired.into())); + }) + }) + } + + #[test] + fn withdraw_inx_initialized_acc_insuff_funds_fail() { + let rent = Rent { + lamports_per_byte_year: 42, + ..Rent::default() + }; + let min_lamports = rent.minimum_balance(NonceState::size()); + with_test_keyed_account(min_lamports + 42, true, |nonce_keyed| { + let mut signers = HashSet::new(); + signers.insert(nonce_keyed.signer_key().unwrap().clone()); + let recent_blockhashes = create_test_recent_blockhashes(95); + nonce_keyed + .nonce(&recent_blockhashes, &rent, &signers) + .unwrap(); + with_test_keyed_account(42, false, |mut to_keyed| { + let recent_blockhashes = create_test_recent_blockhashes(63); + let mut signers = HashSet::new(); + signers.insert(nonce_keyed.signer_key().unwrap().clone()); + let withdraw_lamports = nonce_keyed.account.lamports + 1; + let result = nonce_keyed.withdraw( + withdraw_lamports, + &mut to_keyed, + &recent_blockhashes, + &rent, + &signers, + ); + assert_eq!(result, Err(InstructionError::InsufficientFunds)); + }) + }) + } + + #[test] + fn withdraw_inx_initialized_acc_insuff_rent_fail() { + let rent = Rent { + lamports_per_byte_year: 42, + ..Rent::default() + }; + let min_lamports = rent.minimum_balance(NonceState::size()); + with_test_keyed_account(min_lamports + 42, true, |nonce_keyed| { + let mut signers = HashSet::new(); + signers.insert(nonce_keyed.signer_key().unwrap().clone()); + let recent_blockhashes = create_test_recent_blockhashes(95); + nonce_keyed + .nonce(&recent_blockhashes, &rent, &signers) + .unwrap(); + with_test_keyed_account(42, false, |mut to_keyed| { + let recent_blockhashes = create_test_recent_blockhashes(63); + let mut signers = HashSet::new(); + signers.insert(nonce_keyed.signer_key().unwrap().clone()); + let withdraw_lamports = nonce_keyed.account.lamports - min_lamports + 1; + let result = nonce_keyed.withdraw( + withdraw_lamports, + &mut to_keyed, + &recent_blockhashes, + &rent, + &signers, + ); + assert_eq!(result, Err(InstructionError::InsufficientFunds)); + }) + }) + } +} diff --git a/sdk/src/sysvar/recent_blockhashes.rs b/sdk/src/sysvar/recent_blockhashes.rs index 6640995242..89ed5cd40f 100644 --- a/sdk/src/sysvar/recent_blockhashes.rs +++ b/sdk/src/sysvar/recent_blockhashes.rs @@ -1,4 +1,9 @@ -use crate::{account::Account, hash::Hash, sysvar::Sysvar}; +use crate::{ + account::Account, + hash::{hash, Hash}, + sysvar::Sysvar, +}; +use bincode::serialize; use std::collections::BinaryHeap; use std::iter::FromIterator; use std::ops::Deref; @@ -69,6 +74,13 @@ where account } +pub fn create_test_recent_blockhashes(start: usize) -> RecentBlockhashes { + let bhq: Vec<_> = (start..start + (MAX_ENTRIES - 1)) + .map(|i| hash(&serialize(&i).unwrap())) + .collect(); + RecentBlockhashes::from_iter(bhq.iter()) +} + #[cfg(test)] mod tests { use super::*;