diff --git a/src/instruction_builder/mod.rs b/src/instruction_builder/mod.rs index 14a9623f..8d2393f1 100644 --- a/src/instruction_builder/mod.rs +++ b/src/instruction_builder/mod.rs @@ -42,8 +42,12 @@ pub use undelegate_confined_account::*; pub use validator_claim_fees::*; pub use whitelist_validator_for_program::*; +mod v2_commit; mod v2_commit_finalize; mod v2_commit_finalize_from_buffer; +mod v2_commit_from_buffer; +pub use v2_commit::*; pub use v2_commit_finalize::*; pub use v2_commit_finalize_from_buffer::*; +pub use v2_commit_from_buffer::*; diff --git a/src/instruction_builder/v2_commit.rs b/src/instruction_builder/v2_commit.rs new file mode 100644 index 00000000..4194fbf3 --- /dev/null +++ b/src/instruction_builder/v2_commit.rs @@ -0,0 +1,84 @@ +use solana_program::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + system_program, +}; + +use crate::{ + discriminator::DlpDiscriminator, + pda::{ + commit_record_pda_from_delegated_account, + commit_state_pda_from_delegated_account, + delegation_metadata_pda_from_delegated_account, + delegation_record_pda_from_delegated_account, + program_config_from_program_id, + validator_fees_vault_pda_from_validator, + }, + pod_view::PodView, + total_size_budget, + v2::CommitArgs, + AccountSizeClass, DLP_PROGRAM_DATA_SIZE_CLASS, +}; + +/// Builds a commit state instruction. +/// See [crate::processor::process_commit_state] for docs. +pub fn v2_commit( + validator: Pubkey, + delegated_account: Pubkey, + delegated_account_owner: Pubkey, + commit_args: CommitArgs, + state_or_diff: &[u8], +) -> Instruction { + let delegation_record_pda = + delegation_record_pda_from_delegated_account(&delegated_account); + let commit_state_pda = + commit_state_pda_from_delegated_account(&delegated_account); + let commit_record_pda = + commit_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_readonly(delegated_account, false), + AccountMeta::new(commit_state_pda, false), + AccountMeta::new(commit_record_pda, 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::CommitState.to_vec(), + commit_args.to_bytes(), + state_or_diff.to_vec(), + ] + .concat(), + } +} + +/// +/// Returns accounts-data-size budget for commit_state instruction. +/// +/// This value can be used with ComputeBudgetInstruction::SetLoadedAccountsDataSizeLimit +/// +pub fn v2_commit_size_budget(delegated_account: AccountSizeClass) -> u32 { + total_size_budget(&[ + DLP_PROGRAM_DATA_SIZE_CLASS, + AccountSizeClass::Tiny, // validator + delegated_account, // delegated_account + delegated_account, // commit_state_pda + AccountSizeClass::Tiny, // commit_record_pda + 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/v2_commit_from_buffer.rs b/src/instruction_builder/v2_commit_from_buffer.rs new file mode 100644 index 00000000..9a7b0d93 --- /dev/null +++ b/src/instruction_builder/v2_commit_from_buffer.rs @@ -0,0 +1,87 @@ +use solana_program::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + system_program, +}; + +use crate::{ + discriminator::DlpDiscriminator, + pda::{ + commit_record_pda_from_delegated_account, + commit_state_pda_from_delegated_account, + delegation_metadata_pda_from_delegated_account, + delegation_record_pda_from_delegated_account, + program_config_from_program_id, + validator_fees_vault_pda_from_validator, + }, + pod_view::PodView, + total_size_budget, + v2::CommitArgs, + AccountSizeClass, DLP_PROGRAM_DATA_SIZE_CLASS, +}; + +/// Builds a commit state from buffer instruction. +/// See [crate::processor::process_commit_state_from_buffer] for docs. +pub fn v2_commit_from_buffer( + validator: Pubkey, + delegated_account: Pubkey, + delegated_account_owner: Pubkey, + commit_state_buffer: Pubkey, + commit_args: CommitArgs, +) -> Instruction { + let delegation_record_pda = + delegation_record_pda_from_delegated_account(&delegated_account); + let commit_state_pda = + commit_state_pda_from_delegated_account(&delegated_account); + let commit_record_pda = + commit_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_readonly(delegated_account, false), + AccountMeta::new(commit_state_pda, false), + AccountMeta::new(commit_record_pda, false), + AccountMeta::new_readonly(delegation_record_pda, false), + AccountMeta::new(delegation_metadata_pda, false), + AccountMeta::new_readonly(commit_state_buffer, 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::CommitStateFromBuffer.to_vec(), + commit_args.to_bytes(), + ] + .concat(), + } +} + +/// +/// Returns accounts-data-size budget for commit_state_from_buffer instruction. +/// +/// This value can be used with ComputeBudgetInstruction::SetLoadedAccountsDataSizeLimit +/// +pub fn v2_commit_from_buffer_size_budget( + delegated_account: AccountSizeClass, +) -> u32 { + total_size_budget(&[ + DLP_PROGRAM_DATA_SIZE_CLASS, + AccountSizeClass::Tiny, // validator + delegated_account, // delegated_account + delegated_account, // commit_state_pda + AccountSizeClass::Tiny, // commit_record_pda + AccountSizeClass::Tiny, // delegation_record_pda + AccountSizeClass::Tiny, // delegation_metadata_pda + delegated_account, // commit_state_buffer + AccountSizeClass::Tiny, // validator_fees_vault_pda + AccountSizeClass::Tiny, // program_config_pda + AccountSizeClass::Tiny, // system_program + ]) +} diff --git a/src/v2/instruction.rs b/src/v2/instruction.rs index 4146589e..50a42288 100644 --- a/src/v2/instruction.rs +++ b/src/v2/instruction.rs @@ -121,6 +121,8 @@ pub const IX_TABLE: [Processor; 256] = { table[DelegateWithAnyValidator.index()] = process_delegate_with_any_validator; // Commit group + table[Commit.index()] = process_commit; + table[CommitFromBuffer.index()] = process_commit_from_buffer; table[CommitFinalize.index()] = process_commit_finalize; table[CommitFinalizeFromBuffer.index()] = process_commit_finalize_from_buffer; table[CommitFinalizeInline.index()] = |accounts, data| { diff --git a/src/v2/processor/commit.rs b/src/v2/processor/commit.rs new file mode 100644 index 00000000..375869b5 --- /dev/null +++ b/src/v2/processor/commit.rs @@ -0,0 +1,94 @@ +use bytemuck::{Pod, Zeroable}; +use pinocchio::{AccountView, ProgramResult}; +use pinocchio_log::log; + +use crate::{ + args::Boolean, + processor::fast::NewState, + require_n_accounts, + v2::{ + processor::internal::{process_commit_internal, CommitInternalArgs}, + HeaderWithBuffer, + }, + DiffSet, +}; + +/// Commit a new state, or a diff, directly to the delegated account. Unlike, CommitState and +/// CommitDiff variants, this instruction does not write to any temporary account first. In other +/// words, this instruction commits and finalizes both. +/// +/// Accounts: +/// +/// 0: `[signer]` the validator requesting the commit +/// 1: `[]` the delegated account +/// 2: `[]` the delegation record +/// 3: `[writable]` the delegation metadata +/// 4: `[]` the validator fees vault +/// 5: `[]` the program config account +/// 6: `[]` system program +/// +/// Instruction Data: CommitFinalizeArgsWithBuffer +/// + +#[repr(C)] +#[derive(Copy, Clone, Pod, Zeroable)] +pub struct CommitArgs { + /// the commit_id ensures correct ordering of commits + pub commit_id: u64, + + /// the lamports that the delegated account holds in the ephemeral validator + pub lamports: u64, + + /// whether the account can be undelegated after the commit completes + pub allow_undelegation: Boolean, + + /// whether the data (in the ixdata or in the data account) is diff or full state. + pub data_is_diff: Boolean, + + pub reserved_padding: [u8; 6], +} + +/// buffer is the diff-data or the full-bytes data +pub type CommitArgsWithBuffer<'a> = HeaderWithBuffer<'a, CommitArgs>; + +pub fn process_commit(accounts: &[AccountView], data: &[u8]) -> ProgramResult { + let [ + validator, // force multi-line + delegated_account, + delegation_state, + validator_fees_vault, + _system_program, + ] = require_n_accounts!(accounts, 5); + + let args = CommitArgsWithBuffer::from_bytes(data)?; + + if args.data_is_diff.is_true() { + let commit_args = CommitInternalArgs { + new_state: { + let diffset = DiffSet::try_new(args.buffer)?; + if diffset.segments_count() == 0 { + log!("WARN: noop; empty diff sent"); + } + NewState::Diff(diffset) + }, + commit_id: args.commit_id, + allow_undelegation: args.allow_undelegation.is_true(), + validator, + delegated_account, + delegation_state, + validator_fees_vault, + }; + process_commit_internal(commit_args) + } else { + let commit_args = CommitInternalArgs { + new_state: NewState::FullBytes(args.buffer), + commit_id: args.commit_id, + allow_undelegation: args.allow_undelegation.is_true(), + validator, + delegated_account, + delegation_state, + validator_fees_vault, + }; + process_commit_internal(commit_args) + } +} diff --git a/src/v2/processor/commit_from_buffer.rs b/src/v2/processor/commit_from_buffer.rs new file mode 100644 index 00000000..da94baed --- /dev/null +++ b/src/v2/processor/commit_from_buffer.rs @@ -0,0 +1,75 @@ +use pinocchio::{AccountView, ProgramResult}; +use pinocchio_log::log; + +use crate::{ + pod_view::PodView, + processor::fast::NewState, + require_n_accounts, + v2::{ + processor::internal::{process_commit_internal, CommitInternalArgs}, + CommitArgs, + }, + DiffSet, +}; + +/// Just like Commit, commit a new state, or a diff, directly to the delegated account. Unlike Commit, the state or diff comes from a buffer account. +/// +/// Accounts: +/// +/// 0: `[signer]` the validator requesting the commit +/// 1: `[]` the delegated account +/// 2: `[]` the delegation record +/// 3: `[writable]` the delegation metadata +/// 4: `[]` the validator fees vault +/// 5: `[]` the program config account +/// 6: `[]` system program +/// +/// Instruction Data: CommitArgs +/// +pub fn process_commit_from_buffer( + accounts: &[AccountView], + data: &[u8], +) -> ProgramResult { + let [ + validator, // force multi-line + delegated_account, + delegation_state, + buffer_account, + validator_fees_vault, + _system_program, + ] = require_n_accounts!(accounts, 6); + + let args = CommitArgs::try_view_from(data)?; + + let data = buffer_account.try_borrow()?; + + if args.data_is_diff.is_true() { + let commit_args = CommitInternalArgs { + new_state: { + let diffset = DiffSet::try_new(&data)?; + if diffset.segments_count() == 0 { + log!("WARN: noop; empty diff sent"); + } + NewState::Diff(diffset) + }, + commit_id: args.commit_id, + allow_undelegation: args.allow_undelegation.is_true(), + validator, + delegated_account, + delegation_state, + validator_fees_vault, + }; + process_commit_internal(commit_args) + } else { + let commit_args = CommitInternalArgs { + new_state: NewState::FullBytes(&data), + commit_id: args.commit_id, + allow_undelegation: args.allow_undelegation.is_true(), + validator, + delegated_account, + delegation_state, + validator_fees_vault, + }; + process_commit_internal(commit_args) + } +} diff --git a/src/v2/processor/internal/commit_internal.rs b/src/v2/processor/internal/commit_internal.rs new file mode 100644 index 00000000..135381ab --- /dev/null +++ b/src/v2/processor/internal/commit_internal.rs @@ -0,0 +1,125 @@ +use pinocchio::{error::ProgramError, AccountView}; +use pinocchio_log::log; + +use crate::{ + apply_diff_in_place, error::DlpError, pod_view::PodView, + processor::fast::NewState, v2::DelegationStateHeader, v2_require, + v2_require_eq, v2_require_eq_keys, v2_require_ge, v2_require_owned_by, + v2_require_signer, +}; + +/// Arguments for the commit state internal function +pub(crate) struct CommitInternalArgs<'a> { + pub(crate) new_state: NewState<'a>, + pub(crate) commit_id: u64, + pub(crate) allow_undelegation: bool, + pub(crate) validator: &'a AccountView, + pub(crate) delegated_account: &'a AccountView, + pub(crate) delegation_state: &'a AccountView, + pub(crate) validator_fees_vault: &'a AccountView, +} + +#[inline(always)] +pub(crate) fn process_commit_internal( + args: CommitInternalArgs, +) -> Result<(), ProgramError> { + // check delegated_account is actually delegated to the DLP + v2_require_signer!(args.validator); + + //// we do not really need to check this as the write will fail anyway + // v2_require_owned_by!(args.delegated_account, &crate::fast::ID); + + //DelegationStateHeader::validate(args.delegation_state)?; + + v2_require_owned_by!(args.delegation_state, &crate::fast::ID); + + #[cfg(feature = "use-unsafe")] + let state_data = unsafe { args.delegation_state.borrow_unchecked_mut() }; + + #[cfg(not(feature = "use-unsafe"))] + let mut state_data = args.delegation_state.try_borrow_mut()?; + + v2_require_ge!( + state_data.len(), + DelegationStateHeader::SPACE, + ProgramError::InvalidAccountData + ); + + let (header, _) = state_data.split_at_mut(DelegationStateHeader::SPACE); + + let state_view = + unsafe { &mut *(header.as_mut_ptr() as *mut DelegationStateHeader) }; + + v2_require_eq!( + //unsafe { (header.as_ptr() as *const u64).read() }, + u64::from_le_bytes(state_view.discriminator), + DelegationStateHeader::DISCRIMINATOR_FAST, + ProgramError::InvalidAccountData + ); + + // If there was an issue with the lamport accounting in the past, abort (this should never happen) + v2_require_ge!( + args.delegated_account.lamports(), + state_view.original_lamports, + DlpError::InvalidDelegatedState + ); + + v2_require_eq!( + args.commit_id, + state_view.last_commit_id + 1, + DlpError::NonceOutOfOrder + ); + + v2_require!( + state_view.is_undelegatable.is_false(), + DlpError::NonceOutOfOrder + ); + + state_view.last_commit_id = args.commit_id; + state_view.is_undelegatable = args.allow_undelegation.into(); + + // key comparision cost: 12 CU + v2_require_eq_keys!( + &state_view.bindings.delegated_account, + args.delegated_account.address(), + DlpError::InvalidAuthority + ); + + v2_require_eq_keys!( + &state_view.bindings.validator_as_authority, + args.validator.address(), + DlpError::InvalidAuthority + ); + + if true { + // TODO (snawaz): why do we need validator_fees_vault here? + v2_require_eq_keys!( + &state_view.bindings.validator_fees_vault, + args.validator_fees_vault.address(), + DlpError::InvalidAuthority + ); + } + + if args.delegated_account.data_len() != args.new_state.data_len() { + args.delegated_account.resize(args.new_state.data_len())?; + } + + // copy the new state to the delegated account + #[cfg(feature = "use-unsafe")] + let mut delegated_account_data = + unsafe { args.delegated_account.borrow_unchecked_mut() }; + + #[cfg(not(feature = "use-unsafe"))] + let mut delegated_account_data = args.delegated_account.try_borrow_mut()?; + + 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)?; + } + } + + Ok(()) +} diff --git a/src/v2/processor/internal/mod.rs b/src/v2/processor/internal/mod.rs index 67adbde4..8dc98663 100644 --- a/src/v2/processor/internal/mod.rs +++ b/src/v2/processor/internal/mod.rs @@ -1,7 +1,9 @@ mod commit_finalize_inline_internal; mod commit_finalize_internal; +mod commit_internal; mod delegate_internal; pub(crate) use commit_finalize_inline_internal::*; pub(crate) use commit_finalize_internal::*; +pub(crate) use commit_internal::*; pub(crate) use delegate_internal::*; diff --git a/src/v2/processor/mod.rs b/src/v2/processor/mod.rs index 8df8d9cb..e0b02121 100644 --- a/src/v2/processor/mod.rs +++ b/src/v2/processor/mod.rs @@ -1,16 +1,20 @@ +mod commit; mod commit_finalize; mod commit_finalize_from_buffer; mod commit_finalize_inline; mod commit_finalize_inline_from_buffer; mod commit_finalize_inline_resize; mod commit_finalize_inline_resize_from_buffer; +mod commit_from_buffer; mod delegate; mod internal; +pub use commit::*; pub use commit_finalize::*; pub use commit_finalize_from_buffer::*; pub use commit_finalize_inline::*; pub use commit_finalize_inline_from_buffer::*; pub use commit_finalize_inline_resize::*; pub use commit_finalize_inline_resize_from_buffer::*; +pub use commit_from_buffer::*; pub use delegate::*; diff --git a/tests/test_v2_commit.rs b/tests/test_v2_commit.rs new file mode 100644 index 00000000..f1a986c8 --- /dev/null +++ b/tests/test_v2_commit.rs @@ -0,0 +1,213 @@ +use dlp::{ + args::CommitStateArgs, + pda::{ + commit_record_pda_from_delegated_account, + commit_state_pda_from_delegated_account, + delegation_metadata_pda_from_delegated_account, + delegation_record_pda_from_delegated_account, + validator_fees_vault_pda_from_validator, + }, + state::{CommitRecord, DelegationMetadata}, +}; +use solana_program::{ + hash::Hash, native_token::LAMPORTS_PER_SOL, rent::Rent, 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_v2_commit_perf() { + // 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: 1, + 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; + println!("{:?}", res); + assert!(res.is_ok()); + + // Assert the state commitment was created and contains the new state + let commit_state_pda = + commit_state_pda_from_delegated_account(&DELEGATED_PDA_ID); + let commit_state_account = + banks.get_account(commit_state_pda).await.unwrap().unwrap(); + assert_eq!(commit_state_account.data, new_state.clone()); + + // Check that the commit has enough collateral to finalize the proposed state diff + let delegated_account = + banks.get_account(DELEGATED_PDA_ID).await.unwrap().unwrap(); + assert!( + new_account_balance + < commit_state_account.lamports + delegated_account.lamports + ); + + // Assert the record about the commitment exists + let commit_record_pda = + commit_record_pda_from_delegated_account(&DELEGATED_PDA_ID); + let commit_record_account = + banks.get_account(commit_record_pda).await.unwrap().unwrap(); + let commit_record = CommitRecord::try_from_bytes_with_discriminator( + &commit_record_account.data, + ) + .unwrap(); + assert_eq!(commit_record.account, DELEGATED_PDA_ID); + assert_eq!(commit_record.identity, authority.pubkey()); + assert_eq!(commit_record.nonce, 1); + + 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!(delegation_metadata.is_undelegatable); +} + +#[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) +}