diff --git a/magicblock-chainlink/src/chainlink/fetch_cloner/mod.rs b/magicblock-chainlink/src/chainlink/fetch_cloner/mod.rs index ee0a0f7a8..da91ffed7 100644 --- a/magicblock-chainlink/src/chainlink/fetch_cloner/mod.rs +++ b/magicblock-chainlink/src/chainlink/fetch_cloner/mod.rs @@ -199,6 +199,12 @@ where if let Some(in_bank) = this.accounts_bank.get_account(&pubkey) { + if in_bank.delegated() && !in_bank.undelegating() { + this.unsubscribe_from_delegated_account(pubkey) + .await; + return; + } + if in_bank.undelegating() { // We expect the account to still be delegated, but with the delegation // program owner @@ -258,17 +264,8 @@ where // The subscription will be turned back on once the committor service schedules // a commit for it that includes undelegation if account.delegated() { - if let Err(err) = this - .remote_account_provider - .unsubscribe(&pubkey) - .await - { - error!( - pubkey = %pubkey, - error = %err, - "Failed to unsubscribe from delegated account" - ); - } + this.unsubscribe_from_delegated_account(pubkey) + .await; } if account.executable() { @@ -320,6 +317,18 @@ where .await; } + async fn unsubscribe_from_delegated_account(&self, pubkey: Pubkey) { + if let Err(err) = + self.remote_account_provider.unsubscribe(&pubkey).await + { + warn!( + pubkey = %pubkey, + error = %err, + "Failed to unsubscribe from delegated account" + ); + } + } + async fn resolve_account_to_clone_from_forwarded_sub_with_unsubscribe( &self, update: ForwardedSubscriptionUpdate, @@ -527,6 +536,9 @@ where )?; if let Some(in_bank_ata) = self.accounts_bank.get_account(&ata_pubkey) { + if in_bank_ata.delegated() && !in_bank_ata.undelegating() { + return None; + } if in_bank_ata.remote_slot() >= projected_ata.remote_slot() { return None; } @@ -1329,17 +1341,24 @@ where if lamports == 0 { return Ok(()); } - if let Some(acc) = self.accounts_bank.get_account(&pubkey) { - if acc.lamports() > 0 { - return Ok(()); - } - } + let remote_slot = + if let Some(acc) = self.accounts_bank.get_account(&pubkey) { + if acc.lamports() > 0 { + return Ok(()); + } + acc.remote_slot() + .max(self.remote_account_provider.chain_slot()) + } else { + self.remote_account_provider.chain_slot() + }; // Build a plain system account with the requested balance - let account = + let mut account = AccountSharedData::new(lamports, 0, &system_program::id()); + account.set_remote_slot(remote_slot); debug!( pubkey = %pubkey, lamports, + remote_slot, "Auto-airdropping account" ); let _sig = self diff --git a/magicblock-chainlink/src/chainlink/fetch_cloner/tests.rs b/magicblock-chainlink/src/chainlink/fetch_cloner/tests.rs index dc3bb125c..8dcb206fb 100644 --- a/magicblock-chainlink/src/chainlink/fetch_cloner/tests.rs +++ b/magicblock-chainlink/src/chainlink/fetch_cloner/tests.rs @@ -21,7 +21,8 @@ use crate::{ cloner_stub::ClonerStub, deleg::{add_delegation_record_for, add_invalid_delegation_record_for}, eatas::{ - create_eata_account, derive_ata, derive_eata, EATA_PROGRAM_ID, + create_ata_account, create_eata_account, derive_ata, derive_eata, + EATA_PROGRAM_ID, }, init_logger, rpc_client_mock::{ChainRpcClientMock, ChainRpcClientMockBuilder}, @@ -1097,6 +1098,117 @@ async fn test_undelegation_requested_subscription_behavior() { assert_subscribed!(remote_account_provider, &[&account_pubkey]); } +#[tokio::test] +async fn test_delegated_authoritative_skip_unsubscribes_subscription() { + init_logger(); + let validator_pubkey = random_pubkey(); + let account_owner = random_pubkey(); + const CURRENT_SLOT: u64 = 100; + + let account_pubkey = random_pubkey(); + let delegated_account = Account { + lamports: 1_000_000, + data: vec![1, 2, 3, 4], + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }; + + let FetcherTestCtx { + remote_account_provider, + accounts_bank, + rpc_client, + fetch_cloner, + subscription_tx, + .. + } = setup( + [(account_pubkey, delegated_account.clone())], + CURRENT_SLOT, + validator_pubkey, + ) + .await; + + add_delegation_record_for( + &rpc_client, + account_pubkey, + validator_pubkey, + account_owner, + ); + + // Clone delegated account into bank (authoritative local delegated state). + fetch_cloner + .fetch_and_clone_accounts( + &[account_pubkey], + None, + None, + AccountFetchOrigin::GetAccount, + None, + ) + .await + .expect("delegated account fetch should succeed"); + assert_cloned_delegated_account!( + accounts_bank, + account_pubkey, + delegated_account.clone(), + CURRENT_SLOT, + account_owner + ); + + // Simulate undelegation-tracking subscription being active. + fetch_cloner + .subscribe_to_account(&account_pubkey) + .await + .expect("failed to subscribe delegated account"); + assert_subscribed!(remote_account_provider, &[&account_pubkey]); + + // Send a newer plain update; delegated authoritative-skip path should still unsubscribe. + use crate::remote_account_provider::{ + RemoteAccount, RemoteAccountUpdateSource, + }; + let chain_update = Account { + lamports: 900_000, + data: vec![9, 9, 9, 9], + owner: account_owner, + executable: false, + rent_epoch: 0, + }; + subscription_tx + .send(ForwardedSubscriptionUpdate { + pubkey: account_pubkey, + account: RemoteAccount::from_fresh_account( + chain_update, + CURRENT_SLOT + 1, + RemoteAccountUpdateSource::Subscription, + ), + }) + .await + .unwrap(); + + const POLL_INTERVAL: std::time::Duration = Duration::from_millis(10); + const TIMEOUT: std::time::Duration = Duration::from_millis(500); + tokio::time::timeout(TIMEOUT, async { + loop { + if !remote_account_provider.is_watching(&account_pubkey) { + break; + } + tokio::time::sleep(POLL_INTERVAL).await; + } + }) + .await + .expect("timed out waiting for delegated account unsubscribe"); + + assert_not_subscribed!(remote_account_provider, &[&account_pubkey]); + + // Ensure we did not overwrite the local delegated account state. + assert_cloned_delegated_account!( + accounts_bank, + account_pubkey, + delegated_account, + CURRENT_SLOT, + account_owner + ); +} + #[tokio::test] async fn test_parallel_fetch_prevention_multiple_accounts() { init_logger(); @@ -1448,6 +1560,76 @@ async fn test_fetch_and_clone_undelegating_account_that_is_closed_on_chain() { ); } +#[tokio::test] +async fn test_auto_airdrop_uses_non_stale_remote_slot_from_bank_account() { + init_logger(); + let validator_pubkey = random_pubkey(); + let payer_pubkey = random_pubkey(); + const CURRENT_SLOT: u64 = 100; + const LOCAL_SLOT: u64 = 250; + const AIRDROP_LAMPORTS: u64 = 1_000_000_000; + + let FetcherTestCtx { + accounts_bank, + fetch_cloner, + .. + } = setup( + std::iter::empty::<(Pubkey, Account)>(), + CURRENT_SLOT, + validator_pubkey, + ) + .await; + + let mut empty_local_account = + AccountSharedData::new(0, 0, &system_program::id()); + empty_local_account.set_remote_slot(LOCAL_SLOT); + accounts_bank.insert(payer_pubkey, empty_local_account); + + fetch_cloner + .airdrop_account_if_empty(payer_pubkey, AIRDROP_LAMPORTS) + .await + .expect("airdrop should succeed"); + + let payer_after = accounts_bank + .get_account(&payer_pubkey) + .expect("payer should exist in bank"); + assert_eq!(payer_after.lamports(), AIRDROP_LAMPORTS); + assert_eq!(payer_after.remote_slot(), LOCAL_SLOT); + assert_eq!(*payer_after.owner(), system_program::id()); +} + +#[tokio::test] +async fn test_auto_airdrop_uses_chain_slot_when_account_not_in_bank() { + init_logger(); + let validator_pubkey = random_pubkey(); + let payer_pubkey = random_pubkey(); + const CURRENT_SLOT: u64 = 100; + const AIRDROP_LAMPORTS: u64 = 1_000_000_000; + + let FetcherTestCtx { + accounts_bank, + fetch_cloner, + .. + } = setup( + std::iter::empty::<(Pubkey, Account)>(), + CURRENT_SLOT, + validator_pubkey, + ) + .await; + + fetch_cloner + .airdrop_account_if_empty(payer_pubkey, AIRDROP_LAMPORTS) + .await + .expect("airdrop should succeed"); + + let payer_after = accounts_bank + .get_account(&payer_pubkey) + .expect("payer should exist in bank"); + assert_eq!(payer_after.lamports(), AIRDROP_LAMPORTS); + assert_eq!(payer_after.remote_slot(), CURRENT_SLOT); + assert_eq!(*payer_after.owner(), system_program::id()); +} + // ----------------- // Allowed Programs Tests // ----------------- @@ -2080,3 +2262,90 @@ async fn test_delegated_eata_subscription_update_clones_raw_eata_and_projects_at assert_eq!(projected_owner, wallet_owner); assert_eq!(projected_amount, AMOUNT); } + +#[tokio::test] +async fn test_delegated_eata_update_does_not_override_delegated_ata_in_bank() { + init_logger(); + let validator_pubkey = random_pubkey(); + let wallet_owner = random_pubkey(); + let mint = random_pubkey(); + const CURRENT_SLOT: u64 = 100; + const CHAIN_EATA_AMOUNT: u64 = 777; + const LOCAL_ATA_AMOUNT: u64 = 999; + + let eata_pubkey = derive_eata(&wallet_owner, &mint); + let ata_pubkey = derive_ata(&wallet_owner, &mint); + let eata_account = + create_eata_account(&wallet_owner, &mint, CHAIN_EATA_AMOUNT, true); + + let FetcherTestCtx { + accounts_bank, + rpc_client, + subscription_tx, + .. + } = setup( + [(eata_pubkey, eata_account.clone())], + CURRENT_SLOT, + validator_pubkey, + ) + .await; + + add_delegation_record_for( + &rpc_client, + eata_pubkey, + validator_pubkey, + EATA_PROGRAM_ID, + ); + + // Simulate local delegated ATA state that was already mutated in the validator. + let mut local_ata = create_ata_account(&wallet_owner, &mint); + local_ata.data[64..72].copy_from_slice(&LOCAL_ATA_AMOUNT.to_le_bytes()); + let mut local_ata_shared = AccountSharedData::from(local_ata); + local_ata_shared.set_remote_slot(CURRENT_SLOT - 1); + local_ata_shared.set_delegated(true); + accounts_bank.insert(ata_pubkey, local_ata_shared); + + use crate::remote_account_provider::{ + RemoteAccount, RemoteAccountUpdateSource, + }; + + // A newer chain update for delegated eATA must not override delegated ATA in bank. + subscription_tx + .send(ForwardedSubscriptionUpdate { + pubkey: eata_pubkey, + account: RemoteAccount::from_fresh_account( + eata_account, + CURRENT_SLOT, + RemoteAccountUpdateSource::Subscription, + ), + }) + .await + .unwrap(); + + const POLL_INTERVAL: std::time::Duration = Duration::from_millis(10); + const TIMEOUT: std::time::Duration = Duration::from_millis(500); + tokio::time::timeout(TIMEOUT, async { + while accounts_bank.get_account(&eata_pubkey).is_none() { + tokio::time::sleep(POLL_INTERVAL).await; + } + }) + .await + .expect("timed out waiting for delegated eATA subscription update"); + + let ata_after = accounts_bank + .get_account(&ata_pubkey) + .expect("ATA should still exist in bank"); + assert!(ata_after.delegated(), "ATA must remain delegated"); + assert_eq!( + ata_after.remote_slot(), + CURRENT_SLOT - 1, + "Delegated ATA should not be overwritten by chain update", + ); + + let ata_data = ata_after.data(); + let ata_amount = u64::from_le_bytes(ata_data[64..72].try_into().unwrap()); + assert_eq!( + ata_amount, LOCAL_ATA_AMOUNT, + "Delegated ATA amount should keep local state", + ); +} diff --git a/magicblock-committor-service/src/tasks/buffer_task.rs b/magicblock-committor-service/src/tasks/buffer_task.rs index 700d7f8e5..5545a5499 100644 --- a/magicblock-committor-service/src/tasks/buffer_task.rs +++ b/magicblock-committor-service/src/tasks/buffer_task.rs @@ -58,6 +58,7 @@ impl BufferTask { match task_type { BufferTaskType::Commit(task) => { let data = task.committed_account.account.data.clone(); + let buffer_account_size = data.len() as u64; let chunks = Chunks::from_data_length(data.len(), MAX_WRITE_CHUNK_SIZE); @@ -66,6 +67,7 @@ impl BufferTask { pubkey: task.committed_account.pubkey, committed_data: data, chunks, + buffer_account_size, }) } @@ -75,6 +77,12 @@ impl BufferTask { &task.committed_account.account.data, ) .to_vec(); + + // Fix for #878: Track buffer_account_size separately from diff + // buffer_account_size = committed account size (for realloc instructions) + // committed_data = diff (for write instructions) + // This ensures realloc instructions are generated when account growth > 10KB + let buffer_account_size = task.committed_account.account.data.len(); let chunks = Chunks::from_data_length(diff.len(), MAX_WRITE_CHUNK_SIZE); @@ -83,6 +91,7 @@ impl BufferTask { pubkey: task.committed_account.pubkey, committed_data: diff, chunks, + buffer_account_size: buffer_account_size as u64, }) } } diff --git a/magicblock-committor-service/src/tasks/mod.rs b/magicblock-committor-service/src/tasks/mod.rs index 18e870059..1fc7346a0 100644 --- a/magicblock-committor-service/src/tasks/mod.rs +++ b/magicblock-committor-service/src/tasks/mod.rs @@ -144,6 +144,11 @@ pub struct PreparationTask { // TODO(edwin): replace with reference once done pub committed_data: Vec, + /// Actual buffer account size for realloc instructions + /// For CommitDiff: committed_account.data.len() + /// For Commit: data.len() + /// This may differ from committed_data.len() (which holds the diff for CommitDiff) + pub buffer_account_size: u64, } impl PreparationTask { @@ -156,7 +161,7 @@ impl PreparationTask { // // https://github.com/near/borsh-rs/blob/f1b75a6b50740bfb6231b7d0b1bd93ea58ca5452/borsh/src/ser/helpers.rs#L59 let chunks_account_size = borsh::object_length(&self.chunks).unwrap() as u64; - let buffer_account_size = self.committed_data.len() as u64; + let buffer_account_size = self.buffer_account_size; let (instruction, _, _) = create_init_ix(CreateInitIxArgs { authority: *authority, @@ -179,12 +184,11 @@ impl PreparationTask { /// Returns realloc instruction required for Buffer preparation #[allow(clippy::let_and_return)] pub fn realloc_instructions(&self, authority: &Pubkey) -> Vec { - let buffer_account_size = self.committed_data.len() as u64; let realloc_instructions = create_realloc_buffer_ixs(CreateReallocBufferIxArgs { authority: *authority, pubkey: self.pubkey, - buffer_account_size, + buffer_account_size: self.buffer_account_size, commit_id: self.commit_id, }); @@ -450,6 +454,132 @@ mod serialization_safety_test { } } + #[test] + fn test_commit_diff_preparation_tracks_committed_account_size() { + setup(); + let base_len = 11_264usize; + let committed_len = 12_288usize; + let owner = Pubkey::new_unique(); + + let base_data = vec![1u8; base_len]; + let mut committed_data = base_data.clone(); + committed_data.extend(vec![2u8; committed_len - base_len]); + + let buffer_task = BufferTask::new_preparation_required( + BufferTaskType::CommitDiff(CommitDiffTask { + commit_id: 900, + allow_undelegation: false, + committed_account: CommittedAccount { + pubkey: Pubkey::new_unique(), + account: Account { + lamports: 4000, + data: committed_data, + owner, + executable: false, + rent_epoch: 0, + }, + remote_slot: Default::default(), + }, + base_account: Account { + lamports: 2000, + data: base_data, + owner, + executable: false, + rent_epoch: 0, + }, + }), + ); + + let PreparationState::Required(preparation_task) = + buffer_task.preparation_state() + else { + panic!("invalid preparation state on creation!"); + }; + + assert_eq!(preparation_task.buffer_account_size, committed_len as u64); + } + + #[test] + fn test_commit_diff_preparation_large_growth_splits_reallocs() { + setup(); + let authority = Pubkey::new_unique(); + let base_len = 8_192usize; + let committed_len = 22_528usize; + let owner = Pubkey::new_unique(); + + assert!( + committed_len - base_len + > magicblock_committor_program::consts::MAX_ACCOUNT_ALLOC_PER_INSTRUCTION_SIZE as usize + ); + + let base_data = vec![1u8; base_len]; + let mut committed_data = base_data.clone(); + committed_data.extend(vec![2u8; committed_len - base_len]); + + let buffer_task = BufferTask::new_preparation_required( + BufferTaskType::CommitDiff(CommitDiffTask { + commit_id: 902, + allow_undelegation: false, + committed_account: CommittedAccount { + pubkey: Pubkey::new_unique(), + account: Account { + lamports: 5000, + data: committed_data, + owner, + executable: false, + rent_epoch: 0, + }, + remote_slot: Default::default(), + }, + base_account: Account { + lamports: 2500, + data: base_data, + owner, + executable: false, + rent_epoch: 0, + }, + }), + ); + + let PreparationState::Required(preparation_task) = + buffer_task.preparation_state() + else { + panic!("invalid preparation state on creation!"); + }; + + assert_eq!(preparation_task.buffer_account_size, committed_len as u64); + assert!(preparation_task.realloc_instructions(&authority).len() > 1); + } + + #[test] + fn test_realloc_instructions_use_buffer_account_size_not_diff_size() { + setup(); + let authority = Pubkey::new_unique(); + let committed_data = vec![9u8; 64]; + let make_preparation_task = + |buffer_account_size: u64| PreparationTask { + commit_id: 901, + pubkey: Pubkey::new_unique(), + chunks: Chunks::from_data_length( + committed_data.len(), + crate::consts::MAX_WRITE_CHUNK_SIZE, + ), + committed_data: committed_data.clone(), + buffer_account_size, + }; + + let small_realloc_instructions = + make_preparation_task(12_288).realloc_instructions(&authority); + let large_realloc_instructions = + make_preparation_task(30_720).realloc_instructions(&authority); + + assert_eq!(small_realloc_instructions.len(), 1); + assert_eq!(large_realloc_instructions.len(), 2); + assert!( + large_realloc_instructions.len() > small_realloc_instructions.len() + ); + } + // Helper function to assert serialization succeeds fn assert_serializable(ix: &Instruction) { bincode::serialize(ix).unwrap_or_else(|e| { diff --git a/magicblock-ledger/src/ledger_truncator.rs b/magicblock-ledger/src/ledger_truncator.rs index 0aa729878..471af1e28 100644 --- a/magicblock-ledger/src/ledger_truncator.rs +++ b/magicblock-ledger/src/ledger_truncator.rs @@ -281,7 +281,9 @@ impl LedgerTrunctationWorker { info!(from_slot, to_slot, "Truncating slot range"); ledger.set_lowest_cleanup_slot(to_slot); - Self::delete_slots(ledger, from_slot, to_slot)?; + if let Err(err) = Self::delete_slots(ledger, from_slot, to_slot) { + error!(error = ?err, "Delete failed"); + } // Flush memtables with tombstones prior to compaction if let Err(err) = ledger.flush() { diff --git a/programs/magicblock/src/errors.rs b/programs/magicblock/src/errors.rs index 367c1739f..60e9156cd 100644 --- a/programs/magicblock/src/errors.rs +++ b/programs/magicblock/src/errors.rs @@ -47,4 +47,12 @@ pub enum MagicBlockProgramError { #[error("Encountered an error when persisting account modification data.")] FailedToPersistAccountModData, + + #[error("The account is delegated and not currently undelegating.")] + AccountIsDelegatedAndNotUndelegating, + + #[error( + "Remote slot updates cannot be older than the current remote slot." + )] + IncomingRemoteSlotIsOlderThanCurrentRemoteSlot, } diff --git a/programs/magicblock/src/mutate_accounts/process_mutate_accounts.rs b/programs/magicblock/src/mutate_accounts/process_mutate_accounts.rs index 8deb32296..475a77f9e 100644 --- a/programs/magicblock/src/mutate_accounts/process_mutate_accounts.rs +++ b/programs/magicblock/src/mutate_accounts/process_mutate_accounts.rs @@ -131,6 +131,39 @@ pub(crate) fn process_mutate_accounts( ic_msg!(invoke_context, "MutateAccounts: {}", msg); } + let (is_delegated, is_undelegating) = { + let account_ref = account.borrow(); + (account_ref.delegated(), account_ref.undelegating()) + }; + if is_delegated && !is_undelegating { + ic_msg!( + invoke_context, + "MutateAccounts: account {} is delegated and not undelegating; mutation is forbidden", + account_key + ); + return Err( + MagicBlockProgramError::AccountIsDelegatedAndNotUndelegating + .into(), + ); + } + + let current_remote_slot = account.borrow().remote_slot(); + if let Some(incoming_remote_slot) = modification.remote_slot { + if incoming_remote_slot < current_remote_slot { + ic_msg!( + invoke_context, + "MutateAccounts: account {} incoming remote_slot {} is older than current remote_slot {}; mutation is forbidden", + account_key, + incoming_remote_slot, + current_remote_slot + ); + return Err( + MagicBlockProgramError::IncomingRemoteSlotIsOlderThanCurrentRemoteSlot + .into(), + ); + } + } + // While an account is undelegating and the delegation is not completed, // we will never clone/mutate it. Thus we can safely untoggle this flag // here. @@ -486,6 +519,112 @@ mod tests { ); } + #[test] + fn test_mutate_fails_for_delegated_non_undelegating_account() { + init_logger!(); + + let mod_key = Pubkey::new_unique(); + let mut delegated_account = AccountSharedData::new(100, 0, &mod_key); + delegated_account.set_delegated(true); + + let mut account_data = { + let mut map = HashMap::new(); + map.insert(mod_key, delegated_account); + map + }; + ensure_started_validator(&mut account_data); + + let ix = InstructionUtils::modify_accounts_instruction( + vec![AccountModification { + pubkey: mod_key, + lamports: Some(200), + ..AccountModification::default() + }], + None, + ); + let transaction_accounts = ix + .accounts + .iter() + .flat_map(|acc| { + account_data + .remove(&acc.pubkey) + .map(|shared_data| (acc.pubkey, shared_data)) + }) + .collect(); + + let _accounts = process_instruction( + ix.data.as_slice(), + transaction_accounts, + ix.accounts, + Err(MagicBlockProgramError::AccountIsDelegatedAndNotUndelegating + .into()), + ); + } + + #[test] + fn test_mutate_succeeds_for_delegated_undelegating_account() { + init_logger!(); + + let mod_key = Pubkey::new_unique(); + let mut undelegating_account = AccountSharedData::new(100, 0, &mod_key); + undelegating_account.set_delegated(true); + undelegating_account.set_undelegating(true); + + let mut account_data = { + let mut map = HashMap::new(); + map.insert(mod_key, undelegating_account); + map + }; + ensure_started_validator(&mut account_data); + + let ix = InstructionUtils::modify_accounts_instruction( + vec![AccountModification { + pubkey: mod_key, + lamports: Some(200), + ..AccountModification::default() + }], + None, + ); + let transaction_accounts = ix + .accounts + .iter() + .flat_map(|acc| { + account_data + .remove(&acc.pubkey) + .map(|shared_data| (acc.pubkey, shared_data)) + }) + .collect(); + + let mut accounts = process_instruction( + ix.data.as_slice(), + transaction_accounts, + ix.accounts, + Ok(()), + ); + + // authority account + let _account_authority: AccountSharedData = + accounts.drain(0..1).next().unwrap(); + let modified_account: AccountSharedData = + accounts.drain(0..1).next().unwrap(); + + assert!(modified_account.delegated()); + assert!(!modified_account.undelegating()); + assert_matches!( + modified_account.into(), + Account { + lamports: 200, + owner, + executable: false, + data, + rent_epoch: u64::MAX, + } => { + assert_eq!(owner, mod_key); + assert!(data.is_empty()); + } + ); + } + #[test] fn test_mod_different_properties_of_four_accounts() { init_logger!(); @@ -675,4 +814,91 @@ mod tests { let modified_account = accounts.drain(0..1).next().unwrap(); assert_eq!(modified_account.remote_slot(), remote_slot); } + + #[test] + fn test_mod_remote_slot_rejects_stale_update() { + init_logger!(); + + let mod_key = Pubkey::new_unique(); + let mut account = AccountSharedData::new(100, 0, &mod_key); + account.set_remote_slot(100); + let mut account_data = { + let mut map = HashMap::new(); + map.insert(mod_key, account); + map + }; + ensure_started_validator(&mut account_data); + + let ix = InstructionUtils::modify_accounts_instruction( + vec![AccountModification { + pubkey: mod_key, + lamports: Some(200), + remote_slot: Some(99), + ..Default::default() + }], + None, + ); + let transaction_accounts = ix + .accounts + .iter() + .flat_map(|acc| { + account_data + .remove(&acc.pubkey) + .map(|shared_data| (acc.pubkey, shared_data)) + }) + .collect(); + + let _accounts = process_instruction( + ix.data.as_slice(), + transaction_accounts, + ix.accounts, + Err(MagicBlockProgramError::IncomingRemoteSlotIsOlderThanCurrentRemoteSlot.into()), + ); + } + + #[test] + fn test_mod_remote_slot_allows_equal_update() { + init_logger!(); + + let mod_key = Pubkey::new_unique(); + let mut account = AccountSharedData::new(100, 0, &mod_key); + account.set_remote_slot(100); + let mut account_data = { + let mut map = HashMap::new(); + map.insert(mod_key, account); + map + }; + ensure_started_validator(&mut account_data); + + let ix = InstructionUtils::modify_accounts_instruction( + vec![AccountModification { + pubkey: mod_key, + lamports: Some(200), + remote_slot: Some(100), + ..Default::default() + }], + None, + ); + let transaction_accounts = ix + .accounts + .iter() + .flat_map(|acc| { + account_data + .remove(&acc.pubkey) + .map(|shared_data| (acc.pubkey, shared_data)) + }) + .collect(); + + let mut accounts = process_instruction( + ix.data.as_slice(), + transaction_accounts, + ix.accounts, + Ok(()), + ); + + let _account_authority = accounts.drain(0..1).next().unwrap(); + let modified_account = accounts.drain(0..1).next().unwrap(); + assert_eq!(modified_account.lamports(), 200); + assert_eq!(modified_account.remote_slot(), 100); + } } diff --git a/test-integration/test-chainlink/src/ixtest_context.rs b/test-integration/test-chainlink/src/ixtest_context.rs index 914b4479a..9509bc0dc 100644 --- a/test-integration/test-chainlink/src/ixtest_context.rs +++ b/test-integration/test-chainlink/src/ixtest_context.rs @@ -291,6 +291,10 @@ impl IxtestContext { ) -> &Self { debug!("Undelegating counter account {}", counter_auth.pubkey()); let counter_pda = self.counter_pda(&counter_auth.pubkey()); + // Mirror validator behavior when scheduling commit+undelegate: + // mark account as undelegating in the local bank before we start + // tracking undelegation updates from chain. + self.bank.force_undelegation(&counter_pda); // The committor service will call this in order to have // chainlink subscribe to account updates of the counter account self.chainlink