diff --git a/dlp-api/src/args/mod.rs b/dlp-api/src/args/mod.rs index 492f02f4..fd02dbc0 100644 --- a/dlp-api/src/args/mod.rs +++ b/dlp-api/src/args/mod.rs @@ -3,6 +3,7 @@ mod commit_state; mod delegate; mod delegate_ephemeral_balance; mod delegate_with_actions; +mod preallocate_buffer; mod top_up_ephemeral_balance; mod types; mod validator_claim_fees; @@ -13,6 +14,7 @@ pub use commit_state::*; pub use delegate::*; pub use delegate_ephemeral_balance::*; pub use delegate_with_actions::*; +pub use preallocate_buffer::*; pub use top_up_ephemeral_balance::*; pub use types::*; pub use validator_claim_fees::*; diff --git a/dlp-api/src/args/preallocate_buffer.rs b/dlp-api/src/args/preallocate_buffer.rs new file mode 100644 index 00000000..62642ac5 --- /dev/null +++ b/dlp-api/src/args/preallocate_buffer.rs @@ -0,0 +1,26 @@ +use borsh::{BorshDeserialize, BorshSerialize}; + +use crate::compat::borsh; + +/// Which DLP-owned buffer (or the delegated account itself) a +/// `PreallocateBuffer` instruction should grow. +#[derive( + Default, Debug, Clone, Copy, PartialEq, Eq, BorshSerialize, BorshDeserialize, +)] +pub enum PreallocateBufferKind { + /// The `commit_state` PDA (seeds: `[COMMIT_STATE_TAG, delegated_account]`). + #[default] + CommitState, + /// The `undelegate_buffer` PDA (seeds: `[UNDELEGATE_BUFFER_TAG, delegated_account]`). + UndelegateBuffer, + /// Pre-allocating delegated account itself + DelegatedAccount, +} + +#[derive(Default, Debug, BorshSerialize, BorshDeserialize)] +pub struct PreallocateBufferArgs { + /// Which buffer to grow. + pub kind: PreallocateBufferKind, + /// The final size the buffer should reach + pub target_size: u32, +} diff --git a/dlp-api/src/discriminator.rs b/dlp-api/src/discriminator.rs index e4c8d37d..8c07e136 100644 --- a/dlp-api/src/discriminator.rs +++ b/dlp-api/src/discriminator.rs @@ -68,6 +68,9 @@ pub enum DlpDiscriminator { /// See [crate::processor::process_undelegate_with_rollback_after_timeout] for docs. UndelegateWithRollbackAfterTimeout = 27, + + /// See [crate::processor::process_preallocate_buffer] for docs. + PreallocateBuffer = 28, } impl DlpDiscriminator { diff --git a/dlp-api/src/error.rs b/dlp-api/src/error.rs index fa5c6987..005a5b24 100644 --- a/dlp-api/src/error.rs +++ b/dlp-api/src/error.rs @@ -191,6 +191,15 @@ pub enum DlpError { #[error("Owner program requested undelegation")] OwnerRequestedUndelegation = 55, + #[error("Cannot preallocate a buffer while a commit is in flight")] + PreallocateBufferCommitInFlight = 56, + + #[error("Buffer must be preallocated to the exact required size ahead of time for accounts over the growth cap")] + BufferNotPreallocatedToExactSize = 57, + + #[error("PreallocateBuffer target size does not exceed the growth cap; the buffer PDA is created directly by the consuming instruction instead")] + PreallocateBufferTargetTooSmall = 58, + #[error("An infallible error is encountered possibly due to logic error")] InfallibleError = 100, } diff --git a/dlp-api/src/instruction_builder/mod.rs b/dlp-api/src/instruction_builder/mod.rs index c52d5ce5..38ffaedf 100644 --- a/dlp-api/src/instruction_builder/mod.rs +++ b/dlp-api/src/instruction_builder/mod.rs @@ -17,6 +17,7 @@ mod finalize; mod init_magic_fee_vault; mod init_protocol_fees_vault; mod init_validator_fees_vault; +mod preallocate_buffer; mod protocol_claim_fees; mod request_undelegation; mod top_up_ephemeral_balance; @@ -46,6 +47,7 @@ pub use finalize::*; pub use init_magic_fee_vault::*; pub use init_protocol_fees_vault::*; pub use init_validator_fees_vault::*; +pub use preallocate_buffer::*; pub use protocol_claim_fees::*; pub use request_undelegation::*; pub use top_up_ephemeral_balance::*; diff --git a/dlp-api/src/instruction_builder/preallocate_buffer.rs b/dlp-api/src/instruction_builder/preallocate_buffer.rs new file mode 100644 index 00000000..185e76cb --- /dev/null +++ b/dlp-api/src/instruction_builder/preallocate_buffer.rs @@ -0,0 +1,105 @@ +use dlp::{ + args::{PreallocateBufferArgs, PreallocateBufferKind}, + discriminator::DlpDiscriminator, + pda::{ + commit_record_pda_from_delegated_account, + commit_state_pda_from_delegated_account, + delegation_record_pda_from_delegated_account, + undelegate_buffer_pda_from_delegated_account, + validator_fees_vault_pda_from_validator, + }, + total_size_budget, AccountSizeClass, DLP_PROGRAM_DATA_SIZE_CLASS, +}; +use solana_program::{ + account_info::MAX_PERMITTED_DATA_INCREASE, + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, +}; +use solana_sdk_ids::system_program; + +use crate::compat::{borsh::to_vec, Compatize, Modernize}; + +/// Builds a preallocate buffer instruction. +/// See [dlp::processor::process_preallocate_buffer] for docs. +pub fn preallocate_buffer( + validator: Pubkey, + delegated_account: Pubkey, + kind: PreallocateBufferKind, + target_size: u32, +) -> Instruction { + let args = to_vec(&PreallocateBufferArgs { kind, target_size }).unwrap(); + let validator_compat = validator.compatize(); + let delegated_account_compat = delegated_account.compatize(); + let delegation_record_pda = + delegation_record_pda_from_delegated_account(&delegated_account_compat) + .modernize(); + let commit_record_pda = + commit_record_pda_from_delegated_account(&delegated_account_compat) + .modernize(); + let validator_fees_vault_pda = + validator_fees_vault_pda_from_validator(&validator_compat).modernize(); + let buffer_pda = match kind { + PreallocateBufferKind::CommitState => { + commit_state_pda_from_delegated_account(&delegated_account_compat) + .modernize() + } + PreallocateBufferKind::UndelegateBuffer => { + undelegate_buffer_pda_from_delegated_account( + &delegated_account_compat, + ) + .modernize() + } + PreallocateBufferKind::DelegatedAccount => delegated_account, + }; + Instruction { + program_id: dlp::id().modernize(), + accounts: vec![ + AccountMeta::new(validator, true), + AccountMeta::new_readonly(delegated_account, false), + AccountMeta::new(delegation_record_pda, false), + AccountMeta::new(buffer_pda, false), + AccountMeta::new_readonly(commit_record_pda, false), + AccountMeta::new_readonly(validator_fees_vault_pda, false), + AccountMeta::new_readonly(system_program::id(), false), + ], + data: [DlpDiscriminator::PreallocateBuffer.to_vec(), args].concat(), + } +} + +/// Returns the sequence of `preallocate_buffer` instructions needed to grow +/// a buffer of `kind` from `current_size` up to `target_size` +pub fn preallocate_buffer_chunks( + validator: Pubkey, + delegated_account: Pubkey, + kind: PreallocateBufferKind, + current_size: u32, + target_size: u32, +) -> Vec { + let growth = target_size.saturating_sub(current_size); + let chunks = growth.div_ceil(MAX_PERMITTED_DATA_INCREASE as u32) as usize; + (0..chunks) + .map(|_| { + preallocate_buffer(validator, delegated_account, kind, target_size) + }) + .collect() +} + +/// +/// Returns accounts-data-size budget for preallocate_buffer instruction. +/// +/// This value can be used with ComputeBudgetInstruction::SetLoadedAccountsDataSizeLimit +/// +pub fn preallocate_buffer_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 + delegated_account, // buffer_pda + AccountSizeClass::Tiny, // commit_record_pda + AccountSizeClass::Tiny, // validator_fees_vault_pda + AccountSizeClass::Tiny, // system_program + ]) +} diff --git a/src/lib.rs b/src/lib.rs index b7d3e54c..1fefb61e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -159,6 +159,11 @@ pub fn fast_process_instruction( program_id, accounts, data, ), ), + DlpDiscriminator::PreallocateBuffer => { + Some(processor::fast::process_preallocate_buffer( + program_id, accounts, data, + )) + } _ => None, } } diff --git a/src/processor/fast/commit_state.rs b/src/processor/fast/commit_state.rs index 5c1ee4db..e11dc229 100644 --- a/src/processor/fast/commit_state.rs +++ b/src/processor/fast/commit_state.rs @@ -11,13 +11,15 @@ use crate::{ args::CommitStateArgs, error::DlpError, merge_diff_copy, pda, - processor::fast::utils::pda::create_pda, + processor::fast::utils::pda::{ + create_or_verify_preallocated_pda, create_pda, + }, requires::{ require_initialized_delegation_metadata, require_initialized_delegation_record, require_initialized_validator_fees_vault, require_owned_pda, require_program_config, require_signer, require_uninitialized_pda, - CommitRecordCtx, CommitStateAccountCtx, + CommitRecordCtx, }, state::{ CommitRecord, DelegationMetadata, DelegationRecord, ProgramConfig, @@ -257,17 +259,20 @@ pub(crate) fn process_commit_state_internal( } } - // Load the uninitialized PDAs - let commit_state_bump = require_uninitialized_pda( + // Handle commit state account + let commit_state_len = args.commit_state_bytes.data_len(); + create_or_verify_preallocated_pda( args.commit_state_account, &[ pda::COMMIT_STATE_TAG, args.delegated_account.address().as_ref(), ], - &crate::fast::ID, - true, - CommitStateAccountCtx, + commit_state_len, + args.validator, + "commit state", )?; + + // Ensure commit record isn't initialized let commit_record_bump = require_uninitialized_pda( args.commit_record_account, &[ @@ -279,19 +284,6 @@ pub(crate) fn process_commit_state_internal( CommitRecordCtx, )?; - // Initialize the PDA containing the new committed state - create_pda( - args.commit_state_account, - &crate::fast::ID, - args.commit_state_bytes.data_len(), - &[Signer::from(&seeds!( - pda::COMMIT_STATE_TAG, - args.delegated_account.address().as_ref(), - &[commit_state_bump] - ))], - args.validator, - )?; - // Initialize the PDA containing the record of the committed state create_pda( args.commit_record_account, diff --git a/src/processor/fast/mod.rs b/src/processor/fast/mod.rs index b01c11a5..94122d91 100644 --- a/src/processor/fast/mod.rs +++ b/src/processor/fast/mod.rs @@ -7,6 +7,7 @@ mod commit_state_from_buffer; mod delegate; mod delegate_with_actions; mod finalize; +mod preallocate_buffer; mod request_undelegation; mod undelegate; mod undelegate_confined_account; @@ -24,6 +25,7 @@ pub use commit_state_from_buffer::*; pub use delegate::*; pub use delegate_with_actions::*; pub use finalize::*; +pub use preallocate_buffer::*; pub use request_undelegation::*; pub use undelegate::*; pub use undelegate_confined_account::*; diff --git a/src/processor/fast/preallocate_buffer.rs b/src/processor/fast/preallocate_buffer.rs new file mode 100644 index 00000000..27da2f65 --- /dev/null +++ b/src/processor/fast/preallocate_buffer.rs @@ -0,0 +1,292 @@ +use dlp_api::compat::borsh::BorshDeserialize; +use pinocchio::{ + account::MAX_PERMITTED_DATA_INCREASE, + address::{address_eq, Address}, + cpi::Signer, + error::ProgramError, + instruction::seeds, + AccountView, ProgramResult, +}; +use pinocchio_log::log; + +use super::to_pinocchio_program_error; +use crate::{ + args::{PreallocateBufferArgs, PreallocateBufferKind}, + error::DlpError, + pda, + processor::fast::utils::pda::{create_pda, resize_pda}, + require_gt, + requires::{ + is_uninitialized_account, require_initialized_delegation_record, + require_initialized_validator_fees_vault, require_owned_pda, + require_pda, require_signer, + }, + state::DelegationRecord, +}; + +/// Grows a DLP-owned buffer (or the delegated account itself) towards a +/// target size, in steps of at most `MAX_PERMITTED_DATA_INCREASE` bytes. +/// +/// Since that growth cap resets for every top-level instruction, a validator +/// that needs to commit/undelegate an account larger than the cap can send +/// several `PreallocateBuffer` instructions (same args, repeated) ahead of +/// the commit/finalize (or undelegate) instruction, all packed into the same +/// transaction, to reach the required size before the actual write happens. +/// +/// Accounts: +/// +/// 0: `[signer, writable]` the payer funding rent for the growth +/// 1: `[]` the delegated account +/// 2: `[writable]` the delegation record account (only actually +/// written for `kind = DelegatedAccount`, see below) +/// 3: `[writable]` the buffer PDA to grow (unused for `DelegatedAccount`) +/// 4: `[]` the commit record account +/// 5: `[]` the validator fees vault +/// 6: `[]` the system program +/// +/// Requirements: +/// +/// - delegated account is owned by delegation program +/// - delegation record is initialized +/// - validator is a registered validator (has an initialized fees vault) and +/// is the delegation's authority +/// - for `kind = CommitState`, no commit is currently in flight for the +/// delegated account (commit record must be uninitialized), mirroring the +/// precondition `commit_state`/`commit_diff` themselves enforce before +/// they'll create this buffer. +/// - `kind = DelegatedAccount` / `UndelegateBuffer` have no such +/// restriction: both are meant to run precisely *while* a commit record is +/// initialized -- `DelegatedAccount` between `commit_state` and `finalize`, +/// to pre-grow the account before finalize's single-shot resize; and +/// `UndelegateBuffer` alongside finalize in a two-stage commit-and-undelegate, +/// ahead of `undelegate`'s own check (which requires the commit record to be +/// uninitialized by the time *it* runs, not by the time it's preallocated). +/// - for `kind = CommitState` / `UndelegateBuffer`, `target_size` must exceed +/// `MAX_PERMITTED_DATA_INCREASE` -- those buffer PDAs are always closed and +/// recreated fresh each cycle, so a smaller target can always be created +/// directly, in one shot, by the consuming instruction itself. +/// - for `kind = DelegatedAccount`, rent transferred to fund the growth is +/// also added to `delegation_record.lamports`, keeping the ledger in sync +/// with the account's actual balance -- otherwise finalize/commit_finalize's +/// settlement (which diffs against `delegation_record.lamports`) would fund +/// the same growth a second time. +pub fn process_preallocate_buffer( + _program_id: &Address, + accounts: &[AccountView], + data: &[u8], +) -> ProgramResult { + let [validator, delegated_account, delegation_record_account, buffer_account, commit_record_account, validator_fees_vault, system_program] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + require_initialized_delegation_record( + delegated_account, + delegation_record_account, + true, + )?; + let mut delegation_record_data = + delegation_record_account.try_borrow_mut()?; + let delegation_record = + DelegationRecord::try_from_bytes_with_discriminator_mut( + &mut delegation_record_data, + ) + .map_err(to_pinocchio_program_error)?; + + // Validate common requirements + validate_common( + validator, + delegated_account, + validator_fees_vault, + delegation_record, + )?; + + let args = PreallocateBufferArgs::try_from_slice(data) + .map_err(|_| ProgramError::BorshIoError)?; + let target_size = args.target_size as usize; + match args.kind { + PreallocateBufferKind::CommitState => { + // Buffer is closed and recreated fresh every cycle, so a target + // this small can always be created directly, in one shot, by + // the consuming instruction itself -- there's never a + // legitimate reason to preallocate one. Rejecting it keeps + // "buffer PDA is DLP-owned" synonymous with "it had to be + // preallocated", which consumers rely on. + require_gt!( + target_size, + MAX_PERMITTED_DATA_INCREASE, + DlpError::PreallocateBufferTargetTooSmall + ); + require_no_commit_in_flight( + delegated_account, + commit_record_account, + )?; + let seeds_arr = + [pda::COMMIT_STATE_TAG, delegated_account.address().as_ref()]; + let bump = require_pda( + buffer_account, + &seeds_arr, + &crate::fast::ID, + true, + "commit state", + )?; + grow_target( + buffer_account, + &seeds_arr, + bump, + target_size, + validator, + system_program, + ) + } + PreallocateBufferKind::UndelegateBuffer => { + // Same reasoning as the `CommitState` arm above. + require_gt!( + target_size, + MAX_PERMITTED_DATA_INCREASE, + DlpError::PreallocateBufferTargetTooSmall + ); + let seeds_arr = [ + pda::UNDELEGATE_BUFFER_TAG, + delegated_account.address().as_ref(), + ]; + let bump = require_pda( + buffer_account, + &seeds_arr, + &crate::fast::ID, + true, + "undelegate buffer", + )?; + grow_target( + buffer_account, + &seeds_arr, + bump, + target_size, + validator, + system_program, + ) + } + PreallocateBufferKind::DelegatedAccount => { + let transferred = resize_towards( + delegated_account, + target_size, + validator, + system_program, + )?; + // The delegated account's balance persists across cycles and is + // tracked by `delegation_record.lamports`; finalize/commit_finalize + // settlement later diffs the ER's reported balance against that + // value, so it must reflect rent this instruction already funded -- + // otherwise settlement re-funds the same growth again. + delegation_record.lamports = + delegation_record.lamports.saturating_add(transferred); + Ok(()) + } + } +} + +/// Common account validation shared by every `kind`: the validator must be a +/// signer, a registered validator (has an initialized fees vault), and the +/// authority of this specific delegation. +fn validate_common( + validator: &AccountView, + delegated_account: &AccountView, + validator_fees_vault: &AccountView, + delegation_record: &DelegationRecord, +) -> ProgramResult { + // Ensure validator is signer and is whitelisted + require_signer(validator, "validator")?; + require_initialized_validator_fees_vault( + validator, + validator_fees_vault, + false, + )?; + + // Validate account is delegated + require_owned_pda( + delegated_account, + &crate::fast::ID, + "delegated account", + )?; + + // Ensure validator doesn't intervene in other validator's accounts + if !address_eq( + &delegation_record.authority.to_bytes().into(), + validator.address(), + ) { + log!("validator is not the delegation authority. validator: "); + validator.address().log(); + log!("delegation authority: "); + Address::from(delegation_record.authority.to_bytes()).log(); + Err(DlpError::InvalidAuthority.into()) + } else { + Ok(()) + } +} + +/// Makes sure that there's no in flight commit +fn require_no_commit_in_flight( + delegated_account: &AccountView, + commit_record_account: &AccountView, +) -> ProgramResult { + require_pda( + commit_record_account, + &[pda::COMMIT_RECORD_TAG, delegated_account.address().as_ref()], + &crate::fast::ID, + false, + "commit record", + )?; + if !is_uninitialized_account(commit_record_account) { + Err(DlpError::PreallocateBufferCommitInFlight.into()) + } else { + Ok(()) + } +} + +/// Grows PDA towards `target_size`, creating it fresh if it doesn't exist yet, +/// or resizing it by at most`MAX_PERMITTED_DATA_INCREASE` bytes +fn grow_target( + target_account: &AccountView, + seeds_arr: &[&[u8]; 2], + bump: u8, + target_size: usize, + payer: &AccountView, + system_program: &AccountView, +) -> ProgramResult { + if is_uninitialized_account(target_account) { + let size = target_size.min(MAX_PERMITTED_DATA_INCREASE); + create_pda( + target_account, + &crate::fast::ID, + size, + &[Signer::from(&seeds!(seeds_arr[0], seeds_arr[1], &[bump]))], + payer, + ) + } else { + resize_towards(target_account, target_size, payer, system_program)?; + Ok(()) + } +} + +/// Resizes `account` towards `target_size` by at most +/// `MAX_PERMITTED_DATA_INCREASE` bytes; a no-op if it's already at or past +/// `target_size` (shrinking is never growth-capped, so it's left to whichever +/// instruction actually performs the exact-size write). Returns the amount +/// of lamports transferred to fund the growth (0 if none was needed). +fn resize_towards( + account: &AccountView, + target_size: usize, + payer: &AccountView, + system_program: &AccountView, +) -> Result { + let current = account.data_len(); + if current < target_size { + let realloc_size = + core::cmp::min(target_size - current, MAX_PERMITTED_DATA_INCREASE); + let new_size = current + realloc_size; + resize_pda(payer, account, system_program, new_size) + } else { + Ok(0) + } +} diff --git a/src/processor/fast/undelegate.rs b/src/processor/fast/undelegate.rs index f5dcfdfe..cfd623c1 100644 --- a/src/processor/fast/undelegate.rs +++ b/src/processor/fast/undelegate.rs @@ -20,7 +20,9 @@ use crate::{ }, error::DlpError, pda, - processor::fast::utils::pda::{close_pda, close_pda_with_fees, create_pda}, + processor::fast::utils::pda::{ + close_pda, close_pda_with_fees, create_or_verify_preallocated_pda, + }, require_n_accounts_with_optionals, requires::{ require_initialized_delegation_metadata, @@ -28,7 +30,7 @@ use crate::{ require_initialized_protocol_fees_vault, require_initialized_validator_fees_vault, require_owned_pda, require_signer, require_uninitialized_pda, CommitRecordCtx, - CommitStateAccountCtx, UndelegateBufferCtx, + CommitStateAccountCtx, }, state::{ DelegationMetadata, DelegationRecord, UndelegationRequest, @@ -241,28 +243,17 @@ pub fn process_undelegate( return Ok(()); } - // Initialize the undelegation buffer PDA - let undelegate_buffer_bump: u8 = require_uninitialized_pda( + // Handle undelegate buffer account + let undelegate_buffer_size = delegated_account.data_len(); + let undelegate_buffer_bump = create_or_verify_preallocated_pda( undelegate_buffer_account, &[ pda::UNDELEGATE_BUFFER_TAG, delegated_account.address().as_ref(), ], - &crate::fast::ID, - true, - UndelegateBufferCtx, - )?; - - create_pda( - undelegate_buffer_account, - &crate::fast::ID, - delegated_account.data_len(), - &[Signer::from(&seeds!( - pda::UNDELEGATE_BUFFER_TAG, - delegated_account.address().as_ref(), - &[undelegate_buffer_bump] - ))], + undelegate_buffer_size, validator, + "undelegate buffer", )?; // Copy data in the undelegation buffer PDA diff --git a/src/processor/fast/undelegate_confined_account.rs b/src/processor/fast/undelegate_confined_account.rs index 52879fe9..72917097 100644 --- a/src/processor/fast/undelegate_confined_account.rs +++ b/src/processor/fast/undelegate_confined_account.rs @@ -7,12 +7,14 @@ use super::{process_undelegation_with_cpi, to_pinocchio_program_error}; use crate::{ error::DlpError, pda, - processor::fast::utils::pda::{close_pda, create_pda}, + processor::fast::utils::pda::{ + close_pda, create_or_verify_preallocated_pda, + }, require_eq_keys, requires::{ require_authorization, require_initialized_delegation_metadata, require_initialized_delegation_record, require_owned_pda, - require_signer, require_uninitialized_pda, UndelegateBufferCtx, + require_signer, }, state::{DelegationMetadata, DelegationRecord}, }; @@ -101,28 +103,17 @@ pub fn process_undelegate_confined_account( return Ok(()); } - // Initialize undelegation buffer PDA and copy state - let undelegate_buffer_bump: u8 = require_uninitialized_pda( + // Handle undelegate buffer account + let undelegate_buffer_size = delegated_account.data_len(); + let undelegate_buffer_bump = create_or_verify_preallocated_pda( undelegate_buffer_account, &[ pda::UNDELEGATE_BUFFER_TAG, delegated_account.address().as_ref(), ], - &crate::fast::ID, - true, - UndelegateBufferCtx, - )?; - - create_pda( - undelegate_buffer_account, - &crate::fast::ID, - delegated_account.data_len(), - &[Signer::from(&seeds!( - pda::UNDELEGATE_BUFFER_TAG, - delegated_account.address().as_ref(), - &[undelegate_buffer_bump] - ))], + undelegate_buffer_size, admin, + "undelegate buffer", )?; (*undelegate_buffer_account.try_borrow_mut()?) diff --git a/src/processor/fast/utils/pda.rs b/src/processor/fast/utils/pda.rs index c08b67ed..57bceb08 100644 --- a/src/processor/fast/utils/pda.rs +++ b/src/processor/fast/utils/pda.rs @@ -1,11 +1,17 @@ use pinocchio::{ cpi::Signer, + error::ProgramError, + instruction::seeds, sysvars::{rent::Rent, Sysvar}, AccountView, Address, ProgramResult, }; use pinocchio_system::instructions as system; -use crate::consts::PROTOCOL_FEES_PERCENTAGE; +use crate::{ + consts::PROTOCOL_FEES_PERCENTAGE, + error::DlpError, + requires::{is_uninitialized_account, require_owned_pda, require_pda}, +}; /// Creates a new pda #[inline(always)] @@ -61,6 +67,40 @@ pub(crate) fn create_pda( } } +/// Prepares `buffer_account` to hold exactly `state_size` bytes, seeded by +/// `seeds_arr` under this program. Returns the PDA's bump seed. +pub(crate) fn create_or_verify_preallocated_pda( + target_account: &AccountView, + seeds_arr: &[&[u8]; 2], + state_size: usize, + payer: &AccountView, + label: &str, +) -> Result { + let bump = + require_pda(target_account, seeds_arr, &crate::fast::ID, true, label)?; + + if is_uninitialized_account(target_account) { + // Not preallocated - initialize it here directly. + create_pda( + target_account, + &crate::fast::ID, + state_size, + &[Signer::from(&seeds!(seeds_arr[0], seeds_arr[1], &[bump]))], + payer, + )?; + Ok(bump) + } else { + // Already preallocated by PreallocateBuffer -- must match exactly. + require_owned_pda(target_account, &crate::fast::ID, label)?; + + if target_account.data_len() != state_size { + Err(DlpError::BufferNotPreallocatedToExactSize.into()) + } else { + Ok(bump) + } + } +} + /// Close PDA #[inline(always)] pub(crate) fn close_pda( @@ -115,3 +155,28 @@ pub(crate) fn close_pda_with_fees( target_account.resize(0) } + +/// Resizes `pda` to `new_size`, topping it up with rent from `payer` if +/// needed. Returns the amount of lamports actually transferred (0 if the +/// account was already rent-exempt for `new_size`). +pub(crate) fn resize_pda( + payer: &AccountView, + pda: &AccountView, + _system_program: &AccountView, + new_size: usize, +) -> Result { + let rent = Rent::get()?; + let rent_exempt_balance = rent + .try_minimum_balance(new_size)? + .saturating_sub(pda.lamports()); + if rent_exempt_balance > 0 { + system::Transfer { + from: payer, + to: pda, + lamports: rent_exempt_balance, + } + .invoke()?; + } + pda.resize(new_size)?; + Ok(rent_exempt_balance) +} diff --git a/tests/test_commit_finalize_from_buffer.rs b/tests/test_commit_finalize_from_buffer.rs index 5c861190..8db0e1d5 100644 --- a/tests/test_commit_finalize_from_buffer.rs +++ b/tests/test_commit_finalize_from_buffer.rs @@ -1,6 +1,6 @@ use dlp::solana_program; use dlp_api::{ - args::CommitFinalizeArgs, + args::{CommitFinalizeArgs, PreallocateBufferKind}, pda::{ delegation_metadata_pda_from_delegated_account, delegation_record_pda_from_delegated_account, @@ -8,15 +8,20 @@ use dlp_api::{ }, state::DelegationMetadata, }; -use solana_program::{hash::Hash, native_token::LAMPORTS_PER_SOL, rent::Rent}; +use solana_program::{ + account_info::MAX_PERMITTED_DATA_INCREASE, hash::Hash, + native_token::LAMPORTS_PER_SOL, rent::Rent, +}; use solana_program_test::{ - BanksClient, BanksTransactionResultWithMetadata, ProgramTest, + BanksClient, BanksClientError, BanksTransactionResultWithMetadata, + ProgramTest, }; use solana_sdk::{ account::Account, + instruction::InstructionError, pubkey::Pubkey, signature::{Keypair, Signer}, - transaction::Transaction, + transaction::{Transaction, TransactionError}, }; use solana_sdk_ids::system_program; @@ -162,6 +167,267 @@ async fn test_commit_finalize_from_buffer_out_of_order() { ); } +/// CommitFinalize writes the new state straight into the delegated account +/// (no intermediate `commit_state` PDA), so a target past +/// `MAX_PERMITTED_DATA_INCREASE` needs the delegated account itself grown +/// beforehand via `PreallocateBufferKind::DelegatedAccount`. +#[tokio::test] +async fn test_commit_finalize_from_buffer_large_with_preallocation() { + let target_size = MAX_PERMITTED_DATA_INCREASE + 4_760; // 15_000 + let new_state = vec![7u8; target_size]; + + let (banks, _, authority, blockhash) = + setup_program_test_env(vec![], new_state.clone()).await; + + let state_buffer_pda = + Pubkey::find_program_address(&[b"state_buffer"], &authority.pubkey()).0; + + let mut ixs = dlp_api::instruction_builder::preallocate_buffer_chunks( + authority.pubkey(), + DELEGATED_PDA_ID, + PreallocateBufferKind::DelegatedAccount, + 0, + target_size as u32, + ); + let (ix, _pdas) = dlp_api::instruction_builder::commit_finalize_from_buffer( + authority.pubkey(), + DELEGATED_PDA_ID, + state_buffer_pda, + &mut CommitFinalizeArgs { + commit_id: 1, + allow_undelegation: true.into(), + data_is_diff: false.into(), + lamports: 1_000_000, + bumps: Default::default(), + reserved_padding: Default::default(), + }, + ); + ixs.push(ix); + + let tx = Transaction::new_signed_with_payer( + &ixs, + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok(), "{:?}", res); + + let delegated_account = + banks.get_account(DELEGATED_PDA_ID).await.unwrap().unwrap(); + assert_eq!(delegated_account.data, new_state); +} + +/// Regression test: `PreallocateBuffer(DelegatedAccount)` funds rent to grow +/// the delegated account ahead of CommitFinalize. CommitFinalize's own +/// settlement independently diffs `commit_lamports` (mirrored from the ER, +/// which already paid its own rent for the same growth) against +/// `delegation_record.lamports`. If preallocate doesn't keep that ledger +/// value in sync with what it actually transferred, settlement re-funds the +/// same growth a second time, and the delegated account ends up holding +/// roughly double the correct rent. Assert the final balance is exactly +/// `commit_lamports`, not double it. +#[tokio::test] +async fn test_commit_finalize_from_buffer_large_growth_does_not_double_pay_rent( +) { + let target_size = MAX_PERMITTED_DATA_INCREASE + 4_760; // 15_000 + let new_state = vec![7u8; target_size]; + + // Delegated account starts empty, with lamports exactly matching the + // delegation record's ledger, so the settlement math below is exact. + let initial_lamports = Rent::default().minimum_balance(0); + let min_rent_target = Rent::default().minimum_balance(target_size); + // Lamports the ER holds beyond bare rent-exemption for the new size -- + // settlement must add exactly this much, no more. + let extra_on_er = 12_345u64; + let commit_lamports = min_rent_target + extra_on_er; + + let (banks, _, authority, blockhash) = + setup_program_test_env_with_lamports( + vec![], + new_state.clone(), + initial_lamports, + ) + .await; + + let state_buffer_pda = + Pubkey::find_program_address(&[b"state_buffer"], &authority.pubkey()).0; + + let mut ixs = dlp_api::instruction_builder::preallocate_buffer_chunks( + authority.pubkey(), + DELEGATED_PDA_ID, + PreallocateBufferKind::DelegatedAccount, + 0, + target_size as u32, + ); + let (ix, _pdas) = dlp_api::instruction_builder::commit_finalize_from_buffer( + authority.pubkey(), + DELEGATED_PDA_ID, + state_buffer_pda, + &mut CommitFinalizeArgs { + commit_id: 1, + allow_undelegation: true.into(), + data_is_diff: false.into(), + lamports: commit_lamports, + bumps: Default::default(), + reserved_padding: Default::default(), + }, + ); + ixs.push(ix); + + let tx = Transaction::new_signed_with_payer( + &ixs, + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok(), "{:?}", res); + + let delegated_account = + banks.get_account(DELEGATED_PDA_ID).await.unwrap().unwrap(); + assert_eq!(delegated_account.data, new_state); + assert_eq!( + delegated_account.lamports, commit_lamports, + "delegated account should hold exactly commit_lamports -- \ + preallocate's rent top-up and settlement's delta must not both \ + fund the same growth" + ); + + let delegation_record_account = banks + .get_account(delegation_record_pda_from_delegated_account( + &DELEGATED_PDA_ID, + )) + .await + .unwrap() + .unwrap(); + let delegation_record = + dlp_api::state::DelegationRecord::try_from_bytes_with_discriminator( + &delegation_record_account.data, + ) + .unwrap(); + assert_eq!(delegation_record.lamports, commit_lamports); +} + +#[tokio::test] +async fn test_commit_finalize_from_buffer_large_without_preallocation_fails() { + let target_size = MAX_PERMITTED_DATA_INCREASE + 4_760; // 15_000 + let new_state = vec![7u8; target_size]; + + let (banks, _, authority, blockhash) = + setup_program_test_env(vec![], new_state).await; + + let state_buffer_pda = + Pubkey::find_program_address(&[b"state_buffer"], &authority.pubkey()).0; + + let (ix, _pdas) = dlp_api::instruction_builder::commit_finalize_from_buffer( + authority.pubkey(), + DELEGATED_PDA_ID, + state_buffer_pda, + &mut CommitFinalizeArgs { + commit_id: 1, + allow_undelegation: true.into(), + data_is_diff: false.into(), + lamports: 1_000_000, + bumps: Default::default(), + reserved_padding: Default::default(), + }, + ); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); + let err = banks.process_transaction(tx).await.unwrap_err(); + // The un-preallocated delegated account starts at 0 bytes; CommitFinalize + // writes directly into it, so its own `resize()` to the full 15_000-byte + // target exceeds MAX_PERMITTED_DATA_INCREASE in a single instruction. + assert!( + matches!( + err, + BanksClientError::TransactionError( + TransactionError::InstructionError( + _, + InstructionError::InvalidRealloc, + ) + ) + ), + "expected InvalidRealloc, got {err:?}" + ); +} + +/// Distinct from the "no preallocation at all" case above: here the +/// delegated account *was* preallocated, just not far enough -- the gap left +/// to the actual target still exceeds `MAX_PERMITTED_DATA_INCREASE`. +/// CommitFinalize (unlike `commit_state_from_buffer`) has no "must be +/// preallocated to the exact size" check of its own -- it just resizes +/// directly -- so this is exercising the same native realloc cap, not a +/// dedicated dlp error. +#[tokio::test] +async fn test_commit_finalize_from_buffer_large_wrong_preallocated_size_fails() +{ + let target_size = 25_000usize; + let new_state = vec![7u8; target_size]; + + let (banks, _, authority, blockhash) = + setup_program_test_env(vec![], new_state).await; + + let state_buffer_pda = + Pubkey::find_program_address(&[b"state_buffer"], &authority.pubkey()).0; + + // Only send the first growth step (10_240 bytes) toward the 25_000 + // target, leaving a 14_760-byte gap -- still short of the target by more + // than one instruction can bridge. + let all_steps = dlp_api::instruction_builder::preallocate_buffer_chunks( + authority.pubkey(), + DELEGATED_PDA_ID, + PreallocateBufferKind::DelegatedAccount, + 0, + target_size as u32, + ); + assert!( + all_steps.len() > 1, + "test setup expects the full growth to require multiple steps" + ); + let mut ixs = vec![all_steps[0].clone()]; + + let (ix, _pdas) = dlp_api::instruction_builder::commit_finalize_from_buffer( + authority.pubkey(), + DELEGATED_PDA_ID, + state_buffer_pda, + &mut CommitFinalizeArgs { + commit_id: 1, + allow_undelegation: true.into(), + data_is_diff: false.into(), + lamports: 1_000_000, + bumps: Default::default(), + reserved_padding: Default::default(), + }, + ); + ixs.push(ix); + + let tx = Transaction::new_signed_with_payer( + &ixs, + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); + let err = banks.process_transaction(tx).await.unwrap_err(); + assert!( + matches!( + err, + BanksClientError::TransactionError( + TransactionError::InstructionError( + _, + InstructionError::InvalidRealloc, + ) + ) + ), + "expected InvalidRealloc, got {err:?}" + ); +} + async fn setup_program_test_env( pda_data: Vec, pda_new_state: Vec, @@ -255,3 +521,106 @@ async fn setup_program_test_env( let (banks, payer, blockhash) = program_test.start().await; (banks, payer, validator_keypair, blockhash) } + +/// Like `setup_program_test_env`, but with the delegated account's lamports +/// (and matching delegation record ledger value) both set to +/// `delegated_lamports` -- needed to test settlement math precisely, where +/// the two must start in sync for the invariants being tested to mean +/// anything (unlike the fixed, unrelated values `setup_program_test_env` +/// uses, which are fine for tests that don't assert exact final lamports). +async fn setup_program_test_env_with_lamports( + pda_data: Vec, + pda_new_state: Vec, + delegated_lamports: u64, +) -> (BanksClient, Keypair, Keypair, Hash) { + let mut program_test = ProgramTest::new("dlp", dlp_api::ID, None); + program_test.prefer_bpf(true); + + let validator_keypair = + crate::fixtures::keypair_from_bytes(&TEST_AUTHORITY); + + 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: delegated_lamports, + data: pda_data, + owner: dlp_api::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_api::id(), + executable: false, + rent_epoch: 0, + }, + ); + + // Setup the delegated record PDA + let delegation_record_data = get_delegation_record_data( + validator_keypair.pubkey(), + Some(delegated_lamports), + ); + 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_api::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_api::id(), + executable: false, + rent_epoch: 0, + }, + ); + + program_test.add_account( + Pubkey::find_program_address( + &[b"state_buffer"], + &validator_keypair.pubkey(), + ) + .0, + Account { + lamports: LAMPORTS_PER_SOL, + data: pda_new_state, + owner: dlp_api::id(), + executable: false, + rent_epoch: 0, + }, + ); + + let (banks, payer, blockhash) = program_test.start().await; + (banks, payer, validator_keypair, blockhash) +} diff --git a/tests/test_commit_state_from_buffer.rs b/tests/test_commit_state_from_buffer.rs index 7a76aab5..be50de03 100644 --- a/tests/test_commit_state_from_buffer.rs +++ b/tests/test_commit_state_from_buffer.rs @@ -1,6 +1,8 @@ use dlp::solana_program; use dlp_api::{ - args::CommitStateFromBufferArgs, + args::{CommitStateFromBufferArgs, PreallocateBufferKind}, + diff::compute_diff, + error::DlpError, pda::{ commit_record_pda_from_delegated_account, commit_state_pda_from_delegated_account, @@ -10,13 +12,17 @@ use dlp_api::{ }, state::{CommitRecord, DelegationMetadata}, }; -use solana_program::{hash::Hash, native_token::LAMPORTS_PER_SOL, rent::Rent}; -use solana_program_test::{BanksClient, ProgramTest}; +use solana_program::{ + account_info::MAX_PERMITTED_DATA_INCREASE, hash::Hash, + native_token::LAMPORTS_PER_SOL, rent::Rent, +}; +use solana_program_test::{BanksClient, BanksClientError, ProgramTest}; use solana_sdk::{ account::Account, + instruction::InstructionError, pubkey::Pubkey, signature::{Keypair, Signer}, - transaction::Transaction, + transaction::{Transaction, TransactionError}, }; use solana_sdk_ids::system_program; @@ -32,7 +38,8 @@ const NEW_STATE: [u8; 10] = [0, 1, 2, 9, 9, 9, 6, 7, 8, 9]; #[tokio::test] async fn test_commit_new_state_from_buffer() { // Setup - let (banks, _, authority, blockhash) = setup_program_test_env().await; + let (banks, _, authority, blockhash) = + setup_program_test_env(vec![], NEW_STATE.to_vec()).await; let new_account_balance = 1_000_000; let state_buffer_pda = Pubkey::find_program_address(&[b"state_buffer"], &authority.pubkey()).0; @@ -107,7 +114,276 @@ async fn test_commit_new_state_from_buffer() { ); } -async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { +/// A commit_state target past `MAX_PERMITTED_DATA_INCREASE` needs the +/// commit_state PDA preallocated first via `PreallocateBufferKind::CommitState`. +#[tokio::test] +async fn test_commit_new_state_from_buffer_large_with_preallocation() { + let target_size = MAX_PERMITTED_DATA_INCREASE + 4_760; // 15_000 + let new_state = vec![7u8; target_size]; + + let (banks, _, authority, blockhash) = + setup_program_test_env(vec![], new_state.clone()).await; + let state_buffer_pda = + Pubkey::find_program_address(&[b"state_buffer"], &authority.pubkey()).0; + + let mut ixs = dlp_api::instruction_builder::preallocate_buffer_chunks( + authority.pubkey(), + DELEGATED_PDA_ID, + PreallocateBufferKind::CommitState, + 0, + target_size as u32, + ); + ixs.push(dlp_api::instruction_builder::commit_state_from_buffer( + authority.pubkey(), + DELEGATED_PDA_ID, + DELEGATED_PDA_OWNER_ID, + state_buffer_pda, + CommitStateFromBufferArgs { + nonce: 1, + lamports: 1_000_000, + allow_undelegation: true, + }, + )); + + let tx = Transaction::new_signed_with_payer( + &ixs, + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok(), "{:?}", res); + + 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); +} + +#[tokio::test] +async fn test_commit_new_state_from_buffer_large_without_preallocation_fails() { + let target_size = MAX_PERMITTED_DATA_INCREASE + 4_760; // 15_000 + let new_state = vec![7u8; target_size]; + + let (banks, _, authority, blockhash) = + setup_program_test_env(vec![], new_state).await; + let state_buffer_pda = + Pubkey::find_program_address(&[b"state_buffer"], &authority.pubkey()).0; + + let ix = dlp_api::instruction_builder::commit_state_from_buffer( + authority.pubkey(), + DELEGATED_PDA_ID, + DELEGATED_PDA_OWNER_ID, + state_buffer_pda, + CommitStateFromBufferArgs { + nonce: 1, + lamports: 1_000_000, + allow_undelegation: true, + }, + ); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); + let err = banks.process_transaction(tx).await.unwrap_err(); + // commit_state has no PDA to grow yet at all -- create_or_verify_preallocated_pda + // tries to create it directly at the full 15_000-byte target, which + // exceeds MAX_PERMITTED_DATA_INCREASE in a single instruction. + assert!( + matches!( + err, + BanksClientError::TransactionError( + TransactionError::InstructionError( + _, + InstructionError::InvalidRealloc, + ) + ) + ), + "expected InvalidRealloc, got {err:?}" + ); +} + +/// Distinct from "no preallocation at all": the commit_state PDA *was* +/// preallocated, but not to the exact target size -- `commit_state_from_buffer` +/// (unlike `finalize`/`commit_finalize_from_buffer`) enforces an exact match +/// via `create_or_verify_preallocated_pda`, since it never resizes on its own. +#[tokio::test] +async fn test_commit_new_state_from_buffer_large_wrong_preallocated_size_fails() +{ + let target_size = MAX_PERMITTED_DATA_INCREASE + 4_760; // 15_000 + let new_state = vec![7u8; target_size]; + + let (banks, _, authority, blockhash) = + setup_program_test_env(vec![], new_state).await; + let state_buffer_pda = + Pubkey::find_program_address(&[b"state_buffer"], &authority.pubkey()).0; + + // Preallocate to a size other than the actual target + let mut ixs = dlp_api::instruction_builder::preallocate_buffer_chunks( + authority.pubkey(), + DELEGATED_PDA_ID, + PreallocateBufferKind::CommitState, + 0, + target_size as u32 - 1, + ); + ixs.push(dlp_api::instruction_builder::commit_state_from_buffer( + authority.pubkey(), + DELEGATED_PDA_ID, + DELEGATED_PDA_OWNER_ID, + state_buffer_pda, + CommitStateFromBufferArgs { + nonce: 1, + lamports: 1_000_000, + allow_undelegation: true, + }, + )); + let tx = Transaction::new_signed_with_payer( + &ixs, + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); + let err = banks.process_transaction(tx).await.unwrap_err(); + assert_custom_error(err, DlpError::BufferNotPreallocatedToExactSize); +} + +/// Builds a large, mostly-matching (base, changed) pair so their diff is +/// genuinely small despite the resulting state being large -- unlike diffing +/// against an empty account, where every byte is "new" and the diff ends up +/// roughly as big as the target itself. +fn large_base_and_small_diff_change(target_size: usize) -> (Vec, Vec) { + let base = vec![1u8; target_size]; + let mut changed = base.clone(); + changed[0..50].fill(9); + (base, changed) +} + +/// The diff-based sibling of `commit_state_from_buffer`. The concern this +/// directly guards against: `create_or_verify_preallocated_pda`'s exact-size +/// check must compare against the *full* post-diff state length +/// (`DiffSet::changed_len()`), never the diff payload's own byte length -- +/// those two are very different numbers here (a small diff producing a large +/// resulting state). +#[tokio::test] +async fn test_commit_diff_from_buffer_large_with_preallocation() { + let target_size = MAX_PERMITTED_DATA_INCREASE + 4_760; // 15_000 + let (base, new_state) = large_base_and_small_diff_change(target_size); + let diff = compute_diff(&base, &new_state); + assert!( + diff.len() < 1_000, + "test setup expects a small diff payload despite the large target" + ); + + let (banks, _, authority, blockhash) = + setup_program_test_env(base, diff.to_vec()).await; + let diff_buffer_pda = + Pubkey::find_program_address(&[b"state_buffer"], &authority.pubkey()).0; + + let mut ixs = dlp_api::instruction_builder::preallocate_buffer_chunks( + authority.pubkey(), + DELEGATED_PDA_ID, + PreallocateBufferKind::CommitState, + 0, + target_size as u32, + ); + ixs.push(dlp_api::instruction_builder::commit_diff_from_buffer( + authority.pubkey(), + DELEGATED_PDA_ID, + DELEGATED_PDA_OWNER_ID, + diff_buffer_pda, + CommitStateFromBufferArgs { + nonce: 1, + lamports: 1_000_000, + allow_undelegation: true, + }, + )); + + let tx = Transaction::new_signed_with_payer( + &ixs, + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok(), "{:?}", res); + + 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); +} + +/// Preallocating to the *diff's* length instead of the full post-diff state +/// length must be rejected -- if this ever passed, it would mean the exact- +/// size check regressed to comparing against the wrong number. +#[tokio::test] +async fn test_commit_diff_from_buffer_preallocated_to_diff_len_fails() { + let target_size = MAX_PERMITTED_DATA_INCREASE + 4_760; // 15_000 + let (base, new_state) = large_base_and_small_diff_change(target_size); + let diff = compute_diff(&base, &new_state); + assert!( + diff.len() < MAX_PERMITTED_DATA_INCREASE, + "test setup expects a diff payload that fits in one prealloc step" + ); + + let (banks, _, authority, blockhash) = + setup_program_test_env(base, diff.to_vec()).await; + let diff_buffer_pda = + Pubkey::find_program_address(&[b"state_buffer"], &authority.pubkey()).0; + + // Preallocate to the diff's own length, not the full target -- wrong on + // purpose. + let mut ixs = vec![dlp_api::instruction_builder::preallocate_buffer( + authority.pubkey(), + DELEGATED_PDA_ID, + PreallocateBufferKind::CommitState, + diff.len() as u32, + )]; + ixs.push(dlp_api::instruction_builder::commit_diff_from_buffer( + authority.pubkey(), + DELEGATED_PDA_ID, + DELEGATED_PDA_OWNER_ID, + diff_buffer_pda, + CommitStateFromBufferArgs { + nonce: 1, + lamports: 1_000_000, + allow_undelegation: true, + }, + )); + let tx = Transaction::new_signed_with_payer( + &ixs, + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); + let err = banks.process_transaction(tx).await.unwrap_err(); + // The diff's length is below MAX_PERMITTED_DATA_INCREASE, so PreallocateBuffer + // itself now rejects this target before commit_diff_from_buffer even runs. + assert_custom_error(err, DlpError::PreallocateBufferTargetTooSmall); +} + +fn assert_custom_error(err: BanksClientError, expected: DlpError) { + match err { + BanksClientError::TransactionError( + TransactionError::InstructionError( + _, + InstructionError::Custom(code), + ), + ) => { + assert_eq!(code, expected as u32, "unexpected error code"); + } + other => panic!("expected custom error {expected:?}, got {other:?}"), + } +} + +async fn setup_program_test_env( + delegated_pda_data: Vec, + state_buffer_data: Vec, +) -> (BanksClient, Keypair, Keypair, Hash) { let mut program_test = ProgramTest::new("dlp", dlp_api::ID, None); program_test.prefer_bpf(true); @@ -130,7 +406,7 @@ async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { DELEGATED_PDA_ID, Account { lamports: LAMPORTS_PER_SOL, - data: vec![], + data: delegated_pda_data, owner: dlp_api::id(), executable: false, rent_epoch: 0, @@ -188,7 +464,7 @@ async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { .0, Account { lamports: LAMPORTS_PER_SOL, - data: NEW_STATE.to_vec(), + data: state_buffer_data, owner: dlp_api::id(), executable: false, rent_epoch: 0, diff --git a/tests/test_finalize.rs b/tests/test_finalize.rs index 030141e8..268049d4 100644 --- a/tests/test_finalize.rs +++ b/tests/test_finalize.rs @@ -1,5 +1,6 @@ use dlp::solana_program; use dlp_api::{ + args::PreallocateBufferKind, pda::{ commit_record_pda_from_delegated_account, commit_state_pda_from_delegated_account, @@ -9,12 +10,16 @@ use dlp_api::{ }, state::{CommitRecord, DelegationMetadata}, }; -use solana_program::{hash::Hash, native_token::LAMPORTS_PER_SOL, rent::Rent}; -use solana_program_test::{BanksClient, ProgramTest}; +use solana_program::{ + account_info::MAX_PERMITTED_DATA_INCREASE, hash::Hash, + native_token::LAMPORTS_PER_SOL, rent::Rent, +}; +use solana_program_test::{BanksClient, BanksClientError, ProgramTest}; use solana_sdk::{ account::Account, + instruction::InstructionError, signature::{Keypair, Signer}, - transaction::Transaction, + transaction::{Transaction, TransactionError}, }; use solana_sdk_ids::system_program; @@ -29,7 +34,8 @@ mod fixtures; #[tokio::test] async fn test_finalize() { // Setup - let (banks, _, authority, blockhash) = setup_program_test_env().await; + let (banks, _, authority, blockhash) = + setup_program_test_env(COMMIT_NEW_STATE_ACCOUNT_DATA.into()).await; // Retrieve the accounts let delegation_record_pda = @@ -105,7 +111,115 @@ async fn test_finalize() { assert_eq!(commit_record.nonce, delegation_metadata.last_commit_id); } -async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { +/// Finalize copies `commit_state`'s full content straight into the delegated +/// account (`delegated_account.resize(commit_state_data.len())`), so a +/// target past `MAX_PERMITTED_DATA_INCREASE` needs the delegated account +/// itself grown beforehand via `PreallocateBufferKind::DelegatedAccount`. +#[tokio::test] +async fn test_finalize_large_with_preallocation() { + let target_size = MAX_PERMITTED_DATA_INCREASE + 4_760; // 15_000 + let new_state = vec![7u8; target_size]; + + let (banks, _, authority, blockhash) = + setup_program_test_env(new_state.clone()).await; + + let mut ixs = dlp_api::instruction_builder::preallocate_buffer_chunks( + authority.pubkey(), + DELEGATED_PDA_ID, + PreallocateBufferKind::DelegatedAccount, + 0, + target_size as u32, + ); + ixs.push(dlp_api::instruction_builder::finalize( + authority.pubkey(), + DELEGATED_PDA_ID, + )); + + let tx = Transaction::new_signed_with_payer( + &ixs, + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok(), "{:?}", res); + + // The delegated account grew to the full preallocated size and now holds + // the committed state's content exactly. + let delegated_account = + banks.get_account(DELEGATED_PDA_ID).await.unwrap().unwrap(); + assert_eq!(delegated_account.data, new_state); + + // finalize closes both the commit_state and commit_record PDAs once + // their content has been applied, same as for a small-account finalize. + let commit_state_pda = + commit_state_pda_from_delegated_account(&DELEGATED_PDA_ID); + assert!(banks.get_account(commit_state_pda).await.unwrap().is_none()); + let commit_record_pda = + commit_record_pda_from_delegated_account(&DELEGATED_PDA_ID); + assert!(banks + .get_account(commit_record_pda) + .await + .unwrap() + .is_none()); +} + +/// The delegated account *was* preallocated, just not far enough -- the gap +/// left to the actual target still exceeds `MAX_PERMITTED_DATA_INCREASE`. +/// `finalize` has no "must be preallocated to the exact size" check of its +/// own -- it just resizes directly -- so this exercises the native realloc +/// cap, not a dedicated dlp error. +#[tokio::test] +async fn test_finalize_large_wrong_preallocated_size_fails() { + let target_size = 25_000usize; + let new_state = vec![7u8; target_size]; + + let (banks, _, authority, blockhash) = + setup_program_test_env(new_state).await; + + // Only send the first growth step (10_240 bytes) toward the 25_000 + // target, leaving a 14_760-byte gap. + let all_steps = dlp_api::instruction_builder::preallocate_buffer_chunks( + authority.pubkey(), + DELEGATED_PDA_ID, + PreallocateBufferKind::DelegatedAccount, + 0, + target_size as u32, + ); + assert!( + all_steps.len() > 1, + "test setup expects the full growth to require multiple steps" + ); + let mut ixs = vec![all_steps[0].clone()]; + ixs.push(dlp_api::instruction_builder::finalize( + authority.pubkey(), + DELEGATED_PDA_ID, + )); + + let tx = Transaction::new_signed_with_payer( + &ixs, + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); + let err = banks.process_transaction(tx).await.unwrap_err(); + assert!( + matches!( + err, + BanksClientError::TransactionError( + TransactionError::InstructionError( + _, + InstructionError::InvalidRealloc, + ) + ) + ), + "expected InvalidRealloc, got {err:?}" + ); +} + +async fn setup_program_test_env( + commit_state_data: Vec, +) -> (BanksClient, Keypair, Keypair, Hash) { let mut program_test = ProgramTest::new("dlp", dlp_api::ID, None); program_test.prefer_bpf(true); @@ -169,7 +283,7 @@ async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { commit_state_pda_from_delegated_account(&DELEGATED_PDA_ID), Account { lamports: LAMPORTS_PER_SOL, - data: COMMIT_NEW_STATE_ACCOUNT_DATA.into(), + data: commit_state_data, owner: dlp_api::id(), executable: false, rent_epoch: 0, diff --git a/tests/test_preallocate_buffer.rs b/tests/test_preallocate_buffer.rs new file mode 100644 index 00000000..f98ae238 --- /dev/null +++ b/tests/test_preallocate_buffer.rs @@ -0,0 +1,573 @@ +use dlp::solana_program; +use dlp_api::{ + args::PreallocateBufferKind, + error::DlpError, + 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, + undelegate_buffer_pda_from_delegated_account, + validator_fees_vault_pda_from_validator, + }, +}; +use solana_program::{ + account_info::MAX_PERMITTED_DATA_INCREASE, hash::Hash, + native_token::LAMPORTS_PER_SOL, rent::Rent, +}; +use solana_program_test::{BanksClient, BanksClientError, ProgramTest}; +use solana_sdk::{ + account::Account, + instruction::InstructionError, + pubkey::Pubkey, + signature::{Keypair, Signer}, + transaction::{Transaction, TransactionError}, +}; +use solana_sdk_ids::system_program; + +use crate::fixtures::{ + get_commit_record_account_data, get_delegation_metadata_data, + get_delegation_record_data, DELEGATED_PDA_ID, DELEGATED_PDA_OWNER_ID, + TEST_AUTHORITY, +}; + +mod fixtures; + +fn assert_custom_error(err: BanksClientError, expected: DlpError) { + match err { + BanksClientError::TransactionError( + TransactionError::InstructionError( + _, + InstructionError::Custom(code), + ), + ) => { + assert_eq!(code, expected as u32, "unexpected error code"); + } + other => panic!("expected custom error {expected:?}, got {other:?}"), + } +} + +#[tokio::test] +async fn test_preallocate_commit_state_creates_fresh() { + let (banks, _, validator, blockhash) = + setup_program_test_env(false, None).await; + + // Must exceed MAX_PERMITTED_DATA_INCREASE (see + // test_preallocate_commit_state_rejects_target_at_or_below_cap below), + // so this needs two growth steps to actually reach the target. + let target_size: u32 = MAX_PERMITTED_DATA_INCREASE as u32 + 4_760; // 15_000 + let ixs = dlp_api::instruction_builder::preallocate_buffer_chunks( + validator.pubkey(), + DELEGATED_PDA_ID, + PreallocateBufferKind::CommitState, + 0, + target_size, + ); + let tx = Transaction::new_signed_with_payer( + &ixs, + Some(&validator.pubkey()), + &[&validator], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok(), "{:?}", res); + + 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.owner, dlp_api::id()); + assert_eq!(commit_state_account.data.len(), target_size as usize); + assert!(commit_state_account.data.iter().all(|&b| b == 0)); +} + +#[tokio::test] +async fn test_preallocate_undelegate_buffer_creates_fresh() { + let (banks, _, validator, blockhash) = + setup_program_test_env(false, None).await; + + // Same reasoning as test_preallocate_commit_state_creates_fresh above. + let target_size: u32 = MAX_PERMITTED_DATA_INCREASE as u32 + 4_760; // 15_000 + let ixs = dlp_api::instruction_builder::preallocate_buffer_chunks( + validator.pubkey(), + DELEGATED_PDA_ID, + PreallocateBufferKind::UndelegateBuffer, + 0, + target_size, + ); + let tx = Transaction::new_signed_with_payer( + &ixs, + Some(&validator.pubkey()), + &[&validator], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok(), "{:?}", res); + + let undelegate_buffer_pda = + undelegate_buffer_pda_from_delegated_account(&DELEGATED_PDA_ID); + let undelegate_buffer_account = banks + .get_account(undelegate_buffer_pda) + .await + .unwrap() + .unwrap(); + assert_eq!(undelegate_buffer_account.owner, dlp_api::id()); + assert_eq!(undelegate_buffer_account.data.len(), target_size as usize); + assert!(undelegate_buffer_account.data.iter().all(|&b| b == 0)); +} + +#[tokio::test] +async fn test_preallocate_delegated_account_grows_and_ignores_commit_in_flight() +{ + // A commit is in flight (commit_record already initialized): the + // DelegatedAccount kind is meant to run precisely in this window + // (between commit_state and finalize), so it must not be blocked by it. + let (banks, _, validator, blockhash) = + setup_program_test_env(true, None).await; + + let target_size: u32 = 500; + let ix = dlp_api::instruction_builder::preallocate_buffer( + validator.pubkey(), + DELEGATED_PDA_ID, + PreallocateBufferKind::DelegatedAccount, + target_size, + ); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&validator.pubkey()), + &[&validator], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok(), "{:?}", res); + + let delegated_account = + banks.get_account(DELEGATED_PDA_ID).await.unwrap().unwrap(); + assert_eq!(delegated_account.data.len(), target_size as usize); + assert!(delegated_account.data.iter().all(|&b| b == 0)); +} + +#[tokio::test] +async fn test_preallocate_commit_state_idempotent_past_target() { + let (banks, _, validator, blockhash) = + setup_program_test_env(false, None).await; + + // Grow to 15_000 bytes first (must exceed MAX_PERMITTED_DATA_INCREASE, + // see test_preallocate_commit_state_rejects_target_at_or_below_cap -- + // hence two growth steps to actually reach it). + let ixs = dlp_api::instruction_builder::preallocate_buffer_chunks( + validator.pubkey(), + DELEGATED_PDA_ID, + PreallocateBufferKind::CommitState, + 0, + 15_000, + ); + let tx = Transaction::new_signed_with_payer( + &ixs, + Some(&validator.pubkey()), + &[&validator], + blockhash, + ); + assert!(banks.process_transaction(tx).await.is_ok()); + + // Calling again with a smaller (but still > cap) target must be a no-op + // (never shrinks). + let ix = dlp_api::instruction_builder::preallocate_buffer( + validator.pubkey(), + DELEGATED_PDA_ID, + PreallocateBufferKind::CommitState, + 12_000, + ); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&validator.pubkey()), + &[&validator], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok(), "{:?}", res); + + 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.len(), 15_000); +} + +#[tokio::test] +async fn test_preallocate_commit_state_multiple_instructions_same_tx_exceed_cap( +) { + // The single most important property of this feature: several + // PreallocateBuffer instructions packed into ONE transaction can grow an + // account past MAX_PERMITTED_DATA_INCREASE, because the growth cap + // resets for each top-level instruction. + let (banks, _, validator, blockhash) = + setup_program_test_env(false, None).await; + + let target_size: u32 = MAX_PERMITTED_DATA_INCREASE as u32 + 4_760; // 15_000 + let ixs = dlp_api::instruction_builder::preallocate_buffer_chunks( + validator.pubkey(), + DELEGATED_PDA_ID, + PreallocateBufferKind::CommitState, + 0, + target_size, + ); + assert_eq!(ixs.len(), 2, "expected exactly two growth steps"); + + let tx = Transaction::new_signed_with_payer( + &ixs, + Some(&validator.pubkey()), + &[&validator], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok(), "{:?}", res); + + 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.len(), target_size as usize); + assert!(commit_state_account.data.iter().all(|&b| b == 0)); +} + +#[tokio::test] +async fn test_preallocate_commit_state_rejects_when_commit_in_flight() { + let (banks, _, validator, blockhash) = + setup_program_test_env(true, None).await; + + // Must exceed MAX_PERMITTED_DATA_INCREASE, or this trips + // PreallocateBufferTargetTooSmall before ever reaching the + // commit-in-flight check this test means to exercise. + let ix = dlp_api::instruction_builder::preallocate_buffer( + validator.pubkey(), + DELEGATED_PDA_ID, + PreallocateBufferKind::CommitState, + MAX_PERMITTED_DATA_INCREASE as u32 + 4_760, // 15_000 + ); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&validator.pubkey()), + &[&validator], + blockhash, + ); + let err = banks.process_transaction(tx).await.unwrap_err(); + assert_custom_error(err, DlpError::PreallocateBufferCommitInFlight); +} + +/// `CommitState`/`UndelegateBuffer` buffers are always closed and recreated +/// fresh each cycle, so a target at or below the cap can always be created +/// directly, in one shot, by the consuming instruction itself -- there's +/// never a legitimate reason to preallocate one this small, and doing so +/// would leave `create_or_verify_preallocated_pda` unable to tell a genuine +/// preallocation apart from a stray leftover account. +#[tokio::test] +async fn test_preallocate_commit_state_rejects_target_at_or_below_cap() { + let (banks, _, validator, blockhash) = + setup_program_test_env(false, None).await; + + // Exactly at the cap must also be rejected (the check is strictly `>`). + let ix = dlp_api::instruction_builder::preallocate_buffer( + validator.pubkey(), + DELEGATED_PDA_ID, + PreallocateBufferKind::CommitState, + MAX_PERMITTED_DATA_INCREASE as u32, + ); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&validator.pubkey()), + &[&validator], + blockhash, + ); + let err = banks.process_transaction(tx).await.unwrap_err(); + assert_custom_error(err, DlpError::PreallocateBufferTargetTooSmall); + + let commit_state_pda = + commit_state_pda_from_delegated_account(&DELEGATED_PDA_ID); + assert!( + banks.get_account(commit_state_pda).await.unwrap().is_none(), + "rejected preallocation must not create the buffer" + ); +} + +/// Same reasoning as test_preallocate_commit_state_rejects_target_at_or_below_cap. +#[tokio::test] +async fn test_preallocate_undelegate_buffer_rejects_target_at_or_below_cap() { + let (banks, _, validator, blockhash) = + setup_program_test_env(false, None).await; + + let ix = dlp_api::instruction_builder::preallocate_buffer( + validator.pubkey(), + DELEGATED_PDA_ID, + PreallocateBufferKind::UndelegateBuffer, + 500, + ); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&validator.pubkey()), + &[&validator], + blockhash, + ); + let err = banks.process_transaction(tx).await.unwrap_err(); + assert_custom_error(err, DlpError::PreallocateBufferTargetTooSmall); + + let undelegate_buffer_pda = + undelegate_buffer_pda_from_delegated_account(&DELEGATED_PDA_ID); + assert!( + banks + .get_account(undelegate_buffer_pda) + .await + .unwrap() + .is_none(), + "rejected preallocation must not create the buffer" + ); +} + +/// `DelegatedAccount` targets the delegated account itself, which isn't +/// closed/recreated each cycle the way `CommitState`/`UndelegateBuffer` +/// buffers are -- so it's exempt from the above guard, and small targets are +/// legitimate (e.g. growing a small account by a few bytes). +#[tokio::test] +async fn test_preallocate_delegated_account_allows_target_at_or_below_cap() { + let (banks, _, validator, blockhash) = + setup_program_test_env(false, None).await; + + let target_size: u32 = 500; + let ix = dlp_api::instruction_builder::preallocate_buffer( + validator.pubkey(), + DELEGATED_PDA_ID, + PreallocateBufferKind::DelegatedAccount, + target_size, + ); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&validator.pubkey()), + &[&validator], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok(), "{:?}", res); + + let delegated_account = + banks.get_account(DELEGATED_PDA_ID).await.unwrap().unwrap(); + assert_eq!(delegated_account.data.len(), target_size as usize); +} + +#[tokio::test] +async fn test_preallocate_undelegate_buffer_ignores_commit_in_flight() { + // Unlike CommitState, UndelegateBuffer isn't gated on "no commit in + // flight": in a two-stage commit-and-undelegate, the undelegate buffer + // is prepared alongside finalize -- i.e. precisely while the prior + // stage's commit record is still initialized -- so it must not be + // blocked by it, same reasoning as DelegatedAccount. + let (banks, _, validator, blockhash) = + setup_program_test_env(true, None).await; + + // Must exceed MAX_PERMITTED_DATA_INCREASE, hence two growth steps. + let target_size: u32 = MAX_PERMITTED_DATA_INCREASE as u32 + 4_760; // 15_000 + let ixs = dlp_api::instruction_builder::preallocate_buffer_chunks( + validator.pubkey(), + DELEGATED_PDA_ID, + PreallocateBufferKind::UndelegateBuffer, + 0, + target_size, + ); + let tx = Transaction::new_signed_with_payer( + &ixs, + Some(&validator.pubkey()), + &[&validator], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok(), "{:?}", res); + + let undelegate_buffer_pda = + undelegate_buffer_pda_from_delegated_account(&DELEGATED_PDA_ID); + let undelegate_buffer_account = banks + .get_account(undelegate_buffer_pda) + .await + .unwrap() + .unwrap(); + assert_eq!(undelegate_buffer_account.data.len(), target_size as usize); +} + +#[tokio::test] +async fn test_preallocate_rejects_wrong_authority() { + let other_validator = Keypair::new(); + let (banks, _, _validator, blockhash) = + setup_program_test_env(false, Some(other_validator.pubkey())).await; + + // `other_validator` is a registered validator (has a fees vault) but is + // not this delegation's authority. + let ix = dlp_api::instruction_builder::preallocate_buffer( + other_validator.pubkey(), + DELEGATED_PDA_ID, + PreallocateBufferKind::CommitState, + 500, + ); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&other_validator.pubkey()), + &[&other_validator], + blockhash, + ); + let err = banks.process_transaction(tx).await.unwrap_err(); + assert_custom_error(err, DlpError::InvalidAuthority); +} + +#[tokio::test] +async fn test_preallocate_rejects_unregistered_validator() { + let (banks, _, validator, blockhash) = + setup_program_test_env(false, None).await; + + // Fresh validator with no validator_fees_vault PDA set up at all. + let unregistered = Keypair::new(); + let ix = dlp_api::instruction_builder::preallocate_buffer( + unregistered.pubkey(), + DELEGATED_PDA_ID, + PreallocateBufferKind::CommitState, + 500, + ); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&validator.pubkey()), + &[&validator, &unregistered], + blockhash, + ); + let err = banks.process_transaction(tx).await.unwrap_err(); + assert!( + matches!( + err, + BanksClientError::TransactionError( + TransactionError::InstructionError( + _, + InstructionError::InvalidAccountOwner, + ) + ) + ), + "expected InvalidAccountOwner, got {err:?}" + ); +} + +async fn setup_program_test_env( + with_in_flight_commit: bool, + extra_registered_validator: Option, +) -> (BanksClient, Keypair, Keypair, Hash) { + let mut program_test = ProgramTest::new("dlp", dlp_api::ID, None); + program_test.prefer_bpf(true); + + let validator_keypair = + crate::fixtures::keypair_from_bytes(&TEST_AUTHORITY); + + 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_api::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_api::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_api::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_api::id(), + executable: false, + rent_epoch: 0, + }, + ); + + if let Some(other_validator) = extra_registered_validator { + program_test.add_account( + other_validator, + Account { + lamports: 10 * LAMPORTS_PER_SOL, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + program_test.add_account( + validator_fees_vault_pda_from_validator(&other_validator), + Account { + lamports: LAMPORTS_PER_SOL, + data: vec![], + owner: dlp_api::id(), + executable: false, + rent_epoch: 0, + }, + ); + } + + if with_in_flight_commit { + let commit_record_data = + get_commit_record_account_data(validator_keypair.pubkey()); + program_test.add_account( + commit_record_pda_from_delegated_account(&DELEGATED_PDA_ID), + Account { + lamports: Rent::default() + .minimum_balance(commit_record_data.len()), + data: commit_record_data, + owner: dlp_api::id(), + executable: false, + rent_epoch: 0, + }, + ); + } + + // Absent program config PDA is treated as "no whitelist configured" by + // `require_program_config`, so it's intentionally left unset here. + let _ = program_config_from_program_id(&DELEGATED_PDA_OWNER_ID); + + let (banks, payer, blockhash) = program_test.start().await; + (banks, payer, validator_keypair, blockhash) +}