From 813769b348efd0ef10a7c7412e33a2099c0dd670 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Sun, 1 Mar 2026 12:49:26 +0530 Subject: [PATCH 1/7] Fix: handle lamports properly in CommitFinalize --- dlp-api/src/error.rs | 5 + .../instruction_builder/commit_finalize.rs | 4 +- .../commit_finalize_from_buffer.rs | 4 +- dlp-api/src/requires.rs | 18 +-- src/processor/fast/commit_finalize.rs | 7 +- .../fast/commit_finalize_from_buffer.rs | 13 +- .../fast/internal/commit_finalize_internal.rs | 58 +++++-- src/processor/fast/utils/mod.rs | 28 ++++ tests/test_commit_finalize.rs | 151 ++++++++++++++++-- 9 files changed, 237 insertions(+), 51 deletions(-) diff --git a/dlp-api/src/error.rs b/dlp-api/src/error.rs index 9ab84b5c..ffb8533a 100644 --- a/dlp-api/src/error.rs +++ b/dlp-api/src/error.rs @@ -149,6 +149,11 @@ pub enum DlpError { #[error("Account cannot be delegated to the system program")] DelegationToSystemProgramNotAllowed = 42, + #[error( + "The account lamports is too small to make the account rent-exempt" + )] + InsufficientRent = 43, + #[error("An infallible error is encountered possibly due to logic error")] InfallibleError = 100, } diff --git a/dlp-api/src/instruction_builder/commit_finalize.rs b/dlp-api/src/instruction_builder/commit_finalize.rs index 41701d54..c8a77157 100644 --- a/dlp-api/src/instruction_builder/commit_finalize.rs +++ b/dlp-api/src/instruction_builder/commit_finalize.rs @@ -55,9 +55,9 @@ pub fn commit_finalize( accounts: vec![ AccountMeta::new(validator, true), AccountMeta::new(delegated_account, false), - AccountMeta::new_readonly(delegation_record.0, false), + AccountMeta::new(delegation_record.0, false), AccountMeta::new(delegation_metadata.0, false), - AccountMeta::new_readonly(validator_fees_vault.0, false), + AccountMeta::new(validator_fees_vault.0, false), AccountMeta::new_readonly(system_program::id(), false), ], data: [ diff --git a/dlp-api/src/instruction_builder/commit_finalize_from_buffer.rs b/dlp-api/src/instruction_builder/commit_finalize_from_buffer.rs index 027afa80..63bd1275 100644 --- a/dlp-api/src/instruction_builder/commit_finalize_from_buffer.rs +++ b/dlp-api/src/instruction_builder/commit_finalize_from_buffer.rs @@ -49,10 +49,10 @@ pub fn commit_finalize_from_buffer( accounts: vec![ AccountMeta::new(validator, true), AccountMeta::new(delegated_account, false), - AccountMeta::new_readonly(delegation_record.0, false), + AccountMeta::new(delegation_record.0, false), AccountMeta::new(delegation_metadata.0, false), AccountMeta::new_readonly(data_buffer, false), - AccountMeta::new_readonly(validator_fees_vault.0, false), + AccountMeta::new(validator_fees_vault.0, false), AccountMeta::new_readonly(system_program::id(), false), ], data: [ diff --git a/dlp-api/src/requires.rs b/dlp-api/src/requires.rs index fbf7b77e..096e27ae 100644 --- a/dlp-api/src/requires.rs +++ b/dlp-api/src/requires.rs @@ -30,7 +30,7 @@ macro_rules! require { macro_rules! require_signer { ($info: expr) => {{ if !$info.is_signer() { - log!("require_signer!({}): ", stringify!($info)); + pinocchio_log::log!("require_signer!({}): ", stringify!($info)); $info.address().log(); return Err(ProgramError::MissingRequiredSignature); } @@ -243,7 +243,7 @@ macro_rules! require_initialized_pda { let pda = match pinocchio::Address::create_program_address($seeds, $program_id) { Ok(pda) => pda, Err(_) => { - log!( + pinocchio_log::log!( "require_initialized_pda!({}, {}, {}, {}); create_program_address failed", stringify!($info), stringify!($seeds), @@ -254,7 +254,7 @@ macro_rules! require_initialized_pda { } }; if !address_eq($info.address(), &pda) { - log!( + pinocchio_log::log!( "require_initialized_pda!({}, {}, {}, {}); address_eq failed", stringify!($info), stringify!($seeds), @@ -269,7 +269,7 @@ macro_rules! require_initialized_pda { require_owned_by!($info, $program_id); if $is_writable && !$info.is_writable() { - log!( + pinocchio_log::log!( "require_initialized_pda!({}, {}, {}, {}); is_writable expectation failed", stringify!($info), stringify!($seeds), @@ -287,7 +287,7 @@ macro_rules! require_initialized_pda_fast { ($info:expr, $seeds: expr, $is_writable: expr) => {{ let pda = solana_sha256_hasher::hashv($seeds).to_bytes(); if !address_eq($info.address(), &pda.into()) { - log!( + pinocchio_log::log!( "require_initialized_pda!({}, {}, {}); address_eq failed", stringify!($info), stringify!($seeds), @@ -300,7 +300,7 @@ macro_rules! require_initialized_pda_fast { require_owned_by!($info, &$crate::fast::ID); if $is_writable && !$info.is_writable() { - log!( + pinocchio_log::log!( "require_initialized_pda!({}, {}, {}); is_writable expectation failed", stringify!($info), stringify!($seeds), @@ -318,7 +318,7 @@ macro_rules! require_pda { let pda = match pinocchio::Address::create_program_address($seeds, $program_id) { Ok(pda) => pda, Err(_) => { - log!( + pinocchio_log::log!( "require_pda!({}, {}, {}, {}); create_program_address failed", stringify!($info), stringify!($seeds), @@ -329,7 +329,7 @@ macro_rules! require_pda { } }; if !address_eq($info.address(), &pda) { - log!( + pinocchio_log::log!( "require_pda!({}, {}, {}, {}); address_eq failed", stringify!($info), stringify!($seeds), @@ -342,7 +342,7 @@ macro_rules! require_pda { } if $is_writable && !$info.is_writable() { - log!( + pinocchio_log::log!( "require_pda!({}, {}, {}, {}); is_writable expectation failed", stringify!($info), stringify!($seeds), diff --git a/src/processor/fast/commit_finalize.rs b/src/processor/fast/commit_finalize.rs index 32c5bf01..e0718e39 100644 --- a/src/processor/fast/commit_finalize.rs +++ b/src/processor/fast/commit_finalize.rs @@ -19,10 +19,10 @@ use crate::{ /// Accounts: /// /// 0: `[signer]` the validator requesting the commit -/// 1: `[]` the delegated account -/// 2: `[]` the delegation record +/// 1: `[writable]` the delegated account +/// 2: `[writable]` the delegation record /// 3: `[writable]` the delegation metadata -/// 4: `[]` the validator fees vault +/// 4: `[writable]` the validator fees vault /// 5: `[]` system program /// /// Instruction Data: CommitFinalizeArgsWithBuffer @@ -54,6 +54,7 @@ pub fn process_commit_finalize( } else { NewState::FullBytes(args.buffer) }, + commit_lamports: args.lamports, commit_id: args.commit_id, allow_undelegation: args.allow_undelegation.is_true(), validator, diff --git a/src/processor/fast/commit_finalize_from_buffer.rs b/src/processor/fast/commit_finalize_from_buffer.rs index 8d2fb058..19826c06 100644 --- a/src/processor/fast/commit_finalize_from_buffer.rs +++ b/src/processor/fast/commit_finalize_from_buffer.rs @@ -18,12 +18,12 @@ use crate::{ /// Accounts: /// /// 0: `[signer]` the validator requesting the commit -/// 1: `[]` the delegated account -/// 2: `[writable]` the PDA storing the new state -/// 3: `[writable]` the PDA storing the commit record -/// 4: `[]` the delegation record -/// 5: `[writable]` the delegation metadata -/// 6: `[]` the validator fees vault +/// 1: `[writable]` the delegated account +/// 2: `[writable]` the delegation record +/// 3: `[writable]` the delegation metadata +/// 4: `[]` the data buffer +/// 5: `[writable]` the validator fees vault +/// 6: `[]` system program /// /// Instruction Data: CommitFinalizeArgs /// @@ -72,6 +72,7 @@ pub fn process_commit_finalize_from_buffer( } else { NewState::FullBytes(&data) }, + commit_lamports: args.lamports, commit_id: args.commit_id, allow_undelegation: args.allow_undelegation.is_true(), validator, diff --git a/src/processor/fast/internal/commit_finalize_internal.rs b/src/processor/fast/internal/commit_finalize_internal.rs index 9378dda5..99fc881b 100644 --- a/src/processor/fast/internal/commit_finalize_internal.rs +++ b/src/processor/fast/internal/commit_finalize_internal.rs @@ -1,9 +1,10 @@ use pinocchio::{ address::{address_eq, PDA_MARKER}, error::ProgramError, + sysvars::{rent::Rent, Sysvar}, AccountView, Address, }; -use pinocchio_log::log; +use pinocchio_system::instructions as system; use crate::{ apply_diff_in_place, @@ -11,7 +12,7 @@ use crate::{ error::DlpError, pda, pod_view::PodView, - processor::fast::NewState, + processor::fast::{utils::LamportsOperation, NewState}, require, require_eq, require_eq_keys, require_ge, require_initialized_pda_fast, require_owned_by, require_signer, state::{DelegationMetadataFast, DelegationRecord}, @@ -21,6 +22,7 @@ use crate::{ pub(crate) struct CommitFinalizeInternalArgs<'a> { pub(crate) bumps: &'a CommitBumps, pub(crate) new_state: NewState<'a>, + pub(crate) commit_lamports: u64, pub(crate) commit_id: u64, pub(crate) allow_undelegation: bool, pub(crate) validator: &'a AccountView, @@ -48,7 +50,7 @@ pub(crate) fn process_commit_finalize_internal( crate::fast::ID.as_ref(), PDA_MARKER ], - false + true ); require_initialized_pda_fast!( @@ -72,7 +74,7 @@ pub(crate) fn process_commit_finalize_internal( crate::fast::ID.as_ref(), PDA_MARKER ], - false + true ); // validate and update metadata @@ -91,9 +93,11 @@ pub(crate) fn process_commit_finalize_internal( ); } - let delegation_record_data = args.delegation_record_account.try_borrow()?; - let delegation_record = - DelegationRecord::try_view_from(&delegation_record_data.as_ref()[8..])?; + let mut delegation_record_data = + args.delegation_record_account.try_borrow_mut()?; + let delegation_record = DelegationRecord::try_view_from_mut( + &mut delegation_record_data.as_mut()[8..], + )?; // Check that the authority is allowed to commit require_eq_keys!( @@ -109,22 +113,42 @@ pub(crate) fn process_commit_finalize_internal( DlpError::InvalidDelegatedState ); - // if args.commit_record_lamports > delegation_record.lamports { - // system::Transfer { - // from: args.validator, - // to: args.commit_state_account, - // lamports: args.commit_record_lamports - delegation_record.lamports, - // } - // .invoke()?; - // } - args.delegated_account.resize(args.new_state.data_len())?; + match args.commit_lamports.cmp(&delegation_record.lamports) { + std::cmp::Ordering::Greater => { + system::Transfer { + from: args.validator, + to: args.delegated_account, + lamports: args.commit_lamports - delegation_record.lamports, + } + .invoke()?; + } + std::cmp::Ordering::Less => { + let amount = delegation_record.lamports - args.commit_lamports; + + args.delegated_account.lamports_decrement_by(amount)?; + args.validator_fees_vault.lamports_increment_by(amount)?; + + // require the account is still rent-exempted even after decrementing lamports + require_ge!( + args.delegated_account.lamports(), + Rent::get()? + .try_minimum_balance(args.delegated_account.data_len())?, + DlpError::InsufficientRent + ); + } + std::cmp::Ordering::Equal => {} + } + + // Update the delegation record lamports after settling. + delegation_record.lamports = args.delegated_account.lamports(); + // copy the new state to the delegated account let mut delegated_account_data = args.delegated_account.try_borrow_mut()?; match args.new_state { NewState::FullBytes(bytes) => { - (*delegated_account_data).copy_from_slice(bytes) + (*delegated_account_data).copy_from_slice(bytes); } NewState::Diff(diff) => { apply_diff_in_place(&mut delegated_account_data, &diff)?; diff --git a/src/processor/fast/utils/mod.rs b/src/processor/fast/utils/mod.rs index f52daa8e..6d08617c 100644 --- a/src/processor/fast/utils/mod.rs +++ b/src/processor/fast/utils/mod.rs @@ -1 +1,29 @@ +use pinocchio::{AccountView, ProgramResult}; + +use crate::error::DlpError; + pub(crate) mod pda; + +pub trait LamportsOperation { + fn lamports_increment_by(&self, value: u64) -> ProgramResult; + fn lamports_decrement_by(&self, value: u64) -> ProgramResult; +} + +impl LamportsOperation for AccountView { + fn lamports_increment_by(&self, value: u64) -> ProgramResult { + self.set_lamports( + self.lamports() + .checked_add(value) + .ok_or(DlpError::Overflow)?, + ); + Ok(()) + } + fn lamports_decrement_by(&self, value: u64) -> ProgramResult { + self.set_lamports( + self.lamports() + .checked_sub(value) + .ok_or(DlpError::Overflow)?, + ); + Ok(()) + } +} diff --git a/tests/test_commit_finalize.rs b/tests/test_commit_finalize.rs index 26a8a9b7..9e15fa55 100644 --- a/tests/test_commit_finalize.rs +++ b/tests/test_commit_finalize.rs @@ -6,7 +6,7 @@ use dlp_api::{ delegation_record_pda_from_delegated_account, validator_fees_vault_pda_from_validator, }, - state::DelegationMetadata, + state::{DelegationMetadata, DelegationRecord}, }; use solana_program::{ hash::Hash, native_token::LAMPORTS_PER_SOL, rent::Rent, system_program, @@ -29,12 +29,12 @@ mod fixtures; #[tokio::test] async fn test_commit_finalize_data_perf() { - run_test_commit_finalize(vec![0; 10240], vec![1; 10240], false, 1200).await; + run_test_commit_finalize(vec![0; 10240], vec![1; 10240], false, 1350).await; } #[tokio::test] async fn test_commit_finalize_diff_perf() { - run_test_commit_finalize(vec![0; 10240], vec![1; 10240], true, 1450).await; + run_test_commit_finalize(vec![0; 10240], vec![1; 10240], true, 1600).await; } async fn run_test_commit_finalize( @@ -44,10 +44,11 @@ async fn run_test_commit_finalize( max_expected_cu: u64, ) { // Setup - let (banks, _, authority, blockhash) = + let (banks, _, authority, blockhash, _record_lamports) = setup_program_test_env(old_state.clone()).await; - let new_account_balance = 1_000_000; + let new_account_balance = + solana_program::rent::Rent::default().minimum_balance(new_state.len()); let (ix, pdas) = dlp_api::instruction_builder::commit_finalize( authority.pubkey(), @@ -94,6 +95,8 @@ async fn run_test_commit_finalize( ); } + //tokio::time::sleep(Duration::from_secs(10)).await; + let delegated_account = banks.get_account(DELEGATED_PDA_ID).await.unwrap().unwrap(); assert_eq!(delegated_account.data, new_state); @@ -116,7 +119,8 @@ async fn run_test_commit_finalize( #[tokio::test] async fn test_commit_finalize_out_of_order() { // Setup - let (banks, _, authority, blockhash) = setup_program_test_env(vec![]).await; + let (banks, _, authority, blockhash, _record_lamports) = + setup_program_test_env(vec![]).await; let new_state = vec![0, 1, 2, 9, 9, 9, 6, 7, 8, 9]; let new_account_balance = 1_000_000; @@ -175,8 +179,16 @@ async fn test_commit_finalize_out_of_order() { async fn setup_program_test_env( pda_data: Vec, -) -> (BanksClient, Keypair, Keypair, Hash) { - let mut program_test = ProgramTest::new("dlp", dlp_api::ID, None); +) -> (BanksClient, Keypair, Keypair, Hash, u64) { + setup_program_test_env_with_record_lamports(pda_data, LAMPORTS_PER_SOL) + .await +} + +async fn setup_program_test_env_with_record_lamports( + pda_data: Vec, + record_lamports: u64, +) -> (BanksClient, Keypair, Keypair, Hash, u64) { + let mut program_test = ProgramTest::new("dlp", dlp::ID, None); program_test.prefer_bpf(true); let validator_keypair = Keypair::from_bytes(&TEST_AUTHORITY).unwrap(); @@ -196,7 +208,7 @@ async fn setup_program_test_env( program_test.add_account( DELEGATED_PDA_ID, Account { - lamports: LAMPORTS_PER_SOL, + lamports: record_lamports, data: pda_data, owner: dlp_api::id(), executable: false, @@ -220,8 +232,11 @@ async fn setup_program_test_env( ); // Setup the delegated record PDA - let delegation_record_data = - get_delegation_record_data(validator_keypair.pubkey(), None); + let delegation_record_data = get_delegation_record_data( + validator_keypair.pubkey(), + //None, + Some(record_lamports), + ); program_test.add_account( delegation_record_pda_from_delegated_account(&DELEGATED_PDA_ID), Account { @@ -247,5 +262,117 @@ async fn setup_program_test_env( ); let (banks, payer, blockhash) = program_test.start().await; - (banks, payer, validator_keypair, blockhash) + (banks, payer, validator_keypair, blockhash, record_lamports) +} + +#[tokio::test] +async fn test_commit_finalize_lamports_increase() { + let initial_lamports = LAMPORTS_PER_SOL; + let commit_lamports = initial_lamports + 1_000; + + let (banks, _, authority, blockhash, _record_lamports) = + setup_program_test_env_with_record_lamports( + vec![0; 8], + initial_lamports, + ) + .await; + + let (ix, pdas) = dlp::instruction_builder::commit_finalize( + authority.pubkey(), + DELEGATED_PDA_ID, + &mut CommitFinalizeArgs { + commit_id: 1, + allow_undelegation: false.into(), + data_is_diff: false.into(), + lamports: commit_lamports, + bumps: Default::default(), + reserved_padding: Default::default(), + }, + &vec![1; 8], + ); + + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); + banks.process_transaction(tx).await.unwrap(); + + let delegated_account = + banks.get_account(DELEGATED_PDA_ID).await.unwrap().unwrap(); + assert_eq!(delegated_account.lamports, commit_lamports); + + let delegation_record_account = banks + .get_account(pdas.delegation_record) + .await + .unwrap() + .unwrap(); + let delegation_record = + 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_lamports_decrease() { + let initial_lamports = LAMPORTS_PER_SOL; + let commit_lamports = initial_lamports - 1_000; + + let (banks, _, authority, blockhash, _record_lamports) = + setup_program_test_env_with_record_lamports( + vec![0; 8], + initial_lamports, + ) + .await; + + let (ix, pdas) = dlp::instruction_builder::commit_finalize( + authority.pubkey(), + DELEGATED_PDA_ID, + &mut CommitFinalizeArgs { + commit_id: 1, + allow_undelegation: false.into(), + data_is_diff: false.into(), + lamports: commit_lamports, + bumps: Default::default(), + reserved_padding: Default::default(), + }, + &vec![2; 8], + ); + + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); + banks.process_transaction(tx).await.unwrap(); + + let delegated_account = + banks.get_account(DELEGATED_PDA_ID).await.unwrap().unwrap(); + assert_eq!(delegated_account.lamports, commit_lamports); + + let delegation_record_account = banks + .get_account(pdas.delegation_record) + .await + .unwrap() + .unwrap(); + let delegation_record = + DelegationRecord::try_from_bytes_with_discriminator( + &delegation_record_account.data, + ) + .unwrap(); + assert_eq!(delegation_record.lamports, commit_lamports); + + let fees_vault = banks + .get_account(pdas.validator_fees_vault) + .await + .unwrap() + .unwrap(); + assert_eq!( + fees_vault.lamports, + LAMPORTS_PER_SOL + (initial_lamports - commit_lamports) + ); } From cc556eb35f1285be8c105bb94f398fce106d486b Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Tue, 10 Mar 2026 16:33:01 +0530 Subject: [PATCH 2/7] address rabbit feedback --- .../fast/internal/commit_finalize_internal.rs | 1 + tests/test_commit_finalize.rs | 100 ++++++++++++++++-- tests/test_commit_finalize_from_buffer.rs | 2 +- 3 files changed, 96 insertions(+), 7 deletions(-) diff --git a/src/processor/fast/internal/commit_finalize_internal.rs b/src/processor/fast/internal/commit_finalize_internal.rs index 99fc881b..171ab414 100644 --- a/src/processor/fast/internal/commit_finalize_internal.rs +++ b/src/processor/fast/internal/commit_finalize_internal.rs @@ -117,6 +117,7 @@ pub(crate) fn process_commit_finalize_internal( match args.commit_lamports.cmp(&delegation_record.lamports) { std::cmp::Ordering::Greater => { + require!(args.validator.is_writable(), ProgramError::Immutable); system::Transfer { from: args.validator, to: args.delegated_account, diff --git a/tests/test_commit_finalize.rs b/tests/test_commit_finalize.rs index 9e15fa55..b9a8b56b 100644 --- a/tests/test_commit_finalize.rs +++ b/tests/test_commit_finalize.rs @@ -1,23 +1,26 @@ +use assertables::assert_gt; use dlp_api::{ args::CommitFinalizeArgs, diff::compute_diff, + error::DlpError, pda::{ delegation_metadata_pda_from_delegated_account, delegation_record_pda_from_delegated_account, validator_fees_vault_pda_from_validator, }, - state::{DelegationMetadata, DelegationRecord}, }; use solana_program::{ hash::Hash, native_token::LAMPORTS_PER_SOL, rent::Rent, system_program, }; use solana_program_test::{ - BanksClient, BanksTransactionResultWithMetadata, ProgramTest, + BanksClient, BanksClientError, BanksTransactionResultWithMetadata, + ProgramTest, }; use solana_sdk::{ account::Account, + instruction::InstructionError, signature::{Keypair, Signer}, - transaction::Transaction, + transaction::{Transaction, TransactionError}, }; use crate::fixtures::{ @@ -95,8 +98,6 @@ async fn run_test_commit_finalize( ); } - //tokio::time::sleep(Duration::from_secs(10)).await; - let delegated_account = banks.get_account(DELEGATED_PDA_ID).await.unwrap().unwrap(); assert_eq!(delegated_account.data, new_state); @@ -234,7 +235,6 @@ async fn setup_program_test_env_with_record_lamports( // Setup the delegated record PDA let delegation_record_data = get_delegation_record_data( validator_keypair.pubkey(), - //None, Some(record_lamports), ); program_test.add_account( @@ -291,6 +291,13 @@ async fn test_commit_finalize_lamports_increase() { &vec![1; 8], ); + let before_validator_lamports = banks + .get_account(authority.pubkey()) + .await + .unwrap() + .unwrap() + .lamports; + let tx = Transaction::new_signed_with_payer( &[ix], Some(&authority.pubkey()), @@ -303,6 +310,23 @@ async fn test_commit_finalize_lamports_increase() { banks.get_account(DELEGATED_PDA_ID).await.unwrap().unwrap(); assert_eq!(delegated_account.lamports, commit_lamports); + let after_validator_lamports = banks + .get_account(authority.pubkey()) + .await + .unwrap() + .unwrap() + .lamports; + + // use: gt! instead eq! .. because validator is transaction fee payer as well. + assert_gt!(before_validator_lamports - after_validator_lamports, 1_000); + + let fees_vault = banks + .get_account(pdas.validator_fees_vault) + .await + .unwrap() + .unwrap(); + assert_eq!(fees_vault.lamports, LAMPORTS_PER_SOL); + let delegation_record_account = banks .get_account(pdas.delegation_record) .await @@ -376,3 +400,67 @@ async fn test_commit_finalize_lamports_decrease() { LAMPORTS_PER_SOL + (initial_lamports - commit_lamports) ); } + +#[tokio::test] +async fn test_commit_finalize_rejects_underfunded_account() { + let data_len = 8usize; + let rent_min = Rent::default().minimum_balance(data_len); + let initial_lamports = rent_min + 1_000; + let commit_lamports = rent_min - 1; + + let (banks, _, authority, blockhash, _record_lamports) = + setup_program_test_env_with_record_lamports( + vec![0; data_len], + initial_lamports, + ) + .await; + + let (ix, pdas) = dlp::instruction_builder::commit_finalize( + authority.pubkey(), + DELEGATED_PDA_ID, + &mut CommitFinalizeArgs { + commit_id: 1, + allow_undelegation: false.into(), + data_is_diff: false.into(), + lamports: commit_lamports, + bumps: Default::default(), + reserved_padding: Default::default(), + }, + &vec![3; data_len], + ); + + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); + let err = banks.process_transaction(tx).await.unwrap_err(); + match err { + BanksClientError::TransactionError( + TransactionError::InstructionError( + _, + InstructionError::Custom(code), + ), + ) => { + assert_eq!(code, DlpError::InsufficientRent as u32); + } + _ => panic!("unexpected error: {err:?}"), + } + + let delegated_account = + banks.get_account(DELEGATED_PDA_ID).await.unwrap().unwrap(); + assert_eq!(delegated_account.lamports, initial_lamports); + + let delegation_record_account = banks + .get_account(pdas.delegation_record) + .await + .unwrap() + .unwrap(); + let delegation_record = + DelegationRecord::try_from_bytes_with_discriminator( + &delegation_record_account.data, + ) + .unwrap(); + assert_eq!(delegation_record.lamports, initial_lamports); +} diff --git a/tests/test_commit_finalize_from_buffer.rs b/tests/test_commit_finalize_from_buffer.rs index 886f6d48..78fb9f56 100644 --- a/tests/test_commit_finalize_from_buffer.rs +++ b/tests/test_commit_finalize_from_buffer.rs @@ -68,7 +68,7 @@ async fn test_commit_finalize_from_buffer_perf() { let metadata = metadata.unwrap(); - assertables::assert_lt!(metadata.compute_units_consumed, 1200); + assertables::assert_lt!(metadata.compute_units_consumed, 1400); assert_eq!( metadata.log_messages.len(), From 62a94d5540c70b6f2441a8e1a0673ac574eea15f Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Mon, 23 Mar 2026 02:30:16 +0530 Subject: [PATCH 3/7] rebase fixes --- src/lib.rs | 3 +-- tests/test_commit_finalize.rs | 11 ++++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 07746c6a..8fe04ec7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,8 +20,7 @@ pub(crate) use dlp_api::{ undelegate_buffer_seeds_from_delegated_account, validator_fees_vault_seeds_from_validator, }; -#[allow(unused_imports)] -pub(crate) use dlp_api::{id, ID}; +pub use dlp_api::{id, ID}; #[allow(unused_imports)] pub(crate) use dlp_api::{ require, require_eq, require_eq_keys, require_ge, require_gt, diff --git a/tests/test_commit_finalize.rs b/tests/test_commit_finalize.rs index b9a8b56b..842427c8 100644 --- a/tests/test_commit_finalize.rs +++ b/tests/test_commit_finalize.rs @@ -8,6 +8,7 @@ use dlp_api::{ delegation_record_pda_from_delegated_account, validator_fees_vault_pda_from_validator, }, + state::{DelegationMetadata, DelegationRecord}, }; use solana_program::{ hash::Hash, native_token::LAMPORTS_PER_SOL, rent::Rent, system_program, @@ -32,12 +33,12 @@ mod fixtures; #[tokio::test] async fn test_commit_finalize_data_perf() { - run_test_commit_finalize(vec![0; 10240], vec![1; 10240], false, 1350).await; + run_test_commit_finalize(vec![0; 10240], vec![1; 10240], false, 1400).await; } #[tokio::test] async fn test_commit_finalize_diff_perf() { - run_test_commit_finalize(vec![0; 10240], vec![1; 10240], true, 1600).await; + run_test_commit_finalize(vec![0; 10240], vec![1; 10240], true, 1650).await; } async fn run_test_commit_finalize( @@ -277,7 +278,7 @@ async fn test_commit_finalize_lamports_increase() { ) .await; - let (ix, pdas) = dlp::instruction_builder::commit_finalize( + let (ix, pdas) = dlp_api::instruction_builder::commit_finalize( authority.pubkey(), DELEGATED_PDA_ID, &mut CommitFinalizeArgs { @@ -352,7 +353,7 @@ async fn test_commit_finalize_lamports_decrease() { ) .await; - let (ix, pdas) = dlp::instruction_builder::commit_finalize( + let (ix, pdas) = dlp_api::instruction_builder::commit_finalize( authority.pubkey(), DELEGATED_PDA_ID, &mut CommitFinalizeArgs { @@ -415,7 +416,7 @@ async fn test_commit_finalize_rejects_underfunded_account() { ) .await; - let (ix, pdas) = dlp::instruction_builder::commit_finalize( + let (ix, pdas) = dlp_api::instruction_builder::commit_finalize( authority.pubkey(), DELEGATED_PDA_ID, &mut CommitFinalizeArgs { From ec0fd000234e95ea97234e382eac89b684b65cb7 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Mon, 23 Mar 2026 03:13:59 +0530 Subject: [PATCH 4/7] use check_minimum_balance --- .../fast/internal/commit_finalize_internal.rs | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/processor/fast/internal/commit_finalize_internal.rs b/src/processor/fast/internal/commit_finalize_internal.rs index 171ab414..a4304822 100644 --- a/src/processor/fast/internal/commit_finalize_internal.rs +++ b/src/processor/fast/internal/commit_finalize_internal.rs @@ -113,11 +113,15 @@ pub(crate) fn process_commit_finalize_internal( DlpError::InvalidDelegatedState ); + let mut check_minimum_balance = + args.new_state.data_len() > args.delegated_account.data_len(); + args.delegated_account.resize(args.new_state.data_len())?; match args.commit_lamports.cmp(&delegation_record.lamports) { std::cmp::Ordering::Greater => { require!(args.validator.is_writable(), ProgramError::Immutable); + system::Transfer { from: args.validator, to: args.delegated_account, @@ -131,13 +135,7 @@ pub(crate) fn process_commit_finalize_internal( args.delegated_account.lamports_decrement_by(amount)?; args.validator_fees_vault.lamports_increment_by(amount)?; - // require the account is still rent-exempted even after decrementing lamports - require_ge!( - args.delegated_account.lamports(), - Rent::get()? - .try_minimum_balance(args.delegated_account.data_len())?, - DlpError::InsufficientRent - ); + check_minimum_balance = true; } std::cmp::Ordering::Equal => {} } @@ -145,6 +143,16 @@ pub(crate) fn process_commit_finalize_internal( // Update the delegation record lamports after settling. delegation_record.lamports = args.delegated_account.lamports(); + // require the account is still rent-exempted even after decrementing lamports + if check_minimum_balance { + require_ge!( + args.delegated_account.lamports(), + Rent::get()? + .try_minimum_balance(args.delegated_account.data_len())?, + DlpError::InsufficientRent + ); + } + // copy the new state to the delegated account let mut delegated_account_data = args.delegated_account.try_borrow_mut()?; match args.new_state { From 864abb325f5f7e9e9d7a691bb7dd1ca299e8391e Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Mon, 23 Mar 2026 20:25:24 +0530 Subject: [PATCH 5/7] fix delegation_record.lamports = commit_lamports --- src/processor/fast/finalize.rs | 2 +- src/processor/fast/internal/commit_finalize_internal.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/processor/fast/finalize.rs b/src/processor/fast/finalize.rs index ebfc1136..1832a4c0 100644 --- a/src/processor/fast/finalize.rs +++ b/src/processor/fast/finalize.rs @@ -167,7 +167,7 @@ pub fn process_finalize( .map_err(to_pinocchio_program_error)?; // Update the delegation record - delegation_record.lamports = delegated_account.lamports(); + delegation_record.lamports = commit_record.lamports; // Load commit state let commit_state_data = commit_state_account.try_borrow()?; diff --git a/src/processor/fast/internal/commit_finalize_internal.rs b/src/processor/fast/internal/commit_finalize_internal.rs index a4304822..07b04006 100644 --- a/src/processor/fast/internal/commit_finalize_internal.rs +++ b/src/processor/fast/internal/commit_finalize_internal.rs @@ -141,7 +141,7 @@ pub(crate) fn process_commit_finalize_internal( } // Update the delegation record lamports after settling. - delegation_record.lamports = args.delegated_account.lamports(); + delegation_record.lamports = args.commit_lamports; // require the account is still rent-exempted even after decrementing lamports if check_minimum_balance { From f4e9c2beca1551bbb34674ef883a15882d12d2c1 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Mon, 23 Mar 2026 23:01:58 +0530 Subject: [PATCH 6/7] add tests --- tests/test_commit_finalize.rs | 9 +- tests/test_lamports_settlement.rs | 366 +++++++++++++++++++++++++++++- 2 files changed, 369 insertions(+), 6 deletions(-) diff --git a/tests/test_commit_finalize.rs b/tests/test_commit_finalize.rs index 842427c8..982fcfcc 100644 --- a/tests/test_commit_finalize.rs +++ b/tests/test_commit_finalize.rs @@ -1,4 +1,4 @@ -use assertables::assert_gt; +use assertables::assert_ge; use dlp_api::{ args::CommitFinalizeArgs, diff::compute_diff, @@ -318,8 +318,11 @@ async fn test_commit_finalize_lamports_increase() { .unwrap() .lamports; - // use: gt! instead eq! .. because validator is transaction fee payer as well. - assert_gt!(before_validator_lamports - after_validator_lamports, 1_000); + assert_ne!(before_validator_lamports - after_validator_lamports, 0); + assert_ge!( + before_validator_lamports - after_validator_lamports, + commit_lamports - initial_lamports + ); let fees_vault = banks .get_account(pdas.validator_fees_vault) diff --git a/tests/test_lamports_settlement.rs b/tests/test_lamports_settlement.rs index 35a4182a..701a1423 100644 --- a/tests/test_lamports_settlement.rs +++ b/tests/test_lamports_settlement.rs @@ -1,5 +1,5 @@ use dlp_api::{ - args::CommitStateArgs, + args::{CommitFinalizeArgs, CommitStateArgs, DelegateArgs}, pda::{ commit_record_pda_from_delegated_account, commit_state_pda_from_delegated_account, @@ -7,11 +7,11 @@ use dlp_api::{ delegation_record_pda_from_delegated_account, fees_vault_pda, validator_fees_vault_pda_from_validator, }, - state::{CommitRecord, DelegationMetadata}, + state::{CommitRecord, DelegationMetadata, DelegationRecord}, }; use solana_program::{ hash::Hash, native_token::LAMPORTS_PER_SOL, pubkey::Pubkey, rent::Rent, - system_program, + system_instruction, system_program, }; use solana_program_test::{read_file, BanksClient, ProgramTest}; use solana_sdk::{ @@ -68,6 +68,312 @@ async fn test_commit_undelegate_pda_after_balance_increase() { test_commit_system_account_after_balance_increase(true, true).await; } +async fn get_delegation_record( + banks: &BanksClient, + delegated_account: Pubkey, +) -> DelegationRecord { + let acc = banks + .get_account(delegation_record_pda_from_delegated_account( + &delegated_account, + )) + .await + .unwrap() + .unwrap(); + *DelegationRecord::try_from_bytes_with_discriminator(&acc.data).unwrap() +} + +async fn validator_fees_vault_balance( + banks: &BanksClient, + validator: Pubkey, +) -> u64 { + banks + .get_balance(validator_fees_vault_pda_from_validator(&validator)) + .await + .unwrap() +} + +#[tokio::test] +async fn test_commit_finalize_lamports_settlement() { + let initial_lamports = 1_000_000; + let (base_banks, payer, delegated, validator, blockhash) = + setup_program_for_delegate_base_increase(initial_lamports).await; + + // Assign delegated account to the delegation program. + let assign_ix = + system_instruction::assign(&delegated.pubkey(), &dlp_api::id()); + let assign_tx = Transaction::new_signed_with_payer( + &[assign_ix], + Some(&payer.pubkey()), + &[&payer, &delegated], + blockhash, + ); + base_banks.process_transaction(assign_tx).await.unwrap(); + + assert_eq!( + base_banks.get_balance(delegated.pubkey()).await.unwrap(), + initial_lamports + ); + + // let's predent that lamports_on_ephem is the valid that tracks the current lamports + // on the ER + let mut lamports_on_ephem = initial_lamports; + + // Delegate the account. + { + let delegate_ix = dlp_api::instruction_builder::delegate( + payer.pubkey(), + delegated.pubkey(), + None, + DelegateArgs { + commit_frequency_ms: u32::MAX, + seeds: vec![], + validator: Some(validator.pubkey()), + }, + ); + let delegate_tx = Transaction::new_signed_with_payer( + &[delegate_ix], + Some(&payer.pubkey()), + &[&payer, &delegated], + blockhash, + ); + base_banks.process_transaction(delegate_tx).await.unwrap(); + + assert_eq!( + get_delegation_record(&base_banks, delegated.pubkey()) + .await + .lamports, + initial_lamports, + "delegation_record.lamports == delegated_account.lamports() at the time of delegation" + ); + } + + // send 100 lamports to the delegated account on the base + { + let transfer_ix = system_instruction::transfer( + &payer.pubkey(), + &delegated.pubkey(), + 100, + ); + let transfer_tx = Transaction::new_signed_with_payer( + &[transfer_ix], + Some(&payer.pubkey()), + &[&payer], + blockhash, + ); + base_banks.process_transaction(transfer_tx).await.unwrap(); + + assert_eq!( + base_banks.get_balance(delegated.pubkey()).await.unwrap(), + initial_lamports + 100 + ); + } + + // first commit (assume there is no lamports change on ER) + { + let mut args = CommitFinalizeArgs { + commit_id: 1, + lamports: lamports_on_ephem, + allow_undelegation: false.into(), + data_is_diff: false.into(), + bumps: Default::default(), + reserved_padding: Default::default(), + }; + let (commit_ix, _) = dlp_api::instruction_builder::commit_finalize( + validator.pubkey(), + delegated.pubkey(), + &mut args, + &[], + ); + let commit_tx = Transaction::new_signed_with_payer( + &[commit_ix], + Some(&payer.pubkey()), + &[&validator, &payer], + blockhash, + ); + base_banks.process_transaction(commit_tx).await.unwrap(); + + assert_eq!( + base_banks.get_balance(validator.pubkey()).await.unwrap(), + LAMPORTS_PER_SOL, + "there must not be any change in validator lamports because there is no tx" + ); + assert_eq!( + validator_fees_vault_balance(&base_banks, validator.pubkey()).await, + dlp_api::consts::RENT_EXCEPTION_ZERO_BYTES_LAMPORTS, + "there must not be any change in fees_vault lamports because there is no tx" + ); + + assert_eq!( + get_delegation_record(&base_banks, delegated.pubkey()) + .await + .lamports, + lamports_on_ephem, + "delegation_record.lamports must be same as the lamports on ER (commit_lamports)" + ); + + assert_eq!( + base_banks.get_balance(delegated.pubkey()).await.unwrap(), + initial_lamports + 100, + "account's lamports on the base must be unchanged" + ); + } + + // second commit (still no lamports change on ER) + { + let mut args = CommitFinalizeArgs { + commit_id: 2, + lamports: lamports_on_ephem, + allow_undelegation: false.into(), + data_is_diff: false.into(), + bumps: Default::default(), + reserved_padding: Default::default(), + }; + let (commit_ix, _) = dlp_api::instruction_builder::commit_finalize( + validator.pubkey(), + delegated.pubkey(), + &mut args, + &[], + ); + let commit_tx = Transaction::new_signed_with_payer( + &[commit_ix], + Some(&payer.pubkey()), + &[&validator, &payer], + blockhash, + ); + base_banks.process_transaction(commit_tx).await.unwrap(); + + assert_eq!( + base_banks.get_balance(validator.pubkey()).await.unwrap(), + LAMPORTS_PER_SOL, + "there must not be any change in validator lamports because there is no tx" + ); + assert_eq!( + validator_fees_vault_balance(&base_banks, validator.pubkey()).await, + dlp_api::consts::RENT_EXCEPTION_ZERO_BYTES_LAMPORTS, + "there must not be any change in fees_vault lamports because there is no tx" + ); + + assert_eq!( + get_delegation_record(&base_banks, delegated.pubkey()) + .await + .lamports, + lamports_on_ephem, + "delegation_record.lamports must be same as the lamports on ER (commit_lamports)" + ); + + assert_eq!( + base_banks.get_balance(delegated.pubkey()).await.unwrap(), + initial_lamports + 100, + "account's lamports on the base must be unchanged" + ); + } + + // third commit (lamports on ER has increased by 959) + { + // pretend lamports has increased on the ER + lamports_on_ephem += 959; + + let mut args = CommitFinalizeArgs { + commit_id: 3, + lamports: lamports_on_ephem, // it has increased 959 + allow_undelegation: false.into(), + data_is_diff: false.into(), + bumps: Default::default(), + reserved_padding: Default::default(), + }; + let (commit_ix, _) = dlp_api::instruction_builder::commit_finalize( + validator.pubkey(), + delegated.pubkey(), + &mut args, + &[], + ); + let commit_tx = Transaction::new_signed_with_payer( + &[commit_ix], + Some(&payer.pubkey()), + &[&validator, &payer], + blockhash, + ); + base_banks.process_transaction(commit_tx).await.unwrap(); + + assert_eq!( + base_banks.get_balance(validator.pubkey()).await.unwrap(), + LAMPORTS_PER_SOL - 959, + "validator's lamports must decrease by 959 because 959 must be transferred to delegated_account" + ); + assert_eq!( + validator_fees_vault_balance(&base_banks, validator.pubkey()).await, + dlp_api::consts::RENT_EXCEPTION_ZERO_BYTES_LAMPORTS, + "there must not be any change in fees_vault lamports because tx deals with increased lamports value" + ); + + assert_eq!( + get_delegation_record(&base_banks, delegated.pubkey()) + .await + .lamports, + lamports_on_ephem, + "delegation_record.lamports must have increased by 100, but still same as lamports_on_ephem" + ); + + assert_eq!( + base_banks.get_balance(delegated.pubkey()).await.unwrap(), + initial_lamports + 100 + 959, + "account's lamports on the base must be increased by the same amount as the change on the ER" + ); + } + + // fourth commit (lamports on ER has decreased by 9590) + { + // pretend lamports has decreased on the ER + lamports_on_ephem -= 9590; + + let mut args = CommitFinalizeArgs { + commit_id: 4, + lamports: lamports_on_ephem, // it has increased 959 + allow_undelegation: false.into(), + data_is_diff: false.into(), + bumps: Default::default(), + reserved_padding: Default::default(), + }; + let (commit_ix, _) = dlp_api::instruction_builder::commit_finalize( + validator.pubkey(), + delegated.pubkey(), + &mut args, + &[], + ); + let commit_tx = Transaction::new_signed_with_payer( + &[commit_ix], + Some(&payer.pubkey()), + &[&validator, &payer], + blockhash, + ); + base_banks.process_transaction(commit_tx).await.unwrap(); + + assert_eq!( + base_banks.get_balance(validator.pubkey()).await.unwrap(), + LAMPORTS_PER_SOL - 959, + "validator's lamports must not changhe now" + ); + assert_eq!( + validator_fees_vault_balance(&base_banks, validator.pubkey()).await, + dlp_api::consts::RENT_EXCEPTION_ZERO_BYTES_LAMPORTS + 9590, + "validator_fees_vault_balance must have increased by 9590 now" + ); + + assert_eq!( + get_delegation_record(&base_banks, delegated.pubkey()) + .await + .lamports, + lamports_on_ephem, + ); + + assert_eq!( + base_banks.get_balance(delegated.pubkey()).await.unwrap(), + initial_lamports + 100 + 959 - 9590, + "account's lamports on the base must be decreased by the same amount as the change on the ER" + ); + } +} + #[tokio::test] async fn test_commit_finalise_system_account_after_balance_decrease_and_increase_mainchain( ) { @@ -502,6 +808,60 @@ struct CommitNewStateArgs<'a> { delegated_account_owner: Pubkey, } +async fn setup_program_for_delegate_base_increase( + initial_lamports: u64, +) -> (BanksClient, Keypair, Keypair, Keypair, Hash) { + assert!( + initial_lamports >= dlp_api::consts::RENT_EXCEPTION_ZERO_BYTES_LAMPORTS, + "Please pass lamports >= {}, but passed: {}", + dlp_api::consts::RENT_EXCEPTION_ZERO_BYTES_LAMPORTS, + initial_lamports + ); + + let mut program_test = ProgramTest::new("dlp", dlp_api::ID, None); + program_test.prefer_bpf(true); + + let delegated = Keypair::new(); + let validator = Keypair::from_bytes(&TEST_AUTHORITY).unwrap(); + + program_test.add_account( + delegated.pubkey(), + Account { + lamports: initial_lamports, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + + program_test.add_account( + validator.pubkey(), + Account { + lamports: LAMPORTS_PER_SOL, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + + program_test.add_account( + validator_fees_vault_pda_from_validator(&validator.pubkey()), + Account { + lamports: Rent::default().minimum_balance(0), + data: vec![], + owner: dlp_api::id(), + executable: false, + rent_epoch: 0, + }, + ); + + let (banks, payer, blockhash) = program_test.start().await; + + (banks, payer, delegated, validator, blockhash) +} + async fn commit_new_state(args: CommitNewStateArgs<'_>) { let data = if args.delegated_account.eq(&DELEGATED_PDA_ID) { COMMIT_NEW_STATE_ACCOUNT_DATA.to_vec() From 09e0102df4ff69dfe9795cdf3b642943a3bbdf59 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Tue, 24 Mar 2026 03:10:03 +0530 Subject: [PATCH 7/7] add another test for commit+finalize --- dlp-api/src/instruction_builder/finalize.rs | 2 +- tests/test_lamports_settlement.rs | 340 +++++++++++++++++++- 2 files changed, 337 insertions(+), 5 deletions(-) diff --git a/dlp-api/src/instruction_builder/finalize.rs b/dlp-api/src/instruction_builder/finalize.rs index bff9f77c..e5dd30bc 100644 --- a/dlp-api/src/instruction_builder/finalize.rs +++ b/dlp-api/src/instruction_builder/finalize.rs @@ -31,7 +31,7 @@ pub fn finalize(validator: Pubkey, delegated_account: Pubkey) -> Instruction { Instruction { program_id: dlp::id(), accounts: vec![ - AccountMeta::new_readonly(validator, true), + AccountMeta::new(validator, true), AccountMeta::new(delegated_account, false), AccountMeta::new(commit_state_pda, false), AccountMeta::new(commit_record_pda, false), diff --git a/tests/test_lamports_settlement.rs b/tests/test_lamports_settlement.rs index 701a1423..482bea16 100644 --- a/tests/test_lamports_settlement.rs +++ b/tests/test_lamports_settlement.rs @@ -374,6 +374,272 @@ async fn test_commit_finalize_lamports_settlement() { } } +#[tokio::test] +async fn test_commit_and_finalize_lamports_settlement() { + let initial_lamports = 1_000_000; + let (mut base_banks, payer, delegated, validator, blockhash) = + setup_program_for_delegate_base_increase(initial_lamports).await; + + // Assign delegated account to the delegation program. + let assign_ix = + system_instruction::assign(&delegated.pubkey(), &dlp_api::id()); + let assign_tx = Transaction::new_signed_with_payer( + &[assign_ix], + Some(&payer.pubkey()), + &[&payer, &delegated], + blockhash, + ); + base_banks.process_transaction(assign_tx).await.unwrap(); + + assert_eq!( + base_banks.get_balance(delegated.pubkey()).await.unwrap(), + initial_lamports + ); + + let mut lamports_on_ephem = initial_lamports; + + // Delegate the account. + { + let delegate_ix = dlp_api::instruction_builder::delegate( + payer.pubkey(), + delegated.pubkey(), + None, + DelegateArgs { + commit_frequency_ms: u32::MAX, + seeds: vec![], + validator: Some(validator.pubkey()), + }, + ); + let delegate_tx = Transaction::new_signed_with_payer( + &[delegate_ix], + Some(&payer.pubkey()), + &[&payer, &delegated], + blockhash, + ); + base_banks.process_transaction(delegate_tx).await.unwrap(); + + assert_eq!( + get_delegation_record(&base_banks, delegated.pubkey()) + .await + .lamports, + initial_lamports, + "delegation_record.lamports == delegated_account.lamports() at the time of delegation" + ); + } + + // send 100 lamports to the delegated account on the base + { + let transfer_ix = system_instruction::transfer( + &payer.pubkey(), + &delegated.pubkey(), + 100, + ); + let transfer_tx = Transaction::new_signed_with_payer( + &[transfer_ix], + Some(&payer.pubkey()), + &[&payer], + blockhash, + ); + base_banks.process_transaction(transfer_tx).await.unwrap(); + + assert_eq!( + base_banks.get_balance(delegated.pubkey()).await.unwrap(), + initial_lamports + 100 + ); + } + + // first commit+finalize (assume there is no lamports change on ER) + { + commit_state_with_nonce(CommitStateWithNonceArgs { + banks: &mut base_banks, + authority: &validator, + fee_payer: &payer, + blockhash, + new_delegated_account_lamports: lamports_on_ephem, + nonce: 1, + allow_undelegation: false, + label: "first commit", + delegated_account: delegated.pubkey(), + delegated_account_owner: system_program::id(), + }) + .await; + + finalize_with_fee_payer(FinalizeWithFeePayerArgs { + banks: &mut base_banks, + authority: &validator, + fee_payer: &payer, + blockhash, + label: "first finalize", + delegated_account: delegated.pubkey(), + }) + .await; + + assert_eq!( + validator_fees_vault_balance(&base_banks, validator.pubkey()).await, + dlp_api::consts::RENT_EXCEPTION_ZERO_BYTES_LAMPORTS, + "there must not be any change in fees_vault lamports because there is no tx" + ); + + assert_eq!( + get_delegation_record(&base_banks, delegated.pubkey()) + .await + .lamports, + lamports_on_ephem, + "delegation_record.lamports must be same as the lamports on ER (commit_lamports)" + ); + + assert_eq!( + base_banks.get_balance(delegated.pubkey()).await.unwrap(), + initial_lamports + 100, + "account's lamports on the base must be unchanged" + ); + } + + // second commit+finalize (still no lamports change on ER) + { + commit_state_with_nonce(CommitStateWithNonceArgs { + banks: &mut base_banks, + authority: &validator, + fee_payer: &payer, + blockhash, + new_delegated_account_lamports: lamports_on_ephem, + nonce: 2, + allow_undelegation: false, + label: "second commit", + delegated_account: delegated.pubkey(), + delegated_account_owner: system_program::id(), + }) + .await; + + finalize_with_fee_payer(FinalizeWithFeePayerArgs { + banks: &mut base_banks, + authority: &validator, + fee_payer: &payer, + blockhash, + label: "second finalize", + delegated_account: delegated.pubkey(), + }) + .await; + + assert_eq!( + validator_fees_vault_balance(&base_banks, validator.pubkey()).await, + dlp_api::consts::RENT_EXCEPTION_ZERO_BYTES_LAMPORTS, + "there must not be any change in fees_vault lamports because there is no tx" + ); + + assert_eq!( + get_delegation_record(&base_banks, delegated.pubkey()) + .await + .lamports, + lamports_on_ephem, + "delegation_record.lamports must be same as the lamports on ER (commit_lamports)" + ); + + assert_eq!( + base_banks.get_balance(delegated.pubkey()).await.unwrap(), + initial_lamports + 100, + "account's lamports on the base must be unchanged" + ); + } + + // third commit+finalize (lamports on ER has increased by 959) + { + lamports_on_ephem += 959; + + commit_state_with_nonce(CommitStateWithNonceArgs { + banks: &mut base_banks, + authority: &validator, + fee_payer: &payer, + blockhash, + new_delegated_account_lamports: lamports_on_ephem, + nonce: 3, + allow_undelegation: false, + label: "third commit", + delegated_account: delegated.pubkey(), + delegated_account_owner: system_program::id(), + }) + .await; + + finalize_with_fee_payer(FinalizeWithFeePayerArgs { + banks: &mut base_banks, + authority: &validator, + fee_payer: &payer, + blockhash, + label: "third finalize", + delegated_account: delegated.pubkey(), + }) + .await; + + assert_eq!( + validator_fees_vault_balance(&base_banks, validator.pubkey()).await, + dlp_api::consts::RENT_EXCEPTION_ZERO_BYTES_LAMPORTS, + "there must not be any change in fees_vault lamports because tx deals with increased lamports value" + ); + + assert_eq!( + get_delegation_record(&base_banks, delegated.pubkey()) + .await + .lamports, + lamports_on_ephem, + "delegation_record.lamports must have increased by 100, but still same as lamports_on_ephem" + ); + + assert_eq!( + base_banks.get_balance(delegated.pubkey()).await.unwrap(), + initial_lamports + 100 + 959, + "account's lamports on the base must be increased by the same amount as the change on the ER" + ); + } + + // fourth commit+finalize (lamports on ER has decreased by 9590) + { + lamports_on_ephem -= 9590; + + commit_state_with_nonce(CommitStateWithNonceArgs { + banks: &mut base_banks, + authority: &validator, + fee_payer: &payer, + blockhash, + new_delegated_account_lamports: lamports_on_ephem, + nonce: 4, + allow_undelegation: false, + label: "fourth commit", + delegated_account: delegated.pubkey(), + delegated_account_owner: system_program::id(), + }) + .await; + + finalize_with_fee_payer(FinalizeWithFeePayerArgs { + banks: &mut base_banks, + authority: &validator, + fee_payer: &payer, + blockhash, + label: "fourth finalize", + delegated_account: delegated.pubkey(), + }) + .await; + + assert_eq!( + validator_fees_vault_balance(&base_banks, validator.pubkey()).await, + dlp_api::consts::RENT_EXCEPTION_ZERO_BYTES_LAMPORTS + 9590, + "validator_fees_vault_balance must have increased by 9590 now" + ); + + assert_eq!( + get_delegation_record(&base_banks, delegated.pubkey()) + .await + .lamports, + lamports_on_ephem, + ); + + assert_eq!( + base_banks.get_balance(delegated.pubkey()).await.unwrap(), + initial_lamports + 100 + 959 - 9590, + "account's lamports on the base must be decreased by the same amount as the change on the ER" + ); + } +} + #[tokio::test] async fn test_commit_finalise_system_account_after_balance_decrease_and_increase_mainchain( ) { @@ -740,8 +1006,7 @@ async fn undelegate(args: UndelegateArgs<'_>) { args.blockhash, ); let res = args.banks.process_transaction(tx).await; - println!("{:?}", res); - assert!(res.is_ok()); + assert!(res.is_ok(), "{:?}", res); // Assert the delegation_record_pda was closed let delegation_record_account = @@ -808,6 +1073,19 @@ struct CommitNewStateArgs<'a> { delegated_account_owner: Pubkey, } +struct CommitStateWithNonceArgs<'a> { + banks: &'a mut BanksClient, + authority: &'a Keypair, + fee_payer: &'a Keypair, + blockhash: Hash, + new_delegated_account_lamports: u64, + nonce: u64, + allow_undelegation: bool, + label: &'a str, + delegated_account: Pubkey, + delegated_account_owner: Pubkey, +} + async fn setup_program_for_delegate_base_increase( initial_lamports: u64, ) -> (BanksClient, Keypair, Keypair, Keypair, Hash) { @@ -889,8 +1167,7 @@ async fn commit_new_state(args: CommitNewStateArgs<'_>) { args.blockhash, ); let res = args.banks.process_transaction(tx).await; - println!("{:?}", res); - assert!(res.is_ok()); + assert!(res.is_ok(), "{:?}", res); // Assert the state commitment was created and contains the new state let commit_state_pda = @@ -948,6 +1225,61 @@ async fn commit_new_state(args: CommitNewStateArgs<'_>) { assert!(delegation_metadata.is_undelegatable); } +async fn commit_state_with_nonce(args: CommitStateWithNonceArgs<'_>) { + let data = if args.delegated_account.eq(&DELEGATED_PDA_ID) { + COMMIT_NEW_STATE_ACCOUNT_DATA.to_vec() + } else { + vec![] + }; + let commit_args = CommitStateArgs { + data, + nonce: args.nonce, + allow_undelegation: args.allow_undelegation, + lamports: args.new_delegated_account_lamports, + }; + + let ix = dlp_api::instruction_builder::commit_state( + args.authority.pubkey(), + args.delegated_account, + args.delegated_account_owner, + commit_args, + ); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&args.fee_payer.pubkey()), + &[&args.authority, &args.fee_payer], + args.banks.get_latest_blockhash().await.unwrap(), + ); + let res = args.banks.process_transaction(tx).await; + + assert!(res.is_ok(), "{} failed: {:?}", args.label, res); +} + +struct FinalizeWithFeePayerArgs<'a> { + banks: &'a mut BanksClient, + authority: &'a Keypair, + fee_payer: &'a Keypair, + blockhash: Hash, + label: &'a str, + delegated_account: Pubkey, +} + +async fn finalize_with_fee_payer(args: FinalizeWithFeePayerArgs<'_>) { + let ix = dlp_api::instruction_builder::finalize( + args.authority.pubkey(), + args.delegated_account, + ); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&args.fee_payer.pubkey()), + &[&args.authority, &args.fee_payer], + args.banks.get_latest_blockhash().await.unwrap(), + ); + let res = args.banks.process_transaction(tx).await; + + assert!(res.is_ok(), "{} failed: {:?}", args.label, res); +} + #[derive(Debug)] struct SetupProgramCommitTestEnvArgs { delegated_account_init_lamports: u64,