diff --git a/Cargo.lock b/Cargo.lock index 86a292c765..5e541d89ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4707,6 +4707,7 @@ dependencies = [ "solana-program 1.6.0", "solana-runtime", "solana-sdk", + "thiserror", "tokio 0.3.5", ] diff --git a/banks-client/src/lib.rs b/banks-client/src/lib.rs index 9826eb8f24..c112ab3da2 100644 --- a/banks-client/src/lib.rs +++ b/banks-client/src/lib.rs @@ -289,7 +289,10 @@ pub async fn start_tcp_client(addr: T) -> io::Result) { + fn run(bank_forks: Arc>, transaction_receiver: Receiver) { while let Ok(info) = transaction_receiver.recv() { let mut transaction_infos = vec![info]; while let Ok(info) = transaction_receiver.try_recv() { @@ -72,21 +72,28 @@ impl BanksServer { .into_iter() .map(|info| deserialize(&info.wire_transaction).unwrap()) .collect(); + let bank = bank_forks.read().unwrap().working_bank(); let _ = bank.process_transactions(&transactions); } } /// Useful for unit-testing - fn new_loopback(bank_forks: Arc>) -> Self { + fn new_loopback( + bank_forks: Arc>, + block_commitment_cache: Arc>, + ) -> Self { let (transaction_sender, transaction_receiver) = channel(); let bank = bank_forks.read().unwrap().working_bank(); let slot = bank.slot(); - let block_commitment_cache = Arc::new(RwLock::new( - BlockCommitmentCache::new_for_tests_with_slots(slot, slot), - )); + { + // ensure that the commitment cache and bank are synced + let mut w_block_commitment_cache = block_commitment_cache.write().unwrap(); + w_block_commitment_cache.set_all_slots(slot, slot); + } + let server_bank_forks = bank_forks.clone(); Builder::new() .name("solana-bank-forks-client".to_string()) - .spawn(move || Self::run(&bank, transaction_receiver)) + .spawn(move || Self::run(server_bank_forks, transaction_receiver)) .unwrap(); Self::new(bank_forks, block_commitment_cache, transaction_sender) } @@ -240,9 +247,10 @@ impl Banks for BanksServer { } pub async fn start_local_server( - bank_forks: &Arc>, + bank_forks: Arc>, + block_commitment_cache: Arc>, ) -> UnboundedChannel, ClientMessage> { - let banks_server = BanksServer::new_loopback(bank_forks.clone()); + let banks_server = BanksServer::new_loopback(bank_forks, block_commitment_cache); let (client_transport, server_transport) = transport::channel::unbounded(); let server = server::new(server::Config::default()) .incoming(stream::once(future::ready(server_transport))) diff --git a/program-test/Cargo.toml b/program-test/Cargo.toml index dcbba6ac3d..738a630346 100644 --- a/program-test/Cargo.toml +++ b/program-test/Cargo.toml @@ -21,4 +21,5 @@ solana-logger = { path = "../logger", version = "1.6.0" } solana-program = { path = "../sdk/program", version = "1.6.0" } solana-runtime = { path = "../runtime", version = "1.6.0" } solana-sdk = { path = "../sdk", version = "1.6.0" } +thiserror = "1.0" tokio = { version = "0.3.5", features = ["full"] } diff --git a/program-test/src/lib.rs b/program-test/src/lib.rs index d3f86687aa..abc88edd8a 100644 --- a/program-test/src/lib.rs +++ b/program-test/src/lib.rs @@ -15,10 +15,12 @@ use { solana_runtime::{ bank::{Bank, Builtin, ExecuteTimings}, bank_forks::BankForks, + commitment::BlockCommitmentCache, genesis_utils::create_genesis_config_with_leader, }, solana_sdk::{ account::Account, + clock::Slot, genesis_config::GenesisConfig, keyed_account::KeyedAccount, process_instruction::{ @@ -41,6 +43,7 @@ use { }, time::{Duration, Instant}, }, + thiserror::Error, tokio::task::JoinHandle, }; @@ -70,6 +73,14 @@ pub fn to_instruction_error(error: ProgramError) -> InstructionError { } } +/// Errors from the program test environment +#[derive(Error, Debug, PartialEq)] +pub enum ProgramTestError { + /// The chosen warp slot is not in the future, so warp is not performed + #[error("Warp slot not in the future")] + InvalidWarpSlot, +} + thread_local! { static INVOKE_CONTEXT: RefCell> = RefCell::new(None); } @@ -577,7 +588,15 @@ impl ProgramTest { } } - fn setup_bank(&self) -> (Arc>, Keypair, Hash, GenesisConfig) { + fn setup_bank( + &self, + ) -> ( + Arc>, + Arc>, + Keypair, + Hash, + GenesisConfig, + ) { { use std::sync::Once; static ONCE: Once = Once::new(); @@ -640,15 +659,27 @@ impl ProgramTest { })); } let bank = setup_fee_calculator(bank); + let slot = bank.slot(); let last_blockhash = bank.last_blockhash(); let bank_forks = Arc::new(RwLock::new(BankForks::new(bank))); + let block_commitment_cache = Arc::new(RwLock::new( + BlockCommitmentCache::new_for_tests_with_slots(slot, slot), + )); - (bank_forks, payer, last_blockhash, genesis_config) + ( + bank_forks, + block_commitment_cache, + payer, + last_blockhash, + genesis_config, + ) } pub async fn start(self) -> (BanksClient, Keypair, Hash) { - let (bank_forks, payer, last_blockhash, genesis_config) = self.setup_bank(); - let transport = start_local_server(&bank_forks).await; + let (bank_forks, block_commitment_cache, payer, last_blockhash, genesis_config) = + self.setup_bank(); + let transport = + start_local_server(bank_forks.clone(), block_commitment_cache.clone()).await; let banks_client = start_client(transport) .await .unwrap_or_else(|err| panic!("Failed to start banks client: {}", err)); @@ -675,14 +706,17 @@ impl ProgramTest { /// Returns a `BanksClient` interface into the test environment as well as a payer `Keypair` /// with SOL for sending transactions pub async fn start_with_context(self) -> ProgramTestContext { - let (bank_forks, payer, last_blockhash, genesis_config) = self.setup_bank(); - let transport = start_local_server(&bank_forks).await; + let (bank_forks, block_commitment_cache, payer, last_blockhash, genesis_config) = + self.setup_bank(); + let transport = + start_local_server(bank_forks.clone(), block_commitment_cache.clone()).await; let banks_client = start_client(transport) .await .unwrap_or_else(|err| panic!("Failed to start banks client: {}", err)); ProgramTestContext::new( bank_forks, + block_commitment_cache, banks_client, payer, last_blockhash, @@ -740,12 +774,15 @@ pub struct ProgramTestContext { pub banks_client: BanksClient, pub payer: Keypair, pub last_blockhash: Hash, + bank_forks: Arc>, + block_commitment_cache: Arc>, _bank_task: DroppableTask<()>, } impl ProgramTestContext { fn new( bank_forks: Arc>, + block_commitment_cache: Arc>, banks_client: BanksClient, payer: Keypair, last_blockhash: Hash, @@ -754,6 +791,7 @@ impl ProgramTestContext { // Run a simulated PohService to provide the client with new blockhashes. New blockhashes // are required when sending multiple otherwise identical transactions in series from a // test + let running_bank_forks = bank_forks.clone(); let target_tick_duration = genesis_config.poh_config.target_tick_duration; let exit = Arc::new(AtomicBool::new(false)); let bank_task = DroppableTask( @@ -763,7 +801,7 @@ impl ProgramTestContext { if exit.load(Ordering::Relaxed) { break; } - bank_forks + running_bank_forks .read() .unwrap() .working_bank() @@ -775,9 +813,58 @@ impl ProgramTestContext { Self { banks_client, + block_commitment_cache, payer, last_blockhash, + bank_forks, _bank_task: bank_task, } } + + /// Force the working bank ahead to a new slot + pub fn warp_to_slot(&mut self, warp_slot: Slot) -> Result<(), ProgramTestError> { + let mut bank_forks = self.bank_forks.write().unwrap(); + let bank = bank_forks.working_bank(); + + // Force ticks until a new blockhash, otherwise retried transactions will have + // the same signature + let last_blockhash = bank.last_blockhash(); + while last_blockhash == bank.last_blockhash() { + bank.register_tick(&Hash::new_unique()); + } + + // warp ahead to one slot *before* the desired slot because the warped + // bank is frozen + let working_slot = bank.slot(); + if warp_slot <= working_slot { + return Err(ProgramTestError::InvalidWarpSlot); + } + let pre_warp_slot = warp_slot - 1; + let warp_bank = bank_forks.insert(Bank::warp_from_parent( + &bank, + &Pubkey::default(), + pre_warp_slot, + )); + bank_forks.set_root( + pre_warp_slot, + &solana_runtime::accounts_background_service::ABSRequestSender::default(), + Some(warp_slot), + ); + + // warp bank is frozen, so go forward one slot from it + bank_forks.insert(Bank::new_from_parent( + &warp_bank, + &Pubkey::default(), + warp_slot, + )); + + // Update block commitment cache, otherwise banks server will poll at + // the wrong slot + let mut w_block_commitment_cache = self.block_commitment_cache.write().unwrap(); + w_block_commitment_cache.set_all_slots(pre_warp_slot, warp_slot); + + let bank = bank_forks.working_bank(); + self.last_blockhash = bank.last_blockhash(); + Ok(()) + } } diff --git a/program-test/tests/fuzz.rs b/program-test/tests/fuzz.rs index e8aa09f379..a72278c47a 100644 --- a/program-test/tests/fuzz.rs +++ b/program-test/tests/fuzz.rs @@ -16,7 +16,6 @@ fn process_instruction( _accounts: &[AccountInfo], _input: &[u8], ) -> ProgramResult { - // if we can call `msg!` successfully, then InvokeContext exists as required msg!("Processing instruction"); Ok(()) } diff --git a/program-test/tests/warp.rs b/program-test/tests/warp.rs new file mode 100644 index 0000000000..5fd2e0ee2a --- /dev/null +++ b/program-test/tests/warp.rs @@ -0,0 +1,97 @@ +use { + solana_program::{ + account_info::{next_account_info, AccountInfo}, + clock::Clock, + entrypoint::ProgramResult, + instruction::{AccountMeta, Instruction, InstructionError}, + program_error::ProgramError, + pubkey::Pubkey, + sysvar::{clock, Sysvar}, + }, + solana_program_test::{processor, ProgramTest, ProgramTestError}, + solana_sdk::{ + signature::Signer, + transaction::{Transaction, TransactionError}, + }, + std::convert::TryInto, +}; + +// Use a big number to be sure that we get the right error +const WRONG_SLOT_ERROR: u32 = 123456; + +fn process_instruction( + _program_id: &Pubkey, + accounts: &[AccountInfo], + input: &[u8], +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + let clock_info = next_account_info(account_info_iter)?; + let clock = &Clock::from_account_info(clock_info)?; + let expected_slot = u64::from_le_bytes(input.try_into().unwrap()); + if clock.slot == expected_slot { + Ok(()) + } else { + Err(ProgramError::Custom(WRONG_SLOT_ERROR)) + } +} + +#[tokio::test] +async fn custom_warp() { + let program_id = Pubkey::new_unique(); + // Initialize and start the test network + let program_test = ProgramTest::new( + "program-test-warp", + program_id, + processor!(process_instruction), + ); + + let mut context = program_test.start_with_context().await; + let expected_slot = 5_000_000; + let instruction = Instruction::new( + program_id, + &expected_slot, + vec![AccountMeta::new_readonly(clock::id(), false)], + ); + + // Fail transaction + let transaction = Transaction::new_signed_with_payer( + &[instruction.clone()], + Some(&context.payer.pubkey()), + &[&context.payer], + context.last_blockhash, + ); + assert_eq!( + context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(), + TransactionError::InstructionError(0, InstructionError::Custom(WRONG_SLOT_ERROR)) + ); + + // Warp to success! + context.warp_to_slot(expected_slot).unwrap(); + let instruction = Instruction::new( + program_id, + &expected_slot, + vec![AccountMeta::new_readonly(clock::id(), false)], + ); + let transaction = Transaction::new_signed_with_payer( + &[instruction], + Some(&context.payer.pubkey()), + &[&context.payer], + context.last_blockhash, + ); + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + + // Try warping again to the same slot + assert_eq!( + context.warp_to_slot(expected_slot).unwrap_err(), + ProgramTestError::InvalidWarpSlot, + ); +} diff --git a/runtime/src/commitment.rs b/runtime/src/commitment.rs index f3ff4cdf1d..4fd7840023 100644 --- a/runtime/src/commitment.rs +++ b/runtime/src/commitment.rs @@ -195,6 +195,13 @@ impl BlockCommitmentCache { self.commitment_slots.slot = slot; self.commitment_slots.root = slot; } + + pub fn set_all_slots(&mut self, slot: Slot, root: Slot) { + self.commitment_slots.slot = slot; + self.commitment_slots.highest_confirmed_slot = slot; + self.commitment_slots.root = root; + self.commitment_slots.highest_confirmed_root = root; + } } #[derive(Default, Clone, Copy)]