diff --git a/src/args/commit_state.rs b/src/args/commit_state.rs index a0deb03d..d205749a 100644 --- a/src/args/commit_state.rs +++ b/src/args/commit_state.rs @@ -2,6 +2,21 @@ use std::mem::size_of; use borsh::{BorshDeserialize, BorshSerialize}; +#[derive(Default, Debug, BorshSerialize, BorshDeserialize)] +pub struct CommitFinalizeArgs { + /// "Nonce" of an account. Updates are submitted historically and nonce incremented by 1 + /// Deprecated: The ephemeral slot at which the account data is committed + pub nonce: u64, + /// The lamports that the account holds in the ephemeral validator + pub lamports: u64, + /// Whether the account can be undelegated after the commit completes + pub allow_undelegation: u8, + /// Whether the account can be undelegated after the commit completes + pub data_is_diff: u8, + /// The account data + pub data: Vec, +} + #[derive(Default, Debug, BorshSerialize, BorshDeserialize)] pub struct CommitStateArgs { /// "Nonce" of an account. Updates are submitted historically and nonce incremented by 1 diff --git a/src/discriminator.rs b/src/discriminator.rs index a286a4a5..fb4b1024 100644 --- a/src/discriminator.rs +++ b/src/discriminator.rs @@ -39,6 +39,8 @@ pub enum DlpDiscriminator { CommitDiff = 16, /// See [crate::processor::process_commit_diff_from_buffer] for docs. CommitDiffFromBuffer = 17, + /// See [crate::processor::process_commit_finalize] for docs. + CommitFinalize = 18, } impl DlpDiscriminator { diff --git a/src/instruction_builder/commit_finalize.rs b/src/instruction_builder/commit_finalize.rs new file mode 100644 index 00000000..a7617ba2 --- /dev/null +++ b/src/instruction_builder/commit_finalize.rs @@ -0,0 +1,59 @@ +use borsh::to_vec; +use solana_program::instruction::Instruction; +use solana_program::system_program; +use solana_program::{instruction::AccountMeta, pubkey::Pubkey}; + +use crate::args::CommitFinalizeArgs; +use crate::discriminator::DlpDiscriminator; +use crate::pda::{ + delegation_metadata_pda_from_delegated_account, delegation_record_pda_from_delegated_account, + program_config_from_program_id, validator_fees_vault_pda_from_validator, +}; +use crate::{total_size_budget, AccountSizeClass, DLP_PROGRAM_DATA_SIZE_CLASS}; + +/// Builds a commit finalize instruction. +/// See [crate::processor::process_commit_finalize] for docs. +pub fn commit_finalize( + validator: Pubkey, + delegated_account: Pubkey, + delegated_account_owner: Pubkey, + commit_args: CommitFinalizeArgs, +) -> Instruction { + let commit_args = to_vec(&commit_args).unwrap(); + let delegation_record_pda = delegation_record_pda_from_delegated_account(&delegated_account); + let validator_fees_vault_pda = validator_fees_vault_pda_from_validator(&validator); + let delegation_metadata_pda = + delegation_metadata_pda_from_delegated_account(&delegated_account); + let program_config_pda = program_config_from_program_id(&delegated_account_owner); + Instruction { + program_id: crate::id(), + accounts: vec![ + AccountMeta::new_readonly(validator, true), + AccountMeta::new(delegated_account, false), + AccountMeta::new_readonly(delegation_record_pda, false), + AccountMeta::new(delegation_metadata_pda, false), + AccountMeta::new_readonly(validator_fees_vault_pda, false), + AccountMeta::new_readonly(program_config_pda, false), + AccountMeta::new_readonly(system_program::id(), false), + ], + data: [DlpDiscriminator::CommitFinalize.to_vec(), commit_args].concat(), + } +} + +/// +/// Returns accounts-data-size budget for commit_state instruction. +/// +/// This value can be used with ComputeBudgetInstruction::SetLoadedAccountsDataSizeLimit +/// +pub fn commit_finalize_size_budget(delegated_account: AccountSizeClass) -> u32 { + total_size_budget(&[ + DLP_PROGRAM_DATA_SIZE_CLASS, + AccountSizeClass::Tiny, // validator + delegated_account, // delegated_account + AccountSizeClass::Tiny, // delegation_record_pda + AccountSizeClass::Tiny, // delegation_metadata_pda + AccountSizeClass::Tiny, // validator_fees_vault_pda + AccountSizeClass::Tiny, // program_config_pda + AccountSizeClass::Tiny, // system_program + ]) +} diff --git a/src/instruction_builder/mod.rs b/src/instruction_builder/mod.rs index 2007a420..85234ca4 100644 --- a/src/instruction_builder/mod.rs +++ b/src/instruction_builder/mod.rs @@ -3,6 +3,7 @@ mod close_ephemeral_balance; mod close_validator_fees_vault; mod commit_diff; mod commit_diff_from_buffer; +mod commit_finalize; mod commit_state; mod commit_state_from_buffer; mod delegate; @@ -21,6 +22,7 @@ pub use close_ephemeral_balance::*; pub use close_validator_fees_vault::*; pub use commit_diff::*; pub use commit_diff_from_buffer::*; +pub use commit_finalize::*; pub use commit_state::*; pub use commit_state_from_buffer::*; pub use delegate::*; diff --git a/src/lib.rs b/src/lib.rs index 44a4cc93..da5de211 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -113,6 +113,9 @@ pub fn fast_process_instruction( DlpDiscriminator::CommitDiffFromBuffer => Some( processor::fast::process_commit_diff_from_buffer(program_id, accounts, data), ), + DlpDiscriminator::CommitFinalize => Some(processor::fast::process_commit_finalize( + program_id, accounts, data, + )), DlpDiscriminator::Finalize => Some(processor::fast::process_finalize( program_id, accounts, data, )), diff --git a/src/processor/fast/commit_finalize.rs b/src/processor/fast/commit_finalize.rs new file mode 100644 index 00000000..3853872e --- /dev/null +++ b/src/processor/fast/commit_finalize.rs @@ -0,0 +1,83 @@ +use borsh::BorshDeserialize; +use pinocchio::{ + account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey, ProgramResult, +}; +use pinocchio_log::log; + +use crate::args::CommitFinalizeArgs; +use crate::processor::fast::commit_finalize_internal::{ + process_commit_finalize_internal, CommitFinalizeInternalArgs, +}; +use crate::processor::fast::NewState; +use crate::{require_n_accounts, DiffSet}; + +/// Commit a new state of a delegated PDA +/// +/// Accounts: +/// +/// 0: `[signer]` the validator requesting the commit +/// 1: `[]` the delegated account +/// 2: `[writable]` the PDA storing the new state +/// 3: `[writable]` the PDA storing the commit record +/// 4: `[]` the delegation record +/// 5: `[writable]` the delegation metadata +/// 6: `[]` the validator fees vault +/// 7: `[]` the program config account +/// +/// Requirements: +/// +/// - delegation record is initialized +/// - delegation metadata is initialized +/// - validator fees vault is initialized +/// - program config is initialized +/// - commit state is uninitialized +/// - commit record is uninitialized +/// - delegated account holds at least the lamports indicated in the delegation record +/// - account was not committed at a later slot +/// +/// Steps: +/// 1. Check that the pda is delegated +/// 2. Init a new PDA to store the new state +/// 3. Copy the new state to the new PDA +/// 4. Init a new PDA to store the record of the new state commitment +pub fn process_commit_finalize( + _program_id: &Pubkey, + accounts: &[AccountInfo], + data: &[u8], +) -> ProgramResult { + let [ + validator, // force multi-line + delegated_account, + delegation_record_account, + delegation_metadata_account, + validator_fees_vault, + program_config_account, + _system_program, + ] = require_n_accounts!(accounts, 7); + + let args = CommitFinalizeArgs::try_from_slice(data).map_err(|_| ProgramError::BorshIoError)?; + + let commit_args = CommitFinalizeInternalArgs { + new_state: match args.data_is_diff { + 0 => NewState::FullBytes(&args.data), + 1 => { + let diffset = DiffSet::try_new(&args.data)?; + if diffset.segments_count() == 0 { + log!("WARN: noop; empty diff sent"); + } + NewState::Diff(diffset) + } + _ => return Err(ProgramError::InvalidInstructionData), + }, + commit_record_nonce: args.nonce, + allow_undelegation: args.allow_undelegation == 1, + validator, + delegated_account, + delegation_record_account, + delegation_metadata_account, + validator_fees_vault, + program_config_account, + }; + + process_commit_finalize_internal(commit_args) +} diff --git a/src/processor/fast/commit_finalize_internal.rs b/src/processor/fast/commit_finalize_internal.rs new file mode 100644 index 00000000..e2fe8323 --- /dev/null +++ b/src/processor/fast/commit_finalize_internal.rs @@ -0,0 +1,153 @@ +use pinocchio::pubkey::{self, pubkey_eq}; +use pinocchio::{account_info::AccountInfo, program_error::ProgramError}; +use pinocchio_log::log; + +use crate::apply_diff_in_place; +use crate::error::DlpError; +use crate::processor::fast::utils::requires::{ + require_initialized_delegation_metadata, require_initialized_delegation_record, + require_initialized_validator_fees_vault, require_owned_pda, require_program_config, + require_signer, +}; +use crate::processor::fast::NewState; +use crate::state::{DelegationMetadata, DelegationRecord, ProgramConfig}; + +use super::to_pinocchio_program_error; + +/// Arguments for the commit state internal function +pub(crate) struct CommitFinalizeInternalArgs<'a> { + pub(crate) new_state: NewState<'a>, + pub(crate) commit_record_nonce: u64, + pub(crate) allow_undelegation: bool, + pub(crate) validator: &'a AccountInfo, + pub(crate) delegated_account: &'a AccountInfo, + pub(crate) delegation_record_account: &'a AccountInfo, + pub(crate) delegation_metadata_account: &'a AccountInfo, + pub(crate) validator_fees_vault: &'a AccountInfo, + pub(crate) program_config_account: &'a AccountInfo, +} + +/// Commit a new state of a delegated Pda +pub(crate) fn process_commit_finalize_internal( + args: CommitFinalizeInternalArgs, +) -> Result<(), ProgramError> { + // Check that the origin account is delegated + require_owned_pda( + args.delegated_account, + &crate::fast::ID, + "delegated account", + )?; + require_signer(args.validator, "validator account")?; + require_initialized_delegation_record( + args.delegated_account, + args.delegation_record_account, + false, + )?; + require_initialized_delegation_metadata( + args.delegated_account, + args.delegation_metadata_account, + true, + )?; + require_initialized_validator_fees_vault(args.validator, args.validator_fees_vault, false)?; + + // Read delegation metadata + let mut delegation_metadata_data = args.delegation_metadata_account.try_borrow_mut_data()?; + let mut delegation_metadata = + DelegationMetadata::try_from_bytes_with_discriminator(&delegation_metadata_data) + .map_err(to_pinocchio_program_error)?; + + // To preserve correct history of account updates we require sequential commits + if args.commit_record_nonce != delegation_metadata.last_update_nonce + 1 { + log!( + "Nonce {} is incorrect, previous nonce is {}. Rejecting commit", + args.commit_record_nonce, + delegation_metadata.last_update_nonce + ); + return Err(DlpError::NonceOutOfOrder.into()); + } + + // Once the account is marked as undelegatable, any subsequent commit should fail + if delegation_metadata.is_undelegatable { + log!("delegation metadata is already undelegated: "); + pubkey::log(args.delegation_metadata_account.key()); + return Err(DlpError::AlreadyUndelegated.into()); + } + + // Update delegation metadata undelegation flag + delegation_metadata.is_undelegatable = args.allow_undelegation; + delegation_metadata + .to_bytes_with_discriminator(&mut delegation_metadata_data.as_mut()) + .map_err(to_pinocchio_program_error)?; + + // Load delegation record + let delegation_record_data = args.delegation_record_account.try_borrow_data()?; + let delegation_record = + DelegationRecord::try_from_bytes_with_discriminator(&delegation_record_data) + .map_err(to_pinocchio_program_error)?; + + // Check that the authority is allowed to commit + if !pubkey_eq(delegation_record.authority.as_array(), args.validator.key()) { + log!("validator is not the delegation authority. validator: "); + pubkey::log(args.validator.key()); + log!("delegation authority: "); + pubkey::log(delegation_record.authority.as_array()); + return Err(DlpError::InvalidAuthority.into()); + } + + // If there was an issue with the lamport accounting in the past, abort (this should never happen) + if args.delegated_account.lamports() < delegation_record.lamports { + log!( + "delegated account has less lamports than the delegation record indicates. delegation account: "); + pubkey::log(args.delegated_account.key()); + return Err(DlpError::InvalidDelegatedState.into()); + } + + // If committed lamports are more than the previous lamports balance, deposit the difference in the commitment account + // If committed lamports are less than the previous lamports balance, we have collateral to settle the balance at state finalization + // We need to do that so that the finalizer already have all the lamports from the validators ready at finalize time + // The finalizer can return any extra lamport to the validator during finalize, but this acts as the validator's proof of collateral + // if args.commit_record_lamports > delegation_record.lamports { + // system::Transfer { + // from: args.validator, + // to: args.commit_state_account, + // lamports: args.commit_record_lamports - delegation_record.lamports, + // } + // .invoke()?; + // } + + // Load the program configuration and validate it, if any + let has_program_config = require_program_config( + args.program_config_account, + delegation_record.owner.as_array(), + false, + )?; + if has_program_config { + let program_config_data = args.program_config_account.try_borrow_data()?; + + let program_config = ProgramConfig::try_from_bytes_with_discriminator(&program_config_data) + .map_err(to_pinocchio_program_error)?; + if !program_config + .approved_validators + .contains(&(*args.validator.key()).into()) + { + log!("validator is not whitelisted in the program config: "); + pubkey::log(args.validator.key()); + return Err(DlpError::InvalidWhitelistProgramConfig.into()); + } + } + + args.delegated_account.resize(args.new_state.data_len())?; + + // Copy the new state to the initialized PDA + let mut delegated_account_data = args.delegated_account.try_borrow_mut_data()?; + match args.new_state { + NewState::FullBytes(bytes) => (*delegated_account_data).copy_from_slice(bytes), + NewState::Diff(diff) => { + apply_diff_in_place(&mut delegated_account_data, &diff)?; + } + } + + // TODO - Add additional validation for the commitment, e.g. sufficient validator stake + + Ok(()) +} diff --git a/src/processor/fast/commit_state.rs b/src/processor/fast/commit_state.rs index f7322584..44d1ce0a 100644 --- a/src/processor/fast/commit_state.rs +++ b/src/processor/fast/commit_state.rs @@ -184,6 +184,9 @@ pub(crate) fn process_commit_state_internal( return Err(DlpError::InvalidAuthority.into()); } + // TODO (snawaz): what exactly is ensured here? why can't the delegated_account's lamports be + // different? + // // If there was an issue with the lamport accounting in the past, abort (this should never happen) if args.delegated_account.lamports() < delegation_record.lamports { log!( @@ -197,15 +200,12 @@ pub(crate) fn process_commit_state_internal( // We need to do that so that the finalizer already have all the lamports from the validators ready at finalize time // The finalizer can return any extra lamport to the validator during finalize, but this acts as the validator's proof of collateral if args.commit_record_lamports > delegation_record.lamports { - let extra_lamports = args - .commit_record_lamports - .checked_sub(delegation_record.lamports) - .ok_or(DlpError::Overflow)?; - + // TODO (snawaz): commit_state_account does not exist yet. So how do we transfer lamports + // to non-existent account? we can do that when we create it? system::Transfer { from: args.validator, to: args.commit_state_account, - lamports: extra_lamports, + lamports: args.commit_record_lamports - delegation_record.lamports, } .invoke()?; } diff --git a/src/processor/fast/finalize.rs b/src/processor/fast/finalize.rs index a38acaf1..e3c0cb31 100644 --- a/src/processor/fast/finalize.rs +++ b/src/processor/fast/finalize.rs @@ -132,6 +132,7 @@ pub fn process_finalize( // Copying the new commit state to the delegated account delegated_account.resize(commit_state_data.len())?; + let mut delegated_account_data = delegated_account.try_borrow_mut_data()?; (*delegated_account_data).copy_from_slice(&commit_state_data); diff --git a/src/processor/fast/mod.rs b/src/processor/fast/mod.rs index 94ad5cfd..e03b4d63 100644 --- a/src/processor/fast/mod.rs +++ b/src/processor/fast/mod.rs @@ -1,5 +1,7 @@ mod commit_diff; mod commit_diff_from_buffer; +mod commit_finalize; +mod commit_finalize_internal; mod commit_state; mod commit_state_from_buffer; mod delegate; @@ -9,6 +11,7 @@ mod utils; pub use commit_diff::*; pub use commit_diff_from_buffer::*; +pub use commit_finalize::*; pub use commit_state::*; pub use commit_state_from_buffer::*; pub use delegate::*; diff --git a/tests/integration/Anchor.toml b/tests/integration/Anchor.toml index cf1be308..5421f880 100644 --- a/tests/integration/Anchor.toml +++ b/tests/integration/Anchor.toml @@ -11,7 +11,7 @@ test_delegation = "3vAK9JQiDsKoQNwmcfeEng4Cnv22pYuj1ASfso7U4ukF" url = "https://api.apr.dev" [provider] -cluster = "Localnet" +cluster = "localnet" wallet = "./tests/fixtures/provider.json" [workspace] diff --git a/tests/integration/tests/test-delegation.ts b/tests/integration/tests/test-delegation.ts index c27f1bfd..bab4394e 100644 --- a/tests/integration/tests/test-delegation.ts +++ b/tests/integration/tests/test-delegation.ts @@ -17,6 +17,7 @@ const BPF_LOADER = new web3.PublicKey( ); describe("TestDelegation", () => { + console.log("ANCHOR_PROVIDER_URL: ", process.env.ANCHOR_PROVIDER_URL); // Configure the client to use the local cluster. const provider = anchor.AnchorProvider.env(); anchor.setProvider(provider); @@ -115,15 +116,22 @@ describe("TestDelegation", () => { }); // .skip() because currently tests are not independent and we cannot run two similar tests twice or more. - it.skip("Delegate one PDA", async () => { + it.only("Delegate one PDA", async () => { + console.log("delegate one PDA started: ", pda.toBase58()); + console.log("before getHealth"); + const health = await provider.connection.getVersion(); + console.log("after getHealth", health); const counterAccountInfo = await provider.connection.getAccountInfo(pda); + console.log("delegate one PDA", counterAccountInfo); if (counterAccountInfo === null) { + console.log("delegate one PDA started"); const tx = await testDelegation.methods .initialize() .accounts({ user: provider.wallet.publicKey, }) - .rpc({ skipPreflight: true }); + .rpc({ skipPreflight: false }); + console.log("delegate one PDA started"); console.log("Init Pda Tx: ", tx); } @@ -207,7 +215,7 @@ describe("TestDelegation", () => { console.log("Your transaction signature", txSign); }); - it("Commit a new state to the PDA", async () => { + it.only("Commit a new state to the PDA", async () => { let account = await provider.connection.getAccountInfo(pda); let new_data = account.data; new_data[-1] = (new_data[-1] + 1) % 256; diff --git a/tests/test_commit_finalize.rs b/tests/test_commit_finalize.rs new file mode 100644 index 00000000..f1f2c272 --- /dev/null +++ b/tests/test_commit_finalize.rs @@ -0,0 +1,176 @@ +use dlp::args::{CommitFinalizeArgs, CommitStateArgs}; +use dlp::pda::{ + delegation_metadata_pda_from_delegated_account, delegation_record_pda_from_delegated_account, + validator_fees_vault_pda_from_validator, +}; +use dlp::state::DelegationMetadata; +use solana_program::rent::Rent; +use solana_program::{hash::Hash, native_token::LAMPORTS_PER_SOL, system_program}; +use solana_program_test::{BanksClient, ProgramTest}; +use solana_sdk::{ + account::Account, + signature::{Keypair, Signer}, + transaction::Transaction, +}; + +use crate::fixtures::{ + get_delegation_metadata_data, get_delegation_record_data, DELEGATED_PDA_ID, + DELEGATED_PDA_OWNER_ID, TEST_AUTHORITY, +}; + +mod fixtures; + +#[tokio::test] +async fn test_commit_finalize() { + // Setup + let (banks, _, authority, blockhash) = setup_program_test_env().await; + let new_state = vec![0, 1, 2, 9, 9, 9, 6, 7, 8, 9]; + + let new_account_balance = 1_000_000; + let commit_args = CommitFinalizeArgs { + data: new_state.clone(), + nonce: 1, + allow_undelegation: 1, + data_is_diff: 0, + lamports: new_account_balance, + }; + + // Commit the state for the delegated account + let ix = dlp::instruction_builder::commit_finalize( + authority.pubkey(), + DELEGATED_PDA_ID, + DELEGATED_PDA_OWNER_ID, + commit_args, + ); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); + let res = banks.process_transaction(tx).await; + println!("{:?}", res); + assert!(res.is_ok()); + + let delegated_account = banks.get_account(DELEGATED_PDA_ID).await.unwrap().unwrap(); + assert_eq!(delegated_account.data, new_state); + + let delegation_metadata_pda = delegation_metadata_pda_from_delegated_account(&DELEGATED_PDA_ID); + let delegation_metadata_account = banks + .get_account(delegation_metadata_pda) + .await + .unwrap() + .unwrap(); + let delegation_metadata = + DelegationMetadata::try_from_bytes_with_discriminator(&delegation_metadata_account.data) + .unwrap(); + assert_eq!(delegation_metadata.is_undelegatable, true); +} + +#[tokio::test] +async fn test_commit_out_of_order() { + const OUTDATED_SLOT_ERR_MSG: &str = + "transport transaction error: Error processing Instruction 0: custom program error: 0xc"; + + // Setup + let (banks, _, authority, blockhash) = setup_program_test_env().await; + let new_state = vec![0, 1, 2, 9, 9, 9, 6, 7, 8, 9]; + + let new_account_balance = 1_000_000; + let commit_args = CommitStateArgs { + data: new_state.clone(), + nonce: 101, + allow_undelegation: true, + lamports: new_account_balance, + }; + + // Commit the state for the delegated account + let ix = dlp::instruction_builder::commit_state( + authority.pubkey(), + DELEGATED_PDA_ID, + DELEGATED_PDA_OWNER_ID, + commit_args, + ); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert_eq!( + res.unwrap_err().to_string(), + OUTDATED_SLOT_ERR_MSG.to_string() + ); +} + +async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { + let mut program_test = ProgramTest::new("dlp", dlp::ID, None); + program_test.prefer_bpf(true); + + let validator_keypair = Keypair::from_bytes(&TEST_AUTHORITY).unwrap(); + + program_test.add_account( + validator_keypair.pubkey(), + Account { + lamports: 10 * LAMPORTS_PER_SOL, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + + // Setup a delegated PDA + program_test.add_account( + DELEGATED_PDA_ID, + Account { + lamports: LAMPORTS_PER_SOL, + data: vec![], + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); + + // Setup the delegated account metadata PDA + let delegation_metadata_data = get_delegation_metadata_data(validator_keypair.pubkey(), None); + program_test.add_account( + delegation_metadata_pda_from_delegated_account(&DELEGATED_PDA_ID), + Account { + lamports: Rent::default().minimum_balance(delegation_metadata_data.len()), + data: delegation_metadata_data, + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); + + // Setup the delegated record PDA + let delegation_record_data = get_delegation_record_data(validator_keypair.pubkey(), None); + program_test.add_account( + delegation_record_pda_from_delegated_account(&DELEGATED_PDA_ID), + Account { + lamports: Rent::default().minimum_balance(delegation_record_data.len()), + data: delegation_record_data, + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); + + // Setup the validator fees vault + program_test.add_account( + validator_fees_vault_pda_from_validator(&validator_keypair.pubkey()), + Account { + lamports: LAMPORTS_PER_SOL, + data: vec![], + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); + + let (banks, payer, blockhash) = program_test.start().await; + (banks, payer, validator_keypair, blockhash) +}