diff --git a/dlp-api/src/state/delegation_metadata.rs b/dlp-api/src/state/delegation_metadata.rs index 79d2f077..ba7540c5 100644 --- a/dlp-api/src/state/delegation_metadata.rs +++ b/dlp-api/src/state/delegation_metadata.rs @@ -26,6 +26,10 @@ 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. + // TODO (snawaz): edit the doc once all validators upgrade. OwnerProgram, } diff --git a/src/processor/fast/internal/undelegate_internal.rs b/src/processor/fast/internal/undelegate_internal.rs index dd4e9c28..9a4586ba 100644 --- a/src/processor/fast/internal/undelegate_internal.rs +++ b/src/processor/fast/internal/undelegate_internal.rs @@ -75,12 +75,38 @@ pub(crate) fn process_auto_undelegation_if_requested( system_program: &AccountView, auto_accounts: Option>, ) -> ProgramResult { - if requester != UndelegationRequester::OwnerProgram { - 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, + ) + } + }; - let auto_accounts = - auto_accounts.ok_or(ProgramError::NotEnoughAccountKeys)?; process_undelegation(UndelegationAccounts { validator, delegated_account, @@ -92,11 +118,7 @@ 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, - )), + request_accounts, }) } 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_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_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 1609ecb6..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, @@ -350,12 +348,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 +357,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 +426,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 +477,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 +531,317 @@ 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; +} + +// 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 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, + 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 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, + 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, + 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 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; + 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, + 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( &DELEGATED_PDA_ID, @@ -593,6 +862,41 @@ async fn test_commit_finalize_from_buffer_auto_undelegates_owner_program_request 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 { @@ -602,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, 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 {