From 2aa596bb44cfc9a399cd04a05cc0756265fb9fe6 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Thu, 2 Jul 2026 02:35:13 +0530 Subject: [PATCH 1/5] feat: undelegate during finalize for validator requests --- .../fast/internal/undelegate_internal.rs | 19 +- tests/test_commit_finalize.rs | 4 +- tests/test_commit_finalize_from_buffer.rs | 4 +- tests/test_request_undelegation.rs | 183 ++++++++++++++---- tests/test_undelegate.rs | 2 +- 5 files changed, 158 insertions(+), 54 deletions(-) diff --git a/src/processor/fast/internal/undelegate_internal.rs b/src/processor/fast/internal/undelegate_internal.rs index dd4e9c28..56ed4a8e 100644 --- a/src/processor/fast/internal/undelegate_internal.rs +++ b/src/processor/fast/internal/undelegate_internal.rs @@ -75,12 +75,13 @@ pub(crate) fn process_auto_undelegation_if_requested( system_program: &AccountView, auto_accounts: Option>, ) -> ProgramResult { - if requester != UndelegationRequester::OwnerProgram { + if requester == UndelegationRequester::None { return Ok(()); } let auto_accounts = auto_accounts.ok_or(ProgramError::NotEnoughAccountKeys)?; + process_undelegation(UndelegationAccounts { validator, delegated_account, @@ -92,11 +93,17 @@ pub(crate) fn process_auto_undelegation_if_requested( fees_vault: auto_accounts.fees_vault, validator_fees_vault, system_program, - // See UndelegationRequest::rent_payer for this invariant. - request_accounts: Some(( - auto_accounts.undelegation_request_account, - auto_accounts.rent_reimbursement, - )), + // For OwnerProgram requests, see UndelegationRequest::rent_payer + // for the rent-payer invariant. Validator requests do not use the + // request PDA. + request_accounts: if requester == UndelegationRequester::OwnerProgram { + Some(( + auto_accounts.undelegation_request_account, + auto_accounts.rent_reimbursement, + )) + } else { + None + }, }) } diff --git a/tests/test_commit_finalize.rs b/tests/test_commit_finalize.rs index 8a552836..3dd5db1c 100644 --- a/tests/test_commit_finalize.rs +++ b/tests/test_commit_finalize.rs @@ -61,7 +61,7 @@ async fn run_test_commit_finalize( authority.pubkey(), &mut CommitFinalizeArgs { commit_id: 1, - allow_undelegation: true.into(), + allow_undelegation: false.into(), data_is_diff: data_is_diff.into(), lamports: new_account_balance, bumps: Default::default(), @@ -119,7 +119,7 @@ async fn run_test_commit_finalize( assert_eq!( delegation_metadata.undelegation_requester, - dlp_api::state::UndelegationRequester::Validator + dlp_api::state::UndelegationRequester::None ); } diff --git a/tests/test_commit_finalize_from_buffer.rs b/tests/test_commit_finalize_from_buffer.rs index 53434dfb..6443ec3a 100644 --- a/tests/test_commit_finalize_from_buffer.rs +++ b/tests/test_commit_finalize_from_buffer.rs @@ -47,7 +47,7 @@ async fn test_commit_finalize_from_buffer_perf() { authority.pubkey(), &mut CommitFinalizeArgs { commit_id: 1, - allow_undelegation: true.into(), + allow_undelegation: false.into(), data_is_diff: false.into(), lamports: new_account_balance, bumps: Default::default(), @@ -97,7 +97,7 @@ async fn test_commit_finalize_from_buffer_perf() { assert_eq!( delegation_metadata.undelegation_requester, - dlp_api::state::UndelegationRequester::Validator + dlp_api::state::UndelegationRequester::None ); } diff --git a/tests/test_request_undelegation.rs b/tests/test_request_undelegation.rs index 1609ecb6..792a5433 100644 --- a/tests/test_request_undelegation.rs +++ b/tests/test_request_undelegation.rs @@ -350,12 +350,6 @@ async fn test_undelegate_with_request_closes_request() { .unwrap() .lamports; - let ix_finalize = dlp_api::instruction_builder::finalize( - authority.pubkey(), - DELEGATED_PDA_ID, - DELEGATED_PDA_OWNER_ID, - request_rent_payer.pubkey(), - ); let ix_undelegate = dlp_api::instruction_builder::undelegate_with_request( authority.pubkey(), DELEGATED_PDA_ID, @@ -365,7 +359,7 @@ async fn test_undelegate_with_request_closes_request() { ); let tx = Transaction::new_signed_with_payer( - &[ix_finalize, ix_undelegate], + &[ix_undelegate], Some(&authority.pubkey()), &[&authority], blockhash, @@ -434,24 +428,7 @@ async fn test_finalize_auto_undelegates_owner_program_request_and_trailing_undel assert!(res.is_ok(), "{res:?}"); assert!(banks.get_account(request_pda).await.unwrap().is_none()); - assert!(banks - .get_account(delegation_record_pda_from_delegated_account( - &DELEGATED_PDA_ID, - )) - .await - .unwrap() - .is_none()); - assert!(banks - .get_account(delegation_metadata_pda_from_delegated_account( - &DELEGATED_PDA_ID, - )) - .await - .unwrap() - .is_none()); - - let delegated_account = - banks.get_account(DELEGATED_PDA_ID).await.unwrap().unwrap(); - assert_eq!(delegated_account.owner, DELEGATED_PDA_OWNER_ID); + assert_delegation_closed_and_account_returned(&banks).await; } #[tokio::test] @@ -502,24 +479,7 @@ async fn test_commit_finalize_auto_undelegates_owner_program_request() { assert!(res.is_ok(), "{res:?}"); assert!(banks.get_account(request_pda).await.unwrap().is_none()); - assert!(banks - .get_account(delegation_record_pda_from_delegated_account( - &DELEGATED_PDA_ID, - )) - .await - .unwrap() - .is_none()); - assert!(banks - .get_account(delegation_metadata_pda_from_delegated_account( - &DELEGATED_PDA_ID, - )) - .await - .unwrap() - .is_none()); - - let delegated_account = - banks.get_account(DELEGATED_PDA_ID).await.unwrap().unwrap(); - assert_eq!(delegated_account.owner, DELEGATED_PDA_OWNER_ID); + assert_delegation_closed_and_account_returned(&banks).await; } #[tokio::test] @@ -573,6 +533,143 @@ async fn test_commit_finalize_from_buffer_auto_undelegates_owner_program_request assert!(res.is_ok(), "{res:?}"); assert!(banks.get_account(request_pda).await.unwrap().is_none()); + assert_delegation_closed_and_account_returned(&banks).await; +} + +#[tokio::test] +async fn test_finalize_auto_undelegates_validator_request() { + let SetupContext { + banks, + payer, + authority, + blockhash, + .. + } = setup_env(SetupConfig { + requester: UndelegationRequester::Validator, + with_commit_accounts: true, + with_owner_program: true, + with_fee_accounts: true, + ..Default::default() + }) + .await; + + let ix = dlp_api::instruction_builder::finalize( + authority.pubkey(), + DELEGATED_PDA_ID, + DELEGATED_PDA_OWNER_ID, + payer.pubkey(), + ); + + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok(), "{res:?}"); + + assert_delegation_closed_and_account_returned(&banks).await; +} + +#[tokio::test] +async fn test_commit_finalize_auto_undelegates_validator_request() { + let SetupContext { + banks, + payer, + authority, + blockhash, + .. + } = setup_env(SetupConfig { + with_owner_program: true, + with_fee_accounts: true, + ..Default::default() + }) + .await; + + let request_pda = + undelegation_request_pda_from_delegated_account(&DELEGATED_PDA_ID); + 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 (ix, _) = dlp_api::instruction_builder::commit_finalize( + authority.pubkey(), + DELEGATED_PDA_ID, + DELEGATED_PDA_OWNER_ID, + payer.pubkey(), + &mut args, + &COMMIT_NEW_STATE_ACCOUNT_DATA, + ); + + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok(), "{res:?}"); + + assert!(banks.get_account(request_pda).await.unwrap().is_none()); + assert_delegation_closed_and_account_returned(&banks).await; +} + +#[tokio::test] +async fn test_commit_finalize_from_buffer_auto_undelegates_validator_request() { + let SetupContext { + banks, + payer, + authority, + blockhash, + .. + } = setup_env(SetupConfig { + with_state_buffer: true, + with_owner_program: true, + with_fee_accounts: true, + ..Default::default() + }) + .await; + + let request_pda = + undelegation_request_pda_from_delegated_account(&DELEGATED_PDA_ID); + let state_buffer_pda = + Pubkey::find_program_address(&[b"state_buffer"], &authority.pubkey()).0; + 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 (ix, _) = dlp_api::instruction_builder::commit_finalize_from_buffer( + authority.pubkey(), + DELEGATED_PDA_ID, + state_buffer_pda, + DELEGATED_PDA_OWNER_ID, + payer.pubkey(), + &mut args, + ); + + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok(), "{res:?}"); + + assert!(banks.get_account(request_pda).await.unwrap().is_none()); + assert_delegation_closed_and_account_returned(&banks).await; +} + +async fn assert_delegation_closed_and_account_returned(banks: &BanksClient) { assert!(banks .get_account(delegation_record_pda_from_delegated_account( &DELEGATED_PDA_ID, diff --git a/tests/test_undelegate.rs b/tests/test_undelegate.rs index e05b08a1..5704de24 100644 --- a/tests/test_undelegate.rs +++ b/tests/test_undelegate.rs @@ -122,7 +122,7 @@ async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { // Setup the delegated record PDA let delegation_record_data = - get_delegation_record_data(authority.pubkey(), None); + get_delegation_record_data(authority.pubkey(), Some(LAMPORTS_PER_SOL)); program_test.add_account( delegation_record_pda_from_delegated_account(&DELEGATED_PDA_ID), Account { From f573559a46672b640a24ad41942100d5aa603e08 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Thu, 2 Jul 2026 14:35:50 +0530 Subject: [PATCH 2/5] make undelegate backward-comptible --- dlp-api/src/state/delegation_metadata.rs | 3 +++ src/processor/fast/internal/undelegate_internal.rs | 12 ++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/dlp-api/src/state/delegation_metadata.rs b/dlp-api/src/state/delegation_metadata.rs index 79d2f077..e558f8d4 100644 --- a/dlp-api/src/state/delegation_metadata.rs +++ b/dlp-api/src/state/delegation_metadata.rs @@ -26,6 +26,9 @@ pub enum UndelegationRequester { /// Kept backward compatible with legacy `is_undelegatable = true`. Validator, /// The account owner program requested undelegation. + /// + /// Encoded as byte `2`; legacy clients that deserialize this byte as a + /// bool may reject metadata that contains this variant. OwnerProgram, } diff --git a/src/processor/fast/internal/undelegate_internal.rs b/src/processor/fast/internal/undelegate_internal.rs index 56ed4a8e..14c09c51 100644 --- a/src/processor/fast/internal/undelegate_internal.rs +++ b/src/processor/fast/internal/undelegate_internal.rs @@ -79,8 +79,16 @@ pub(crate) fn process_auto_undelegation_if_requested( return Ok(()); } - let auto_accounts = - auto_accounts.ok_or(ProgramError::NotEnoughAccountKeys)?; + let Some(auto_accounts) = auto_accounts else { + if requester == UndelegationRequester::Validator { + log!( + "WARN: validator-requested undelegation skipped; \ + auto-undelegation accounts were not provided" + ); + return Ok(()); + } + return Err(ProgramError::NotEnoughAccountKeys); + }; process_undelegation(UndelegationAccounts { validator, From a968fcb47efcaa0e6c7cd10cf9e27ccb8980d92d Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Fri, 3 Jul 2026 02:59:20 +0530 Subject: [PATCH 3/5] add backward-compatability tests --- dlp-api/src/state/delegation_metadata.rs | 1 + tests/test_request_undelegation.rs | 194 +++++++++++++++++++++++ 2 files changed, 195 insertions(+) diff --git a/dlp-api/src/state/delegation_metadata.rs b/dlp-api/src/state/delegation_metadata.rs index e558f8d4..ba7540c5 100644 --- a/dlp-api/src/state/delegation_metadata.rs +++ b/dlp-api/src/state/delegation_metadata.rs @@ -29,6 +29,7 @@ pub enum UndelegationRequester { /// /// Encoded as byte `2`; legacy clients that deserialize this byte as a /// bool may reject metadata that contains this variant. + // TODO (snawaz): edit the doc once all validators upgrade. OwnerProgram, } diff --git a/tests/test_request_undelegation.rs b/tests/test_request_undelegation.rs index 792a5433..fbf33387 100644 --- a/tests/test_request_undelegation.rs +++ b/tests/test_request_undelegation.rs @@ -669,6 +669,165 @@ async fn test_commit_finalize_from_buffer_auto_undelegates_validator_request() { assert_delegation_closed_and_account_returned(&banks).await; } +// Covers older validators that still send the pre-auto-undelegation Finalize +// account layout. Validator-requested undelegation should not make finalize +// fail; it should skip undelegation and leave the standalone undelegate flow +// available. +#[tokio::test] +async fn test_finalize_skips_validator_undelegation_without_auto_accounts_for_backward_compat( +) { + let (banks, _, authority, rent_payer, blockhash) = + setup_undelegate_with_requester_env_config( + UndelegationRequester::Validator, + false, + true, + false, + ) + .await; + + let mut ix = dlp_api::instruction_builder::finalize( + authority.pubkey(), + DELEGATED_PDA_ID, + DELEGATED_PDA_OWNER_ID, + rent_payer.pubkey(), + ); + // Simulate an older validator that sends the pre-auto-undelegation + // Finalize account list. The state should still finalize, while + // undelegation is skipped for backward compatibility. + ix.accounts.truncate(8); + + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok(), "{res:?}"); + + 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); + assert!(banks.get_account(commit_state_pda).await.unwrap().is_none()); + assert!(banks + .get_account(commit_record_pda) + .await + .unwrap() + .is_none()); + assert_delegation_still_active_with_validator_requester( + &banks, + &COMMIT_NEW_STATE_ACCOUNT_DATA, + ) + .await; +} + +// Covers older validators that still send the pre-auto-undelegation +// CommitFinalize account layout. The commit should finalize, while +// undelegation is skipped for backward compatibility. +#[tokio::test] +async fn test_commit_finalize_skips_validator_undelegation_without_auto_accounts_for_backward_compat( +) { + let (banks, _, authority, rent_payer, blockhash) = + setup_undelegate_with_requester_env_config( + UndelegationRequester::None, + false, + false, + false, + ) + .await; + + 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 (mut ix, _) = dlp_api::instruction_builder::commit_finalize( + authority.pubkey(), + DELEGATED_PDA_ID, + DELEGATED_PDA_OWNER_ID, + rent_payer.pubkey(), + &mut args, + &COMMIT_NEW_STATE_ACCOUNT_DATA, + ); + // Simulate an older validator that sends the pre-auto-undelegation + // CommitFinalize account list. The commit should still finalize, while + // undelegation is skipped for backward compatibility. + ix.accounts.truncate(6); + + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok(), "{res:?}"); + + assert_delegation_still_active_with_validator_requester( + &banks, + &COMMIT_NEW_STATE_ACCOUNT_DATA, + ) + .await; +} + +// Covers older validators that still send the pre-auto-undelegation +// CommitFinalizeFromBuffer account layout. The commit should finalize, while +// undelegation is skipped for backward compatibility. +#[tokio::test] +async fn test_commit_finalize_from_buffer_skips_validator_undelegation_without_auto_accounts_for_backward_compat( +) { + let (banks, _, authority, rent_payer, blockhash) = + setup_undelegate_with_requester_env_config( + UndelegationRequester::None, + false, + false, + true, + ) + .await; + + let state_buffer_pda = + Pubkey::find_program_address(&[b"state_buffer"], &authority.pubkey()).0; + 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 (mut ix, _) = dlp_api::instruction_builder::commit_finalize_from_buffer( + authority.pubkey(), + DELEGATED_PDA_ID, + state_buffer_pda, + DELEGATED_PDA_OWNER_ID, + rent_payer.pubkey(), + &mut args, + ); + // Simulate an older validator that sends the pre-auto-undelegation + // CommitFinalizeFromBuffer account list. The commit should still + // finalize, while undelegation is skipped for backward compatibility. + ix.accounts.truncate(7); + + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok(), "{res:?}"); + + assert_delegation_still_active_with_validator_requester( + &banks, + &COMMIT_NEW_STATE_ACCOUNT_DATA, + ) + .await; +} + async fn assert_delegation_closed_and_account_returned(banks: &BanksClient) { assert!(banks .get_account(delegation_record_pda_from_delegated_account( @@ -690,6 +849,41 @@ async fn assert_delegation_closed_and_account_returned(banks: &BanksClient) { assert_eq!(delegated_account.owner, DELEGATED_PDA_OWNER_ID); } +async fn assert_delegation_still_active_with_validator_requester( + banks: &BanksClient, + expected_data: &[u8], +) { + assert!(banks + .get_account(delegation_record_pda_from_delegated_account( + &DELEGATED_PDA_ID, + )) + .await + .unwrap() + .is_some()); + + let delegation_metadata_account = banks + .get_account(delegation_metadata_pda_from_delegated_account( + &DELEGATED_PDA_ID, + )) + .await + .unwrap() + .unwrap(); + let delegation_metadata = + DelegationMetadata::try_from_bytes_with_discriminator( + &delegation_metadata_account.data, + ) + .unwrap(); + assert_eq!( + delegation_metadata.undelegation_requester, + UndelegationRequester::Validator + ); + + let delegated_account = + banks.get_account(DELEGATED_PDA_ID).await.unwrap().unwrap(); + assert_eq!(delegated_account.owner, dlp_api::id()); + assert_eq!(delegated_account.data, expected_data); +} + #[tokio::test] async fn test_undelegate_with_malformed_optional_request_accounts_rejected() { let SetupContext { From 05c9ab5ff9e7cadb9c5a4bfc1b7cb712f74d549e Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Fri, 3 Jul 2026 16:05:14 +0530 Subject: [PATCH 4/5] use config-driven undelegation request setup in tests --- tests/test_call_handler.rs | 31 ++++++++++-- tests/test_call_handler_v2.rs | 31 ++++++++++-- tests/test_lamports_settlement.rs | 23 +++++++-- tests/test_request_undelegation.rs | 75 ++++++++++++++++++------------ 4 files changed, 115 insertions(+), 45 deletions(-) diff --git a/tests/test_call_handler.rs b/tests/test_call_handler.rs index f05e832b..2c9f4f62 100644 --- a/tests/test_call_handler.rs +++ b/tests/test_call_handler.rs @@ -15,7 +15,9 @@ use dlp_api::{ }, }; use solana_program::{ - hash::Hash, instruction::AccountMeta, native_token::LAMPORTS_PER_SOL, + hash::Hash, + instruction::{AccountMeta, Instruction}, + native_token::LAMPORTS_PER_SOL, rent::Rent, }; use solana_program_test::{processor, read_file, BanksClient, ProgramTest}; @@ -328,6 +330,25 @@ async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { (banks, payer, validator, blockhash) } +fn legacy_finalize_ix( + validator: Pubkey, + delegated_account: Pubkey, + owner_program: Pubkey, + rent_reimbursement: Pubkey, +) -> Instruction { + let mut ix = dlp_api::instruction_builder::finalize( + validator, + delegated_account, + owner_program, + rent_reimbursement, + ); + // These tests exercise call_handler with the pre-auto-undelegation finalize + // layout. Validator-requested undelegation is skipped when auto accounts are + // absent, keeping standalone undelegate available for the following ix. + ix.accounts.truncate(8); + ix +} + /// Test call_handler in finalize context #[tokio::test] async fn test_finalize_call_handler() { @@ -336,7 +357,7 @@ async fn test_finalize_call_handler() { let (banks, payer, validator, blockhash) = setup_program_test_env().await; let transfer_destination = Keypair::new(); - let finalize_ix = dlp_api::instruction_builder::finalize( + let finalize_ix = legacy_finalize_ix( validator.pubkey(), DELEGATED_PDA_ID, DELEGATED_PDA_OWNER_ID, @@ -386,7 +407,7 @@ async fn test_undelegate_call_handler() { let (banks, payer, validator, blockhash) = setup_program_test_env().await; let transfer_destination = Keypair::new(); - let finalize_ix = dlp_api::instruction_builder::finalize( + let finalize_ix = legacy_finalize_ix( validator.pubkey(), DELEGATED_PDA_ID, DELEGATED_PDA_OWNER_ID, @@ -455,7 +476,7 @@ async fn test_finalize_invalid_escrow_call_handler() { // Submit the finalize with handler tx let transfer_destination = Keypair::new(); - let finalize_ix = dlp_api::instruction_builder::finalize( + let finalize_ix = legacy_finalize_ix( authority.pubkey(), DELEGATED_PDA_ID, DELEGATED_PDA_OWNER_ID, @@ -492,7 +513,7 @@ async fn test_undelegate_invalid_escow_call_handler() { // Submit the finalize with handler tx let destination = Keypair::new(); - let finalize_ix = dlp_api::instruction_builder::finalize( + let finalize_ix = legacy_finalize_ix( authority.pubkey(), DELEGATED_PDA_ID, DELEGATED_PDA_OWNER_ID, diff --git a/tests/test_call_handler_v2.rs b/tests/test_call_handler_v2.rs index c200df7e..6f06d4a5 100644 --- a/tests/test_call_handler_v2.rs +++ b/tests/test_call_handler_v2.rs @@ -13,7 +13,9 @@ use dlp_api::{ }, }; use solana_program::{ - hash::Hash, instruction::AccountMeta, native_token::LAMPORTS_PER_SOL, + hash::Hash, + instruction::{AccountMeta, Instruction}, + native_token::LAMPORTS_PER_SOL, rent::Rent, }; use solana_program_test::{read_file, BanksClient, ProgramTest}; @@ -322,6 +324,25 @@ async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { (banks, payer, validator, blockhash) } +fn legacy_finalize_ix( + validator: Pubkey, + delegated_account: Pubkey, + owner_program: Pubkey, + rent_reimbursement: Pubkey, +) -> Instruction { + let mut ix = dlp_api::instruction_builder::finalize( + validator, + delegated_account, + owner_program, + rent_reimbursement, + ); + // These tests exercise call_handler_v2 with the pre-auto-undelegation + // finalize layout. Validator-requested undelegation is skipped when auto + // accounts are absent, keeping standalone undelegate available afterward. + ix.accounts.truncate(8); + ix +} + /// Test call_handler_v2 in finalize context #[tokio::test] async fn test_finalize_call_handler_v2() { @@ -330,7 +351,7 @@ async fn test_finalize_call_handler_v2() { let (banks, payer, validator, blockhash) = setup_program_test_env().await; let transfer_destination = Keypair::new(); - let finalize_ix = dlp_api::instruction_builder::finalize( + let finalize_ix = legacy_finalize_ix( validator.pubkey(), DELEGATED_PDA_ID, DELEGATED_PDA_OWNER_ID, @@ -381,7 +402,7 @@ async fn test_undelegate_call_handler_v2() { let (banks, payer, validator, blockhash) = setup_program_test_env().await; let transfer_destination = Keypair::new(); - let finalize_ix = dlp_api::instruction_builder::finalize( + let finalize_ix = legacy_finalize_ix( validator.pubkey(), DELEGATED_PDA_ID, DELEGATED_PDA_OWNER_ID, @@ -449,7 +470,7 @@ async fn test_finalize_invalid_escrow_call_handler_v2() { // Submit the finalize with handler tx let transfer_destination = Keypair::new(); - let finalize_ix = dlp_api::instruction_builder::finalize( + let finalize_ix = legacy_finalize_ix( authority.pubkey(), DELEGATED_PDA_ID, DELEGATED_PDA_OWNER_ID, @@ -487,7 +508,7 @@ async fn test_undelegate_invalid_escrow_call_handler_v2() { // Submit the finalize with handler tx let destination = Keypair::new(); - let finalize_ix = dlp_api::instruction_builder::finalize( + let finalize_ix = legacy_finalize_ix( authority.pubkey(), DELEGATED_PDA_ID, DELEGATED_PDA_OWNER_ID, diff --git a/tests/test_lamports_settlement.rs b/tests/test_lamports_settlement.rs index cff8e14a..62abcc76 100644 --- a/tests/test_lamports_settlement.rs +++ b/tests/test_lamports_settlement.rs @@ -742,6 +742,7 @@ pub async fn test_commit_system_account_after_balance_decrease( authority: &authority, blockhash, new_delegated_account_lamports, + allow_undelegation: also_undelegate, delegated_account, delegated_account_owner: owner_program, }) @@ -799,6 +800,7 @@ async fn test_commit_system_account_after_balance_increase( authority: &authority, blockhash, new_delegated_account_lamports, + allow_undelegation: also_undelegate, delegated_account, delegated_account_owner: owner_program, }) @@ -854,6 +856,7 @@ async fn test_commit_system_account_after_balance_decrease_and_increase_mainchai authority: &authority, blockhash, new_delegated_account_lamports, + allow_undelegation: also_undelegate, delegated_account, delegated_account_owner: owner_program, }) @@ -912,6 +915,7 @@ async fn test_commit_system_account_after_balance_increase_and_increase_mainchai authority: &authority, blockhash, new_delegated_account_lamports, + allow_undelegation: also_undelegate, delegated_account, delegated_account_owner: owner_program, }) @@ -1049,12 +1053,17 @@ struct FinalizeNewStateArgs<'a> { } async fn finalize_new_state(args: FinalizeNewStateArgs<'_>) { - let ix = dlp_api::instruction_builder::finalize( + let mut ix = dlp_api::instruction_builder::finalize( args.authority.pubkey(), args.delegated_account, args.owner_program, args.authority.pubkey(), ); + // These lamports-settlement tests cover the original two-step + // finalize-then-undelegate flow. Keep finalize on the pre-auto-undelegation + // layout so `commit_undelegate_*` variants exercise the standalone + // undelegate instruction below. + ix.accounts.truncate(8); let tx = Transaction::new_signed_with_payer( &[ix], Some(&args.authority.pubkey()), @@ -1064,7 +1073,7 @@ async fn finalize_new_state(args: FinalizeNewStateArgs<'_>) { let res = args.banks.process_transaction(tx).await; assert!(res.is_ok()); - // Assert that the account owner is still the delegation program + // Assert that the account owner is still the delegation program. let pda_account = args .banks .get_account(args.delegated_account) @@ -1079,6 +1088,7 @@ struct CommitNewStateArgs<'a> { authority: &'a Keypair, blockhash: Hash, new_delegated_account_lamports: u64, + allow_undelegation: bool, delegated_account: Pubkey, delegated_account_owner: Pubkey, } @@ -1158,7 +1168,7 @@ async fn commit_new_state(args: CommitNewStateArgs<'_>) { let commit_args = CommitStateArgs { data: data.clone(), nonce: 1, - allow_undelegation: true, + allow_undelegation: args.allow_undelegation, lamports: args.new_delegated_account_lamports, }; @@ -1231,9 +1241,14 @@ async fn commit_new_state(args: CommitNewStateArgs<'_>) { &delegation_metadata_account.data, ) .unwrap(); + let expected_requester = if args.allow_undelegation { + dlp_api::state::UndelegationRequester::Validator + } else { + dlp_api::state::UndelegationRequester::None + }; assert_eq!( delegation_metadata.undelegation_requester, - dlp_api::state::UndelegationRequester::Validator + expected_requester ); } diff --git a/tests/test_request_undelegation.rs b/tests/test_request_undelegation.rs index fbf33387..e067d89f 100644 --- a/tests/test_request_undelegation.rs +++ b/tests/test_request_undelegation.rs @@ -325,9 +325,7 @@ async fn test_undelegate_with_request_closes_request() { blockhash, .. } = setup_env(SetupConfig { - requester: UndelegationRequester::Validator, - with_commit_accounts: true, - with_state_buffer: true, + requester: UndelegationRequester::OwnerProgram, with_owner_program: true, with_fee_accounts: true, with_request_account: true, @@ -676,20 +674,26 @@ async fn test_commit_finalize_from_buffer_auto_undelegates_validator_request() { #[tokio::test] async fn test_finalize_skips_validator_undelegation_without_auto_accounts_for_backward_compat( ) { - let (banks, _, authority, rent_payer, blockhash) = - setup_undelegate_with_requester_env_config( - UndelegationRequester::Validator, - false, - true, - false, - ) - .await; + let SetupContext { + banks, + payer, + authority, + blockhash, + .. + } = setup_env(SetupConfig { + requester: UndelegationRequester::Validator, + with_commit_accounts: true, + with_owner_program: true, + with_fee_accounts: true, + ..Default::default() + }) + .await; let mut ix = dlp_api::instruction_builder::finalize( authority.pubkey(), DELEGATED_PDA_ID, DELEGATED_PDA_OWNER_ID, - rent_payer.pubkey(), + payer.pubkey(), ); // Simulate an older validator that sends the pre-auto-undelegation // Finalize account list. The state should still finalize, while @@ -728,14 +732,18 @@ async fn test_finalize_skips_validator_undelegation_without_auto_accounts_for_ba #[tokio::test] async fn test_commit_finalize_skips_validator_undelegation_without_auto_accounts_for_backward_compat( ) { - let (banks, _, authority, rent_payer, blockhash) = - setup_undelegate_with_requester_env_config( - UndelegationRequester::None, - false, - false, - false, - ) - .await; + let SetupContext { + banks, + payer, + authority, + blockhash, + .. + } = setup_env(SetupConfig { + with_owner_program: true, + with_fee_accounts: true, + ..Default::default() + }) + .await; let mut args = CommitFinalizeArgs { commit_id: 1, @@ -749,7 +757,7 @@ async fn test_commit_finalize_skips_validator_undelegation_without_auto_accounts authority.pubkey(), DELEGATED_PDA_ID, DELEGATED_PDA_OWNER_ID, - rent_payer.pubkey(), + payer.pubkey(), &mut args, &COMMIT_NEW_STATE_ACCOUNT_DATA, ); @@ -780,14 +788,19 @@ async fn test_commit_finalize_skips_validator_undelegation_without_auto_accounts #[tokio::test] async fn test_commit_finalize_from_buffer_skips_validator_undelegation_without_auto_accounts_for_backward_compat( ) { - let (banks, _, authority, rent_payer, blockhash) = - setup_undelegate_with_requester_env_config( - UndelegationRequester::None, - false, - false, - true, - ) - .await; + let SetupContext { + banks, + payer, + authority, + blockhash, + .. + } = setup_env(SetupConfig { + with_state_buffer: true, + with_owner_program: true, + with_fee_accounts: true, + ..Default::default() + }) + .await; let state_buffer_pda = Pubkey::find_program_address(&[b"state_buffer"], &authority.pubkey()).0; @@ -804,7 +817,7 @@ async fn test_commit_finalize_from_buffer_skips_validator_undelegation_without_a DELEGATED_PDA_ID, state_buffer_pda, DELEGATED_PDA_OWNER_ID, - rent_payer.pubkey(), + payer.pubkey(), &mut args, ); // Simulate an older validator that sends the pre-auto-undelegation @@ -893,7 +906,7 @@ async fn test_undelegate_with_malformed_optional_request_accounts_rejected() { blockhash, .. } = setup_env(SetupConfig { - requester: UndelegationRequester::Validator, + requester: UndelegationRequester::OwnerProgram, with_commit_accounts: true, with_state_buffer: true, with_owner_program: true, From ccb7d57e37bbb18a1958a25d63730c4b06ac45ad Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Fri, 3 Jul 2026 17:09:34 +0530 Subject: [PATCH 5/5] refactor and add a comment on Backward compatibility --- .../fast/internal/undelegate_internal.rs | 53 +++++++++++-------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/src/processor/fast/internal/undelegate_internal.rs b/src/processor/fast/internal/undelegate_internal.rs index 14c09c51..9a4586ba 100644 --- a/src/processor/fast/internal/undelegate_internal.rs +++ b/src/processor/fast/internal/undelegate_internal.rs @@ -75,19 +75,36 @@ pub(crate) fn process_auto_undelegation_if_requested( system_program: &AccountView, auto_accounts: Option>, ) -> ProgramResult { - if requester == UndelegationRequester::None { - return Ok(()); - } - - let Some(auto_accounts) = auto_accounts else { - if requester == UndelegationRequester::Validator { - log!( - "WARN: validator-requested undelegation skipped; \ - auto-undelegation accounts were not provided" - ); - return Ok(()); + let (request_accounts, auto_accounts) = match requester { + UndelegationRequester::None => return Ok(()), + UndelegationRequester::Validator => { + let Some(auto_accounts) = auto_accounts else { + // Backward compatibility: this is normally an error case + // because Finalize cannot process validator-requested + // undelegation without auto-undelegation accounts. Returning + // Ok here keeps older validators compatible: they do not pass + // these accounts and instead send standalone Undelegate after + // Finalize. + log!("WARN: validator-requested undelegation skipped; auto-undelegation accounts were not provided"); + return Ok(()); + }; + (None, auto_accounts) + } + UndelegationRequester::OwnerProgram => { + let Some(auto_accounts) = auto_accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + // For OwnerProgram requests, see UndelegationRequest::rent_payer + // for the rent-payer invariant. Validator requests do not use the + // request PDA. + ( + Some(( + auto_accounts.undelegation_request_account, + auto_accounts.rent_reimbursement, + )), + auto_accounts, + ) } - return Err(ProgramError::NotEnoughAccountKeys); }; process_undelegation(UndelegationAccounts { @@ -101,17 +118,7 @@ pub(crate) fn process_auto_undelegation_if_requested( fees_vault: auto_accounts.fees_vault, validator_fees_vault, system_program, - // For OwnerProgram requests, see UndelegationRequest::rent_payer - // for the rent-payer invariant. Validator requests do not use the - // request PDA. - request_accounts: if requester == UndelegationRequester::OwnerProgram { - Some(( - auto_accounts.undelegation_request_account, - auto_accounts.rent_reimbursement, - )) - } else { - None - }, + request_accounts, }) }