From 790a28fb00043f97782b16d314b13fc5543ea934 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Thu, 22 Oct 2026 01:59:15 +0530 Subject: [PATCH 1/4] feat(direct): Implement CommitFinalize Add the primary Direct-cluster instruction, wire layout, and builder for committing inline full-state or diff payloads. Authorize the cluster's current validator, validate DelegationState ordering and undelegation rules, settle lamports, apply the update without temporary state accounts, and advance the delegation state atomically. Cover full data, aligned zero-copy diffs, authority, owner, cluster mode, commit ordering, and owner-requested undelegation in SBF tests. --- interface/src/args/commit_finalize.rs | 42 ++ interface/src/args/mod.rs | 2 + interface/src/instruction.rs | 2 + .../instruction_builder/commit_finalize.rs | 37 ++ interface/src/instruction_builder/mod.rs | 2 + .../src/processor/direct/commit_finalize.rs | 62 +++ .../direct/commit_finalize_internal.rs | 215 ++++++++++ program/src/processor/direct/mod.rs | 7 + program/src/processor/mod.rs | 5 + program/tests/test_primary_commit_finalize.rs | 366 ++++++++++++++++++ 10 files changed, 740 insertions(+) create mode 100644 interface/src/args/commit_finalize.rs create mode 100644 interface/src/instruction_builder/commit_finalize.rs create mode 100644 program/src/processor/direct/commit_finalize.rs create mode 100644 program/src/processor/direct/commit_finalize_internal.rs create mode 100644 program/src/processor/direct/mod.rs create mode 100644 program/tests/test_primary_commit_finalize.rs diff --git a/interface/src/args/commit_finalize.rs b/interface/src/args/commit_finalize.rs new file mode 100644 index 00000000..2ed7442e --- /dev/null +++ b/interface/src/args/commit_finalize.rs @@ -0,0 +1,42 @@ +use wheels::variable_offset_layout; + +use crate::compat::Pubkey; + +#[derive(Clone, Debug, PartialEq, Eq)] +#[variable_offset_layout(buffer_offset = 1)] +pub struct CommitFinalizeArgs { + /// Next commit id for this delegated account. + pub commit_id: u64, + + /// Account lamports on the ER after this commit. + pub lamports: u64, + + /// Account owner committed by the ER. + pub owner: Pubkey, + + /// Whether the validator requests undelegation after this commit. + pub allow_undelegation: bool, + + /// Stored as `CommitPayloadKind::value()`. + pub payload_kind: u8, + + /// Keeps the inline payload four-byte aligned for zero-copy diff decoding. + pub payload_alignment_padding: u8, + + /// Complete account data or an encoded diff, according to `payload_kind`. + #[flexible = 4] + pub payload: Vec, +} + +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum CommitPayloadKind { + FullData = 0, + Diff = 1, +} + +impl CommitPayloadKind { + pub const fn value(self) -> u8 { + self as u8 + } +} diff --git a/interface/src/args/mod.rs b/interface/src/args/mod.rs index 127da0fc..c7c35e5f 100644 --- a/interface/src/args/mod.rs +++ b/interface/src/args/mod.rs @@ -2,6 +2,7 @@ // instruction tag, so v2 instruction args use `buffer_offset = 1`. mod challenger_reveal; +mod commit_finalize; mod delegate; mod init_delegation_cluster; mod propose_commitment; @@ -14,6 +15,7 @@ mod update_verifier_registry; mod write_state_buffer; pub use challenger_reveal::*; +pub use commit_finalize::*; pub use delegate::*; pub use init_delegation_cluster::*; pub use propose_commitment::*; diff --git a/interface/src/instruction.rs b/interface/src/instruction.rs index e0a65cb9..bae5e411 100644 --- a/interface/src/instruction.rs +++ b/interface/src/instruction.rs @@ -35,6 +35,8 @@ pub enum DlpV2Instruction { ResolveDispute = 111, /// Delegates an account to one delegation cluster. Delegate = 112, + /// Commits and immediately applies inline state for a Direct cluster. + CommitFinalize = 113, } impl DlpV2Instruction { diff --git a/interface/src/instruction_builder/commit_finalize.rs b/interface/src/instruction_builder/commit_finalize.rs new file mode 100644 index 00000000..ce636540 --- /dev/null +++ b/interface/src/instruction_builder/commit_finalize.rs @@ -0,0 +1,37 @@ +use solana_program::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, +}; +use solana_sdk_ids::system_program; +use wheels::{layout::Encodable, DataLayoutError}; + +use crate::{ + compat::{Compatize, Modernize}, + pda::delegation_state_pda, + CommitFinalizeArgs, DlpV2Instruction, +}; + +/// Builds a direct-cluster commit that carries its payload inline. +pub fn commit_finalize( + current_validator: Pubkey, + delegation_cluster: Pubkey, + delegated_account: Pubkey, + args: CommitFinalizeArgs, +) -> Result { + Ok(Instruction { + program_id: crate::id().modernize(), + accounts: vec![ + AccountMeta::new(current_validator, true), + AccountMeta::new(delegated_account, false), + AccountMeta::new( + delegation_state_pda(&delegated_account.compatize()) + .modernize(), + false, + ), + AccountMeta::new_readonly(delegation_cluster, false), + AccountMeta::new_readonly(system_program::id(), false), + ], + data: [DlpV2Instruction::CommitFinalize.to_vec(), args.encode()?] + .concat(), + }) +} diff --git a/interface/src/instruction_builder/mod.rs b/interface/src/instruction_builder/mod.rs index 2a4b5eb0..db2640ae 100644 --- a/interface/src/instruction_builder/mod.rs +++ b/interface/src/instruction_builder/mod.rs @@ -1,5 +1,6 @@ mod approve_commitment; mod challenger_reveal; +mod commit_finalize; mod delegate; mod finalize_commitment; mod init_delegation_cluster; @@ -14,6 +15,7 @@ mod write_state_buffer; pub use approve_commitment::*; pub use challenger_reveal::*; +pub use commit_finalize::*; pub use delegate::*; pub use finalize_commitment::*; pub use init_delegation_cluster::*; diff --git a/program/src/processor/direct/commit_finalize.rs b/program/src/processor/direct/commit_finalize.rs new file mode 100644 index 00000000..1c8833b9 --- /dev/null +++ b/program/src/processor/direct/commit_finalize.rs @@ -0,0 +1,62 @@ +use delegation_program_interface as dpi; +use dpi::{CommitFinalizeArgs, CommitPayloadKind}; +use pinocchio::{error::ProgramError, AccountView, ProgramResult}; +use wheels::{layout::Decodable, require_eq, require_n_accounts}; + +use super::{ + process_commit_finalize_internal, CommitFinalizeInput, CommitStateUpdate, +}; + +/// Commit and immediately apply one state update for a Direct cluster. +/// +/// Accounts: +/// 0: `[signer, writable]` current validator and lamport settlement account +/// 1: `[writable]` delegated account +/// 2: `[writable]` DelegationState PDA +/// 3: `[]` DelegationCluster PDA +/// 4: `[]` system program, required by system CPI +#[inline(never)] +pub fn process_commit_finalize( + accounts: &[AccountView], + data: &[u8], +) -> ProgramResult { + let [ + current_validator, // force multi-line + delegated_account, + delegation_state, + delegation_cluster, + _system_program, + ] = require_n_accounts!(accounts, 5); + + let args = CommitFinalizeArgs::decode(data)?; + require_eq!( + args.payload_alignment_padding(), + 0, + ProgramError::InvalidInstructionData + ); + let state_update = match args.payload_kind() { + value if value == CommitPayloadKind::FullData.value() => { + CommitStateUpdate::FullData(args.payload()) + } + value if value == CommitPayloadKind::Diff.value() => { + CommitStateUpdate::Diff(dpi::diff::DiffSet::try_new( + args.payload(), + )?) + } + _ => return Err(ProgramError::InvalidInstructionData), + }; + + process_commit_finalize_internal( + current_validator, + delegated_account, + delegation_state, + delegation_cluster, + CommitFinalizeInput { + commit_id: args.commit_id(), + lamports: args.lamports(), + owner: args.owner(), + allow_undelegation: args.allow_undelegation(), + state_update, + }, + ) +} diff --git a/program/src/processor/direct/commit_finalize_internal.rs b/program/src/processor/direct/commit_finalize_internal.rs new file mode 100644 index 00000000..de3db4d0 --- /dev/null +++ b/program/src/processor/direct/commit_finalize_internal.rs @@ -0,0 +1,215 @@ +use delegation_program_interface as dpi; +use dpi::{ + error::DlpError, + pda::{DELEGATION_CLUSTER_SEED, DELEGATION_STATE_SEED}, + DelegationCluster, DelegationState, UndelegationRequester, +}; +use pinocchio::{ + address::address_eq, + error::ProgramError, + sysvars::{rent::Rent, Sysvar}, + AccountView, ProgramResult, +}; +use pinocchio_system::instructions as system; +use wheels::{ + layout::Decodable, require, require_eq, require_eq_keys, require_ge, + require_owned_by, require_signer, +}; + +use crate::{ + apply_diff_in_place, legacy::processor::fast::utils::LamportsOperation, +}; + +pub(crate) enum CommitStateUpdate<'a> { + FullData(&'a [u8]), + Diff(dpi::diff::DiffSet<'a>), +} + +impl CommitStateUpdate<'_> { + fn data_len(&self) -> usize { + match self { + Self::FullData(data) => data.len(), + Self::Diff(diff) => diff.changed_len(), + } + } +} + +pub(crate) struct CommitFinalizeInput<'a> { + pub(crate) commit_id: u64, + pub(crate) lamports: u64, + pub(crate) owner: &'a dpi::compat::Pubkey, + pub(crate) allow_undelegation: bool, + pub(crate) state_update: CommitStateUpdate<'a>, +} + +pub(crate) fn process_commit_finalize_internal( + current_validator: &AccountView, + delegated_account: &AccountView, + delegation_state: &AccountView, + delegation_cluster: &AccountView, + input: CommitFinalizeInput<'_>, +) -> ProgramResult { + require_signer!(current_validator); + require!(current_validator.is_writable(), ProgramError::Immutable); + require_owned_by!(delegated_account, &crate::fast::ID); + require!(delegated_account.is_writable(), ProgramError::Immutable); + + let cluster_data = delegation_cluster.try_borrow()?; + let cluster = DelegationCluster::decode(cluster_data.as_ref())?; + let cluster_bump = [cluster.bump()]; + dpi::require_initialized_pda!( + delegation_cluster, + &[ + DELEGATION_CLUSTER_SEED, + cluster.cluster_id().as_ref(), + &cluster_bump, + ], + &crate::fast::ID, + false + ); + require_eq!( + &cluster.discriminator(), + &DelegationCluster::DISCRIMINATOR, + ProgramError::InvalidAccountData + ); + require!(!cluster.paused(), ProgramError::InvalidAccountData); + require_eq!( + cluster.challenge_config().is_none(), + true, + ProgramError::InvalidInstructionData + ); + require_eq_keys!( + cluster.current_validator(), + current_validator.address(), + DlpError::InvalidAuthority + ); + drop(cluster_data); + + let state_data = delegation_state.try_borrow()?; + let state = DelegationState::decode(state_data.as_ref())?; + let state_bump = [state.bump()]; + dpi::require_initialized_pda!( + delegation_state, + &[ + DELEGATION_STATE_SEED, + delegated_account.address().as_ref(), + &state_bump, + ], + &crate::fast::ID, + true + ); + require_eq!( + &state.discriminator(), + &DelegationState::DISCRIMINATOR, + ProgramError::InvalidAccountData + ); + require_eq_keys!( + state.delegated_account(), + delegated_account.address(), + DlpError::InvalidDelegatedAccount + ); + require_eq_keys!( + state.delegation_cluster(), + delegation_cluster.address(), + ProgramError::InvalidAccountData + ); + require_eq_keys!( + state.original_owner(), + input.owner, + ProgramError::InvalidAccountData + ); + require_eq!( + input.commit_id, + state + .last_commit_id() + .checked_add(1) + .ok_or(DlpError::Overflow)?, + DlpError::NonceOutOfOrder + ); + + let next_undelegation_requester = match state.undelegation_requester() { + value if value == UndelegationRequester::None.value() => { + if input.allow_undelegation { + UndelegationRequester::Validator.value() + } else { + value + } + } + value if value == UndelegationRequester::Validator.value() => { + return Err(DlpError::AlreadyUndelegated.into()); + } + value if value == UndelegationRequester::OwnerProgram.value() => { + require!( + input.allow_undelegation, + DlpError::OwnerRequestedUndelegation + ); + value + } + _ => return Err(ProgramError::InvalidAccountData), + }; + let previous_lamports = state.lamports(); + drop(state_data); + + require_ge!( + delegated_account.lamports(), + previous_lamports, + DlpError::InvalidDelegatedState + ); + + delegated_account.resize(input.state_update.data_len())?; + settle_lamports( + current_validator, + delegated_account, + previous_lamports, + input.lamports, + )?; + require_ge!( + delegated_account.lamports(), + Rent::get()?.try_minimum_balance(delegated_account.data_len())?, + DlpError::InsufficientRent + ); + + let mut account_data = delegated_account.try_borrow_mut()?; + match input.state_update { + CommitStateUpdate::FullData(data) => { + account_data.as_mut().copy_from_slice(data); + } + CommitStateUpdate::Diff(diff) => { + apply_diff_in_place(account_data.as_mut(), &diff)?; + } + } + drop(account_data); + + let mut state = DelegationState::decode_mut(delegation_state)?; + state.lamports_mut()?.set(input.lamports)?; + state.last_commit_id_mut()?.set(input.commit_id)?; + *state.undelegation_requester_mut()? = next_undelegation_requester; + + Ok(()) +} + +fn settle_lamports( + current_validator: &AccountView, + delegated_account: &AccountView, + previous_lamports: u64, + commit_lamports: u64, +) -> ProgramResult { + match commit_lamports.cmp(&previous_lamports) { + core::cmp::Ordering::Greater => system::Transfer { + from: current_validator, + to: delegated_account, + lamports: commit_lamports + .checked_sub(previous_lamports) + .ok_or(DlpError::Overflow)?, + } + .invoke(), + core::cmp::Ordering::Less => { + let delta = previous_lamports + .checked_sub(commit_lamports) + .ok_or(DlpError::Overflow)?; + delegated_account.lamports_decrement_by(delta)?; + current_validator.lamports_increment_by(delta) + } + core::cmp::Ordering::Equal => Ok(()), + } +} diff --git a/program/src/processor/direct/mod.rs b/program/src/processor/direct/mod.rs new file mode 100644 index 00000000..8d912a6c --- /dev/null +++ b/program/src/processor/direct/mod.rs @@ -0,0 +1,7 @@ +//! Processors for clusters that finalize without fraud proofs. + +mod commit_finalize; +mod commit_finalize_internal; + +pub use commit_finalize::*; +pub(crate) use commit_finalize_internal::*; diff --git a/program/src/processor/mod.rs b/program/src/processor/mod.rs index 11fbba97..8cf6c0dc 100644 --- a/program/src/processor/mod.rs +++ b/program/src/processor/mod.rs @@ -1,10 +1,12 @@ use delegation_program_interface as dpi; mod bootstrap; mod delegate; +mod direct; mod fraud_proofs; pub use bootstrap::*; pub use delegate::*; +pub use direct::*; use dpi::DlpV2Instruction; pub use fraud_proofs::*; use pinocchio::{AccountView, ProgramResult}; @@ -53,5 +55,8 @@ pub fn process_instruction( process_resolve_dispute(accounts, data) } DlpV2Instruction::Delegate => process_delegate(accounts, data), + DlpV2Instruction::CommitFinalize => { + process_commit_finalize(accounts, data) + } } } diff --git a/program/tests/test_primary_commit_finalize.rs b/program/tests/test_primary_commit_finalize.rs new file mode 100644 index 00000000..7f74cd0c --- /dev/null +++ b/program/tests/test_primary_commit_finalize.rs @@ -0,0 +1,366 @@ +use delegation_program::solana_program; +use delegation_program_interface as dpi; +use dpi::{ + diff::compute_diff, + instruction_builder::commit_finalize, + pda::{delegation_cluster_pda, delegation_state_pda}, + CommitFinalizeArgs, CommitPayloadKind, DelegationCluster, DelegationState, + UndelegationRequester, +}; +use solana_program::{ + native_token::LAMPORTS_PER_SOL, pubkey::Pubkey, rent::Rent, +}; +use solana_program_test::{BanksClient, BanksClientError, ProgramTest}; +use solana_sdk::{ + account::Account, + instruction::InstructionError, + signature::{Keypair, Signer}, + transaction::{Transaction, TransactionError}, +}; +use solana_sdk_ids::system_program; +use wheels::layout::{Decodable, Encodable}; + +const INITIAL_DATA: &[u8] = b"initial account data"; +const FULL_DATA: &[u8] = b"complete committed account data"; +const INITIAL_LAMPORTS: u64 = LAMPORTS_PER_SOL; + +struct CommitFinalizeTestContext { + banks: BanksClient, + payer: Keypair, + current_validator: Keypair, + delegated_account: Pubkey, + delegation_state: Pubkey, + delegation_cluster: Pubkey, + original_owner: Pubkey, +} + +#[tokio::test] +async fn test_commit_finalize_applies_full_data_and_diff() { + let context = + setup_commit_finalize_test(false, UndelegationRequester::None).await; + + submit_commit( + &context, + &context.current_validator, + CommitFinalizeArgs { + commit_id: 1, + lamports: INITIAL_LAMPORTS + 500, + owner: context.original_owner, + allow_undelegation: false, + payload_kind: CommitPayloadKind::FullData.value(), + payload_alignment_padding: 0, + payload: FULL_DATA.to_vec(), + }, + ) + .await + .unwrap(); + + { + let account = context + .banks + .get_account(context.delegated_account) + .await + .unwrap() + .unwrap(); + let state_account = context + .banks + .get_account(context.delegation_state) + .await + .unwrap() + .unwrap(); + let state = DelegationState::decode(&state_account.data).unwrap(); + + assert_eq!(account.data, FULL_DATA); + assert_eq!(account.lamports, INITIAL_LAMPORTS + 500); + assert_eq!(state.last_commit_id(), 1); + assert_eq!(state.lamports(), INITIAL_LAMPORTS + 500); + assert_eq!( + state.undelegation_requester(), + UndelegationRequester::None.value() + ); + } + + let changed_data = b"complete changed account data and tail"; + submit_commit( + &context, + &context.current_validator, + CommitFinalizeArgs { + commit_id: 2, + lamports: INITIAL_LAMPORTS + 300, + owner: context.original_owner, + allow_undelegation: true, + payload_kind: CommitPayloadKind::Diff.value(), + payload_alignment_padding: 0, + payload: compute_diff(FULL_DATA, changed_data).to_vec(), + }, + ) + .await + .unwrap(); + + let account = context + .banks + .get_account(context.delegated_account) + .await + .unwrap() + .unwrap(); + let state_account = context + .banks + .get_account(context.delegation_state) + .await + .unwrap() + .unwrap(); + let state = DelegationState::decode(&state_account.data).unwrap(); + + assert_eq!(account.data, changed_data); + assert_eq!(account.lamports, INITIAL_LAMPORTS + 300); + assert_eq!(state.last_commit_id(), 2); + assert_eq!(state.lamports(), INITIAL_LAMPORTS + 300); + assert_eq!( + state.undelegation_requester(), + UndelegationRequester::Validator.value() + ); +} + +#[tokio::test] +async fn test_commit_finalize_rejects_out_of_order_commit() { + let context = + setup_commit_finalize_test(false, UndelegationRequester::None).await; + let result = submit_commit( + &context, + &context.current_validator, + full_data_args(&context, 2), + ) + .await; + + assert_custom_error( + result.unwrap_err(), + dpi::error::DlpError::NonceOutOfOrder, + ); +} + +#[tokio::test] +async fn test_commit_finalize_rejects_challenge_cluster() { + let context = + setup_commit_finalize_test(true, UndelegationRequester::None).await; + + assert!(submit_commit( + &context, + &context.current_validator, + full_data_args(&context, 1), + ) + .await + .is_err()); +} + +#[tokio::test] +async fn test_commit_finalize_rejects_wrong_validator_and_owner() { + let context = + setup_commit_finalize_test(false, UndelegationRequester::None).await; + + assert_custom_error( + submit_commit(&context, &context.payer, full_data_args(&context, 1)) + .await + .unwrap_err(), + dpi::error::DlpError::InvalidAuthority, + ); + + let mut args = full_data_args(&context, 1); + args.owner = Pubkey::new_unique(); + assert!(submit_commit(&context, &context.current_validator, args) + .await + .is_err()); +} + +#[tokio::test] +async fn test_commit_finalize_requires_owner_undelegation_to_be_allowed() { + let context = + setup_commit_finalize_test(false, UndelegationRequester::OwnerProgram) + .await; + + assert_custom_error( + submit_commit( + &context, + &context.current_validator, + full_data_args(&context, 1), + ) + .await + .unwrap_err(), + dpi::error::DlpError::OwnerRequestedUndelegation, + ); +} + +fn full_data_args( + context: &CommitFinalizeTestContext, + commit_id: u64, +) -> CommitFinalizeArgs { + CommitFinalizeArgs { + commit_id, + lamports: INITIAL_LAMPORTS, + owner: context.original_owner, + allow_undelegation: false, + payload_kind: CommitPayloadKind::FullData.value(), + payload_alignment_padding: 0, + payload: FULL_DATA.to_vec(), + } +} + +async fn submit_commit( + context: &CommitFinalizeTestContext, + validator: &Keypair, + args: CommitFinalizeArgs, +) -> Result<(), BanksClientError> { + let instruction = commit_finalize( + validator.pubkey(), + context.delegation_cluster, + context.delegated_account, + args, + ) + .unwrap(); + let tx = Transaction::new_signed_with_payer( + &[instruction], + Some(&context.payer.pubkey()), + &[&context.payer, validator], + context.banks.get_latest_blockhash().await.unwrap(), + ); + + context.banks.process_transaction(tx).await +} + +async fn setup_commit_finalize_test( + challenge_enabled: bool, + undelegation_requester: UndelegationRequester, +) -> CommitFinalizeTestContext { + let mut program_test = ProgramTest::default(); + program_test.prefer_bpf(true); + program_test.add_program("delegation_program", dpi::ID, None); + + let current_validator = Keypair::new(); + let delegated_account = Pubkey::new_unique(); + let original_owner = Pubkey::new_unique(); + let cluster_id = Pubkey::new_unique(); + let delegation_cluster = delegation_cluster_pda(&cluster_id); + let (_, cluster_bump) = Pubkey::find_program_address( + &[dpi::pda::DELEGATION_CLUSTER_SEED, cluster_id.as_ref()], + &dpi::id(), + ); + let delegation_state = delegation_state_pda(&delegated_account); + let (_, state_bump) = Pubkey::find_program_address( + &[dpi::pda::DELEGATION_STATE_SEED, delegated_account.as_ref()], + &dpi::id(), + ); + + program_test.add_account( + current_validator.pubkey(), + Account { + lamports: 10 * LAMPORTS_PER_SOL, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + program_test.add_account( + delegated_account, + Account { + lamports: INITIAL_LAMPORTS, + data: INITIAL_DATA.to_vec(), + owner: dpi::id(), + executable: false, + rent_epoch: 0, + }, + ); + + let cluster_data = DelegationCluster { + discriminator: DelegationCluster::DISCRIMINATOR, + bump: cluster_bump, + cluster_id, + config_authority: Pubkey::new_unique(), + validator_rotation_authority: Pubkey::new_unique(), + current_validator: current_validator.pubkey(), + paused: false, + protocol_fee_vault: Pubkey::new_unique(), + challenge_config: challenge_enabled.then(|| dpi::ChallengeConfig { + resolver: Pubkey::new_unique(), + min_operator_bond: 1, + min_verifier_bond: 1, + min_challenger_stake: 1, + challenge_window_slots: 1, + operator_response_timeout_slots: 1, + challenger_reveal_timeout_slots: 1, + payout_timelock_slots: 1, + verifiers_per_commitment: 1, + approval_threshold: 1, + max_window_extensions: 1, + match_penalty_bps: 1, + }), + } + .encode() + .unwrap(); + program_test.add_account( + delegation_cluster, + Account { + lamports: Rent::default().minimum_balance(cluster_data.len()), + data: cluster_data, + owner: dpi::id(), + executable: false, + rent_epoch: 0, + }, + ); + + let state_data = DelegationState { + discriminator: DelegationState::DISCRIMINATOR, + bump: state_bump, + delegated_account, + delegation_cluster, + original_owner, + rent_payer: Pubkey::new_unique(), + delegation_slot: 0, + lamports: INITIAL_LAMPORTS, + last_commit_id: 0, + undelegation_requester: undelegation_requester.value(), + delegated_account_bump: 0, + seed_count: 0, + encoded_seeds: vec![], + } + .encode() + .unwrap(); + program_test.add_account( + delegation_state, + Account { + lamports: Rent::default().minimum_balance(state_data.len()), + data: state_data, + owner: dpi::id(), + executable: false, + rent_epoch: 0, + }, + ); + + let (banks, payer, _) = program_test.start().await; + CommitFinalizeTestContext { + banks, + payer, + current_validator, + delegated_account, + delegation_state, + delegation_cluster, + original_owner, + } +} + +fn assert_custom_error( + error: BanksClientError, + expected: dpi::error::DlpError, +) { + assert!( + matches!( + error, + BanksClientError::TransactionError( + TransactionError::InstructionError( + 0, + InstructionError::Custom(code), + ), + ) if code == expected as u32 + ), + "unexpected error: {error:?}" + ); +} From 255ff95b3c807cf1f20d6f86004923835ead2bb5 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Thu, 22 Oct 2026 02:15:59 +0530 Subject: [PATCH 2/4] Move shared commit logic into processor internals Place the reusable commit/finalize state transition under processor/internal so instruction modules depend on common processor machinery instead of sibling processor modules. --- program/src/processor/direct/commit_finalize.rs | 6 +++--- program/src/processor/direct/mod.rs | 2 -- .../commit_finalize.rs} | 2 +- program/src/processor/internal/mod.rs | 3 +++ program/src/processor/mod.rs | 1 + 5 files changed, 8 insertions(+), 6 deletions(-) rename program/src/processor/{direct/commit_finalize_internal.rs => internal/commit_finalize.rs} (99%) create mode 100644 program/src/processor/internal/mod.rs diff --git a/program/src/processor/direct/commit_finalize.rs b/program/src/processor/direct/commit_finalize.rs index 1c8833b9..631b37bc 100644 --- a/program/src/processor/direct/commit_finalize.rs +++ b/program/src/processor/direct/commit_finalize.rs @@ -3,8 +3,8 @@ use dpi::{CommitFinalizeArgs, CommitPayloadKind}; use pinocchio::{error::ProgramError, AccountView, ProgramResult}; use wheels::{layout::Decodable, require_eq, require_n_accounts}; -use super::{ - process_commit_finalize_internal, CommitFinalizeInput, CommitStateUpdate, +use crate::processor::internal::{ + apply_commit_finalize, CommitFinalizeInput, CommitStateUpdate, }; /// Commit and immediately apply one state update for a Direct cluster. @@ -46,7 +46,7 @@ pub fn process_commit_finalize( _ => return Err(ProgramError::InvalidInstructionData), }; - process_commit_finalize_internal( + apply_commit_finalize( current_validator, delegated_account, delegation_state, diff --git a/program/src/processor/direct/mod.rs b/program/src/processor/direct/mod.rs index 8d912a6c..64a66c1e 100644 --- a/program/src/processor/direct/mod.rs +++ b/program/src/processor/direct/mod.rs @@ -1,7 +1,5 @@ //! Processors for clusters that finalize without fraud proofs. mod commit_finalize; -mod commit_finalize_internal; pub use commit_finalize::*; -pub(crate) use commit_finalize_internal::*; diff --git a/program/src/processor/direct/commit_finalize_internal.rs b/program/src/processor/internal/commit_finalize.rs similarity index 99% rename from program/src/processor/direct/commit_finalize_internal.rs rename to program/src/processor/internal/commit_finalize.rs index de3db4d0..f7448a44 100644 --- a/program/src/processor/direct/commit_finalize_internal.rs +++ b/program/src/processor/internal/commit_finalize.rs @@ -42,7 +42,7 @@ pub(crate) struct CommitFinalizeInput<'a> { pub(crate) state_update: CommitStateUpdate<'a>, } -pub(crate) fn process_commit_finalize_internal( +pub(crate) fn apply_commit_finalize( current_validator: &AccountView, delegated_account: &AccountView, delegation_state: &AccountView, diff --git a/program/src/processor/internal/mod.rs b/program/src/processor/internal/mod.rs new file mode 100644 index 00000000..fe84489c --- /dev/null +++ b/program/src/processor/internal/mod.rs @@ -0,0 +1,3 @@ +mod commit_finalize; + +pub(crate) use commit_finalize::*; diff --git a/program/src/processor/mod.rs b/program/src/processor/mod.rs index 8cf6c0dc..81cb2afa 100644 --- a/program/src/processor/mod.rs +++ b/program/src/processor/mod.rs @@ -3,6 +3,7 @@ mod bootstrap; mod delegate; mod direct; mod fraud_proofs; +pub(crate) mod internal; pub use bootstrap::*; pub use delegate::*; From f3c3e2852bbe5946241c59e83189311a37bd3ee7 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Fri, 30 Oct 2026 22:48:38 +0530 Subject: [PATCH 3/4] Use commit sequence terminology --- interface/src/args/commit_finalize.rs | 2 +- program/src/processor/direct/commit_finalize.rs | 2 +- program/src/processor/internal/commit_finalize.rs | 8 ++++---- program/tests/test_primary_commit_finalize.rs | 14 +++++++------- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/interface/src/args/commit_finalize.rs b/interface/src/args/commit_finalize.rs index 2ed7442e..2594853f 100644 --- a/interface/src/args/commit_finalize.rs +++ b/interface/src/args/commit_finalize.rs @@ -6,7 +6,7 @@ use crate::compat::Pubkey; #[variable_offset_layout(buffer_offset = 1)] pub struct CommitFinalizeArgs { /// Next commit id for this delegated account. - pub commit_id: u64, + pub commit_seq: u64, /// Account lamports on the ER after this commit. pub lamports: u64, diff --git a/program/src/processor/direct/commit_finalize.rs b/program/src/processor/direct/commit_finalize.rs index 631b37bc..b785e4f2 100644 --- a/program/src/processor/direct/commit_finalize.rs +++ b/program/src/processor/direct/commit_finalize.rs @@ -52,7 +52,7 @@ pub fn process_commit_finalize( delegation_state, delegation_cluster, CommitFinalizeInput { - commit_id: args.commit_id(), + commit_seq: args.commit_seq(), lamports: args.lamports(), owner: args.owner(), allow_undelegation: args.allow_undelegation(), diff --git a/program/src/processor/internal/commit_finalize.rs b/program/src/processor/internal/commit_finalize.rs index f7448a44..35103339 100644 --- a/program/src/processor/internal/commit_finalize.rs +++ b/program/src/processor/internal/commit_finalize.rs @@ -35,7 +35,7 @@ impl CommitStateUpdate<'_> { } pub(crate) struct CommitFinalizeInput<'a> { - pub(crate) commit_id: u64, + pub(crate) commit_seq: u64, pub(crate) lamports: u64, pub(crate) owner: &'a dpi::compat::Pubkey, pub(crate) allow_undelegation: bool, @@ -119,9 +119,9 @@ pub(crate) fn apply_commit_finalize( ProgramError::InvalidAccountData ); require_eq!( - input.commit_id, + input.commit_seq, state - .last_commit_id() + .last_commit_seq() .checked_add(1) .ok_or(DlpError::Overflow)?, DlpError::NonceOutOfOrder @@ -182,7 +182,7 @@ pub(crate) fn apply_commit_finalize( let mut state = DelegationState::decode_mut(delegation_state)?; state.lamports_mut()?.set(input.lamports)?; - state.last_commit_id_mut()?.set(input.commit_id)?; + state.last_commit_seq_mut()?.set(input.commit_seq)?; *state.undelegation_requester_mut()? = next_undelegation_requester; Ok(()) diff --git a/program/tests/test_primary_commit_finalize.rs b/program/tests/test_primary_commit_finalize.rs index 7f74cd0c..287bd2c6 100644 --- a/program/tests/test_primary_commit_finalize.rs +++ b/program/tests/test_primary_commit_finalize.rs @@ -43,7 +43,7 @@ async fn test_commit_finalize_applies_full_data_and_diff() { &context, &context.current_validator, CommitFinalizeArgs { - commit_id: 1, + commit_seq: 1, lamports: INITIAL_LAMPORTS + 500, owner: context.original_owner, allow_undelegation: false, @@ -72,7 +72,7 @@ async fn test_commit_finalize_applies_full_data_and_diff() { assert_eq!(account.data, FULL_DATA); assert_eq!(account.lamports, INITIAL_LAMPORTS + 500); - assert_eq!(state.last_commit_id(), 1); + assert_eq!(state.last_commit_seq(), 1); assert_eq!(state.lamports(), INITIAL_LAMPORTS + 500); assert_eq!( state.undelegation_requester(), @@ -85,7 +85,7 @@ async fn test_commit_finalize_applies_full_data_and_diff() { &context, &context.current_validator, CommitFinalizeArgs { - commit_id: 2, + commit_seq: 2, lamports: INITIAL_LAMPORTS + 300, owner: context.original_owner, allow_undelegation: true, @@ -113,7 +113,7 @@ async fn test_commit_finalize_applies_full_data_and_diff() { assert_eq!(account.data, changed_data); assert_eq!(account.lamports, INITIAL_LAMPORTS + 300); - assert_eq!(state.last_commit_id(), 2); + assert_eq!(state.last_commit_seq(), 2); assert_eq!(state.lamports(), INITIAL_LAMPORTS + 300); assert_eq!( state.undelegation_requester(), @@ -191,10 +191,10 @@ async fn test_commit_finalize_requires_owner_undelegation_to_be_allowed() { fn full_data_args( context: &CommitFinalizeTestContext, - commit_id: u64, + commit_seq: u64, ) -> CommitFinalizeArgs { CommitFinalizeArgs { - commit_id, + commit_seq, lamports: INITIAL_LAMPORTS, owner: context.original_owner, allow_undelegation: false, @@ -316,7 +316,7 @@ async fn setup_commit_finalize_test( rent_payer: Pubkey::new_unique(), delegation_slot: 0, lamports: INITIAL_LAMPORTS, - last_commit_id: 0, + last_commit_seq: 0, undelegation_requester: undelegation_requester.value(), delegated_account_bump: 0, seed_count: 0, From dffc2f681cc57fdc65c55f5d698a3292f9ef0c14 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Fri, 30 Oct 2026 22:55:40 +0530 Subject: [PATCH 4/4] Spell out commit sequence number in prose --- interface/src/args/commit_finalize.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/args/commit_finalize.rs b/interface/src/args/commit_finalize.rs index 2594853f..f37dea16 100644 --- a/interface/src/args/commit_finalize.rs +++ b/interface/src/args/commit_finalize.rs @@ -5,7 +5,7 @@ use crate::compat::Pubkey; #[derive(Clone, Debug, PartialEq, Eq)] #[variable_offset_layout(buffer_offset = 1)] pub struct CommitFinalizeArgs { - /// Next commit id for this delegated account. + /// Next commit sequence number for this delegated account. pub commit_seq: u64, /// Account lamports on the ER after this commit.