diff --git a/interface/src/args/commit_finalize_from_buffer.rs b/interface/src/args/commit_finalize_from_buffer.rs new file mode 100644 index 00000000..89389021 --- /dev/null +++ b/interface/src/args/commit_finalize_from_buffer.rs @@ -0,0 +1,32 @@ +use wheels::variable_offset_layout; + +use crate::compat::Pubkey; + +#[derive(Clone, Debug, PartialEq, Eq)] +#[variable_offset_layout(buffer_offset = 1)] +pub struct CommitFinalizeFromBufferArgs { + /// Next commit sequence number for this delegated account and its StateBuffer. + pub commit_seq: u32, + + /// Account lamports on the ER after this commit. + pub lamports: u64, + + /// Account owner committed by the ER. + pub owner: Pubkey, + + /// Whether the validator requests undelegation after this commit. + pub allow_undelegation: bool, + + /// Stored as `CommitPayloadKind::value()`. + pub payload_kind: u8, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +#[variable_offset_layout(buffer_offset = 1)] +pub struct PrepareCommitFinalizeArgs { + /// Next commit sequence number whose finalization needs this account size. + pub commit_seq: u32, + + /// Final delegated-account data length. + pub target_data_len: u32, +} diff --git a/interface/src/args/mod.rs b/interface/src/args/mod.rs index c7c35e5f..66d1b7b3 100644 --- a/interface/src/args/mod.rs +++ b/interface/src/args/mod.rs @@ -3,6 +3,7 @@ mod challenger_reveal; mod commit_finalize; +mod commit_finalize_from_buffer; mod delegate; mod init_delegation_cluster; mod propose_commitment; @@ -16,6 +17,7 @@ mod write_state_buffer; pub use challenger_reveal::*; pub use commit_finalize::*; +pub use commit_finalize_from_buffer::*; pub use delegate::*; pub use init_delegation_cluster::*; pub use propose_commitment::*; diff --git a/interface/src/instruction.rs b/interface/src/instruction.rs index bae5e411..5a518a7b 100644 --- a/interface/src/instruction.rs +++ b/interface/src/instruction.rs @@ -19,7 +19,7 @@ pub enum DlpV2Instruction { ProposeCommitment = 105, /// Records approval from the selected verifier for a v2 commitment. ApproveCommitment = 106, - /// Writes full account-state bytes into a v2 state buffer. + /// Writes account-state payload bytes into a state buffer. /// /// TODO (snawaz/optimization): we can split this into two instructions such that /// InitStateBuffer takes more arguments and AppendStateBuffer takes as less as @@ -37,6 +37,10 @@ pub enum DlpV2Instruction { Delegate = 112, /// Commits and immediately applies inline state for a Direct cluster. CommitFinalize = 113, + /// Commits and applies state from a finalized buffer for a Direct cluster. + CommitFinalizeFromBuffer = 114, + /// Grows a delegated account by one step before a large direct finalization. + PrepareCommitFinalize = 115, } impl DlpV2Instruction { diff --git a/interface/src/instruction_builder/commit_finalize_from_buffer.rs b/interface/src/instruction_builder/commit_finalize_from_buffer.rs new file mode 100644 index 00000000..b4517de1 --- /dev/null +++ b/interface/src/instruction_builder/commit_finalize_from_buffer.rs @@ -0,0 +1,106 @@ +use solana_program::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, +}; +use solana_sdk_ids::system_program; +use wheels::{layout::Encodable, DataLayoutError}; + +use crate::{ + compat::{Compatize, Modernize}, + pda::{delegation_state_pda, state_buffer_pda}, + CommitFinalizeFromBufferArgs, DlpV2Instruction, PrepareCommitFinalizeArgs, +}; + +/// Builds a direct-cluster commit that consumes a finalized StateBuffer. +pub fn commit_finalize_from_buffer( + current_validator: Pubkey, + delegation_cluster: Pubkey, + delegated_account: Pubkey, + args: CommitFinalizeFromBufferArgs, +) -> Result { + Ok(Instruction { + program_id: crate::id().modernize(), + accounts: vec![ + AccountMeta::new(current_validator, true), + AccountMeta::new(delegated_account, false), + AccountMeta::new( + delegation_state_pda(&delegated_account.compatize()) + .modernize(), + false, + ), + AccountMeta::new_readonly( + state_buffer_pda( + &delegated_account.compatize(), + args.commit_seq, + ¤t_validator.compatize(), + ) + .modernize(), + false, + ), + AccountMeta::new_readonly(delegation_cluster, false), + AccountMeta::new_readonly(system_program::id(), false), + ], + data: [ + DlpV2Instruction::CommitFinalizeFromBuffer.to_vec(), + args.encode()?, + ] + .concat(), + }) +} + +/// Builds one growth step to run before finalizing a large account update. +pub fn prepare_commit_finalize( + current_validator: Pubkey, + delegation_cluster: Pubkey, + delegated_account: Pubkey, + args: PrepareCommitFinalizeArgs, +) -> Result { + Ok(Instruction { + program_id: crate::id().modernize(), + accounts: vec![ + AccountMeta::new(current_validator, true), + AccountMeta::new(delegated_account, false), + AccountMeta::new_readonly( + delegation_state_pda(&delegated_account.compatize()) + .modernize(), + false, + ), + AccountMeta::new_readonly(delegation_cluster, false), + AccountMeta::new_readonly(system_program::id(), false), + ], + data: [ + DlpV2Instruction::PrepareCommitFinalize.to_vec(), + args.encode()?, + ] + .concat(), + }) +} + +/// Builds every growth step needed before finalizing `target_data_len`. +pub fn prepare_commit_finalize_instructions( + current_validator: Pubkey, + delegation_cluster: Pubkey, + delegated_account: Pubkey, + commit_seq: u32, + current_data_len: usize, + target_data_len: u32, +) -> Result, DataLayoutError> { + let growth = (target_data_len as usize).saturating_sub(current_data_len); + let step_count = + growth.div_ceil(crate::StateBuffer::MAX_ACCOUNT_DATA_GROWTH_PER_WRITE); + let mut instructions = Vec::with_capacity(step_count); + + for _ in 0..step_count { + instructions.push(prepare_commit_finalize( + current_validator, + delegation_cluster, + delegated_account, + PrepareCommitFinalizeArgs { + commit_seq, + target_data_len, + }, + )?); + } + + Ok(instructions) +} diff --git a/interface/src/instruction_builder/mod.rs b/interface/src/instruction_builder/mod.rs index db2640ae..8210273a 100644 --- a/interface/src/instruction_builder/mod.rs +++ b/interface/src/instruction_builder/mod.rs @@ -1,6 +1,7 @@ mod approve_commitment; mod challenger_reveal; mod commit_finalize; +mod commit_finalize_from_buffer; mod delegate; mod finalize_commitment; mod init_delegation_cluster; @@ -16,6 +17,7 @@ mod write_state_buffer; pub use approve_commitment::*; pub use challenger_reveal::*; pub use commit_finalize::*; +pub use commit_finalize_from_buffer::*; pub use delegate::*; pub use finalize_commitment::*; pub use init_delegation_cluster::*; diff --git a/interface/src/instruction_builder/write_state_buffer.rs b/interface/src/instruction_builder/write_state_buffer.rs index 4c5417be..71d89ba7 100644 --- a/interface/src/instruction_builder/write_state_buffer.rs +++ b/interface/src/instruction_builder/write_state_buffer.rs @@ -11,7 +11,7 @@ use crate::{ DlpV2Instruction, WriteStateBufferArgs, }; -/// Builds the instruction that writes full account-state bytes to a v2 buffer. +/// Builds the instruction that writes account-state payload bytes to a buffer. pub fn write_state_buffer( payer: Pubkey, authority: Pubkey, diff --git a/interface/src/state/state_buffer.rs b/interface/src/state/state_buffer.rs index f375f9d8..4756cc8a 100644 --- a/interface/src/state/state_buffer.rs +++ b/interface/src/state/state_buffer.rs @@ -11,6 +11,9 @@ pub struct StateBuffer { /// Account type marker. pub discriminator: [u8; 8], + /// Canonical PDA bump for this buffer. + pub bump: u8, + /// Writer that owns this opened buffer. /// /// This is the operator identity for an operator commitment buffer, or the @@ -32,7 +35,10 @@ pub struct StateBuffer { /// Once true, buffer content cannot change except exact duplicate retries. pub finalized: bool, - /// Active payload bytes for `ProposeCommitment` or `RaiseChallenge`. + /// Keeps `payload` four-byte aligned for zero-copy diff decoding. + pub payload_alignment_padding: [u8; 2], + + /// Active payload bytes for commitment, challenge, or direct finalization. /// /// This is not a serialized Solana account. Depending on the flow, it can /// be the delegated account's complete `Account::data` bytes or an encoded diff --git a/program/src/processor/direct/commit_finalize_from_buffer.rs b/program/src/processor/direct/commit_finalize_from_buffer.rs new file mode 100644 index 00000000..40d4ad28 --- /dev/null +++ b/program/src/processor/direct/commit_finalize_from_buffer.rs @@ -0,0 +1,158 @@ +use delegation_program_interface as dpi; +use dpi::{ + error::DlpError, pda::STATE_BUFFER_SEED, CommitFinalizeFromBufferArgs, + CommitPayloadKind, StateBuffer, +}; +use pinocchio::{ + address::address_eq, error::ProgramError, AccountView, ProgramResult, +}; +use wheels::{ + layout::Decodable, require_eq, require_eq_keys, require_n_accounts, +}; + +use crate::processor::internal::{ + apply_commit_finalize, CommitFinalizeInput, CommitStateUpdate, +}; + +/// Commit and apply one finalized StateBuffer for a Direct cluster. +/// +/// Accounts: +/// 0: `[signer, writable]` current validator and lamport settlement account +/// 1: `[writable]` delegated account +/// 2: `[writable]` DelegationState PDA +/// 3: `[]` finalized StateBuffer PDA +/// 4: `[]` DelegationCluster PDA +/// 5: `[]` system program, required by system CPI +#[inline(never)] +pub fn process_commit_finalize_from_buffer( + accounts: &[AccountView], + data: &[u8], +) -> ProgramResult { + let [ + current_validator, // force multi-line + delegated_account, + delegation_state, + state_buffer, + delegation_cluster, + _system_program, + ] = require_n_accounts!(accounts, 6); + + let args = CommitFinalizeFromBufferArgs::decode(data)?; + let state_buffer_data = state_buffer.try_borrow()?; + let buffer = StateBuffer::decode(state_buffer_data.as_ref())?; + validate_state_buffer( + current_validator, + delegated_account, + state_buffer, + &buffer, + args.commit_seq(), + )?; + + let state_update = match args.payload_kind() { + value if value == CommitPayloadKind::FullData.value() => { + CommitStateUpdate::FullData(buffer.payload().as_slice()) + } + value if value == CommitPayloadKind::Diff.value() => { + CommitStateUpdate::Diff(dpi::diff::DiffSet::try_new( + buffer.payload().as_slice(), + )?) + } + _ => return Err(ProgramError::InvalidInstructionData), + }; + + apply_commit_finalize( + current_validator, + delegated_account, + delegation_state, + delegation_cluster, + CommitFinalizeInput { + commit_seq: args.commit_seq(), + lamports: args.lamports(), + owner: args.owner(), + allow_undelegation: args.allow_undelegation(), + state_update, + }, + ) +} + +fn validate_state_buffer( + current_validator: &AccountView, + delegated_account: &AccountView, + state_buffer: &AccountView, + buffer: &dpi::StateBufferView<'_>, + commit_seq: u32, +) -> ProgramResult { + let commit_seq_bytes = commit_seq.to_le_bytes(); + let bump = [buffer.bump()]; + dpi::require_initialized_pda!( + state_buffer, + &[ + STATE_BUFFER_SEED, + delegated_account.address().as_ref(), + &commit_seq_bytes, + current_validator.address().as_ref(), + &bump, + ], + &crate::fast::ID, + false + ); + require_eq!( + &buffer.discriminator(), + &StateBuffer::DISCRIMINATOR, + ProgramError::InvalidAccountData + ); + require_eq_keys!( + buffer.authority(), + current_validator.address(), + DlpError::InvalidAuthority + ); + require_eq_keys!( + buffer.account_pubkey(), + delegated_account.address(), + DlpError::InvalidDelegatedAccount + ); + require_eq!( + buffer.commit_seq(), + commit_seq, + ProgramError::InvalidInstructionData + ); + require_eq!( + buffer.finalized(), + true, + ProgramError::InvalidInstructionData + ); + require_eq!( + &buffer.payload_alignment_padding(), + &[0; 2], + ProgramError::InvalidAccountData + ); + require_eq!( + buffer.payload().len(), + buffer.total_len() as usize, + ProgramError::InvalidInstructionData + ); + require_eq!( + buffer.payload().capacity(), + buffer.total_len() as usize, + ProgramError::InvalidInstructionData + ); + require_eq!( + state_buffer.data_len(), + StateBuffer::data_len_from_payload_capacity( + buffer.payload().capacity() + )?, + ProgramError::InvalidAccountData + ); + require_eq!( + buffer.data_hash(), + &account_data_hash(buffer.payload().as_slice()), + ProgramError::InvalidInstructionData + ); + + Ok(()) +} + +fn account_data_hash(data: &[u8]) -> [u8; 32] { + solana_sha256_hasher::hashv(&[b"magicblock.account_data.v1", data]) + .to_bytes() +} diff --git a/program/src/processor/direct/mod.rs b/program/src/processor/direct/mod.rs index 64a66c1e..9d041f5d 100644 --- a/program/src/processor/direct/mod.rs +++ b/program/src/processor/direct/mod.rs @@ -1,5 +1,9 @@ //! Processors for clusters that finalize without fraud proofs. mod commit_finalize; +mod commit_finalize_from_buffer; +mod prepare_commit_finalize; pub use commit_finalize::*; +pub use commit_finalize_from_buffer::*; +pub use prepare_commit_finalize::*; diff --git a/program/src/processor/direct/prepare_commit_finalize.rs b/program/src/processor/direct/prepare_commit_finalize.rs new file mode 100644 index 00000000..aff78e12 --- /dev/null +++ b/program/src/processor/direct/prepare_commit_finalize.rs @@ -0,0 +1,105 @@ +use delegation_program_interface as dpi; +use dpi::{ + error::DlpError, DelegationCluster, DelegationState, + PrepareCommitFinalizeArgs, StateBuffer, +}; +use pinocchio::{ + error::ProgramError, + sysvars::{rent::Rent, Sysvar}, + AccountView, ProgramResult, +}; +use pinocchio_system::instructions as system; +use wheels::{ + layout::Decodable, require, require_ge, require_le, require_n_accounts, + require_owned_by, require_signer, +}; + +use crate::processor::internal::{ + validate_commit_state, validate_direct_cluster, +}; + +/// Grow a delegated account by one runtime-permitted step before finalization. +/// +/// Repeat this instruction in the same transaction as finalization when the +/// target size exceeds one growth step. +/// +/// Accounts: +/// 0: `[signer, writable]` current validator and rent payer +/// 1: `[writable]` delegated account +/// 2: `[]` DelegationState PDA +/// 3: `[]` DelegationCluster PDA +/// 4: `[]` system program, required by system CPI +#[inline(never)] +pub fn process_prepare_commit_finalize( + accounts: &[AccountView], + data: &[u8], +) -> ProgramResult { + let [ + current_validator, // force multi-line + delegated_account, + delegation_state, + delegation_cluster, + _system_program, + ] = require_n_accounts!(accounts, 5); + + require_signer!(current_validator); + require!(current_validator.is_writable(), ProgramError::Immutable); + require_owned_by!(delegated_account, &crate::fast::ID); + require!(delegated_account.is_writable(), ProgramError::Immutable); + + let args = PrepareCommitFinalizeArgs::decode(data)?; + require_le!( + args.target_data_len() as usize, + StateBuffer::MAX_ACCOUNT_DATA_LEN, + ProgramError::InvalidInstructionData + ); + + let cluster_data = delegation_cluster.try_borrow()?; + let cluster = DelegationCluster::decode(cluster_data.as_ref())?; + validate_direct_cluster(current_validator, delegation_cluster, &cluster)?; + drop(cluster_data); + + let state_data = delegation_state.try_borrow()?; + let state = DelegationState::decode(state_data.as_ref())?; + validate_commit_state( + delegated_account, + delegation_state, + delegation_cluster, + &state, + args.commit_seq(), + false, + )?; + require_ge!( + delegated_account.lamports(), + state.lamports(), + DlpError::InvalidDelegatedState + ); + drop(state_data); + + let target_data_len = args.target_data_len() as usize; + if delegated_account.data_len() >= target_data_len { + return Ok(()); + } + + delegated_account.resize( + delegated_account + .data_len() + .saturating_add(pinocchio::account::MAX_PERMITTED_DATA_INCREASE) + .min(target_data_len), + )?; + + let minimum_balance = + Rent::get()?.try_minimum_balance(delegated_account.data_len())?; + if delegated_account.lamports() < minimum_balance { + system::Transfer { + from: current_validator, + to: delegated_account, + lamports: minimum_balance + .checked_sub(delegated_account.lamports()) + .ok_or(DlpError::Overflow)?, + } + .invoke()?; + } + + Ok(()) +} diff --git a/program/src/processor/fraud_proofs/write_state_buffer.rs b/program/src/processor/fraud_proofs/write_state_buffer.rs index 54363e59..5698d8d2 100644 --- a/program/src/processor/fraud_proofs/write_state_buffer.rs +++ b/program/src/processor/fraud_proofs/write_state_buffer.rs @@ -107,12 +107,14 @@ pub fn process_write_state_buffer( StateBuffer { discriminator: StateBuffer::DISCRIMINATOR, + bump: state_buffer_bump, authority: *authority.address(), account_pubkey: *delegated_account.address(), commit_seq: args.commit_seq(), data_hash: [0; 32], total_len: args.total_len(), finalized: false, + payload_alignment_padding: [0; 2], payload: Vec::new(), } .encode_to(state_buffer.try_borrow_mut()?.as_mut())?; @@ -283,6 +285,11 @@ fn validate_state_buffer( args.total_len(), ProgramError::InvalidInstructionData ); + require_eq!( + &state.payload_alignment_padding(), + &[0; 2], + ProgramError::InvalidAccountData + ); require_le!( state.payload().len(), state.total_len() as usize, diff --git a/program/src/processor/internal/commit_finalize.rs b/program/src/processor/internal/commit_finalize.rs index ccc7b4ff..6f94a9c2 100644 --- a/program/src/processor/internal/commit_finalize.rs +++ b/program/src/processor/internal/commit_finalize.rs @@ -2,7 +2,8 @@ use delegation_program_interface as dpi; use dpi::{ error::DlpError, pda::{DELEGATION_CLUSTER_SEED, DELEGATION_STATE_SEED}, - DelegationCluster, DelegationState, UndelegationRequester, + DelegationCluster, DelegationClusterView, DelegationState, + DelegationStateView, UndelegationRequester, }; use pinocchio::{ address::address_eq, @@ -56,77 +57,24 @@ pub(crate) fn apply_commit_finalize( let cluster_data = delegation_cluster.try_borrow()?; let cluster = DelegationCluster::decode(cluster_data.as_ref())?; - let cluster_bump = [cluster.bump()]; - dpi::require_initialized_pda!( - delegation_cluster, - &[ - DELEGATION_CLUSTER_SEED, - cluster.cluster_id().as_ref(), - &cluster_bump, - ], - &crate::fast::ID, - false - ); - require_eq!( - &cluster.discriminator(), - &DelegationCluster::DISCRIMINATOR, - ProgramError::InvalidAccountData - ); - require!(!cluster.paused(), ProgramError::InvalidAccountData); - require_eq!( - cluster.challenge_config().is_none(), - true, - ProgramError::InvalidInstructionData - ); - require_eq_keys!( - cluster.current_validator(), - current_validator.address(), - DlpError::InvalidAuthority - ); + validate_direct_cluster(current_validator, delegation_cluster, &cluster)?; drop(cluster_data); let state_data = delegation_state.try_borrow()?; let state = DelegationState::decode(state_data.as_ref())?; - let state_bump = [state.bump()]; - dpi::require_initialized_pda!( + validate_commit_state( + delegated_account, delegation_state, - &[ - DELEGATION_STATE_SEED, - delegated_account.address().as_ref(), - &state_bump, - ], - &crate::fast::ID, - true - ); - require_eq!( - &state.discriminator(), - &DelegationState::DISCRIMINATOR, - ProgramError::InvalidAccountData - ); - require_eq_keys!( - state.delegated_account(), - delegated_account.address(), - DlpError::InvalidDelegatedAccount - ); - require_eq_keys!( - state.delegation_cluster(), - delegation_cluster.address(), - ProgramError::InvalidAccountData - ); + delegation_cluster, + &state, + input.commit_seq, + true, + )?; require_eq_keys!( state.original_owner(), input.owner, ProgramError::InvalidAccountData ); - require_eq!( - input.commit_seq, - state - .last_commit_seq() - .checked_add(1) - .ok_or(DlpError::Overflow)?, - DlpError::NonceOutOfOrder - ); - let next_undelegation_requester = match state.undelegation_requester() { value if value == UndelegationRequester::None.value() => { if input.allow_undelegation { @@ -188,6 +136,88 @@ pub(crate) fn apply_commit_finalize( Ok(()) } +pub(crate) fn validate_direct_cluster( + current_validator: &AccountView, + delegation_cluster: &AccountView, + cluster: &DelegationClusterView<'_>, +) -> ProgramResult { + let cluster_bump = [cluster.bump()]; + dpi::require_initialized_pda!( + delegation_cluster, + &[ + DELEGATION_CLUSTER_SEED, + cluster.cluster_id().as_ref(), + &cluster_bump, + ], + &crate::fast::ID, + false + ); + require_eq!( + &cluster.discriminator(), + &DelegationCluster::DISCRIMINATOR, + ProgramError::InvalidAccountData + ); + require!(!cluster.paused(), ProgramError::InvalidAccountData); + require_eq!( + cluster.challenge_config().is_none(), + true, + ProgramError::InvalidInstructionData + ); + require_eq_keys!( + cluster.current_validator(), + current_validator.address(), + DlpError::InvalidAuthority + ); + + Ok(()) +} + +pub(crate) fn validate_commit_state( + delegated_account: &AccountView, + delegation_state: &AccountView, + delegation_cluster: &AccountView, + state: &DelegationStateView<'_>, + commit_seq: u64, + is_writable: bool, +) -> ProgramResult { + let state_bump = [state.bump()]; + dpi::require_initialized_pda!( + delegation_state, + &[ + DELEGATION_STATE_SEED, + delegated_account.address().as_ref(), + &state_bump, + ], + &crate::fast::ID, + is_writable + ); + require_eq!( + &state.discriminator(), + &DelegationState::DISCRIMINATOR, + ProgramError::InvalidAccountData + ); + require_eq_keys!( + state.delegated_account(), + delegated_account.address(), + DlpError::InvalidDelegatedAccount + ); + require_eq_keys!( + state.delegation_cluster(), + delegation_cluster.address(), + ProgramError::InvalidAccountData + ); + require_eq!( + commit_seq, + state + .last_commit_seq() + .checked_add(1) + .ok_or(DlpError::Overflow)?, + DlpError::NonceOutOfOrder + ); + + Ok(()) +} + fn settle_lamports( current_validator: &AccountView, delegated_account: &AccountView, diff --git a/program/src/processor/mod.rs b/program/src/processor/mod.rs index 81cb2afa..6b76be2a 100644 --- a/program/src/processor/mod.rs +++ b/program/src/processor/mod.rs @@ -59,5 +59,11 @@ pub fn process_instruction( DlpV2Instruction::CommitFinalize => { process_commit_finalize(accounts, data) } + DlpV2Instruction::CommitFinalizeFromBuffer => { + process_commit_finalize_from_buffer(accounts, data) + } + DlpV2Instruction::PrepareCommitFinalize => { + process_prepare_commit_finalize(accounts, data) + } } } diff --git a/program/tests/test_primary_commit_finalize_from_buffer.rs b/program/tests/test_primary_commit_finalize_from_buffer.rs new file mode 100644 index 00000000..36a92f0f --- /dev/null +++ b/program/tests/test_primary_commit_finalize_from_buffer.rs @@ -0,0 +1,376 @@ +use delegation_program::solana_program; +use delegation_program_interface as dpi; +use dpi::{ + diff::compute_diff, + instruction_builder::{ + commit_finalize_from_buffer, prepare_commit_finalize_instructions, + write_state_buffer, + }, + pda::{delegation_cluster_pda, delegation_state_pda, state_buffer_pda}, + CommitFinalizeFromBufferArgs, CommitPayloadKind, DelegationCluster, + DelegationState, StateBuffer, UndelegationRequester, WriteStateBufferArgs, +}; +use solana_program::{ + native_token::LAMPORTS_PER_SOL, pubkey::Pubkey, rent::Rent, +}; +use solana_program_test::{BanksClient, ProgramTest}; +use solana_sdk::{ + account::Account, + instruction::Instruction, + signature::{Keypair, Signer}, + transaction::Transaction, +}; +use solana_sdk_ids::system_program; +use wheels::layout::{Decodable, Encodable}; + +const INITIAL_LAMPORTS: u64 = LAMPORTS_PER_SOL; + +struct CommitFinalizeFromBufferTestContext { + banks: BanksClient, + payer: Keypair, + current_validator: Keypair, + delegated_account: Pubkey, + delegation_state: Pubkey, + delegation_cluster: Pubkey, + original_owner: Pubkey, +} + +struct PreloadedBuffer { + commit_seq: u32, + payload: Vec, + data_hash: [u8; 32], +} + +#[tokio::test] +async fn test_commit_finalize_from_buffer_applies_large_chunked_payload() { + let initial_data = b"initial data"; + let context = setup_test(initial_data, None).await; + let target_len = 2 * pinocchio::account::MAX_PERMITTED_DATA_INCREASE + 17; + let payload = (0..target_len) + .map(|index| (index % u8::MAX as usize) as u8) + .collect::>(); + + upload_payload(&context, 1, &payload).await; + + let mut instructions = prepare_commit_finalize_instructions( + context.current_validator.pubkey(), + context.delegation_cluster, + context.delegated_account, + 1, + initial_data.len(), + target_len as u32, + ) + .unwrap(); + assert_eq!(instructions.len(), 3); + instructions.push(finalize_instruction( + &context, + 1, + CommitPayloadKind::FullData, + )); + submit_instructions(&context, instructions).await.unwrap(); + + let delegated_account = context + .banks + .get_account(context.delegated_account) + .await + .unwrap() + .unwrap(); + let state_account = context + .banks + .get_account(context.delegation_state) + .await + .unwrap() + .unwrap(); + let state = DelegationState::decode(&state_account.data).unwrap(); + let buffer_account = context + .banks + .get_account(state_buffer_pda( + &context.delegated_account, + 1, + &context.current_validator.pubkey(), + )) + .await + .unwrap() + .unwrap(); + + assert_eq!(delegated_account.data, payload); + assert_eq!(state.last_commit_seq(), 1); + assert_eq!(state.lamports(), INITIAL_LAMPORTS); + assert!(StateBuffer::decode(&buffer_account.data) + .unwrap() + .finalized()); +} + +#[tokio::test] +async fn test_commit_finalize_from_buffer_applies_aligned_diff() { + let initial_data = b"the original account state"; + let context = setup_test(initial_data, None).await; + let changed_data = b"the changed account state!"; + let diff = compute_diff(initial_data, changed_data).to_vec(); + + upload_payload(&context, 1, &diff).await; + submit_instructions( + &context, + vec![finalize_instruction(&context, 1, CommitPayloadKind::Diff)], + ) + .await + .unwrap(); + + let delegated_account = context + .banks + .get_account(context.delegated_account) + .await + .unwrap() + .unwrap(); + assert_eq!(delegated_account.data, changed_data); +} + +#[tokio::test] +async fn test_commit_finalize_from_buffer_rejects_invalid_finalized_hash() { + let payload = b"buffered state".to_vec(); + let context = setup_test( + b"initial data", + Some(PreloadedBuffer { + commit_seq: 1, + payload, + data_hash: [7; 32], + }), + ) + .await; + + assert!(submit_instructions( + &context, + vec![finalize_instruction( + &context, + 1, + CommitPayloadKind::FullData, + )], + ) + .await + .is_err()); + + let state_account = context + .banks + .get_account(context.delegation_state) + .await + .unwrap() + .unwrap(); + assert_eq!( + DelegationState::decode(&state_account.data) + .unwrap() + .last_commit_seq(), + 0 + ); +} + +fn finalize_instruction( + context: &CommitFinalizeFromBufferTestContext, + commit_seq: u32, + payload_kind: CommitPayloadKind, +) -> Instruction { + commit_finalize_from_buffer( + context.current_validator.pubkey(), + context.delegation_cluster, + context.delegated_account, + CommitFinalizeFromBufferArgs { + commit_seq, + lamports: INITIAL_LAMPORTS, + owner: context.original_owner, + allow_undelegation: false, + payload_kind: payload_kind.value(), + }, + ) + .unwrap() +} + +async fn upload_payload( + context: &CommitFinalizeFromBufferTestContext, + commit_seq: u32, + payload: &[u8], +) { + let mut offset = 0; + while offset < payload.len() { + let end = (offset + 512).min(payload.len()); + let instruction = write_state_buffer( + context.payer.pubkey(), + context.current_validator.pubkey(), + context.delegated_account, + WriteStateBufferArgs { + commit_seq, + total_len: payload.len() as u32, + offset: offset as u32, + chunk: payload[offset..end].to_vec(), + }, + ); + let tx = Transaction::new_signed_with_payer( + &[instruction], + Some(&context.payer.pubkey()), + &[&context.payer, &context.current_validator], + context.banks.get_latest_blockhash().await.unwrap(), + ); + context.banks.process_transaction(tx).await.unwrap(); + offset = end; + } +} + +async fn submit_instructions( + context: &CommitFinalizeFromBufferTestContext, + instructions: Vec, +) -> Result<(), solana_program_test::BanksClientError> { + let tx = Transaction::new_signed_with_payer( + &instructions, + Some(&context.payer.pubkey()), + &[&context.payer, &context.current_validator], + context.banks.get_latest_blockhash().await.unwrap(), + ); + context.banks.process_transaction(tx).await +} + +async fn setup_test( + initial_data: &[u8], + preloaded_buffer: Option, +) -> CommitFinalizeFromBufferTestContext { + let mut program_test = ProgramTest::default(); + program_test.prefer_bpf(true); + program_test.add_program("delegation_program", dpi::ID, None); + + let current_validator = Keypair::new(); + let delegated_account = Pubkey::new_unique(); + let original_owner = Pubkey::new_unique(); + let cluster_id = Pubkey::new_unique(); + let delegation_cluster = delegation_cluster_pda(&cluster_id); + let (_, cluster_bump) = Pubkey::find_program_address( + &[dpi::pda::DELEGATION_CLUSTER_SEED, cluster_id.as_ref()], + &dpi::id(), + ); + let delegation_state = delegation_state_pda(&delegated_account); + let (_, state_bump) = Pubkey::find_program_address( + &[dpi::pda::DELEGATION_STATE_SEED, delegated_account.as_ref()], + &dpi::id(), + ); + + program_test.add_account( + current_validator.pubkey(), + Account { + lamports: 10 * LAMPORTS_PER_SOL, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + program_test.add_account( + delegated_account, + Account { + lamports: INITIAL_LAMPORTS, + data: initial_data.to_vec(), + owner: dpi::id(), + executable: false, + rent_epoch: 0, + }, + ); + + let cluster_data = DelegationCluster { + discriminator: DelegationCluster::DISCRIMINATOR, + bump: cluster_bump, + cluster_id, + config_authority: Pubkey::new_unique(), + validator_rotation_authority: Pubkey::new_unique(), + current_validator: current_validator.pubkey(), + paused: false, + protocol_fee_vault: Pubkey::new_unique(), + challenge_config: None, + } + .encode() + .unwrap(); + program_test.add_account( + delegation_cluster, + Account { + lamports: Rent::default().minimum_balance(cluster_data.len()), + data: cluster_data, + owner: dpi::id(), + executable: false, + rent_epoch: 0, + }, + ); + + let state_data = DelegationState { + discriminator: DelegationState::DISCRIMINATOR, + bump: state_bump, + delegated_account, + delegation_cluster, + original_owner, + rent_payer: Pubkey::new_unique(), + delegation_slot: 0, + lamports: INITIAL_LAMPORTS, + last_commit_seq: 0, + undelegation_requester: UndelegationRequester::None.value(), + delegated_account_bump: 0, + seed_count: 0, + encoded_seeds: vec![], + } + .encode() + .unwrap(); + program_test.add_account( + delegation_state, + Account { + lamports: Rent::default().minimum_balance(state_data.len()), + data: state_data, + owner: dpi::id(), + executable: false, + rent_epoch: 0, + }, + ); + + if let Some(preloaded) = preloaded_buffer { + let state_buffer = state_buffer_pda( + &delegated_account, + preloaded.commit_seq, + ¤t_validator.pubkey(), + ); + let (_, bump) = Pubkey::find_program_address( + &[ + dpi::pda::STATE_BUFFER_SEED, + delegated_account.as_ref(), + &preloaded.commit_seq.to_le_bytes(), + current_validator.pubkey().as_ref(), + ], + &dpi::id(), + ); + let buffer_data = StateBuffer { + discriminator: StateBuffer::DISCRIMINATOR, + bump, + authority: current_validator.pubkey(), + account_pubkey: delegated_account, + commit_seq: preloaded.commit_seq, + data_hash: preloaded.data_hash, + total_len: preloaded.payload.len() as u32, + finalized: true, + payload_alignment_padding: [0; 2], + payload: preloaded.payload, + } + .encode() + .unwrap(); + program_test.add_account( + state_buffer, + Account { + lamports: Rent::default().minimum_balance(buffer_data.len()), + data: buffer_data, + owner: dpi::id(), + executable: false, + rent_epoch: 0, + }, + ); + } + + let (banks, payer, _) = program_test.start().await; + CommitFinalizeFromBufferTestContext { + banks, + payer, + current_validator, + delegated_account, + delegation_state, + delegation_cluster, + original_owner, + } +}