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/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/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/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/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/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 9378dda5..07b04006 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,51 @@ 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()?; - // } + 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, + 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)?; + + check_minimum_balance = true; + } + std::cmp::Ordering::Equal => {} + } + + // Update the delegation record lamports after settling. + delegation_record.lamports = args.commit_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 { 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..982fcfcc 100644 --- a/tests/test_commit_finalize.rs +++ b/tests/test_commit_finalize.rs @@ -1,23 +1,27 @@ +use assertables::assert_ge; 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, + 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::{ @@ -29,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, 1200).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, 1450).await; + run_test_commit_finalize(vec![0; 10240], vec![1; 10240], true, 1650).await; } async fn run_test_commit_finalize( @@ -44,10 +48,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(), @@ -116,7 +121,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 +181,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 +210,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 +234,10 @@ 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(), + Some(record_lamports), + ); program_test.add_account( delegation_record_pda_from_delegated_account(&DELEGATED_PDA_ID), Account { @@ -247,5 +263,208 @@ 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_api::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 before_validator_lamports = banks + .get_account(authority.pubkey()) + .await + .unwrap() + .unwrap() + .lamports; + + 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 after_validator_lamports = banks + .get_account(authority.pubkey()) + .await + .unwrap() + .unwrap() + .lamports; + + 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) + .await + .unwrap() + .unwrap(); + assert_eq!(fees_vault.lamports, LAMPORTS_PER_SOL); + + 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_api::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) + ); +} + +#[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_api::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(), diff --git a/tests/test_lamports_settlement.rs b/tests/test_lamports_settlement.rs index 35a4182a..482bea16 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,578 @@ 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_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( ) { @@ -434,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 = @@ -502,6 +1073,73 @@ 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) { + 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() @@ -529,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 = @@ -588,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,