From bf93ac182e58e88c96e9f01548b376e9bddf26a4 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Fri, 19 Jun 2026 23:30:21 +0530 Subject: [PATCH 01/30] feat: Impl RequestUndelegation --- dlp-api/src/discriminator.rs | 3 + dlp-api/src/error.rs | 18 + dlp-api/src/instruction_builder/mod.rs | 2 + .../request_undelegation.rs | 73 +++ dlp-api/src/instruction_builder/undelegate.rs | 104 +++- dlp-api/src/pda.rs | 21 + dlp-api/src/requires.rs | 10 + dlp-api/src/state/mod.rs | 2 + dlp-api/src/state/undelegation_request.rs | 41 ++ dlp-api/src/state/utils/discriminator.rs | 1 + src/lib.rs | 6 + src/processor/fast/mod.rs | 2 + src/processor/fast/request_undelegation.rs | 170 ++++++ src/processor/fast/undelegate.rs | 100 +++- tests/fixtures/accounts.rs | 19 + tests/test_request_undelegation.rs | 555 ++++++++++++++++++ 16 files changed, 1107 insertions(+), 20 deletions(-) create mode 100644 dlp-api/src/instruction_builder/request_undelegation.rs create mode 100644 dlp-api/src/state/undelegation_request.rs create mode 100644 src/processor/fast/request_undelegation.rs create mode 100644 tests/test_request_undelegation.rs diff --git a/dlp-api/src/discriminator.rs b/dlp-api/src/discriminator.rs index e632bfd6..6ee64dbf 100644 --- a/dlp-api/src/discriminator.rs +++ b/dlp-api/src/discriminator.rs @@ -62,6 +62,9 @@ pub enum DlpDiscriminator { /// See [crate::processor::process_delegate_magic_fee_vault] for docs. DelegateMagicFeeVault = 25, + + /// See [crate::processor::process_request_undelegation] for docs. + RequestUndelegation = 26, } impl DlpDiscriminator { diff --git a/dlp-api/src/error.rs b/dlp-api/src/error.rs index 883d8a1a..e1bf57b3 100644 --- a/dlp-api/src/error.rs +++ b/dlp-api/src/error.rs @@ -155,6 +155,24 @@ pub enum DlpError { )] InsufficientRent = 43, + #[error("Undelegation request PDA invalid seeds")] + UndelegationRequestInvalidSeeds = 44, + + #[error("Undelegation request PDA invalid account owner")] + UndelegationRequestInvalidAccountOwner = 45, + + #[error("Undelegation request PDA is already initialized")] + UndelegationRequestAlreadyInitialized = 46, + + #[error("Undelegation request PDA immutable")] + UndelegationRequestImmutable = 47, + + #[error("Invalid undelegation request")] + InvalidUndelegationRequest = 48, + + #[error("Request undelegation is only supported for off-curve delegated accounts")] + RequestUndelegationOnCurveAccount = 49, + #[error("An infallible error is encountered possibly due to logic error")] InfallibleError = 100, } diff --git a/dlp-api/src/instruction_builder/mod.rs b/dlp-api/src/instruction_builder/mod.rs index adf93ffc..64cf1a0e 100644 --- a/dlp-api/src/instruction_builder/mod.rs +++ b/dlp-api/src/instruction_builder/mod.rs @@ -18,6 +18,7 @@ mod init_magic_fee_vault; mod init_protocol_fees_vault; mod init_validator_fees_vault; mod protocol_claim_fees; +mod request_undelegation; mod top_up_ephemeral_balance; mod types; mod undelegate; @@ -45,6 +46,7 @@ pub use init_magic_fee_vault::*; pub use init_protocol_fees_vault::*; pub use init_validator_fees_vault::*; pub use protocol_claim_fees::*; +pub use request_undelegation::*; pub use top_up_ephemeral_balance::*; pub use types::*; pub use undelegate::*; diff --git a/dlp-api/src/instruction_builder/request_undelegation.rs b/dlp-api/src/instruction_builder/request_undelegation.rs new file mode 100644 index 00000000..3019f92d --- /dev/null +++ b/dlp-api/src/instruction_builder/request_undelegation.rs @@ -0,0 +1,73 @@ +use dlp::{ + discriminator::DlpDiscriminator, + pda::{ + delegation_metadata_pda_from_delegated_account, + delegation_record_pda_from_delegated_account, + undelegation_request_pda_from_delegated_account, + }, + total_size_budget, AccountSizeClass, DLP_PROGRAM_DATA_SIZE_CLASS, +}; +use solana_program::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, +}; +use solana_sdk_ids::system_program; + +use crate::compat::{Compatize, Modernize}; + +/// Builds a request undelegation instruction. +/// See [dlp::processor::process_request_undelegation] for docs. +pub fn request_undelegation( + payer: Pubkey, + delegated_account: Pubkey, + owner_program: Pubkey, +) -> Instruction { + let delegated_account_compat = delegated_account.compatize(); + let undelegation_request_pda = + undelegation_request_pda_from_delegated_account( + &delegated_account_compat, + ) + .modernize(); + let delegation_record_pda = + delegation_record_pda_from_delegated_account(&delegated_account_compat) + .modernize(); + let delegation_metadata_pda = + delegation_metadata_pda_from_delegated_account( + &delegated_account_compat, + ) + .modernize(); + + Instruction { + program_id: dlp::id().modernize(), + accounts: vec![ + AccountMeta::new(payer, true), + AccountMeta::new_readonly(delegated_account, true), + AccountMeta::new_readonly(owner_program, false), + AccountMeta::new(undelegation_request_pda, false), + AccountMeta::new_readonly(delegation_record_pda, false), + AccountMeta::new_readonly(delegation_metadata_pda, false), + AccountMeta::new_readonly(system_program::id(), false), + ], + data: DlpDiscriminator::RequestUndelegation.to_vec(), + } +} + +/// +/// Returns accounts-data-size budget for request undelegation instruction. +/// +/// This value can be used with ComputeBudgetInstruction::SetLoadedAccountsDataSizeLimit +/// +pub fn request_undelegation_size_budget( + delegated_account: AccountSizeClass, +) -> u32 { + total_size_budget(&[ + DLP_PROGRAM_DATA_SIZE_CLASS, + AccountSizeClass::Tiny, // payer + delegated_account, // delegated_account + AccountSizeClass::Tiny, // owner_program + AccountSizeClass::Tiny, // undelegation_request_pda + AccountSizeClass::Tiny, // delegation_record_pda + AccountSizeClass::Tiny, // delegation_metadata_pda + AccountSizeClass::Tiny, // system_program + ]) +} diff --git a/dlp-api/src/instruction_builder/undelegate.rs b/dlp-api/src/instruction_builder/undelegate.rs index ca58317c..d526fcd5 100644 --- a/dlp-api/src/instruction_builder/undelegate.rs +++ b/dlp-api/src/instruction_builder/undelegate.rs @@ -6,6 +6,7 @@ use dlp::{ delegation_metadata_pda_from_delegated_account, delegation_record_pda_from_delegated_account, fees_vault_pda, undelegate_buffer_pda_from_delegated_account, + undelegation_request_pda_from_delegated_account, validator_fees_vault_pda_from_validator, }, total_size_budget, AccountSizeClass, DLP_PROGRAM_DATA_SIZE_CLASS, @@ -26,6 +27,41 @@ pub fn undelegate( delegated_account: Pubkey, owner_program: Pubkey, rent_reimbursement: Pubkey, +) -> Instruction { + build_undelegate( + validator, + delegated_account, + owner_program, + rent_reimbursement, + None, + ) +} + +/// Builds an undelegate instruction that also closes a matching request. +/// See [dlp::processor::process_undelegate] for docs. +#[allow(clippy::too_many_arguments)] +pub fn undelegate_with_request( + validator: Pubkey, + delegated_account: Pubkey, + owner_program: Pubkey, + rent_reimbursement: Pubkey, + request_rent_payer: Pubkey, +) -> Instruction { + build_undelegate( + validator, + delegated_account, + owner_program, + rent_reimbursement, + Some(request_rent_payer), + ) +} + +fn build_undelegate( + validator: Pubkey, + delegated_account: Pubkey, + owner_program: Pubkey, + rent_reimbursement: Pubkey, + request_rent_payer: Option, ) -> Instruction { let validator_compat = validator.compatize(); let delegated_account_compat = delegated_account.compatize(); @@ -49,22 +85,34 @@ pub fn undelegate( let fees_vault_pda = fees_vault_pda().modernize(); let validator_fees_vault_pda = validator_fees_vault_pda_from_validator(&validator_compat).modernize(); + let mut accounts = vec![ + AccountMeta::new(validator, true), + AccountMeta::new(delegated_account, false), + AccountMeta::new_readonly(owner_program, false), + AccountMeta::new(undelegate_buffer_pda, false), + AccountMeta::new_readonly(commit_state_pda, false), + AccountMeta::new_readonly(commit_record_pda, false), + AccountMeta::new(delegation_record_pda, false), + AccountMeta::new(delegation_metadata_pda, false), + AccountMeta::new(rent_reimbursement, false), + AccountMeta::new(fees_vault_pda, false), + AccountMeta::new(validator_fees_vault_pda, false), + AccountMeta::new_readonly(system_program::id(), false), + ]; + + if let Some(request_rent_payer) = request_rent_payer { + let undelegation_request_pda = + undelegation_request_pda_from_delegated_account( + &delegated_account_compat, + ) + .modernize(); + accounts.push(AccountMeta::new(undelegation_request_pda, false)); + accounts.push(AccountMeta::new(request_rent_payer, false)); + } + Instruction { program_id: dlp::id().modernize(), - accounts: vec![ - AccountMeta::new(validator, true), - AccountMeta::new(delegated_account, false), - AccountMeta::new_readonly(owner_program, false), - AccountMeta::new(undelegate_buffer_pda, false), - AccountMeta::new_readonly(commit_state_pda, false), - AccountMeta::new_readonly(commit_record_pda, false), - AccountMeta::new(delegation_record_pda, false), - AccountMeta::new(delegation_metadata_pda, false), - AccountMeta::new(rent_reimbursement, false), - AccountMeta::new(fees_vault_pda, false), - AccountMeta::new(validator_fees_vault_pda, false), - AccountMeta::new_readonly(system_program::id(), false), - ], + accounts, data: DlpDiscriminator::Undelegate.to_vec(), } } @@ -91,3 +139,31 @@ pub fn undelegate_size_budget(delegated_account: AccountSizeClass) -> u32 { AccountSizeClass::Tiny, // system_program ]) } + +/// +/// Returns accounts-data-size budget for undelegate instruction with request +/// accounts. +/// +/// This value can be used with ComputeBudgetInstruction::SetLoadedAccountsDataSizeLimit +/// +pub fn undelegate_with_request_size_budget( + delegated_account: AccountSizeClass, +) -> u32 { + total_size_budget(&[ + DLP_PROGRAM_DATA_SIZE_CLASS, + AccountSizeClass::Tiny, // validator + delegated_account, // delegated_account + AccountSizeClass::Tiny, // owner_program + delegated_account, // undelegate_buffer_pda + delegated_account, // commit_state_pda + AccountSizeClass::Tiny, // commit_record_pda + AccountSizeClass::Tiny, // delegation_record_pda + AccountSizeClass::Tiny, // delegation_metadata_pda + AccountSizeClass::Tiny, // rent_reimbursement + AccountSizeClass::Tiny, // fees_vault_pda + AccountSizeClass::Tiny, // validator_fees_vault_pda + AccountSizeClass::Tiny, // system_program + AccountSizeClass::Tiny, // undelegation_request_pda + AccountSizeClass::Tiny, // request_rent_payer + ]) +} diff --git a/dlp-api/src/pda.rs b/dlp-api/src/pda.rs index 9bf18858..4f0eb56d 100644 --- a/dlp-api/src/pda.rs +++ b/dlp-api/src/pda.rs @@ -60,6 +60,17 @@ macro_rules! undelegate_buffer_seeds_from_delegated_account { }; } +pub const UNDELEGATION_REQUEST_TAG: &[u8] = b"undelegation-request"; +#[macro_export] +macro_rules! undelegation_request_seeds_from_delegated_account { + ($delegated_account: expr) => { + &[ + $crate::pda::UNDELEGATION_REQUEST_TAG, + &$delegated_account.as_ref(), + ] + }; +} + #[macro_export] macro_rules! fees_vault_seeds { () => { @@ -172,6 +183,16 @@ pub fn undelegate_buffer_pda_from_delegated_account( .0 } +pub fn undelegation_request_pda_from_delegated_account( + delegated_account: &Pubkey, +) -> Pubkey { + Pubkey::find_program_address( + undelegation_request_seeds_from_delegated_account!(delegated_account), + &crate::id(), + ) + .0 +} + pub fn fees_vault_pda() -> Pubkey { Pubkey::find_program_address(fees_vault_seeds!(), &crate::id()).0 } diff --git a/dlp-api/src/requires.rs b/dlp-api/src/requires.rs index 096e27ae..a71d2b86 100644 --- a/dlp-api/src/requires.rs +++ b/dlp-api/src/requires.rs @@ -758,6 +758,16 @@ define_uninitialized_ctx!( immutable = DlpError::CommitRecordImmutable ); +define_uninitialized_ctx!( + UndelegationRequestCtx, + label = "undelegation request", + invalid_seeds = DlpError::UndelegationRequestInvalidSeeds, + invalid_account_owner = DlpError::UndelegationRequestInvalidAccountOwner, + account_already_initialized = + DlpError::UndelegationRequestAlreadyInitialized, + immutable = DlpError::UndelegationRequestImmutable +); + define_uninitialized_ctx!( DelegationRecordCtx, label = "delegation record", diff --git a/dlp-api/src/state/mod.rs b/dlp-api/src/state/mod.rs index 59f89366..de088362 100644 --- a/dlp-api/src/state/mod.rs +++ b/dlp-api/src/state/mod.rs @@ -2,10 +2,12 @@ mod commit_record; mod delegation_metadata; mod delegation_record; mod program_config; +mod undelegation_request; mod utils; pub use commit_record::*; pub use delegation_metadata::*; pub use delegation_record::*; pub use program_config::*; +pub use undelegation_request::*; pub use utils::*; diff --git a/dlp-api/src/state/undelegation_request.rs b/dlp-api/src/state/undelegation_request.rs new file mode 100644 index 00000000..0f349a56 --- /dev/null +++ b/dlp-api/src/state/undelegation_request.rs @@ -0,0 +1,41 @@ +use std::mem::size_of; + +use bytemuck::{Pod, Zeroable}; + +use super::discriminator::{AccountDiscriminator, AccountWithDiscriminator}; +use crate::{ + compat::Pubkey, impl_to_bytes_with_discriminator_zero_copy, + impl_try_from_bytes_with_discriminator_zero_copy, +}; + +/// Request for the validator to undelegate one delegated account. +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] +pub struct UndelegationRequest { + /// The delegated account this request targets. + pub delegated_account: Pubkey, + + /// The original owner program recorded for the delegated account. + pub owner_program: Pubkey, + + /// The account that paid rent for this request PDA. + pub rent_payer: Pubkey, + + /// The slot at which the request was created. + pub created_slot: u64, +} + +impl AccountWithDiscriminator for UndelegationRequest { + fn discriminator() -> AccountDiscriminator { + AccountDiscriminator::UndelegationRequest + } +} + +impl UndelegationRequest { + pub fn size_with_discriminator() -> usize { + 8 + size_of::() + } +} + +impl_to_bytes_with_discriminator_zero_copy!(UndelegationRequest); +impl_try_from_bytes_with_discriminator_zero_copy!(UndelegationRequest); diff --git a/dlp-api/src/state/utils/discriminator.rs b/dlp-api/src/state/utils/discriminator.rs index 617d1c3d..2c040893 100644 --- a/dlp-api/src/state/utils/discriminator.rs +++ b/dlp-api/src/state/utils/discriminator.rs @@ -9,6 +9,7 @@ pub enum AccountDiscriminator { DelegationMetadata = 102, CommitRecord = 101, ProgramConfig = 103, + UndelegationRequest = 104, } impl AccountDiscriminator { diff --git a/src/lib.rs b/src/lib.rs index b3da9f09..2f431bb4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,6 +19,7 @@ pub(crate) use dlp_api::{ ephemeral_balance_seeds_from_payer, fees_vault_seeds, magic_fee_vault_seeds_from_validator, program_config_seeds_from_program_id, undelegate_buffer_seeds_from_delegated_account, + undelegation_request_seeds_from_delegated_account, validator_fees_vault_seeds_from_validator, }; pub use dlp_api::{id, ID}; @@ -148,6 +149,11 @@ pub fn fast_process_instruction( program_id, accounts, data, )) } + DlpDiscriminator::RequestUndelegation => { + Some(processor::fast::process_request_undelegation( + program_id, accounts, data, + )) + } _ => None, } } diff --git a/src/processor/fast/mod.rs b/src/processor/fast/mod.rs index 2e975201..593ede72 100644 --- a/src/processor/fast/mod.rs +++ b/src/processor/fast/mod.rs @@ -7,6 +7,7 @@ mod commit_state_from_buffer; mod delegate; mod delegate_with_actions; mod finalize; +mod request_undelegation; mod undelegate; mod undelegate_confined_account; mod utils; @@ -22,6 +23,7 @@ pub use commit_state_from_buffer::*; pub use delegate::*; pub use delegate_with_actions::*; pub use finalize::*; +pub use request_undelegation::*; pub use undelegate::*; pub use undelegate_confined_account::*; diff --git a/src/processor/fast/request_undelegation.rs b/src/processor/fast/request_undelegation.rs new file mode 100644 index 00000000..29999018 --- /dev/null +++ b/src/processor/fast/request_undelegation.rs @@ -0,0 +1,170 @@ +use pinocchio::{ + address::{address_eq, Address}, + cpi::Signer, + error::ProgramError, + instruction::seeds, + sysvars::{clock::Clock, Sysvar}, + AccountView, ProgramResult, +}; +use pinocchio_log::log; + +use super::to_pinocchio_program_error; +use crate::{ + error::DlpError, + pda, + processor::{fast::utils::pda::create_pda, utils::curve::is_on_curve_fast}, + require_n_accounts, + requires::{ + is_uninitialized_account, require_initialized_delegation_metadata, + require_initialized_delegation_record, require_owned_pda, require_pda, + require_signer, require_uninitialized_pda, UndelegationRequestCtx, + }, + state::{DelegationMetadata, DelegationRecord, UndelegationRequest}, +}; + +/// Request undelegation for one delegated account. +/// +/// Accounts: +/// +/// 0: `[signer, writable]` payer +/// 1: `[signer]` delegated account +/// 2: `[]` owner program of the delegated account +/// 3: `[writable]` undelegation request PDA +/// 4: `[]` delegation record PDA +/// 5: `[]` delegation metadata PDA +/// 6: `[]` system program +pub fn process_request_undelegation( + _program_id: &Address, + accounts: &[AccountView], + _data: &[u8], +) -> ProgramResult { + let [ + payer, // force multi-line + delegated_account, + owner_program, + undelegation_request_account, + delegation_record_account, + delegation_metadata_account, + _system_program, + ] = require_n_accounts!(accounts, 7); + + require_signer(payer, "payer")?; + if !payer.is_writable() { + return Err(ProgramError::Immutable); + } + require_signer(delegated_account, "delegated account")?; + require_owned_pda( + delegated_account, + &crate::fast::ID, + "delegated account", + )?; + + if is_on_curve_fast(delegated_account.address()) { + log!("request undelegation is only supported for off-curve accounts"); + return Err(DlpError::RequestUndelegationOnCurveAccount.into()); + } + + require_initialized_delegation_record( + delegated_account, + delegation_record_account, + false, + )?; + require_initialized_delegation_metadata( + delegated_account, + delegation_metadata_account, + false, + )?; + + let delegation_record_data = delegation_record_account.try_borrow()?; + let delegation_record = + DelegationRecord::try_from_bytes_with_discriminator( + &delegation_record_data, + ) + .map_err(to_pinocchio_program_error)?; + + if !address_eq( + &delegation_record.owner.to_bytes().into(), + owner_program.address(), + ) { + return Err(ProgramError::InvalidAccountOwner); + } + + let delegation_metadata_data = delegation_metadata_account.try_borrow()?; + DelegationMetadata::try_from_bytes_with_discriminator( + &delegation_metadata_data, + ) + .map_err(to_pinocchio_program_error)?; + + drop(delegation_record_data); + drop(delegation_metadata_data); + + let request_seeds = &[ + pda::UNDELEGATION_REQUEST_TAG, + delegated_account.address().as_ref(), + ]; + + if is_uninitialized_account(undelegation_request_account) { + let request_bump = require_uninitialized_pda( + undelegation_request_account, + request_seeds, + &crate::fast::ID, + true, + UndelegationRequestCtx, + )?; + + create_pda( + undelegation_request_account, + &crate::fast::ID, + UndelegationRequest::size_with_discriminator(), + &[Signer::from(&seeds!( + pda::UNDELEGATION_REQUEST_TAG, + delegated_account.address().as_ref(), + &[request_bump] + ))], + payer, + )?; + + let request = UndelegationRequest { + delegated_account: delegated_account.address().to_bytes().into(), + owner_program: owner_program.address().to_bytes().into(), + rent_payer: payer.address().to_bytes().into(), + created_slot: Clock::get()?.slot, + }; + let mut request_data = undelegation_request_account.try_borrow_mut()?; + request + .to_bytes_with_discriminator(&mut request_data) + .map_err(to_pinocchio_program_error)?; + + return Ok(()); + } + + require_pda( + undelegation_request_account, + request_seeds, + &crate::fast::ID, + true, + "undelegation request", + )?; + require_owned_pda( + undelegation_request_account, + &crate::fast::ID, + "undelegation request", + )?; + + let request_data = undelegation_request_account.try_borrow()?; + let request = + UndelegationRequest::try_from_bytes_with_discriminator(&request_data) + .map_err(to_pinocchio_program_error)?; + + if !address_eq( + &request.delegated_account.to_bytes().into(), + delegated_account.address(), + ) || !address_eq( + &request.owner_program.to_bytes().into(), + owner_program.address(), + ) { + return Err(DlpError::InvalidUndelegationRequest.into()); + } + + Ok(()) +} diff --git a/src/processor/fast/undelegate.rs b/src/processor/fast/undelegate.rs index 217d1a87..16b778a2 100644 --- a/src/processor/fast/undelegate.rs +++ b/src/processor/fast/undelegate.rs @@ -21,15 +21,16 @@ use crate::{ error::DlpError, pda, processor::fast::utils::pda::{close_pda, close_pda_with_fees, create_pda}, + require_n_accounts, require_n_accounts_with_optionals, requires::{ require_initialized_delegation_metadata, - require_initialized_delegation_record, + require_initialized_delegation_record, require_initialized_pda, require_initialized_protocol_fees_vault, require_initialized_validator_fees_vault, require_owned_pda, require_signer, require_uninitialized_pda, CommitRecordCtx, CommitStateAccountCtx, UndelegateBufferCtx, }, - state::{DelegationMetadata, DelegationRecord}, + state::{DelegationMetadata, DelegationRecord, UndelegationRequest}, }; /// Undelegate a delegated account @@ -79,10 +80,45 @@ pub fn process_undelegate( accounts: &[AccountView], _data: &[u8], ) -> ProgramResult { - let [validator, delegated_account, owner_program, undelegate_buffer_account, commit_state_account, commit_record_account, delegation_record_account, delegation_metadata_account, rent_reimbursement, fees_vault, validator_fees_vault, system_program] = - accounts - else { - return Err(ProgramError::NotEnoughAccountKeys); + let ( + [ + validator, // force multi-line + delegated_account, + owner_program, + undelegate_buffer_account, + commit_state_account, + commit_record_account, + delegation_record_account, + delegation_metadata_account, + rent_reimbursement, + fees_vault, + validator_fees_vault, + system_program, + ], + optional_accounts, + ) = require_n_accounts_with_optionals!(accounts, 12); + + let request_accounts = match optional_accounts.len() { + 0 => None, + 2 => { + let [ + undelegation_request_account, // force multi-line + request_rent_payer, + ] = require_n_accounts!(optional_accounts, 2); + Some((undelegation_request_account, request_rent_payer)) + } + _ => return Err(ProgramError::InvalidInstructionData), + }; + + if let Some((undelegation_request_account, request_rent_payer)) = + request_accounts + { + require_valid_undelegation_request( + delegated_account, + owner_program, + undelegation_request_account, + request_rent_payer, + )?; }; // Check accounts @@ -195,6 +231,11 @@ pub fn process_undelegate( validator_fees_vault, delegation_last_update_nonce, )?; + if let Some((undelegation_request_account, request_rent_payer)) = + request_accounts + { + close_pda(undelegation_request_account, request_rent_payer)?; + } return Ok(()); } @@ -253,6 +294,53 @@ pub fn process_undelegate( validator_fees_vault, delegation_last_update_nonce, )?; + if let Some((undelegation_request_account, request_rent_payer)) = + request_accounts + { + close_pda(undelegation_request_account, request_rent_payer)?; + } + Ok(()) +} + +fn require_valid_undelegation_request( + delegated_account: &AccountView, + owner_program: &AccountView, + undelegation_request_account: &AccountView, + request_rent_payer: &AccountView, +) -> ProgramResult { + require_initialized_pda( + undelegation_request_account, + &[ + pda::UNDELEGATION_REQUEST_TAG, + delegated_account.address().as_ref(), + ], + &crate::fast::ID, + true, + "undelegation request", + )?; + + if !request_rent_payer.is_writable() { + return Err(ProgramError::Immutable); + } + + let request_data = undelegation_request_account.try_borrow()?; + let request = + UndelegationRequest::try_from_bytes_with_discriminator(&request_data) + .map_err(to_pinocchio_program_error)?; + + if !address_eq( + &request.delegated_account.to_bytes().into(), + delegated_account.address(), + ) || !address_eq( + &request.owner_program.to_bytes().into(), + owner_program.address(), + ) || !address_eq( + &request.rent_payer.to_bytes().into(), + request_rent_payer.address(), + ) { + return Err(DlpError::InvalidUndelegationRequest.into()); + } + Ok(()) } diff --git a/tests/fixtures/accounts.rs b/tests/fixtures/accounts.rs index 40f06058..e959bef1 100644 --- a/tests/fixtures/accounts.rs +++ b/tests/fixtures/accounts.rs @@ -1,6 +1,7 @@ use dlp::solana_program; use dlp_api::state::{ CommitRecord, DelegationMetadata, DelegationRecord, ProgramConfig, + UndelegationRequest, }; use solana_program::{ native_token::LAMPORTS_PER_SOL, pubkey::Pubkey, rent::Rent, @@ -198,3 +199,21 @@ pub fn create_program_config_data(approved_validator: Pubkey) -> Vec { .unwrap(); bytes } + +#[allow(dead_code)] +pub fn create_undelegation_request_data( + delegated_account: Pubkey, + owner_program: Pubkey, + rent_payer: Pubkey, + created_slot: u64, +) -> Vec { + let request = UndelegationRequest { + delegated_account, + owner_program, + rent_payer, + created_slot, + }; + let mut bytes = vec![0u8; UndelegationRequest::size_with_discriminator()]; + request.to_bytes_with_discriminator(&mut bytes).unwrap(); + bytes +} diff --git a/tests/test_request_undelegation.rs b/tests/test_request_undelegation.rs new file mode 100644 index 00000000..4a88014e --- /dev/null +++ b/tests/test_request_undelegation.rs @@ -0,0 +1,555 @@ +use dlp::solana_program; +use dlp_api::{ + pda::{ + commit_record_pda_from_delegated_account, + commit_state_pda_from_delegated_account, + delegation_metadata_pda_from_delegated_account, + delegation_record_pda_from_delegated_account, fees_vault_pda, + undelegation_request_pda_from_delegated_account, + validator_fees_vault_pda_from_validator, + }, + state::UndelegationRequest, +}; +use solana_program::{ + account_info::AccountInfo, + entrypoint::ProgramResult, + hash::Hash, + instruction::{AccountMeta, Instruction}, + native_token::LAMPORTS_PER_SOL, + program::invoke_signed, + program_error::ProgramError, + pubkey::Pubkey, + rent::Rent, +}; +use solana_program_test::{processor, read_file, BanksClient, ProgramTest}; +use solana_sdk::{ + account::Account, + signature::{Keypair, Signer}, + transaction::Transaction, +}; +use solana_sdk_ids::system_program; + +use crate::fixtures::{ + create_undelegation_request_data, get_commit_record_account_data, + get_delegation_metadata_data, get_delegation_metadata_data_on_curve, + get_delegation_record_data, get_delegation_record_on_curve_data, + keypair_from_bytes, COMMIT_NEW_STATE_ACCOUNT_DATA, DELEGATED_PDA_ID, + DELEGATED_PDA_OWNER_ID, ON_CURVE_KEYPAIR, TEST_AUTHORITY, +}; + +mod fixtures; + +const TEST_PDA_SEED: &[u8] = b"test-pda"; + +#[tokio::test] +async fn test_request_undelegation_creates_request() { + let (banks, payer, _, blockhash) = setup_request_env().await; + + let ix = request_undelegation_from_owner_program(payer.pubkey()); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer], + blockhash, + ); + + let res = banks.process_transaction(tx).await; + println!("{:?}", res); + assert!(res.is_ok()); + + let request_pda = + undelegation_request_pda_from_delegated_account(&DELEGATED_PDA_ID); + let request_account = + banks.get_account(request_pda).await.unwrap().unwrap(); + assert_eq!(request_account.owner, dlp_api::id()); + + let request = UndelegationRequest::try_from_bytes_with_discriminator( + &request_account.data, + ) + .unwrap(); + assert_eq!(request.delegated_account, DELEGATED_PDA_ID); + assert_eq!(request.owner_program, DELEGATED_PDA_OWNER_ID); + assert_eq!(request.rent_payer, payer.pubkey()); +} + +#[tokio::test] +async fn test_request_undelegation_is_idempotent() { + let (banks, payer, second_payer, blockhash) = setup_request_env().await; + + let first_ix = request_undelegation_from_owner_program(payer.pubkey()); + let first_tx = Transaction::new_signed_with_payer( + &[first_ix], + Some(&payer.pubkey()), + &[&payer], + blockhash, + ); + let first_res = banks.process_transaction(first_tx).await; + assert!(first_res.is_ok()); + + let request_pda = + undelegation_request_pda_from_delegated_account(&DELEGATED_PDA_ID); + let request_before = banks.get_account(request_pda).await.unwrap().unwrap(); + + let second_ix = + request_undelegation_from_owner_program(second_payer.pubkey()); + let second_tx = Transaction::new_signed_with_payer( + &[second_ix], + Some(&second_payer.pubkey()), + &[&second_payer], + blockhash, + ); + let second_res = banks.process_transaction(second_tx).await; + println!("{:?}", second_res); + assert!(second_res.is_ok()); + + let request_after = banks.get_account(request_pda).await.unwrap().unwrap(); + assert_eq!(request_after.data, request_before.data); +} + +#[tokio::test] +async fn test_request_undelegation_rejects_missing_delegated_signer() { + let (banks, payer, _, blockhash) = setup_request_env().await; + + let request_pda = + undelegation_request_pda_from_delegated_account(&DELEGATED_PDA_ID); + let ix = Instruction { + program_id: dlp_api::id(), + accounts: vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new(DELEGATED_PDA_ID, false), + AccountMeta::new_readonly(DELEGATED_PDA_OWNER_ID, false), + AccountMeta::new(request_pda, false), + AccountMeta::new_readonly( + delegation_record_pda_from_delegated_account(&DELEGATED_PDA_ID), + false, + ), + AccountMeta::new_readonly( + delegation_metadata_pda_from_delegated_account( + &DELEGATED_PDA_ID, + ), + false, + ), + AccountMeta::new_readonly(system_program::id(), false), + ], + data: dlp_api::discriminator::DlpDiscriminator::RequestUndelegation + .to_vec(), + }; + + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_err()); +} + +#[tokio::test] +async fn test_request_undelegation_rejects_on_curve_delegated_account() { + let (banks, payer, delegated_on_curve, blockhash) = + setup_on_curve_request_env().await; + + let ix = dlp_api::instruction_builder::request_undelegation( + payer.pubkey(), + delegated_on_curve.pubkey(), + system_program::id(), + ); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer, &delegated_on_curve], + blockhash, + ); + + let res = banks.process_transaction(tx).await; + assert!(res.is_err()); +} + +#[tokio::test] +async fn test_undelegate_with_request_closes_request() { + let (banks, _, authority, request_rent_payer, blockhash) = + setup_undelegate_with_request_env().await; + + let request_pda = + undelegation_request_pda_from_delegated_account(&DELEGATED_PDA_ID); + let request_lamports_before = banks + .get_account(request_pda) + .await + .unwrap() + .unwrap() + .lamports; + let payer_lamports_before = banks + .get_account(request_rent_payer.pubkey()) + .await + .unwrap() + .unwrap() + .lamports; + + let ix_finalize = dlp_api::instruction_builder::finalize( + authority.pubkey(), + DELEGATED_PDA_ID, + ); + let ix_undelegate = dlp_api::instruction_builder::undelegate_with_request( + authority.pubkey(), + DELEGATED_PDA_ID, + DELEGATED_PDA_OWNER_ID, + authority.pubkey(), + request_rent_payer.pubkey(), + ); + + let tx = Transaction::new_signed_with_payer( + &[ix_finalize, ix_undelegate], + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); + let res = banks.process_transaction(tx).await; + println!("{:?}", res); + assert!(res.is_ok()); + + let request_account = banks.get_account(request_pda).await.unwrap(); + assert!(request_account.is_none()); + + let payer_lamports_after = banks + .get_account(request_rent_payer.pubkey()) + .await + .unwrap() + .unwrap() + .lamports; + assert_eq!( + payer_lamports_after, + payer_lamports_before + request_lamports_before + ); +} + +fn process_request_wrapper( + program_id: &Pubkey, + accounts: &[AccountInfo], + _data: &[u8], +) -> ProgramResult { + let [payer, delegated_account, owner_program, request_account, delegation_record_account, delegation_metadata_account, system_program_account, dlp_program] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + if owner_program.key != program_id { + return Err(ProgramError::IncorrectProgramId); + } + + let ix = dlp_api::instruction_builder::request_undelegation( + *payer.key, + *delegated_account.key, + *program_id, + ); + let (_, bump) = Pubkey::find_program_address(&[TEST_PDA_SEED], program_id); + let bump_seed = [bump]; + invoke_signed( + &ix, + &[ + payer.clone(), + delegated_account.clone(), + owner_program.clone(), + request_account.clone(), + delegation_record_account.clone(), + delegation_metadata_account.clone(), + system_program_account.clone(), + dlp_program.clone(), + ], + &[&[TEST_PDA_SEED, &bump_seed]], + ) +} + +fn request_undelegation_from_owner_program(payer: Pubkey) -> Instruction { + Instruction { + program_id: DELEGATED_PDA_OWNER_ID, + accounts: vec![ + AccountMeta::new(payer, true), + AccountMeta::new(DELEGATED_PDA_ID, false), + AccountMeta::new_readonly(DELEGATED_PDA_OWNER_ID, false), + AccountMeta::new( + undelegation_request_pda_from_delegated_account( + &DELEGATED_PDA_ID, + ), + false, + ), + AccountMeta::new_readonly( + delegation_record_pda_from_delegated_account(&DELEGATED_PDA_ID), + false, + ), + AccountMeta::new_readonly( + delegation_metadata_pda_from_delegated_account( + &DELEGATED_PDA_ID, + ), + false, + ), + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new_readonly(dlp_api::id(), false), + ], + data: vec![], + } +} + +async fn setup_request_env() -> (BanksClient, Keypair, Keypair, Hash) { + let mut program_test = ProgramTest::default(); + program_test.prefer_bpf(true); + program_test.add_program("dlp", dlp_api::ID, None); + program_test.prefer_bpf(false); + program_test.add_program( + "request-wrapper", + DELEGATED_PDA_OWNER_ID, + processor!(process_request_wrapper), + ); + program_test.prefer_bpf(true); + + let payer = Keypair::new(); + let second_payer = Keypair::new(); + let authority = keypair_from_bytes(&TEST_AUTHORITY); + + add_system_account(&mut program_test, payer.pubkey(), LAMPORTS_PER_SOL); + add_system_account( + &mut program_test, + second_payer.pubkey(), + LAMPORTS_PER_SOL, + ); + add_system_account(&mut program_test, authority.pubkey(), LAMPORTS_PER_SOL); + + add_delegated_account(&mut program_test, DELEGATED_PDA_ID); + add_delegation_accounts(&mut program_test, authority.pubkey()); + + let (banks, _, blockhash) = program_test.start().await; + (banks, payer, second_payer, blockhash) +} + +async fn setup_on_curve_request_env() -> (BanksClient, Keypair, Keypair, Hash) { + let mut program_test = ProgramTest::new("dlp", dlp_api::ID, None); + program_test.prefer_bpf(true); + let payer = Keypair::new(); + let delegated_on_curve = keypair_from_bytes(&ON_CURVE_KEYPAIR); + + add_system_account(&mut program_test, payer.pubkey(), LAMPORTS_PER_SOL); + program_test.add_account( + delegated_on_curve.pubkey(), + Account { + lamports: LAMPORTS_PER_SOL, + data: vec![], + owner: dlp_api::id(), + executable: false, + rent_epoch: 0, + }, + ); + + let delegation_record_data = get_delegation_record_on_curve_data( + delegated_on_curve.pubkey(), + Some(LAMPORTS_PER_SOL), + ); + program_test.add_account( + delegation_record_pda_from_delegated_account( + &delegated_on_curve.pubkey(), + ), + Account { + lamports: Rent::default() + .minimum_balance(delegation_record_data.len()), + data: delegation_record_data, + owner: dlp_api::id(), + executable: false, + rent_epoch: 0, + }, + ); + let delegation_metadata_data = + get_delegation_metadata_data_on_curve(payer.pubkey(), Some(false)); + program_test.add_account( + delegation_metadata_pda_from_delegated_account( + &delegated_on_curve.pubkey(), + ), + Account { + lamports: Rent::default() + .minimum_balance(delegation_metadata_data.len()), + data: delegation_metadata_data, + owner: dlp_api::id(), + executable: false, + rent_epoch: 0, + }, + ); + + let (banks, _, blockhash) = program_test.start().await; + (banks, payer, delegated_on_curve, blockhash) +} + +async fn setup_undelegate_with_request_env( +) -> (BanksClient, Keypair, Keypair, Keypair, Hash) { + let mut program_test = ProgramTest::new("dlp", dlp_api::ID, None); + program_test.prefer_bpf(true); + let authority = keypair_from_bytes(&TEST_AUTHORITY); + let request_rent_payer = Keypair::new(); + + add_system_account(&mut program_test, authority.pubkey(), LAMPORTS_PER_SOL); + add_system_account( + &mut program_test, + request_rent_payer.pubkey(), + LAMPORTS_PER_SOL, + ); + + add_delegated_account(&mut program_test, DELEGATED_PDA_ID); + add_delegation_accounts_with_metadata( + &mut program_test, + authority.pubkey(), + Some(true), + ); + add_commit_accounts(&mut program_test, authority.pubkey()); + add_owner_program(&mut program_test); + add_fee_accounts(&mut program_test, authority.pubkey()); + add_request_account(&mut program_test, request_rent_payer.pubkey()); + + let (banks, payer, blockhash) = program_test.start().await; + (banks, payer, authority, request_rent_payer, blockhash) +} + +fn add_system_account( + program_test: &mut ProgramTest, + pubkey: Pubkey, + lamports: u64, +) { + program_test.add_account( + pubkey, + Account { + lamports, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); +} + +fn add_delegated_account(program_test: &mut ProgramTest, pubkey: Pubkey) { + program_test.add_account( + pubkey, + Account { + lamports: LAMPORTS_PER_SOL, + data: vec![], + owner: dlp_api::id(), + executable: false, + rent_epoch: 0, + }, + ); +} + +fn add_delegation_accounts(program_test: &mut ProgramTest, authority: Pubkey) { + add_delegation_accounts_with_metadata(program_test, authority, Some(false)); +} + +fn add_delegation_accounts_with_metadata( + program_test: &mut ProgramTest, + authority: Pubkey, + is_undelegatable: Option, +) { + let delegation_record_data = get_delegation_record_data(authority, None); + program_test.add_account( + delegation_record_pda_from_delegated_account(&DELEGATED_PDA_ID), + Account { + lamports: Rent::default() + .minimum_balance(delegation_record_data.len()), + data: delegation_record_data, + owner: dlp_api::id(), + executable: false, + rent_epoch: 0, + }, + ); + + let delegation_metadata_data = + get_delegation_metadata_data(authority, is_undelegatable); + program_test.add_account( + delegation_metadata_pda_from_delegated_account(&DELEGATED_PDA_ID), + Account { + lamports: Rent::default() + .minimum_balance(delegation_metadata_data.len()), + data: delegation_metadata_data, + owner: dlp_api::id(), + executable: false, + rent_epoch: 0, + }, + ); +} + +fn add_commit_accounts(program_test: &mut ProgramTest, authority: Pubkey) { + program_test.add_account( + commit_state_pda_from_delegated_account(&DELEGATED_PDA_ID), + Account { + lamports: LAMPORTS_PER_SOL, + data: COMMIT_NEW_STATE_ACCOUNT_DATA.into(), + owner: dlp_api::id(), + executable: false, + rent_epoch: 0, + }, + ); + + let commit_record_data = get_commit_record_account_data(authority); + program_test.add_account( + commit_record_pda_from_delegated_account(&DELEGATED_PDA_ID), + Account { + lamports: Rent::default().minimum_balance(commit_record_data.len()), + data: commit_record_data, + owner: dlp_api::id(), + executable: false, + rent_epoch: 0, + }, + ); +} + +fn add_owner_program(program_test: &mut ProgramTest) { + let data = read_file("tests/buffers/test_delegation.so"); + program_test.add_account( + DELEGATED_PDA_OWNER_ID, + Account { + lamports: Rent::default().minimum_balance(data.len()), + data, + owner: solana_sdk::bpf_loader::id(), + executable: true, + rent_epoch: 0, + }, + ); +} + +fn add_fee_accounts(program_test: &mut ProgramTest, authority: Pubkey) { + program_test.add_account( + fees_vault_pda(), + Account { + lamports: Rent::default().minimum_balance(0), + data: vec![], + owner: dlp_api::id(), + executable: false, + rent_epoch: 0, + }, + ); + program_test.add_account( + validator_fees_vault_pda_from_validator(&authority), + Account { + lamports: LAMPORTS_PER_SOL, + data: vec![], + owner: dlp_api::id(), + executable: false, + rent_epoch: 0, + }, + ); +} + +fn add_request_account(program_test: &mut ProgramTest, rent_payer: Pubkey) { + let request_data = create_undelegation_request_data( + DELEGATED_PDA_ID, + DELEGATED_PDA_OWNER_ID, + rent_payer, + 1, + ); + program_test.add_account( + undelegation_request_pda_from_delegated_account(&DELEGATED_PDA_ID), + Account { + lamports: Rent::default().minimum_balance(request_data.len()), + data: request_data, + owner: dlp_api::id(), + executable: false, + rent_epoch: 0, + }, + ); +} From 4d45bec9951293de02deb55e8b6cfb40f4cbbee0 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Mon, 22 Jun 2026 18:47:26 +0530 Subject: [PATCH 02/30] add CarryOverRequestedUndelegation --- dlp-api/src/consts.rs | 3 + dlp-api/src/discriminator.rs | 3 + dlp-api/src/error.rs | 9 + .../carry_over_requested_undelegation.rs | 100 +++++ dlp-api/src/instruction_builder/mod.rs | 2 + .../request_undelegation.rs | 31 +- dlp-api/src/state/undelegation_request.rs | 12 + src/lib.rs | 5 + .../fast/carry_over_requested_undelegation.rs | 323 ++++++++++++++ src/processor/fast/mod.rs | 2 + src/processor/fast/request_undelegation.rs | 41 +- tests/fixtures/accounts.rs | 37 +- .../test_carry_over_requested_undelegation.rs | 415 ++++++++++++++++++ tests/test_request_undelegation.rs | 100 ++++- 14 files changed, 1066 insertions(+), 17 deletions(-) create mode 100644 dlp-api/src/instruction_builder/carry_over_requested_undelegation.rs create mode 100644 src/processor/fast/carry_over_requested_undelegation.rs create mode 100644 tests/test_carry_over_requested_undelegation.rs diff --git a/dlp-api/src/consts.rs b/dlp-api/src/consts.rs index 4da961fb..a8914e0b 100644 --- a/dlp-api/src/consts.rs +++ b/dlp-api/src/consts.rs @@ -14,6 +14,9 @@ pub const COMMIT_FEE_LAMPORTS: u64 = 100_000; /// Fixed fee per delegation session (0.0003 SOL). pub const SESSION_FEE_LAMPORTS: u64 = 300_000; +/// Default and minimum timeout for requested undelegation. +pub const DEFAULT_UNDELEGATION_REQUEST_TIMEOUT_SLOTS: u64 = 300; + /// The discriminator for the external undelegate instruction. pub const EXTERNAL_UNDELEGATE_DISCRIMINATOR: [u8; 8] = [196, 28, 41, 206, 48, 37, 51, 167]; diff --git a/dlp-api/src/discriminator.rs b/dlp-api/src/discriminator.rs index 6ee64dbf..3445a2fe 100644 --- a/dlp-api/src/discriminator.rs +++ b/dlp-api/src/discriminator.rs @@ -65,6 +65,9 @@ pub enum DlpDiscriminator { /// See [crate::processor::process_request_undelegation] for docs. RequestUndelegation = 26, + + /// See [crate::processor::process_carry_over_requested_undelegation] for docs. + CarryOverRequestedUndelegation = 27, } impl DlpDiscriminator { diff --git a/dlp-api/src/error.rs b/dlp-api/src/error.rs index e1bf57b3..cf383f8e 100644 --- a/dlp-api/src/error.rs +++ b/dlp-api/src/error.rs @@ -173,6 +173,15 @@ pub enum DlpError { #[error("Request undelegation is only supported for off-curve delegated accounts")] RequestUndelegationOnCurveAccount = 49, + #[error("Undelegation request timeout is below the minimum")] + UndelegationRequestTimeoutTooShort = 50, + + #[error("Undelegation request has not expired")] + UndelegationRequestNotExpired = 51, + + #[error("Invalid pending commit state for timeout carry-over")] + InvalidPendingCommitState = 52, + #[error("An infallible error is encountered possibly due to logic error")] InfallibleError = 100, } diff --git a/dlp-api/src/instruction_builder/carry_over_requested_undelegation.rs b/dlp-api/src/instruction_builder/carry_over_requested_undelegation.rs new file mode 100644 index 00000000..c4588bb6 --- /dev/null +++ b/dlp-api/src/instruction_builder/carry_over_requested_undelegation.rs @@ -0,0 +1,100 @@ +use dlp::{ + discriminator::DlpDiscriminator, + pda::{ + commit_record_pda_from_delegated_account, + commit_state_pda_from_delegated_account, + delegation_metadata_pda_from_delegated_account, + delegation_record_pda_from_delegated_account, + undelegate_buffer_pda_from_delegated_account, + undelegation_request_pda_from_delegated_account, + }, + total_size_budget, AccountSizeClass, DLP_PROGRAM_DATA_SIZE_CLASS, +}; +use solana_program::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, +}; +use solana_sdk_ids::system_program; + +use crate::compat::{Compatize, Modernize}; + +/// Builds a timeout carry-over instruction for a requested undelegation. +/// See [dlp::processor::process_carry_over_requested_undelegation] for docs. +#[allow(clippy::too_many_arguments)] +pub fn carry_over_requested_undelegation( + caller: Pubkey, + delegated_account: Pubkey, + owner_program: Pubkey, + request_rent_payer: Pubkey, + delegation_rent_payer: Pubkey, + commit_reimbursement: Pubkey, +) -> Instruction { + let delegated_account_compat = delegated_account.compatize(); + let undelegate_buffer_pda = + undelegate_buffer_pda_from_delegated_account(&delegated_account_compat) + .modernize(); + let request_pda = undelegation_request_pda_from_delegated_account( + &delegated_account_compat, + ) + .modernize(); + let delegation_record_pda = + delegation_record_pda_from_delegated_account(&delegated_account_compat) + .modernize(); + let delegation_metadata_pda = + delegation_metadata_pda_from_delegated_account( + &delegated_account_compat, + ) + .modernize(); + let commit_state_pda = + commit_state_pda_from_delegated_account(&delegated_account_compat) + .modernize(); + let commit_record_pda = + commit_record_pda_from_delegated_account(&delegated_account_compat) + .modernize(); + + Instruction { + program_id: dlp::id().modernize(), + accounts: vec![ + AccountMeta::new(caller, true), + AccountMeta::new(delegated_account, false), + AccountMeta::new_readonly(owner_program, false), + AccountMeta::new(undelegate_buffer_pda, false), + AccountMeta::new(request_pda, false), + AccountMeta::new(delegation_record_pda, false), + AccountMeta::new(delegation_metadata_pda, false), + AccountMeta::new(request_rent_payer, false), + AccountMeta::new(delegation_rent_payer, false), + AccountMeta::new(commit_state_pda, false), + AccountMeta::new(commit_record_pda, false), + AccountMeta::new(commit_reimbursement, false), + AccountMeta::new_readonly(system_program::id(), false), + ], + data: DlpDiscriminator::CarryOverRequestedUndelegation.to_vec(), + } +} + +/// +/// Returns accounts-data-size budget for requested undelegation carry-over. +/// +/// This value can be used with ComputeBudgetInstruction::SetLoadedAccountsDataSizeLimit +/// +pub fn carry_over_requested_undelegation_size_budget( + delegated_account: AccountSizeClass, +) -> u32 { + total_size_budget(&[ + DLP_PROGRAM_DATA_SIZE_CLASS, + AccountSizeClass::Tiny, // caller + delegated_account, // delegated_account + AccountSizeClass::Tiny, // owner_program + delegated_account, // undelegate_buffer_pda + AccountSizeClass::Tiny, // undelegation_request_pda + AccountSizeClass::Tiny, // delegation_record_pda + AccountSizeClass::Tiny, // delegation_metadata_pda + AccountSizeClass::Tiny, // request_rent_payer + AccountSizeClass::Tiny, // delegation_rent_payer + delegated_account, // commit_state_pda + AccountSizeClass::Tiny, // commit_record_pda + AccountSizeClass::Tiny, // commit_reimbursement + AccountSizeClass::Tiny, // system_program + ]) +} diff --git a/dlp-api/src/instruction_builder/mod.rs b/dlp-api/src/instruction_builder/mod.rs index 64cf1a0e..a441d2c5 100644 --- a/dlp-api/src/instruction_builder/mod.rs +++ b/dlp-api/src/instruction_builder/mod.rs @@ -1,5 +1,6 @@ mod call_handler; mod call_handler_v2; +mod carry_over_requested_undelegation; mod close_ephemeral_balance; mod close_validator_fees_vault; mod commit_diff; @@ -28,6 +29,7 @@ mod whitelist_validator_for_program; pub use call_handler::*; pub use call_handler_v2::*; +pub use carry_over_requested_undelegation::*; pub use close_ephemeral_balance::*; pub use close_validator_fees_vault::*; pub use commit_diff::*; diff --git a/dlp-api/src/instruction_builder/request_undelegation.rs b/dlp-api/src/instruction_builder/request_undelegation.rs index 3019f92d..1a32f088 100644 --- a/dlp-api/src/instruction_builder/request_undelegation.rs +++ b/dlp-api/src/instruction_builder/request_undelegation.rs @@ -21,6 +21,31 @@ pub fn request_undelegation( payer: Pubkey, delegated_account: Pubkey, owner_program: Pubkey, +) -> Instruction { + build_request_undelegation(payer, delegated_account, owner_program, None) +} + +/// Builds a request undelegation instruction with a custom timeout. +/// See [dlp::processor::process_request_undelegation] for docs. +pub fn request_undelegation_with_timeout( + payer: Pubkey, + delegated_account: Pubkey, + owner_program: Pubkey, + timeout_slots: u64, +) -> Instruction { + build_request_undelegation( + payer, + delegated_account, + owner_program, + Some(timeout_slots), + ) +} + +fn build_request_undelegation( + payer: Pubkey, + delegated_account: Pubkey, + owner_program: Pubkey, + timeout_slots: Option, ) -> Instruction { let delegated_account_compat = delegated_account.compatize(); let undelegation_request_pda = @@ -36,6 +61,10 @@ pub fn request_undelegation( &delegated_account_compat, ) .modernize(); + let mut data = DlpDiscriminator::RequestUndelegation.to_vec(); + if let Some(timeout_slots) = timeout_slots { + data.extend_from_slice(&timeout_slots.to_le_bytes()); + } Instruction { program_id: dlp::id().modernize(), @@ -48,7 +77,7 @@ pub fn request_undelegation( AccountMeta::new_readonly(delegation_metadata_pda, false), AccountMeta::new_readonly(system_program::id(), false), ], - data: DlpDiscriminator::RequestUndelegation.to_vec(), + data, } } diff --git a/dlp-api/src/state/undelegation_request.rs b/dlp-api/src/state/undelegation_request.rs index 0f349a56..13433797 100644 --- a/dlp-api/src/state/undelegation_request.rs +++ b/dlp-api/src/state/undelegation_request.rs @@ -23,6 +23,18 @@ pub struct UndelegationRequest { /// The slot at which the request was created. pub created_slot: u64, + + /// The first slot at which timeout carry-over is allowed. + pub expires_at_slot: u64, + + /// Delegation metadata nonce observed when the request was created. + pub delegation_nonce_at_request: u64, + + /// PDA bump for this request. + pub bump: u8, + + /// Explicit padding keeps this type Pod-safe with repr(C). + pub _padding: [u8; 7], } impl AccountWithDiscriminator for UndelegationRequest { diff --git a/src/lib.rs b/src/lib.rs index 2f431bb4..d8835726 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -154,6 +154,11 @@ pub fn fast_process_instruction( program_id, accounts, data, )) } + DlpDiscriminator::CarryOverRequestedUndelegation => { + Some(processor::fast::process_carry_over_requested_undelegation( + program_id, accounts, data, + )) + } _ => None, } } diff --git a/src/processor/fast/carry_over_requested_undelegation.rs b/src/processor/fast/carry_over_requested_undelegation.rs new file mode 100644 index 00000000..6767b0ac --- /dev/null +++ b/src/processor/fast/carry_over_requested_undelegation.rs @@ -0,0 +1,323 @@ +use pinocchio::{ + address::{address_eq, Address}, + cpi::Signer, + error::ProgramError, + instruction::seeds, + sysvars::{clock::Clock, Sysvar}, + AccountView, ProgramResult, +}; + +use super::{process_undelegation_with_cpi, to_pinocchio_program_error}; +use crate::{ + error::DlpError, + pda, + processor::fast::utils::pda::{close_pda, create_pda}, + require_n_accounts, + requires::{ + is_uninitialized_account, require_initialized_commit_record, + require_initialized_commit_state, + require_initialized_delegation_metadata, + require_initialized_delegation_record, require_initialized_pda, + require_owned_pda, require_signer, require_uninitialized_pda, + UndelegateBufferCtx, + }, + state::{ + CommitRecord, DelegationMetadata, DelegationRecord, UndelegationRequest, + }, +}; + +/// Permissionless timeout path for a requested undelegation. +/// +/// This intentionally returns the currently available base-chain delegated +/// account state. It does not accept or apply any pending validator commit. +/// +/// Data-loss warning: +/// this is a rollback/escape hatch for validator non-response. If the +/// ephemeral validator has newer state that was never finalized on the base +/// chain, that state is intentionally not used here and can be lost from the +/// returned account's perspective. The safety property is that this path never +/// trusts fresh validator data after timeout; the tradeoff is that Program A +/// gets back only the last base-chain state available in the delegated account. +/// +/// Accounts: +/// +/// 0: `[signer, writable]` caller +/// 1: `[writable]` delegated account +/// 2: `[]` owner program of the delegated account +/// 3: `[writable]` undelegate buffer PDA +/// 4: `[writable]` undelegation request PDA +/// 5: `[writable]` delegation record PDA +/// 6: `[writable]` delegation metadata PDA +/// 7: `[writable]` request rent payer +/// 8: `[writable]` delegation rent payer +/// 9: `[writable]` commit state PDA +/// 10: `[writable]` commit record PDA +/// 11: `[writable]` commit reimbursement account +/// 12: `[]` system program +pub fn process_carry_over_requested_undelegation( + _program_id: &Address, + accounts: &[AccountView], + _data: &[u8], +) -> ProgramResult { + let [caller, delegated_account, owner_program, undelegate_buffer_account, undelegation_request_account, delegation_record_account, delegation_metadata_account, request_rent_payer, delegation_rent_payer, commit_state_account, commit_record_account, commit_reimbursement, system_program] = + require_n_accounts!(accounts, 13); + + require_signer(caller, "caller")?; + if !caller.is_writable() { + return Err(ProgramError::Immutable); + } + require_owned_pda( + delegated_account, + &crate::fast::ID, + "delegated account", + )?; + + let request = load_valid_request( + delegated_account, + owner_program, + undelegation_request_account, + request_rent_payer, + )?; + if Clock::get()?.slot < request.expires_at_slot { + return Err(DlpError::UndelegationRequestNotExpired.into()); + } + + require_initialized_delegation_record( + delegated_account, + delegation_record_account, + true, + )?; + require_initialized_delegation_metadata( + delegated_account, + delegation_metadata_account, + true, + )?; + + let (delegation_owner, delegation_metadata) = { + let delegation_record_data = delegation_record_account.try_borrow()?; + let delegation_record = + DelegationRecord::try_from_bytes_with_discriminator( + &delegation_record_data, + ) + .map_err(to_pinocchio_program_error)?; + + let delegation_metadata_data = + delegation_metadata_account.try_borrow()?; + let delegation_metadata = + DelegationMetadata::try_from_bytes_with_discriminator( + &delegation_metadata_data, + ) + .map_err(to_pinocchio_program_error)?; + + (delegation_record.owner, delegation_metadata) + }; + + if !address_eq(&delegation_owner.to_bytes().into(), owner_program.address()) + { + return Err(ProgramError::InvalidAccountOwner); + } + if !address_eq( + &delegation_metadata.rent_payer.to_bytes().into(), + delegation_rent_payer.address(), + ) { + return Err( + DlpError::InvalidReimbursementAddressForDelegationRent.into() + ); + } + if !delegation_rent_payer.is_writable() { + return Err(ProgramError::Immutable); + } + + if delegated_account.is_data_empty() { + unsafe { + delegated_account.assign(owner_program.address()); + } + } else { + carry_over_with_undelegation_cpi( + caller, + delegated_account, + owner_program, + undelegate_buffer_account, + delegation_metadata, + system_program, + )?; + } + + // If a validator started a commit but did not finish finalizing it before + // timeout, the commit PDAs are cleanup-only inputs. Do not move their data + // into the delegated account. That would turn this rollback path into a + // late validator-state acceptance path. + cleanup_pending_commit( + delegated_account, + commit_state_account, + commit_record_account, + commit_reimbursement, + )?; + + close_pda(undelegation_request_account, request_rent_payer)?; + close_pda(delegation_record_account, delegation_rent_payer)?; + close_pda(delegation_metadata_account, delegation_rent_payer)?; + + Ok(()) +} + +fn load_valid_request( + delegated_account: &AccountView, + owner_program: &AccountView, + undelegation_request_account: &AccountView, + request_rent_payer: &AccountView, +) -> Result { + let request_bump = require_initialized_pda( + undelegation_request_account, + &[ + pda::UNDELEGATION_REQUEST_TAG, + delegated_account.address().as_ref(), + ], + &crate::fast::ID, + true, + "undelegation request", + )?; + + if !request_rent_payer.is_writable() { + return Err(ProgramError::Immutable); + } + + let request_data = undelegation_request_account.try_borrow()?; + let request = + *UndelegationRequest::try_from_bytes_with_discriminator(&request_data) + .map_err(to_pinocchio_program_error)?; + + if !address_eq( + &request.delegated_account.to_bytes().into(), + delegated_account.address(), + ) || !address_eq( + &request.owner_program.to_bytes().into(), + owner_program.address(), + ) || !address_eq( + &request.rent_payer.to_bytes().into(), + request_rent_payer.address(), + ) || request.bump != request_bump + { + return Err(DlpError::InvalidUndelegationRequest.into()); + } + + Ok(request) +} + +fn carry_over_with_undelegation_cpi( + caller: &AccountView, + delegated_account: &AccountView, + owner_program: &AccountView, + undelegate_buffer_account: &AccountView, + delegation_metadata: DelegationMetadata, + system_program: &AccountView, +) -> ProgramResult { + let undelegate_buffer_bump: u8 = require_uninitialized_pda( + undelegate_buffer_account, + &[ + pda::UNDELEGATE_BUFFER_TAG, + delegated_account.address().as_ref(), + ], + &crate::fast::ID, + true, + UndelegateBufferCtx, + )?; + + create_pda( + undelegate_buffer_account, + &crate::fast::ID, + delegated_account.data_len(), + &[Signer::from(&seeds!( + pda::UNDELEGATE_BUFFER_TAG, + delegated_account.address().as_ref(), + &[undelegate_buffer_bump] + ))], + caller, + )?; + + (*undelegate_buffer_account.try_borrow_mut()?) + .copy_from_slice(&delegated_account.try_borrow()?); + + process_undelegation_with_cpi( + caller, + delegated_account, + owner_program, + undelegate_buffer_account, + &[Signer::from(&seeds!( + pda::UNDELEGATE_BUFFER_TAG, + delegated_account.address().as_ref(), + &[undelegate_buffer_bump] + ))], + delegation_metadata, + system_program, + )?; + + close_pda(undelegate_buffer_account, caller) +} + +fn cleanup_pending_commit( + delegated_account: &AccountView, + commit_state_account: &AccountView, + commit_record_account: &AccountView, + commit_reimbursement: &AccountView, +) -> ProgramResult { + let commit_state_uninitialized = + is_uninitialized_account(commit_state_account); + let commit_record_uninitialized = + is_uninitialized_account(commit_record_account); + + if commit_state_uninitialized && commit_record_uninitialized { + return Ok(()); + } + + if commit_state_uninitialized != commit_record_uninitialized { + return Err(DlpError::InvalidPendingCommitState.into()); + } + + require_initialized_commit_state( + delegated_account, + commit_state_account, + true, + )?; + require_initialized_commit_record( + delegated_account, + commit_record_account, + true, + )?; + + { + let commit_record_data = commit_record_account.try_borrow()?; + let commit_record = CommitRecord::try_from_bytes_with_discriminator( + &commit_record_data, + ) + .map_err(to_pinocchio_program_error)?; + + if !address_eq( + &commit_record.account.to_bytes().into(), + delegated_account.address(), + ) || !address_eq( + &commit_record.identity.to_bytes().into(), + commit_reimbursement.address(), + ) { + return Err(DlpError::InvalidPendingCommitState.into()); + } + } + + if !commit_reimbursement.is_writable() { + return Err(ProgramError::Immutable); + } + + // Timeout carry-over is a rollback/escape hatch. At this point the + // validator has committed state into the commit PDAs, but that state has + // not been finalized into the delegated account. Applying it here would let + // this permissionless timeout path accept fresh validator state, which is + // exactly what the timeout design forbids. + // + // We still close both commit PDAs so the delegated account cannot leave + // orphaned DLP-owned accounts behind. The commit record identity is the + // validator that created the pending commit, so sending both PDA balances to + // that identity refunds commit rent/collateral without treating the pending + // commit as valid state. + close_pda(commit_state_account, commit_reimbursement)?; + close_pda(commit_record_account, commit_reimbursement) +} diff --git a/src/processor/fast/mod.rs b/src/processor/fast/mod.rs index 593ede72..49600052 100644 --- a/src/processor/fast/mod.rs +++ b/src/processor/fast/mod.rs @@ -1,3 +1,4 @@ +mod carry_over_requested_undelegation; mod commit_diff; mod commit_diff_from_buffer; mod commit_finalize; @@ -14,6 +15,7 @@ mod utils; pub(crate) mod internal; +pub use carry_over_requested_undelegation::*; pub use commit_diff::*; pub use commit_diff_from_buffer::*; pub use commit_finalize::*; diff --git a/src/processor/fast/request_undelegation.rs b/src/processor/fast/request_undelegation.rs index 29999018..d657b0bf 100644 --- a/src/processor/fast/request_undelegation.rs +++ b/src/processor/fast/request_undelegation.rs @@ -10,6 +10,7 @@ use pinocchio_log::log; use super::to_pinocchio_program_error; use crate::{ + consts::DEFAULT_UNDELEGATION_REQUEST_TIMEOUT_SLOTS, error::DlpError, pda, processor::{fast::utils::pda::create_pda, utils::curve::is_on_curve_fast}, @@ -36,7 +37,7 @@ use crate::{ pub fn process_request_undelegation( _program_id: &Address, accounts: &[AccountView], - _data: &[u8], + data: &[u8], ) -> ProgramResult { let [ payer, // force multi-line @@ -90,10 +91,11 @@ pub fn process_request_undelegation( } let delegation_metadata_data = delegation_metadata_account.try_borrow()?; - DelegationMetadata::try_from_bytes_with_discriminator( - &delegation_metadata_data, - ) - .map_err(to_pinocchio_program_error)?; + let delegation_metadata = + DelegationMetadata::try_from_bytes_with_discriminator( + &delegation_metadata_data, + ) + .map_err(to_pinocchio_program_error)?; drop(delegation_record_data); drop(delegation_metadata_data); @@ -102,8 +104,14 @@ pub fn process_request_undelegation( pda::UNDELEGATION_REQUEST_TAG, delegated_account.address().as_ref(), ]; + let timeout_slots = parse_timeout_slots(data)?; if is_uninitialized_account(undelegation_request_account) { + let created_slot = Clock::get()?.slot; + let expires_at_slot = created_slot + .checked_add(timeout_slots) + .ok_or(DlpError::Overflow)?; + let request_bump = require_uninitialized_pda( undelegation_request_account, request_seeds, @@ -128,7 +136,11 @@ pub fn process_request_undelegation( delegated_account: delegated_account.address().to_bytes().into(), owner_program: owner_program.address().to_bytes().into(), rent_payer: payer.address().to_bytes().into(), - created_slot: Clock::get()?.slot, + created_slot, + expires_at_slot, + delegation_nonce_at_request: delegation_metadata.last_update_nonce, + bump: request_bump, + _padding: [0; 7], }; let mut request_data = undelegation_request_account.try_borrow_mut()?; request @@ -168,3 +180,20 @@ pub fn process_request_undelegation( Ok(()) } + +fn parse_timeout_slots(data: &[u8]) -> Result { + match data.len() { + 0 => Ok(DEFAULT_UNDELEGATION_REQUEST_TIMEOUT_SLOTS), + 8 => { + let timeout_slots = u64::from_le_bytes( + data.try_into() + .map_err(|_| ProgramError::InvalidInstructionData)?, + ); + if timeout_slots < DEFAULT_UNDELEGATION_REQUEST_TIMEOUT_SLOTS { + return Err(DlpError::UndelegationRequestTimeoutTooShort.into()); + } + Ok(timeout_slots) + } + _ => Err(ProgramError::InvalidInstructionData), + } +} diff --git a/tests/fixtures/accounts.rs b/tests/fixtures/accounts.rs index e959bef1..4aadf5eb 100644 --- a/tests/fixtures/accounts.rs +++ b/tests/fixtures/accounts.rs @@ -1,7 +1,11 @@ use dlp::solana_program; -use dlp_api::state::{ - CommitRecord, DelegationMetadata, DelegationRecord, ProgramConfig, - UndelegationRequest, +use dlp_api::{ + consts::DEFAULT_UNDELEGATION_REQUEST_TIMEOUT_SLOTS, + pda::UNDELEGATION_REQUEST_TAG, + state::{ + CommitRecord, DelegationMetadata, DelegationRecord, ProgramConfig, + UndelegationRequest, + }, }; use solana_program::{ native_token::LAMPORTS_PER_SOL, pubkey::Pubkey, rent::Rent, @@ -207,11 +211,38 @@ pub fn create_undelegation_request_data( rent_payer: Pubkey, created_slot: u64, ) -> Vec { + create_undelegation_request_data_with_expiry( + delegated_account, + owner_program, + rent_payer, + created_slot, + created_slot + DEFAULT_UNDELEGATION_REQUEST_TIMEOUT_SLOTS, + DEFAULT_LAST_UPDATE_EXTERNAL_SLOT, + ) +} + +#[allow(dead_code)] +pub fn create_undelegation_request_data_with_expiry( + delegated_account: Pubkey, + owner_program: Pubkey, + rent_payer: Pubkey, + created_slot: u64, + expires_at_slot: u64, + delegation_nonce_at_request: u64, +) -> Vec { + let (_, bump) = Pubkey::find_program_address( + &[UNDELEGATION_REQUEST_TAG, delegated_account.as_ref()], + &dlp_api::id(), + ); let request = UndelegationRequest { delegated_account, owner_program, rent_payer, created_slot, + expires_at_slot, + delegation_nonce_at_request, + bump, + _padding: [0; 7], }; let mut bytes = vec![0u8; UndelegationRequest::size_with_discriminator()]; request.to_bytes_with_discriminator(&mut bytes).unwrap(); diff --git a/tests/test_carry_over_requested_undelegation.rs b/tests/test_carry_over_requested_undelegation.rs new file mode 100644 index 00000000..1550a61b --- /dev/null +++ b/tests/test_carry_over_requested_undelegation.rs @@ -0,0 +1,415 @@ +use dlp::solana_program; +use dlp_api::pda::{ + commit_record_pda_from_delegated_account, + commit_state_pda_from_delegated_account, + delegation_metadata_pda_from_delegated_account, + delegation_record_pda_from_delegated_account, + undelegation_request_pda_from_delegated_account, +}; +use solana_program::{hash::Hash, native_token::LAMPORTS_PER_SOL, rent::Rent}; +use solana_program_test::{read_file, BanksClient, ProgramTest}; +use solana_sdk::{ + account::Account, + signature::{Keypair, Signer}, + transaction::Transaction, +}; +use solana_sdk_ids::system_program; + +use crate::fixtures::{ + create_undelegation_request_data_with_expiry, + get_commit_record_account_data, get_delegation_metadata_data, + get_delegation_record_data, keypair_from_bytes, + COMMIT_NEW_STATE_ACCOUNT_DATA, DELEGATED_PDA, DELEGATED_PDA_ID, + DELEGATED_PDA_OWNER_ID, TEST_AUTHORITY, +}; + +mod fixtures; + +#[tokio::test] +async fn test_carry_over_requested_undelegation_after_expiry() { + let ( + banks, + caller, + request_rent_payer, + delegation_rent_payer, + _, + blockhash, + ) = setup_carry_over_env(false, 0).await; + + let request_pda = + undelegation_request_pda_from_delegated_account(&DELEGATED_PDA_ID); + let delegation_record_pda = + delegation_record_pda_from_delegated_account(&DELEGATED_PDA_ID); + let delegation_metadata_pda = + delegation_metadata_pda_from_delegated_account(&DELEGATED_PDA_ID); + + let request_lamports = banks + .get_account(request_pda) + .await + .unwrap() + .unwrap() + .lamports; + let delegation_record_lamports = banks + .get_account(delegation_record_pda) + .await + .unwrap() + .unwrap() + .lamports; + let delegation_metadata_lamports = banks + .get_account(delegation_metadata_pda) + .await + .unwrap() + .unwrap() + .lamports; + let request_payer_before = banks + .get_account(request_rent_payer.pubkey()) + .await + .unwrap() + .unwrap() + .lamports; + let delegation_payer_before = banks + .get_account(delegation_rent_payer.pubkey()) + .await + .unwrap() + .unwrap() + .lamports; + let delegated_before = + banks.get_account(DELEGATED_PDA_ID).await.unwrap().unwrap(); + + let ix = dlp_api::instruction_builder::carry_over_requested_undelegation( + caller.pubkey(), + DELEGATED_PDA_ID, + DELEGATED_PDA_OWNER_ID, + request_rent_payer.pubkey(), + delegation_rent_payer.pubkey(), + caller.pubkey(), + ); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&caller.pubkey()), + &[&caller], + blockhash, + ); + + let res = banks.process_transaction(tx).await; + assert!(res.is_ok()); + + assert!(banks.get_account(request_pda).await.unwrap().is_none()); + assert!(banks + .get_account(delegation_record_pda) + .await + .unwrap() + .is_none()); + assert!(banks + .get_account(delegation_metadata_pda) + .await + .unwrap() + .is_none()); + + let delegated_after = + banks.get_account(DELEGATED_PDA_ID).await.unwrap().unwrap(); + assert_eq!(delegated_after.owner, DELEGATED_PDA_OWNER_ID); + assert_eq!(delegated_after.data, delegated_before.data); + + let request_payer_after = banks + .get_account(request_rent_payer.pubkey()) + .await + .unwrap() + .unwrap() + .lamports; + assert_eq!(request_payer_after, request_payer_before + request_lamports); + + let delegation_payer_after = banks + .get_account(delegation_rent_payer.pubkey()) + .await + .unwrap() + .unwrap() + .lamports; + assert_eq!( + delegation_payer_after, + delegation_payer_before + + delegation_record_lamports + + delegation_metadata_lamports + ); +} + +#[tokio::test] +async fn test_carry_over_requested_undelegation_rejects_before_expiry() { + let ( + banks, + caller, + request_rent_payer, + delegation_rent_payer, + _, + blockhash, + ) = setup_carry_over_env(false, 1_000_000).await; + + let ix = dlp_api::instruction_builder::carry_over_requested_undelegation( + caller.pubkey(), + DELEGATED_PDA_ID, + DELEGATED_PDA_OWNER_ID, + request_rent_payer.pubkey(), + delegation_rent_payer.pubkey(), + caller.pubkey(), + ); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&caller.pubkey()), + &[&caller], + blockhash, + ); + + let res = banks.process_transaction(tx).await; + assert!(res.is_err()); + + let request_pda = + undelegation_request_pda_from_delegated_account(&DELEGATED_PDA_ID); + assert!(banks.get_account(request_pda).await.unwrap().is_some()); +} + +#[tokio::test] +async fn test_carry_over_closes_pending_commit_without_applying_it() { + let ( + banks, + caller, + request_rent_payer, + delegation_rent_payer, + validator, + blockhash, + ) = setup_carry_over_env(true, 0).await; + + let commit_state_pda = + commit_state_pda_from_delegated_account(&DELEGATED_PDA_ID); + let commit_record_pda = + commit_record_pda_from_delegated_account(&DELEGATED_PDA_ID); + let commit_state_lamports = banks + .get_account(commit_state_pda) + .await + .unwrap() + .unwrap() + .lamports; + let commit_record_lamports = banks + .get_account(commit_record_pda) + .await + .unwrap() + .unwrap() + .lamports; + let validator_before = banks + .get_account(validator.pubkey()) + .await + .unwrap() + .unwrap() + .lamports; + + let ix = dlp_api::instruction_builder::carry_over_requested_undelegation( + caller.pubkey(), + DELEGATED_PDA_ID, + DELEGATED_PDA_OWNER_ID, + request_rent_payer.pubkey(), + delegation_rent_payer.pubkey(), + validator.pubkey(), + ); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&caller.pubkey()), + &[&caller], + blockhash, + ); + + let res = banks.process_transaction(tx).await; + assert!(res.is_ok()); + + assert!(banks.get_account(commit_state_pda).await.unwrap().is_none()); + assert!(banks + .get_account(commit_record_pda) + .await + .unwrap() + .is_none()); + + let delegated_after = + banks.get_account(DELEGATED_PDA_ID).await.unwrap().unwrap(); + assert_eq!(delegated_after.owner, DELEGATED_PDA_OWNER_ID); + assert_eq!(delegated_after.data, DELEGATED_PDA); + assert_ne!(delegated_after.data, COMMIT_NEW_STATE_ACCOUNT_DATA); + + let validator_after = banks + .get_account(validator.pubkey()) + .await + .unwrap() + .unwrap() + .lamports; + assert_eq!( + validator_after, + validator_before + commit_state_lamports + commit_record_lamports + ); +} + +async fn setup_carry_over_env( + with_pending_commit: bool, + expires_at_slot: u64, +) -> (BanksClient, Keypair, Keypair, Keypair, Keypair, Hash) { + let mut program_test = ProgramTest::new("dlp", dlp_api::ID, None); + program_test.prefer_bpf(true); + + let caller = Keypair::new(); + let request_rent_payer = Keypair::new(); + let delegation_rent_payer = Keypair::new(); + let validator = keypair_from_bytes(&TEST_AUTHORITY); + + add_system_account(&mut program_test, caller.pubkey()); + add_system_account(&mut program_test, request_rent_payer.pubkey()); + add_system_account(&mut program_test, delegation_rent_payer.pubkey()); + add_system_account(&mut program_test, validator.pubkey()); + add_delegated_account(&mut program_test); + add_delegation_accounts(&mut program_test, delegation_rent_payer.pubkey()); + add_request_account( + &mut program_test, + request_rent_payer.pubkey(), + expires_at_slot, + ); + add_owner_program(&mut program_test); + if with_pending_commit { + add_pending_commit_accounts(&mut program_test, validator.pubkey()); + } + + let (banks, _, blockhash) = program_test.start().await; + ( + banks, + caller, + request_rent_payer, + delegation_rent_payer, + validator, + blockhash, + ) +} + +fn add_system_account( + program_test: &mut ProgramTest, + pubkey: solana_program::pubkey::Pubkey, +) { + program_test.add_account( + pubkey, + Account { + lamports: LAMPORTS_PER_SOL, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); +} + +fn add_delegated_account(program_test: &mut ProgramTest) { + program_test.add_account( + DELEGATED_PDA_ID, + Account { + lamports: LAMPORTS_PER_SOL, + data: DELEGATED_PDA.into(), + owner: dlp_api::id(), + executable: false, + rent_epoch: 0, + }, + ); +} + +fn add_delegation_accounts( + program_test: &mut ProgramTest, + delegation_rent_payer: solana_program::pubkey::Pubkey, +) { + let delegation_record_data = get_delegation_record_data( + keypair_from_bytes(&TEST_AUTHORITY).pubkey(), + None, + ); + program_test.add_account( + delegation_record_pda_from_delegated_account(&DELEGATED_PDA_ID), + Account { + lamports: Rent::default() + .minimum_balance(delegation_record_data.len()), + data: delegation_record_data, + owner: dlp_api::id(), + executable: false, + rent_epoch: 0, + }, + ); + + let delegation_metadata_data = + get_delegation_metadata_data(delegation_rent_payer, Some(false)); + program_test.add_account( + delegation_metadata_pda_from_delegated_account(&DELEGATED_PDA_ID), + Account { + lamports: Rent::default() + .minimum_balance(delegation_metadata_data.len()), + data: delegation_metadata_data, + owner: dlp_api::id(), + executable: false, + rent_epoch: 0, + }, + ); +} + +fn add_request_account( + program_test: &mut ProgramTest, + request_rent_payer: solana_program::pubkey::Pubkey, + expires_at_slot: u64, +) { + let request_data = create_undelegation_request_data_with_expiry( + DELEGATED_PDA_ID, + DELEGATED_PDA_OWNER_ID, + request_rent_payer, + 0, + expires_at_slot, + 0, + ); + program_test.add_account( + undelegation_request_pda_from_delegated_account(&DELEGATED_PDA_ID), + Account { + lamports: Rent::default().minimum_balance(request_data.len()), + data: request_data, + owner: dlp_api::id(), + executable: false, + rent_epoch: 0, + }, + ); +} + +fn add_owner_program(program_test: &mut ProgramTest) { + let data = read_file("tests/buffers/test_delegation.so"); + program_test.add_account( + DELEGATED_PDA_OWNER_ID, + Account { + lamports: Rent::default().minimum_balance(data.len()).max(1), + data, + owner: solana_sdk::bpf_loader::id(), + executable: true, + rent_epoch: 0, + }, + ); +} + +fn add_pending_commit_accounts( + program_test: &mut ProgramTest, + validator: solana_program::pubkey::Pubkey, +) { + program_test.add_account( + commit_state_pda_from_delegated_account(&DELEGATED_PDA_ID), + Account { + lamports: LAMPORTS_PER_SOL, + data: COMMIT_NEW_STATE_ACCOUNT_DATA.into(), + owner: dlp_api::id(), + executable: false, + rent_epoch: 0, + }, + ); + + let commit_record_data = get_commit_record_account_data(validator); + program_test.add_account( + commit_record_pda_from_delegated_account(&DELEGATED_PDA_ID), + Account { + lamports: Rent::default().minimum_balance(commit_record_data.len()), + data: commit_record_data, + owner: dlp_api::id(), + executable: false, + rent_epoch: 0, + }, + ); +} diff --git a/tests/test_request_undelegation.rs b/tests/test_request_undelegation.rs index 4a88014e..5f477758 100644 --- a/tests/test_request_undelegation.rs +++ b/tests/test_request_undelegation.rs @@ -1,5 +1,6 @@ use dlp::solana_program; use dlp_api::{ + consts::DEFAULT_UNDELEGATION_REQUEST_TIMEOUT_SLOTS, pda::{ commit_record_pda_from_delegated_account, commit_state_pda_from_delegated_account, @@ -70,6 +71,11 @@ async fn test_request_undelegation_creates_request() { assert_eq!(request.delegated_account, DELEGATED_PDA_ID); assert_eq!(request.owner_program, DELEGATED_PDA_OWNER_ID); assert_eq!(request.rent_payer, payer.pubkey()); + assert_eq!( + request.expires_at_slot, + request.created_slot + DEFAULT_UNDELEGATION_REQUEST_TIMEOUT_SLOTS + ); + assert_eq!(request.delegation_nonce_at_request, 0); } #[tokio::test] @@ -106,6 +112,54 @@ async fn test_request_undelegation_is_idempotent() { assert_eq!(request_after.data, request_before.data); } +#[tokio::test] +async fn test_request_undelegation_accepts_custom_timeout() { + let (banks, payer, _, blockhash) = setup_request_env().await; + + let ix = request_undelegation_from_owner_program_with_timeout( + payer.pubkey(), + 600, + ); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer], + blockhash, + ); + + let res = banks.process_transaction(tx).await; + assert!(res.is_ok()); + + let request_pda = + undelegation_request_pda_from_delegated_account(&DELEGATED_PDA_ID); + let request_account = + banks.get_account(request_pda).await.unwrap().unwrap(); + let request = UndelegationRequest::try_from_bytes_with_discriminator( + &request_account.data, + ) + .unwrap(); + assert_eq!(request.expires_at_slot, request.created_slot + 600); +} + +#[tokio::test] +async fn test_request_undelegation_rejects_short_timeout() { + let (banks, payer, _, blockhash) = setup_request_env().await; + + let ix = request_undelegation_from_owner_program_with_timeout( + payer.pubkey(), + DEFAULT_UNDELEGATION_REQUEST_TIMEOUT_SLOTS - 1, + ); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer], + blockhash, + ); + + let res = banks.process_transaction(tx).await; + assert!(res.is_err()); +} + #[tokio::test] async fn test_request_undelegation_rejects_missing_delegated_signer() { let (banks, payer, _, blockhash) = setup_request_env().await; @@ -226,7 +280,7 @@ async fn test_undelegate_with_request_closes_request() { fn process_request_wrapper( program_id: &Pubkey, accounts: &[AccountInfo], - _data: &[u8], + data: &[u8], ) -> ProgramResult { let [payer, delegated_account, owner_program, request_account, delegation_record_account, delegation_metadata_account, system_program_account, dlp_program] = accounts @@ -238,11 +292,26 @@ fn process_request_wrapper( return Err(ProgramError::IncorrectProgramId); } - let ix = dlp_api::instruction_builder::request_undelegation( - *payer.key, - *delegated_account.key, - *program_id, - ); + let ix = match data.len() { + 0 => dlp_api::instruction_builder::request_undelegation( + *payer.key, + *delegated_account.key, + *program_id, + ), + 8 => { + let timeout_slots = u64::from_le_bytes( + data.try_into() + .map_err(|_| ProgramError::InvalidInstructionData)?, + ); + dlp_api::instruction_builder::request_undelegation_with_timeout( + *payer.key, + *delegated_account.key, + *program_id, + timeout_slots, + ) + } + _ => return Err(ProgramError::InvalidInstructionData), + }; let (_, bump) = Pubkey::find_program_address(&[TEST_PDA_SEED], program_id); let bump_seed = [bump]; invoke_signed( @@ -262,6 +331,23 @@ fn process_request_wrapper( } fn request_undelegation_from_owner_program(payer: Pubkey) -> Instruction { + request_undelegation_from_owner_program_with_data(payer, vec![]) +} + +fn request_undelegation_from_owner_program_with_timeout( + payer: Pubkey, + timeout_slots: u64, +) -> Instruction { + request_undelegation_from_owner_program_with_data( + payer, + timeout_slots.to_le_bytes().to_vec(), + ) +} + +fn request_undelegation_from_owner_program_with_data( + payer: Pubkey, + data: Vec, +) -> Instruction { Instruction { program_id: DELEGATED_PDA_OWNER_ID, accounts: vec![ @@ -287,7 +373,7 @@ fn request_undelegation_from_owner_program(payer: Pubkey) -> Instruction { AccountMeta::new_readonly(system_program::id(), false), AccountMeta::new_readonly(dlp_api::id(), false), ], - data: vec![], + data, } } From fc36480335cb6ffe10a2a6192db4de0c501e3780 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Wed, 24 Jun 2026 18:46:09 +0530 Subject: [PATCH 03/30] address rabbit's feedback --- .../fast/carry_over_requested_undelegation.rs | 22 ++++- .../test_carry_over_requested_undelegation.rs | 85 ++++++++++++++++--- tests/test_request_undelegation.rs | 64 +++++++++++++- 3 files changed, 151 insertions(+), 20 deletions(-) diff --git a/src/processor/fast/carry_over_requested_undelegation.rs b/src/processor/fast/carry_over_requested_undelegation.rs index 6767b0ac..cbffd8c0 100644 --- a/src/processor/fast/carry_over_requested_undelegation.rs +++ b/src/processor/fast/carry_over_requested_undelegation.rs @@ -12,14 +12,14 @@ use crate::{ error::DlpError, pda, processor::fast::utils::pda::{close_pda, create_pda}, - require_n_accounts, + require, require_n_accounts, requires::{ is_uninitialized_account, require_initialized_commit_record, require_initialized_commit_state, require_initialized_delegation_metadata, require_initialized_delegation_record, require_initialized_pda, - require_owned_pda, require_signer, require_uninitialized_pda, - UndelegateBufferCtx, + require_owned_pda, require_pda, require_signer, + require_uninitialized_pda, UndelegateBufferCtx, }, state::{ CommitRecord, DelegationMetadata, DelegationRecord, UndelegationRequest, @@ -71,6 +71,7 @@ pub fn process_carry_over_requested_undelegation( &crate::fast::ID, "delegated account", )?; + require!(delegated_account.is_writable(), ProgramError::Immutable); let request = load_valid_request( delegated_account, @@ -261,6 +262,21 @@ fn cleanup_pending_commit( commit_record_account: &AccountView, commit_reimbursement: &AccountView, ) -> ProgramResult { + require_pda( + commit_state_account, + &[pda::COMMIT_STATE_TAG, delegated_account.address().as_ref()], + &crate::fast::ID, + false, + "commit state", + )?; + require_pda( + commit_record_account, + &[pda::COMMIT_RECORD_TAG, delegated_account.address().as_ref()], + &crate::fast::ID, + false, + "commit record", + )?; + let commit_state_uninitialized = is_uninitialized_account(commit_state_account); let commit_record_uninitialized = diff --git a/tests/test_carry_over_requested_undelegation.rs b/tests/test_carry_over_requested_undelegation.rs index 1550a61b..8c5405bd 100644 --- a/tests/test_carry_over_requested_undelegation.rs +++ b/tests/test_carry_over_requested_undelegation.rs @@ -1,17 +1,23 @@ use dlp::solana_program; -use dlp_api::pda::{ - commit_record_pda_from_delegated_account, - commit_state_pda_from_delegated_account, - delegation_metadata_pda_from_delegated_account, - delegation_record_pda_from_delegated_account, - undelegation_request_pda_from_delegated_account, +use dlp_api::{ + error::DlpError, + pda::{ + commit_record_pda_from_delegated_account, + commit_state_pda_from_delegated_account, + delegation_metadata_pda_from_delegated_account, + delegation_record_pda_from_delegated_account, + undelegation_request_pda_from_delegated_account, + }, }; use solana_program::{hash::Hash, native_token::LAMPORTS_PER_SOL, rent::Rent}; -use solana_program_test::{read_file, BanksClient, ProgramTest}; +use solana_program_test::{ + read_file, BanksClient, BanksClientError, ProgramTest, +}; use solana_sdk::{ account::Account, + instruction::InstructionError, signature::{Keypair, Signer}, - transaction::Transaction, + transaction::{Transaction, TransactionError}, }; use solana_sdk_ids::system_program; @@ -144,6 +150,27 @@ async fn test_carry_over_requested_undelegation_rejects_before_expiry() { blockhash, ) = setup_carry_over_env(false, 1_000_000).await; + let request_pda = + undelegation_request_pda_from_delegated_account(&DELEGATED_PDA_ID); + let delegation_record_pda = + delegation_record_pda_from_delegated_account(&DELEGATED_PDA_ID); + let delegation_metadata_pda = + delegation_metadata_pda_from_delegated_account(&DELEGATED_PDA_ID); + + let request_before = banks.get_account(request_pda).await.unwrap().unwrap(); + let delegation_record_before = banks + .get_account(delegation_record_pda) + .await + .unwrap() + .unwrap(); + let delegation_metadata_before = banks + .get_account(delegation_metadata_pda) + .await + .unwrap() + .unwrap(); + let delegated_before = + banks.get_account(DELEGATED_PDA_ID).await.unwrap().unwrap(); + let ix = dlp_api::instruction_builder::carry_over_requested_undelegation( caller.pubkey(), DELEGATED_PDA_ID, @@ -159,12 +186,44 @@ async fn test_carry_over_requested_undelegation_rejects_before_expiry() { blockhash, ); - let res = banks.process_transaction(tx).await; - assert!(res.is_err()); + let err = banks.process_transaction(tx).await.unwrap_err(); + assert!( + matches!( + err, + BanksClientError::TransactionError( + TransactionError::InstructionError( + 0, + InstructionError::Custom(code), + ) + ) if code == DlpError::UndelegationRequestNotExpired as u32 + ), + "expected UndelegationRequestNotExpired, got {err:?}" + ); - let request_pda = - undelegation_request_pda_from_delegated_account(&DELEGATED_PDA_ID); - assert!(banks.get_account(request_pda).await.unwrap().is_some()); + assert_eq!( + banks.get_account(request_pda).await.unwrap().unwrap(), + request_before + ); + assert_eq!( + banks + .get_account(delegation_record_pda) + .await + .unwrap() + .unwrap(), + delegation_record_before + ); + assert_eq!( + banks + .get_account(delegation_metadata_pda) + .await + .unwrap() + .unwrap(), + delegation_metadata_before + ); + assert_eq!( + banks.get_account(DELEGATED_PDA_ID).await.unwrap().unwrap(), + delegated_before + ); } #[tokio::test] diff --git a/tests/test_request_undelegation.rs b/tests/test_request_undelegation.rs index 5f477758..987363fd 100644 --- a/tests/test_request_undelegation.rs +++ b/tests/test_request_undelegation.rs @@ -1,6 +1,7 @@ use dlp::solana_program; use dlp_api::{ consts::DEFAULT_UNDELEGATION_REQUEST_TIMEOUT_SLOTS, + error::DlpError, pda::{ commit_record_pda_from_delegated_account, commit_state_pda_from_delegated_account, @@ -22,11 +23,14 @@ use solana_program::{ pubkey::Pubkey, rent::Rent, }; -use solana_program_test::{processor, read_file, BanksClient, ProgramTest}; +use solana_program_test::{ + processor, read_file, BanksClient, BanksClientError, ProgramTest, +}; use solana_sdk::{ account::Account, + instruction::InstructionError, signature::{Keypair, Signer}, - transaction::Transaction, + transaction::{Transaction, TransactionError}, }; use solana_sdk_ids::system_program; @@ -156,8 +160,19 @@ async fn test_request_undelegation_rejects_short_timeout() { blockhash, ); - let res = banks.process_transaction(tx).await; - assert!(res.is_err()); + let err = banks.process_transaction(tx).await.unwrap_err(); + assert!( + matches!( + err, + BanksClientError::TransactionError( + TransactionError::InstructionError( + 0, + InstructionError::Custom(code), + ) + ) if code == DlpError::UndelegationRequestTimeoutTooShort as u32 + ), + "expected UndelegationRequestTimeoutTooShort, got {err:?}" + ); } #[tokio::test] @@ -277,6 +292,47 @@ async fn test_undelegate_with_request_closes_request() { ); } +#[tokio::test] +async fn test_undelegate_with_malformed_optional_request_accounts_rejected() { + let (banks, _, authority, request_rent_payer, blockhash) = + setup_undelegate_with_request_env().await; + + let ix_finalize = dlp_api::instruction_builder::finalize( + authority.pubkey(), + DELEGATED_PDA_ID, + ); + let mut ix_undelegate = + dlp_api::instruction_builder::undelegate_with_request( + authority.pubkey(), + DELEGATED_PDA_ID, + DELEGATED_PDA_OWNER_ID, + authority.pubkey(), + request_rent_payer.pubkey(), + ); + ix_undelegate.accounts.pop(); + + let tx = Transaction::new_signed_with_payer( + &[ix_finalize, ix_undelegate], + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); + let err = banks.process_transaction(tx).await.unwrap_err(); + assert!( + matches!( + err, + BanksClientError::TransactionError( + TransactionError::InstructionError( + 1, + InstructionError::InvalidInstructionData, + ) + ) + ), + "expected malformed optional request accounts to fail undelegate \ + with InvalidInstructionData, got {err:?}" + ); +} + fn process_request_wrapper( program_id: &Pubkey, accounts: &[AccountInfo], From 723ead035b4113cabd1291ab1605a28f290308de Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Wed, 24 Jun 2026 18:49:05 +0530 Subject: [PATCH 04/30] fix lint issues --- tests/test_call_handler_v2.rs | 2 +- tests/test_lamports_settlement.rs | 10 ---------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/tests/test_call_handler_v2.rs b/tests/test_call_handler_v2.rs index d280016e..716b01c1 100644 --- a/tests/test_call_handler_v2.rs +++ b/tests/test_call_handler_v2.rs @@ -1,7 +1,7 @@ use dlp::solana_program; use dlp_api::{ args::CallHandlerArgs, - compat::borsh::{self, to_vec, BorshDeserialize, BorshSerialize}, + compat::borsh::{to_vec, BorshDeserialize, BorshSerialize}, ephemeral_balance_seeds_from_payer, pda::{ commit_record_pda_from_delegated_account, diff --git a/tests/test_lamports_settlement.rs b/tests/test_lamports_settlement.rs index df7bdd47..8ca4ec07 100644 --- a/tests/test_lamports_settlement.rs +++ b/tests/test_lamports_settlement.rs @@ -456,7 +456,6 @@ async fn test_commit_and_finalize_lamports_settlement() { banks: &mut base_banks, authority: &validator, fee_payer: &payer, - blockhash, new_delegated_account_lamports: lamports_on_ephem, nonce: 1, allow_undelegation: false, @@ -470,7 +469,6 @@ async fn test_commit_and_finalize_lamports_settlement() { banks: &mut base_banks, authority: &validator, fee_payer: &payer, - blockhash, label: "first finalize", delegated_account: delegated.pubkey(), }) @@ -503,7 +501,6 @@ async fn test_commit_and_finalize_lamports_settlement() { banks: &mut base_banks, authority: &validator, fee_payer: &payer, - blockhash, new_delegated_account_lamports: lamports_on_ephem, nonce: 2, allow_undelegation: false, @@ -517,7 +514,6 @@ async fn test_commit_and_finalize_lamports_settlement() { banks: &mut base_banks, authority: &validator, fee_payer: &payer, - blockhash, label: "second finalize", delegated_account: delegated.pubkey(), }) @@ -552,7 +548,6 @@ async fn test_commit_and_finalize_lamports_settlement() { banks: &mut base_banks, authority: &validator, fee_payer: &payer, - blockhash, new_delegated_account_lamports: lamports_on_ephem, nonce: 3, allow_undelegation: false, @@ -566,7 +561,6 @@ async fn test_commit_and_finalize_lamports_settlement() { banks: &mut base_banks, authority: &validator, fee_payer: &payer, - blockhash, label: "third finalize", delegated_account: delegated.pubkey(), }) @@ -601,7 +595,6 @@ async fn test_commit_and_finalize_lamports_settlement() { banks: &mut base_banks, authority: &validator, fee_payer: &payer, - blockhash, new_delegated_account_lamports: lamports_on_ephem, nonce: 4, allow_undelegation: false, @@ -615,7 +608,6 @@ async fn test_commit_and_finalize_lamports_settlement() { banks: &mut base_banks, authority: &validator, fee_payer: &payer, - blockhash, label: "fourth finalize", delegated_account: delegated.pubkey(), }) @@ -1079,7 +1071,6 @@ 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, @@ -1261,7 +1252,6 @@ struct FinalizeWithFeePayerArgs<'a> { banks: &'a mut BanksClient, authority: &'a Keypair, fee_payer: &'a Keypair, - blockhash: Hash, label: &'a str, delegated_account: Pubkey, } From 4ac27be494a3c840cea567fa0246081c047c7996 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Wed, 24 Jun 2026 19:58:14 +0530 Subject: [PATCH 05/30] address rabbit's feedback --- .../fast/carry_over_requested_undelegation.rs | 9 ++ tests/fixtures/accounts.rs | 16 ++- .../test_carry_over_requested_undelegation.rs | 117 +++++++++++++++++- 3 files changed, 136 insertions(+), 6 deletions(-) diff --git a/src/processor/fast/carry_over_requested_undelegation.rs b/src/processor/fast/carry_over_requested_undelegation.rs index cbffd8c0..43e33495 100644 --- a/src/processor/fast/carry_over_requested_undelegation.rs +++ b/src/processor/fast/carry_over_requested_undelegation.rs @@ -117,6 +117,15 @@ pub fn process_carry_over_requested_undelegation( { return Err(ProgramError::InvalidAccountOwner); } + // CHECKPOINT: A timeout request is bound to the delegation nonce observed + // when it was created. If undelegation is still desired after the nonce + // changes, one possible design is to let request_undelegation refresh an + // existing request so it records the current nonce. + if request.delegation_nonce_at_request + != delegation_metadata.last_update_nonce + { + return Err(DlpError::InvalidUndelegationRequest.into()); + } if !address_eq( &delegation_metadata.rent_payer.to_bytes().into(), delegation_rent_payer.address(), diff --git a/tests/fixtures/accounts.rs b/tests/fixtures/accounts.rs index 4aadf5eb..25c88d5e 100644 --- a/tests/fixtures/accounts.rs +++ b/tests/fixtures/accounts.rs @@ -134,10 +134,24 @@ pub fn get_delegation_metadata_data( rent_payer: Pubkey, is_undelegatable: Option, ) -> Vec { - create_delegation_metadata_data( + get_delegation_metadata_data_with_nonce( + rent_payer, + is_undelegatable, + DEFAULT_LAST_UPDATE_EXTERNAL_SLOT, + ) +} + +#[allow(dead_code)] +pub fn get_delegation_metadata_data_with_nonce( + rent_payer: Pubkey, + is_undelegatable: Option, + last_update_nonce: u64, +) -> Vec { + create_delegation_metadata_data_with_nonce( rent_payer, DEFAULT_SEEDS, is_undelegatable.unwrap_or(DEFAULT_IS_UNDELEGATABLE), + last_update_nonce, ) } diff --git a/tests/test_carry_over_requested_undelegation.rs b/tests/test_carry_over_requested_undelegation.rs index 8c5405bd..a487d77c 100644 --- a/tests/test_carry_over_requested_undelegation.rs +++ b/tests/test_carry_over_requested_undelegation.rs @@ -23,7 +23,7 @@ use solana_sdk_ids::system_program; use crate::fixtures::{ create_undelegation_request_data_with_expiry, - get_commit_record_account_data, get_delegation_metadata_data, + get_commit_record_account_data, get_delegation_metadata_data_with_nonce, get_delegation_record_data, keypair_from_bytes, COMMIT_NEW_STATE_ACCOUNT_DATA, DELEGATED_PDA, DELEGATED_PDA_ID, DELEGATED_PDA_OWNER_ID, TEST_AUTHORITY, @@ -226,6 +226,93 @@ async fn test_carry_over_requested_undelegation_rejects_before_expiry() { ); } +#[tokio::test] +async fn test_carry_over_requested_undelegation_rejects_stale_request() { + let ( + banks, + caller, + request_rent_payer, + delegation_rent_payer, + _, + blockhash, + ) = setup_carry_over_env_with_nonces(false, 0, 1, 0).await; + + let request_pda = + undelegation_request_pda_from_delegated_account(&DELEGATED_PDA_ID); + let delegation_record_pda = + delegation_record_pda_from_delegated_account(&DELEGATED_PDA_ID); + let delegation_metadata_pda = + delegation_metadata_pda_from_delegated_account(&DELEGATED_PDA_ID); + + let request_before = banks.get_account(request_pda).await.unwrap().unwrap(); + let delegation_record_before = banks + .get_account(delegation_record_pda) + .await + .unwrap() + .unwrap(); + let delegation_metadata_before = banks + .get_account(delegation_metadata_pda) + .await + .unwrap() + .unwrap(); + let delegated_before = + banks.get_account(DELEGATED_PDA_ID).await.unwrap().unwrap(); + + let ix = dlp_api::instruction_builder::carry_over_requested_undelegation( + caller.pubkey(), + DELEGATED_PDA_ID, + DELEGATED_PDA_OWNER_ID, + request_rent_payer.pubkey(), + delegation_rent_payer.pubkey(), + caller.pubkey(), + ); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&caller.pubkey()), + &[&caller], + blockhash, + ); + + let err = banks.process_transaction(tx).await.unwrap_err(); + assert!( + matches!( + err, + BanksClientError::TransactionError( + TransactionError::InstructionError( + 0, + InstructionError::Custom(code), + ) + ) if code == DlpError::InvalidUndelegationRequest as u32 + ), + "expected InvalidUndelegationRequest, got {err:?}" + ); + + assert_eq!( + banks.get_account(request_pda).await.unwrap().unwrap(), + request_before + ); + assert_eq!( + banks + .get_account(delegation_record_pda) + .await + .unwrap() + .unwrap(), + delegation_record_before + ); + assert_eq!( + banks + .get_account(delegation_metadata_pda) + .await + .unwrap() + .unwrap(), + delegation_metadata_before + ); + assert_eq!( + banks.get_account(DELEGATED_PDA_ID).await.unwrap().unwrap(), + delegated_before + ); +} + #[tokio::test] async fn test_carry_over_closes_pending_commit_without_applying_it() { let ( @@ -306,6 +393,16 @@ async fn test_carry_over_closes_pending_commit_without_applying_it() { async fn setup_carry_over_env( with_pending_commit: bool, expires_at_slot: u64, +) -> (BanksClient, Keypair, Keypair, Keypair, Keypair, Hash) { + setup_carry_over_env_with_nonces(with_pending_commit, expires_at_slot, 0, 0) + .await +} + +async fn setup_carry_over_env_with_nonces( + with_pending_commit: bool, + expires_at_slot: u64, + delegation_last_update_nonce: u64, + request_delegation_nonce: u64, ) -> (BanksClient, Keypair, Keypair, Keypair, Keypair, Hash) { let mut program_test = ProgramTest::new("dlp", dlp_api::ID, None); program_test.prefer_bpf(true); @@ -320,11 +417,16 @@ async fn setup_carry_over_env( add_system_account(&mut program_test, delegation_rent_payer.pubkey()); add_system_account(&mut program_test, validator.pubkey()); add_delegated_account(&mut program_test); - add_delegation_accounts(&mut program_test, delegation_rent_payer.pubkey()); + add_delegation_accounts( + &mut program_test, + delegation_rent_payer.pubkey(), + delegation_last_update_nonce, + ); add_request_account( &mut program_test, request_rent_payer.pubkey(), expires_at_slot, + request_delegation_nonce, ); add_owner_program(&mut program_test); if with_pending_commit { @@ -374,6 +476,7 @@ fn add_delegated_account(program_test: &mut ProgramTest) { fn add_delegation_accounts( program_test: &mut ProgramTest, delegation_rent_payer: solana_program::pubkey::Pubkey, + last_update_nonce: u64, ) { let delegation_record_data = get_delegation_record_data( keypair_from_bytes(&TEST_AUTHORITY).pubkey(), @@ -391,8 +494,11 @@ fn add_delegation_accounts( }, ); - let delegation_metadata_data = - get_delegation_metadata_data(delegation_rent_payer, Some(false)); + let delegation_metadata_data = get_delegation_metadata_data_with_nonce( + delegation_rent_payer, + Some(false), + last_update_nonce, + ); program_test.add_account( delegation_metadata_pda_from_delegated_account(&DELEGATED_PDA_ID), Account { @@ -410,6 +516,7 @@ fn add_request_account( program_test: &mut ProgramTest, request_rent_payer: solana_program::pubkey::Pubkey, expires_at_slot: u64, + delegation_nonce_at_request: u64, ) { let request_data = create_undelegation_request_data_with_expiry( DELEGATED_PDA_ID, @@ -417,7 +524,7 @@ fn add_request_account( request_rent_payer, 0, expires_at_slot, - 0, + delegation_nonce_at_request, ); program_test.add_account( undelegation_request_pda_from_delegated_account(&DELEGATED_PDA_ID), From 3cd51cc7b41b40a3540ee5ce7178e7995ddcffa7 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Wed, 24 Jun 2026 20:00:13 +0530 Subject: [PATCH 06/30] Use 30min as timeout for validator --- dlp-api/src/consts.rs | 3 ++- tests/test_request_undelegation.rs | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/dlp-api/src/consts.rs b/dlp-api/src/consts.rs index a8914e0b..49ff3b7a 100644 --- a/dlp-api/src/consts.rs +++ b/dlp-api/src/consts.rs @@ -15,7 +15,8 @@ pub const COMMIT_FEE_LAMPORTS: u64 = 100_000; pub const SESSION_FEE_LAMPORTS: u64 = 300_000; /// Default and minimum timeout for requested undelegation. -pub const DEFAULT_UNDELEGATION_REQUEST_TIMEOUT_SLOTS: u64 = 300; +/// Assuming 1 slot is roughly 400ms, then 4500 slots = 30min. +pub const DEFAULT_UNDELEGATION_REQUEST_TIMEOUT_SLOTS: u64 = 4500; /// The discriminator for the external undelegate instruction. pub const EXTERNAL_UNDELEGATE_DISCRIMINATOR: [u8; 8] = diff --git a/tests/test_request_undelegation.rs b/tests/test_request_undelegation.rs index 987363fd..452b09cd 100644 --- a/tests/test_request_undelegation.rs +++ b/tests/test_request_undelegation.rs @@ -122,7 +122,7 @@ async fn test_request_undelegation_accepts_custom_timeout() { let ix = request_undelegation_from_owner_program_with_timeout( payer.pubkey(), - 600, + 6000, ); let tx = Transaction::new_signed_with_payer( &[ix], @@ -142,7 +142,7 @@ async fn test_request_undelegation_accepts_custom_timeout() { &request_account.data, ) .unwrap(); - assert_eq!(request.expires_at_slot, request.created_slot + 600); + assert_eq!(request.expires_at_slot, request.created_slot + 6000); } #[tokio::test] From 2f367f4c44c9d29cdeec5e1a0a63580e2bedf5e3 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Wed, 24 Jun 2026 20:33:13 +0530 Subject: [PATCH 07/30] add `df -h` to CI --- .github/workflows/run-tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 6487cf91..89bb4a31 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -76,6 +76,9 @@ jobs: solana --version solana-keygen new --silent --no-bip39-passphrase + - name: log disk space + run: df -h + - name: run build run: | cargo build From 7cdfee4ff2ce4a3ddd76437b7f9572173c0bad29 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Wed, 24 Jun 2026 22:35:54 +0530 Subject: [PATCH 08/30] rename CarryOverRequestedUndelegation -> UndelegateAfterRequestTimeout --- dlp-api/src/discriminator.rs | 4 +- dlp-api/src/error.rs | 2 +- dlp-api/src/instruction_builder/mod.rs | 4 +- ...rs => undelegate_after_request_timeout.rs} | 12 +++--- dlp-api/src/state/undelegation_request.rs | 2 +- src/lib.rs | 4 +- src/processor/fast/mod.rs | 4 +- ...rs => undelegate_after_request_timeout.rs} | 8 ++-- ... test_undelegate_after_request_timeout.rs} | 37 +++++++++++-------- 9 files changed, 41 insertions(+), 36 deletions(-) rename dlp-api/src/instruction_builder/{carry_over_requested_undelegation.rs => undelegate_after_request_timeout.rs} (89%) rename src/processor/fast/{carry_over_requested_undelegation.rs => undelegate_after_request_timeout.rs} (98%) rename tests/{test_carry_over_requested_undelegation.rs => test_undelegate_after_request_timeout.rs} (93%) diff --git a/dlp-api/src/discriminator.rs b/dlp-api/src/discriminator.rs index 3445a2fe..71df4bdd 100644 --- a/dlp-api/src/discriminator.rs +++ b/dlp-api/src/discriminator.rs @@ -66,8 +66,8 @@ pub enum DlpDiscriminator { /// See [crate::processor::process_request_undelegation] for docs. RequestUndelegation = 26, - /// See [crate::processor::process_carry_over_requested_undelegation] for docs. - CarryOverRequestedUndelegation = 27, + /// See [crate::processor::process_undelegate_after_request_timeout] for docs. + UndelegateAfterRequestTimeout = 27, } impl DlpDiscriminator { diff --git a/dlp-api/src/error.rs b/dlp-api/src/error.rs index cf383f8e..458a370e 100644 --- a/dlp-api/src/error.rs +++ b/dlp-api/src/error.rs @@ -179,7 +179,7 @@ pub enum DlpError { #[error("Undelegation request has not expired")] UndelegationRequestNotExpired = 51, - #[error("Invalid pending commit state for timeout carry-over")] + #[error("Invalid pending commit state for request-timeout undelegation")] InvalidPendingCommitState = 52, #[error("An infallible error is encountered possibly due to logic error")] diff --git a/dlp-api/src/instruction_builder/mod.rs b/dlp-api/src/instruction_builder/mod.rs index a441d2c5..84b1f70c 100644 --- a/dlp-api/src/instruction_builder/mod.rs +++ b/dlp-api/src/instruction_builder/mod.rs @@ -1,6 +1,5 @@ mod call_handler; mod call_handler_v2; -mod carry_over_requested_undelegation; mod close_ephemeral_balance; mod close_validator_fees_vault; mod commit_diff; @@ -23,13 +22,13 @@ mod request_undelegation; mod top_up_ephemeral_balance; mod types; mod undelegate; +mod undelegate_after_request_timeout; mod undelegate_confined_account; mod validator_claim_fees; mod whitelist_validator_for_program; pub use call_handler::*; pub use call_handler_v2::*; -pub use carry_over_requested_undelegation::*; pub use close_ephemeral_balance::*; pub use close_validator_fees_vault::*; pub use commit_diff::*; @@ -52,6 +51,7 @@ pub use request_undelegation::*; pub use top_up_ephemeral_balance::*; pub use types::*; pub use undelegate::*; +pub use undelegate_after_request_timeout::*; pub use undelegate_confined_account::*; pub use validator_claim_fees::*; pub use whitelist_validator_for_program::*; diff --git a/dlp-api/src/instruction_builder/carry_over_requested_undelegation.rs b/dlp-api/src/instruction_builder/undelegate_after_request_timeout.rs similarity index 89% rename from dlp-api/src/instruction_builder/carry_over_requested_undelegation.rs rename to dlp-api/src/instruction_builder/undelegate_after_request_timeout.rs index c4588bb6..38c19f28 100644 --- a/dlp-api/src/instruction_builder/carry_over_requested_undelegation.rs +++ b/dlp-api/src/instruction_builder/undelegate_after_request_timeout.rs @@ -18,10 +18,10 @@ use solana_sdk_ids::system_program; use crate::compat::{Compatize, Modernize}; -/// Builds a timeout carry-over instruction for a requested undelegation. -/// See [dlp::processor::process_carry_over_requested_undelegation] for docs. +/// Builds a timeout fallback instruction for a requested undelegation. +/// See [dlp::processor::process_undelegate_after_request_timeout] for docs. #[allow(clippy::too_many_arguments)] -pub fn carry_over_requested_undelegation( +pub fn undelegate_after_request_timeout( caller: Pubkey, delegated_account: Pubkey, owner_program: Pubkey, @@ -69,16 +69,16 @@ pub fn carry_over_requested_undelegation( AccountMeta::new(commit_reimbursement, false), AccountMeta::new_readonly(system_program::id(), false), ], - data: DlpDiscriminator::CarryOverRequestedUndelegation.to_vec(), + data: DlpDiscriminator::UndelegateAfterRequestTimeout.to_vec(), } } /// -/// Returns accounts-data-size budget for requested undelegation carry-over. +/// Returns accounts-data-size budget for undelegate-after-request-timeout. /// /// This value can be used with ComputeBudgetInstruction::SetLoadedAccountsDataSizeLimit /// -pub fn carry_over_requested_undelegation_size_budget( +pub fn undelegate_after_request_timeout_size_budget( delegated_account: AccountSizeClass, ) -> u32 { total_size_budget(&[ diff --git a/dlp-api/src/state/undelegation_request.rs b/dlp-api/src/state/undelegation_request.rs index 13433797..45350c81 100644 --- a/dlp-api/src/state/undelegation_request.rs +++ b/dlp-api/src/state/undelegation_request.rs @@ -24,7 +24,7 @@ pub struct UndelegationRequest { /// The slot at which the request was created. pub created_slot: u64, - /// The first slot at which timeout carry-over is allowed. + /// The first slot at which undelegate-after-request-timeout is allowed. pub expires_at_slot: u64, /// Delegation metadata nonce observed when the request was created. diff --git a/src/lib.rs b/src/lib.rs index d8835726..ea84839c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -154,8 +154,8 @@ pub fn fast_process_instruction( program_id, accounts, data, )) } - DlpDiscriminator::CarryOverRequestedUndelegation => { - Some(processor::fast::process_carry_over_requested_undelegation( + DlpDiscriminator::UndelegateAfterRequestTimeout => { + Some(processor::fast::process_undelegate_after_request_timeout( program_id, accounts, data, )) } diff --git a/src/processor/fast/mod.rs b/src/processor/fast/mod.rs index 49600052..208d3511 100644 --- a/src/processor/fast/mod.rs +++ b/src/processor/fast/mod.rs @@ -1,4 +1,3 @@ -mod carry_over_requested_undelegation; mod commit_diff; mod commit_diff_from_buffer; mod commit_finalize; @@ -10,12 +9,12 @@ mod delegate_with_actions; mod finalize; mod request_undelegation; mod undelegate; +mod undelegate_after_request_timeout; mod undelegate_confined_account; mod utils; pub(crate) mod internal; -pub use carry_over_requested_undelegation::*; pub use commit_diff::*; pub use commit_diff_from_buffer::*; pub use commit_finalize::*; @@ -27,6 +26,7 @@ pub use delegate_with_actions::*; pub use finalize::*; pub use request_undelegation::*; pub use undelegate::*; +pub use undelegate_after_request_timeout::*; pub use undelegate_confined_account::*; pub fn to_pinocchio_program_error( diff --git a/src/processor/fast/carry_over_requested_undelegation.rs b/src/processor/fast/undelegate_after_request_timeout.rs similarity index 98% rename from src/processor/fast/carry_over_requested_undelegation.rs rename to src/processor/fast/undelegate_after_request_timeout.rs index 43e33495..8d31ad47 100644 --- a/src/processor/fast/carry_over_requested_undelegation.rs +++ b/src/processor/fast/undelegate_after_request_timeout.rs @@ -54,7 +54,7 @@ use crate::{ /// 10: `[writable]` commit record PDA /// 11: `[writable]` commit reimbursement account /// 12: `[]` system program -pub fn process_carry_over_requested_undelegation( +pub fn process_undelegate_after_request_timeout( _program_id: &Address, accounts: &[AccountView], _data: &[u8], @@ -143,7 +143,7 @@ pub fn process_carry_over_requested_undelegation( delegated_account.assign(owner_program.address()); } } else { - carry_over_with_undelegation_cpi( + undelegate_with_buffer_cpi( caller, delegated_account, owner_program, @@ -214,7 +214,7 @@ fn load_valid_request( Ok(request) } -fn carry_over_with_undelegation_cpi( +fn undelegate_with_buffer_cpi( caller: &AccountView, delegated_account: &AccountView, owner_program: &AccountView, @@ -332,7 +332,7 @@ fn cleanup_pending_commit( return Err(ProgramError::Immutable); } - // Timeout carry-over is a rollback/escape hatch. At this point the + // Request-timeout undelegation is a rollback/escape hatch. At this point the // validator has committed state into the commit PDAs, but that state has // not been finalized into the delegated account. Applying it here would let // this permissionless timeout path accept fresh validator state, which is diff --git a/tests/test_carry_over_requested_undelegation.rs b/tests/test_undelegate_after_request_timeout.rs similarity index 93% rename from tests/test_carry_over_requested_undelegation.rs rename to tests/test_undelegate_after_request_timeout.rs index a487d77c..b2d1b83e 100644 --- a/tests/test_carry_over_requested_undelegation.rs +++ b/tests/test_undelegate_after_request_timeout.rs @@ -32,7 +32,7 @@ use crate::fixtures::{ mod fixtures; #[tokio::test] -async fn test_carry_over_requested_undelegation_after_expiry() { +async fn test_undelegate_after_request_timeout_after_expiry() { let ( banks, caller, @@ -40,7 +40,7 @@ async fn test_carry_over_requested_undelegation_after_expiry() { delegation_rent_payer, _, blockhash, - ) = setup_carry_over_env(false, 0).await; + ) = setup_request_timeout_env(false, 0).await; let request_pda = undelegation_request_pda_from_delegated_account(&DELEGATED_PDA_ID); @@ -82,7 +82,7 @@ async fn test_carry_over_requested_undelegation_after_expiry() { let delegated_before = banks.get_account(DELEGATED_PDA_ID).await.unwrap().unwrap(); - let ix = dlp_api::instruction_builder::carry_over_requested_undelegation( + let ix = dlp_api::instruction_builder::undelegate_after_request_timeout( caller.pubkey(), DELEGATED_PDA_ID, DELEGATED_PDA_OWNER_ID, @@ -140,7 +140,7 @@ async fn test_carry_over_requested_undelegation_after_expiry() { } #[tokio::test] -async fn test_carry_over_requested_undelegation_rejects_before_expiry() { +async fn test_undelegate_after_request_timeout_rejects_before_expiry() { let ( banks, caller, @@ -148,7 +148,7 @@ async fn test_carry_over_requested_undelegation_rejects_before_expiry() { delegation_rent_payer, _, blockhash, - ) = setup_carry_over_env(false, 1_000_000).await; + ) = setup_request_timeout_env(false, 1_000_000).await; let request_pda = undelegation_request_pda_from_delegated_account(&DELEGATED_PDA_ID); @@ -171,7 +171,7 @@ async fn test_carry_over_requested_undelegation_rejects_before_expiry() { let delegated_before = banks.get_account(DELEGATED_PDA_ID).await.unwrap().unwrap(); - let ix = dlp_api::instruction_builder::carry_over_requested_undelegation( + let ix = dlp_api::instruction_builder::undelegate_after_request_timeout( caller.pubkey(), DELEGATED_PDA_ID, DELEGATED_PDA_OWNER_ID, @@ -227,7 +227,7 @@ async fn test_carry_over_requested_undelegation_rejects_before_expiry() { } #[tokio::test] -async fn test_carry_over_requested_undelegation_rejects_stale_request() { +async fn test_undelegate_after_request_timeout_rejects_stale_request() { let ( banks, caller, @@ -235,7 +235,7 @@ async fn test_carry_over_requested_undelegation_rejects_stale_request() { delegation_rent_payer, _, blockhash, - ) = setup_carry_over_env_with_nonces(false, 0, 1, 0).await; + ) = setup_request_timeout_env_with_nonces(false, 0, 1, 0).await; let request_pda = undelegation_request_pda_from_delegated_account(&DELEGATED_PDA_ID); @@ -258,7 +258,7 @@ async fn test_carry_over_requested_undelegation_rejects_stale_request() { let delegated_before = banks.get_account(DELEGATED_PDA_ID).await.unwrap().unwrap(); - let ix = dlp_api::instruction_builder::carry_over_requested_undelegation( + let ix = dlp_api::instruction_builder::undelegate_after_request_timeout( caller.pubkey(), DELEGATED_PDA_ID, DELEGATED_PDA_OWNER_ID, @@ -314,7 +314,7 @@ async fn test_carry_over_requested_undelegation_rejects_stale_request() { } #[tokio::test] -async fn test_carry_over_closes_pending_commit_without_applying_it() { +async fn test_request_timeout_closes_pending_commit_without_applying_it() { let ( banks, caller, @@ -322,7 +322,7 @@ async fn test_carry_over_closes_pending_commit_without_applying_it() { delegation_rent_payer, validator, blockhash, - ) = setup_carry_over_env(true, 0).await; + ) = setup_request_timeout_env(true, 0).await; let commit_state_pda = commit_state_pda_from_delegated_account(&DELEGATED_PDA_ID); @@ -347,7 +347,7 @@ async fn test_carry_over_closes_pending_commit_without_applying_it() { .unwrap() .lamports; - let ix = dlp_api::instruction_builder::carry_over_requested_undelegation( + let ix = dlp_api::instruction_builder::undelegate_after_request_timeout( caller.pubkey(), DELEGATED_PDA_ID, DELEGATED_PDA_OWNER_ID, @@ -390,15 +390,20 @@ async fn test_carry_over_closes_pending_commit_without_applying_it() { ); } -async fn setup_carry_over_env( +async fn setup_request_timeout_env( with_pending_commit: bool, expires_at_slot: u64, ) -> (BanksClient, Keypair, Keypair, Keypair, Keypair, Hash) { - setup_carry_over_env_with_nonces(with_pending_commit, expires_at_slot, 0, 0) - .await + setup_request_timeout_env_with_nonces( + with_pending_commit, + expires_at_slot, + 0, + 0, + ) + .await } -async fn setup_carry_over_env_with_nonces( +async fn setup_request_timeout_env_with_nonces( with_pending_commit: bool, expires_at_slot: u64, delegation_last_update_nonce: u64, From 4c1531e14b910667b8f56efeba1439465234e311 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Thu, 25 Jun 2026 00:19:53 +0530 Subject: [PATCH 09/30] Use requires macros where possible --- src/processor/fast/request_undelegation.rs | 55 +++++---- .../fast/undelegate_after_request_timeout.rs | 109 +++++++++--------- 2 files changed, 83 insertions(+), 81 deletions(-) diff --git a/src/processor/fast/request_undelegation.rs b/src/processor/fast/request_undelegation.rs index d657b0bf..8772e789 100644 --- a/src/processor/fast/request_undelegation.rs +++ b/src/processor/fast/request_undelegation.rs @@ -1,12 +1,11 @@ use pinocchio::{ - address::{address_eq, Address}, + address::Address, cpi::Signer, error::ProgramError, instruction::seeds, sysvars::{clock::Clock, Sysvar}, AccountView, ProgramResult, }; -use pinocchio_log::log; use super::to_pinocchio_program_error; use crate::{ @@ -14,7 +13,7 @@ use crate::{ error::DlpError, pda, processor::{fast::utils::pda::create_pda, utils::curve::is_on_curve_fast}, - require_n_accounts, + require, require_eq_keys, require_ge, require_n_accounts, requires::{ is_uninitialized_account, require_initialized_delegation_metadata, require_initialized_delegation_record, require_owned_pda, require_pda, @@ -50,9 +49,7 @@ pub fn process_request_undelegation( ] = require_n_accounts!(accounts, 7); require_signer(payer, "payer")?; - if !payer.is_writable() { - return Err(ProgramError::Immutable); - } + require!(payer.is_writable(), ProgramError::Immutable); require_signer(delegated_account, "delegated account")?; require_owned_pda( delegated_account, @@ -60,10 +57,10 @@ pub fn process_request_undelegation( "delegated account", )?; - if is_on_curve_fast(delegated_account.address()) { - log!("request undelegation is only supported for off-curve accounts"); - return Err(DlpError::RequestUndelegationOnCurveAccount.into()); - } + require!( + !is_on_curve_fast(delegated_account.address()), + DlpError::RequestUndelegationOnCurveAccount + ); require_initialized_delegation_record( delegated_account, @@ -83,12 +80,11 @@ pub fn process_request_undelegation( ) .map_err(to_pinocchio_program_error)?; - if !address_eq( - &delegation_record.owner.to_bytes().into(), + require_eq_keys!( + &delegation_record.owner, owner_program.address(), - ) { - return Err(ProgramError::InvalidAccountOwner); - } + ProgramError::InvalidAccountOwner + ); let delegation_metadata_data = delegation_metadata_account.try_borrow()?; let delegation_metadata = @@ -133,9 +129,9 @@ pub fn process_request_undelegation( )?; let request = UndelegationRequest { - delegated_account: delegated_account.address().to_bytes().into(), - owner_program: owner_program.address().to_bytes().into(), - rent_payer: payer.address().to_bytes().into(), + delegated_account: *delegated_account.address(), + owner_program: *owner_program.address(), + rent_payer: *payer.address(), created_slot, expires_at_slot, delegation_nonce_at_request: delegation_metadata.last_update_nonce, @@ -168,15 +164,16 @@ pub fn process_request_undelegation( UndelegationRequest::try_from_bytes_with_discriminator(&request_data) .map_err(to_pinocchio_program_error)?; - if !address_eq( - &request.delegated_account.to_bytes().into(), + require_eq_keys!( + &request.delegated_account, delegated_account.address(), - ) || !address_eq( - &request.owner_program.to_bytes().into(), + DlpError::InvalidUndelegationRequest + ); + require_eq_keys!( + &request.owner_program, owner_program.address(), - ) { - return Err(DlpError::InvalidUndelegationRequest.into()); - } + DlpError::InvalidUndelegationRequest + ); Ok(()) } @@ -189,9 +186,11 @@ fn parse_timeout_slots(data: &[u8]) -> Result { data.try_into() .map_err(|_| ProgramError::InvalidInstructionData)?, ); - if timeout_slots < DEFAULT_UNDELEGATION_REQUEST_TIMEOUT_SLOTS { - return Err(DlpError::UndelegationRequestTimeoutTooShort.into()); - } + require_ge!( + timeout_slots, + DEFAULT_UNDELEGATION_REQUEST_TIMEOUT_SLOTS, + DlpError::UndelegationRequestTimeoutTooShort + ); Ok(timeout_slots) } _ => Err(ProgramError::InvalidInstructionData), diff --git a/src/processor/fast/undelegate_after_request_timeout.rs b/src/processor/fast/undelegate_after_request_timeout.rs index 8d31ad47..0bb9da8b 100644 --- a/src/processor/fast/undelegate_after_request_timeout.rs +++ b/src/processor/fast/undelegate_after_request_timeout.rs @@ -1,5 +1,5 @@ use pinocchio::{ - address::{address_eq, Address}, + address::Address, cpi::Signer, error::ProgramError, instruction::seeds, @@ -12,7 +12,7 @@ use crate::{ error::DlpError, pda, processor::fast::utils::pda::{close_pda, create_pda}, - require, require_n_accounts, + require, require_eq, require_eq_keys, require_ge, require_n_accounts, requires::{ is_uninitialized_account, require_initialized_commit_record, require_initialized_commit_state, @@ -63,9 +63,7 @@ pub fn process_undelegate_after_request_timeout( require_n_accounts!(accounts, 13); require_signer(caller, "caller")?; - if !caller.is_writable() { - return Err(ProgramError::Immutable); - } + require!(caller.is_writable(), ProgramError::Immutable); require_owned_pda( delegated_account, &crate::fast::ID, @@ -79,9 +77,12 @@ pub fn process_undelegate_after_request_timeout( undelegation_request_account, request_rent_payer, )?; - if Clock::get()?.slot < request.expires_at_slot { - return Err(DlpError::UndelegationRequestNotExpired.into()); - } + let current_slot = Clock::get()?.slot; + require_ge!( + current_slot, + request.expires_at_slot, + DlpError::UndelegationRequestNotExpired + ); require_initialized_delegation_record( delegated_account, @@ -113,30 +114,26 @@ pub fn process_undelegate_after_request_timeout( (delegation_record.owner, delegation_metadata) }; - if !address_eq(&delegation_owner.to_bytes().into(), owner_program.address()) - { - return Err(ProgramError::InvalidAccountOwner); - } + require_eq_keys!( + &delegation_owner, + owner_program.address(), + ProgramError::InvalidAccountOwner + ); // CHECKPOINT: A timeout request is bound to the delegation nonce observed // when it was created. If undelegation is still desired after the nonce // changes, one possible design is to let request_undelegation refresh an // existing request so it records the current nonce. - if request.delegation_nonce_at_request - != delegation_metadata.last_update_nonce - { - return Err(DlpError::InvalidUndelegationRequest.into()); - } - if !address_eq( - &delegation_metadata.rent_payer.to_bytes().into(), + require_eq!( + request.delegation_nonce_at_request, + delegation_metadata.last_update_nonce, + DlpError::InvalidUndelegationRequest + ); + require_eq_keys!( + &delegation_metadata.rent_payer, delegation_rent_payer.address(), - ) { - return Err( - DlpError::InvalidReimbursementAddressForDelegationRent.into() - ); - } - if !delegation_rent_payer.is_writable() { - return Err(ProgramError::Immutable); - } + DlpError::InvalidReimbursementAddressForDelegationRent + ); + require!(delegation_rent_payer.is_writable(), ProgramError::Immutable); if delegated_account.is_data_empty() { unsafe { @@ -188,28 +185,33 @@ fn load_valid_request( "undelegation request", )?; - if !request_rent_payer.is_writable() { - return Err(ProgramError::Immutable); - } + require!(request_rent_payer.is_writable(), ProgramError::Immutable); let request_data = undelegation_request_account.try_borrow()?; let request = *UndelegationRequest::try_from_bytes_with_discriminator(&request_data) .map_err(to_pinocchio_program_error)?; - if !address_eq( - &request.delegated_account.to_bytes().into(), + require_eq_keys!( + &request.delegated_account, delegated_account.address(), - ) || !address_eq( - &request.owner_program.to_bytes().into(), + DlpError::InvalidUndelegationRequest + ); + require_eq_keys!( + &request.owner_program, owner_program.address(), - ) || !address_eq( - &request.rent_payer.to_bytes().into(), + DlpError::InvalidUndelegationRequest + ); + require_eq_keys!( + &request.rent_payer, request_rent_payer.address(), - ) || request.bump != request_bump - { - return Err(DlpError::InvalidUndelegationRequest.into()); - } + DlpError::InvalidUndelegationRequest + ); + require_eq!( + request.bump, + request_bump, + DlpError::InvalidUndelegationRequest + ); Ok(request) } @@ -295,9 +297,11 @@ fn cleanup_pending_commit( return Ok(()); } - if commit_state_uninitialized != commit_record_uninitialized { - return Err(DlpError::InvalidPendingCommitState.into()); - } + require_eq!( + commit_state_uninitialized, + commit_record_uninitialized, + DlpError::InvalidPendingCommitState + ); require_initialized_commit_state( delegated_account, @@ -317,20 +321,19 @@ fn cleanup_pending_commit( ) .map_err(to_pinocchio_program_error)?; - if !address_eq( - &commit_record.account.to_bytes().into(), + require_eq_keys!( + &commit_record.account, delegated_account.address(), - ) || !address_eq( - &commit_record.identity.to_bytes().into(), + DlpError::InvalidPendingCommitState + ); + require_eq_keys!( + &commit_record.identity, commit_reimbursement.address(), - ) { - return Err(DlpError::InvalidPendingCommitState.into()); - } + DlpError::InvalidPendingCommitState + ); } - if !commit_reimbursement.is_writable() { - return Err(ProgramError::Immutable); - } + require!(commit_reimbursement.is_writable(), ProgramError::Immutable); // Request-timeout undelegation is a rollback/escape hatch. At this point the // validator has committed state into the commit PDAs, but that state has From f87a7e58f4f8fcfaf770bc04577d90b174dd17ee Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Thu, 25 Jun 2026 00:47:22 +0530 Subject: [PATCH 10/30] replace parse_timeout_slots with RequestUndelegationArgs --- Cargo.lock | 23 ++++++++++ dlp-api/Cargo.toml | 1 + dlp-api/src/args/mod.rs | 2 + dlp-api/src/args/request_undelegation.rs | 6 +++ .../request_undelegation.rs | 43 +++++-------------- dlp-api/src/lib.rs | 1 + src/processor/fast/request_undelegation.rs | 39 ++++++++--------- tests/test_request_undelegation.rs | 41 +++++++++++++++--- 8 files changed, 96 insertions(+), 60 deletions(-) create mode 100644 dlp-api/src/args/request_undelegation.rs diff --git a/Cargo.lock b/Cargo.lock index 9c35433d..536485ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3204,6 +3204,7 @@ dependencies = [ "static_assertions", "strum 0.28.0", "thiserror 1.0.69", + "wheels", ] [[package]] @@ -9629,6 +9630,28 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "wheels" +version = "0.1.0" +source = "git+https://github.com/magicblock-labs/magicblock-wheels.git?rev=5b83b56#5b83b5610ccbd58fb0eec79700c322d09594e951" +dependencies = [ + "bytemuck", + "pinocchio 0.10.2", + "pinocchio-log", + "solana-address 2.0.0", + "wheels-macros", +] + +[[package]] +name = "wheels-macros" +version = "0.1.0" +source = "git+https://github.com/magicblock-labs/magicblock-wheels.git?rev=5b83b56#5b83b5610ccbd58fb0eec79700c322d09594e951" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/dlp-api/Cargo.toml b/dlp-api/Cargo.toml index 6fcd5c08..bfeaf9aa 100644 --- a/dlp-api/Cargo.toml +++ b/dlp-api/Cargo.toml @@ -51,6 +51,7 @@ thiserror = { version = ">=1" } serde = { version = "1.0.228", default-features = false, features = ["derive"] } solana-pubkey-compat = { package = "solana-pubkey", version = "2.4", features = ["borsh", "bytemuck", "curve25519"] } +wheels = { git = "https://github.com/magicblock-labs/magicblock-wheels.git", rev = "5b83b56" } [dev-dependencies] rand = { version = "0.8.5", features = ["small_rng"] } diff --git a/dlp-api/src/args/mod.rs b/dlp-api/src/args/mod.rs index 492f02f4..e79fd7c5 100644 --- a/dlp-api/src/args/mod.rs +++ b/dlp-api/src/args/mod.rs @@ -3,6 +3,7 @@ mod commit_state; mod delegate; mod delegate_ephemeral_balance; mod delegate_with_actions; +mod request_undelegation; mod top_up_ephemeral_balance; mod types; mod validator_claim_fees; @@ -13,6 +14,7 @@ pub use commit_state::*; pub use delegate::*; pub use delegate_ephemeral_balance::*; pub use delegate_with_actions::*; +pub use request_undelegation::*; pub use top_up_ephemeral_balance::*; pub use types::*; pub use validator_claim_fees::*; diff --git a/dlp-api/src/args/request_undelegation.rs b/dlp-api/src/args/request_undelegation.rs new file mode 100644 index 00000000..314713f6 --- /dev/null +++ b/dlp-api/src/args/request_undelegation.rs @@ -0,0 +1,6 @@ +use wheels::variable_offset_layout; + +#[variable_offset_layout(buffer_offset = 0)] +pub struct RequestUndelegationArgs { + pub timeout_slots: Option, // number of slots as timeout +} diff --git a/dlp-api/src/instruction_builder/request_undelegation.rs b/dlp-api/src/instruction_builder/request_undelegation.rs index 1a32f088..0bd224eb 100644 --- a/dlp-api/src/instruction_builder/request_undelegation.rs +++ b/dlp-api/src/instruction_builder/request_undelegation.rs @@ -12,8 +12,12 @@ use solana_program::{ pubkey::Pubkey, }; use solana_sdk_ids::system_program; +use wheels::layout::Encodable; -use crate::compat::{Compatize, Modernize}; +use crate::{ + args::RequestUndelegationArgs, + compat::{Compatize, Modernize}, +}; /// Builds a request undelegation instruction. /// See [dlp::processor::process_request_undelegation] for docs. @@ -21,31 +25,7 @@ pub fn request_undelegation( payer: Pubkey, delegated_account: Pubkey, owner_program: Pubkey, -) -> Instruction { - build_request_undelegation(payer, delegated_account, owner_program, None) -} - -/// Builds a request undelegation instruction with a custom timeout. -/// See [dlp::processor::process_request_undelegation] for docs. -pub fn request_undelegation_with_timeout( - payer: Pubkey, - delegated_account: Pubkey, - owner_program: Pubkey, - timeout_slots: u64, -) -> Instruction { - build_request_undelegation( - payer, - delegated_account, - owner_program, - Some(timeout_slots), - ) -} - -fn build_request_undelegation( - payer: Pubkey, - delegated_account: Pubkey, - owner_program: Pubkey, - timeout_slots: Option, + args: RequestUndelegationArgs, ) -> Instruction { let delegated_account_compat = delegated_account.compatize(); let undelegation_request_pda = @@ -61,11 +41,6 @@ fn build_request_undelegation( &delegated_account_compat, ) .modernize(); - let mut data = DlpDiscriminator::RequestUndelegation.to_vec(); - if let Some(timeout_slots) = timeout_slots { - data.extend_from_slice(&timeout_slots.to_le_bytes()); - } - Instruction { program_id: dlp::id().modernize(), accounts: vec![ @@ -77,7 +52,11 @@ fn build_request_undelegation( AccountMeta::new_readonly(delegation_metadata_pda, false), AccountMeta::new_readonly(system_program::id(), false), ], - data, + data: { + let mut data = DlpDiscriminator::RequestUndelegation.to_vec(); + data.extend_from_slice(&args.encode().unwrap()); + data + }, } } diff --git a/dlp-api/src/lib.rs b/dlp-api/src/lib.rs index 01a22b04..f9c0fa07 100644 --- a/dlp-api/src/lib.rs +++ b/dlp-api/src/lib.rs @@ -19,6 +19,7 @@ pub mod requires; pub mod state; pub use account_size_class::*; +pub use wheels; #[cfg(feature = "cpi")] pub mod cpi; diff --git a/src/processor/fast/request_undelegation.rs b/src/processor/fast/request_undelegation.rs index 8772e789..f9990bea 100644 --- a/src/processor/fast/request_undelegation.rs +++ b/src/processor/fast/request_undelegation.rs @@ -1,3 +1,7 @@ +use dlp_api::{ + consts::DEFAULT_UNDELEGATION_REQUEST_TIMEOUT_SLOTS, require_ge, + wheels::layout::Decodable as _, +}; use pinocchio::{ address::Address, cpi::Signer, @@ -9,11 +13,11 @@ use pinocchio::{ use super::to_pinocchio_program_error; use crate::{ - consts::DEFAULT_UNDELEGATION_REQUEST_TIMEOUT_SLOTS, + args::RequestUndelegationArgs, error::DlpError, pda, processor::{fast::utils::pda::create_pda, utils::curve::is_on_curve_fast}, - require, require_eq_keys, require_ge, require_n_accounts, + require, require_eq_keys, require_n_accounts, requires::{ is_uninitialized_account, require_initialized_delegation_metadata, require_initialized_delegation_record, require_owned_pda, require_pda, @@ -100,7 +104,17 @@ pub fn process_request_undelegation( pda::UNDELEGATION_REQUEST_TAG, delegated_account.address().as_ref(), ]; - let timeout_slots = parse_timeout_slots(data)?; + + let timeout_slots = RequestUndelegationArgs::decode(data)? + .timeout_slots() + .unwrap_or(DEFAULT_UNDELEGATION_REQUEST_TIMEOUT_SLOTS as u16) + as u64; + + require_ge!( + timeout_slots, + DEFAULT_UNDELEGATION_REQUEST_TIMEOUT_SLOTS, + DlpError::UndelegationRequestTimeoutTooShort + ); if is_uninitialized_account(undelegation_request_account) { let created_slot = Clock::get()?.slot; @@ -177,22 +191,3 @@ pub fn process_request_undelegation( Ok(()) } - -fn parse_timeout_slots(data: &[u8]) -> Result { - match data.len() { - 0 => Ok(DEFAULT_UNDELEGATION_REQUEST_TIMEOUT_SLOTS), - 8 => { - let timeout_slots = u64::from_le_bytes( - data.try_into() - .map_err(|_| ProgramError::InvalidInstructionData)?, - ); - require_ge!( - timeout_slots, - DEFAULT_UNDELEGATION_REQUEST_TIMEOUT_SLOTS, - DlpError::UndelegationRequestTimeoutTooShort - ); - Ok(timeout_slots) - } - _ => Err(ProgramError::InvalidInstructionData), - } -} diff --git a/tests/test_request_undelegation.rs b/tests/test_request_undelegation.rs index 452b09cd..2ea7799b 100644 --- a/tests/test_request_undelegation.rs +++ b/tests/test_request_undelegation.rs @@ -1,5 +1,6 @@ use dlp::solana_program; use dlp_api::{ + args::RequestUndelegationArgs, consts::DEFAULT_UNDELEGATION_REQUEST_TIMEOUT_SLOTS, error::DlpError, pda::{ @@ -11,6 +12,7 @@ use dlp_api::{ validator_fees_vault_pda_from_validator, }, state::UndelegationRequest, + wheels::layout::Encodable, }; use solana_program::{ account_info::AccountInfo, @@ -200,8 +202,19 @@ async fn test_request_undelegation_rejects_missing_delegated_signer() { ), AccountMeta::new_readonly(system_program::id(), false), ], - data: dlp_api::discriminator::DlpDiscriminator::RequestUndelegation - .to_vec(), + data: { + let mut data = + dlp_api::discriminator::DlpDiscriminator::RequestUndelegation + .to_vec(); + data.extend_from_slice( + &RequestUndelegationArgs { + timeout_slots: None, + } + .encode() + .unwrap(), + ); + data + }, }; let tx = Transaction::new_signed_with_payer( @@ -223,6 +236,9 @@ async fn test_request_undelegation_rejects_on_curve_delegated_account() { payer.pubkey(), delegated_on_curve.pubkey(), system_program::id(), + RequestUndelegationArgs { + timeout_slots: None, + }, ); let tx = Transaction::new_signed_with_payer( &[ix], @@ -333,7 +349,7 @@ async fn test_undelegate_with_malformed_optional_request_accounts_rejected() { ); } -fn process_request_wrapper( +fn imaginary_program_processor_requesting_undelegation_through_cpi( program_id: &Pubkey, accounts: &[AccountInfo], data: &[u8], @@ -353,17 +369,22 @@ fn process_request_wrapper( *payer.key, *delegated_account.key, *program_id, + RequestUndelegationArgs { + timeout_slots: None, + }, ), 8 => { let timeout_slots = u64::from_le_bytes( data.try_into() .map_err(|_| ProgramError::InvalidInstructionData)?, ); - dlp_api::instruction_builder::request_undelegation_with_timeout( + dlp_api::instruction_builder::request_undelegation( *payer.key, *delegated_account.key, *program_id, - timeout_slots, + RequestUndelegationArgs { + timeout_slots: Some(timeout_slots as u16), + }, ) } _ => return Err(ProgramError::InvalidInstructionData), @@ -400,6 +421,12 @@ fn request_undelegation_from_owner_program_with_timeout( ) } +/// +/// Note this instruction invokes an imaginary "owner program" which then calls the DLP program to +/// request undelegatation, which is why "data" doesn't contain any "instruction discriminator" +/// because the imaginary program doesn't require it. See imaginary_program_processor_requesting_undelegation_through_cpi() +/// which is supposed to be the processor of the imaginary program. +/// fn request_undelegation_from_owner_program_with_data( payer: Pubkey, data: Vec, @@ -441,7 +468,9 @@ async fn setup_request_env() -> (BanksClient, Keypair, Keypair, Hash) { program_test.add_program( "request-wrapper", DELEGATED_PDA_OWNER_ID, - processor!(process_request_wrapper), + processor!( + imaginary_program_processor_requesting_undelegation_through_cpi + ), ); program_test.prefer_bpf(true); From 607a7376cd985f541358a04bbb8873c7a30005ed Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Fri, 26 Jun 2026 21:10:44 +0530 Subject: [PATCH 11/30] rename UndelegateAfterRequestTimeout -> UndelegateWithRollbackAfterTimeout and enforce program-authorization, removing permissionless --- dlp-api/src/discriminator.rs | 4 +- dlp-api/src/error.rs | 3 + dlp-api/src/instruction_builder/mod.rs | 4 +- ...undelegate_with_rollback_after_timeout.rs} | 13 +- dlp-api/src/state/delegation_metadata.rs | 17 +- dlp-api/src/state/undelegation_request.rs | 7 +- src/lib.rs | 8 +- src/processor/fast/commit_state.rs | 4 +- src/processor/fast/delegate.rs | 2 +- src/processor/fast/delegate_with_actions.rs | 2 +- src/processor/fast/finalize.rs | 2 +- .../fast/internal/commit_finalize_internal.rs | 2 +- src/processor/fast/mod.rs | 4 +- src/processor/fast/request_undelegation.rs | 25 +- src/processor/fast/undelegate.rs | 10 +- ...undelegate_with_rollback_after_timeout.rs} | 47 ++- tests/fixtures/accounts.rs | 22 +- tests/test_commit_fees_on_undelegation.rs | 28 +- tests/test_finalize.rs | 2 +- tests/test_request_undelegation.rs | 2 +- ...undelegate_with_rollback_after_timeout.rs} | 368 +++++++++++++++--- 21 files changed, 427 insertions(+), 149 deletions(-) rename dlp-api/src/instruction_builder/{undelegate_after_request_timeout.rs => undelegate_with_rollback_after_timeout.rs} (88%) rename src/processor/fast/{undelegate_after_request_timeout.rs => undelegate_with_rollback_after_timeout.rs} (86%) rename tests/{test_undelegate_after_request_timeout.rs => test_undelegate_with_rollback_after_timeout.rs} (60%) diff --git a/dlp-api/src/discriminator.rs b/dlp-api/src/discriminator.rs index 71df4bdd..e4c8d37d 100644 --- a/dlp-api/src/discriminator.rs +++ b/dlp-api/src/discriminator.rs @@ -66,8 +66,8 @@ pub enum DlpDiscriminator { /// See [crate::processor::process_request_undelegation] for docs. RequestUndelegation = 26, - /// See [crate::processor::process_undelegate_after_request_timeout] for docs. - UndelegateAfterRequestTimeout = 27, + /// See [crate::processor::process_undelegate_with_rollback_after_timeout] for docs. + UndelegateWithRollbackAfterTimeout = 27, } impl DlpDiscriminator { diff --git a/dlp-api/src/error.rs b/dlp-api/src/error.rs index 458a370e..2e989a9f 100644 --- a/dlp-api/src/error.rs +++ b/dlp-api/src/error.rs @@ -182,6 +182,9 @@ pub enum DlpError { #[error("Invalid pending commit state for request-timeout undelegation")] InvalidPendingCommitState = 52, + #[error("CommitId didn't match. Re-request to update commitId")] + RollbackCommitIdMismatch = 53, + #[error("An infallible error is encountered possibly due to logic error")] InfallibleError = 100, } diff --git a/dlp-api/src/instruction_builder/mod.rs b/dlp-api/src/instruction_builder/mod.rs index 84b1f70c..c52d5ce5 100644 --- a/dlp-api/src/instruction_builder/mod.rs +++ b/dlp-api/src/instruction_builder/mod.rs @@ -22,8 +22,8 @@ mod request_undelegation; mod top_up_ephemeral_balance; mod types; mod undelegate; -mod undelegate_after_request_timeout; mod undelegate_confined_account; +mod undelegate_with_rollback_after_timeout; mod validator_claim_fees; mod whitelist_validator_for_program; @@ -51,7 +51,7 @@ pub use request_undelegation::*; pub use top_up_ephemeral_balance::*; pub use types::*; pub use undelegate::*; -pub use undelegate_after_request_timeout::*; pub use undelegate_confined_account::*; +pub use undelegate_with_rollback_after_timeout::*; pub use validator_claim_fees::*; pub use whitelist_validator_for_program::*; diff --git a/dlp-api/src/instruction_builder/undelegate_after_request_timeout.rs b/dlp-api/src/instruction_builder/undelegate_with_rollback_after_timeout.rs similarity index 88% rename from dlp-api/src/instruction_builder/undelegate_after_request_timeout.rs rename to dlp-api/src/instruction_builder/undelegate_with_rollback_after_timeout.rs index 38c19f28..a3249a20 100644 --- a/dlp-api/src/instruction_builder/undelegate_after_request_timeout.rs +++ b/dlp-api/src/instruction_builder/undelegate_with_rollback_after_timeout.rs @@ -18,10 +18,11 @@ use solana_sdk_ids::system_program; use crate::compat::{Compatize, Modernize}; -/// Builds a timeout fallback instruction for a requested undelegation. -/// See [dlp::processor::process_undelegate_after_request_timeout] for docs. +/// Builds a request-authorized timeout rollback instruction for a requested +/// undelegation. +/// See [dlp::processor::process_undelegate_with_rollback_after_timeout] for docs. #[allow(clippy::too_many_arguments)] -pub fn undelegate_after_request_timeout( +pub fn undelegate_with_rollback_after_timeout( caller: Pubkey, delegated_account: Pubkey, owner_program: Pubkey, @@ -69,16 +70,16 @@ pub fn undelegate_after_request_timeout( AccountMeta::new(commit_reimbursement, false), AccountMeta::new_readonly(system_program::id(), false), ], - data: DlpDiscriminator::UndelegateAfterRequestTimeout.to_vec(), + data: DlpDiscriminator::UndelegateWithRollbackAfterTimeout.to_vec(), } } /// -/// Returns accounts-data-size budget for undelegate-after-request-timeout. +/// Returns accounts-data-size budget for undelegate-with-rollback-after-timeout. /// /// This value can be used with ComputeBudgetInstruction::SetLoadedAccountsDataSizeLimit /// -pub fn undelegate_after_request_timeout_size_budget( +pub fn undelegate_with_rollback_after_timeout_size_budget( delegated_account: AccountSizeClass, ) -> u32 { total_size_budget(&[ diff --git a/dlp-api/src/state/delegation_metadata.rs b/dlp-api/src/state/delegation_metadata.rs index bc3a70c7..ff67f5b0 100644 --- a/dlp-api/src/state/delegation_metadata.rs +++ b/dlp-api/src/state/delegation_metadata.rs @@ -15,9 +15,8 @@ use crate::{ /// * Everything necessary at cloning time is instead stored in the delegation record. #[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq)] pub struct DelegationMetadata { - /// The last nonce account had during delegation update - /// Deprecated: The last slot at which the delegation was updated - pub last_update_nonce: u64, + /// Latest finalized commit id applied to the delegated account on base. + pub last_commit_id: u64, /// Whether the account can be undelegated or not pub is_undelegatable: bool, /// The seeds of the account, used to reopen it on undelegation @@ -37,7 +36,7 @@ impl<'a> DelegationMetadataFast<'a> { require_ge!( account.data_len(), AccountDiscriminator::SPACE - + 8 // last_update_nonce + + 8 // last_commit_id + 1 // is_undelegatable + 32 // rent_payer + 4, // seeds (at least 4) @@ -49,14 +48,14 @@ impl<'a> DelegationMetadataFast<'a> { }) } - pub fn last_update_nonce(&self) -> u64 { + pub fn last_commit_id(&self) -> u64 { unsafe { ptr::read(self.data.as_ptr().add(AccountDiscriminator::SPACE) as *const u64) } } - pub fn set_last_update_nonce(&mut self, val: u64) { + pub fn set_last_commit_id(&mut self, val: u64) { unsafe { ptr::write( self.data.as_mut_ptr().add(AccountDiscriminator::SPACE) @@ -66,7 +65,7 @@ impl<'a> DelegationMetadataFast<'a> { } } - pub fn replace_last_update_nonce(&mut self, val: u64) -> u64 { + pub fn replace_last_commit_id(&mut self, val: u64) -> u64 { unsafe { ptr::replace( self.data.as_mut_ptr().add(AccountDiscriminator::SPACE) @@ -106,7 +105,7 @@ impl AccountWithDiscriminator for DelegationMetadata { impl DelegationMetadata { pub fn serialized_size(&self) -> usize { AccountDiscriminator::SPACE - + 8 // last_update_nonce (u64) + + 8 // last_commit_id (u64) + 1 // is_undelegatable (bool) + 32 // rent_payer (Pubkey) + (4 + self.seeds.iter().map(|s| 4 + s.len()).sum::()) // seeds (Vec>) @@ -133,7 +132,7 @@ mod tests { ], ], is_undelegatable: false, - last_update_nonce: 0, + last_commit_id: 0, rent_payer: Pubkey::default(), }; diff --git a/dlp-api/src/state/undelegation_request.rs b/dlp-api/src/state/undelegation_request.rs index 45350c81..0ca77b5a 100644 --- a/dlp-api/src/state/undelegation_request.rs +++ b/dlp-api/src/state/undelegation_request.rs @@ -24,11 +24,12 @@ pub struct UndelegationRequest { /// The slot at which the request was created. pub created_slot: u64, - /// The first slot at which undelegate-after-request-timeout is allowed. + /// The first slot at which timeout rollback is allowed. pub expires_at_slot: u64, - /// Delegation metadata nonce observed when the request was created. - pub delegation_nonce_at_request: u64, + /// DelegationMetadata.last_commit_id observed when the request was last + /// made or refreshed. + pub last_commit_id_at_request: u64, /// PDA bump for this request. pub bump: u8, diff --git a/src/lib.rs b/src/lib.rs index ea84839c..b7d3e54c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -154,11 +154,11 @@ pub fn fast_process_instruction( program_id, accounts, data, )) } - DlpDiscriminator::UndelegateAfterRequestTimeout => { - Some(processor::fast::process_undelegate_after_request_timeout( + DlpDiscriminator::UndelegateWithRollbackAfterTimeout => Some( + processor::fast::process_undelegate_with_rollback_after_timeout( program_id, accounts, data, - )) - } + ), + ), _ => None, } } diff --git a/src/processor/fast/commit_state.rs b/src/processor/fast/commit_state.rs index 82495ada..c0c378cb 100644 --- a/src/processor/fast/commit_state.rs +++ b/src/processor/fast/commit_state.rs @@ -157,11 +157,11 @@ pub(crate) fn process_commit_state_internal( .map_err(to_pinocchio_program_error)?; // To preserve correct history of account updates we require sequential commits - if args.commit_record_nonce != delegation_metadata.last_update_nonce + 1 { + if args.commit_record_nonce != delegation_metadata.last_commit_id + 1 { log!( "Nonce {} is incorrect, previous nonce is {}. Rejecting commit", args.commit_record_nonce, - delegation_metadata.last_update_nonce + delegation_metadata.last_commit_id ); return Err(DlpError::NonceOutOfOrder.into()); } diff --git a/src/processor/fast/delegate.rs b/src/processor/fast/delegate.rs index 48d40c7e..46b7b4ba 100644 --- a/src/processor/fast/delegate.rs +++ b/src/processor/fast/delegate.rs @@ -241,7 +241,7 @@ fn process_delegate_inner( let delegation_metadata = DelegationMetadata { seeds: args.seeds, - last_update_nonce: 0, + last_commit_id: 0, is_undelegatable: false, rent_payer: payer.address().to_bytes().into(), }; diff --git a/src/processor/fast/delegate_with_actions.rs b/src/processor/fast/delegate_with_actions.rs index 840099a3..ac734e87 100644 --- a/src/processor/fast/delegate_with_actions.rs +++ b/src/processor/fast/delegate_with_actions.rs @@ -253,7 +253,7 @@ pub fn process_delegate_with_actions( let delegation_metadata = DelegationMetadata { seeds: args.delegate.seeds, - last_update_nonce: 0, + last_commit_id: 0, is_undelegatable: false, rent_payer: payer.address().to_bytes().into(), }; diff --git a/src/processor/fast/finalize.rs b/src/processor/fast/finalize.rs index 1832a4c0..beb364f4 100644 --- a/src/processor/fast/finalize.rs +++ b/src/processor/fast/finalize.rs @@ -161,7 +161,7 @@ pub fn process_finalize( )?; // Update the delegation metadata - delegation_metadata.last_update_nonce = commit_record.nonce; + delegation_metadata.last_commit_id = commit_record.nonce; delegation_metadata .to_bytes_with_discriminator(&mut delegation_metadata_data.as_mut()) .map_err(to_pinocchio_program_error)?; diff --git a/src/processor/fast/internal/commit_finalize_internal.rs b/src/processor/fast/internal/commit_finalize_internal.rs index 07b04006..581858a2 100644 --- a/src/processor/fast/internal/commit_finalize_internal.rs +++ b/src/processor/fast/internal/commit_finalize_internal.rs @@ -83,7 +83,7 @@ pub(crate) fn process_commit_finalize_internal( args.delegation_metadata_account, )?; - let prev_id = metadata.replace_last_update_nonce(args.commit_id); + let prev_id = metadata.replace_last_commit_id(args.commit_id); require_eq!(args.commit_id, prev_id + 1, DlpError::NonceOutOfOrder); diff --git a/src/processor/fast/mod.rs b/src/processor/fast/mod.rs index 208d3511..b01c11a5 100644 --- a/src/processor/fast/mod.rs +++ b/src/processor/fast/mod.rs @@ -9,8 +9,8 @@ mod delegate_with_actions; mod finalize; mod request_undelegation; mod undelegate; -mod undelegate_after_request_timeout; mod undelegate_confined_account; +mod undelegate_with_rollback_after_timeout; mod utils; pub(crate) mod internal; @@ -26,8 +26,8 @@ pub use delegate_with_actions::*; pub use finalize::*; pub use request_undelegation::*; pub use undelegate::*; -pub use undelegate_after_request_timeout::*; pub use undelegate_confined_account::*; +pub use undelegate_with_rollback_after_timeout::*; pub fn to_pinocchio_program_error( error: solana_program::program_error::ProgramError, diff --git a/src/processor/fast/request_undelegation.rs b/src/processor/fast/request_undelegation.rs index f9990bea..7d160d58 100644 --- a/src/processor/fast/request_undelegation.rs +++ b/src/processor/fast/request_undelegation.rs @@ -17,7 +17,7 @@ use crate::{ error::DlpError, pda, processor::{fast::utils::pda::create_pda, utils::curve::is_on_curve_fast}, - require, require_eq_keys, require_n_accounts, + require, require_eq, require_eq_keys, require_n_accounts, requires::{ is_uninitialized_account, require_initialized_delegation_metadata, require_initialized_delegation_record, require_owned_pda, require_pda, @@ -148,7 +148,7 @@ pub fn process_request_undelegation( rent_payer: *payer.address(), created_slot, expires_at_slot, - delegation_nonce_at_request: delegation_metadata.last_update_nonce, + last_commit_id_at_request: delegation_metadata.last_commit_id, bump: request_bump, _padding: [0; 7], }; @@ -160,7 +160,7 @@ pub fn process_request_undelegation( return Ok(()); } - require_pda( + let request_bump = require_pda( undelegation_request_account, request_seeds, &crate::fast::ID, @@ -173,10 +173,11 @@ pub fn process_request_undelegation( "undelegation request", )?; - let request_data = undelegation_request_account.try_borrow()?; - let request = - UndelegationRequest::try_from_bytes_with_discriminator(&request_data) - .map_err(to_pinocchio_program_error)?; + let mut request_data = undelegation_request_account.try_borrow_mut()?; + let request = UndelegationRequest::try_from_bytes_with_discriminator_mut( + &mut request_data, + ) + .map_err(to_pinocchio_program_error)?; require_eq_keys!( &request.delegated_account, @@ -188,6 +189,16 @@ pub fn process_request_undelegation( owner_program.address(), DlpError::InvalidUndelegationRequest ); + require_eq!( + request.bump, + request_bump, + DlpError::InvalidUndelegationRequest + ); + + // Refresh only the base-chain commit checkpoint. Repeated requests must not + // reset or extend the timeout, but they are allowed to make rollback an + // explicit owner-program decision after finalized base state has changed. + request.last_commit_id_at_request = delegation_metadata.last_commit_id; Ok(()) } diff --git a/src/processor/fast/undelegate.rs b/src/processor/fast/undelegate.rs index 16b778a2..e0855a40 100644 --- a/src/processor/fast/undelegate.rs +++ b/src/processor/fast/undelegate.rs @@ -188,7 +188,7 @@ pub fn process_undelegate( &delegation_metadata_data, ) .map_err(to_pinocchio_program_error)?; - let delegation_last_update_nonce = delegation_metadata.last_update_nonce; + let delegation_last_commit_id = delegation_metadata.last_commit_id; // Check if the delegated account is undelegatable if !delegation_metadata.is_undelegatable { @@ -229,7 +229,7 @@ pub fn process_undelegate( rent_reimbursement, fees_vault, validator_fees_vault, - delegation_last_update_nonce, + delegation_last_commit_id, )?; if let Some((undelegation_request_account, request_rent_payer)) = request_accounts @@ -292,7 +292,7 @@ pub fn process_undelegate( rent_reimbursement, fees_vault, validator_fees_vault, - delegation_last_update_nonce, + delegation_last_commit_id, )?; if let Some((undelegation_request_account, request_rent_payer)) = request_accounts @@ -460,9 +460,9 @@ fn process_delegation_cleanup( rent_reimbursement: &AccountView, fees_vault: &AccountView, validator_fees_vault: &AccountView, - delegation_last_update_nonce: u64, + delegation_last_commit_id: u64, ) -> ProgramResult { - let commit_count = delegation_last_update_nonce.saturating_sub(1); + let commit_count = delegation_last_commit_id.saturating_sub(1); let commit_fee = COMMIT_FEE_LAMPORTS .checked_mul(commit_count) .ok_or(DlpError::Overflow)?; diff --git a/src/processor/fast/undelegate_after_request_timeout.rs b/src/processor/fast/undelegate_with_rollback_after_timeout.rs similarity index 86% rename from src/processor/fast/undelegate_after_request_timeout.rs rename to src/processor/fast/undelegate_with_rollback_after_timeout.rs index 0bb9da8b..f5e34f82 100644 --- a/src/processor/fast/undelegate_after_request_timeout.rs +++ b/src/processor/fast/undelegate_with_rollback_after_timeout.rs @@ -26,7 +26,7 @@ use crate::{ }, }; -/// Permissionless timeout path for a requested undelegation. +/// Request-authorized rollback after a requested undelegation times out. /// /// This intentionally returns the currently available base-chain delegated /// account state. It does not accept or apply any pending validator commit. @@ -36,8 +36,15 @@ use crate::{ /// ephemeral validator has newer state that was never finalized on the base /// chain, that state is intentionally not used here and can be lost from the /// returned account's perspective. The safety property is that this path never -/// trusts fresh validator data after timeout; the tradeoff is that Program A -/// gets back only the last base-chain state available in the delegated account. +/// trusts fresh validator data after timeout; the tradeoff is that the owner +/// program gets back only the last base-chain state available in the delegated +/// account. +/// +/// The owner program authorizes the rollback window by signing the +/// request/re-request path for the delegated account. The timed-out rollback is +/// then restricted to the request rent payer so unrelated callers cannot race to +/// execute it. For non-empty accounts, the owner program must also accept the +/// external undelegate CPI that restores the account. /// /// Accounts: /// @@ -54,7 +61,7 @@ use crate::{ /// 10: `[writable]` commit record PDA /// 11: `[writable]` commit reimbursement account /// 12: `[]` system program -pub fn process_undelegate_after_request_timeout( +pub fn process_undelegate_with_rollback_after_timeout( _program_id: &Address, accounts: &[AccountView], _data: &[u8], @@ -77,6 +84,11 @@ pub fn process_undelegate_after_request_timeout( undelegation_request_account, request_rent_payer, )?; + require_eq_keys!( + caller.address(), + request_rent_payer.address(), + DlpError::InvalidUndelegationRequest + ); let current_slot = Clock::get()?.slot; require_ge!( current_slot, @@ -119,14 +131,17 @@ pub fn process_undelegate_after_request_timeout( owner_program.address(), ProgramError::InvalidAccountOwner ); - // CHECKPOINT: A timeout request is bound to the delegation nonce observed - // when it was created. If undelegation is still desired after the nonce - // changes, one possible design is to let request_undelegation refresh an - // existing request so it records the current nonce. + // CHECKPOINT: This does not prove whether unfinalized ER state exists. It + // only proves that no finalized base-chain commit changed the delegated + // account since the owner program requested/refreshed undelegation. When the + // base state has moved, require a fresh owner-program re-request so rollback + // is an explicit decision against the current base-chain state. This + // minimizes, but cannot prevent, discarding newer ER state that has not been + // finalized to base. require_eq!( - request.delegation_nonce_at_request, - delegation_metadata.last_update_nonce, - DlpError::InvalidUndelegationRequest + request.last_commit_id_at_request, + delegation_metadata.last_commit_id, + DlpError::RollbackCommitIdMismatch ); require_eq_keys!( &delegation_metadata.rent_payer, @@ -335,11 +350,11 @@ fn cleanup_pending_commit( require!(commit_reimbursement.is_writable(), ProgramError::Immutable); - // Request-timeout undelegation is a rollback/escape hatch. At this point the - // validator has committed state into the commit PDAs, but that state has - // not been finalized into the delegated account. Applying it here would let - // this permissionless timeout path accept fresh validator state, which is - // exactly what the timeout design forbids. + // Timeout rollback is a request-authorized escape hatch. At this point the + // validator has committed state into the commit PDAs, but that state has not + // been finalized into the delegated account. Applying it here would let this + // rollback path accept fresh validator state, which is exactly what the + // timeout rollback design forbids. // // We still close both commit PDAs so the delegated account cannot leave // orphaned DLP-owned accounts behind. The commit record identity is the diff --git a/tests/fixtures/accounts.rs b/tests/fixtures/accounts.rs index 25c88d5e..fd0d858e 100644 --- a/tests/fixtures/accounts.rs +++ b/tests/fixtures/accounts.rs @@ -134,7 +134,7 @@ pub fn get_delegation_metadata_data( rent_payer: Pubkey, is_undelegatable: Option, ) -> Vec { - get_delegation_metadata_data_with_nonce( + get_delegation_metadata_data_with_commit_id( rent_payer, is_undelegatable, DEFAULT_LAST_UPDATE_EXTERNAL_SLOT, @@ -142,16 +142,16 @@ pub fn get_delegation_metadata_data( } #[allow(dead_code)] -pub fn get_delegation_metadata_data_with_nonce( +pub fn get_delegation_metadata_data_with_commit_id( rent_payer: Pubkey, is_undelegatable: Option, - last_update_nonce: u64, + last_commit_id: u64, ) -> Vec { - create_delegation_metadata_data_with_nonce( + create_delegation_metadata_data_with_commit_id( rent_payer, DEFAULT_SEEDS, is_undelegatable.unwrap_or(DEFAULT_IS_UNDELEGATABLE), - last_update_nonce, + last_commit_id, ) } @@ -160,7 +160,7 @@ pub fn create_delegation_metadata_data( seeds: &[&[u8]], is_undelegatable: bool, ) -> Vec { - create_delegation_metadata_data_with_nonce( + create_delegation_metadata_data_with_commit_id( rent_payer, seeds, is_undelegatable, @@ -169,14 +169,14 @@ pub fn create_delegation_metadata_data( } #[allow(dead_code)] -pub fn create_delegation_metadata_data_with_nonce( +pub fn create_delegation_metadata_data_with_commit_id( rent_payer: Pubkey, seeds: &[&[u8]], is_undelegatable: bool, - last_update_nonce: u64, + last_commit_id: u64, ) -> Vec { let delegation_metadata = DelegationMetadata { - last_update_nonce, + last_commit_id, is_undelegatable, seeds: seeds.iter().map(|s| s.to_vec()).collect(), rent_payer, @@ -242,7 +242,7 @@ pub fn create_undelegation_request_data_with_expiry( rent_payer: Pubkey, created_slot: u64, expires_at_slot: u64, - delegation_nonce_at_request: u64, + last_commit_id_at_request: u64, ) -> Vec { let (_, bump) = Pubkey::find_program_address( &[UNDELEGATION_REQUEST_TAG, delegated_account.as_ref()], @@ -254,7 +254,7 @@ pub fn create_undelegation_request_data_with_expiry( rent_payer, created_slot, expires_at_slot, - delegation_nonce_at_request, + last_commit_id_at_request, bump, _padding: [0; 7], }; diff --git a/tests/test_commit_fees_on_undelegation.rs b/tests/test_commit_fees_on_undelegation.rs index 98caa13e..791fc9dc 100644 --- a/tests/test_commit_fees_on_undelegation.rs +++ b/tests/test_commit_fees_on_undelegation.rs @@ -17,7 +17,7 @@ use solana_sdk::{ use solana_sdk_ids::system_program; use crate::fixtures::{ - create_delegation_metadata_data_with_nonce, get_delegation_record_data, + create_delegation_metadata_data_with_commit_id, get_delegation_record_data, DELEGATED_PDA_ID, DELEGATED_PDA_OWNER_ID, TEST_AUTHORITY, }; @@ -37,12 +37,13 @@ async fn test_commit_fees_on_undelegation() { let delegation_record_data = get_delegation_record_data(validator.pubkey(), None); - let delegation_metadata_data = create_delegation_metadata_data_with_nonce( - validator.pubkey(), - &[], - true, - 101, - ); + let delegation_metadata_data = + create_delegation_metadata_data_with_commit_id( + validator.pubkey(), + &[], + true, + 101, + ); let record_rent = Rent::default().minimum_balance(delegation_record_data.len()); @@ -124,12 +125,13 @@ async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { ); // Setup the delegated account metadata PDA with high nonce - let delegation_metadata_data = create_delegation_metadata_data_with_nonce( - validator.pubkey(), - &[], - true, - 101, - ); + let delegation_metadata_data = + create_delegation_metadata_data_with_commit_id( + validator.pubkey(), + &[], + true, + 101, + ); program_test.add_account( delegation_metadata_pda_from_delegated_account(&DELEGATED_PDA_ID), Account { diff --git a/tests/test_finalize.rs b/tests/test_finalize.rs index 83f6a779..030141e8 100644 --- a/tests/test_finalize.rs +++ b/tests/test_finalize.rs @@ -102,7 +102,7 @@ async fn test_finalize() { &delegation_metadata_account.data, ) .unwrap(); - assert_eq!(commit_record.nonce, delegation_metadata.last_update_nonce); + assert_eq!(commit_record.nonce, delegation_metadata.last_commit_id); } async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { diff --git a/tests/test_request_undelegation.rs b/tests/test_request_undelegation.rs index 2ea7799b..b9fb25d2 100644 --- a/tests/test_request_undelegation.rs +++ b/tests/test_request_undelegation.rs @@ -81,7 +81,7 @@ async fn test_request_undelegation_creates_request() { request.expires_at_slot, request.created_slot + DEFAULT_UNDELEGATION_REQUEST_TIMEOUT_SLOTS ); - assert_eq!(request.delegation_nonce_at_request, 0); + assert_eq!(request.last_commit_id_at_request, 0); } #[tokio::test] diff --git a/tests/test_undelegate_after_request_timeout.rs b/tests/test_undelegate_with_rollback_after_timeout.rs similarity index 60% rename from tests/test_undelegate_after_request_timeout.rs rename to tests/test_undelegate_with_rollback_after_timeout.rs index b2d1b83e..d0931fec 100644 --- a/tests/test_undelegate_after_request_timeout.rs +++ b/tests/test_undelegate_with_rollback_after_timeout.rs @@ -1,5 +1,7 @@ use dlp::solana_program; use dlp_api::{ + args::RequestUndelegationArgs, + consts::EXTERNAL_UNDELEGATE_DISCRIMINATOR, error::DlpError, pda::{ commit_record_pda_from_delegated_account, @@ -9,9 +11,20 @@ use dlp_api::{ undelegation_request_pda_from_delegated_account, }, }; -use solana_program::{hash::Hash, native_token::LAMPORTS_PER_SOL, rent::Rent}; +use solana_program::{ + account_info::AccountInfo, + entrypoint::ProgramResult, + hash::Hash, + instruction::{AccountMeta, Instruction}, + native_token::LAMPORTS_PER_SOL, + program::invoke_signed, + program_error::ProgramError, + pubkey::Pubkey, + rent::Rent, + sysvar::Sysvar, +}; use solana_program_test::{ - read_file, BanksClient, BanksClientError, ProgramTest, + processor, BanksClient, BanksClientError, ProgramTest, }; use solana_sdk::{ account::Account, @@ -20,19 +33,66 @@ use solana_sdk::{ transaction::{Transaction, TransactionError}, }; use solana_sdk_ids::system_program; +use solana_system_interface::instruction as system_instruction; use crate::fixtures::{ create_undelegation_request_data_with_expiry, - get_commit_record_account_data, get_delegation_metadata_data_with_nonce, - get_delegation_record_data, keypair_from_bytes, - COMMIT_NEW_STATE_ACCOUNT_DATA, DELEGATED_PDA, DELEGATED_PDA_ID, - DELEGATED_PDA_OWNER_ID, TEST_AUTHORITY, + get_commit_record_account_data, + get_delegation_metadata_data_with_commit_id, get_delegation_record_data, + keypair_from_bytes, COMMIT_NEW_STATE_ACCOUNT_DATA, DELEGATED_PDA, + DELEGATED_PDA_ID, DELEGATED_PDA_OWNER_ID, TEST_AUTHORITY, }; mod fixtures; +const TEST_PDA_SEED: &[u8] = b"test-pda"; + +#[tokio::test] +async fn test_undelegate_with_rollback_after_timeout_rejects_non_request_payer() +{ + let ( + banks, + caller, + request_rent_payer, + delegation_rent_payer, + _, + blockhash, + ) = setup_request_timeout_env(false, 0).await; + + let ix = + dlp_api::instruction_builder::undelegate_with_rollback_after_timeout( + caller.pubkey(), + DELEGATED_PDA_ID, + DELEGATED_PDA_OWNER_ID, + request_rent_payer.pubkey(), + delegation_rent_payer.pubkey(), + caller.pubkey(), + ); + + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&caller.pubkey()), + &[&caller], + blockhash, + ); + + let err = banks.process_transaction(tx).await.unwrap_err(); + assert!( + matches!( + err, + BanksClientError::TransactionError( + TransactionError::InstructionError( + 0, + InstructionError::Custom(code), + ) + ) if code == DlpError::InvalidUndelegationRequest as u32 + ), + "expected InvalidUndelegationRequest, got {err:?}" + ); +} + #[tokio::test] -async fn test_undelegate_after_request_timeout_after_expiry() { +async fn test_undelegate_with_rollback_after_timeout_after_expiry() { let ( banks, caller, @@ -82,18 +142,16 @@ async fn test_undelegate_after_request_timeout_after_expiry() { let delegated_before = banks.get_account(DELEGATED_PDA_ID).await.unwrap().unwrap(); - let ix = dlp_api::instruction_builder::undelegate_after_request_timeout( - caller.pubkey(), - DELEGATED_PDA_ID, - DELEGATED_PDA_OWNER_ID, + let ix = rollback_ix( + request_rent_payer.pubkey(), request_rent_payer.pubkey(), delegation_rent_payer.pubkey(), - caller.pubkey(), + request_rent_payer.pubkey(), ); let tx = Transaction::new_signed_with_payer( &[ix], Some(&caller.pubkey()), - &[&caller], + &[&caller, &request_rent_payer], blockhash, ); @@ -140,7 +198,7 @@ async fn test_undelegate_after_request_timeout_after_expiry() { } #[tokio::test] -async fn test_undelegate_after_request_timeout_rejects_before_expiry() { +async fn test_undelegate_with_rollback_after_timeout_rejects_before_expiry() { let ( banks, caller, @@ -171,18 +229,16 @@ async fn test_undelegate_after_request_timeout_rejects_before_expiry() { let delegated_before = banks.get_account(DELEGATED_PDA_ID).await.unwrap().unwrap(); - let ix = dlp_api::instruction_builder::undelegate_after_request_timeout( - caller.pubkey(), - DELEGATED_PDA_ID, - DELEGATED_PDA_OWNER_ID, + let ix = rollback_ix( + request_rent_payer.pubkey(), request_rent_payer.pubkey(), delegation_rent_payer.pubkey(), - caller.pubkey(), + request_rent_payer.pubkey(), ); let tx = Transaction::new_signed_with_payer( &[ix], Some(&caller.pubkey()), - &[&caller], + &[&caller, &request_rent_payer], blockhash, ); @@ -227,7 +283,7 @@ async fn test_undelegate_after_request_timeout_rejects_before_expiry() { } #[tokio::test] -async fn test_undelegate_after_request_timeout_rejects_stale_request() { +async fn test_undelegate_with_rollback_after_timeout_rejects_stale_request() { let ( banks, caller, @@ -235,7 +291,7 @@ async fn test_undelegate_after_request_timeout_rejects_stale_request() { delegation_rent_payer, _, blockhash, - ) = setup_request_timeout_env_with_nonces(false, 0, 1, 0).await; + ) = setup_request_timeout_env_with_commit_ids(false, 0, 1, 0).await; let request_pda = undelegation_request_pda_from_delegated_account(&DELEGATED_PDA_ID); @@ -258,18 +314,16 @@ async fn test_undelegate_after_request_timeout_rejects_stale_request() { let delegated_before = banks.get_account(DELEGATED_PDA_ID).await.unwrap().unwrap(); - let ix = dlp_api::instruction_builder::undelegate_after_request_timeout( - caller.pubkey(), - DELEGATED_PDA_ID, - DELEGATED_PDA_OWNER_ID, + let ix = rollback_ix( + request_rent_payer.pubkey(), request_rent_payer.pubkey(), delegation_rent_payer.pubkey(), - caller.pubkey(), + request_rent_payer.pubkey(), ); let tx = Transaction::new_signed_with_payer( &[ix], Some(&caller.pubkey()), - &[&caller], + &[&caller, &request_rent_payer], blockhash, ); @@ -282,7 +336,7 @@ async fn test_undelegate_after_request_timeout_rejects_stale_request() { 0, InstructionError::Custom(code), ) - ) if code == DlpError::InvalidUndelegationRequest as u32 + ) if code == DlpError::RollbackCommitIdMismatch as u32 ), "expected InvalidUndelegationRequest, got {err:?}" ); @@ -347,10 +401,8 @@ async fn test_request_timeout_closes_pending_commit_without_applying_it() { .unwrap() .lamports; - let ix = dlp_api::instruction_builder::undelegate_after_request_timeout( - caller.pubkey(), - DELEGATED_PDA_ID, - DELEGATED_PDA_OWNER_ID, + let ix = rollback_ix( + request_rent_payer.pubkey(), request_rent_payer.pubkey(), delegation_rent_payer.pubkey(), validator.pubkey(), @@ -358,7 +410,7 @@ async fn test_request_timeout_closes_pending_commit_without_applying_it() { let tx = Transaction::new_signed_with_payer( &[ix], Some(&caller.pubkey()), - &[&caller], + &[&caller, &request_rent_payer], blockhash, ); @@ -390,11 +442,75 @@ async fn test_request_timeout_closes_pending_commit_without_applying_it() { ); } +#[tokio::test] +async fn test_re_request_refreshes_commit_id_without_extending_timeout() { + let ( + banks, + caller, + request_rent_payer, + delegation_rent_payer, + _, + blockhash, + ) = setup_request_timeout_env_with_commit_ids(false, 0, 1, 0).await; + + let request_pda = + undelegation_request_pda_from_delegated_account(&DELEGATED_PDA_ID); + let request_before_account = + banks.get_account(request_pda).await.unwrap().unwrap(); + let request_before = + dlp_api::state::UndelegationRequest::try_from_bytes_with_discriminator( + &request_before_account.data, + ) + .unwrap(); + assert_eq!(request_before.last_commit_id_at_request, 0); + + let request_ix = + request_undelegation_from_owner_program(request_rent_payer.pubkey()); + let request_tx = Transaction::new_signed_with_payer( + &[request_ix], + Some(&request_rent_payer.pubkey()), + &[&request_rent_payer], + blockhash, + ); + let res = banks.process_transaction(request_tx).await; + assert!(res.is_ok()); + + let request_after_account = + banks.get_account(request_pda).await.unwrap().unwrap(); + let request_after = + dlp_api::state::UndelegationRequest::try_from_bytes_with_discriminator( + &request_after_account.data, + ) + .unwrap(); + assert_eq!(request_after.last_commit_id_at_request, 1); + assert_eq!(request_after.created_slot, request_before.created_slot); + assert_eq!( + request_after.expires_at_slot, + request_before.expires_at_slot + ); + assert_eq!(request_after.rent_payer, request_rent_payer.pubkey()); + + let rollback_ix = rollback_ix( + request_rent_payer.pubkey(), + request_rent_payer.pubkey(), + delegation_rent_payer.pubkey(), + request_rent_payer.pubkey(), + ); + let rollback_tx = Transaction::new_signed_with_payer( + &[rollback_ix], + Some(&caller.pubkey()), + &[&caller, &request_rent_payer], + blockhash, + ); + let res = banks.process_transaction(rollback_tx).await; + assert!(res.is_ok()); +} + async fn setup_request_timeout_env( with_pending_commit: bool, expires_at_slot: u64, ) -> (BanksClient, Keypair, Keypair, Keypair, Keypair, Hash) { - setup_request_timeout_env_with_nonces( + setup_request_timeout_env_with_commit_ids( with_pending_commit, expires_at_slot, 0, @@ -403,13 +519,21 @@ async fn setup_request_timeout_env( .await } -async fn setup_request_timeout_env_with_nonces( +async fn setup_request_timeout_env_with_commit_ids( with_pending_commit: bool, expires_at_slot: u64, - delegation_last_update_nonce: u64, - request_delegation_nonce: u64, + delegation_last_commit_id: u64, + request_last_commit_id: u64, ) -> (BanksClient, Keypair, Keypair, Keypair, Keypair, Hash) { - let mut program_test = ProgramTest::new("dlp", dlp_api::ID, None); + let mut program_test = ProgramTest::default(); + program_test.prefer_bpf(true); + program_test.add_program("dlp", dlp_api::ID, None); + program_test.prefer_bpf(false); + program_test.add_program( + "rollback-wrapper", + DELEGATED_PDA_OWNER_ID, + processor!(owner_program_processor), + ); program_test.prefer_bpf(true); let caller = Keypair::new(); @@ -425,15 +549,14 @@ async fn setup_request_timeout_env_with_nonces( add_delegation_accounts( &mut program_test, delegation_rent_payer.pubkey(), - delegation_last_update_nonce, + delegation_last_commit_id, ); add_request_account( &mut program_test, request_rent_payer.pubkey(), expires_at_slot, - request_delegation_nonce, + request_last_commit_id, ); - add_owner_program(&mut program_test); if with_pending_commit { add_pending_commit_accounts(&mut program_test, validator.pubkey()); } @@ -481,7 +604,7 @@ fn add_delegated_account(program_test: &mut ProgramTest) { fn add_delegation_accounts( program_test: &mut ProgramTest, delegation_rent_payer: solana_program::pubkey::Pubkey, - last_update_nonce: u64, + last_commit_id: u64, ) { let delegation_record_data = get_delegation_record_data( keypair_from_bytes(&TEST_AUTHORITY).pubkey(), @@ -499,10 +622,10 @@ fn add_delegation_accounts( }, ); - let delegation_metadata_data = get_delegation_metadata_data_with_nonce( + let delegation_metadata_data = get_delegation_metadata_data_with_commit_id( delegation_rent_payer, Some(false), - last_update_nonce, + last_commit_id, ); program_test.add_account( delegation_metadata_pda_from_delegated_account(&DELEGATED_PDA_ID), @@ -521,7 +644,7 @@ fn add_request_account( program_test: &mut ProgramTest, request_rent_payer: solana_program::pubkey::Pubkey, expires_at_slot: u64, - delegation_nonce_at_request: u64, + last_commit_id_at_request: u64, ) { let request_data = create_undelegation_request_data_with_expiry( DELEGATED_PDA_ID, @@ -529,7 +652,7 @@ fn add_request_account( request_rent_payer, 0, expires_at_slot, - delegation_nonce_at_request, + last_commit_id_at_request, ); program_test.add_account( undelegation_request_pda_from_delegated_account(&DELEGATED_PDA_ID), @@ -543,20 +666,6 @@ fn add_request_account( ); } -fn add_owner_program(program_test: &mut ProgramTest) { - let data = read_file("tests/buffers/test_delegation.so"); - program_test.add_account( - DELEGATED_PDA_OWNER_ID, - Account { - lamports: Rent::default().minimum_balance(data.len()).max(1), - data, - owner: solana_sdk::bpf_loader::id(), - executable: true, - rent_epoch: 0, - }, - ); -} - fn add_pending_commit_accounts( program_test: &mut ProgramTest, validator: solana_program::pubkey::Pubkey, @@ -584,3 +693,140 @@ fn add_pending_commit_accounts( }, ); } + +fn rollback_ix( + caller: Pubkey, + request_rent_payer: Pubkey, + delegation_rent_payer: Pubkey, + commit_reimbursement: Pubkey, +) -> Instruction { + dlp_api::instruction_builder::undelegate_with_rollback_after_timeout( + caller, + DELEGATED_PDA_ID, + DELEGATED_PDA_OWNER_ID, + request_rent_payer, + delegation_rent_payer, + commit_reimbursement, + ) +} + +fn request_undelegation_from_owner_program(payer: Pubkey) -> Instruction { + Instruction { + program_id: DELEGATED_PDA_OWNER_ID, + accounts: vec![ + AccountMeta::new(payer, true), + AccountMeta::new(DELEGATED_PDA_ID, false), + AccountMeta::new_readonly(DELEGATED_PDA_OWNER_ID, false), + AccountMeta::new( + undelegation_request_pda_from_delegated_account( + &DELEGATED_PDA_ID, + ), + false, + ), + AccountMeta::new_readonly( + delegation_record_pda_from_delegated_account(&DELEGATED_PDA_ID), + false, + ), + AccountMeta::new_readonly( + delegation_metadata_pda_from_delegated_account( + &DELEGATED_PDA_ID, + ), + false, + ), + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new_readonly(dlp_api::id(), false), + ], + data: vec![1], + } +} + +fn owner_program_processor( + program_id: &Pubkey, + accounts: &[AccountInfo], + data: &[u8], +) -> ProgramResult { + if data.starts_with(&EXTERNAL_UNDELEGATE_DISCRIMINATOR) { + return process_external_undelegate(program_id, accounts); + } + + match data.first().copied() { + Some(1) => process_request_undelegation(program_id, accounts), + _ => Err(ProgramError::InvalidInstructionData), + } +} + +fn process_request_undelegation( + program_id: &Pubkey, + accounts: &[AccountInfo], +) -> ProgramResult { + let [payer, delegated_account, owner_program, request_account, delegation_record_account, delegation_metadata_account, system_program_account, dlp_program] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + if owner_program.key != program_id { + return Err(ProgramError::IncorrectProgramId); + } + + let ix = dlp_api::instruction_builder::request_undelegation( + *payer.key, + *delegated_account.key, + *program_id, + RequestUndelegationArgs { + timeout_slots: None, + }, + ); + let (_, bump) = Pubkey::find_program_address(&[TEST_PDA_SEED], program_id); + let bump_seed = [bump]; + invoke_signed( + &ix, + &[ + payer.clone(), + delegated_account.clone(), + owner_program.clone(), + request_account.clone(), + delegation_record_account.clone(), + delegation_metadata_account.clone(), + system_program_account.clone(), + dlp_program.clone(), + ], + &[&[TEST_PDA_SEED, &bump_seed]], + ) +} + +fn process_external_undelegate( + program_id: &Pubkey, + accounts: &[AccountInfo], +) -> ProgramResult { + let [delegated_account, undelegate_buffer_account, payer, system_program_account] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + let space = undelegate_buffer_account.data_len(); + let rent_lamports = Rent::get()?.minimum_balance(space); + let (_, bump) = Pubkey::find_program_address(&[TEST_PDA_SEED], program_id); + let bump_seed = [bump]; + invoke_signed( + &system_instruction::create_account( + payer.key, + delegated_account.key, + rent_lamports, + space as u64, + program_id, + ), + &[ + payer.clone(), + delegated_account.clone(), + system_program_account.clone(), + ], + &[&[TEST_PDA_SEED, &bump_seed]], + )?; + + let buffer_data = undelegate_buffer_account.try_borrow_data()?; + let mut delegated_data = delegated_account.try_borrow_mut_data()?; + delegated_data.copy_from_slice(&buffer_data); + Ok(()) +} From 28bc3c10c972e25b6cea397ca912c475ac84fe8b Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Fri, 26 Jun 2026 22:38:42 +0530 Subject: [PATCH 12/30] revert unrelated changes regaring name changes --- dlp-api/src/error.rs | 4 +- dlp-api/src/state/delegation_metadata.rs | 14 +++--- dlp-api/src/state/undelegation_request.rs | 4 +- src/processor/fast/commit_state.rs | 4 +- src/processor/fast/delegate.rs | 2 +- src/processor/fast/delegate_with_actions.rs | 2 +- src/processor/fast/finalize.rs | 2 +- .../fast/internal/commit_finalize_internal.rs | 2 +- src/processor/fast/request_undelegation.rs | 5 ++- src/processor/fast/undelegate.rs | 10 ++--- .../undelegate_with_rollback_after_timeout.rs | 6 +-- tests/fixtures/accounts.rs | 22 +++++----- tests/test_commit_fees_on_undelegation.rs | 28 ++++++------ tests/test_finalize.rs | 2 +- tests/test_request_undelegation.rs | 2 +- ..._undelegate_with_rollback_after_timeout.rs | 44 +++++++++---------- 16 files changed, 76 insertions(+), 77 deletions(-) diff --git a/dlp-api/src/error.rs b/dlp-api/src/error.rs index 2e989a9f..c53d7a05 100644 --- a/dlp-api/src/error.rs +++ b/dlp-api/src/error.rs @@ -182,8 +182,8 @@ pub enum DlpError { #[error("Invalid pending commit state for request-timeout undelegation")] InvalidPendingCommitState = 52, - #[error("CommitId didn't match. Re-request to update commitId")] - RollbackCommitIdMismatch = 53, + #[error("Commit nonce did not match. Re-request to update commit nonce")] + RollbackCommitNonceMismatch = 53, #[error("An infallible error is encountered possibly due to logic error")] InfallibleError = 100, diff --git a/dlp-api/src/state/delegation_metadata.rs b/dlp-api/src/state/delegation_metadata.rs index ff67f5b0..3c2e1c47 100644 --- a/dlp-api/src/state/delegation_metadata.rs +++ b/dlp-api/src/state/delegation_metadata.rs @@ -16,7 +16,7 @@ use crate::{ #[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq)] pub struct DelegationMetadata { /// Latest finalized commit id applied to the delegated account on base. - pub last_commit_id: u64, + pub last_update_nonce: u64, /// Whether the account can be undelegated or not pub is_undelegatable: bool, /// The seeds of the account, used to reopen it on undelegation @@ -36,7 +36,7 @@ impl<'a> DelegationMetadataFast<'a> { require_ge!( account.data_len(), AccountDiscriminator::SPACE - + 8 // last_commit_id + + 8 // last_update_nonce + 1 // is_undelegatable + 32 // rent_payer + 4, // seeds (at least 4) @@ -48,14 +48,14 @@ impl<'a> DelegationMetadataFast<'a> { }) } - pub fn last_commit_id(&self) -> u64 { + pub fn last_update_nonce(&self) -> u64 { unsafe { ptr::read(self.data.as_ptr().add(AccountDiscriminator::SPACE) as *const u64) } } - pub fn set_last_commit_id(&mut self, val: u64) { + pub fn set_last_update_nonce(&mut self, val: u64) { unsafe { ptr::write( self.data.as_mut_ptr().add(AccountDiscriminator::SPACE) @@ -65,7 +65,7 @@ impl<'a> DelegationMetadataFast<'a> { } } - pub fn replace_last_commit_id(&mut self, val: u64) -> u64 { + pub fn replace_last_update_nonce(&mut self, val: u64) -> u64 { unsafe { ptr::replace( self.data.as_mut_ptr().add(AccountDiscriminator::SPACE) @@ -105,7 +105,7 @@ impl AccountWithDiscriminator for DelegationMetadata { impl DelegationMetadata { pub fn serialized_size(&self) -> usize { AccountDiscriminator::SPACE - + 8 // last_commit_id (u64) + + 8 // last_update_nonce (u64) + 1 // is_undelegatable (bool) + 32 // rent_payer (Pubkey) + (4 + self.seeds.iter().map(|s| 4 + s.len()).sum::()) // seeds (Vec>) @@ -132,7 +132,7 @@ mod tests { ], ], is_undelegatable: false, - last_commit_id: 0, + last_update_nonce: 0, rent_payer: Pubkey::default(), }; diff --git a/dlp-api/src/state/undelegation_request.rs b/dlp-api/src/state/undelegation_request.rs index 0ca77b5a..ddf20f20 100644 --- a/dlp-api/src/state/undelegation_request.rs +++ b/dlp-api/src/state/undelegation_request.rs @@ -27,9 +27,9 @@ pub struct UndelegationRequest { /// The first slot at which timeout rollback is allowed. pub expires_at_slot: u64, - /// DelegationMetadata.last_commit_id observed when the request was last + /// DelegationMetadata.last_update_nonce observed when the request was last /// made or refreshed. - pub last_commit_id_at_request: u64, + pub last_commit_nonce_at_request: u64, /// PDA bump for this request. pub bump: u8, diff --git a/src/processor/fast/commit_state.rs b/src/processor/fast/commit_state.rs index c0c378cb..82495ada 100644 --- a/src/processor/fast/commit_state.rs +++ b/src/processor/fast/commit_state.rs @@ -157,11 +157,11 @@ pub(crate) fn process_commit_state_internal( .map_err(to_pinocchio_program_error)?; // To preserve correct history of account updates we require sequential commits - if args.commit_record_nonce != delegation_metadata.last_commit_id + 1 { + if args.commit_record_nonce != delegation_metadata.last_update_nonce + 1 { log!( "Nonce {} is incorrect, previous nonce is {}. Rejecting commit", args.commit_record_nonce, - delegation_metadata.last_commit_id + delegation_metadata.last_update_nonce ); return Err(DlpError::NonceOutOfOrder.into()); } diff --git a/src/processor/fast/delegate.rs b/src/processor/fast/delegate.rs index 46b7b4ba..48d40c7e 100644 --- a/src/processor/fast/delegate.rs +++ b/src/processor/fast/delegate.rs @@ -241,7 +241,7 @@ fn process_delegate_inner( let delegation_metadata = DelegationMetadata { seeds: args.seeds, - last_commit_id: 0, + last_update_nonce: 0, is_undelegatable: false, rent_payer: payer.address().to_bytes().into(), }; diff --git a/src/processor/fast/delegate_with_actions.rs b/src/processor/fast/delegate_with_actions.rs index ac734e87..840099a3 100644 --- a/src/processor/fast/delegate_with_actions.rs +++ b/src/processor/fast/delegate_with_actions.rs @@ -253,7 +253,7 @@ pub fn process_delegate_with_actions( let delegation_metadata = DelegationMetadata { seeds: args.delegate.seeds, - last_commit_id: 0, + last_update_nonce: 0, is_undelegatable: false, rent_payer: payer.address().to_bytes().into(), }; diff --git a/src/processor/fast/finalize.rs b/src/processor/fast/finalize.rs index beb364f4..1832a4c0 100644 --- a/src/processor/fast/finalize.rs +++ b/src/processor/fast/finalize.rs @@ -161,7 +161,7 @@ pub fn process_finalize( )?; // Update the delegation metadata - delegation_metadata.last_commit_id = commit_record.nonce; + delegation_metadata.last_update_nonce = commit_record.nonce; delegation_metadata .to_bytes_with_discriminator(&mut delegation_metadata_data.as_mut()) .map_err(to_pinocchio_program_error)?; diff --git a/src/processor/fast/internal/commit_finalize_internal.rs b/src/processor/fast/internal/commit_finalize_internal.rs index 581858a2..07b04006 100644 --- a/src/processor/fast/internal/commit_finalize_internal.rs +++ b/src/processor/fast/internal/commit_finalize_internal.rs @@ -83,7 +83,7 @@ pub(crate) fn process_commit_finalize_internal( args.delegation_metadata_account, )?; - let prev_id = metadata.replace_last_commit_id(args.commit_id); + let prev_id = metadata.replace_last_update_nonce(args.commit_id); require_eq!(args.commit_id, prev_id + 1, DlpError::NonceOutOfOrder); diff --git a/src/processor/fast/request_undelegation.rs b/src/processor/fast/request_undelegation.rs index 7d160d58..41fe9c99 100644 --- a/src/processor/fast/request_undelegation.rs +++ b/src/processor/fast/request_undelegation.rs @@ -148,7 +148,7 @@ pub fn process_request_undelegation( rent_payer: *payer.address(), created_slot, expires_at_slot, - last_commit_id_at_request: delegation_metadata.last_commit_id, + last_commit_nonce_at_request: delegation_metadata.last_update_nonce, bump: request_bump, _padding: [0; 7], }; @@ -198,7 +198,8 @@ pub fn process_request_undelegation( // Refresh only the base-chain commit checkpoint. Repeated requests must not // reset or extend the timeout, but they are allowed to make rollback an // explicit owner-program decision after finalized base state has changed. - request.last_commit_id_at_request = delegation_metadata.last_commit_id; + request.last_commit_nonce_at_request = + delegation_metadata.last_update_nonce; Ok(()) } diff --git a/src/processor/fast/undelegate.rs b/src/processor/fast/undelegate.rs index e0855a40..16b778a2 100644 --- a/src/processor/fast/undelegate.rs +++ b/src/processor/fast/undelegate.rs @@ -188,7 +188,7 @@ pub fn process_undelegate( &delegation_metadata_data, ) .map_err(to_pinocchio_program_error)?; - let delegation_last_commit_id = delegation_metadata.last_commit_id; + let delegation_last_update_nonce = delegation_metadata.last_update_nonce; // Check if the delegated account is undelegatable if !delegation_metadata.is_undelegatable { @@ -229,7 +229,7 @@ pub fn process_undelegate( rent_reimbursement, fees_vault, validator_fees_vault, - delegation_last_commit_id, + delegation_last_update_nonce, )?; if let Some((undelegation_request_account, request_rent_payer)) = request_accounts @@ -292,7 +292,7 @@ pub fn process_undelegate( rent_reimbursement, fees_vault, validator_fees_vault, - delegation_last_commit_id, + delegation_last_update_nonce, )?; if let Some((undelegation_request_account, request_rent_payer)) = request_accounts @@ -460,9 +460,9 @@ fn process_delegation_cleanup( rent_reimbursement: &AccountView, fees_vault: &AccountView, validator_fees_vault: &AccountView, - delegation_last_commit_id: u64, + delegation_last_update_nonce: u64, ) -> ProgramResult { - let commit_count = delegation_last_commit_id.saturating_sub(1); + let commit_count = delegation_last_update_nonce.saturating_sub(1); let commit_fee = COMMIT_FEE_LAMPORTS .checked_mul(commit_count) .ok_or(DlpError::Overflow)?; diff --git a/src/processor/fast/undelegate_with_rollback_after_timeout.rs b/src/processor/fast/undelegate_with_rollback_after_timeout.rs index f5e34f82..8e2ea9db 100644 --- a/src/processor/fast/undelegate_with_rollback_after_timeout.rs +++ b/src/processor/fast/undelegate_with_rollback_after_timeout.rs @@ -139,9 +139,9 @@ pub fn process_undelegate_with_rollback_after_timeout( // minimizes, but cannot prevent, discarding newer ER state that has not been // finalized to base. require_eq!( - request.last_commit_id_at_request, - delegation_metadata.last_commit_id, - DlpError::RollbackCommitIdMismatch + request.last_commit_nonce_at_request, + delegation_metadata.last_update_nonce, + DlpError::RollbackCommitNonceMismatch ); require_eq_keys!( &delegation_metadata.rent_payer, diff --git a/tests/fixtures/accounts.rs b/tests/fixtures/accounts.rs index fd0d858e..e4e34c83 100644 --- a/tests/fixtures/accounts.rs +++ b/tests/fixtures/accounts.rs @@ -134,7 +134,7 @@ pub fn get_delegation_metadata_data( rent_payer: Pubkey, is_undelegatable: Option, ) -> Vec { - get_delegation_metadata_data_with_commit_id( + get_delegation_metadata_data_with_nonce( rent_payer, is_undelegatable, DEFAULT_LAST_UPDATE_EXTERNAL_SLOT, @@ -142,16 +142,16 @@ pub fn get_delegation_metadata_data( } #[allow(dead_code)] -pub fn get_delegation_metadata_data_with_commit_id( +pub fn get_delegation_metadata_data_with_nonce( rent_payer: Pubkey, is_undelegatable: Option, - last_commit_id: u64, + last_update_nonce: u64, ) -> Vec { - create_delegation_metadata_data_with_commit_id( + create_delegation_metadata_data_with_nonce( rent_payer, DEFAULT_SEEDS, is_undelegatable.unwrap_or(DEFAULT_IS_UNDELEGATABLE), - last_commit_id, + last_update_nonce, ) } @@ -160,7 +160,7 @@ pub fn create_delegation_metadata_data( seeds: &[&[u8]], is_undelegatable: bool, ) -> Vec { - create_delegation_metadata_data_with_commit_id( + create_delegation_metadata_data_with_nonce( rent_payer, seeds, is_undelegatable, @@ -169,14 +169,14 @@ pub fn create_delegation_metadata_data( } #[allow(dead_code)] -pub fn create_delegation_metadata_data_with_commit_id( +pub fn create_delegation_metadata_data_with_nonce( rent_payer: Pubkey, seeds: &[&[u8]], is_undelegatable: bool, - last_commit_id: u64, + last_update_nonce: u64, ) -> Vec { let delegation_metadata = DelegationMetadata { - last_commit_id, + last_update_nonce, is_undelegatable, seeds: seeds.iter().map(|s| s.to_vec()).collect(), rent_payer, @@ -242,7 +242,7 @@ pub fn create_undelegation_request_data_with_expiry( rent_payer: Pubkey, created_slot: u64, expires_at_slot: u64, - last_commit_id_at_request: u64, + last_commit_nonce_at_request: u64, ) -> Vec { let (_, bump) = Pubkey::find_program_address( &[UNDELEGATION_REQUEST_TAG, delegated_account.as_ref()], @@ -254,7 +254,7 @@ pub fn create_undelegation_request_data_with_expiry( rent_payer, created_slot, expires_at_slot, - last_commit_id_at_request, + last_commit_nonce_at_request, bump, _padding: [0; 7], }; diff --git a/tests/test_commit_fees_on_undelegation.rs b/tests/test_commit_fees_on_undelegation.rs index 791fc9dc..98caa13e 100644 --- a/tests/test_commit_fees_on_undelegation.rs +++ b/tests/test_commit_fees_on_undelegation.rs @@ -17,7 +17,7 @@ use solana_sdk::{ use solana_sdk_ids::system_program; use crate::fixtures::{ - create_delegation_metadata_data_with_commit_id, get_delegation_record_data, + create_delegation_metadata_data_with_nonce, get_delegation_record_data, DELEGATED_PDA_ID, DELEGATED_PDA_OWNER_ID, TEST_AUTHORITY, }; @@ -37,13 +37,12 @@ async fn test_commit_fees_on_undelegation() { let delegation_record_data = get_delegation_record_data(validator.pubkey(), None); - let delegation_metadata_data = - create_delegation_metadata_data_with_commit_id( - validator.pubkey(), - &[], - true, - 101, - ); + let delegation_metadata_data = create_delegation_metadata_data_with_nonce( + validator.pubkey(), + &[], + true, + 101, + ); let record_rent = Rent::default().minimum_balance(delegation_record_data.len()); @@ -125,13 +124,12 @@ async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { ); // Setup the delegated account metadata PDA with high nonce - let delegation_metadata_data = - create_delegation_metadata_data_with_commit_id( - validator.pubkey(), - &[], - true, - 101, - ); + let delegation_metadata_data = create_delegation_metadata_data_with_nonce( + validator.pubkey(), + &[], + true, + 101, + ); program_test.add_account( delegation_metadata_pda_from_delegated_account(&DELEGATED_PDA_ID), Account { diff --git a/tests/test_finalize.rs b/tests/test_finalize.rs index 030141e8..83f6a779 100644 --- a/tests/test_finalize.rs +++ b/tests/test_finalize.rs @@ -102,7 +102,7 @@ async fn test_finalize() { &delegation_metadata_account.data, ) .unwrap(); - assert_eq!(commit_record.nonce, delegation_metadata.last_commit_id); + assert_eq!(commit_record.nonce, delegation_metadata.last_update_nonce); } async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { diff --git a/tests/test_request_undelegation.rs b/tests/test_request_undelegation.rs index b9fb25d2..3cdc2410 100644 --- a/tests/test_request_undelegation.rs +++ b/tests/test_request_undelegation.rs @@ -81,7 +81,7 @@ async fn test_request_undelegation_creates_request() { request.expires_at_slot, request.created_slot + DEFAULT_UNDELEGATION_REQUEST_TIMEOUT_SLOTS ); - assert_eq!(request.last_commit_id_at_request, 0); + assert_eq!(request.last_commit_nonce_at_request, 0); } #[tokio::test] diff --git a/tests/test_undelegate_with_rollback_after_timeout.rs b/tests/test_undelegate_with_rollback_after_timeout.rs index d0931fec..86e5b66f 100644 --- a/tests/test_undelegate_with_rollback_after_timeout.rs +++ b/tests/test_undelegate_with_rollback_after_timeout.rs @@ -37,10 +37,10 @@ use solana_system_interface::instruction as system_instruction; use crate::fixtures::{ create_undelegation_request_data_with_expiry, - get_commit_record_account_data, - get_delegation_metadata_data_with_commit_id, get_delegation_record_data, - keypair_from_bytes, COMMIT_NEW_STATE_ACCOUNT_DATA, DELEGATED_PDA, - DELEGATED_PDA_ID, DELEGATED_PDA_OWNER_ID, TEST_AUTHORITY, + get_commit_record_account_data, get_delegation_metadata_data_with_nonce, + get_delegation_record_data, keypair_from_bytes, + COMMIT_NEW_STATE_ACCOUNT_DATA, DELEGATED_PDA, DELEGATED_PDA_ID, + DELEGATED_PDA_OWNER_ID, TEST_AUTHORITY, }; mod fixtures; @@ -291,7 +291,7 @@ async fn test_undelegate_with_rollback_after_timeout_rejects_stale_request() { delegation_rent_payer, _, blockhash, - ) = setup_request_timeout_env_with_commit_ids(false, 0, 1, 0).await; + ) = setup_request_timeout_env_with_nonces(false, 0, 1, 0).await; let request_pda = undelegation_request_pda_from_delegated_account(&DELEGATED_PDA_ID); @@ -336,9 +336,9 @@ async fn test_undelegate_with_rollback_after_timeout_rejects_stale_request() { 0, InstructionError::Custom(code), ) - ) if code == DlpError::RollbackCommitIdMismatch as u32 + ) if code == DlpError::RollbackCommitNonceMismatch as u32 ), - "expected InvalidUndelegationRequest, got {err:?}" + "expected RollbackCommitNonceMismatch, got {err:?}" ); assert_eq!( @@ -443,7 +443,7 @@ async fn test_request_timeout_closes_pending_commit_without_applying_it() { } #[tokio::test] -async fn test_re_request_refreshes_commit_id_without_extending_timeout() { +async fn test_re_request_refreshes_commit_nonce_without_extending_timeout() { let ( banks, caller, @@ -451,7 +451,7 @@ async fn test_re_request_refreshes_commit_id_without_extending_timeout() { delegation_rent_payer, _, blockhash, - ) = setup_request_timeout_env_with_commit_ids(false, 0, 1, 0).await; + ) = setup_request_timeout_env_with_nonces(false, 0, 1, 0).await; let request_pda = undelegation_request_pda_from_delegated_account(&DELEGATED_PDA_ID); @@ -462,7 +462,7 @@ async fn test_re_request_refreshes_commit_id_without_extending_timeout() { &request_before_account.data, ) .unwrap(); - assert_eq!(request_before.last_commit_id_at_request, 0); + assert_eq!(request_before.last_commit_nonce_at_request, 0); let request_ix = request_undelegation_from_owner_program(request_rent_payer.pubkey()); @@ -482,7 +482,7 @@ async fn test_re_request_refreshes_commit_id_without_extending_timeout() { &request_after_account.data, ) .unwrap(); - assert_eq!(request_after.last_commit_id_at_request, 1); + assert_eq!(request_after.last_commit_nonce_at_request, 1); assert_eq!(request_after.created_slot, request_before.created_slot); assert_eq!( request_after.expires_at_slot, @@ -510,7 +510,7 @@ async fn setup_request_timeout_env( with_pending_commit: bool, expires_at_slot: u64, ) -> (BanksClient, Keypair, Keypair, Keypair, Keypair, Hash) { - setup_request_timeout_env_with_commit_ids( + setup_request_timeout_env_with_nonces( with_pending_commit, expires_at_slot, 0, @@ -519,11 +519,11 @@ async fn setup_request_timeout_env( .await } -async fn setup_request_timeout_env_with_commit_ids( +async fn setup_request_timeout_env_with_nonces( with_pending_commit: bool, expires_at_slot: u64, - delegation_last_commit_id: u64, - request_last_commit_id: u64, + delegation_last_update_nonce: u64, + request_last_commit_nonce: u64, ) -> (BanksClient, Keypair, Keypair, Keypair, Keypair, Hash) { let mut program_test = ProgramTest::default(); program_test.prefer_bpf(true); @@ -549,13 +549,13 @@ async fn setup_request_timeout_env_with_commit_ids( add_delegation_accounts( &mut program_test, delegation_rent_payer.pubkey(), - delegation_last_commit_id, + delegation_last_update_nonce, ); add_request_account( &mut program_test, request_rent_payer.pubkey(), expires_at_slot, - request_last_commit_id, + request_last_commit_nonce, ); if with_pending_commit { add_pending_commit_accounts(&mut program_test, validator.pubkey()); @@ -604,7 +604,7 @@ fn add_delegated_account(program_test: &mut ProgramTest) { fn add_delegation_accounts( program_test: &mut ProgramTest, delegation_rent_payer: solana_program::pubkey::Pubkey, - last_commit_id: u64, + last_update_nonce: u64, ) { let delegation_record_data = get_delegation_record_data( keypair_from_bytes(&TEST_AUTHORITY).pubkey(), @@ -622,10 +622,10 @@ fn add_delegation_accounts( }, ); - let delegation_metadata_data = get_delegation_metadata_data_with_commit_id( + let delegation_metadata_data = get_delegation_metadata_data_with_nonce( delegation_rent_payer, Some(false), - last_commit_id, + last_update_nonce, ); program_test.add_account( delegation_metadata_pda_from_delegated_account(&DELEGATED_PDA_ID), @@ -644,7 +644,7 @@ fn add_request_account( program_test: &mut ProgramTest, request_rent_payer: solana_program::pubkey::Pubkey, expires_at_slot: u64, - last_commit_id_at_request: u64, + last_commit_nonce_at_request: u64, ) { let request_data = create_undelegation_request_data_with_expiry( DELEGATED_PDA_ID, @@ -652,7 +652,7 @@ fn add_request_account( request_rent_payer, 0, expires_at_slot, - last_commit_id_at_request, + last_commit_nonce_at_request, ); program_test.add_account( undelegation_request_pda_from_delegated_account(&DELEGATED_PDA_ID), From 94497322b1672f296b61c01f39d5856353407804 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Sat, 27 Jun 2026 16:47:33 +0530 Subject: [PATCH 13/30] Remove request undelegation timeout args; timeout not configurable now --- dlp-api/src/args/mod.rs | 2 - dlp-api/src/args/request_undelegation.rs | 6 - .../request_undelegation.rs | 13 +- src/processor/fast/request_undelegation.rs | 21 +-- tests/test_request_undelegation.rs | 123 +++--------------- ..._undelegate_with_rollback_after_timeout.rs | 4 - 6 files changed, 26 insertions(+), 143 deletions(-) delete mode 100644 dlp-api/src/args/request_undelegation.rs diff --git a/dlp-api/src/args/mod.rs b/dlp-api/src/args/mod.rs index e79fd7c5..492f02f4 100644 --- a/dlp-api/src/args/mod.rs +++ b/dlp-api/src/args/mod.rs @@ -3,7 +3,6 @@ mod commit_state; mod delegate; mod delegate_ephemeral_balance; mod delegate_with_actions; -mod request_undelegation; mod top_up_ephemeral_balance; mod types; mod validator_claim_fees; @@ -14,7 +13,6 @@ pub use commit_state::*; pub use delegate::*; pub use delegate_ephemeral_balance::*; pub use delegate_with_actions::*; -pub use request_undelegation::*; pub use top_up_ephemeral_balance::*; pub use types::*; pub use validator_claim_fees::*; diff --git a/dlp-api/src/args/request_undelegation.rs b/dlp-api/src/args/request_undelegation.rs deleted file mode 100644 index 314713f6..00000000 --- a/dlp-api/src/args/request_undelegation.rs +++ /dev/null @@ -1,6 +0,0 @@ -use wheels::variable_offset_layout; - -#[variable_offset_layout(buffer_offset = 0)] -pub struct RequestUndelegationArgs { - pub timeout_slots: Option, // number of slots as timeout -} diff --git a/dlp-api/src/instruction_builder/request_undelegation.rs b/dlp-api/src/instruction_builder/request_undelegation.rs index 0bd224eb..a2e8fa6f 100644 --- a/dlp-api/src/instruction_builder/request_undelegation.rs +++ b/dlp-api/src/instruction_builder/request_undelegation.rs @@ -12,12 +12,8 @@ use solana_program::{ pubkey::Pubkey, }; use solana_sdk_ids::system_program; -use wheels::layout::Encodable; -use crate::{ - args::RequestUndelegationArgs, - compat::{Compatize, Modernize}, -}; +use crate::compat::{Compatize, Modernize}; /// Builds a request undelegation instruction. /// See [dlp::processor::process_request_undelegation] for docs. @@ -25,7 +21,6 @@ pub fn request_undelegation( payer: Pubkey, delegated_account: Pubkey, owner_program: Pubkey, - args: RequestUndelegationArgs, ) -> Instruction { let delegated_account_compat = delegated_account.compatize(); let undelegation_request_pda = @@ -52,11 +47,7 @@ pub fn request_undelegation( AccountMeta::new_readonly(delegation_metadata_pda, false), AccountMeta::new_readonly(system_program::id(), false), ], - data: { - let mut data = DlpDiscriminator::RequestUndelegation.to_vec(); - data.extend_from_slice(&args.encode().unwrap()); - data - }, + data: DlpDiscriminator::RequestUndelegation.to_vec(), } } diff --git a/src/processor/fast/request_undelegation.rs b/src/processor/fast/request_undelegation.rs index 41fe9c99..8e24c0ba 100644 --- a/src/processor/fast/request_undelegation.rs +++ b/src/processor/fast/request_undelegation.rs @@ -1,7 +1,4 @@ -use dlp_api::{ - consts::DEFAULT_UNDELEGATION_REQUEST_TIMEOUT_SLOTS, require_ge, - wheels::layout::Decodable as _, -}; +use dlp_api::consts::DEFAULT_UNDELEGATION_REQUEST_TIMEOUT_SLOTS; use pinocchio::{ address::Address, cpi::Signer, @@ -13,7 +10,6 @@ use pinocchio::{ use super::to_pinocchio_program_error; use crate::{ - args::RequestUndelegationArgs, error::DlpError, pda, processor::{fast::utils::pda::create_pda, utils::curve::is_on_curve_fast}, @@ -42,6 +38,8 @@ pub fn process_request_undelegation( accounts: &[AccountView], data: &[u8], ) -> ProgramResult { + require!(data.is_empty(), ProgramError::InvalidInstructionData); + let [ payer, // force multi-line delegated_account, @@ -105,21 +103,10 @@ pub fn process_request_undelegation( delegated_account.address().as_ref(), ]; - let timeout_slots = RequestUndelegationArgs::decode(data)? - .timeout_slots() - .unwrap_or(DEFAULT_UNDELEGATION_REQUEST_TIMEOUT_SLOTS as u16) - as u64; - - require_ge!( - timeout_slots, - DEFAULT_UNDELEGATION_REQUEST_TIMEOUT_SLOTS, - DlpError::UndelegationRequestTimeoutTooShort - ); - if is_uninitialized_account(undelegation_request_account) { let created_slot = Clock::get()?.slot; let expires_at_slot = created_slot - .checked_add(timeout_slots) + .checked_add(DEFAULT_UNDELEGATION_REQUEST_TIMEOUT_SLOTS) .ok_or(DlpError::Overflow)?; let request_bump = require_uninitialized_pda( diff --git a/tests/test_request_undelegation.rs b/tests/test_request_undelegation.rs index 3cdc2410..924c2168 100644 --- a/tests/test_request_undelegation.rs +++ b/tests/test_request_undelegation.rs @@ -1,8 +1,6 @@ use dlp::solana_program; use dlp_api::{ - args::RequestUndelegationArgs, consts::DEFAULT_UNDELEGATION_REQUEST_TIMEOUT_SLOTS, - error::DlpError, pda::{ commit_record_pda_from_delegated_account, commit_state_pda_from_delegated_account, @@ -12,7 +10,6 @@ use dlp_api::{ validator_fees_vault_pda_from_validator, }, state::UndelegationRequest, - wheels::layout::Encodable, }; use solana_program::{ account_info::AccountInfo, @@ -119,42 +116,11 @@ async fn test_request_undelegation_is_idempotent() { } #[tokio::test] -async fn test_request_undelegation_accepts_custom_timeout() { +async fn test_request_undelegation_rejects_payload() { let (banks, payer, _, blockhash) = setup_request_env().await; - let ix = request_undelegation_from_owner_program_with_timeout( - payer.pubkey(), - 6000, - ); - let tx = Transaction::new_signed_with_payer( - &[ix], - Some(&payer.pubkey()), - &[&payer], - blockhash, - ); - - let res = banks.process_transaction(tx).await; - assert!(res.is_ok()); - - let request_pda = - undelegation_request_pda_from_delegated_account(&DELEGATED_PDA_ID); - let request_account = - banks.get_account(request_pda).await.unwrap().unwrap(); - let request = UndelegationRequest::try_from_bytes_with_discriminator( - &request_account.data, - ) - .unwrap(); - assert_eq!(request.expires_at_slot, request.created_slot + 6000); -} - -#[tokio::test] -async fn test_request_undelegation_rejects_short_timeout() { - let (banks, payer, _, blockhash) = setup_request_env().await; - - let ix = request_undelegation_from_owner_program_with_timeout( - payer.pubkey(), - DEFAULT_UNDELEGATION_REQUEST_TIMEOUT_SLOTS - 1, - ); + let mut ix = request_undelegation_from_owner_program(payer.pubkey()); + ix.data = 123_u64.to_le_bytes().to_vec(); let tx = Transaction::new_signed_with_payer( &[ix], Some(&payer.pubkey()), @@ -169,11 +135,11 @@ async fn test_request_undelegation_rejects_short_timeout() { BanksClientError::TransactionError( TransactionError::InstructionError( 0, - InstructionError::Custom(code), + InstructionError::InvalidInstructionData, ) - ) if code == DlpError::UndelegationRequestTimeoutTooShort as u32 + ) ), - "expected UndelegationRequestTimeoutTooShort, got {err:?}" + "expected request payload to fail with InvalidInstructionData, got {err:?}" ); } @@ -202,19 +168,8 @@ async fn test_request_undelegation_rejects_missing_delegated_signer() { ), AccountMeta::new_readonly(system_program::id(), false), ], - data: { - let mut data = - dlp_api::discriminator::DlpDiscriminator::RequestUndelegation - .to_vec(); - data.extend_from_slice( - &RequestUndelegationArgs { - timeout_slots: None, - } - .encode() - .unwrap(), - ); - data - }, + data: dlp_api::discriminator::DlpDiscriminator::RequestUndelegation + .to_vec(), }; let tx = Transaction::new_signed_with_payer( @@ -236,9 +191,6 @@ async fn test_request_undelegation_rejects_on_curve_delegated_account() { payer.pubkey(), delegated_on_curve.pubkey(), system_program::id(), - RequestUndelegationArgs { - timeout_slots: None, - }, ); let tx = Transaction::new_signed_with_payer( &[ix], @@ -364,31 +316,12 @@ fn imaginary_program_processor_requesting_undelegation_through_cpi( return Err(ProgramError::IncorrectProgramId); } - let ix = match data.len() { - 0 => dlp_api::instruction_builder::request_undelegation( - *payer.key, - *delegated_account.key, - *program_id, - RequestUndelegationArgs { - timeout_slots: None, - }, - ), - 8 => { - let timeout_slots = u64::from_le_bytes( - data.try_into() - .map_err(|_| ProgramError::InvalidInstructionData)?, - ); - dlp_api::instruction_builder::request_undelegation( - *payer.key, - *delegated_account.key, - *program_id, - RequestUndelegationArgs { - timeout_slots: Some(timeout_slots as u16), - }, - ) - } - _ => return Err(ProgramError::InvalidInstructionData), - }; + let mut ix = dlp_api::instruction_builder::request_undelegation( + *payer.key, + *delegated_account.key, + *program_id, + ); + ix.data.extend_from_slice(data); let (_, bump) = Pubkey::find_program_address(&[TEST_PDA_SEED], program_id); let bump_seed = [bump]; invoke_signed( @@ -407,30 +340,14 @@ fn imaginary_program_processor_requesting_undelegation_through_cpi( ) } -fn request_undelegation_from_owner_program(payer: Pubkey) -> Instruction { - request_undelegation_from_owner_program_with_data(payer, vec![]) -} - -fn request_undelegation_from_owner_program_with_timeout( - payer: Pubkey, - timeout_slots: u64, -) -> Instruction { - request_undelegation_from_owner_program_with_data( - payer, - timeout_slots.to_le_bytes().to_vec(), - ) -} - /// -/// Note this instruction invokes an imaginary "owner program" which then calls the DLP program to -/// request undelegatation, which is why "data" doesn't contain any "instruction discriminator" -/// because the imaginary program doesn't require it. See imaginary_program_processor_requesting_undelegation_through_cpi() +/// Note this instruction invokes an imaginary "owner program" which then calls +/// the DLP program to request undelegation, which is why "data" doesn't contain +/// any "instruction discriminator" because the imaginary program doesn't +/// require it. See imaginary_program_processor_requesting_undelegation_through_cpi() /// which is supposed to be the processor of the imaginary program. /// -fn request_undelegation_from_owner_program_with_data( - payer: Pubkey, - data: Vec, -) -> Instruction { +fn request_undelegation_from_owner_program(payer: Pubkey) -> Instruction { Instruction { program_id: DELEGATED_PDA_OWNER_ID, accounts: vec![ @@ -456,7 +373,7 @@ fn request_undelegation_from_owner_program_with_data( AccountMeta::new_readonly(system_program::id(), false), AccountMeta::new_readonly(dlp_api::id(), false), ], - data, + data: vec![], } } diff --git a/tests/test_undelegate_with_rollback_after_timeout.rs b/tests/test_undelegate_with_rollback_after_timeout.rs index 86e5b66f..c788e09f 100644 --- a/tests/test_undelegate_with_rollback_after_timeout.rs +++ b/tests/test_undelegate_with_rollback_after_timeout.rs @@ -1,6 +1,5 @@ use dlp::solana_program; use dlp_api::{ - args::RequestUndelegationArgs, consts::EXTERNAL_UNDELEGATE_DISCRIMINATOR, error::DlpError, pda::{ @@ -773,9 +772,6 @@ fn process_request_undelegation( *payer.key, *delegated_account.key, *program_id, - RequestUndelegationArgs { - timeout_slots: None, - }, ); let (_, bump) = Pubkey::find_program_address(&[TEST_PDA_SEED], program_id); let bump_seed = [bump]; From 23fdbe8d6bb79b12085f8c7e7f50f00ffd949f50 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Sat, 27 Jun 2026 16:55:37 +0530 Subject: [PATCH 14/30] remove wheels --- Cargo.lock | 23 ----------------------- dlp-api/Cargo.toml | 1 - dlp-api/src/lib.rs | 1 - 3 files changed, 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 536485ed..9c35433d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3204,7 +3204,6 @@ dependencies = [ "static_assertions", "strum 0.28.0", "thiserror 1.0.69", - "wheels", ] [[package]] @@ -9630,28 +9629,6 @@ dependencies = [ "rustls-pki-types", ] -[[package]] -name = "wheels" -version = "0.1.0" -source = "git+https://github.com/magicblock-labs/magicblock-wheels.git?rev=5b83b56#5b83b5610ccbd58fb0eec79700c322d09594e951" -dependencies = [ - "bytemuck", - "pinocchio 0.10.2", - "pinocchio-log", - "solana-address 2.0.0", - "wheels-macros", -] - -[[package]] -name = "wheels-macros" -version = "0.1.0" -source = "git+https://github.com/magicblock-labs/magicblock-wheels.git?rev=5b83b56#5b83b5610ccbd58fb0eec79700c322d09594e951" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "winapi" version = "0.3.9" diff --git a/dlp-api/Cargo.toml b/dlp-api/Cargo.toml index bfeaf9aa..6fcd5c08 100644 --- a/dlp-api/Cargo.toml +++ b/dlp-api/Cargo.toml @@ -51,7 +51,6 @@ thiserror = { version = ">=1" } serde = { version = "1.0.228", default-features = false, features = ["derive"] } solana-pubkey-compat = { package = "solana-pubkey", version = "2.4", features = ["borsh", "bytemuck", "curve25519"] } -wheels = { git = "https://github.com/magicblock-labs/magicblock-wheels.git", rev = "5b83b56" } [dev-dependencies] rand = { version = "0.8.5", features = ["small_rng"] } diff --git a/dlp-api/src/lib.rs b/dlp-api/src/lib.rs index f9c0fa07..01a22b04 100644 --- a/dlp-api/src/lib.rs +++ b/dlp-api/src/lib.rs @@ -19,7 +19,6 @@ pub mod requires; pub mod state; pub use account_size_class::*; -pub use wheels; #[cfg(feature = "cpi")] pub mod cpi; From 7f6c793dbcb25b4fcf8dd714bcbcdc202c3384ce Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Sat, 27 Jun 2026 17:44:08 +0530 Subject: [PATCH 15/30] Require owner-program authorization for rollback --- .../undelegate_with_rollback_after_timeout.rs | 24 +-- .../undelegate_with_rollback_after_timeout.rs | 160 +++++------------- ..._undelegate_with_rollback_after_timeout.rs | 142 ++++++++++++---- 3 files changed, 165 insertions(+), 161 deletions(-) diff --git a/dlp-api/src/instruction_builder/undelegate_with_rollback_after_timeout.rs b/dlp-api/src/instruction_builder/undelegate_with_rollback_after_timeout.rs index a3249a20..7eb0917c 100644 --- a/dlp-api/src/instruction_builder/undelegate_with_rollback_after_timeout.rs +++ b/dlp-api/src/instruction_builder/undelegate_with_rollback_after_timeout.rs @@ -5,7 +5,6 @@ use dlp::{ commit_state_pda_from_delegated_account, delegation_metadata_pda_from_delegated_account, delegation_record_pda_from_delegated_account, - undelegate_buffer_pda_from_delegated_account, undelegation_request_pda_from_delegated_account, }, total_size_budget, AccountSizeClass, DLP_PROGRAM_DATA_SIZE_CLASS, @@ -14,26 +13,21 @@ use solana_program::{ instruction::{AccountMeta, Instruction}, pubkey::Pubkey, }; -use solana_sdk_ids::system_program; use crate::compat::{Compatize, Modernize}; -/// Builds a request-authorized timeout rollback instruction for a requested -/// undelegation. +/// Builds an owner-program-authorized timeout rollback instruction for a +/// requested undelegation. /// See [dlp::processor::process_undelegate_with_rollback_after_timeout] for docs. #[allow(clippy::too_many_arguments)] pub fn undelegate_with_rollback_after_timeout( - caller: Pubkey, + request_rent_payer: Pubkey, delegated_account: Pubkey, owner_program: Pubkey, - request_rent_payer: Pubkey, delegation_rent_payer: Pubkey, commit_reimbursement: Pubkey, ) -> Instruction { let delegated_account_compat = delegated_account.compatize(); - let undelegate_buffer_pda = - undelegate_buffer_pda_from_delegated_account(&delegated_account_compat) - .modernize(); let request_pda = undelegation_request_pda_from_delegated_account( &delegated_account_compat, ) @@ -56,19 +50,16 @@ pub fn undelegate_with_rollback_after_timeout( Instruction { program_id: dlp::id().modernize(), accounts: vec![ - AccountMeta::new(caller, true), - AccountMeta::new(delegated_account, false), + AccountMeta::new(request_rent_payer, false), + AccountMeta::new(delegated_account, true), AccountMeta::new_readonly(owner_program, false), - AccountMeta::new(undelegate_buffer_pda, false), AccountMeta::new(request_pda, false), AccountMeta::new(delegation_record_pda, false), AccountMeta::new(delegation_metadata_pda, false), - AccountMeta::new(request_rent_payer, false), AccountMeta::new(delegation_rent_payer, false), AccountMeta::new(commit_state_pda, false), AccountMeta::new(commit_record_pda, false), AccountMeta::new(commit_reimbursement, false), - AccountMeta::new_readonly(system_program::id(), false), ], data: DlpDiscriminator::UndelegateWithRollbackAfterTimeout.to_vec(), } @@ -84,18 +75,15 @@ pub fn undelegate_with_rollback_after_timeout_size_budget( ) -> u32 { total_size_budget(&[ DLP_PROGRAM_DATA_SIZE_CLASS, - AccountSizeClass::Tiny, // caller + AccountSizeClass::Tiny, // request_rent_payer delegated_account, // delegated_account AccountSizeClass::Tiny, // owner_program - delegated_account, // undelegate_buffer_pda AccountSizeClass::Tiny, // undelegation_request_pda AccountSizeClass::Tiny, // delegation_record_pda AccountSizeClass::Tiny, // delegation_metadata_pda - AccountSizeClass::Tiny, // request_rent_payer AccountSizeClass::Tiny, // delegation_rent_payer delegated_account, // commit_state_pda AccountSizeClass::Tiny, // commit_record_pda AccountSizeClass::Tiny, // commit_reimbursement - AccountSizeClass::Tiny, // system_program ]) } diff --git a/src/processor/fast/undelegate_with_rollback_after_timeout.rs b/src/processor/fast/undelegate_with_rollback_after_timeout.rs index 8e2ea9db..3bf69aae 100644 --- a/src/processor/fast/undelegate_with_rollback_after_timeout.rs +++ b/src/processor/fast/undelegate_with_rollback_after_timeout.rs @@ -1,17 +1,15 @@ use pinocchio::{ address::Address, - cpi::Signer, error::ProgramError, - instruction::seeds, sysvars::{clock::Clock, Sysvar}, AccountView, ProgramResult, }; -use super::{process_undelegation_with_cpi, to_pinocchio_program_error}; +use super::to_pinocchio_program_error; use crate::{ error::DlpError, pda, - processor::fast::utils::pda::{close_pda, create_pda}, + processor::fast::utils::pda::close_pda, require, require_eq, require_eq_keys, require_ge, require_n_accounts, requires::{ is_uninitialized_account, require_initialized_commit_record, @@ -19,58 +17,60 @@ use crate::{ require_initialized_delegation_metadata, require_initialized_delegation_record, require_initialized_pda, require_owned_pda, require_pda, require_signer, - require_uninitialized_pda, UndelegateBufferCtx, }, state::{ CommitRecord, DelegationMetadata, DelegationRecord, UndelegationRequest, }, }; -/// Request-authorized rollback after a requested undelegation times out. +/// Owner-program-authorized rollback after a requested undelegation times out. /// -/// This intentionally returns the currently available base-chain delegated -/// account state. It does not accept or apply any pending validator commit. +/// This returns the currently available base-chain delegated +/// account state. It does not accept or apply any pending validator +/// commit, which means it might cause data-loss! /// -/// Data-loss warning: -/// this is a rollback/escape hatch for validator non-response. If the -/// ephemeral validator has newer state that was never finalized on the base -/// chain, that state is intentionally not used here and can be lost from the -/// returned account's perspective. The safety property is that this path never -/// trusts fresh validator data after timeout; the tradeoff is that the owner -/// program gets back only the last base-chain state available in the delegated -/// account. +/// Data-loss Warning: /// -/// The owner program authorizes the rollback window by signing the -/// request/re-request path for the delegated account. The timed-out rollback is -/// then restricted to the request rent payer so unrelated callers cannot race to -/// execute it. For non-empty accounts, the owner program must also accept the -/// external undelegate CPI that restores the account. +/// This is a rollback/escape hatch for validator non-response. If the ephemeral +/// validator has newer state that was never finalized on the base chain, that +/// state can be lost from the returned account's perspective. The tradeoff +/// is that the owner program gets the account back with only the last base-chain +/// state available in the delegated account. +/// +/// Authorization: +/// +/// The owner program authorizes rollback by invoking this instruction through +/// CPI and signing for the delegated account, same as the request/re-request +/// path. The request rent payer is only the recorded request rent recipient; +/// the rollback authority is the delegated-account signer. +/// +/// This instruction releases the delegated account back to the owner program +/// without making an external undelegate CPI, because the owner program is +/// already on the invocation stack. The owner-program wrapper must preserve the +/// delegated account data before invoking DLP and restore it after DLP returns. /// /// Accounts: /// -/// 0: `[signer, writable]` caller -/// 1: `[writable]` delegated account +/// 0: `[writable]` request rent payer +/// 1: `[signer, writable]` delegated account /// 2: `[]` owner program of the delegated account -/// 3: `[writable]` undelegate buffer PDA -/// 4: `[writable]` undelegation request PDA -/// 5: `[writable]` delegation record PDA -/// 6: `[writable]` delegation metadata PDA -/// 7: `[writable]` request rent payer -/// 8: `[writable]` delegation rent payer -/// 9: `[writable]` commit state PDA -/// 10: `[writable]` commit record PDA -/// 11: `[writable]` commit reimbursement account -/// 12: `[]` system program +/// 3: `[writable]` undelegation request PDA +/// 4: `[writable]` delegation record PDA +/// 5: `[writable]` delegation metadata PDA +/// 6: `[writable]` delegation rent payer +/// 7: `[writable]` commit state PDA +/// 8: `[writable]` commit record PDA +/// 9: `[writable]` commit reimbursement account pub fn process_undelegate_with_rollback_after_timeout( _program_id: &Address, accounts: &[AccountView], _data: &[u8], ) -> ProgramResult { - let [caller, delegated_account, owner_program, undelegate_buffer_account, undelegation_request_account, delegation_record_account, delegation_metadata_account, request_rent_payer, delegation_rent_payer, commit_state_account, commit_record_account, commit_reimbursement, system_program] = - require_n_accounts!(accounts, 13); + let [request_rent_payer, delegated_account, owner_program, undelegation_request_account, delegation_record_account, delegation_metadata_account, delegation_rent_payer, commit_state_account, commit_record_account, commit_reimbursement] = + require_n_accounts!(accounts, 10); - require_signer(caller, "caller")?; - require!(caller.is_writable(), ProgramError::Immutable); + require!(request_rent_payer.is_writable(), ProgramError::Immutable); + require_signer(delegated_account, "delegated account")?; require_owned_pda( delegated_account, &crate::fast::ID, @@ -84,11 +84,6 @@ pub fn process_undelegate_with_rollback_after_timeout( undelegation_request_account, request_rent_payer, )?; - require_eq_keys!( - caller.address(), - request_rent_payer.address(), - DlpError::InvalidUndelegationRequest - ); let current_slot = Clock::get()?.slot; require_ge!( current_slot, @@ -150,21 +145,6 @@ pub fn process_undelegate_with_rollback_after_timeout( ); require!(delegation_rent_payer.is_writable(), ProgramError::Immutable); - if delegated_account.is_data_empty() { - unsafe { - delegated_account.assign(owner_program.address()); - } - } else { - undelegate_with_buffer_cpi( - caller, - delegated_account, - owner_program, - undelegate_buffer_account, - delegation_metadata, - system_program, - )?; - } - // If a validator started a commit but did not finish finalizing it before // timeout, the commit PDAs are cleanup-only inputs. Do not move their data // into the delegated account. That would turn this rollback path into a @@ -176,6 +156,11 @@ pub fn process_undelegate_with_rollback_after_timeout( commit_reimbursement, )?; + delegated_account.resize(0)?; + unsafe { + delegated_account.assign(owner_program.address()); + } + close_pda(undelegation_request_account, request_rent_payer)?; close_pda(delegation_record_account, delegation_rent_payer)?; close_pda(delegation_metadata_account, delegation_rent_payer)?; @@ -231,57 +216,6 @@ fn load_valid_request( Ok(request) } -fn undelegate_with_buffer_cpi( - caller: &AccountView, - delegated_account: &AccountView, - owner_program: &AccountView, - undelegate_buffer_account: &AccountView, - delegation_metadata: DelegationMetadata, - system_program: &AccountView, -) -> ProgramResult { - let undelegate_buffer_bump: u8 = require_uninitialized_pda( - undelegate_buffer_account, - &[ - pda::UNDELEGATE_BUFFER_TAG, - delegated_account.address().as_ref(), - ], - &crate::fast::ID, - true, - UndelegateBufferCtx, - )?; - - create_pda( - undelegate_buffer_account, - &crate::fast::ID, - delegated_account.data_len(), - &[Signer::from(&seeds!( - pda::UNDELEGATE_BUFFER_TAG, - delegated_account.address().as_ref(), - &[undelegate_buffer_bump] - ))], - caller, - )?; - - (*undelegate_buffer_account.try_borrow_mut()?) - .copy_from_slice(&delegated_account.try_borrow()?); - - process_undelegation_with_cpi( - caller, - delegated_account, - owner_program, - undelegate_buffer_account, - &[Signer::from(&seeds!( - pda::UNDELEGATE_BUFFER_TAG, - delegated_account.address().as_ref(), - &[undelegate_buffer_bump] - ))], - delegation_metadata, - system_program, - )?; - - close_pda(undelegate_buffer_account, caller) -} - fn cleanup_pending_commit( delegated_account: &AccountView, commit_state_account: &AccountView, @@ -350,11 +284,11 @@ fn cleanup_pending_commit( require!(commit_reimbursement.is_writable(), ProgramError::Immutable); - // Timeout rollback is a request-authorized escape hatch. At this point the - // validator has committed state into the commit PDAs, but that state has not - // been finalized into the delegated account. Applying it here would let this - // rollback path accept fresh validator state, which is exactly what the - // timeout rollback design forbids. + // Timeout rollback is an owner-program-authorized escape hatch. At this + // point the validator has committed state into the commit PDAs, but that + // state has not been finalized into the delegated account. Applying it here + // would let this rollback path accept fresh validator state, which is + // exactly what the timeout rollback design forbids. // // We still close both commit PDAs so the delegated account cannot leave // orphaned DLP-owned accounts behind. The commit record identity is the diff --git a/tests/test_undelegate_with_rollback_after_timeout.rs b/tests/test_undelegate_with_rollback_after_timeout.rs index c788e09f..2665f341 100644 --- a/tests/test_undelegate_with_rollback_after_timeout.rs +++ b/tests/test_undelegate_with_rollback_after_timeout.rs @@ -47,26 +47,22 @@ mod fixtures; const TEST_PDA_SEED: &[u8] = b"test-pda"; #[tokio::test] -async fn test_undelegate_with_rollback_after_timeout_rejects_non_request_payer() -{ +async fn test_undelegate_with_rollback_after_timeout_rejects_wrong_request_payer( +) { let ( banks, caller, - request_rent_payer, + _request_rent_payer, delegation_rent_payer, _, blockhash, ) = setup_request_timeout_env(false, 0).await; - let ix = - dlp_api::instruction_builder::undelegate_with_rollback_after_timeout( - caller.pubkey(), - DELEGATED_PDA_ID, - DELEGATED_PDA_OWNER_ID, - request_rent_payer.pubkey(), - delegation_rent_payer.pubkey(), - caller.pubkey(), - ); + let ix = rollback_from_owner_program( + caller.pubkey(), + delegation_rent_payer.pubkey(), + caller.pubkey(), + ); let tx = Transaction::new_signed_with_payer( &[ix], @@ -141,8 +137,7 @@ async fn test_undelegate_with_rollback_after_timeout_after_expiry() { let delegated_before = banks.get_account(DELEGATED_PDA_ID).await.unwrap().unwrap(); - let ix = rollback_ix( - request_rent_payer.pubkey(), + let ix = rollback_from_owner_program( request_rent_payer.pubkey(), delegation_rent_payer.pubkey(), request_rent_payer.pubkey(), @@ -150,7 +145,7 @@ async fn test_undelegate_with_rollback_after_timeout_after_expiry() { let tx = Transaction::new_signed_with_payer( &[ix], Some(&caller.pubkey()), - &[&caller, &request_rent_payer], + &[&caller], blockhash, ); @@ -228,8 +223,7 @@ async fn test_undelegate_with_rollback_after_timeout_rejects_before_expiry() { let delegated_before = banks.get_account(DELEGATED_PDA_ID).await.unwrap().unwrap(); - let ix = rollback_ix( - request_rent_payer.pubkey(), + let ix = rollback_from_owner_program( request_rent_payer.pubkey(), delegation_rent_payer.pubkey(), request_rent_payer.pubkey(), @@ -237,7 +231,7 @@ async fn test_undelegate_with_rollback_after_timeout_rejects_before_expiry() { let tx = Transaction::new_signed_with_payer( &[ix], Some(&caller.pubkey()), - &[&caller, &request_rent_payer], + &[&caller], blockhash, ); @@ -313,8 +307,7 @@ async fn test_undelegate_with_rollback_after_timeout_rejects_stale_request() { let delegated_before = banks.get_account(DELEGATED_PDA_ID).await.unwrap().unwrap(); - let ix = rollback_ix( - request_rent_payer.pubkey(), + let ix = rollback_from_owner_program( request_rent_payer.pubkey(), delegation_rent_payer.pubkey(), request_rent_payer.pubkey(), @@ -322,7 +315,7 @@ async fn test_undelegate_with_rollback_after_timeout_rejects_stale_request() { let tx = Transaction::new_signed_with_payer( &[ix], Some(&caller.pubkey()), - &[&caller, &request_rent_payer], + &[&caller], blockhash, ); @@ -400,8 +393,7 @@ async fn test_request_timeout_closes_pending_commit_without_applying_it() { .unwrap() .lamports; - let ix = rollback_ix( - request_rent_payer.pubkey(), + let ix = rollback_from_owner_program( request_rent_payer.pubkey(), delegation_rent_payer.pubkey(), validator.pubkey(), @@ -409,7 +401,7 @@ async fn test_request_timeout_closes_pending_commit_without_applying_it() { let tx = Transaction::new_signed_with_payer( &[ix], Some(&caller.pubkey()), - &[&caller, &request_rent_payer], + &[&caller], blockhash, ); @@ -489,8 +481,7 @@ async fn test_re_request_refreshes_commit_nonce_without_extending_timeout() { ); assert_eq!(request_after.rent_payer, request_rent_payer.pubkey()); - let rollback_ix = rollback_ix( - request_rent_payer.pubkey(), + let rollback_ix = rollback_from_owner_program( request_rent_payer.pubkey(), delegation_rent_payer.pubkey(), request_rent_payer.pubkey(), @@ -498,7 +489,7 @@ async fn test_re_request_refreshes_commit_nonce_without_extending_timeout() { let rollback_tx = Transaction::new_signed_with_payer( &[rollback_ix], Some(&caller.pubkey()), - &[&caller, &request_rent_payer], + &[&caller], blockhash, ); let res = banks.process_transaction(rollback_tx).await; @@ -693,17 +684,58 @@ fn add_pending_commit_accounts( ); } +fn rollback_from_owner_program( + request_rent_payer: Pubkey, + delegation_rent_payer: Pubkey, + commit_reimbursement: Pubkey, +) -> Instruction { + Instruction { + program_id: DELEGATED_PDA_OWNER_ID, + accounts: vec![ + AccountMeta::new(request_rent_payer, false), + AccountMeta::new(DELEGATED_PDA_ID, false), + AccountMeta::new_readonly(DELEGATED_PDA_OWNER_ID, false), + AccountMeta::new( + undelegation_request_pda_from_delegated_account( + &DELEGATED_PDA_ID, + ), + false, + ), + AccountMeta::new( + delegation_record_pda_from_delegated_account(&DELEGATED_PDA_ID), + false, + ), + AccountMeta::new( + delegation_metadata_pda_from_delegated_account( + &DELEGATED_PDA_ID, + ), + false, + ), + AccountMeta::new(delegation_rent_payer, false), + AccountMeta::new( + commit_state_pda_from_delegated_account(&DELEGATED_PDA_ID), + false, + ), + AccountMeta::new( + commit_record_pda_from_delegated_account(&DELEGATED_PDA_ID), + false, + ), + AccountMeta::new(commit_reimbursement, false), + AccountMeta::new_readonly(dlp_api::id(), false), + ], + data: vec![0], + } +} + fn rollback_ix( - caller: Pubkey, request_rent_payer: Pubkey, delegation_rent_payer: Pubkey, commit_reimbursement: Pubkey, ) -> Instruction { dlp_api::instruction_builder::undelegate_with_rollback_after_timeout( - caller, + request_rent_payer, DELEGATED_PDA_ID, DELEGATED_PDA_OWNER_ID, - request_rent_payer, delegation_rent_payer, commit_reimbursement, ) @@ -749,11 +781,61 @@ fn owner_program_processor( } match data.first().copied() { + Some(0) => process_rollback(program_id, accounts), Some(1) => process_request_undelegation(program_id, accounts), _ => Err(ProgramError::InvalidInstructionData), } } +fn process_rollback( + program_id: &Pubkey, + accounts: &[AccountInfo], +) -> ProgramResult { + let [request_rent_payer, delegated_account, owner_program, request_account, delegation_record_account, delegation_metadata_account, delegation_rent_payer, commit_state_account, commit_record_account, commit_reimbursement, dlp_program] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + if owner_program.key != program_id { + return Err(ProgramError::IncorrectProgramId); + } + + let delegated_data = delegated_account.try_borrow_data()?.to_vec(); + + let ix = rollback_ix( + *request_rent_payer.key, + *delegation_rent_payer.key, + *commit_reimbursement.key, + ); + let (_, bump) = Pubkey::find_program_address(&[TEST_PDA_SEED], program_id); + let bump_seed = [bump]; + invoke_signed( + &ix, + &[ + request_rent_payer.clone(), + delegated_account.clone(), + owner_program.clone(), + request_account.clone(), + delegation_record_account.clone(), + delegation_metadata_account.clone(), + delegation_rent_payer.clone(), + commit_state_account.clone(), + commit_record_account.clone(), + commit_reimbursement.clone(), + dlp_program.clone(), + ], + &[&[TEST_PDA_SEED, &bump_seed]], + )?; + + delegated_account.resize(delegated_data.len())?; + delegated_account + .try_borrow_mut_data()? + .copy_from_slice(&delegated_data); + + Ok(()) +} + fn process_request_undelegation( program_id: &Pubkey, accounts: &[AccountInfo], From fb47f7c30efc84d8c9217b5df149e87f64f6ffde Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Sat, 27 Jun 2026 19:55:14 +0530 Subject: [PATCH 16/30] remove is_writable checks --- .../fast/undelegate_with_rollback_after_timeout.rs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/processor/fast/undelegate_with_rollback_after_timeout.rs b/src/processor/fast/undelegate_with_rollback_after_timeout.rs index 3bf69aae..48e0251b 100644 --- a/src/processor/fast/undelegate_with_rollback_after_timeout.rs +++ b/src/processor/fast/undelegate_with_rollback_after_timeout.rs @@ -10,7 +10,7 @@ use crate::{ error::DlpError, pda, processor::fast::utils::pda::close_pda, - require, require_eq, require_eq_keys, require_ge, require_n_accounts, + require_eq, require_eq_keys, require_ge, require_n_accounts, requires::{ is_uninitialized_account, require_initialized_commit_record, require_initialized_commit_state, @@ -69,14 +69,12 @@ pub fn process_undelegate_with_rollback_after_timeout( let [request_rent_payer, delegated_account, owner_program, undelegation_request_account, delegation_record_account, delegation_metadata_account, delegation_rent_payer, commit_state_account, commit_record_account, commit_reimbursement] = require_n_accounts!(accounts, 10); - require!(request_rent_payer.is_writable(), ProgramError::Immutable); require_signer(delegated_account, "delegated account")?; require_owned_pda( delegated_account, &crate::fast::ID, "delegated account", )?; - require!(delegated_account.is_writable(), ProgramError::Immutable); let request = load_valid_request( delegated_account, @@ -143,8 +141,6 @@ pub fn process_undelegate_with_rollback_after_timeout( delegation_rent_payer.address(), DlpError::InvalidReimbursementAddressForDelegationRent ); - require!(delegation_rent_payer.is_writable(), ProgramError::Immutable); - // If a validator started a commit but did not finish finalizing it before // timeout, the commit PDAs are cleanup-only inputs. Do not move their data // into the delegated account. That would turn this rollback path into a @@ -185,8 +181,6 @@ fn load_valid_request( "undelegation request", )?; - require!(request_rent_payer.is_writable(), ProgramError::Immutable); - let request_data = undelegation_request_account.try_borrow()?; let request = *UndelegationRequest::try_from_bytes_with_discriminator(&request_data) @@ -282,8 +276,6 @@ fn cleanup_pending_commit( ); } - require!(commit_reimbursement.is_writable(), ProgramError::Immutable); - // Timeout rollback is an owner-program-authorized escape hatch. At this // point the validator has committed state into the commit PDAs, but that // state has not been finalized into the delegated account. Applying it here From ba823bbbdac7685f5f44cfac259b42f2682e4b8e Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Sat, 27 Jun 2026 22:53:28 +0530 Subject: [PATCH 17/30] rename is_undelegatable to undelegatable and replace bool with enum UndelegationRequester --- dlp-api/src/error.rs | 2 +- .../request_undelegation.rs | 2 +- dlp-api/src/state/delegation_metadata.rs | 113 ++++++++++++++---- src/processor/fast/commit_state.rs | 12 +- src/processor/fast/delegate.rs | 4 +- src/processor/fast/delegate_with_actions.rs | 4 +- .../fast/internal/commit_finalize_internal.rs | 9 +- src/processor/fast/request_undelegation.rs | 25 ++-- src/processor/fast/undelegate.rs | 15 +-- tests/fixtures/accounts.rs | 26 ++-- tests/test_commit_finalize.rs | 5 +- tests/test_commit_finalize_from_buffer.rs | 7 +- tests/test_commit_on_curve.rs | 5 +- tests/test_commit_state.rs | 5 +- tests/test_commit_state_from_buffer.rs | 5 +- .../test_commit_state_with_program_config.rs | 5 +- tests/test_delegate_on_curve.rs | 5 +- tests/test_lamports_settlement.rs | 5 +- tests/test_request_undelegation.rs | 27 ++++- ..._undelegate_with_rollback_after_timeout.rs | 2 +- 20 files changed, 203 insertions(+), 80 deletions(-) diff --git a/dlp-api/src/error.rs b/dlp-api/src/error.rs index c53d7a05..5faffa50 100644 --- a/dlp-api/src/error.rs +++ b/dlp-api/src/error.rs @@ -24,7 +24,7 @@ pub enum DlpError { #[error("Invalid Authority")] InvalidAuthority = 0, - #[error("Account cannot be undelegated, is_undelegatable is false")] + #[error("Account cannot be undelegated, no undelegation requester")] NotUndelegatable = 1, #[error("Unauthorized Operation")] diff --git a/dlp-api/src/instruction_builder/request_undelegation.rs b/dlp-api/src/instruction_builder/request_undelegation.rs index a2e8fa6f..510a8a1b 100644 --- a/dlp-api/src/instruction_builder/request_undelegation.rs +++ b/dlp-api/src/instruction_builder/request_undelegation.rs @@ -44,7 +44,7 @@ pub fn request_undelegation( AccountMeta::new_readonly(owner_program, false), AccountMeta::new(undelegation_request_pda, false), AccountMeta::new_readonly(delegation_record_pda, false), - AccountMeta::new_readonly(delegation_metadata_pda, false), + AccountMeta::new(delegation_metadata_pda, false), AccountMeta::new_readonly(system_program::id(), false), ], data: DlpDiscriminator::RequestUndelegation.to_vec(), diff --git a/dlp-api/src/state/delegation_metadata.rs b/dlp-api/src/state/delegation_metadata.rs index 3c2e1c47..cdc0b4bc 100644 --- a/dlp-api/src/state/delegation_metadata.rs +++ b/dlp-api/src/state/delegation_metadata.rs @@ -10,6 +10,45 @@ use crate::{ impl_try_from_bytes_with_discriminator_borsh, require_ge, }; +/// Identifies who requested that a delegated account should move toward +/// undelegation. +#[derive( + BorshSerialize, BorshDeserialize, Clone, Copy, Debug, Eq, PartialEq, +)] +#[borsh(use_discriminant = true)] +#[repr(u8)] +pub enum UndelegationRequester { + /// No undelegation request has been recorded. + /// + /// Kept backward compatible with legacy `is_undelegatable = false`. + None = 0, + /// The validator requested undelegation through a commit/finalize path. + /// + /// Kept backward compatible with legacy `is_undelegatable = true`. + Validator = 1, + /// The account owner program requested undelegation. + OwnerProgram = 2, +} + +impl UndelegationRequester { + pub fn from_allow_undelegation(allow_undelegation: bool) -> Self { + if allow_undelegation { + Self::Validator + } else { + Self::None + } + } + + pub fn try_from_byte(value: u8) -> Result { + match value { + 0 => Ok(Self::None), + 1 => Ok(Self::Validator), + 2 => Ok(Self::OwnerProgram), + _ => Err(ProgramError::InvalidAccountData), + } + } +} + /// The Delegated Metadata includes Account Seeds, max delegation time, seeds /// and other meta information about the delegated account. /// * Everything necessary at cloning time is instead stored in the delegation record. @@ -17,8 +56,8 @@ use crate::{ pub struct DelegationMetadata { /// Latest finalized commit id applied to the delegated account on base. pub last_update_nonce: u64, - /// Whether the account can be undelegated or not - pub is_undelegatable: bool, + /// Who requested undelegation, stored in the legacy undelegatable byte. + pub undelegatable: UndelegationRequester, /// The seeds of the account, used to reopen it on undelegation pub seeds: Vec>, /// The account that paid the rent for the delegation PDAs @@ -37,7 +76,7 @@ impl<'a> DelegationMetadataFast<'a> { account.data_len(), AccountDiscriminator::SPACE + 8 // last_update_nonce - + 1 // is_undelegatable + + 1 // undelegatable + 32 // rent_payer + 4, // seeds (at least 4) ProgramError::InvalidAccountData @@ -75,24 +114,25 @@ impl<'a> DelegationMetadataFast<'a> { } } - pub fn set_is_undelegatable(&mut self, val: bool) { - unsafe { - ptr::write( - self.data.as_mut_ptr().add(AccountDiscriminator::SPACE + 8) - as *mut bool, - val, - ) - } + pub fn undelegation_requester( + &self, + ) -> Result { + UndelegationRequester::try_from_byte( + self.data[AccountDiscriminator::SPACE + 8], + ) } - pub fn replace_is_undelegatable(&mut self, val: bool) -> bool { - unsafe { - ptr::replace( - self.data.as_mut_ptr().add(AccountDiscriminator::SPACE + 8) - as *mut bool, - val, - ) - } + pub fn set_undelegation_requester(&mut self, val: UndelegationRequester) { + self.data[AccountDiscriminator::SPACE + 8] = val as u8; + } + + pub fn replace_undelegation_requester( + &mut self, + val: UndelegationRequester, + ) -> Result { + let previous = self.undelegation_requester()?; + self.set_undelegation_requester(val); + Ok(previous) } } @@ -106,7 +146,7 @@ impl DelegationMetadata { pub fn serialized_size(&self) -> usize { AccountDiscriminator::SPACE + 8 // last_update_nonce (u64) - + 1 // is_undelegatable (bool) + + 1 // undelegatable (UndelegationRequester) + 32 // rent_payer (Pubkey) + (4 + self.seeds.iter().map(|s| 4 + s.len()).sum::()) // seeds (Vec>) } @@ -131,7 +171,7 @@ mod tests { 211, 164, 97, 139, 136, 136, 77, ], ], - is_undelegatable: false, + undelegatable: UndelegationRequester::None, last_update_nonce: 0, rent_payer: Pubkey::default(), }; @@ -146,4 +186,35 @@ mod tests { assert_eq!(deserialized, original); } + + #[derive(BorshSerialize)] + struct LegacyDelegationMetadata { + pub last_update_nonce: u64, + pub is_undelegatable: bool, + pub seeds: Vec>, + pub rent_payer: Pubkey, + } + + #[test] + fn test_deserializes_legacy_undelegatable_bool() { + for (legacy_value, expected) in [ + (false, UndelegationRequester::None), + (true, UndelegationRequester::Validator), + ] { + let legacy = LegacyDelegationMetadata { + last_update_nonce: 42, + is_undelegatable: legacy_value, + seeds: vec![b"seed".to_vec()], + rent_payer: Pubkey::default(), + }; + + let serialized = to_vec(&legacy).expect("Serialization failed"); + let deserialized: DelegationMetadata = + DelegationMetadata::try_from_slice(&serialized) + .expect("Deserialization failed"); + + assert_eq!(deserialized.undelegatable, expected); + assert_eq!(deserialized.last_update_nonce, 42); + } + } } diff --git a/src/processor/fast/commit_state.rs b/src/processor/fast/commit_state.rs index 82495ada..dbaa93d9 100644 --- a/src/processor/fast/commit_state.rs +++ b/src/processor/fast/commit_state.rs @@ -21,6 +21,7 @@ use crate::{ }, state::{ CommitRecord, DelegationMetadata, DelegationRecord, ProgramConfig, + UndelegationRequester, }, DiffSet, }; @@ -166,15 +167,16 @@ pub(crate) fn process_commit_state_internal( return Err(DlpError::NonceOutOfOrder.into()); } - // Once the account is marked as undelegatable, any subsequent commit should fail - if delegation_metadata.is_undelegatable { - log!("delegation metadata is already undelegated: "); + // Once undelegation has been requested, any subsequent commit should fail. + if delegation_metadata.undelegatable != UndelegationRequester::None { + log!("delegation metadata already has an undelegation requester: "); args.delegation_metadata_account.address().log(); return Err(DlpError::AlreadyUndelegated.into()); } - // Update delegation metadata undelegation flag - delegation_metadata.is_undelegatable = args.allow_undelegation; + // Record whether this commit requested undelegation. + delegation_metadata.undelegatable = + UndelegationRequester::from_allow_undelegation(args.allow_undelegation); delegation_metadata .to_bytes_with_discriminator(&mut delegation_metadata_data.as_mut()) .map_err(to_pinocchio_program_error)?; diff --git a/src/processor/fast/delegate.rs b/src/processor/fast/delegate.rs index 48d40c7e..02091528 100644 --- a/src/processor/fast/delegate.rs +++ b/src/processor/fast/delegate.rs @@ -22,7 +22,7 @@ use crate::{ require_owned_pda, require_pda, require_signer, require_uninitialized_pda, DelegationMetadataCtx, DelegationRecordCtx, }, - state::{DelegationMetadata, DelegationRecord}, + state::{DelegationMetadata, DelegationRecord, UndelegationRequester}, }; /// Delegates an account @@ -242,7 +242,7 @@ fn process_delegate_inner( let delegation_metadata = DelegationMetadata { seeds: args.seeds, last_update_nonce: 0, - is_undelegatable: false, + undelegatable: UndelegationRequester::None, rent_payer: payer.address().to_bytes().into(), }; diff --git a/src/processor/fast/delegate_with_actions.rs b/src/processor/fast/delegate_with_actions.rs index 840099a3..951daa81 100644 --- a/src/processor/fast/delegate_with_actions.rs +++ b/src/processor/fast/delegate_with_actions.rs @@ -23,7 +23,7 @@ use crate::{ require_owned_pda, require_pda, require_signer, require_uninitialized_pda, DelegationMetadataCtx, DelegationRecordCtx, }, - state::{DelegationMetadata, DelegationRecord}, + state::{DelegationMetadata, DelegationRecord, UndelegationRequester}, }; /// Delegates an account and stores an actions payload. @@ -254,7 +254,7 @@ pub fn process_delegate_with_actions( let delegation_metadata = DelegationMetadata { seeds: args.delegate.seeds, last_update_nonce: 0, - is_undelegatable: false, + undelegatable: UndelegationRequester::None, rent_payer: payer.address().to_bytes().into(), }; diff --git a/src/processor/fast/internal/commit_finalize_internal.rs b/src/processor/fast/internal/commit_finalize_internal.rs index 07b04006..1ba7a95a 100644 --- a/src/processor/fast/internal/commit_finalize_internal.rs +++ b/src/processor/fast/internal/commit_finalize_internal.rs @@ -15,7 +15,7 @@ use crate::{ 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}, + state::{DelegationMetadataFast, DelegationRecord, UndelegationRequester}, }; /// Arguments for the commit state internal function @@ -87,8 +87,13 @@ pub(crate) fn process_commit_finalize_internal( require_eq!(args.commit_id, prev_id + 1, DlpError::NonceOutOfOrder); + let previous_requester = metadata.replace_undelegation_requester( + UndelegationRequester::from_allow_undelegation( + args.allow_undelegation, + ), + )?; require!( - !metadata.replace_is_undelegatable(args.allow_undelegation), + previous_requester == UndelegationRequester::None, DlpError::AlreadyUndelegated ); } diff --git a/src/processor/fast/request_undelegation.rs b/src/processor/fast/request_undelegation.rs index 8e24c0ba..c19cbc88 100644 --- a/src/processor/fast/request_undelegation.rs +++ b/src/processor/fast/request_undelegation.rs @@ -19,7 +19,10 @@ use crate::{ require_initialized_delegation_record, require_owned_pda, require_pda, require_signer, require_uninitialized_pda, UndelegationRequestCtx, }, - state::{DelegationMetadata, DelegationRecord, UndelegationRequest}, + state::{ + DelegationMetadataFast, DelegationRecord, UndelegationRequest, + UndelegationRequester, + }, }; /// Request undelegation for one delegated account. @@ -31,7 +34,7 @@ use crate::{ /// 2: `[]` owner program of the delegated account /// 3: `[writable]` undelegation request PDA /// 4: `[]` delegation record PDA -/// 5: `[]` delegation metadata PDA +/// 5: `[writable]` delegation metadata PDA /// 6: `[]` system program pub fn process_request_undelegation( _program_id: &Address, @@ -88,15 +91,14 @@ pub fn process_request_undelegation( ProgramError::InvalidAccountOwner ); - let delegation_metadata_data = delegation_metadata_account.try_borrow()?; - let delegation_metadata = - DelegationMetadata::try_from_bytes_with_discriminator( - &delegation_metadata_data, - ) - .map_err(to_pinocchio_program_error)?; + let mut delegation_metadata = + DelegationMetadataFast::from_account(delegation_metadata_account)?; + delegation_metadata + .set_undelegation_requester(UndelegationRequester::OwnerProgram); + let last_commit_nonce_at_request = delegation_metadata.last_update_nonce(); drop(delegation_record_data); - drop(delegation_metadata_data); + drop(delegation_metadata); let request_seeds = &[ pda::UNDELEGATION_REQUEST_TAG, @@ -135,7 +137,7 @@ pub fn process_request_undelegation( rent_payer: *payer.address(), created_slot, expires_at_slot, - last_commit_nonce_at_request: delegation_metadata.last_update_nonce, + last_commit_nonce_at_request, bump: request_bump, _padding: [0; 7], }; @@ -185,8 +187,7 @@ pub fn process_request_undelegation( // Refresh only the base-chain commit checkpoint. Repeated requests must not // reset or extend the timeout, but they are allowed to make rollback an // explicit owner-program decision after finalized base state has changed. - request.last_commit_nonce_at_request = - delegation_metadata.last_update_nonce; + request.last_commit_nonce_at_request = last_commit_nonce_at_request; Ok(()) } diff --git a/src/processor/fast/undelegate.rs b/src/processor/fast/undelegate.rs index 16b778a2..c59bd336 100644 --- a/src/processor/fast/undelegate.rs +++ b/src/processor/fast/undelegate.rs @@ -30,7 +30,10 @@ use crate::{ require_signer, require_uninitialized_pda, CommitRecordCtx, CommitStateAccountCtx, UndelegateBufferCtx, }, - state::{DelegationMetadata, DelegationRecord, UndelegationRequest}, + state::{ + DelegationMetadata, DelegationRecord, UndelegationRequest, + UndelegationRequester, + }, }; /// Undelegate a delegated account @@ -59,7 +62,7 @@ use crate::{ /// - validator fees vault is initialized /// - commit state is uninitialized /// - commit record is uninitialized -/// - delegated account is NOT undelegatable +/// - undelegation has been requested for the delegated account /// - owner program account matches the owner in the delegation record /// - rent reimbursement account matches the rent payer in the delegation metadata /// @@ -190,11 +193,9 @@ pub fn process_undelegate( .map_err(to_pinocchio_program_error)?; let delegation_last_update_nonce = delegation_metadata.last_update_nonce; - // Check if the delegated account is undelegatable - if !delegation_metadata.is_undelegatable { - log!( - "delegation metadata indicates the account is not undelegatable : " - ); + // Check if undelegation has been requested for the delegated account. + if delegation_metadata.undelegatable == UndelegationRequester::None { + log!("delegation metadata has no undelegation requester: "); delegation_metadata_account.address().log(); return Err(DlpError::NotUndelegatable.into()); } diff --git a/tests/fixtures/accounts.rs b/tests/fixtures/accounts.rs index e4e34c83..950ffb92 100644 --- a/tests/fixtures/accounts.rs +++ b/tests/fixtures/accounts.rs @@ -4,7 +4,7 @@ use dlp_api::{ pda::UNDELEGATION_REQUEST_TAG, state::{ CommitRecord, DelegationMetadata, DelegationRecord, ProgramConfig, - UndelegationRequest, + UndelegationRequest, UndelegationRequester, }, }; use solana_program::{ @@ -17,7 +17,7 @@ use solana_sdk_ids::system_program; const DEFAULT_DELEGATION_SLOT: u64 = 0; const DEFAULT_COMMIT_FREQUENCY_MS: u64 = 0; const DEFAULT_LAST_UPDATE_EXTERNAL_SLOT: u64 = 0; -const DEFAULT_IS_UNDELEGATABLE: bool = false; +const DEFAULT_UNDELEGATABLE: bool = false; const DEFAULT_SEEDS: &[&[u8]] = &[&[116, 101, 115, 116, 45, 112, 100, 97]]; #[allow(dead_code)] @@ -120,23 +120,23 @@ pub fn create_delegation_record_data( #[allow(dead_code)] pub fn get_delegation_metadata_data_on_curve( rent_payer: Pubkey, - is_undelegatable: Option, + undelegatable: Option, ) -> Vec { create_delegation_metadata_data( rent_payer, &[], - is_undelegatable.unwrap_or(DEFAULT_IS_UNDELEGATABLE), + undelegatable.unwrap_or(DEFAULT_UNDELEGATABLE), ) } #[allow(dead_code)] pub fn get_delegation_metadata_data( rent_payer: Pubkey, - is_undelegatable: Option, + undelegatable: Option, ) -> Vec { get_delegation_metadata_data_with_nonce( rent_payer, - is_undelegatable, + undelegatable, DEFAULT_LAST_UPDATE_EXTERNAL_SLOT, ) } @@ -144,13 +144,13 @@ pub fn get_delegation_metadata_data( #[allow(dead_code)] pub fn get_delegation_metadata_data_with_nonce( rent_payer: Pubkey, - is_undelegatable: Option, + undelegatable: Option, last_update_nonce: u64, ) -> Vec { create_delegation_metadata_data_with_nonce( rent_payer, DEFAULT_SEEDS, - is_undelegatable.unwrap_or(DEFAULT_IS_UNDELEGATABLE), + undelegatable.unwrap_or(DEFAULT_UNDELEGATABLE), last_update_nonce, ) } @@ -158,12 +158,12 @@ pub fn get_delegation_metadata_data_with_nonce( pub fn create_delegation_metadata_data( rent_payer: Pubkey, seeds: &[&[u8]], - is_undelegatable: bool, + undelegatable: bool, ) -> Vec { create_delegation_metadata_data_with_nonce( rent_payer, seeds, - is_undelegatable, + undelegatable, DEFAULT_LAST_UPDATE_EXTERNAL_SLOT, ) } @@ -172,12 +172,14 @@ pub fn create_delegation_metadata_data( pub fn create_delegation_metadata_data_with_nonce( rent_payer: Pubkey, seeds: &[&[u8]], - is_undelegatable: bool, + undelegatable: bool, last_update_nonce: u64, ) -> Vec { let delegation_metadata = DelegationMetadata { last_update_nonce, - is_undelegatable, + undelegatable: UndelegationRequester::from_allow_undelegation( + undelegatable, + ), seeds: seeds.iter().map(|s| s.to_vec()).collect(), rent_payer, }; diff --git a/tests/test_commit_finalize.rs b/tests/test_commit_finalize.rs index 37d29261..cecce9af 100644 --- a/tests/test_commit_finalize.rs +++ b/tests/test_commit_finalize.rs @@ -115,7 +115,10 @@ async fn run_test_commit_finalize( ) .unwrap(); - assert!(delegation_metadata.is_undelegatable); + assert_eq!( + delegation_metadata.undelegatable, + dlp_api::state::UndelegationRequester::Validator + ); } #[tokio::test] diff --git a/tests/test_commit_finalize_from_buffer.rs b/tests/test_commit_finalize_from_buffer.rs index fd40443d..9513cbcb 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, 1400); + assertables::assert_lt!(metadata.compute_units_consumed, 1450); assert_eq!( metadata.log_messages.len(), @@ -93,7 +93,10 @@ async fn test_commit_finalize_from_buffer_perf() { ) .unwrap(); - assert!(delegation_metadata.is_undelegatable); + assert_eq!( + delegation_metadata.undelegatable, + dlp_api::state::UndelegationRequester::Validator + ); } #[tokio::test] diff --git a/tests/test_commit_on_curve.rs b/tests/test_commit_on_curve.rs index 2265a727..db12b358 100644 --- a/tests/test_commit_on_curve.rs +++ b/tests/test_commit_on_curve.rs @@ -91,7 +91,10 @@ async fn test_commit_on_curve() { &delegation_metadata_account.data, ) .unwrap(); - assert!(delegation_metadata.is_undelegatable); + assert_eq!( + delegation_metadata.undelegatable, + dlp_api::state::UndelegationRequester::Validator + ); } async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { diff --git a/tests/test_commit_state.rs b/tests/test_commit_state.rs index 73a312f2..21757885 100644 --- a/tests/test_commit_state.rs +++ b/tests/test_commit_state.rs @@ -97,7 +97,10 @@ async fn test_commit_new_state() { &delegation_metadata_account.data, ) .unwrap(); - assert!(delegation_metadata.is_undelegatable); + assert_eq!( + delegation_metadata.undelegatable, + dlp_api::state::UndelegationRequester::Validator + ); } #[tokio::test] diff --git a/tests/test_commit_state_from_buffer.rs b/tests/test_commit_state_from_buffer.rs index b986cb29..d8063ed3 100644 --- a/tests/test_commit_state_from_buffer.rs +++ b/tests/test_commit_state_from_buffer.rs @@ -101,7 +101,10 @@ async fn test_commit_new_state_from_buffer() { &delegation_metadata_account.data, ) .unwrap(); - assert!(delegation_metadata.is_undelegatable); + assert_eq!( + delegation_metadata.undelegatable, + dlp_api::state::UndelegationRequester::Validator + ); } async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { diff --git a/tests/test_commit_state_with_program_config.rs b/tests/test_commit_state_with_program_config.rs index 736e3ffb..216b2f6b 100644 --- a/tests/test_commit_state_with_program_config.rs +++ b/tests/test_commit_state_with_program_config.rs @@ -112,7 +112,10 @@ async fn test_commit_new_state(valid_config: bool) { &delegation_metadata_account.data, ) .unwrap(); - assert!(delegation_metadata.is_undelegatable); + assert_eq!( + delegation_metadata.undelegatable, + dlp_api::state::UndelegationRequester::Validator + ); } } diff --git a/tests/test_delegate_on_curve.rs b/tests/test_delegate_on_curve.rs index 02a19153..77e78686 100644 --- a/tests/test_delegate_on_curve.rs +++ b/tests/test_delegate_on_curve.rs @@ -130,7 +130,10 @@ async fn test_delegate_on_curve() { &delegation_metadata.data, ) .unwrap(); - assert!(!delegation_metadata.is_undelegatable); + assert_eq!( + delegation_metadata.undelegatable, + dlp_api::state::UndelegationRequester::None + ); } async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { diff --git a/tests/test_lamports_settlement.rs b/tests/test_lamports_settlement.rs index 8ca4ec07..7b0a9cce 100644 --- a/tests/test_lamports_settlement.rs +++ b/tests/test_lamports_settlement.rs @@ -1215,7 +1215,10 @@ async fn commit_new_state(args: CommitNewStateArgs<'_>) { &delegation_metadata_account.data, ) .unwrap(); - assert!(delegation_metadata.is_undelegatable); + assert_eq!( + delegation_metadata.undelegatable, + dlp_api::state::UndelegationRequester::Validator + ); } async fn commit_state_with_nonce(args: CommitStateWithNonceArgs<'_>) { diff --git a/tests/test_request_undelegation.rs b/tests/test_request_undelegation.rs index 924c2168..c226e109 100644 --- a/tests/test_request_undelegation.rs +++ b/tests/test_request_undelegation.rs @@ -9,7 +9,7 @@ use dlp_api::{ undelegation_request_pda_from_delegated_account, validator_fees_vault_pda_from_validator, }, - state::UndelegationRequest, + state::{DelegationMetadata, UndelegationRequest, UndelegationRequester}, }; use solana_program::{ account_info::AccountInfo, @@ -79,6 +79,23 @@ async fn test_request_undelegation_creates_request() { request.created_slot + DEFAULT_UNDELEGATION_REQUEST_TIMEOUT_SLOTS ); assert_eq!(request.last_commit_nonce_at_request, 0); + + let delegation_metadata_pda = + delegation_metadata_pda_from_delegated_account(&DELEGATED_PDA_ID); + let delegation_metadata_account = banks + .get_account(delegation_metadata_pda) + .await + .unwrap() + .unwrap(); + let delegation_metadata = + DelegationMetadata::try_from_bytes_with_discriminator( + &delegation_metadata_account.data, + ) + .unwrap(); + assert_eq!( + delegation_metadata.undelegatable, + UndelegationRequester::OwnerProgram + ); } #[tokio::test] @@ -160,7 +177,7 @@ async fn test_request_undelegation_rejects_missing_delegated_signer() { delegation_record_pda_from_delegated_account(&DELEGATED_PDA_ID), false, ), - AccountMeta::new_readonly( + AccountMeta::new( delegation_metadata_pda_from_delegated_account( &DELEGATED_PDA_ID, ), @@ -364,7 +381,7 @@ fn request_undelegation_from_owner_program(payer: Pubkey) -> Instruction { delegation_record_pda_from_delegated_account(&DELEGATED_PDA_ID), false, ), - AccountMeta::new_readonly( + AccountMeta::new( delegation_metadata_pda_from_delegated_account( &DELEGATED_PDA_ID, ), @@ -531,7 +548,7 @@ fn add_delegation_accounts(program_test: &mut ProgramTest, authority: Pubkey) { fn add_delegation_accounts_with_metadata( program_test: &mut ProgramTest, authority: Pubkey, - is_undelegatable: Option, + undelegatable: Option, ) { let delegation_record_data = get_delegation_record_data(authority, None); program_test.add_account( @@ -547,7 +564,7 @@ fn add_delegation_accounts_with_metadata( ); let delegation_metadata_data = - get_delegation_metadata_data(authority, is_undelegatable); + get_delegation_metadata_data(authority, undelegatable); program_test.add_account( delegation_metadata_pda_from_delegated_account(&DELEGATED_PDA_ID), Account { diff --git a/tests/test_undelegate_with_rollback_after_timeout.rs b/tests/test_undelegate_with_rollback_after_timeout.rs index 2665f341..e576a8fa 100644 --- a/tests/test_undelegate_with_rollback_after_timeout.rs +++ b/tests/test_undelegate_with_rollback_after_timeout.rs @@ -758,7 +758,7 @@ fn request_undelegation_from_owner_program(payer: Pubkey) -> Instruction { delegation_record_pda_from_delegated_account(&DELEGATED_PDA_ID), false, ), - AccountMeta::new_readonly( + AccountMeta::new( delegation_metadata_pda_from_delegated_account( &DELEGATED_PDA_ID, ), From 0cca5a6183afd993b172249184b22e5e1dcbcf47 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Sat, 27 Jun 2026 23:37:03 +0530 Subject: [PATCH 18/30] rename last_update_nonce -> last_commit_id --- dlp-api/src/error.rs | 4 +- dlp-api/src/state/delegation_metadata.rs | 16 +++---- dlp-api/src/state/undelegation_request.rs | 4 +- src/processor/fast/commit_state.rs | 6 +-- src/processor/fast/delegate.rs | 2 +- src/processor/fast/delegate_with_actions.rs | 2 +- src/processor/fast/finalize.rs | 2 +- .../fast/internal/commit_finalize_internal.rs | 2 +- src/processor/fast/request_undelegation.rs | 6 +-- src/processor/fast/undelegate.rs | 10 ++--- .../undelegate_with_rollback_after_timeout.rs | 6 +-- tests/fixtures/accounts.rs | 22 +++++----- tests/test_commit_fees_on_undelegation.rs | 30 +++++++------ tests/test_finalize.rs | 2 +- tests/test_request_undelegation.rs | 2 +- ..._undelegate_with_rollback_after_timeout.rs | 44 +++++++++---------- 16 files changed, 81 insertions(+), 79 deletions(-) diff --git a/dlp-api/src/error.rs b/dlp-api/src/error.rs index 5faffa50..844d05a2 100644 --- a/dlp-api/src/error.rs +++ b/dlp-api/src/error.rs @@ -182,8 +182,8 @@ pub enum DlpError { #[error("Invalid pending commit state for request-timeout undelegation")] InvalidPendingCommitState = 52, - #[error("Commit nonce did not match. Re-request to update commit nonce")] - RollbackCommitNonceMismatch = 53, + #[error("Commit id did not match. Re-request to update commit id")] + RollbackCommitIdMismatch = 53, #[error("An infallible error is encountered possibly due to logic error")] InfallibleError = 100, diff --git a/dlp-api/src/state/delegation_metadata.rs b/dlp-api/src/state/delegation_metadata.rs index cdc0b4bc..297da9ae 100644 --- a/dlp-api/src/state/delegation_metadata.rs +++ b/dlp-api/src/state/delegation_metadata.rs @@ -55,7 +55,7 @@ impl UndelegationRequester { #[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq)] pub struct DelegationMetadata { /// Latest finalized commit id applied to the delegated account on base. - pub last_update_nonce: u64, + pub last_commit_id: u64, /// Who requested undelegation, stored in the legacy undelegatable byte. pub undelegatable: UndelegationRequester, /// The seeds of the account, used to reopen it on undelegation @@ -75,7 +75,7 @@ impl<'a> DelegationMetadataFast<'a> { require_ge!( account.data_len(), AccountDiscriminator::SPACE - + 8 // last_update_nonce + + 8 // last_commit_id + 1 // undelegatable + 32 // rent_payer + 4, // seeds (at least 4) @@ -87,14 +87,14 @@ impl<'a> DelegationMetadataFast<'a> { }) } - pub fn last_update_nonce(&self) -> u64 { + pub fn last_commit_id(&self) -> u64 { unsafe { ptr::read(self.data.as_ptr().add(AccountDiscriminator::SPACE) as *const u64) } } - pub fn set_last_update_nonce(&mut self, val: u64) { + pub fn set_last_commit_id(&mut self, val: u64) { unsafe { ptr::write( self.data.as_mut_ptr().add(AccountDiscriminator::SPACE) @@ -104,7 +104,7 @@ impl<'a> DelegationMetadataFast<'a> { } } - pub fn replace_last_update_nonce(&mut self, val: u64) -> u64 { + pub fn replace_last_commit_id(&mut self, val: u64) -> u64 { unsafe { ptr::replace( self.data.as_mut_ptr().add(AccountDiscriminator::SPACE) @@ -145,7 +145,7 @@ impl AccountWithDiscriminator for DelegationMetadata { impl DelegationMetadata { pub fn serialized_size(&self) -> usize { AccountDiscriminator::SPACE - + 8 // last_update_nonce (u64) + + 8 // last_commit_id (u64) + 1 // undelegatable (UndelegationRequester) + 32 // rent_payer (Pubkey) + (4 + self.seeds.iter().map(|s| 4 + s.len()).sum::()) // seeds (Vec>) @@ -172,7 +172,7 @@ mod tests { ], ], undelegatable: UndelegationRequester::None, - last_update_nonce: 0, + last_commit_id: 0, rent_payer: Pubkey::default(), }; @@ -214,7 +214,7 @@ mod tests { .expect("Deserialization failed"); assert_eq!(deserialized.undelegatable, expected); - assert_eq!(deserialized.last_update_nonce, 42); + assert_eq!(deserialized.last_commit_id, 42); } } } diff --git a/dlp-api/src/state/undelegation_request.rs b/dlp-api/src/state/undelegation_request.rs index ddf20f20..0ca77b5a 100644 --- a/dlp-api/src/state/undelegation_request.rs +++ b/dlp-api/src/state/undelegation_request.rs @@ -27,9 +27,9 @@ pub struct UndelegationRequest { /// The first slot at which timeout rollback is allowed. pub expires_at_slot: u64, - /// DelegationMetadata.last_update_nonce observed when the request was last + /// DelegationMetadata.last_commit_id observed when the request was last /// made or refreshed. - pub last_commit_nonce_at_request: u64, + pub last_commit_id_at_request: u64, /// PDA bump for this request. pub bump: u8, diff --git a/src/processor/fast/commit_state.rs b/src/processor/fast/commit_state.rs index dbaa93d9..f3722eed 100644 --- a/src/processor/fast/commit_state.rs +++ b/src/processor/fast/commit_state.rs @@ -158,11 +158,11 @@ pub(crate) fn process_commit_state_internal( .map_err(to_pinocchio_program_error)?; // To preserve correct history of account updates we require sequential commits - if args.commit_record_nonce != delegation_metadata.last_update_nonce + 1 { + if args.commit_record_nonce != delegation_metadata.last_commit_id + 1 { log!( - "Nonce {} is incorrect, previous nonce is {}. Rejecting commit", + "Commit id {} is incorrect, previous commit id is {}. Rejecting commit", args.commit_record_nonce, - delegation_metadata.last_update_nonce + delegation_metadata.last_commit_id ); return Err(DlpError::NonceOutOfOrder.into()); } diff --git a/src/processor/fast/delegate.rs b/src/processor/fast/delegate.rs index 02091528..92e8a4fd 100644 --- a/src/processor/fast/delegate.rs +++ b/src/processor/fast/delegate.rs @@ -241,7 +241,7 @@ fn process_delegate_inner( let delegation_metadata = DelegationMetadata { seeds: args.seeds, - last_update_nonce: 0, + last_commit_id: 0, undelegatable: UndelegationRequester::None, rent_payer: payer.address().to_bytes().into(), }; diff --git a/src/processor/fast/delegate_with_actions.rs b/src/processor/fast/delegate_with_actions.rs index 951daa81..a7eef636 100644 --- a/src/processor/fast/delegate_with_actions.rs +++ b/src/processor/fast/delegate_with_actions.rs @@ -253,7 +253,7 @@ pub fn process_delegate_with_actions( let delegation_metadata = DelegationMetadata { seeds: args.delegate.seeds, - last_update_nonce: 0, + last_commit_id: 0, undelegatable: UndelegationRequester::None, rent_payer: payer.address().to_bytes().into(), }; diff --git a/src/processor/fast/finalize.rs b/src/processor/fast/finalize.rs index 1832a4c0..beb364f4 100644 --- a/src/processor/fast/finalize.rs +++ b/src/processor/fast/finalize.rs @@ -161,7 +161,7 @@ pub fn process_finalize( )?; // Update the delegation metadata - delegation_metadata.last_update_nonce = commit_record.nonce; + delegation_metadata.last_commit_id = commit_record.nonce; delegation_metadata .to_bytes_with_discriminator(&mut delegation_metadata_data.as_mut()) .map_err(to_pinocchio_program_error)?; diff --git a/src/processor/fast/internal/commit_finalize_internal.rs b/src/processor/fast/internal/commit_finalize_internal.rs index 1ba7a95a..d281da75 100644 --- a/src/processor/fast/internal/commit_finalize_internal.rs +++ b/src/processor/fast/internal/commit_finalize_internal.rs @@ -83,7 +83,7 @@ pub(crate) fn process_commit_finalize_internal( args.delegation_metadata_account, )?; - let prev_id = metadata.replace_last_update_nonce(args.commit_id); + let prev_id = metadata.replace_last_commit_id(args.commit_id); require_eq!(args.commit_id, prev_id + 1, DlpError::NonceOutOfOrder); diff --git a/src/processor/fast/request_undelegation.rs b/src/processor/fast/request_undelegation.rs index c19cbc88..4ada5363 100644 --- a/src/processor/fast/request_undelegation.rs +++ b/src/processor/fast/request_undelegation.rs @@ -95,7 +95,7 @@ pub fn process_request_undelegation( DelegationMetadataFast::from_account(delegation_metadata_account)?; delegation_metadata .set_undelegation_requester(UndelegationRequester::OwnerProgram); - let last_commit_nonce_at_request = delegation_metadata.last_update_nonce(); + let last_commit_id_at_request = delegation_metadata.last_commit_id(); drop(delegation_record_data); drop(delegation_metadata); @@ -137,7 +137,7 @@ pub fn process_request_undelegation( rent_payer: *payer.address(), created_slot, expires_at_slot, - last_commit_nonce_at_request, + last_commit_id_at_request, bump: request_bump, _padding: [0; 7], }; @@ -187,7 +187,7 @@ pub fn process_request_undelegation( // Refresh only the base-chain commit checkpoint. Repeated requests must not // reset or extend the timeout, but they are allowed to make rollback an // explicit owner-program decision after finalized base state has changed. - request.last_commit_nonce_at_request = last_commit_nonce_at_request; + request.last_commit_id_at_request = last_commit_id_at_request; Ok(()) } diff --git a/src/processor/fast/undelegate.rs b/src/processor/fast/undelegate.rs index c59bd336..3255e3e7 100644 --- a/src/processor/fast/undelegate.rs +++ b/src/processor/fast/undelegate.rs @@ -191,7 +191,7 @@ pub fn process_undelegate( &delegation_metadata_data, ) .map_err(to_pinocchio_program_error)?; - let delegation_last_update_nonce = delegation_metadata.last_update_nonce; + let delegation_last_commit_id = delegation_metadata.last_commit_id; // Check if undelegation has been requested for the delegated account. if delegation_metadata.undelegatable == UndelegationRequester::None { @@ -230,7 +230,7 @@ pub fn process_undelegate( rent_reimbursement, fees_vault, validator_fees_vault, - delegation_last_update_nonce, + delegation_last_commit_id, )?; if let Some((undelegation_request_account, request_rent_payer)) = request_accounts @@ -293,7 +293,7 @@ pub fn process_undelegate( rent_reimbursement, fees_vault, validator_fees_vault, - delegation_last_update_nonce, + delegation_last_commit_id, )?; if let Some((undelegation_request_account, request_rent_payer)) = request_accounts @@ -461,9 +461,9 @@ fn process_delegation_cleanup( rent_reimbursement: &AccountView, fees_vault: &AccountView, validator_fees_vault: &AccountView, - delegation_last_update_nonce: u64, + delegation_last_commit_id: u64, ) -> ProgramResult { - let commit_count = delegation_last_update_nonce.saturating_sub(1); + let commit_count = delegation_last_commit_id.saturating_sub(1); let commit_fee = COMMIT_FEE_LAMPORTS .checked_mul(commit_count) .ok_or(DlpError::Overflow)?; diff --git a/src/processor/fast/undelegate_with_rollback_after_timeout.rs b/src/processor/fast/undelegate_with_rollback_after_timeout.rs index 48e0251b..a175608c 100644 --- a/src/processor/fast/undelegate_with_rollback_after_timeout.rs +++ b/src/processor/fast/undelegate_with_rollback_after_timeout.rs @@ -132,9 +132,9 @@ pub fn process_undelegate_with_rollback_after_timeout( // minimizes, but cannot prevent, discarding newer ER state that has not been // finalized to base. require_eq!( - request.last_commit_nonce_at_request, - delegation_metadata.last_update_nonce, - DlpError::RollbackCommitNonceMismatch + request.last_commit_id_at_request, + delegation_metadata.last_commit_id, + DlpError::RollbackCommitIdMismatch ); require_eq_keys!( &delegation_metadata.rent_payer, diff --git a/tests/fixtures/accounts.rs b/tests/fixtures/accounts.rs index 950ffb92..4ca57569 100644 --- a/tests/fixtures/accounts.rs +++ b/tests/fixtures/accounts.rs @@ -134,7 +134,7 @@ pub fn get_delegation_metadata_data( rent_payer: Pubkey, undelegatable: Option, ) -> Vec { - get_delegation_metadata_data_with_nonce( + get_delegation_metadata_data_with_commit_id( rent_payer, undelegatable, DEFAULT_LAST_UPDATE_EXTERNAL_SLOT, @@ -142,16 +142,16 @@ pub fn get_delegation_metadata_data( } #[allow(dead_code)] -pub fn get_delegation_metadata_data_with_nonce( +pub fn get_delegation_metadata_data_with_commit_id( rent_payer: Pubkey, undelegatable: Option, - last_update_nonce: u64, + last_commit_id: u64, ) -> Vec { - create_delegation_metadata_data_with_nonce( + create_delegation_metadata_data_with_commit_id( rent_payer, DEFAULT_SEEDS, undelegatable.unwrap_or(DEFAULT_UNDELEGATABLE), - last_update_nonce, + last_commit_id, ) } @@ -160,7 +160,7 @@ pub fn create_delegation_metadata_data( seeds: &[&[u8]], undelegatable: bool, ) -> Vec { - create_delegation_metadata_data_with_nonce( + create_delegation_metadata_data_with_commit_id( rent_payer, seeds, undelegatable, @@ -169,14 +169,14 @@ pub fn create_delegation_metadata_data( } #[allow(dead_code)] -pub fn create_delegation_metadata_data_with_nonce( +pub fn create_delegation_metadata_data_with_commit_id( rent_payer: Pubkey, seeds: &[&[u8]], undelegatable: bool, - last_update_nonce: u64, + last_commit_id: u64, ) -> Vec { let delegation_metadata = DelegationMetadata { - last_update_nonce, + last_commit_id, undelegatable: UndelegationRequester::from_allow_undelegation( undelegatable, ), @@ -244,7 +244,7 @@ pub fn create_undelegation_request_data_with_expiry( rent_payer: Pubkey, created_slot: u64, expires_at_slot: u64, - last_commit_nonce_at_request: u64, + last_commit_id_at_request: u64, ) -> Vec { let (_, bump) = Pubkey::find_program_address( &[UNDELEGATION_REQUEST_TAG, delegated_account.as_ref()], @@ -256,7 +256,7 @@ pub fn create_undelegation_request_data_with_expiry( rent_payer, created_slot, expires_at_slot, - last_commit_nonce_at_request, + last_commit_id_at_request, bump, _padding: [0; 7], }; diff --git a/tests/test_commit_fees_on_undelegation.rs b/tests/test_commit_fees_on_undelegation.rs index 98caa13e..76b36ca5 100644 --- a/tests/test_commit_fees_on_undelegation.rs +++ b/tests/test_commit_fees_on_undelegation.rs @@ -17,7 +17,7 @@ use solana_sdk::{ use solana_sdk_ids::system_program; use crate::fixtures::{ - create_delegation_metadata_data_with_nonce, get_delegation_record_data, + create_delegation_metadata_data_with_commit_id, get_delegation_record_data, DELEGATED_PDA_ID, DELEGATED_PDA_OWNER_ID, TEST_AUTHORITY, }; @@ -37,12 +37,13 @@ async fn test_commit_fees_on_undelegation() { let delegation_record_data = get_delegation_record_data(validator.pubkey(), None); - let delegation_metadata_data = create_delegation_metadata_data_with_nonce( - validator.pubkey(), - &[], - true, - 101, - ); + let delegation_metadata_data = + create_delegation_metadata_data_with_commit_id( + validator.pubkey(), + &[], + true, + 101, + ); let record_rent = Rent::default().minimum_balance(delegation_record_data.len()); @@ -123,13 +124,14 @@ async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { }, ); - // Setup the delegated account metadata PDA with high nonce - let delegation_metadata_data = create_delegation_metadata_data_with_nonce( - validator.pubkey(), - &[], - true, - 101, - ); + // Setup the delegated account metadata PDA with high commit id + let delegation_metadata_data = + create_delegation_metadata_data_with_commit_id( + validator.pubkey(), + &[], + true, + 101, + ); program_test.add_account( delegation_metadata_pda_from_delegated_account(&DELEGATED_PDA_ID), Account { diff --git a/tests/test_finalize.rs b/tests/test_finalize.rs index 83f6a779..030141e8 100644 --- a/tests/test_finalize.rs +++ b/tests/test_finalize.rs @@ -102,7 +102,7 @@ async fn test_finalize() { &delegation_metadata_account.data, ) .unwrap(); - assert_eq!(commit_record.nonce, delegation_metadata.last_update_nonce); + assert_eq!(commit_record.nonce, delegation_metadata.last_commit_id); } async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { diff --git a/tests/test_request_undelegation.rs b/tests/test_request_undelegation.rs index c226e109..10a58b5e 100644 --- a/tests/test_request_undelegation.rs +++ b/tests/test_request_undelegation.rs @@ -78,7 +78,7 @@ async fn test_request_undelegation_creates_request() { request.expires_at_slot, request.created_slot + DEFAULT_UNDELEGATION_REQUEST_TIMEOUT_SLOTS ); - assert_eq!(request.last_commit_nonce_at_request, 0); + assert_eq!(request.last_commit_id_at_request, 0); let delegation_metadata_pda = delegation_metadata_pda_from_delegated_account(&DELEGATED_PDA_ID); diff --git a/tests/test_undelegate_with_rollback_after_timeout.rs b/tests/test_undelegate_with_rollback_after_timeout.rs index e576a8fa..e2f04d00 100644 --- a/tests/test_undelegate_with_rollback_after_timeout.rs +++ b/tests/test_undelegate_with_rollback_after_timeout.rs @@ -36,10 +36,10 @@ use solana_system_interface::instruction as system_instruction; use crate::fixtures::{ create_undelegation_request_data_with_expiry, - get_commit_record_account_data, get_delegation_metadata_data_with_nonce, - get_delegation_record_data, keypair_from_bytes, - COMMIT_NEW_STATE_ACCOUNT_DATA, DELEGATED_PDA, DELEGATED_PDA_ID, - DELEGATED_PDA_OWNER_ID, TEST_AUTHORITY, + get_commit_record_account_data, + get_delegation_metadata_data_with_commit_id, get_delegation_record_data, + keypair_from_bytes, COMMIT_NEW_STATE_ACCOUNT_DATA, DELEGATED_PDA, + DELEGATED_PDA_ID, DELEGATED_PDA_OWNER_ID, TEST_AUTHORITY, }; mod fixtures; @@ -284,7 +284,7 @@ async fn test_undelegate_with_rollback_after_timeout_rejects_stale_request() { delegation_rent_payer, _, blockhash, - ) = setup_request_timeout_env_with_nonces(false, 0, 1, 0).await; + ) = setup_request_timeout_env_with_commit_ids(false, 0, 1, 0).await; let request_pda = undelegation_request_pda_from_delegated_account(&DELEGATED_PDA_ID); @@ -328,9 +328,9 @@ async fn test_undelegate_with_rollback_after_timeout_rejects_stale_request() { 0, InstructionError::Custom(code), ) - ) if code == DlpError::RollbackCommitNonceMismatch as u32 + ) if code == DlpError::RollbackCommitIdMismatch as u32 ), - "expected RollbackCommitNonceMismatch, got {err:?}" + "expected RollbackCommitIdMismatch, got {err:?}" ); assert_eq!( @@ -434,7 +434,7 @@ async fn test_request_timeout_closes_pending_commit_without_applying_it() { } #[tokio::test] -async fn test_re_request_refreshes_commit_nonce_without_extending_timeout() { +async fn test_re_request_refreshes_commit_id_without_extending_timeout() { let ( banks, caller, @@ -442,7 +442,7 @@ async fn test_re_request_refreshes_commit_nonce_without_extending_timeout() { delegation_rent_payer, _, blockhash, - ) = setup_request_timeout_env_with_nonces(false, 0, 1, 0).await; + ) = setup_request_timeout_env_with_commit_ids(false, 0, 1, 0).await; let request_pda = undelegation_request_pda_from_delegated_account(&DELEGATED_PDA_ID); @@ -453,7 +453,7 @@ async fn test_re_request_refreshes_commit_nonce_without_extending_timeout() { &request_before_account.data, ) .unwrap(); - assert_eq!(request_before.last_commit_nonce_at_request, 0); + assert_eq!(request_before.last_commit_id_at_request, 0); let request_ix = request_undelegation_from_owner_program(request_rent_payer.pubkey()); @@ -473,7 +473,7 @@ async fn test_re_request_refreshes_commit_nonce_without_extending_timeout() { &request_after_account.data, ) .unwrap(); - assert_eq!(request_after.last_commit_nonce_at_request, 1); + assert_eq!(request_after.last_commit_id_at_request, 1); assert_eq!(request_after.created_slot, request_before.created_slot); assert_eq!( request_after.expires_at_slot, @@ -500,7 +500,7 @@ async fn setup_request_timeout_env( with_pending_commit: bool, expires_at_slot: u64, ) -> (BanksClient, Keypair, Keypair, Keypair, Keypair, Hash) { - setup_request_timeout_env_with_nonces( + setup_request_timeout_env_with_commit_ids( with_pending_commit, expires_at_slot, 0, @@ -509,11 +509,11 @@ async fn setup_request_timeout_env( .await } -async fn setup_request_timeout_env_with_nonces( +async fn setup_request_timeout_env_with_commit_ids( with_pending_commit: bool, expires_at_slot: u64, - delegation_last_update_nonce: u64, - request_last_commit_nonce: u64, + delegation_last_commit_id: u64, + request_last_commit_id: u64, ) -> (BanksClient, Keypair, Keypair, Keypair, Keypair, Hash) { let mut program_test = ProgramTest::default(); program_test.prefer_bpf(true); @@ -539,13 +539,13 @@ async fn setup_request_timeout_env_with_nonces( add_delegation_accounts( &mut program_test, delegation_rent_payer.pubkey(), - delegation_last_update_nonce, + delegation_last_commit_id, ); add_request_account( &mut program_test, request_rent_payer.pubkey(), expires_at_slot, - request_last_commit_nonce, + request_last_commit_id, ); if with_pending_commit { add_pending_commit_accounts(&mut program_test, validator.pubkey()); @@ -594,7 +594,7 @@ fn add_delegated_account(program_test: &mut ProgramTest) { fn add_delegation_accounts( program_test: &mut ProgramTest, delegation_rent_payer: solana_program::pubkey::Pubkey, - last_update_nonce: u64, + last_commit_id: u64, ) { let delegation_record_data = get_delegation_record_data( keypair_from_bytes(&TEST_AUTHORITY).pubkey(), @@ -612,10 +612,10 @@ fn add_delegation_accounts( }, ); - let delegation_metadata_data = get_delegation_metadata_data_with_nonce( + let delegation_metadata_data = get_delegation_metadata_data_with_commit_id( delegation_rent_payer, Some(false), - last_update_nonce, + last_commit_id, ); program_test.add_account( delegation_metadata_pda_from_delegated_account(&DELEGATED_PDA_ID), @@ -634,7 +634,7 @@ fn add_request_account( program_test: &mut ProgramTest, request_rent_payer: solana_program::pubkey::Pubkey, expires_at_slot: u64, - last_commit_nonce_at_request: u64, + last_commit_id_at_request: u64, ) { let request_data = create_undelegation_request_data_with_expiry( DELEGATED_PDA_ID, @@ -642,7 +642,7 @@ fn add_request_account( request_rent_payer, 0, expires_at_slot, - last_commit_nonce_at_request, + last_commit_id_at_request, ); program_test.add_account( undelegation_request_pda_from_delegated_account(&DELEGATED_PDA_ID), From 3f0768beabb60197f72c96c164c52a96d09473c6 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Sun, 28 Jun 2026 00:21:57 +0530 Subject: [PATCH 19/30] make request_undelegation truly idempotent --- dlp-api/src/error.rs | 2 +- dlp-api/src/state/undelegation_request.rs | 3 +- src/processor/fast/request_undelegation.rs | 93 +++++++++---------- .../undelegate_with_rollback_after_timeout.rs | 22 ++--- ..._undelegate_with_rollback_after_timeout.rs | 23 +---- 5 files changed, 59 insertions(+), 84 deletions(-) diff --git a/dlp-api/src/error.rs b/dlp-api/src/error.rs index 844d05a2..64b95c32 100644 --- a/dlp-api/src/error.rs +++ b/dlp-api/src/error.rs @@ -182,7 +182,7 @@ pub enum DlpError { #[error("Invalid pending commit state for request-timeout undelegation")] InvalidPendingCommitState = 52, - #[error("Commit id did not match. Re-request to update commit id")] + #[error("Commit id did not match undelegation request checkpoint")] RollbackCommitIdMismatch = 53, #[error("An infallible error is encountered possibly due to logic error")] diff --git a/dlp-api/src/state/undelegation_request.rs b/dlp-api/src/state/undelegation_request.rs index 0ca77b5a..dbc8411f 100644 --- a/dlp-api/src/state/undelegation_request.rs +++ b/dlp-api/src/state/undelegation_request.rs @@ -27,8 +27,7 @@ pub struct UndelegationRequest { /// The first slot at which timeout rollback is allowed. pub expires_at_slot: u64, - /// DelegationMetadata.last_commit_id observed when the request was last - /// made or refreshed. + /// DelegationMetadata.last_commit_id observed when the request was created. pub last_commit_id_at_request: u64, /// PDA bump for this request. diff --git a/src/processor/fast/request_undelegation.rs b/src/processor/fast/request_undelegation.rs index 4ada5363..f82d51eb 100644 --- a/src/processor/fast/request_undelegation.rs +++ b/src/processor/fast/request_undelegation.rs @@ -91,20 +91,10 @@ pub fn process_request_undelegation( ProgramError::InvalidAccountOwner ); - let mut delegation_metadata = - DelegationMetadataFast::from_account(delegation_metadata_account)?; - delegation_metadata - .set_undelegation_requester(UndelegationRequester::OwnerProgram); - let last_commit_id_at_request = delegation_metadata.last_commit_id(); - - drop(delegation_record_data); - drop(delegation_metadata); - let request_seeds = &[ pda::UNDELEGATION_REQUEST_TAG, delegated_account.address().as_ref(), ]; - if is_uninitialized_account(undelegation_request_account) { let created_slot = Clock::get()?.slot; let expires_at_slot = created_slot @@ -131,6 +121,12 @@ pub fn process_request_undelegation( payer, )?; + let mut delegation_metadata = + DelegationMetadataFast::from_account(delegation_metadata_account)?; + delegation_metadata + .set_undelegation_requester(UndelegationRequester::OwnerProgram); + let last_commit_id_at_request = delegation_metadata.last_commit_id(); + let request = UndelegationRequest { delegated_account: *delegated_account.address(), owner_program: *owner_program.address(), @@ -147,47 +143,42 @@ pub fn process_request_undelegation( .map_err(to_pinocchio_program_error)?; return Ok(()); - } - - let request_bump = require_pda( - undelegation_request_account, - request_seeds, - &crate::fast::ID, - true, - "undelegation request", - )?; - require_owned_pda( - undelegation_request_account, - &crate::fast::ID, - "undelegation request", - )?; - - let mut request_data = undelegation_request_account.try_borrow_mut()?; - let request = UndelegationRequest::try_from_bytes_with_discriminator_mut( - &mut request_data, - ) - .map_err(to_pinocchio_program_error)?; - - require_eq_keys!( - &request.delegated_account, - delegated_account.address(), - DlpError::InvalidUndelegationRequest - ); - require_eq_keys!( - &request.owner_program, - owner_program.address(), - DlpError::InvalidUndelegationRequest - ); - require_eq!( - request.bump, - request_bump, - DlpError::InvalidUndelegationRequest - ); + } else { + let request_bump = require_pda( + undelegation_request_account, + request_seeds, + &crate::fast::ID, + true, + "undelegation request", + )?; + require_owned_pda( + undelegation_request_account, + &crate::fast::ID, + "undelegation request", + )?; - // Refresh only the base-chain commit checkpoint. Repeated requests must not - // reset or extend the timeout, but they are allowed to make rollback an - // explicit owner-program decision after finalized base state has changed. - request.last_commit_id_at_request = last_commit_id_at_request; + let request_data = undelegation_request_account.try_borrow()?; + let request = UndelegationRequest::try_from_bytes_with_discriminator( + &request_data, + ) + .map_err(to_pinocchio_program_error)?; - Ok(()) + require_eq_keys!( + &request.delegated_account, + delegated_account.address(), + DlpError::InvalidUndelegationRequest + ); + require_eq_keys!( + &request.owner_program, + owner_program.address(), + DlpError::InvalidUndelegationRequest + ); + require_eq!( + request.bump, + request_bump, + DlpError::InvalidUndelegationRequest + ); + + Ok(()) + } } diff --git a/src/processor/fast/undelegate_with_rollback_after_timeout.rs b/src/processor/fast/undelegate_with_rollback_after_timeout.rs index a175608c..e0ab6408 100644 --- a/src/processor/fast/undelegate_with_rollback_after_timeout.rs +++ b/src/processor/fast/undelegate_with_rollback_after_timeout.rs @@ -124,18 +124,16 @@ pub fn process_undelegate_with_rollback_after_timeout( owner_program.address(), ProgramError::InvalidAccountOwner ); - // CHECKPOINT: This does not prove whether unfinalized ER state exists. It - // only proves that no finalized base-chain commit changed the delegated - // account since the owner program requested/refreshed undelegation. When the - // base state has moved, require a fresh owner-program re-request so rollback - // is an explicit decision against the current base-chain state. This - // minimizes, but cannot prevent, discarding newer ER state that has not been - // finalized to base. - require_eq!( - request.last_commit_id_at_request, - delegation_metadata.last_commit_id, - DlpError::RollbackCommitIdMismatch - ); + // // CHECKPOINT: This does not prove whether unfinalized ER state exists. It + // // only proves that no finalized base-chain commit changed the delegated + // // account after the owner program requested undelegation. This minimizes, + // // but cannot prevent, discarding newer ER state that has not been finalized + // // to base. + // require_eq!( + // request.last_commit_id_at_request, + // delegation_metadata.last_commit_id, + // DlpError::RollbackCommitIdMismatch + // ); require_eq_keys!( &delegation_metadata.rent_payer, delegation_rent_payer.address(), diff --git a/tests/test_undelegate_with_rollback_after_timeout.rs b/tests/test_undelegate_with_rollback_after_timeout.rs index e2f04d00..581e6510 100644 --- a/tests/test_undelegate_with_rollback_after_timeout.rs +++ b/tests/test_undelegate_with_rollback_after_timeout.rs @@ -434,12 +434,12 @@ async fn test_request_timeout_closes_pending_commit_without_applying_it() { } #[tokio::test] -async fn test_re_request_refreshes_commit_id_without_extending_timeout() { +async fn test_re_request_is_idempotent_without_extending_timeout() { let ( banks, - caller, + _caller, request_rent_payer, - delegation_rent_payer, + _delegation_rent_payer, _, blockhash, ) = setup_request_timeout_env_with_commit_ids(false, 0, 1, 0).await; @@ -468,32 +468,19 @@ async fn test_re_request_refreshes_commit_id_without_extending_timeout() { let request_after_account = banks.get_account(request_pda).await.unwrap().unwrap(); + assert_eq!(request_after_account.data, request_before_account.data); let request_after = dlp_api::state::UndelegationRequest::try_from_bytes_with_discriminator( &request_after_account.data, ) .unwrap(); - assert_eq!(request_after.last_commit_id_at_request, 1); + assert_eq!(request_after.last_commit_id_at_request, 0); assert_eq!(request_after.created_slot, request_before.created_slot); assert_eq!( request_after.expires_at_slot, request_before.expires_at_slot ); assert_eq!(request_after.rent_payer, request_rent_payer.pubkey()); - - let rollback_ix = rollback_from_owner_program( - request_rent_payer.pubkey(), - delegation_rent_payer.pubkey(), - request_rent_payer.pubkey(), - ); - let rollback_tx = Transaction::new_signed_with_payer( - &[rollback_ix], - Some(&caller.pubkey()), - &[&caller], - blockhash, - ); - let res = banks.process_transaction(rollback_tx).await; - assert!(res.is_ok()); } async fn setup_request_timeout_env( From 9301d099868f879a8457b4f4fcf159046071b955 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Sun, 28 Jun 2026 00:47:00 +0530 Subject: [PATCH 20/30] rename undelegatable -> undelegation_requester --- dlp-api/src/state/delegation_metadata.rs | 10 +++++----- src/processor/fast/commit_state.rs | 5 +++-- src/processor/fast/delegate.rs | 2 +- src/processor/fast/delegate_with_actions.rs | 2 +- src/processor/fast/request_undelegation.rs | 9 +++------ src/processor/fast/undelegate.rs | 3 ++- tests/fixtures/accounts.rs | 2 +- tests/test_commit_finalize.rs | 2 +- tests/test_commit_finalize_from_buffer.rs | 2 +- tests/test_commit_on_curve.rs | 2 +- tests/test_commit_state.rs | 2 +- tests/test_commit_state_from_buffer.rs | 2 +- tests/test_commit_state_with_program_config.rs | 2 +- tests/test_delegate_on_curve.rs | 2 +- tests/test_lamports_settlement.rs | 2 +- tests/test_request_undelegation.rs | 2 +- 16 files changed, 25 insertions(+), 26 deletions(-) diff --git a/dlp-api/src/state/delegation_metadata.rs b/dlp-api/src/state/delegation_metadata.rs index 297da9ae..d4fc7807 100644 --- a/dlp-api/src/state/delegation_metadata.rs +++ b/dlp-api/src/state/delegation_metadata.rs @@ -57,7 +57,7 @@ pub struct DelegationMetadata { /// Latest finalized commit id applied to the delegated account on base. pub last_commit_id: u64, /// Who requested undelegation, stored in the legacy undelegatable byte. - pub undelegatable: UndelegationRequester, + pub undelegation_requester: UndelegationRequester, /// The seeds of the account, used to reopen it on undelegation pub seeds: Vec>, /// The account that paid the rent for the delegation PDAs @@ -76,7 +76,7 @@ impl<'a> DelegationMetadataFast<'a> { account.data_len(), AccountDiscriminator::SPACE + 8 // last_commit_id - + 1 // undelegatable + + 1 // undelegation_requester + 32 // rent_payer + 4, // seeds (at least 4) ProgramError::InvalidAccountData @@ -146,7 +146,7 @@ impl DelegationMetadata { pub fn serialized_size(&self) -> usize { AccountDiscriminator::SPACE + 8 // last_commit_id (u64) - + 1 // undelegatable (UndelegationRequester) + + 1 // undelegation_requester (UndelegationRequester) + 32 // rent_payer (Pubkey) + (4 + self.seeds.iter().map(|s| 4 + s.len()).sum::()) // seeds (Vec>) } @@ -171,7 +171,7 @@ mod tests { 211, 164, 97, 139, 136, 136, 77, ], ], - undelegatable: UndelegationRequester::None, + undelegation_requester: UndelegationRequester::None, last_commit_id: 0, rent_payer: Pubkey::default(), }; @@ -213,7 +213,7 @@ mod tests { DelegationMetadata::try_from_slice(&serialized) .expect("Deserialization failed"); - assert_eq!(deserialized.undelegatable, expected); + assert_eq!(deserialized.undelegation_requester, expected); assert_eq!(deserialized.last_commit_id, 42); } } diff --git a/src/processor/fast/commit_state.rs b/src/processor/fast/commit_state.rs index f3722eed..bf31c69c 100644 --- a/src/processor/fast/commit_state.rs +++ b/src/processor/fast/commit_state.rs @@ -168,14 +168,15 @@ pub(crate) fn process_commit_state_internal( } // Once undelegation has been requested, any subsequent commit should fail. - if delegation_metadata.undelegatable != UndelegationRequester::None { + if delegation_metadata.undelegation_requester != UndelegationRequester::None + { log!("delegation metadata already has an undelegation requester: "); args.delegation_metadata_account.address().log(); return Err(DlpError::AlreadyUndelegated.into()); } // Record whether this commit requested undelegation. - delegation_metadata.undelegatable = + delegation_metadata.undelegation_requester = UndelegationRequester::from_allow_undelegation(args.allow_undelegation); delegation_metadata .to_bytes_with_discriminator(&mut delegation_metadata_data.as_mut()) diff --git a/src/processor/fast/delegate.rs b/src/processor/fast/delegate.rs index 92e8a4fd..b768a22d 100644 --- a/src/processor/fast/delegate.rs +++ b/src/processor/fast/delegate.rs @@ -242,7 +242,7 @@ fn process_delegate_inner( let delegation_metadata = DelegationMetadata { seeds: args.seeds, last_commit_id: 0, - undelegatable: UndelegationRequester::None, + undelegation_requester: UndelegationRequester::None, rent_payer: payer.address().to_bytes().into(), }; diff --git a/src/processor/fast/delegate_with_actions.rs b/src/processor/fast/delegate_with_actions.rs index a7eef636..d78bfd0d 100644 --- a/src/processor/fast/delegate_with_actions.rs +++ b/src/processor/fast/delegate_with_actions.rs @@ -254,7 +254,7 @@ pub fn process_delegate_with_actions( let delegation_metadata = DelegationMetadata { seeds: args.delegate.seeds, last_commit_id: 0, - undelegatable: UndelegationRequester::None, + undelegation_requester: UndelegationRequester::None, rent_payer: payer.address().to_bytes().into(), }; diff --git a/src/processor/fast/request_undelegation.rs b/src/processor/fast/request_undelegation.rs index f82d51eb..bb161691 100644 --- a/src/processor/fast/request_undelegation.rs +++ b/src/processor/fast/request_undelegation.rs @@ -41,8 +41,6 @@ pub fn process_request_undelegation( accounts: &[AccountView], data: &[u8], ) -> ProgramResult { - require!(data.is_empty(), ProgramError::InvalidInstructionData); - let [ payer, // force multi-line delegated_account, @@ -53,6 +51,8 @@ pub fn process_request_undelegation( _system_program, ] = require_n_accounts!(accounts, 7); + require!(data.is_empty(), ProgramError::InvalidInstructionData); + require_signer(payer, "payer")?; require!(payer.is_writable(), ProgramError::Immutable); require_signer(delegated_account, "delegated account")?; @@ -141,8 +141,6 @@ pub fn process_request_undelegation( request .to_bytes_with_discriminator(&mut request_data) .map_err(to_pinocchio_program_error)?; - - return Ok(()); } else { let request_bump = require_pda( undelegation_request_account, @@ -178,7 +176,6 @@ pub fn process_request_undelegation( request_bump, DlpError::InvalidUndelegationRequest ); - - Ok(()) } + Ok(()) } diff --git a/src/processor/fast/undelegate.rs b/src/processor/fast/undelegate.rs index 3255e3e7..edb2ea8c 100644 --- a/src/processor/fast/undelegate.rs +++ b/src/processor/fast/undelegate.rs @@ -194,7 +194,8 @@ pub fn process_undelegate( let delegation_last_commit_id = delegation_metadata.last_commit_id; // Check if undelegation has been requested for the delegated account. - if delegation_metadata.undelegatable == UndelegationRequester::None { + if delegation_metadata.undelegation_requester == UndelegationRequester::None + { log!("delegation metadata has no undelegation requester: "); delegation_metadata_account.address().log(); return Err(DlpError::NotUndelegatable.into()); diff --git a/tests/fixtures/accounts.rs b/tests/fixtures/accounts.rs index 4ca57569..c791859e 100644 --- a/tests/fixtures/accounts.rs +++ b/tests/fixtures/accounts.rs @@ -177,7 +177,7 @@ pub fn create_delegation_metadata_data_with_commit_id( ) -> Vec { let delegation_metadata = DelegationMetadata { last_commit_id, - undelegatable: UndelegationRequester::from_allow_undelegation( + undelegation_requester: UndelegationRequester::from_allow_undelegation( undelegatable, ), seeds: seeds.iter().map(|s| s.to_vec()).collect(), diff --git a/tests/test_commit_finalize.rs b/tests/test_commit_finalize.rs index cecce9af..3f78ecf8 100644 --- a/tests/test_commit_finalize.rs +++ b/tests/test_commit_finalize.rs @@ -116,7 +116,7 @@ async fn run_test_commit_finalize( .unwrap(); assert_eq!( - delegation_metadata.undelegatable, + delegation_metadata.undelegation_requester, dlp_api::state::UndelegationRequester::Validator ); } diff --git a/tests/test_commit_finalize_from_buffer.rs b/tests/test_commit_finalize_from_buffer.rs index 9513cbcb..5c861190 100644 --- a/tests/test_commit_finalize_from_buffer.rs +++ b/tests/test_commit_finalize_from_buffer.rs @@ -94,7 +94,7 @@ async fn test_commit_finalize_from_buffer_perf() { .unwrap(); assert_eq!( - delegation_metadata.undelegatable, + delegation_metadata.undelegation_requester, dlp_api::state::UndelegationRequester::Validator ); } diff --git a/tests/test_commit_on_curve.rs b/tests/test_commit_on_curve.rs index db12b358..17319739 100644 --- a/tests/test_commit_on_curve.rs +++ b/tests/test_commit_on_curve.rs @@ -92,7 +92,7 @@ async fn test_commit_on_curve() { ) .unwrap(); assert_eq!( - delegation_metadata.undelegatable, + delegation_metadata.undelegation_requester, dlp_api::state::UndelegationRequester::Validator ); } diff --git a/tests/test_commit_state.rs b/tests/test_commit_state.rs index 21757885..c6221217 100644 --- a/tests/test_commit_state.rs +++ b/tests/test_commit_state.rs @@ -98,7 +98,7 @@ async fn test_commit_new_state() { ) .unwrap(); assert_eq!( - delegation_metadata.undelegatable, + delegation_metadata.undelegation_requester, dlp_api::state::UndelegationRequester::Validator ); } diff --git a/tests/test_commit_state_from_buffer.rs b/tests/test_commit_state_from_buffer.rs index d8063ed3..7a76aab5 100644 --- a/tests/test_commit_state_from_buffer.rs +++ b/tests/test_commit_state_from_buffer.rs @@ -102,7 +102,7 @@ async fn test_commit_new_state_from_buffer() { ) .unwrap(); assert_eq!( - delegation_metadata.undelegatable, + delegation_metadata.undelegation_requester, dlp_api::state::UndelegationRequester::Validator ); } diff --git a/tests/test_commit_state_with_program_config.rs b/tests/test_commit_state_with_program_config.rs index 216b2f6b..b3365b97 100644 --- a/tests/test_commit_state_with_program_config.rs +++ b/tests/test_commit_state_with_program_config.rs @@ -113,7 +113,7 @@ async fn test_commit_new_state(valid_config: bool) { ) .unwrap(); assert_eq!( - delegation_metadata.undelegatable, + delegation_metadata.undelegation_requester, dlp_api::state::UndelegationRequester::Validator ); } diff --git a/tests/test_delegate_on_curve.rs b/tests/test_delegate_on_curve.rs index 77e78686..a37a3aec 100644 --- a/tests/test_delegate_on_curve.rs +++ b/tests/test_delegate_on_curve.rs @@ -131,7 +131,7 @@ async fn test_delegate_on_curve() { ) .unwrap(); assert_eq!( - delegation_metadata.undelegatable, + delegation_metadata.undelegation_requester, dlp_api::state::UndelegationRequester::None ); } diff --git a/tests/test_lamports_settlement.rs b/tests/test_lamports_settlement.rs index 7b0a9cce..40cf41e7 100644 --- a/tests/test_lamports_settlement.rs +++ b/tests/test_lamports_settlement.rs @@ -1216,7 +1216,7 @@ async fn commit_new_state(args: CommitNewStateArgs<'_>) { ) .unwrap(); assert_eq!( - delegation_metadata.undelegatable, + delegation_metadata.undelegation_requester, dlp_api::state::UndelegationRequester::Validator ); } diff --git a/tests/test_request_undelegation.rs b/tests/test_request_undelegation.rs index 10a58b5e..0ab3f5f0 100644 --- a/tests/test_request_undelegation.rs +++ b/tests/test_request_undelegation.rs @@ -93,7 +93,7 @@ async fn test_request_undelegation_creates_request() { ) .unwrap(); assert_eq!( - delegation_metadata.undelegatable, + delegation_metadata.undelegation_requester, UndelegationRequester::OwnerProgram ); } From fb847a7f77332fe4f2661a3ae1a1ac0c901fba93 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Sun, 28 Jun 2026 18:27:31 +0530 Subject: [PATCH 21/30] be compatible with borsh 0.10 --- dlp-api/src/state/delegation_metadata.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/dlp-api/src/state/delegation_metadata.rs b/dlp-api/src/state/delegation_metadata.rs index d4fc7807..79d2f077 100644 --- a/dlp-api/src/state/delegation_metadata.rs +++ b/dlp-api/src/state/delegation_metadata.rs @@ -15,19 +15,18 @@ use crate::{ #[derive( BorshSerialize, BorshDeserialize, Clone, Copy, Debug, Eq, PartialEq, )] -#[borsh(use_discriminant = true)] #[repr(u8)] pub enum UndelegationRequester { /// No undelegation request has been recorded. /// /// Kept backward compatible with legacy `is_undelegatable = false`. - None = 0, + None, /// The validator requested undelegation through a commit/finalize path. /// /// Kept backward compatible with legacy `is_undelegatable = true`. - Validator = 1, + Validator, /// The account owner program requested undelegation. - OwnerProgram = 2, + OwnerProgram, } impl UndelegationRequester { From ac0600f0e2c97d3b6233097512de003a6d6a688f Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Sun, 28 Jun 2026 19:02:43 +0530 Subject: [PATCH 22/30] increase timeout slots and remove stale test --- dlp-api/src/consts.rs | 4 +- .../undelegate_with_rollback_after_timeout.rs | 10 --- ..._undelegate_with_rollback_after_timeout.rs | 84 ------------------- 3 files changed, 2 insertions(+), 96 deletions(-) diff --git a/dlp-api/src/consts.rs b/dlp-api/src/consts.rs index 49ff3b7a..db940329 100644 --- a/dlp-api/src/consts.rs +++ b/dlp-api/src/consts.rs @@ -15,8 +15,8 @@ pub const COMMIT_FEE_LAMPORTS: u64 = 100_000; pub const SESSION_FEE_LAMPORTS: u64 = 300_000; /// Default and minimum timeout for requested undelegation. -/// Assuming 1 slot is roughly 400ms, then 4500 slots = 30min. -pub const DEFAULT_UNDELEGATION_REQUEST_TIMEOUT_SLOTS: u64 = 4500; +/// Assuming 1 slot is roughly 400ms, then 9000 slots = 60min. +pub const DEFAULT_UNDELEGATION_REQUEST_TIMEOUT_SLOTS: u64 = 9000; /// The discriminator for the external undelegate instruction. pub const EXTERNAL_UNDELEGATE_DISCRIMINATOR: [u8; 8] = diff --git a/src/processor/fast/undelegate_with_rollback_after_timeout.rs b/src/processor/fast/undelegate_with_rollback_after_timeout.rs index e0ab6408..44da8ffc 100644 --- a/src/processor/fast/undelegate_with_rollback_after_timeout.rs +++ b/src/processor/fast/undelegate_with_rollback_after_timeout.rs @@ -124,16 +124,6 @@ pub fn process_undelegate_with_rollback_after_timeout( owner_program.address(), ProgramError::InvalidAccountOwner ); - // // CHECKPOINT: This does not prove whether unfinalized ER state exists. It - // // only proves that no finalized base-chain commit changed the delegated - // // account after the owner program requested undelegation. This minimizes, - // // but cannot prevent, discarding newer ER state that has not been finalized - // // to base. - // require_eq!( - // request.last_commit_id_at_request, - // delegation_metadata.last_commit_id, - // DlpError::RollbackCommitIdMismatch - // ); require_eq_keys!( &delegation_metadata.rent_payer, delegation_rent_payer.address(), diff --git a/tests/test_undelegate_with_rollback_after_timeout.rs b/tests/test_undelegate_with_rollback_after_timeout.rs index 581e6510..de284b10 100644 --- a/tests/test_undelegate_with_rollback_after_timeout.rs +++ b/tests/test_undelegate_with_rollback_after_timeout.rs @@ -275,90 +275,6 @@ async fn test_undelegate_with_rollback_after_timeout_rejects_before_expiry() { ); } -#[tokio::test] -async fn test_undelegate_with_rollback_after_timeout_rejects_stale_request() { - let ( - banks, - caller, - request_rent_payer, - delegation_rent_payer, - _, - blockhash, - ) = setup_request_timeout_env_with_commit_ids(false, 0, 1, 0).await; - - let request_pda = - undelegation_request_pda_from_delegated_account(&DELEGATED_PDA_ID); - let delegation_record_pda = - delegation_record_pda_from_delegated_account(&DELEGATED_PDA_ID); - let delegation_metadata_pda = - delegation_metadata_pda_from_delegated_account(&DELEGATED_PDA_ID); - - let request_before = banks.get_account(request_pda).await.unwrap().unwrap(); - let delegation_record_before = banks - .get_account(delegation_record_pda) - .await - .unwrap() - .unwrap(); - let delegation_metadata_before = banks - .get_account(delegation_metadata_pda) - .await - .unwrap() - .unwrap(); - let delegated_before = - banks.get_account(DELEGATED_PDA_ID).await.unwrap().unwrap(); - - let ix = rollback_from_owner_program( - request_rent_payer.pubkey(), - delegation_rent_payer.pubkey(), - request_rent_payer.pubkey(), - ); - let tx = Transaction::new_signed_with_payer( - &[ix], - Some(&caller.pubkey()), - &[&caller], - blockhash, - ); - - let err = banks.process_transaction(tx).await.unwrap_err(); - assert!( - matches!( - err, - BanksClientError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(code), - ) - ) if code == DlpError::RollbackCommitIdMismatch as u32 - ), - "expected RollbackCommitIdMismatch, got {err:?}" - ); - - assert_eq!( - banks.get_account(request_pda).await.unwrap().unwrap(), - request_before - ); - assert_eq!( - banks - .get_account(delegation_record_pda) - .await - .unwrap() - .unwrap(), - delegation_record_before - ); - assert_eq!( - banks - .get_account(delegation_metadata_pda) - .await - .unwrap() - .unwrap(), - delegation_metadata_before - ); - assert_eq!( - banks.get_account(DELEGATED_PDA_ID).await.unwrap().unwrap(), - delegated_before - ); -} - #[tokio::test] async fn test_request_timeout_closes_pending_commit_without_applying_it() { let ( From ac3af3bc6526ef9e6cd1b2b7bedc27497ccbf57e Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Sun, 28 Jun 2026 23:27:13 +0530 Subject: [PATCH 23/30] Allow UndelegationRequester::OwnerProgram to go through and fail iff UndelegationRequester:Validator --- src/processor/fast/commit_state.rs | 3 ++- src/processor/fast/internal/commit_finalize_internal.rs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/processor/fast/commit_state.rs b/src/processor/fast/commit_state.rs index bf31c69c..70fbcee3 100644 --- a/src/processor/fast/commit_state.rs +++ b/src/processor/fast/commit_state.rs @@ -168,7 +168,8 @@ pub(crate) fn process_commit_state_internal( } // Once undelegation has been requested, any subsequent commit should fail. - if delegation_metadata.undelegation_requester != UndelegationRequester::None + if delegation_metadata.undelegation_requester + == UndelegationRequester::Validator { log!("delegation metadata already has an undelegation requester: "); args.delegation_metadata_account.address().log(); diff --git a/src/processor/fast/internal/commit_finalize_internal.rs b/src/processor/fast/internal/commit_finalize_internal.rs index d281da75..8a13c201 100644 --- a/src/processor/fast/internal/commit_finalize_internal.rs +++ b/src/processor/fast/internal/commit_finalize_internal.rs @@ -93,7 +93,7 @@ pub(crate) fn process_commit_finalize_internal( ), )?; require!( - previous_requester == UndelegationRequester::None, + previous_requester != UndelegationRequester::Validator, DlpError::AlreadyUndelegated ); } From db7ad483ca00cfeec5791d35d87ee8a977010c52 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Thu, 2 Jul 2026 01:25:37 +0530 Subject: [PATCH 24/30] remove else-block in process_request_undelegation() --- src/processor/fast/request_undelegation.rs | 39 ++-------------------- 1 file changed, 2 insertions(+), 37 deletions(-) diff --git a/src/processor/fast/request_undelegation.rs b/src/processor/fast/request_undelegation.rs index bb161691..cd81c946 100644 --- a/src/processor/fast/request_undelegation.rs +++ b/src/processor/fast/request_undelegation.rs @@ -13,10 +13,10 @@ use crate::{ error::DlpError, pda, processor::{fast::utils::pda::create_pda, utils::curve::is_on_curve_fast}, - require, require_eq, require_eq_keys, require_n_accounts, + require, require_eq_keys, require_n_accounts, requires::{ is_uninitialized_account, require_initialized_delegation_metadata, - require_initialized_delegation_record, require_owned_pda, require_pda, + require_initialized_delegation_record, require_owned_pda, require_signer, require_uninitialized_pda, UndelegationRequestCtx, }, state::{ @@ -141,41 +141,6 @@ pub fn process_request_undelegation( request .to_bytes_with_discriminator(&mut request_data) .map_err(to_pinocchio_program_error)?; - } else { - let request_bump = require_pda( - undelegation_request_account, - request_seeds, - &crate::fast::ID, - true, - "undelegation request", - )?; - require_owned_pda( - undelegation_request_account, - &crate::fast::ID, - "undelegation request", - )?; - - let request_data = undelegation_request_account.try_borrow()?; - let request = UndelegationRequest::try_from_bytes_with_discriminator( - &request_data, - ) - .map_err(to_pinocchio_program_error)?; - - require_eq_keys!( - &request.delegated_account, - delegated_account.address(), - DlpError::InvalidUndelegationRequest - ); - require_eq_keys!( - &request.owner_program, - owner_program.address(), - DlpError::InvalidUndelegationRequest - ); - require_eq!( - request.bump, - request_bump, - DlpError::InvalidUndelegationRequest - ); } Ok(()) } From 628413596e6d4212d70bf09ea7510493acf2e661 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Fri, 3 Jul 2026 03:27:07 +0530 Subject: [PATCH 25/30] use config-driven undelegation request setup in tests --- tests/test_request_undelegation.rs | 344 +++++++++++++++++++---------- 1 file changed, 222 insertions(+), 122 deletions(-) diff --git a/tests/test_request_undelegation.rs b/tests/test_request_undelegation.rs index 0ab3f5f0..1db38f6c 100644 --- a/tests/test_request_undelegation.rs +++ b/tests/test_request_undelegation.rs @@ -47,7 +47,16 @@ const TEST_PDA_SEED: &[u8] = b"test-pda"; #[tokio::test] async fn test_request_undelegation_creates_request() { - let (banks, payer, _, blockhash) = setup_request_env().await; + let SetupContext { + banks, + payer, + blockhash, + .. + } = setup_env(SetupConfig { + with_request_wrapper: true, + ..Default::default() + }) + .await; let ix = request_undelegation_from_owner_program(payer.pubkey()); let tx = Transaction::new_signed_with_payer( @@ -100,7 +109,17 @@ async fn test_request_undelegation_creates_request() { #[tokio::test] async fn test_request_undelegation_is_idempotent() { - let (banks, payer, second_payer, blockhash) = setup_request_env().await; + let SetupContext { + banks, + payer, + second_payer, + blockhash, + .. + } = setup_env(SetupConfig { + with_request_wrapper: true, + ..Default::default() + }) + .await; let first_ix = request_undelegation_from_owner_program(payer.pubkey()); let first_tx = Transaction::new_signed_with_payer( @@ -134,7 +153,16 @@ async fn test_request_undelegation_is_idempotent() { #[tokio::test] async fn test_request_undelegation_rejects_payload() { - let (banks, payer, _, blockhash) = setup_request_env().await; + let SetupContext { + banks, + payer, + blockhash, + .. + } = setup_env(SetupConfig { + with_request_wrapper: true, + ..Default::default() + }) + .await; let mut ix = request_undelegation_from_owner_program(payer.pubkey()); ix.data = 123_u64.to_le_bytes().to_vec(); @@ -162,7 +190,16 @@ async fn test_request_undelegation_rejects_payload() { #[tokio::test] async fn test_request_undelegation_rejects_missing_delegated_signer() { - let (banks, payer, _, blockhash) = setup_request_env().await; + let SetupContext { + banks, + payer, + blockhash, + .. + } = setup_env(SetupConfig { + with_request_wrapper: true, + ..Default::default() + }) + .await; let request_pda = undelegation_request_pda_from_delegated_account(&DELEGATED_PDA_ID); @@ -201,12 +238,22 @@ async fn test_request_undelegation_rejects_missing_delegated_signer() { #[tokio::test] async fn test_request_undelegation_rejects_on_curve_delegated_account() { - let (banks, payer, delegated_on_curve, blockhash) = - setup_on_curve_request_env().await; + let SetupContext { + banks, + payer, + delegated_on_curve, + delegated_account, + blockhash, + .. + } = setup_env(SetupConfig { + delegated_account: DelegatedAccountSetup::OnCurveKeypair, + ..Default::default() + }) + .await; let ix = dlp_api::instruction_builder::request_undelegation( payer.pubkey(), - delegated_on_curve.pubkey(), + delegated_account, system_program::id(), ); let tx = Transaction::new_signed_with_payer( @@ -222,8 +269,21 @@ async fn test_request_undelegation_rejects_on_curve_delegated_account() { #[tokio::test] async fn test_undelegate_with_request_closes_request() { - let (banks, _, authority, request_rent_payer, blockhash) = - setup_undelegate_with_request_env().await; + let SetupContext { + banks, + authority, + request_rent_payer, + blockhash, + .. + } = setup_env(SetupConfig { + metadata_undelegatable: true, + with_commit_accounts: true, + with_owner_program: true, + with_fee_accounts: true, + with_request_account: true, + ..Default::default() + }) + .await; let request_pda = undelegation_request_pda_from_delegated_account(&DELEGATED_PDA_ID); @@ -279,8 +339,21 @@ async fn test_undelegate_with_request_closes_request() { #[tokio::test] async fn test_undelegate_with_malformed_optional_request_accounts_rejected() { - let (banks, _, authority, request_rent_payer, blockhash) = - setup_undelegate_with_request_env().await; + let SetupContext { + banks, + authority, + request_rent_payer, + blockhash, + .. + } = setup_env(SetupConfig { + metadata_undelegatable: true, + with_commit_accounts: true, + with_owner_program: true, + with_fee_accounts: true, + with_request_account: true, + ..Default::default() + }) + .await; let ix_finalize = dlp_api::instruction_builder::finalize( authority.pubkey(), @@ -394,101 +467,71 @@ fn request_undelegation_from_owner_program(payer: Pubkey) -> Instruction { } } -async fn setup_request_env() -> (BanksClient, Keypair, Keypair, Hash) { - let mut program_test = ProgramTest::default(); - program_test.prefer_bpf(true); - program_test.add_program("dlp", dlp_api::ID, None); - program_test.prefer_bpf(false); - program_test.add_program( - "request-wrapper", - DELEGATED_PDA_OWNER_ID, - processor!( - imaginary_program_processor_requesting_undelegation_through_cpi - ), - ); - program_test.prefer_bpf(true); - - let payer = Keypair::new(); - let second_payer = Keypair::new(); - let authority = keypair_from_bytes(&TEST_AUTHORITY); +#[derive(Clone, Copy)] +enum DelegatedAccountSetup { + OffCurvePda, + OnCurveKeypair, +} - add_system_account(&mut program_test, payer.pubkey(), LAMPORTS_PER_SOL); - add_system_account( - &mut program_test, - second_payer.pubkey(), - LAMPORTS_PER_SOL, - ); - add_system_account(&mut program_test, authority.pubkey(), LAMPORTS_PER_SOL); +impl Default for DelegatedAccountSetup { + fn default() -> Self { + Self::OffCurvePda + } +} - add_delegated_account(&mut program_test, DELEGATED_PDA_ID); - add_delegation_accounts(&mut program_test, authority.pubkey()); +#[derive(Default)] +struct SetupConfig { + delegated_account: DelegatedAccountSetup, + with_request_wrapper: bool, + metadata_undelegatable: bool, + with_commit_accounts: bool, + with_owner_program: bool, + with_fee_accounts: bool, + with_request_account: bool, +} - let (banks, _, blockhash) = program_test.start().await; - (banks, payer, second_payer, blockhash) +struct SetupContext { + banks: BanksClient, + payer: Keypair, + second_payer: Keypair, + authority: Keypair, + request_rent_payer: Keypair, + delegated_on_curve: Keypair, + delegated_account: Pubkey, + blockhash: Hash, } -async fn setup_on_curve_request_env() -> (BanksClient, Keypair, Keypair, Hash) { +async fn setup_env(config: SetupConfig) -> SetupContext { let mut program_test = ProgramTest::new("dlp", dlp_api::ID, None); program_test.prefer_bpf(true); + if config.with_request_wrapper { + program_test.prefer_bpf(false); + program_test.add_program( + "request-wrapper", + DELEGATED_PDA_OWNER_ID, + processor!( + imaginary_program_processor_requesting_undelegation_through_cpi + ), + ); + program_test.prefer_bpf(true); + } + let payer = Keypair::new(); + let second_payer = Keypair::new(); + let authority = keypair_from_bytes(&TEST_AUTHORITY); + let request_rent_payer = Keypair::new(); let delegated_on_curve = keypair_from_bytes(&ON_CURVE_KEYPAIR); + let delegated_account = match config.delegated_account { + DelegatedAccountSetup::OffCurvePda => DELEGATED_PDA_ID, + DelegatedAccountSetup::OnCurveKeypair => delegated_on_curve.pubkey(), + }; add_system_account(&mut program_test, payer.pubkey(), LAMPORTS_PER_SOL); - program_test.add_account( - delegated_on_curve.pubkey(), - Account { - lamports: LAMPORTS_PER_SOL, - data: vec![], - owner: dlp_api::id(), - executable: false, - rent_epoch: 0, - }, - ); - - let delegation_record_data = get_delegation_record_on_curve_data( - delegated_on_curve.pubkey(), - Some(LAMPORTS_PER_SOL), - ); - program_test.add_account( - delegation_record_pda_from_delegated_account( - &delegated_on_curve.pubkey(), - ), - Account { - lamports: Rent::default() - .minimum_balance(delegation_record_data.len()), - data: delegation_record_data, - owner: dlp_api::id(), - executable: false, - rent_epoch: 0, - }, - ); - let delegation_metadata_data = - get_delegation_metadata_data_on_curve(payer.pubkey(), Some(false)); - program_test.add_account( - delegation_metadata_pda_from_delegated_account( - &delegated_on_curve.pubkey(), - ), - Account { - lamports: Rent::default() - .minimum_balance(delegation_metadata_data.len()), - data: delegation_metadata_data, - owner: dlp_api::id(), - executable: false, - rent_epoch: 0, - }, + add_system_account( + &mut program_test, + second_payer.pubkey(), + LAMPORTS_PER_SOL, ); - - let (banks, _, blockhash) = program_test.start().await; - (banks, payer, delegated_on_curve, blockhash) -} - -async fn setup_undelegate_with_request_env( -) -> (BanksClient, Keypair, Keypair, Keypair, Hash) { - let mut program_test = ProgramTest::new("dlp", dlp_api::ID, None); - program_test.prefer_bpf(true); - let authority = keypair_from_bytes(&TEST_AUTHORITY); - let request_rent_payer = Keypair::new(); - add_system_account(&mut program_test, authority.pubkey(), LAMPORTS_PER_SOL); add_system_account( &mut program_test, @@ -496,19 +539,62 @@ async fn setup_undelegate_with_request_env( LAMPORTS_PER_SOL, ); - add_delegated_account(&mut program_test, DELEGATED_PDA_ID); - add_delegation_accounts_with_metadata( - &mut program_test, - authority.pubkey(), - Some(true), - ); - add_commit_accounts(&mut program_test, authority.pubkey()); - add_owner_program(&mut program_test); - add_fee_accounts(&mut program_test, authority.pubkey()); - add_request_account(&mut program_test, request_rent_payer.pubkey()); + add_delegated_account(&mut program_test, delegated_account); + match config.delegated_account { + DelegatedAccountSetup::OffCurvePda => { + add_delegation_accounts_with_metadata( + &mut program_test, + delegated_account, + authority.pubkey(), + authority.pubkey(), + config.metadata_undelegatable, + false, + ); + } + DelegatedAccountSetup::OnCurveKeypair => { + add_delegation_accounts_with_metadata( + &mut program_test, + delegated_account, + delegated_on_curve.pubkey(), + payer.pubkey(), + config.metadata_undelegatable, + true, + ); + } + } + + if config.with_commit_accounts { + add_commit_accounts( + &mut program_test, + delegated_account, + authority.pubkey(), + ); + } + if config.with_owner_program { + add_owner_program(&mut program_test); + } + if config.with_fee_accounts { + add_fee_accounts(&mut program_test, authority.pubkey()); + } + if config.with_request_account { + add_request_account( + &mut program_test, + delegated_account, + request_rent_payer.pubkey(), + ); + } - let (banks, payer, blockhash) = program_test.start().await; - (banks, payer, authority, request_rent_payer, blockhash) + let (banks, _, blockhash) = program_test.start().await; + SetupContext { + banks, + payer, + second_payer, + authority, + request_rent_payer, + delegated_on_curve, + delegated_account, + blockhash, + } } fn add_system_account( @@ -541,18 +627,21 @@ fn add_delegated_account(program_test: &mut ProgramTest, pubkey: Pubkey) { ); } -fn add_delegation_accounts(program_test: &mut ProgramTest, authority: Pubkey) { - add_delegation_accounts_with_metadata(program_test, authority, Some(false)); -} - fn add_delegation_accounts_with_metadata( program_test: &mut ProgramTest, + delegated_account: Pubkey, authority: Pubkey, - undelegatable: Option, + rent_payer: Pubkey, + undelegatable: bool, + on_curve: bool, ) { - let delegation_record_data = get_delegation_record_data(authority, None); + let delegation_record_data = if on_curve { + get_delegation_record_on_curve_data(authority, Some(LAMPORTS_PER_SOL)) + } else { + get_delegation_record_data(authority, None) + }; program_test.add_account( - delegation_record_pda_from_delegated_account(&DELEGATED_PDA_ID), + delegation_record_pda_from_delegated_account(&delegated_account), Account { lamports: Rent::default() .minimum_balance(delegation_record_data.len()), @@ -563,10 +652,13 @@ fn add_delegation_accounts_with_metadata( }, ); - let delegation_metadata_data = - get_delegation_metadata_data(authority, undelegatable); + let delegation_metadata_data = if on_curve { + get_delegation_metadata_data_on_curve(rent_payer, Some(undelegatable)) + } else { + get_delegation_metadata_data(rent_payer, Some(undelegatable)) + }; program_test.add_account( - delegation_metadata_pda_from_delegated_account(&DELEGATED_PDA_ID), + delegation_metadata_pda_from_delegated_account(&delegated_account), Account { lamports: Rent::default() .minimum_balance(delegation_metadata_data.len()), @@ -578,9 +670,13 @@ fn add_delegation_accounts_with_metadata( ); } -fn add_commit_accounts(program_test: &mut ProgramTest, authority: Pubkey) { +fn add_commit_accounts( + program_test: &mut ProgramTest, + delegated_account: Pubkey, + authority: Pubkey, +) { program_test.add_account( - commit_state_pda_from_delegated_account(&DELEGATED_PDA_ID), + commit_state_pda_from_delegated_account(&delegated_account), Account { lamports: LAMPORTS_PER_SOL, data: COMMIT_NEW_STATE_ACCOUNT_DATA.into(), @@ -592,7 +688,7 @@ fn add_commit_accounts(program_test: &mut ProgramTest, authority: Pubkey) { let commit_record_data = get_commit_record_account_data(authority); program_test.add_account( - commit_record_pda_from_delegated_account(&DELEGATED_PDA_ID), + commit_record_pda_from_delegated_account(&delegated_account), Account { lamports: Rent::default().minimum_balance(commit_record_data.len()), data: commit_record_data, @@ -640,15 +736,19 @@ fn add_fee_accounts(program_test: &mut ProgramTest, authority: Pubkey) { ); } -fn add_request_account(program_test: &mut ProgramTest, rent_payer: Pubkey) { +fn add_request_account( + program_test: &mut ProgramTest, + delegated_account: Pubkey, + rent_payer: Pubkey, +) { let request_data = create_undelegation_request_data( - DELEGATED_PDA_ID, + delegated_account, DELEGATED_PDA_OWNER_ID, rent_payer, 1, ); program_test.add_account( - undelegation_request_pda_from_delegated_account(&DELEGATED_PDA_ID), + undelegation_request_pda_from_delegated_account(&delegated_account), Account { lamports: Rent::default().minimum_balance(request_data.len()), data: request_data, From 212d3445ed6384caf3661fbc34e35df4ba9fcd40 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Tue, 7 Jul 2026 02:56:45 +0530 Subject: [PATCH 26/30] require undelegation request accounts for owner-program requests in Undelegate --- dlp-api/src/error.rs | 3 + src/processor/fast/commit_state.rs | 15 +- .../fast/internal/commit_finalize_internal.rs | 13 +- src/processor/fast/request_undelegation.rs | 2 +- src/processor/fast/undelegate.rs | 6 + tests/test_request_undelegation.rs | 180 ++++++++++++++++++ 6 files changed, 209 insertions(+), 10 deletions(-) diff --git a/dlp-api/src/error.rs b/dlp-api/src/error.rs index 64b95c32..7c429214 100644 --- a/dlp-api/src/error.rs +++ b/dlp-api/src/error.rs @@ -185,6 +185,9 @@ pub enum DlpError { #[error("Commit id did not match undelegation request checkpoint")] RollbackCommitIdMismatch = 53, + #[error("Undelegation request accounts are required")] + MissingUndelegationRequest = 54, + #[error("An infallible error is encountered possibly due to logic error")] InfallibleError = 100, } diff --git a/src/processor/fast/commit_state.rs b/src/processor/fast/commit_state.rs index 70fbcee3..3bcf65d7 100644 --- a/src/processor/fast/commit_state.rs +++ b/src/processor/fast/commit_state.rs @@ -167,7 +167,8 @@ pub(crate) fn process_commit_state_internal( return Err(DlpError::NonceOutOfOrder.into()); } - // Once undelegation has been requested, any subsequent commit should fail. + // Validator-requested undelegation is recorded by commit/finalize itself, + // so a later commit must not run again once that request exists. if delegation_metadata.undelegation_requester == UndelegationRequester::Validator { @@ -176,9 +177,15 @@ pub(crate) fn process_commit_state_internal( return Err(DlpError::AlreadyUndelegated.into()); } - // Record whether this commit requested undelegation. - delegation_metadata.undelegation_requester = - UndelegationRequester::from_allow_undelegation(args.allow_undelegation); + // Record whether this commit requested undelegation. Preserve + // OwnerProgram requests so Undelegate can require request accounts. + if delegation_metadata.undelegation_requester == UndelegationRequester::None + { + delegation_metadata.undelegation_requester = + UndelegationRequester::from_allow_undelegation( + args.allow_undelegation, + ); + } delegation_metadata .to_bytes_with_discriminator(&mut delegation_metadata_data.as_mut()) .map_err(to_pinocchio_program_error)?; diff --git a/src/processor/fast/internal/commit_finalize_internal.rs b/src/processor/fast/internal/commit_finalize_internal.rs index 8a13c201..1ef95640 100644 --- a/src/processor/fast/internal/commit_finalize_internal.rs +++ b/src/processor/fast/internal/commit_finalize_internal.rs @@ -87,15 +87,18 @@ pub(crate) fn process_commit_finalize_internal( require_eq!(args.commit_id, prev_id + 1, DlpError::NonceOutOfOrder); - let previous_requester = metadata.replace_undelegation_requester( - UndelegationRequester::from_allow_undelegation( - args.allow_undelegation, - ), - )?; + let previous_requester = metadata.undelegation_requester()?; require!( previous_requester != UndelegationRequester::Validator, DlpError::AlreadyUndelegated ); + if previous_requester == UndelegationRequester::None { + metadata.set_undelegation_requester( + UndelegationRequester::from_allow_undelegation( + args.allow_undelegation, + ), + ); + } } let mut delegation_record_data = diff --git a/src/processor/fast/request_undelegation.rs b/src/processor/fast/request_undelegation.rs index cd81c946..b641f0b5 100644 --- a/src/processor/fast/request_undelegation.rs +++ b/src/processor/fast/request_undelegation.rs @@ -54,7 +54,7 @@ pub fn process_request_undelegation( require!(data.is_empty(), ProgramError::InvalidInstructionData); require_signer(payer, "payer")?; - require!(payer.is_writable(), ProgramError::Immutable); + require_signer(delegated_account, "delegated account")?; require_owned_pda( delegated_account, diff --git a/src/processor/fast/undelegate.rs b/src/processor/fast/undelegate.rs index edb2ea8c..ec278d31 100644 --- a/src/processor/fast/undelegate.rs +++ b/src/processor/fast/undelegate.rs @@ -200,6 +200,12 @@ pub fn process_undelegate( delegation_metadata_account.address().log(); return Err(DlpError::NotUndelegatable.into()); } + if delegation_metadata.undelegation_requester + == UndelegationRequester::OwnerProgram + && request_accounts.is_none() + { + return Err(DlpError::MissingUndelegationRequest.into()); + } // Check if the rent payer is correct if !address_eq( diff --git a/tests/test_request_undelegation.rs b/tests/test_request_undelegation.rs index 1db38f6c..3a797e94 100644 --- a/tests/test_request_undelegation.rs +++ b/tests/test_request_undelegation.rs @@ -1,6 +1,8 @@ use dlp::solana_program; use dlp_api::{ + args::{CommitFinalizeArgs, CommitStateArgs}, consts::DEFAULT_UNDELEGATION_REQUEST_TIMEOUT_SLOTS, + error::DlpError, pda::{ commit_record_pda_from_delegated_account, commit_state_pda_from_delegated_account, @@ -337,6 +339,184 @@ async fn test_undelegate_with_request_closes_request() { ); } +#[tokio::test] +async fn test_commit_state_preserves_owner_program_requester() { + let SetupContext { + banks, + payer, + authority, + blockhash, + .. + } = setup_env(SetupConfig { + with_request_wrapper: true, + with_fee_accounts: true, + ..Default::default() + }) + .await; + + let request_ix = request_undelegation_from_owner_program(payer.pubkey()); + let commit_ix = dlp_api::instruction_builder::commit_state( + authority.pubkey(), + DELEGATED_PDA_ID, + DELEGATED_PDA_OWNER_ID, + CommitStateArgs { + data: COMMIT_NEW_STATE_ACCOUNT_DATA.to_vec(), + nonce: 1, + allow_undelegation: false, + lamports: LAMPORTS_PER_SOL, + }, + ); + let tx = Transaction::new_signed_with_payer( + &[request_ix, commit_ix], + Some(&payer.pubkey()), + &[&payer, &authority], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok(), "{res:?}"); + + assert_delegation_metadata_requester( + &banks, + UndelegationRequester::OwnerProgram, + ) + .await; +} + +#[tokio::test] +async fn test_commit_finalize_preserves_owner_program_requester() { + let SetupContext { + banks, + payer, + authority, + blockhash, + .. + } = setup_env(SetupConfig { + with_request_wrapper: true, + with_fee_accounts: true, + ..Default::default() + }) + .await; + + let request_ix = request_undelegation_from_owner_program(payer.pubkey()); + let mut args = CommitFinalizeArgs { + commit_id: 1, + lamports: LAMPORTS_PER_SOL, + allow_undelegation: false.into(), + data_is_diff: false.into(), + bumps: Default::default(), + reserved_padding: Default::default(), + }; + let (commit_finalize_ix, _) = dlp_api::instruction_builder::commit_finalize( + authority.pubkey(), + DELEGATED_PDA_ID, + &mut args, + &COMMIT_NEW_STATE_ACCOUNT_DATA, + ); + let tx = Transaction::new_signed_with_payer( + &[request_ix, commit_finalize_ix], + Some(&payer.pubkey()), + &[&payer, &authority], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok(), "{res:?}"); + + assert_delegation_metadata_requester( + &banks, + UndelegationRequester::OwnerProgram, + ) + .await; +} + +#[tokio::test] +async fn test_undelegate_owner_program_request_without_request_accounts_rejected( +) { + let SetupContext { + banks, + payer, + authority, + blockhash, + .. + } = setup_env(SetupConfig { + with_request_wrapper: true, + with_commit_accounts: true, + with_fee_accounts: true, + ..Default::default() + }) + .await; + + let request_ix = request_undelegation_from_owner_program(payer.pubkey()); + let request_tx = Transaction::new_signed_with_payer( + &[request_ix], + Some(&payer.pubkey()), + &[&payer], + blockhash, + ); + let request_res = banks.process_transaction(request_tx).await; + assert!(request_res.is_ok(), "{request_res:?}"); + + let finalize_ix = dlp_api::instruction_builder::finalize( + authority.pubkey(), + DELEGATED_PDA_ID, + ); + let finalize_tx = Transaction::new_signed_with_payer( + &[finalize_ix], + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); + let finalize_res = banks.process_transaction(finalize_tx).await; + assert!(finalize_res.is_ok(), "{finalize_res:?}"); + + let undelegate_ix = dlp_api::instruction_builder::undelegate( + authority.pubkey(), + DELEGATED_PDA_ID, + DELEGATED_PDA_OWNER_ID, + authority.pubkey(), + ); + let undelegate_tx = Transaction::new_signed_with_payer( + &[undelegate_ix], + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); + let err = banks.process_transaction(undelegate_tx).await.unwrap_err(); + assert!( + matches!( + err, + BanksClientError::TransactionError( + TransactionError::InstructionError( + 0, + InstructionError::Custom(code), + ) + ) if code == DlpError::MissingUndelegationRequest as u32 + ), + "expected MissingUndelegationRequest, got {err:?}" + ); +} + +async fn assert_delegation_metadata_requester( + banks: &BanksClient, + expected_requester: UndelegationRequester, +) { + let delegation_metadata_pda = + delegation_metadata_pda_from_delegated_account(&DELEGATED_PDA_ID); + let delegation_metadata_account = banks + .get_account(delegation_metadata_pda) + .await + .unwrap() + .unwrap(); + let delegation_metadata = + DelegationMetadata::try_from_bytes_with_discriminator( + &delegation_metadata_account.data, + ) + .unwrap(); + assert_eq!( + delegation_metadata.undelegation_requester, + expected_requester + ); +} + #[tokio::test] async fn test_undelegate_with_malformed_optional_request_accounts_rejected() { let SetupContext { From 074a1a4175faea2e47b745e15dcabecb046e815d Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Tue, 7 Jul 2026 04:36:02 +0530 Subject: [PATCH 27/30] ensure rent_payer is same as the delegation rent payer --- .../request_undelegation.rs | 9 +- dlp-api/src/instruction_builder/undelegate.rs | 33 ++--- src/processor/fast/request_undelegation.rs | 24 +++- src/processor/fast/undelegate.rs | 60 ++++----- tests/test_request_undelegation.rs | 116 +++++++----------- ..._undelegate_with_rollback_after_timeout.rs | 14 ++- 6 files changed, 115 insertions(+), 141 deletions(-) diff --git a/dlp-api/src/instruction_builder/request_undelegation.rs b/dlp-api/src/instruction_builder/request_undelegation.rs index 510a8a1b..985bf60a 100644 --- a/dlp-api/src/instruction_builder/request_undelegation.rs +++ b/dlp-api/src/instruction_builder/request_undelegation.rs @@ -16,9 +16,12 @@ use solana_sdk_ids::system_program; use crate::compat::{Compatize, Modernize}; /// Builds a request undelegation instruction. +/// +/// `delegation_rent_payer` must match the rent payer stored in the delegation +/// metadata PDA. /// See [dlp::processor::process_request_undelegation] for docs. pub fn request_undelegation( - payer: Pubkey, + delegation_rent_payer: Pubkey, delegated_account: Pubkey, owner_program: Pubkey, ) -> Instruction { @@ -39,7 +42,7 @@ pub fn request_undelegation( Instruction { program_id: dlp::id().modernize(), accounts: vec![ - AccountMeta::new(payer, true), + AccountMeta::new(delegation_rent_payer, true), AccountMeta::new_readonly(delegated_account, true), AccountMeta::new_readonly(owner_program, false), AccountMeta::new(undelegation_request_pda, false), @@ -61,7 +64,7 @@ pub fn request_undelegation_size_budget( ) -> u32 { total_size_budget(&[ DLP_PROGRAM_DATA_SIZE_CLASS, - AccountSizeClass::Tiny, // payer + AccountSizeClass::Tiny, // delegation_rent_payer delegated_account, // delegated_account AccountSizeClass::Tiny, // owner_program AccountSizeClass::Tiny, // undelegation_request_pda diff --git a/dlp-api/src/instruction_builder/undelegate.rs b/dlp-api/src/instruction_builder/undelegate.rs index d526fcd5..4d6029ac 100644 --- a/dlp-api/src/instruction_builder/undelegate.rs +++ b/dlp-api/src/instruction_builder/undelegate.rs @@ -20,39 +20,44 @@ use solana_sdk_ids::system_program; use crate::compat::{Compatize, Modernize}; /// Builds an undelegate instruction. +/// +/// `delegation_rent_payer` must match the rent payer stored in the delegation +/// metadata PDA. /// See [dlp::processor::process_undelegate] for docs. #[allow(clippy::too_many_arguments)] pub fn undelegate( validator: Pubkey, delegated_account: Pubkey, owner_program: Pubkey, - rent_reimbursement: Pubkey, + delegation_rent_payer: Pubkey, ) -> Instruction { build_undelegate( validator, delegated_account, owner_program, - rent_reimbursement, - None, + delegation_rent_payer, + false, ) } /// Builds an undelegate instruction that also closes a matching request. +/// +/// `delegation_rent_payer` must match the rent payer stored in the delegation +/// metadata PDA and undelegation request PDA. /// See [dlp::processor::process_undelegate] for docs. #[allow(clippy::too_many_arguments)] pub fn undelegate_with_request( validator: Pubkey, delegated_account: Pubkey, owner_program: Pubkey, - rent_reimbursement: Pubkey, - request_rent_payer: Pubkey, + delegation_rent_payer: Pubkey, ) -> Instruction { build_undelegate( validator, delegated_account, owner_program, - rent_reimbursement, - Some(request_rent_payer), + delegation_rent_payer, + true, ) } @@ -60,8 +65,8 @@ fn build_undelegate( validator: Pubkey, delegated_account: Pubkey, owner_program: Pubkey, - rent_reimbursement: Pubkey, - request_rent_payer: Option, + delegation_rent_payer: Pubkey, + include_undelegation_request: bool, ) -> Instruction { let validator_compat = validator.compatize(); let delegated_account_compat = delegated_account.compatize(); @@ -94,20 +99,19 @@ fn build_undelegate( AccountMeta::new_readonly(commit_record_pda, false), AccountMeta::new(delegation_record_pda, false), AccountMeta::new(delegation_metadata_pda, false), - AccountMeta::new(rent_reimbursement, false), + AccountMeta::new(delegation_rent_payer, false), AccountMeta::new(fees_vault_pda, false), AccountMeta::new(validator_fees_vault_pda, false), AccountMeta::new_readonly(system_program::id(), false), ]; - if let Some(request_rent_payer) = request_rent_payer { + if include_undelegation_request { let undelegation_request_pda = undelegation_request_pda_from_delegated_account( &delegated_account_compat, ) .modernize(); accounts.push(AccountMeta::new(undelegation_request_pda, false)); - accounts.push(AccountMeta::new(request_rent_payer, false)); } Instruction { @@ -133,7 +137,7 @@ pub fn undelegate_size_budget(delegated_account: AccountSizeClass) -> u32 { AccountSizeClass::Tiny, // commit_record_pda AccountSizeClass::Tiny, // delegation_record_pda AccountSizeClass::Tiny, // delegation_metadata_pda - AccountSizeClass::Tiny, // rent_reimbursement + AccountSizeClass::Tiny, // delegation_rent_payer AccountSizeClass::Tiny, // fees_vault_pda AccountSizeClass::Tiny, // validator_fees_vault_pda AccountSizeClass::Tiny, // system_program @@ -159,11 +163,10 @@ pub fn undelegate_with_request_size_budget( AccountSizeClass::Tiny, // commit_record_pda AccountSizeClass::Tiny, // delegation_record_pda AccountSizeClass::Tiny, // delegation_metadata_pda - AccountSizeClass::Tiny, // rent_reimbursement + AccountSizeClass::Tiny, // delegation_rent_payer AccountSizeClass::Tiny, // fees_vault_pda AccountSizeClass::Tiny, // validator_fees_vault_pda AccountSizeClass::Tiny, // system_program AccountSizeClass::Tiny, // undelegation_request_pda - AccountSizeClass::Tiny, // request_rent_payer ]) } diff --git a/src/processor/fast/request_undelegation.rs b/src/processor/fast/request_undelegation.rs index b641f0b5..b40a5a21 100644 --- a/src/processor/fast/request_undelegation.rs +++ b/src/processor/fast/request_undelegation.rs @@ -20,8 +20,8 @@ use crate::{ require_signer, require_uninitialized_pda, UndelegationRequestCtx, }, state::{ - DelegationMetadataFast, DelegationRecord, UndelegationRequest, - UndelegationRequester, + DelegationMetadata, DelegationMetadataFast, DelegationRecord, + UndelegationRequest, UndelegationRequester, }, }; @@ -29,7 +29,7 @@ use crate::{ /// /// Accounts: /// -/// 0: `[signer, writable]` payer +/// 0: `[signer, writable]` delegation rent payer /// 1: `[signer]` delegated account /// 2: `[]` owner program of the delegated account /// 3: `[writable]` undelegation request PDA @@ -91,6 +91,20 @@ pub fn process_request_undelegation( ProgramError::InvalidAccountOwner ); + let delegation_metadata_data = delegation_metadata_account.try_borrow()?; + let delegation_metadata = + DelegationMetadata::try_from_bytes_with_discriminator( + &delegation_metadata_data, + ) + .map_err(to_pinocchio_program_error)?; + require_eq_keys!( + &delegation_metadata.rent_payer, + payer.address(), + DlpError::InvalidReimbursementAddressForDelegationRent + ); + let last_commit_id_at_request = delegation_metadata.last_commit_id; + drop(delegation_metadata_data); + let request_seeds = &[ pda::UNDELEGATION_REQUEST_TAG, delegated_account.address().as_ref(), @@ -123,9 +137,11 @@ pub fn process_request_undelegation( let mut delegation_metadata = DelegationMetadataFast::from_account(delegation_metadata_account)?; + // An explicit owner-program request carries the request PDA used by the + // owner-driven undelegation flow, so it takes precedence over any + // validator-requested undelegation marker already stored in metadata. delegation_metadata .set_undelegation_requester(UndelegationRequester::OwnerProgram); - let last_commit_id_at_request = delegation_metadata.last_commit_id(); let request = UndelegationRequest { delegated_account: *delegated_account.address(), diff --git a/src/processor/fast/undelegate.rs b/src/processor/fast/undelegate.rs index ec278d31..4534cb5b 100644 --- a/src/processor/fast/undelegate.rs +++ b/src/processor/fast/undelegate.rs @@ -21,7 +21,7 @@ use crate::{ error::DlpError, pda, processor::fast::utils::pda::{close_pda, close_pda_with_fees, create_pda}, - require_n_accounts, require_n_accounts_with_optionals, + require_n_accounts_with_optionals, requires::{ require_initialized_delegation_metadata, require_initialized_delegation_record, require_initialized_pda, @@ -48,7 +48,7 @@ use crate::{ /// 5: `[]` the commit record PDA /// 6: `[writable]` the delegation record PDA /// 7: `[writable]` the delegation metadata PDA -/// 8: `[]` the rent reimbursement account +/// 8: `[writable]` the delegation rent payer account /// 9: `[writable]` the protocol fees vault account /// 10: `[writable]` the validator fees vault account /// 11: `[]` the system program (TODO (snawaz): soon to be removed from the requirement) @@ -64,7 +64,7 @@ use crate::{ /// - commit record is uninitialized /// - undelegation has been requested for the delegated account /// - owner program account matches the owner in the delegation record -/// - rent reimbursement account matches the rent payer in the delegation metadata +/// - delegation rent payer account matches the rent payer in the delegation metadata /// /// Steps: /// @@ -93,7 +93,7 @@ pub fn process_undelegate( commit_record_account, delegation_record_account, delegation_metadata_account, - rent_reimbursement, + delegation_rent_payer, fees_vault, validator_fees_vault, system_program, @@ -101,26 +101,18 @@ pub fn process_undelegate( optional_accounts, ) = require_n_accounts_with_optionals!(accounts, 12); - let request_accounts = match optional_accounts.len() { + let request_account = match optional_accounts.len() { 0 => None, - 2 => { - let [ - undelegation_request_account, // force multi-line - request_rent_payer, - ] = require_n_accounts!(optional_accounts, 2); - Some((undelegation_request_account, request_rent_payer)) - } + 1 => Some(&optional_accounts[0]), _ => return Err(ProgramError::InvalidInstructionData), }; - if let Some((undelegation_request_account, request_rent_payer)) = - request_accounts - { + if let Some(undelegation_request_account) = request_account { require_valid_undelegation_request( delegated_account, owner_program, undelegation_request_account, - request_rent_payer, + delegation_rent_payer, )?; }; @@ -202,7 +194,7 @@ pub fn process_undelegate( } if delegation_metadata.undelegation_requester == UndelegationRequester::OwnerProgram - && request_accounts.is_none() + && request_account.is_none() { return Err(DlpError::MissingUndelegationRequest.into()); } @@ -210,12 +202,12 @@ pub fn process_undelegate( // Check if the rent payer is correct if !address_eq( &delegation_metadata.rent_payer.to_bytes().into(), - rent_reimbursement.address(), + delegation_rent_payer.address(), ) { log!("Expected rent payer to be : "); Address::from(delegation_metadata.rent_payer.to_bytes()).log(); log!("but got : "); - rent_reimbursement.address().log(); + delegation_rent_payer.address().log(); return Err( DlpError::InvalidReimbursementAddressForDelegationRent.into() ); @@ -234,15 +226,13 @@ pub fn process_undelegate( process_delegation_cleanup( delegation_record_account, delegation_metadata_account, - rent_reimbursement, + delegation_rent_payer, fees_vault, validator_fees_vault, delegation_last_commit_id, )?; - if let Some((undelegation_request_account, request_rent_payer)) = - request_accounts - { - close_pda(undelegation_request_account, request_rent_payer)?; + if let Some(undelegation_request_account) = request_account { + close_pda(undelegation_request_account, delegation_rent_payer)?; } return Ok(()); } @@ -297,15 +287,13 @@ pub fn process_undelegate( process_delegation_cleanup( delegation_record_account, delegation_metadata_account, - rent_reimbursement, + delegation_rent_payer, fees_vault, validator_fees_vault, delegation_last_commit_id, )?; - if let Some((undelegation_request_account, request_rent_payer)) = - request_accounts - { - close_pda(undelegation_request_account, request_rent_payer)?; + if let Some(undelegation_request_account) = request_account { + close_pda(undelegation_request_account, delegation_rent_payer)?; } Ok(()) } @@ -314,7 +302,7 @@ fn require_valid_undelegation_request( delegated_account: &AccountView, owner_program: &AccountView, undelegation_request_account: &AccountView, - request_rent_payer: &AccountView, + delegation_rent_payer: &AccountView, ) -> ProgramResult { require_initialized_pda( undelegation_request_account, @@ -327,10 +315,6 @@ fn require_valid_undelegation_request( "undelegation request", )?; - if !request_rent_payer.is_writable() { - return Err(ProgramError::Immutable); - } - let request_data = undelegation_request_account.try_borrow()?; let request = UndelegationRequest::try_from_bytes_with_discriminator(&request_data) @@ -344,7 +328,7 @@ fn require_valid_undelegation_request( owner_program.address(), ) || !address_eq( &request.rent_payer.to_bytes().into(), - request_rent_payer.address(), + delegation_rent_payer.address(), ) { return Err(DlpError::InvalidUndelegationRequest.into()); } @@ -465,7 +449,7 @@ fn cpi_external_undelegate( fn process_delegation_cleanup( delegation_record_account: &AccountView, delegation_metadata_account: &AccountView, - rent_reimbursement: &AccountView, + delegation_rent_payer: &AccountView, fees_vault: &AccountView, validator_fees_vault: &AccountView, delegation_last_commit_id: u64, @@ -480,14 +464,14 @@ fn process_delegation_cleanup( let mut fee_remaining = total_fee_requested.min(total_lamports); close_pda_with_fees( delegation_record_account, - rent_reimbursement, + delegation_rent_payer, fees_vault, validator_fees_vault, &mut fee_remaining, )?; close_pda_with_fees( delegation_metadata_account, - rent_reimbursement, + delegation_rent_payer, fees_vault, validator_fees_vault, &mut fee_remaining, diff --git a/tests/test_request_undelegation.rs b/tests/test_request_undelegation.rs index 3a797e94..8b69be01 100644 --- a/tests/test_request_undelegation.rs +++ b/tests/test_request_undelegation.rs @@ -51,7 +51,7 @@ const TEST_PDA_SEED: &[u8] = b"test-pda"; async fn test_request_undelegation_creates_request() { let SetupContext { banks, - payer, + authority, blockhash, .. } = setup_env(SetupConfig { @@ -60,11 +60,11 @@ async fn test_request_undelegation_creates_request() { }) .await; - let ix = request_undelegation_from_owner_program(payer.pubkey()); + let ix = request_undelegation_from_owner_program(authority.pubkey()); let tx = Transaction::new_signed_with_payer( &[ix], - Some(&payer.pubkey()), - &[&payer], + Some(&authority.pubkey()), + &[&authority], blockhash, ); @@ -84,7 +84,7 @@ async fn test_request_undelegation_creates_request() { .unwrap(); assert_eq!(request.delegated_account, DELEGATED_PDA_ID); assert_eq!(request.owner_program, DELEGATED_PDA_OWNER_ID); - assert_eq!(request.rent_payer, payer.pubkey()); + assert_eq!(request.rent_payer, authority.pubkey()); assert_eq!( request.expires_at_slot, request.created_slot + DEFAULT_UNDELEGATION_REQUEST_TIMEOUT_SLOTS @@ -113,8 +113,7 @@ async fn test_request_undelegation_creates_request() { async fn test_request_undelegation_is_idempotent() { let SetupContext { banks, - payer, - second_payer, + authority, blockhash, .. } = setup_env(SetupConfig { @@ -123,11 +122,11 @@ async fn test_request_undelegation_is_idempotent() { }) .await; - let first_ix = request_undelegation_from_owner_program(payer.pubkey()); + let first_ix = request_undelegation_from_owner_program(authority.pubkey()); let first_tx = Transaction::new_signed_with_payer( &[first_ix], - Some(&payer.pubkey()), - &[&payer], + Some(&authority.pubkey()), + &[&authority], blockhash, ); let first_res = banks.process_transaction(first_tx).await; @@ -137,12 +136,11 @@ async fn test_request_undelegation_is_idempotent() { undelegation_request_pda_from_delegated_account(&DELEGATED_PDA_ID); let request_before = banks.get_account(request_pda).await.unwrap().unwrap(); - let second_ix = - request_undelegation_from_owner_program(second_payer.pubkey()); + let second_ix = request_undelegation_from_owner_program(authority.pubkey()); let second_tx = Transaction::new_signed_with_payer( &[second_ix], - Some(&second_payer.pubkey()), - &[&second_payer], + Some(&authority.pubkey()), + &[&authority], blockhash, ); let second_res = banks.process_transaction(second_tx).await; @@ -274,7 +272,6 @@ async fn test_undelegate_with_request_closes_request() { let SetupContext { banks, authority, - request_rent_payer, blockhash, .. } = setup_env(SetupConfig { @@ -289,18 +286,6 @@ async fn test_undelegate_with_request_closes_request() { let request_pda = undelegation_request_pda_from_delegated_account(&DELEGATED_PDA_ID); - let request_lamports_before = banks - .get_account(request_pda) - .await - .unwrap() - .unwrap() - .lamports; - let payer_lamports_before = banks - .get_account(request_rent_payer.pubkey()) - .await - .unwrap() - .unwrap() - .lamports; let ix_finalize = dlp_api::instruction_builder::finalize( authority.pubkey(), @@ -311,7 +296,6 @@ async fn test_undelegate_with_request_closes_request() { DELEGATED_PDA_ID, DELEGATED_PDA_OWNER_ID, authority.pubkey(), - request_rent_payer.pubkey(), ); let tx = Transaction::new_signed_with_payer( @@ -326,24 +310,12 @@ async fn test_undelegate_with_request_closes_request() { let request_account = banks.get_account(request_pda).await.unwrap(); assert!(request_account.is_none()); - - let payer_lamports_after = banks - .get_account(request_rent_payer.pubkey()) - .await - .unwrap() - .unwrap() - .lamports; - assert_eq!( - payer_lamports_after, - payer_lamports_before + request_lamports_before - ); } #[tokio::test] async fn test_commit_state_preserves_owner_program_requester() { let SetupContext { banks, - payer, authority, blockhash, .. @@ -354,7 +326,8 @@ async fn test_commit_state_preserves_owner_program_requester() { }) .await; - let request_ix = request_undelegation_from_owner_program(payer.pubkey()); + let request_ix = + request_undelegation_from_owner_program(authority.pubkey()); let commit_ix = dlp_api::instruction_builder::commit_state( authority.pubkey(), DELEGATED_PDA_ID, @@ -368,8 +341,8 @@ async fn test_commit_state_preserves_owner_program_requester() { ); let tx = Transaction::new_signed_with_payer( &[request_ix, commit_ix], - Some(&payer.pubkey()), - &[&payer, &authority], + Some(&authority.pubkey()), + &[&authority], blockhash, ); let res = banks.process_transaction(tx).await; @@ -386,7 +359,6 @@ async fn test_commit_state_preserves_owner_program_requester() { async fn test_commit_finalize_preserves_owner_program_requester() { let SetupContext { banks, - payer, authority, blockhash, .. @@ -397,7 +369,8 @@ async fn test_commit_finalize_preserves_owner_program_requester() { }) .await; - let request_ix = request_undelegation_from_owner_program(payer.pubkey()); + let request_ix = + request_undelegation_from_owner_program(authority.pubkey()); let mut args = CommitFinalizeArgs { commit_id: 1, lamports: LAMPORTS_PER_SOL, @@ -414,8 +387,8 @@ async fn test_commit_finalize_preserves_owner_program_requester() { ); let tx = Transaction::new_signed_with_payer( &[request_ix, commit_finalize_ix], - Some(&payer.pubkey()), - &[&payer, &authority], + Some(&authority.pubkey()), + &[&authority], blockhash, ); let res = banks.process_transaction(tx).await; @@ -433,7 +406,6 @@ async fn test_undelegate_owner_program_request_without_request_accounts_rejected ) { let SetupContext { banks, - payer, authority, blockhash, .. @@ -445,11 +417,12 @@ async fn test_undelegate_owner_program_request_without_request_accounts_rejected }) .await; - let request_ix = request_undelegation_from_owner_program(payer.pubkey()); + let request_ix = + request_undelegation_from_owner_program(authority.pubkey()); let request_tx = Transaction::new_signed_with_payer( &[request_ix], - Some(&payer.pubkey()), - &[&payer], + Some(&authority.pubkey()), + &[&authority], blockhash, ); let request_res = banks.process_transaction(request_tx).await; @@ -522,7 +495,6 @@ async fn test_undelegate_with_malformed_optional_request_accounts_rejected() { let SetupContext { banks, authority, - request_rent_payer, blockhash, .. } = setup_env(SetupConfig { @@ -545,9 +517,13 @@ async fn test_undelegate_with_malformed_optional_request_accounts_rejected() { DELEGATED_PDA_ID, DELEGATED_PDA_OWNER_ID, authority.pubkey(), - request_rent_payer.pubkey(), ); - ix_undelegate.accounts.pop(); + ix_undelegate + .accounts + .push(AccountMeta::new_readonly(system_program::id(), false)); + ix_undelegate + .accounts + .push(AccountMeta::new_readonly(dlp_api::id(), false)); let tx = Transaction::new_signed_with_payer( &[ix_finalize, ix_undelegate], @@ -617,11 +593,13 @@ fn imaginary_program_processor_requesting_undelegation_through_cpi( /// require it. See imaginary_program_processor_requesting_undelegation_through_cpi() /// which is supposed to be the processor of the imaginary program. /// -fn request_undelegation_from_owner_program(payer: Pubkey) -> Instruction { +fn request_undelegation_from_owner_program( + delegation_rent_payer: Pubkey, +) -> Instruction { Instruction { program_id: DELEGATED_PDA_OWNER_ID, accounts: vec![ - AccountMeta::new(payer, true), + AccountMeta::new(delegation_rent_payer, true), AccountMeta::new(DELEGATED_PDA_ID, false), AccountMeta::new_readonly(DELEGATED_PDA_OWNER_ID, false), AccountMeta::new( @@ -673,9 +651,7 @@ struct SetupConfig { struct SetupContext { banks: BanksClient, payer: Keypair, - second_payer: Keypair, authority: Keypair, - request_rent_payer: Keypair, delegated_on_curve: Keypair, delegated_account: Pubkey, blockhash: Hash, @@ -697,27 +673,19 @@ async fn setup_env(config: SetupConfig) -> SetupContext { } let payer = Keypair::new(); - let second_payer = Keypair::new(); let authority = keypair_from_bytes(&TEST_AUTHORITY); - let request_rent_payer = Keypair::new(); let delegated_on_curve = keypair_from_bytes(&ON_CURVE_KEYPAIR); let delegated_account = match config.delegated_account { DelegatedAccountSetup::OffCurvePda => DELEGATED_PDA_ID, DelegatedAccountSetup::OnCurveKeypair => delegated_on_curve.pubkey(), }; + let delegation_rent_payer = match config.delegated_account { + DelegatedAccountSetup::OffCurvePda => authority.pubkey(), + DelegatedAccountSetup::OnCurveKeypair => payer.pubkey(), + }; add_system_account(&mut program_test, payer.pubkey(), LAMPORTS_PER_SOL); - add_system_account( - &mut program_test, - second_payer.pubkey(), - LAMPORTS_PER_SOL, - ); add_system_account(&mut program_test, authority.pubkey(), LAMPORTS_PER_SOL); - add_system_account( - &mut program_test, - request_rent_payer.pubkey(), - LAMPORTS_PER_SOL, - ); add_delegated_account(&mut program_test, delegated_account); match config.delegated_account { @@ -726,7 +694,7 @@ async fn setup_env(config: SetupConfig) -> SetupContext { &mut program_test, delegated_account, authority.pubkey(), - authority.pubkey(), + delegation_rent_payer, config.metadata_undelegatable, false, ); @@ -736,7 +704,7 @@ async fn setup_env(config: SetupConfig) -> SetupContext { &mut program_test, delegated_account, delegated_on_curve.pubkey(), - payer.pubkey(), + delegation_rent_payer, config.metadata_undelegatable, true, ); @@ -760,7 +728,7 @@ async fn setup_env(config: SetupConfig) -> SetupContext { add_request_account( &mut program_test, delegated_account, - request_rent_payer.pubkey(), + delegation_rent_payer, ); } @@ -768,9 +736,7 @@ async fn setup_env(config: SetupConfig) -> SetupContext { SetupContext { banks, payer, - second_payer, authority, - request_rent_payer, delegated_on_curve, delegated_account, blockhash, diff --git a/tests/test_undelegate_with_rollback_after_timeout.rs b/tests/test_undelegate_with_rollback_after_timeout.rs index de284b10..039c9329 100644 --- a/tests/test_undelegate_with_rollback_after_timeout.rs +++ b/tests/test_undelegate_with_rollback_after_timeout.rs @@ -355,7 +355,7 @@ async fn test_re_request_is_idempotent_without_extending_timeout() { banks, _caller, request_rent_payer, - _delegation_rent_payer, + delegation_rent_payer, _, blockhash, ) = setup_request_timeout_env_with_commit_ids(false, 0, 1, 0).await; @@ -372,11 +372,11 @@ async fn test_re_request_is_idempotent_without_extending_timeout() { assert_eq!(request_before.last_commit_id_at_request, 0); let request_ix = - request_undelegation_from_owner_program(request_rent_payer.pubkey()); + request_undelegation_from_owner_program(delegation_rent_payer.pubkey()); let request_tx = Transaction::new_signed_with_payer( &[request_ix], - Some(&request_rent_payer.pubkey()), - &[&request_rent_payer], + Some(&delegation_rent_payer.pubkey()), + &[&delegation_rent_payer], blockhash, ); let res = banks.process_transaction(request_tx).await; @@ -644,11 +644,13 @@ fn rollback_ix( ) } -fn request_undelegation_from_owner_program(payer: Pubkey) -> Instruction { +fn request_undelegation_from_owner_program( + delegation_rent_payer: Pubkey, +) -> Instruction { Instruction { program_id: DELEGATED_PDA_OWNER_ID, accounts: vec![ - AccountMeta::new(payer, true), + AccountMeta::new(delegation_rent_payer, true), AccountMeta::new(DELEGATED_PDA_ID, false), AccountMeta::new_readonly(DELEGATED_PDA_OWNER_ID, false), AccountMeta::new( From 2ab3a15f25113f19e059acfaf75bbacac6d9ff77 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Tue, 7 Jul 2026 13:38:44 +0530 Subject: [PATCH 28/30] cleanup --- tests/test_request_undelegation.rs | 150 +----------- ..._undelegate_with_rollback_after_timeout.rs | 227 +----------------- 2 files changed, 22 insertions(+), 355 deletions(-) diff --git a/tests/test_request_undelegation.rs b/tests/test_request_undelegation.rs index 8b69be01..393d9a12 100644 --- a/tests/test_request_undelegation.rs +++ b/tests/test_request_undelegation.rs @@ -48,7 +48,7 @@ mod fixtures; const TEST_PDA_SEED: &[u8] = b"test-pda"; #[tokio::test] -async fn test_request_undelegation_creates_request() { +async fn test_request_undelegation_creates_request_and_is_idempotent() { let SetupContext { banks, authority, @@ -68,9 +68,7 @@ async fn test_request_undelegation_creates_request() { blockhash, ); - let res = banks.process_transaction(tx).await; - println!("{:?}", res); - assert!(res.is_ok()); + banks.process_transaction(tx).await.unwrap(); let request_pda = undelegation_request_pda_from_delegated_account(&DELEGATED_PDA_ID); @@ -107,34 +105,6 @@ async fn test_request_undelegation_creates_request() { delegation_metadata.undelegation_requester, UndelegationRequester::OwnerProgram ); -} - -#[tokio::test] -async fn test_request_undelegation_is_idempotent() { - let SetupContext { - banks, - authority, - blockhash, - .. - } = setup_env(SetupConfig { - with_request_wrapper: true, - ..Default::default() - }) - .await; - - let first_ix = request_undelegation_from_owner_program(authority.pubkey()); - let first_tx = Transaction::new_signed_with_payer( - &[first_ix], - Some(&authority.pubkey()), - &[&authority], - blockhash, - ); - let first_res = banks.process_transaction(first_tx).await; - assert!(first_res.is_ok()); - - let request_pda = - undelegation_request_pda_from_delegated_account(&DELEGATED_PDA_ID); - let request_before = banks.get_account(request_pda).await.unwrap().unwrap(); let second_ix = request_undelegation_from_owner_program(authority.pubkey()); let second_tx = Transaction::new_signed_with_payer( @@ -143,49 +113,10 @@ async fn test_request_undelegation_is_idempotent() { &[&authority], blockhash, ); - let second_res = banks.process_transaction(second_tx).await; - println!("{:?}", second_res); - assert!(second_res.is_ok()); + banks.process_transaction(second_tx).await.unwrap(); let request_after = banks.get_account(request_pda).await.unwrap().unwrap(); - assert_eq!(request_after.data, request_before.data); -} - -#[tokio::test] -async fn test_request_undelegation_rejects_payload() { - let SetupContext { - banks, - payer, - blockhash, - .. - } = setup_env(SetupConfig { - with_request_wrapper: true, - ..Default::default() - }) - .await; - - let mut ix = request_undelegation_from_owner_program(payer.pubkey()); - ix.data = 123_u64.to_le_bytes().to_vec(); - let tx = Transaction::new_signed_with_payer( - &[ix], - Some(&payer.pubkey()), - &[&payer], - blockhash, - ); - - let err = banks.process_transaction(tx).await.unwrap_err(); - assert!( - matches!( - err, - BanksClientError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::InvalidInstructionData, - ) - ) - ), - "expected request payload to fail with InvalidInstructionData, got {err:?}" - ); + assert_eq!(request_after.data, request_account.data); } #[tokio::test] @@ -304,9 +235,7 @@ async fn test_undelegate_with_request_closes_request() { &[&authority], blockhash, ); - let res = banks.process_transaction(tx).await; - println!("{:?}", res); - assert!(res.is_ok()); + banks.process_transaction(tx).await.unwrap(); let request_account = banks.get_account(request_pda).await.unwrap(); assert!(request_account.is_none()); @@ -345,8 +274,7 @@ async fn test_commit_state_preserves_owner_program_requester() { &[&authority], blockhash, ); - let res = banks.process_transaction(tx).await; - assert!(res.is_ok(), "{res:?}"); + banks.process_transaction(tx).await.unwrap(); assert_delegation_metadata_requester( &banks, @@ -391,8 +319,7 @@ async fn test_commit_finalize_preserves_owner_program_requester() { &[&authority], blockhash, ); - let res = banks.process_transaction(tx).await; - assert!(res.is_ok(), "{res:?}"); + banks.process_transaction(tx).await.unwrap(); assert_delegation_metadata_requester( &banks, @@ -425,8 +352,7 @@ async fn test_undelegate_owner_program_request_without_request_accounts_rejected &[&authority], blockhash, ); - let request_res = banks.process_transaction(request_tx).await; - assert!(request_res.is_ok(), "{request_res:?}"); + banks.process_transaction(request_tx).await.unwrap(); let finalize_ix = dlp_api::instruction_builder::finalize( authority.pubkey(), @@ -438,8 +364,7 @@ async fn test_undelegate_owner_program_request_without_request_accounts_rejected &[&authority], blockhash, ); - let finalize_res = banks.process_transaction(finalize_tx).await; - assert!(finalize_res.is_ok(), "{finalize_res:?}"); + banks.process_transaction(finalize_tx).await.unwrap(); let undelegate_ix = dlp_api::instruction_builder::undelegate( authority.pubkey(), @@ -490,63 +415,6 @@ async fn assert_delegation_metadata_requester( ); } -#[tokio::test] -async fn test_undelegate_with_malformed_optional_request_accounts_rejected() { - let SetupContext { - banks, - authority, - blockhash, - .. - } = setup_env(SetupConfig { - metadata_undelegatable: true, - with_commit_accounts: true, - with_owner_program: true, - with_fee_accounts: true, - with_request_account: true, - ..Default::default() - }) - .await; - - let ix_finalize = dlp_api::instruction_builder::finalize( - authority.pubkey(), - DELEGATED_PDA_ID, - ); - let mut ix_undelegate = - dlp_api::instruction_builder::undelegate_with_request( - authority.pubkey(), - DELEGATED_PDA_ID, - DELEGATED_PDA_OWNER_ID, - authority.pubkey(), - ); - ix_undelegate - .accounts - .push(AccountMeta::new_readonly(system_program::id(), false)); - ix_undelegate - .accounts - .push(AccountMeta::new_readonly(dlp_api::id(), false)); - - let tx = Transaction::new_signed_with_payer( - &[ix_finalize, ix_undelegate], - Some(&authority.pubkey()), - &[&authority], - blockhash, - ); - let err = banks.process_transaction(tx).await.unwrap_err(); - assert!( - matches!( - err, - BanksClientError::TransactionError( - TransactionError::InstructionError( - 1, - InstructionError::InvalidInstructionData, - ) - ) - ), - "expected malformed optional request accounts to fail undelegate \ - with InvalidInstructionData, got {err:?}" - ); -} - fn imaginary_program_processor_requesting_undelegation_through_cpi( program_id: &Pubkey, accounts: &[AccountInfo], diff --git a/tests/test_undelegate_with_rollback_after_timeout.rs b/tests/test_undelegate_with_rollback_after_timeout.rs index 039c9329..ea6ba561 100644 --- a/tests/test_undelegate_with_rollback_after_timeout.rs +++ b/tests/test_undelegate_with_rollback_after_timeout.rs @@ -149,8 +149,7 @@ async fn test_undelegate_with_rollback_after_timeout_after_expiry() { blockhash, ); - let res = banks.process_transaction(tx).await; - assert!(res.is_ok()); + banks.process_transaction(tx).await.unwrap(); assert!(banks.get_account(request_pda).await.unwrap().is_none()); assert!(banks @@ -202,27 +201,6 @@ async fn test_undelegate_with_rollback_after_timeout_rejects_before_expiry() { blockhash, ) = setup_request_timeout_env(false, 1_000_000).await; - let request_pda = - undelegation_request_pda_from_delegated_account(&DELEGATED_PDA_ID); - let delegation_record_pda = - delegation_record_pda_from_delegated_account(&DELEGATED_PDA_ID); - let delegation_metadata_pda = - delegation_metadata_pda_from_delegated_account(&DELEGATED_PDA_ID); - - let request_before = banks.get_account(request_pda).await.unwrap().unwrap(); - let delegation_record_before = banks - .get_account(delegation_record_pda) - .await - .unwrap() - .unwrap(); - let delegation_metadata_before = banks - .get_account(delegation_metadata_pda) - .await - .unwrap() - .unwrap(); - let delegated_before = - banks.get_account(DELEGATED_PDA_ID).await.unwrap().unwrap(); - let ix = rollback_from_owner_program( request_rent_payer.pubkey(), delegation_rent_payer.pubkey(), @@ -248,31 +226,6 @@ async fn test_undelegate_with_rollback_after_timeout_rejects_before_expiry() { ), "expected UndelegationRequestNotExpired, got {err:?}" ); - - assert_eq!( - banks.get_account(request_pda).await.unwrap().unwrap(), - request_before - ); - assert_eq!( - banks - .get_account(delegation_record_pda) - .await - .unwrap() - .unwrap(), - delegation_record_before - ); - assert_eq!( - banks - .get_account(delegation_metadata_pda) - .await - .unwrap() - .unwrap(), - delegation_metadata_before - ); - assert_eq!( - banks.get_account(DELEGATED_PDA_ID).await.unwrap().unwrap(), - delegated_before - ); } #[tokio::test] @@ -321,8 +274,7 @@ async fn test_request_timeout_closes_pending_commit_without_applying_it() { blockhash, ); - let res = banks.process_transaction(tx).await; - assert!(res.is_ok()); + banks.process_transaction(tx).await.unwrap(); assert!(banks.get_account(commit_state_pda).await.unwrap().is_none()); assert!(banks @@ -349,74 +301,9 @@ async fn test_request_timeout_closes_pending_commit_without_applying_it() { ); } -#[tokio::test] -async fn test_re_request_is_idempotent_without_extending_timeout() { - let ( - banks, - _caller, - request_rent_payer, - delegation_rent_payer, - _, - blockhash, - ) = setup_request_timeout_env_with_commit_ids(false, 0, 1, 0).await; - - let request_pda = - undelegation_request_pda_from_delegated_account(&DELEGATED_PDA_ID); - let request_before_account = - banks.get_account(request_pda).await.unwrap().unwrap(); - let request_before = - dlp_api::state::UndelegationRequest::try_from_bytes_with_discriminator( - &request_before_account.data, - ) - .unwrap(); - assert_eq!(request_before.last_commit_id_at_request, 0); - - let request_ix = - request_undelegation_from_owner_program(delegation_rent_payer.pubkey()); - let request_tx = Transaction::new_signed_with_payer( - &[request_ix], - Some(&delegation_rent_payer.pubkey()), - &[&delegation_rent_payer], - blockhash, - ); - let res = banks.process_transaction(request_tx).await; - assert!(res.is_ok()); - - let request_after_account = - banks.get_account(request_pda).await.unwrap().unwrap(); - assert_eq!(request_after_account.data, request_before_account.data); - let request_after = - dlp_api::state::UndelegationRequest::try_from_bytes_with_discriminator( - &request_after_account.data, - ) - .unwrap(); - assert_eq!(request_after.last_commit_id_at_request, 0); - assert_eq!(request_after.created_slot, request_before.created_slot); - assert_eq!( - request_after.expires_at_slot, - request_before.expires_at_slot - ); - assert_eq!(request_after.rent_payer, request_rent_payer.pubkey()); -} - async fn setup_request_timeout_env( with_pending_commit: bool, expires_at_slot: u64, -) -> (BanksClient, Keypair, Keypair, Keypair, Keypair, Hash) { - setup_request_timeout_env_with_commit_ids( - with_pending_commit, - expires_at_slot, - 0, - 0, - ) - .await -} - -async fn setup_request_timeout_env_with_commit_ids( - with_pending_commit: bool, - expires_at_slot: u64, - delegation_last_commit_id: u64, - request_last_commit_id: u64, ) -> (BanksClient, Keypair, Keypair, Keypair, Keypair, Hash) { let mut program_test = ProgramTest::default(); program_test.prefer_bpf(true); @@ -439,16 +326,11 @@ async fn setup_request_timeout_env_with_commit_ids( add_system_account(&mut program_test, delegation_rent_payer.pubkey()); add_system_account(&mut program_test, validator.pubkey()); add_delegated_account(&mut program_test); - add_delegation_accounts( - &mut program_test, - delegation_rent_payer.pubkey(), - delegation_last_commit_id, - ); + add_delegation_accounts(&mut program_test, delegation_rent_payer.pubkey()); add_request_account( &mut program_test, request_rent_payer.pubkey(), expires_at_slot, - request_last_commit_id, ); if with_pending_commit { add_pending_commit_accounts(&mut program_test, validator.pubkey()); @@ -497,7 +379,6 @@ fn add_delegated_account(program_test: &mut ProgramTest) { fn add_delegation_accounts( program_test: &mut ProgramTest, delegation_rent_payer: solana_program::pubkey::Pubkey, - last_commit_id: u64, ) { let delegation_record_data = get_delegation_record_data( keypair_from_bytes(&TEST_AUTHORITY).pubkey(), @@ -518,7 +399,7 @@ fn add_delegation_accounts( let delegation_metadata_data = get_delegation_metadata_data_with_commit_id( delegation_rent_payer, Some(false), - last_commit_id, + 0, ); program_test.add_account( delegation_metadata_pda_from_delegated_account(&DELEGATED_PDA_ID), @@ -537,7 +418,6 @@ fn add_request_account( program_test: &mut ProgramTest, request_rent_payer: solana_program::pubkey::Pubkey, expires_at_slot: u64, - last_commit_id_at_request: u64, ) { let request_data = create_undelegation_request_data_with_expiry( DELEGATED_PDA_ID, @@ -545,7 +425,7 @@ fn add_request_account( request_rent_payer, 0, expires_at_slot, - last_commit_id_at_request, + 0, ); program_test.add_account( undelegation_request_pda_from_delegated_account(&DELEGATED_PDA_ID), @@ -630,52 +510,6 @@ fn rollback_from_owner_program( } } -fn rollback_ix( - request_rent_payer: Pubkey, - delegation_rent_payer: Pubkey, - commit_reimbursement: Pubkey, -) -> Instruction { - dlp_api::instruction_builder::undelegate_with_rollback_after_timeout( - request_rent_payer, - DELEGATED_PDA_ID, - DELEGATED_PDA_OWNER_ID, - delegation_rent_payer, - commit_reimbursement, - ) -} - -fn request_undelegation_from_owner_program( - delegation_rent_payer: Pubkey, -) -> Instruction { - Instruction { - program_id: DELEGATED_PDA_OWNER_ID, - accounts: vec![ - AccountMeta::new(delegation_rent_payer, true), - AccountMeta::new(DELEGATED_PDA_ID, false), - AccountMeta::new_readonly(DELEGATED_PDA_OWNER_ID, false), - AccountMeta::new( - undelegation_request_pda_from_delegated_account( - &DELEGATED_PDA_ID, - ), - false, - ), - AccountMeta::new_readonly( - delegation_record_pda_from_delegated_account(&DELEGATED_PDA_ID), - false, - ), - AccountMeta::new( - delegation_metadata_pda_from_delegated_account( - &DELEGATED_PDA_ID, - ), - false, - ), - AccountMeta::new_readonly(system_program::id(), false), - AccountMeta::new_readonly(dlp_api::id(), false), - ], - data: vec![1], - } -} - fn owner_program_processor( program_id: &Pubkey, accounts: &[AccountInfo], @@ -687,7 +521,6 @@ fn owner_program_processor( match data.first().copied() { Some(0) => process_rollback(program_id, accounts), - Some(1) => process_request_undelegation(program_id, accounts), _ => Err(ProgramError::InvalidInstructionData), } } @@ -708,11 +541,14 @@ fn process_rollback( let delegated_data = delegated_account.try_borrow_data()?.to_vec(); - let ix = rollback_ix( - *request_rent_payer.key, - *delegation_rent_payer.key, - *commit_reimbursement.key, - ); + let ix = + dlp_api::instruction_builder::undelegate_with_rollback_after_timeout( + *request_rent_payer.key, + *delegated_account.key, + *owner_program.key, + *delegation_rent_payer.key, + *commit_reimbursement.key, + ); let (_, bump) = Pubkey::find_program_address(&[TEST_PDA_SEED], program_id); let bump_seed = [bump]; invoke_signed( @@ -741,43 +577,6 @@ fn process_rollback( Ok(()) } -fn process_request_undelegation( - program_id: &Pubkey, - accounts: &[AccountInfo], -) -> ProgramResult { - let [payer, delegated_account, owner_program, request_account, delegation_record_account, delegation_metadata_account, system_program_account, dlp_program] = - accounts - else { - return Err(ProgramError::NotEnoughAccountKeys); - }; - - if owner_program.key != program_id { - return Err(ProgramError::IncorrectProgramId); - } - - let ix = dlp_api::instruction_builder::request_undelegation( - *payer.key, - *delegated_account.key, - *program_id, - ); - let (_, bump) = Pubkey::find_program_address(&[TEST_PDA_SEED], program_id); - let bump_seed = [bump]; - invoke_signed( - &ix, - &[ - payer.clone(), - delegated_account.clone(), - owner_program.clone(), - request_account.clone(), - delegation_record_account.clone(), - delegation_metadata_account.clone(), - system_program_account.clone(), - dlp_program.clone(), - ], - &[&[TEST_PDA_SEED, &bump_seed]], - ) -} - fn process_external_undelegate( program_id: &Pubkey, accounts: &[AccountInfo], From 26d01c4b5c0c29acbebd41d14b303e1608bbaa71 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Tue, 7 Jul 2026 14:47:19 +0530 Subject: [PATCH 29/30] Shrink undelegation request state, removing fields present in metadata/record --- .../undelegate_with_rollback_after_timeout.rs | 4 - dlp-api/src/state/undelegation_request.rs | 25 +--- src/processor/fast/request_undelegation.rs | 7 - src/processor/fast/undelegate.rs | 22 +-- .../undelegate_with_rollback_after_timeout.rs | 61 +++------ tests/fixtures/accounts.rs | 21 --- tests/test_request_undelegation.rs | 22 +-- ..._undelegate_with_rollback_after_timeout.rs | 127 ++---------------- 8 files changed, 41 insertions(+), 248 deletions(-) diff --git a/dlp-api/src/instruction_builder/undelegate_with_rollback_after_timeout.rs b/dlp-api/src/instruction_builder/undelegate_with_rollback_after_timeout.rs index 7eb0917c..b13d6627 100644 --- a/dlp-api/src/instruction_builder/undelegate_with_rollback_after_timeout.rs +++ b/dlp-api/src/instruction_builder/undelegate_with_rollback_after_timeout.rs @@ -19,9 +19,7 @@ use crate::compat::{Compatize, Modernize}; /// Builds an owner-program-authorized timeout rollback instruction for a /// requested undelegation. /// See [dlp::processor::process_undelegate_with_rollback_after_timeout] for docs. -#[allow(clippy::too_many_arguments)] pub fn undelegate_with_rollback_after_timeout( - request_rent_payer: Pubkey, delegated_account: Pubkey, owner_program: Pubkey, delegation_rent_payer: Pubkey, @@ -50,7 +48,6 @@ pub fn undelegate_with_rollback_after_timeout( Instruction { program_id: dlp::id().modernize(), accounts: vec![ - AccountMeta::new(request_rent_payer, false), AccountMeta::new(delegated_account, true), AccountMeta::new_readonly(owner_program, false), AccountMeta::new(request_pda, false), @@ -75,7 +72,6 @@ pub fn undelegate_with_rollback_after_timeout_size_budget( ) -> u32 { total_size_budget(&[ DLP_PROGRAM_DATA_SIZE_CLASS, - AccountSizeClass::Tiny, // request_rent_payer delegated_account, // delegated_account AccountSizeClass::Tiny, // owner_program AccountSizeClass::Tiny, // undelegation_request_pda diff --git a/dlp-api/src/state/undelegation_request.rs b/dlp-api/src/state/undelegation_request.rs index dbc8411f..eab1b136 100644 --- a/dlp-api/src/state/undelegation_request.rs +++ b/dlp-api/src/state/undelegation_request.rs @@ -12,29 +12,14 @@ use crate::{ #[repr(C)] #[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] pub struct UndelegationRequest { - /// The delegated account this request targets. + /// Delegated account this request is for. + /// + /// DLP validates the request by PDA seeds, so this is stored only to let + /// validators identify the matching delegated account when scanning + /// request accounts. pub delegated_account: Pubkey, - - /// The original owner program recorded for the delegated account. - pub owner_program: Pubkey, - - /// The account that paid rent for this request PDA. - pub rent_payer: Pubkey, - - /// The slot at which the request was created. - pub created_slot: u64, - /// The first slot at which timeout rollback is allowed. pub expires_at_slot: u64, - - /// DelegationMetadata.last_commit_id observed when the request was created. - pub last_commit_id_at_request: u64, - - /// PDA bump for this request. - pub bump: u8, - - /// Explicit padding keeps this type Pod-safe with repr(C). - pub _padding: [u8; 7], } impl AccountWithDiscriminator for UndelegationRequest { diff --git a/src/processor/fast/request_undelegation.rs b/src/processor/fast/request_undelegation.rs index b40a5a21..a7538b72 100644 --- a/src/processor/fast/request_undelegation.rs +++ b/src/processor/fast/request_undelegation.rs @@ -102,7 +102,6 @@ pub fn process_request_undelegation( payer.address(), DlpError::InvalidReimbursementAddressForDelegationRent ); - let last_commit_id_at_request = delegation_metadata.last_commit_id; drop(delegation_metadata_data); let request_seeds = &[ @@ -145,13 +144,7 @@ pub fn process_request_undelegation( let request = UndelegationRequest { delegated_account: *delegated_account.address(), - owner_program: *owner_program.address(), - rent_payer: *payer.address(), - created_slot, expires_at_slot, - last_commit_id_at_request, - bump: request_bump, - _padding: [0; 7], }; let mut request_data = undelegation_request_account.try_borrow_mut()?; request diff --git a/src/processor/fast/undelegate.rs b/src/processor/fast/undelegate.rs index 4534cb5b..828c8528 100644 --- a/src/processor/fast/undelegate.rs +++ b/src/processor/fast/undelegate.rs @@ -110,9 +110,7 @@ pub fn process_undelegate( if let Some(undelegation_request_account) = request_account { require_valid_undelegation_request( delegated_account, - owner_program, undelegation_request_account, - delegation_rent_payer, )?; }; @@ -300,9 +298,7 @@ pub fn process_undelegate( fn require_valid_undelegation_request( delegated_account: &AccountView, - owner_program: &AccountView, undelegation_request_account: &AccountView, - delegation_rent_payer: &AccountView, ) -> ProgramResult { require_initialized_pda( undelegation_request_account, @@ -316,22 +312,8 @@ fn require_valid_undelegation_request( )?; let request_data = undelegation_request_account.try_borrow()?; - let request = - UndelegationRequest::try_from_bytes_with_discriminator(&request_data) - .map_err(to_pinocchio_program_error)?; - - if !address_eq( - &request.delegated_account.to_bytes().into(), - delegated_account.address(), - ) || !address_eq( - &request.owner_program.to_bytes().into(), - owner_program.address(), - ) || !address_eq( - &request.rent_payer.to_bytes().into(), - delegation_rent_payer.address(), - ) { - return Err(DlpError::InvalidUndelegationRequest.into()); - } + UndelegationRequest::try_from_bytes_with_discriminator(&request_data) + .map_err(to_pinocchio_program_error)?; Ok(()) } diff --git a/src/processor/fast/undelegate_with_rollback_after_timeout.rs b/src/processor/fast/undelegate_with_rollback_after_timeout.rs index 44da8ffc..c22000e9 100644 --- a/src/processor/fast/undelegate_with_rollback_after_timeout.rs +++ b/src/processor/fast/undelegate_with_rollback_after_timeout.rs @@ -41,8 +41,7 @@ use crate::{ /// /// The owner program authorizes rollback by invoking this instruction through /// CPI and signing for the delegated account, same as the request/re-request -/// path. The request rent payer is only the recorded request rent recipient; -/// the rollback authority is the delegated-account signer. +/// path. /// /// This instruction releases the delegated account back to the owner program /// without making an external undelegate CPI, because the owner program is @@ -51,23 +50,22 @@ use crate::{ /// /// Accounts: /// -/// 0: `[writable]` request rent payer -/// 1: `[signer, writable]` delegated account -/// 2: `[]` owner program of the delegated account -/// 3: `[writable]` undelegation request PDA -/// 4: `[writable]` delegation record PDA -/// 5: `[writable]` delegation metadata PDA -/// 6: `[writable]` delegation rent payer -/// 7: `[writable]` commit state PDA -/// 8: `[writable]` commit record PDA -/// 9: `[writable]` commit reimbursement account +/// 0: `[signer, writable]` delegated account +/// 1: `[]` owner program of the delegated account +/// 2: `[writable]` undelegation request PDA +/// 3: `[writable]` delegation record PDA +/// 4: `[writable]` delegation metadata PDA +/// 5: `[writable]` delegation rent payer +/// 6: `[writable]` commit state PDA +/// 7: `[writable]` commit record PDA +/// 8: `[writable]` commit reimbursement account pub fn process_undelegate_with_rollback_after_timeout( _program_id: &Address, accounts: &[AccountView], _data: &[u8], ) -> ProgramResult { - let [request_rent_payer, delegated_account, owner_program, undelegation_request_account, delegation_record_account, delegation_metadata_account, delegation_rent_payer, commit_state_account, commit_record_account, commit_reimbursement] = - require_n_accounts!(accounts, 10); + let [delegated_account, owner_program, undelegation_request_account, delegation_record_account, delegation_metadata_account, delegation_rent_payer, commit_state_account, commit_record_account, commit_reimbursement] = + require_n_accounts!(accounts, 9); require_signer(delegated_account, "delegated account")?; require_owned_pda( @@ -76,12 +74,8 @@ pub fn process_undelegate_with_rollback_after_timeout( "delegated account", )?; - let request = load_valid_request( - delegated_account, - owner_program, - undelegation_request_account, - request_rent_payer, - )?; + let request = + load_valid_request(delegated_account, undelegation_request_account)?; let current_slot = Clock::get()?.slot; require_ge!( current_slot, @@ -145,7 +139,7 @@ pub fn process_undelegate_with_rollback_after_timeout( delegated_account.assign(owner_program.address()); } - close_pda(undelegation_request_account, request_rent_payer)?; + close_pda(undelegation_request_account, delegation_rent_payer)?; close_pda(delegation_record_account, delegation_rent_payer)?; close_pda(delegation_metadata_account, delegation_rent_payer)?; @@ -154,11 +148,9 @@ pub fn process_undelegate_with_rollback_after_timeout( fn load_valid_request( delegated_account: &AccountView, - owner_program: &AccountView, undelegation_request_account: &AccountView, - request_rent_payer: &AccountView, ) -> Result { - let request_bump = require_initialized_pda( + require_initialized_pda( undelegation_request_account, &[ pda::UNDELEGATION_REQUEST_TAG, @@ -174,27 +166,6 @@ fn load_valid_request( *UndelegationRequest::try_from_bytes_with_discriminator(&request_data) .map_err(to_pinocchio_program_error)?; - require_eq_keys!( - &request.delegated_account, - delegated_account.address(), - DlpError::InvalidUndelegationRequest - ); - require_eq_keys!( - &request.owner_program, - owner_program.address(), - DlpError::InvalidUndelegationRequest - ); - require_eq_keys!( - &request.rent_payer, - request_rent_payer.address(), - DlpError::InvalidUndelegationRequest - ); - require_eq!( - request.bump, - request_bump, - DlpError::InvalidUndelegationRequest - ); - Ok(request) } diff --git a/tests/fixtures/accounts.rs b/tests/fixtures/accounts.rs index c791859e..ee5ab8be 100644 --- a/tests/fixtures/accounts.rs +++ b/tests/fixtures/accounts.rs @@ -1,7 +1,6 @@ use dlp::solana_program; use dlp_api::{ consts::DEFAULT_UNDELEGATION_REQUEST_TIMEOUT_SLOTS, - pda::UNDELEGATION_REQUEST_TAG, state::{ CommitRecord, DelegationMetadata, DelegationRecord, ProgramConfig, UndelegationRequest, UndelegationRequester, @@ -223,42 +222,22 @@ pub fn create_program_config_data(approved_validator: Pubkey) -> Vec { #[allow(dead_code)] pub fn create_undelegation_request_data( delegated_account: Pubkey, - owner_program: Pubkey, - rent_payer: Pubkey, created_slot: u64, ) -> Vec { create_undelegation_request_data_with_expiry( delegated_account, - owner_program, - rent_payer, - created_slot, created_slot + DEFAULT_UNDELEGATION_REQUEST_TIMEOUT_SLOTS, - DEFAULT_LAST_UPDATE_EXTERNAL_SLOT, ) } #[allow(dead_code)] pub fn create_undelegation_request_data_with_expiry( delegated_account: Pubkey, - owner_program: Pubkey, - rent_payer: Pubkey, - created_slot: u64, expires_at_slot: u64, - last_commit_id_at_request: u64, ) -> Vec { - let (_, bump) = Pubkey::find_program_address( - &[UNDELEGATION_REQUEST_TAG, delegated_account.as_ref()], - &dlp_api::id(), - ); let request = UndelegationRequest { delegated_account, - owner_program, - rent_payer, - created_slot, expires_at_slot, - last_commit_id_at_request, - bump, - _padding: [0; 7], }; let mut bytes = vec![0u8; UndelegationRequest::size_with_discriminator()]; request.to_bytes_with_discriminator(&mut bytes).unwrap(); diff --git a/tests/test_request_undelegation.rs b/tests/test_request_undelegation.rs index 393d9a12..3ab75d10 100644 --- a/tests/test_request_undelegation.rs +++ b/tests/test_request_undelegation.rs @@ -81,13 +81,9 @@ async fn test_request_undelegation_creates_request_and_is_idempotent() { ) .unwrap(); assert_eq!(request.delegated_account, DELEGATED_PDA_ID); - assert_eq!(request.owner_program, DELEGATED_PDA_OWNER_ID); - assert_eq!(request.rent_payer, authority.pubkey()); - assert_eq!( - request.expires_at_slot, - request.created_slot + DEFAULT_UNDELEGATION_REQUEST_TIMEOUT_SLOTS + assert!( + request.expires_at_slot >= DEFAULT_UNDELEGATION_REQUEST_TIMEOUT_SLOTS ); - assert_eq!(request.last_commit_id_at_request, 0); let delegation_metadata_pda = delegation_metadata_pda_from_delegated_account(&DELEGATED_PDA_ID); @@ -593,11 +589,7 @@ async fn setup_env(config: SetupConfig) -> SetupContext { add_fee_accounts(&mut program_test, authority.pubkey()); } if config.with_request_account { - add_request_account( - &mut program_test, - delegated_account, - delegation_rent_payer, - ); + add_request_account(&mut program_test, delegated_account); } let (banks, _, blockhash) = program_test.start().await; @@ -753,14 +745,8 @@ fn add_fee_accounts(program_test: &mut ProgramTest, authority: Pubkey) { fn add_request_account( program_test: &mut ProgramTest, delegated_account: Pubkey, - rent_payer: Pubkey, ) { - let request_data = create_undelegation_request_data( - delegated_account, - DELEGATED_PDA_OWNER_ID, - rent_payer, - 1, - ); + let request_data = create_undelegation_request_data(delegated_account, 1); program_test.add_account( undelegation_request_pda_from_delegated_account(&delegated_account), Account { diff --git a/tests/test_undelegate_with_rollback_after_timeout.rs b/tests/test_undelegate_with_rollback_after_timeout.rs index ea6ba561..37c5aeba 100644 --- a/tests/test_undelegate_with_rollback_after_timeout.rs +++ b/tests/test_undelegate_with_rollback_after_timeout.rs @@ -46,56 +46,10 @@ mod fixtures; const TEST_PDA_SEED: &[u8] = b"test-pda"; -#[tokio::test] -async fn test_undelegate_with_rollback_after_timeout_rejects_wrong_request_payer( -) { - let ( - banks, - caller, - _request_rent_payer, - delegation_rent_payer, - _, - blockhash, - ) = setup_request_timeout_env(false, 0).await; - - let ix = rollback_from_owner_program( - caller.pubkey(), - delegation_rent_payer.pubkey(), - caller.pubkey(), - ); - - let tx = Transaction::new_signed_with_payer( - &[ix], - Some(&caller.pubkey()), - &[&caller], - blockhash, - ); - - let err = banks.process_transaction(tx).await.unwrap_err(); - assert!( - matches!( - err, - BanksClientError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(code), - ) - ) if code == DlpError::InvalidUndelegationRequest as u32 - ), - "expected InvalidUndelegationRequest, got {err:?}" - ); -} - #[tokio::test] async fn test_undelegate_with_rollback_after_timeout_after_expiry() { - let ( - banks, - caller, - request_rent_payer, - delegation_rent_payer, - _, - blockhash, - ) = setup_request_timeout_env(false, 0).await; + let (banks, caller, delegation_rent_payer, _, blockhash) = + setup_request_timeout_env(false, 0).await; let request_pda = undelegation_request_pda_from_delegated_account(&DELEGATED_PDA_ID); @@ -122,12 +76,6 @@ async fn test_undelegate_with_rollback_after_timeout_after_expiry() { .unwrap() .unwrap() .lamports; - let request_payer_before = banks - .get_account(request_rent_payer.pubkey()) - .await - .unwrap() - .unwrap() - .lamports; let delegation_payer_before = banks .get_account(delegation_rent_payer.pubkey()) .await @@ -138,9 +86,8 @@ async fn test_undelegate_with_rollback_after_timeout_after_expiry() { banks.get_account(DELEGATED_PDA_ID).await.unwrap().unwrap(); let ix = rollback_from_owner_program( - request_rent_payer.pubkey(), delegation_rent_payer.pubkey(), - request_rent_payer.pubkey(), + delegation_rent_payer.pubkey(), ); let tx = Transaction::new_signed_with_payer( &[ix], @@ -168,14 +115,6 @@ async fn test_undelegate_with_rollback_after_timeout_after_expiry() { assert_eq!(delegated_after.owner, DELEGATED_PDA_OWNER_ID); assert_eq!(delegated_after.data, delegated_before.data); - let request_payer_after = banks - .get_account(request_rent_payer.pubkey()) - .await - .unwrap() - .unwrap() - .lamports; - assert_eq!(request_payer_after, request_payer_before + request_lamports); - let delegation_payer_after = banks .get_account(delegation_rent_payer.pubkey()) .await @@ -185,6 +124,7 @@ async fn test_undelegate_with_rollback_after_timeout_after_expiry() { assert_eq!( delegation_payer_after, delegation_payer_before + + request_lamports + delegation_record_lamports + delegation_metadata_lamports ); @@ -192,19 +132,12 @@ async fn test_undelegate_with_rollback_after_timeout_after_expiry() { #[tokio::test] async fn test_undelegate_with_rollback_after_timeout_rejects_before_expiry() { - let ( - banks, - caller, - request_rent_payer, - delegation_rent_payer, - _, - blockhash, - ) = setup_request_timeout_env(false, 1_000_000).await; + let (banks, caller, delegation_rent_payer, _, blockhash) = + setup_request_timeout_env(false, 1_000_000).await; let ix = rollback_from_owner_program( - request_rent_payer.pubkey(), delegation_rent_payer.pubkey(), - request_rent_payer.pubkey(), + delegation_rent_payer.pubkey(), ); let tx = Transaction::new_signed_with_payer( &[ix], @@ -230,14 +163,8 @@ async fn test_undelegate_with_rollback_after_timeout_rejects_before_expiry() { #[tokio::test] async fn test_request_timeout_closes_pending_commit_without_applying_it() { - let ( - banks, - caller, - request_rent_payer, - delegation_rent_payer, - validator, - blockhash, - ) = setup_request_timeout_env(true, 0).await; + let (banks, caller, delegation_rent_payer, validator, blockhash) = + setup_request_timeout_env(true, 0).await; let commit_state_pda = commit_state_pda_from_delegated_account(&DELEGATED_PDA_ID); @@ -263,7 +190,6 @@ async fn test_request_timeout_closes_pending_commit_without_applying_it() { .lamports; let ix = rollback_from_owner_program( - request_rent_payer.pubkey(), delegation_rent_payer.pubkey(), validator.pubkey(), ); @@ -304,7 +230,7 @@ async fn test_request_timeout_closes_pending_commit_without_applying_it() { async fn setup_request_timeout_env( with_pending_commit: bool, expires_at_slot: u64, -) -> (BanksClient, Keypair, Keypair, Keypair, Keypair, Hash) { +) -> (BanksClient, Keypair, Keypair, Keypair, Hash) { let mut program_test = ProgramTest::default(); program_test.prefer_bpf(true); program_test.add_program("dlp", dlp_api::ID, None); @@ -317,34 +243,21 @@ async fn setup_request_timeout_env( program_test.prefer_bpf(true); let caller = Keypair::new(); - let request_rent_payer = Keypair::new(); let delegation_rent_payer = Keypair::new(); let validator = keypair_from_bytes(&TEST_AUTHORITY); add_system_account(&mut program_test, caller.pubkey()); - add_system_account(&mut program_test, request_rent_payer.pubkey()); add_system_account(&mut program_test, delegation_rent_payer.pubkey()); add_system_account(&mut program_test, validator.pubkey()); add_delegated_account(&mut program_test); add_delegation_accounts(&mut program_test, delegation_rent_payer.pubkey()); - add_request_account( - &mut program_test, - request_rent_payer.pubkey(), - expires_at_slot, - ); + add_request_account(&mut program_test, expires_at_slot); if with_pending_commit { add_pending_commit_accounts(&mut program_test, validator.pubkey()); } let (banks, _, blockhash) = program_test.start().await; - ( - banks, - caller, - request_rent_payer, - delegation_rent_payer, - validator, - blockhash, - ) + (banks, caller, delegation_rent_payer, validator, blockhash) } fn add_system_account( @@ -414,18 +327,10 @@ fn add_delegation_accounts( ); } -fn add_request_account( - program_test: &mut ProgramTest, - request_rent_payer: solana_program::pubkey::Pubkey, - expires_at_slot: u64, -) { +fn add_request_account(program_test: &mut ProgramTest, expires_at_slot: u64) { let request_data = create_undelegation_request_data_with_expiry( DELEGATED_PDA_ID, - DELEGATED_PDA_OWNER_ID, - request_rent_payer, - 0, expires_at_slot, - 0, ); program_test.add_account( undelegation_request_pda_from_delegated_account(&DELEGATED_PDA_ID), @@ -468,14 +373,12 @@ fn add_pending_commit_accounts( } fn rollback_from_owner_program( - request_rent_payer: Pubkey, delegation_rent_payer: Pubkey, commit_reimbursement: Pubkey, ) -> Instruction { Instruction { program_id: DELEGATED_PDA_OWNER_ID, accounts: vec![ - AccountMeta::new(request_rent_payer, false), AccountMeta::new(DELEGATED_PDA_ID, false), AccountMeta::new_readonly(DELEGATED_PDA_OWNER_ID, false), AccountMeta::new( @@ -529,7 +432,7 @@ fn process_rollback( program_id: &Pubkey, accounts: &[AccountInfo], ) -> ProgramResult { - let [request_rent_payer, delegated_account, owner_program, request_account, delegation_record_account, delegation_metadata_account, delegation_rent_payer, commit_state_account, commit_record_account, commit_reimbursement, dlp_program] = + let [delegated_account, owner_program, request_account, delegation_record_account, delegation_metadata_account, delegation_rent_payer, commit_state_account, commit_record_account, commit_reimbursement, dlp_program] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); @@ -543,7 +446,6 @@ fn process_rollback( let ix = dlp_api::instruction_builder::undelegate_with_rollback_after_timeout( - *request_rent_payer.key, *delegated_account.key, *owner_program.key, *delegation_rent_payer.key, @@ -554,7 +456,6 @@ fn process_rollback( invoke_signed( &ix, &[ - request_rent_payer.clone(), delegated_account.clone(), owner_program.clone(), request_account.clone(), From a5cb1901c4b5f73bce813cb5c6cda4aae0bd9bbe Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Wed, 8 Jul 2026 14:24:23 +0530 Subject: [PATCH 30/30] Require allow_undelegation = true for owner-requested commits --- dlp-api/src/error.rs | 3 + src/processor/fast/commit_state.rs | 39 ++++--- .../fast/internal/commit_finalize_internal.rs | 36 ++++-- tests/test_request_undelegation.rs | 109 +++++++++++++++--- 4 files changed, 144 insertions(+), 43 deletions(-) diff --git a/dlp-api/src/error.rs b/dlp-api/src/error.rs index 7c429214..fa5c6987 100644 --- a/dlp-api/src/error.rs +++ b/dlp-api/src/error.rs @@ -188,6 +188,9 @@ pub enum DlpError { #[error("Undelegation request accounts are required")] MissingUndelegationRequest = 54, + #[error("Owner program requested undelegation")] + OwnerRequestedUndelegation = 55, + #[error("An infallible error is encountered possibly due to logic error")] InfallibleError = 100, } diff --git a/src/processor/fast/commit_state.rs b/src/processor/fast/commit_state.rs index 3bcf65d7..5c1ee4db 100644 --- a/src/processor/fast/commit_state.rs +++ b/src/processor/fast/commit_state.rs @@ -167,24 +167,27 @@ pub(crate) fn process_commit_state_internal( return Err(DlpError::NonceOutOfOrder.into()); } - // Validator-requested undelegation is recorded by commit/finalize itself, - // so a later commit must not run again once that request exists. - if delegation_metadata.undelegation_requester - == UndelegationRequester::Validator - { - log!("delegation metadata already has an undelegation requester: "); - args.delegation_metadata_account.address().log(); - return Err(DlpError::AlreadyUndelegated.into()); - } - - // Record whether this commit requested undelegation. Preserve - // OwnerProgram requests so Undelegate can require request accounts. - if delegation_metadata.undelegation_requester == UndelegationRequester::None - { - delegation_metadata.undelegation_requester = - UndelegationRequester::from_allow_undelegation( - args.allow_undelegation, - ); + match delegation_metadata.undelegation_requester { + UndelegationRequester::Validator => { + log!("delegation metadata already has an undelegation requester: "); + args.delegation_metadata_account.address().log(); + return Err(DlpError::AlreadyUndelegated.into()); + } + UndelegationRequester::OwnerProgram => { + if !args.allow_undelegation { + log!( + "owner program requested undelegation but commit did not allow undelegation: " + ); + args.delegation_metadata_account.address().log(); + return Err(DlpError::OwnerRequestedUndelegation.into()); + } + } + UndelegationRequester::None => { + delegation_metadata.undelegation_requester = + UndelegationRequester::from_allow_undelegation( + args.allow_undelegation, + ); + } } delegation_metadata .to_bytes_with_discriminator(&mut delegation_metadata_data.as_mut()) diff --git a/src/processor/fast/internal/commit_finalize_internal.rs b/src/processor/fast/internal/commit_finalize_internal.rs index 1ef95640..0c0abe18 100644 --- a/src/processor/fast/internal/commit_finalize_internal.rs +++ b/src/processor/fast/internal/commit_finalize_internal.rs @@ -4,6 +4,7 @@ use pinocchio::{ sysvars::{rent::Rent, Sysvar}, AccountView, Address, }; +use pinocchio_log::log; use pinocchio_system::instructions as system; use crate::{ @@ -83,22 +84,35 @@ pub(crate) fn process_commit_finalize_internal( args.delegation_metadata_account, )?; - let prev_id = metadata.replace_last_commit_id(args.commit_id); + let prev_id = metadata.last_commit_id(); require_eq!(args.commit_id, prev_id + 1, DlpError::NonceOutOfOrder); let previous_requester = metadata.undelegation_requester()?; - require!( - previous_requester != UndelegationRequester::Validator, - DlpError::AlreadyUndelegated - ); - if previous_requester == UndelegationRequester::None { - metadata.set_undelegation_requester( - UndelegationRequester::from_allow_undelegation( - args.allow_undelegation, - ), - ); + match previous_requester { + UndelegationRequester::Validator => { + log!("delegation metadata already has an undelegation requester: "); + args.delegation_metadata_account.address().log(); + return Err(DlpError::AlreadyUndelegated.into()); + } + UndelegationRequester::OwnerProgram => { + if !args.allow_undelegation { + log!( + "owner program requested undelegation but commit did not allow undelegation: " + ); + args.delegation_metadata_account.address().log(); + return Err(DlpError::OwnerRequestedUndelegation.into()); + } + } + UndelegationRequester::None => { + metadata.set_undelegation_requester( + UndelegationRequester::from_allow_undelegation( + args.allow_undelegation, + ), + ); + } } + metadata.set_last_commit_id(args.commit_id); } let mut delegation_record_data = diff --git a/tests/test_request_undelegation.rs b/tests/test_request_undelegation.rs index 3ab75d10..22c0428b 100644 --- a/tests/test_request_undelegation.rs +++ b/tests/test_request_undelegation.rs @@ -238,7 +238,7 @@ async fn test_undelegate_with_request_closes_request() { } #[tokio::test] -async fn test_commit_state_preserves_owner_program_requester() { +async fn test_commit_state_rejects_owner_request_without_allow() { let SetupContext { banks, authority, @@ -270,6 +270,43 @@ async fn test_commit_state_preserves_owner_program_requester() { &[&authority], blockhash, ); + let err = banks.process_transaction(tx).await.unwrap_err(); + assert_custom_error(err, DlpError::OwnerRequestedUndelegation); +} + +#[tokio::test] +async fn test_commit_state_preserves_owner_request_with_allow() { + let SetupContext { + banks, + authority, + blockhash, + .. + } = setup_env(SetupConfig { + with_request_wrapper: true, + with_fee_accounts: true, + ..Default::default() + }) + .await; + + let request_ix = + request_undelegation_from_owner_program(authority.pubkey()); + let commit_ix = dlp_api::instruction_builder::commit_state( + authority.pubkey(), + DELEGATED_PDA_ID, + DELEGATED_PDA_OWNER_ID, + CommitStateArgs { + data: COMMIT_NEW_STATE_ACCOUNT_DATA.to_vec(), + nonce: 1, + allow_undelegation: true, + lamports: LAMPORTS_PER_SOL, + }, + ); + let tx = Transaction::new_signed_with_payer( + &[request_ix, commit_ix], + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); banks.process_transaction(tx).await.unwrap(); assert_delegation_metadata_requester( @@ -280,7 +317,7 @@ async fn test_commit_state_preserves_owner_program_requester() { } #[tokio::test] -async fn test_commit_finalize_preserves_owner_program_requester() { +async fn test_commit_finalize_rejects_owner_request_without_allow() { let SetupContext { banks, authority, @@ -315,6 +352,46 @@ async fn test_commit_finalize_preserves_owner_program_requester() { &[&authority], blockhash, ); + let err = banks.process_transaction(tx).await.unwrap_err(); + assert_custom_error(err, DlpError::OwnerRequestedUndelegation); +} + +#[tokio::test] +async fn test_commit_finalize_preserves_owner_request_with_allow() { + let SetupContext { + banks, + authority, + blockhash, + .. + } = setup_env(SetupConfig { + with_request_wrapper: true, + with_fee_accounts: true, + ..Default::default() + }) + .await; + + let request_ix = + request_undelegation_from_owner_program(authority.pubkey()); + let mut args = CommitFinalizeArgs { + commit_id: 1, + lamports: LAMPORTS_PER_SOL, + allow_undelegation: true.into(), + data_is_diff: false.into(), + bumps: Default::default(), + reserved_padding: Default::default(), + }; + let (commit_finalize_ix, _) = dlp_api::instruction_builder::commit_finalize( + authority.pubkey(), + DELEGATED_PDA_ID, + &mut args, + &COMMIT_NEW_STATE_ACCOUNT_DATA, + ); + let tx = Transaction::new_signed_with_payer( + &[request_ix, commit_finalize_ix], + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); banks.process_transaction(tx).await.unwrap(); assert_delegation_metadata_requester( @@ -375,18 +452,7 @@ async fn test_undelegate_owner_program_request_without_request_accounts_rejected blockhash, ); let err = banks.process_transaction(undelegate_tx).await.unwrap_err(); - assert!( - matches!( - err, - BanksClientError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(code), - ) - ) if code == DlpError::MissingUndelegationRequest as u32 - ), - "expected MissingUndelegationRequest, got {err:?}" - ); + assert_custom_error(err, DlpError::MissingUndelegationRequest); } async fn assert_delegation_metadata_requester( @@ -411,6 +477,21 @@ async fn assert_delegation_metadata_requester( ); } +fn assert_custom_error(err: BanksClientError, expected: DlpError) { + assert!( + matches!( + err, + BanksClientError::TransactionError( + TransactionError::InstructionError( + _, + InstructionError::Custom(code), + ) + ) if code == expected as u32 + ), + "expected {expected:?}, got {err:?}" + ); +} + fn imaginary_program_processor_requesting_undelegation_through_cpi( program_id: &Pubkey, accounts: &[AccountInfo],