From f6d2a8fc3f108aa94dd9bbad20ecfbc15b8ff48c Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Sun, 2 Nov 2025 18:39:41 +0400 Subject: [PATCH 01/17] feat: Implement thread local storage based execution stash This stash can be used for communication between magic program and the rest of the validator, as a more performant and easier to use alternative to Context accounts. --- Cargo.lock | 1 + magicblock-api/src/magic_validator.rs | 1 + magicblock-core/src/link.rs | 11 +++++-- magicblock-core/src/link/transactions.rs | 8 ++++- magicblock-magic-program-api/src/args.rs | 28 ++++++++++++++++ magicblock-magic-program-api/src/lib.rs | 1 + magicblock-magic-program-api/src/tls.rs | 21 ++++++++++++ magicblock-processor/Cargo.toml | 1 + magicblock-processor/src/executor/mod.rs | 6 +++- .../src/executor/processing.rs | 23 +++++++++++-- magicblock-processor/src/scheduler/state.rs | 6 +++- magicblock-task-scheduler/src/service.rs | 4 +-- programs/magicblock/src/lib.rs | 4 +-- .../src/schedule_task/process_cancel_task.rs | 4 +-- .../schedule_task/process_schedule_task.rs | 7 ++-- programs/magicblock/src/task_context.rs | 33 +++---------------- test-kit/src/lib.rs | 1 + 17 files changed, 114 insertions(+), 46 deletions(-) create mode 100644 magicblock-magic-program-api/src/tls.rs diff --git a/Cargo.lock b/Cargo.lock index 5ff4a459f..1700adacd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3861,6 +3861,7 @@ dependencies = [ "magicblock-accounts-db", "magicblock-core", "magicblock-ledger", + "magicblock-magic-program-api", "magicblock-metrics", "magicblock-program", "parking_lot 0.12.4", diff --git a/magicblock-api/src/magic_validator.rs b/magicblock-api/src/magic_validator.rs index 749489458..0cc2811a6 100644 --- a/magicblock-api/src/magic_validator.rs +++ b/magicblock-api/src/magic_validator.rs @@ -284,6 +284,7 @@ impl MagicValidator { txn_to_process_rx: validator_channels.transaction_to_process, account_update_tx: validator_channels.account_update, environment: build_svm_env(&accountsdb, latest_block.blockhash, 0), + tasks_tx: validator_channels.tasks_service, }; txn_scheduler_state .load_upgradeable_programs(&programs_to_load(&config.programs)) diff --git a/magicblock-core/src/link.rs b/magicblock-core/src/link.rs index 83be2f3f1..c429b6505 100644 --- a/magicblock-core/src/link.rs +++ b/magicblock-core/src/link.rs @@ -2,8 +2,8 @@ use accounts::{AccountUpdateRx, AccountUpdateTx}; use blocks::{BlockUpdateRx, BlockUpdateTx}; use tokio::sync::mpsc; use transactions::{ - TransactionSchedulerHandle, TransactionStatusRx, TransactionStatusTx, - TransactionToProcessRx, + ScheduledTasksRx, ScheduledTasksTx, TransactionSchedulerHandle, + TransactionStatusRx, TransactionStatusTx, TransactionToProcessRx, }; pub mod accounts; @@ -27,6 +27,8 @@ pub struct DispatchEndpoints { pub account_update: AccountUpdateRx, /// Receives notifications when a new block is produced. pub block_update: BlockUpdateRx, + /// Receives scheduled (crank) tasks from transactions executor. + pub tasks_service: ScheduledTasksRx, } /// A collection of channel endpoints for the **validator's internal core**. @@ -43,6 +45,8 @@ pub struct ValidatorChannelEndpoints { pub account_update: AccountUpdateTx, /// Sends notifications when a new block is produced to the pool of EventProcessor workers. pub block_update: BlockUpdateTx, + /// Sends scheduled (crank) tasks to tasks service from transactions executor. + pub tasks_service: ScheduledTasksTx, } /// Creates and connects the full set of communication channels between the dispatch @@ -58,6 +62,7 @@ pub fn link() -> (DispatchEndpoints, ValidatorChannelEndpoints) { let (transaction_status_tx, transaction_status_rx) = flume::unbounded(); let (account_update_tx, account_update_rx) = flume::unbounded(); let (block_update_tx, block_update_rx) = flume::unbounded(); + let (tasks_tx, tasks_rx) = mpsc::unbounded_channel(); // Bounded channels for command queues where applying backpressure is important. let (txn_to_process_tx, txn_to_process_rx) = mpsc::channel(LINK_CAPACITY); @@ -68,6 +73,7 @@ pub fn link() -> (DispatchEndpoints, ValidatorChannelEndpoints) { transaction_status: transaction_status_rx, account_update: account_update_rx, block_update: block_update_rx, + tasks_service: tasks_rx, }; // Bundle the corresponding channel ends for the validator's internal core. @@ -76,6 +82,7 @@ pub fn link() -> (DispatchEndpoints, ValidatorChannelEndpoints) { transaction_status: transaction_status_tx, account_update: account_update_tx, block_update: block_update_tx, + tasks_service: tasks_tx, }; (dispatch, validator) diff --git a/magicblock-core/src/link/transactions.rs b/magicblock-core/src/link/transactions.rs index 779c871a7..ed4a3d271 100644 --- a/magicblock-core/src/link/transactions.rs +++ b/magicblock-core/src/link/transactions.rs @@ -1,4 +1,5 @@ use flume::{Receiver as MpmcReceiver, Sender as MpmcSender}; +use magicblock_magic_program_api::args::TaskRequest; use solana_program::message::{ inner_instruction::InnerInstructionsList, SimpleAddressLoader, }; @@ -11,7 +12,7 @@ use solana_transaction::{ use solana_transaction_context::TransactionReturnData; use solana_transaction_error::TransactionError; use tokio::sync::{ - mpsc::{Receiver, Sender}, + mpsc::{Receiver, Sender, UnboundedReceiver, UnboundedSender}, oneshot, }; @@ -30,6 +31,11 @@ pub type TransactionToProcessRx = Receiver; /// The sender end of the channel used to send new transactions to the scheduler for processing. type TransactionToProcessTx = Sender; +/// The receiver end of the channel used to send scheduled tasks (cranking) +pub type ScheduledTasksRx = UnboundedReceiver; +/// The sender end of the channel used to send scheduled tasks (cranking) +pub type ScheduledTasksTx = UnboundedSender; + /// A cloneable handle that provides a high-level API for /// submitting transactions to the processing pipeline. /// diff --git a/magicblock-magic-program-api/src/args.rs b/magicblock-magic-program-api/src/args.rs index 322d50675..cb339e153 100644 --- a/magicblock-magic-program-api/src/args.rs +++ b/magicblock-magic-program-api/src/args.rs @@ -109,3 +109,31 @@ pub struct ScheduleTaskArgs { pub iterations: u64, pub instructions: Vec, } + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub enum TaskRequest { + Schedule(ScheduleTaskRequest), + Cancel(CancelTaskRequest), +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct ScheduleTaskRequest { + /// Unique identifier for this task + pub id: u64, + /// Unsigned instructions to execute when triggered + pub instructions: Vec, + /// Authority that can modify or cancel this task + pub authority: Pubkey, + /// How frequently the task should be executed, in milliseconds + pub execution_interval_millis: u64, + /// Number of times this task will be executed + pub iterations: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct CancelTaskRequest { + /// Unique identifier for the task to cancel + pub task_id: u64, + /// Authority that can cancel this task + pub authority: Pubkey, +} diff --git a/magicblock-magic-program-api/src/lib.rs b/magicblock-magic-program-api/src/lib.rs index b22b7e975..9aa584220 100644 --- a/magicblock-magic-program-api/src/lib.rs +++ b/magicblock-magic-program-api/src/lib.rs @@ -1,5 +1,6 @@ pub mod args; pub mod instruction; +pub mod tls; pub use solana_program::{declare_id, pubkey, pubkey::Pubkey}; diff --git a/magicblock-magic-program-api/src/tls.rs b/magicblock-magic-program-api/src/tls.rs new file mode 100644 index 000000000..8f2c6ba70 --- /dev/null +++ b/magicblock-magic-program-api/src/tls.rs @@ -0,0 +1,21 @@ +use std::{cell::RefCell, collections::VecDeque}; + +use crate::args::TaskRequest; + +#[derive(Default, Debug)] +pub struct ExecutionTlsStash { + pub tasks: VecDeque, + // TODO(bmuddha/taco-paco): intents should go in here + pub intents: VecDeque<()>, +} + +thread_local! { + pub static EXECUTION_TLS_STASH: RefCell = RefCell::default(); +} + +impl ExecutionTlsStash { + pub fn clear(&mut self) { + self.tasks.clear(); + self.intents.clear(); + } +} diff --git a/magicblock-processor/Cargo.toml b/magicblock-processor/Cargo.toml index 8aa007057..3577ed1f4 100644 --- a/magicblock-processor/Cargo.toml +++ b/magicblock-processor/Cargo.toml @@ -18,6 +18,7 @@ magicblock-core = { workspace = true } magicblock-ledger = { workspace = true } magicblock-metrics = { workspace = true } magicblock-program = { workspace = true } +magicblock-magic-program-api = { workspace = true } solana-account = { workspace = true } solana-bpf-loader-program = { workspace = true } diff --git a/magicblock-processor/src/executor/mod.rs b/magicblock-processor/src/executor/mod.rs index 08eb55c90..3aa47ef1b 100644 --- a/magicblock-processor/src/executor/mod.rs +++ b/magicblock-processor/src/executor/mod.rs @@ -5,7 +5,8 @@ use magicblock_accounts_db::{AccountsDb, StWLock}; use magicblock_core::link::{ accounts::AccountUpdateTx, transactions::{ - TransactionProcessingMode, TransactionStatusTx, TransactionToProcessRx, + ScheduledTasksTx, TransactionProcessingMode, TransactionStatusTx, + TransactionToProcessRx, }, }; use magicblock_ledger::{LatestBlock, LatestBlockInner, Ledger}; @@ -49,6 +50,8 @@ pub(super) struct TransactionExecutor { transaction_tx: TransactionStatusTx, /// A channel to send out account state updates after processing. accounts_tx: AccountUpdateTx, + /// A channel to send scheduled (crank) tasks created by transactions. + tasks_tx: ScheduledTasksTx, /// A back-channel to notify the `TransactionScheduler` that this worker is ready for more work. ready_tx: Sender, /// A read lock held during a slot's processing to synchronize with critical global @@ -99,6 +102,7 @@ impl TransactionExecutor { ready_tx, accounts_tx: state.account_update_tx.clone(), transaction_tx: state.transaction_status_tx.clone(), + tasks_tx: state.tasks_tx.clone(), }; this.processor.fill_missing_sysvar_cache_entries(&this); diff --git a/magicblock-processor/src/executor/processing.rs b/magicblock-processor/src/executor/processing.rs index 6bbd88098..ef4b78a37 100644 --- a/magicblock-processor/src/executor/processing.rs +++ b/magicblock-processor/src/executor/processing.rs @@ -1,4 +1,4 @@ -use log::error; +use log::{error, warn}; use magicblock_core::link::{ accounts::{AccountWithSlot, LockedAccount}, transactions::{ @@ -7,6 +7,7 @@ use magicblock_core::link::{ }, }; use magicblock_metrics::metrics::FAILED_TRANSACTIONS_COUNT; +use magicblock_program::tls::EXECUTION_TLS_STASH; use solana_pubkey::Pubkey; use solana_svm::{ account_loader::{AccountsBalances, CheckedTransactionDetails}, @@ -76,13 +77,28 @@ impl super::TransactionExecutor { // Otherwise commit the account state changes self.commit_accounts(feepayer, &processed, is_replay); - // And commit transaction to the ledger + // Commit transaction to the ledger and schedule tasks and intents (if any) if !is_replay { self.commit_transaction(txn, processed, balances); + // If the transaction succeeded, check for potential tasks/intents + // that may have been scheduled during the transaction execution + if result.is_ok() { + EXECUTION_TLS_STASH.with(|stash| { + for task in stash.borrow_mut().tasks.drain(..) { + // This is a best effort send, if the tasks service has terminated + // for some reason, logging is the best we can do at this point + let _ = self.tasks_tx.send(task).inspect_err(|_| + warn!("Scheduled tasks service has hung up and longer running") + ); + } + }); + } } result }); + // Make sure that no matter what happened to the transaction we clear the stash + EXECUTION_TLS_STASH.with(|s| s.borrow_mut().clear()); // Send the final result back to the caller if they are waiting. tx.map(|tx| tx.send(result)); @@ -128,6 +144,9 @@ impl super::TransactionExecutor { inner_instructions: None, }, }; + // Make sure that we clear the stash, so that simulations + // don't interfere with actual transaction executions + EXECUTION_TLS_STASH.with(|s| s.borrow_mut().clear()); let _ = tx.send(result); } diff --git a/magicblock-processor/src/scheduler/state.rs b/magicblock-processor/src/scheduler/state.rs index 531ac1ca5..bed813087 100644 --- a/magicblock-processor/src/scheduler/state.rs +++ b/magicblock-processor/src/scheduler/state.rs @@ -3,7 +3,9 @@ use std::sync::{Arc, OnceLock, RwLock}; use magicblock_accounts_db::AccountsDb; use magicblock_core::link::{ accounts::AccountUpdateTx, - transactions::{TransactionStatusTx, TransactionToProcessRx}, + transactions::{ + ScheduledTasksTx, TransactionStatusTx, TransactionToProcessRx, + }, }; use magicblock_ledger::Ledger; use solana_account::AccountSharedData; @@ -40,6 +42,8 @@ pub struct TransactionSchedulerState { pub account_update_tx: AccountUpdateTx, /// The channel for sending final transaction statuses to downstream consumers. pub transaction_status_tx: TransactionStatusTx, + /// A channel to send scheduled (crank) tasks created by transactions. + pub tasks_tx: ScheduledTasksTx, } impl TransactionSchedulerState { diff --git a/magicblock-task-scheduler/src/service.rs b/magicblock-task-scheduler/src/service.rs index f479e4409..76fb3a9c1 100644 --- a/magicblock-task-scheduler/src/service.rs +++ b/magicblock-task-scheduler/src/service.rs @@ -15,10 +15,10 @@ use magicblock_core::{ }; use magicblock_ledger::LatestBlock; use magicblock_program::{ + args::{CancelTaskRequest, ScheduleTaskRequest, TaskRequest}, instruction_utils::InstructionUtils, validator::{validator_authority, validator_authority_id}, - CancelTaskRequest, CrankTask, ScheduleTaskRequest, TaskContext, - TaskRequest, TASK_CONTEXT_PUBKEY, + CrankTask, TaskContext, TASK_CONTEXT_PUBKEY, }; use solana_sdk::{ account::ReadableAccount, instruction::Instruction, message::Message, diff --git a/programs/magicblock/src/lib.rs b/programs/magicblock/src/lib.rs index 2b40ffde8..bedcc414e 100644 --- a/programs/magicblock/src/lib.rs +++ b/programs/magicblock/src/lib.rs @@ -7,9 +7,7 @@ mod toggle_executable_check; pub use magic_context::MagicContext; pub mod magic_scheduled_base_intent; pub mod task_context; -pub use task_context::{ - CancelTaskRequest, CrankTask, ScheduleTaskRequest, TaskContext, TaskRequest, -}; +pub use task_context::{CrankTask, TaskContext}; pub mod magicblock_processor; pub mod test_utils; mod utils; diff --git a/programs/magicblock/src/schedule_task/process_cancel_task.rs b/programs/magicblock/src/schedule_task/process_cancel_task.rs index 3da2df793..0a3a5cc34 100644 --- a/programs/magicblock/src/schedule_task/process_cancel_task.rs +++ b/programs/magicblock/src/schedule_task/process_cancel_task.rs @@ -1,16 +1,16 @@ use std::collections::HashSet; +use magicblock_magic_program_api::args::{CancelTaskRequest, TaskRequest}; use solana_log_collector::ic_msg; use solana_program_runtime::invoke_context::InvokeContext; use solana_sdk::{instruction::InstructionError, pubkey::Pubkey}; use crate::{ schedule_task::utils::check_task_context_id, - task_context::{CancelTaskRequest, TaskContext}, + task_context::TaskContext, utils::accounts::{ get_instruction_account_with_idx, get_instruction_pubkey_with_idx, }, - TaskRequest, }; pub(crate) fn process_cancel_task( diff --git a/programs/magicblock/src/schedule_task/process_schedule_task.rs b/programs/magicblock/src/schedule_task/process_schedule_task.rs index a40935b20..febfc251b 100644 --- a/programs/magicblock/src/schedule_task/process_schedule_task.rs +++ b/programs/magicblock/src/schedule_task/process_schedule_task.rs @@ -1,18 +1,19 @@ use std::collections::HashSet; -use magicblock_magic_program_api::args::ScheduleTaskArgs; +use magicblock_magic_program_api::args::{ + ScheduleTaskArgs, ScheduleTaskRequest, TaskRequest, +}; use solana_log_collector::ic_msg; use solana_program_runtime::invoke_context::InvokeContext; use solana_sdk::{instruction::InstructionError, pubkey::Pubkey}; use crate::{ schedule_task::utils::check_task_context_id, - task_context::{ScheduleTaskRequest, TaskContext, MIN_EXECUTION_INTERVAL}, + task_context::{TaskContext, MIN_EXECUTION_INTERVAL}, utils::accounts::{ get_instruction_account_with_idx, get_instruction_pubkey_with_idx, }, validator::validator_authority_id, - TaskRequest, }; pub(crate) fn process_schedule_task( diff --git a/programs/magicblock/src/task_context.rs b/programs/magicblock/src/task_context.rs index fec6a5448..6aa4b0b01 100644 --- a/programs/magicblock/src/task_context.rs +++ b/programs/magicblock/src/task_context.rs @@ -1,6 +1,9 @@ use std::cell::RefCell; -use magicblock_magic_program_api::TASK_CONTEXT_SIZE; +use magicblock_magic_program_api::{ + args::{ScheduleTaskRequest, TaskRequest}, + TASK_CONTEXT_SIZE, +}; use serde::{Deserialize, Serialize}; use solana_sdk::{ account::{AccountSharedData, ReadableAccount}, @@ -10,34 +13,6 @@ use solana_sdk::{ pub const MIN_EXECUTION_INTERVAL: u64 = 10; -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub enum TaskRequest { - Schedule(ScheduleTaskRequest), - Cancel(CancelTaskRequest), -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct ScheduleTaskRequest { - /// Unique identifier for this task - pub id: u64, - /// Unsigned instructions to execute when triggered - pub instructions: Vec, - /// Authority that can modify or cancel this task - pub authority: Pubkey, - /// How frequently the task should be executed, in milliseconds - pub execution_interval_millis: u64, - /// Number of times this task will be executed - pub iterations: u64, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct CancelTaskRequest { - /// Unique identifier for the task to cancel - pub task_id: u64, - /// Authority that can cancel this task - pub authority: Pubkey, -} - #[derive(Debug, Default, Serialize, Deserialize)] pub struct TaskContext { /// List of requests diff --git a/test-kit/src/lib.rs b/test-kit/src/lib.rs index a69b204d8..555542ae9 100644 --- a/test-kit/src/lib.rs +++ b/test-kit/src/lib.rs @@ -121,6 +121,7 @@ impl ExecutionTestEnv { account_update_tx: validator_channels.account_update, transaction_status_tx: validator_channels.transaction_status, txn_to_process_rx: validator_channels.transaction_to_process, + tasks_tx: validator_channels.tasks_service, environment, }; From 6bc24906913f43c63827daf45c94018588c6388e Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Tue, 4 Nov 2025 18:51:09 +0100 Subject: [PATCH 02/17] feat: use scheduled task channel --- magicblock-api/src/magic_validator.rs | 55 ++++-- magicblock-config/src/lib.rs | 20 +- magicblock-config/src/task_scheduler.rs | 27 +-- .../tests/fixtures/11_everything-defined.toml | 1 - magicblock-config/tests/parse_config.rs | 5 +- magicblock-config/tests/read_config.rs | 6 +- magicblock-magic-program-api/src/args.rs | 9 + magicblock-task-scheduler/src/service.rs | 187 +++++++----------- .../configs/schedule-task.ephem.toml | 1 - .../test-task-scheduler/src/lib.rs | 5 +- 10 files changed, 135 insertions(+), 181 deletions(-) diff --git a/magicblock-api/src/magic_validator.rs b/magicblock-api/src/magic_validator.rs index 0cc2811a6..7a9882c13 100644 --- a/magicblock-api/src/magic_validator.rs +++ b/magicblock-api/src/magic_validator.rs @@ -139,7 +139,7 @@ pub struct MagicValidator { block_udpate_tx: BlockUpdateTx, _metrics: Option<(MetricsService, tokio::task::JoinHandle<()>)>, claim_fees_task: ClaimFeesTask, - task_scheduler_handle: Option>, + task_scheduler: Option, } impl MagicValidator { @@ -323,6 +323,23 @@ impl MagicValidator { .await?; let rpc_handle = tokio::spawn(rpc.run()); + let task_scheduler_db_path = + SchedulerDatabase::path(ledger.ledger_path().parent().expect( + "ledger_path didn't have a parent, should never happen", + )); + debug!( + "Task scheduler persists to: {}", + task_scheduler_db_path.display() + ); + let task_scheduler = TaskSchedulerService::new( + &task_scheduler_db_path, + &config.task_scheduler, + dispatch.transaction_scheduler.clone(), + dispatch.tasks_service, + ledger.latest_block().clone(), + token.clone(), + )?; + Ok(Self { accountsdb, config, @@ -341,7 +358,7 @@ impl MagicValidator { identity: validator_pubkey, transaction_scheduler: dispatch.transaction_scheduler, block_udpate_tx: validator_channels.block_update, - task_scheduler_handle: None, + task_scheduler: Some(task_scheduler), }) } @@ -654,30 +671,26 @@ impl MagicValidator { self.ledger_truncator.start(); - let task_scheduler_db_path = - SchedulerDatabase::path(self.ledger.ledger_path().parent().expect( - "ledger_path didn't have a parent, should never happen", - )); - debug!( - "Task scheduler persists to: {}", - task_scheduler_db_path.display() - ); - let task_scheduler_handle = TaskSchedulerService::start( - &task_scheduler_db_path, - &self.config.task_scheduler, - self.accountsdb.clone(), - self.transaction_scheduler.clone(), - self.ledger.latest_block().clone(), - self.token.clone(), - )?; // TODO: we should shutdown gracefully. // This is discussed in this comment: // https://github.com/magicblock-labs/magicblock-validator/pull/493#discussion_r2324560798 // However there is no proper solution for this right now. // An issue to create a shutdown system is open here: // https://github.com/magicblock-labs/magicblock-validator/issues/524 - self.task_scheduler_handle = Some(tokio::spawn(async move { - match task_scheduler_handle.await { + let task_scheduler = self + .task_scheduler + .take() + .expect("task_scheduler should be initialized"); + tokio::spawn(async move { + let join_handle = match task_scheduler.start() { + Ok(join_handle) => join_handle, + Err(err) => { + error!("Failed to start task scheduler: {:?}", err); + error!("Exiting process..."); + std::process::exit(1); + } + }; + match join_handle.await { Ok(Ok(())) => {} Ok(Err(err)) => { error!("An error occurred while running the task scheduler: {:?}", err); @@ -690,7 +703,7 @@ impl MagicValidator { std::process::exit(1); } } - })); + }); validator::finished_starting_up(); Ok(()) diff --git a/magicblock-config/src/lib.rs b/magicblock-config/src/lib.rs index 921da754b..6008040cf 100644 --- a/magicblock-config/src/lib.rs +++ b/magicblock-config/src/lib.rs @@ -268,10 +268,7 @@ mod tests { port: 9090, }, }, - task_scheduler: TaskSchedulerConfig { - reset: true, - millis_per_tick: 1000, - }, + task_scheduler: TaskSchedulerConfig { reset: true }, }; let original_config = config.clone(); let other = EphemeralConfig::default(); @@ -356,10 +353,7 @@ mod tests { port: 9090, }, }, - task_scheduler: TaskSchedulerConfig { - reset: true, - millis_per_tick: 1000, - }, + task_scheduler: TaskSchedulerConfig { reset: true }, }; config.merge(other.clone()); @@ -441,10 +435,7 @@ mod tests { port: 9090, }, }, - task_scheduler: TaskSchedulerConfig { - reset: true, - millis_per_tick: 2000, - }, + task_scheduler: TaskSchedulerConfig { reset: true }, }; let original_config = config.clone(); let other = EphemeralConfig { @@ -519,10 +510,7 @@ mod tests { port: 9090, }, }, - task_scheduler: TaskSchedulerConfig { - reset: true, - millis_per_tick: 1000, - }, + task_scheduler: TaskSchedulerConfig { reset: true }, }; config.merge(other); diff --git a/magicblock-config/src/task_scheduler.rs b/magicblock-config/src/task_scheduler.rs index 478d3f290..078e23286 100644 --- a/magicblock-config/src/task_scheduler.rs +++ b/magicblock-config/src/task_scheduler.rs @@ -5,7 +5,15 @@ use serde::{Deserialize, Serialize}; #[clap_prefix("task-scheduler")] #[clap_from_serde] #[derive( - Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Args, Mergeable, + Debug, + Default, + Clone, + Serialize, + Deserialize, + PartialEq, + Eq, + Args, + Mergeable, )] #[serde(deny_unknown_fields, rename_all = "kebab-case")] pub struct TaskSchedulerConfig { @@ -13,21 +21,4 @@ pub struct TaskSchedulerConfig { #[derive_env_var] #[serde(default)] pub reset: bool, - /// Determines how frequently the task scheduler will check for executable tasks. - #[derive_env_var] - #[serde(default = "default_millis_per_tick")] - pub millis_per_tick: u64, -} - -impl Default for TaskSchedulerConfig { - fn default() -> Self { - Self { - reset: bool::default(), - millis_per_tick: default_millis_per_tick(), - } - } -} - -fn default_millis_per_tick() -> u64 { - 200 } diff --git a/magicblock-config/tests/fixtures/11_everything-defined.toml b/magicblock-config/tests/fixtures/11_everything-defined.toml index 01273cc48..f78d3f850 100644 --- a/magicblock-config/tests/fixtures/11_everything-defined.toml +++ b/magicblock-config/tests/fixtures/11_everything-defined.toml @@ -50,4 +50,3 @@ system-metrics-tick-interval-secs = 10 [task-scheduler] reset = true -millis-per-tick = 1000 diff --git a/magicblock-config/tests/parse_config.rs b/magicblock-config/tests/parse_config.rs index e9bfd1bda..35f6fd8a7 100644 --- a/magicblock-config/tests/parse_config.rs +++ b/magicblock-config/tests/parse_config.rs @@ -282,10 +282,7 @@ fn test_everything_defined() { addr: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), }, }, - task_scheduler: TaskSchedulerConfig { - reset: true, - millis_per_tick: 1000, - }, + task_scheduler: TaskSchedulerConfig { reset: true }, } ); } diff --git a/magicblock-config/tests/read_config.rs b/magicblock-config/tests/read_config.rs index 84b865a6b..95375f0ab 100644 --- a/magicblock-config/tests/read_config.rs +++ b/magicblock-config/tests/read_config.rs @@ -142,7 +142,6 @@ fn test_load_local_dev_with_programs_toml_envs_override() { env::set_var("METRICS_SYSTEM_METRICS_TICK_INTERVAL_SECS", "10"); env::set_var("CLONE_AUTO_AIRDROP_LAMPORTS", "123"); env::set_var("TASK_SCHEDULER_RESET", "true"); - env::set_var("TASK_SCHEDULER_MILLIS_PER_TICK", "1000"); let config = parse_config_with_file(&config_file_dir); @@ -202,10 +201,7 @@ fn test_load_local_dev_with_programs_toml_envs_override() { }, system_metrics_tick_interval_secs: 10, }, - task_scheduler: TaskSchedulerConfig { - reset: true, - millis_per_tick: 1000, - }, + task_scheduler: TaskSchedulerConfig { reset: true }, } ); diff --git a/magicblock-magic-program-api/src/args.rs b/magicblock-magic-program-api/src/args.rs index cb339e153..1a4b58af1 100644 --- a/magicblock-magic-program-api/src/args.rs +++ b/magicblock-magic-program-api/src/args.rs @@ -137,3 +137,12 @@ pub struct CancelTaskRequest { /// Authority that can cancel this task pub authority: Pubkey, } + +impl TaskRequest { + pub fn id(&self) -> u64 { + match self { + Self::Schedule(request) => request.id, + Self::Cancel(request) => request.task_id, + } + } +} diff --git a/magicblock-task-scheduler/src/service.rs b/magicblock-task-scheduler/src/service.rs index 76fb3a9c1..6d40b964e 100644 --- a/magicblock-task-scheduler/src/service.rs +++ b/magicblock-task-scheduler/src/service.rs @@ -1,30 +1,26 @@ use std::{ collections::HashMap, path::Path, - sync::{ - atomic::{AtomicU64, Ordering}, - Arc, - }, + sync::atomic::{AtomicU64, Ordering}, }; use futures_util::StreamExt; use log::*; use magicblock_config::TaskSchedulerConfig; -use magicblock_core::{ - link::transactions::TransactionSchedulerHandle, traits::AccountsBank, +use magicblock_core::link::transactions::{ + ScheduledTasksRx, TransactionSchedulerHandle, }; use magicblock_ledger::LatestBlock; use magicblock_program::{ args::{CancelTaskRequest, ScheduleTaskRequest, TaskRequest}, - instruction_utils::InstructionUtils, validator::{validator_authority, validator_authority_id}, - CrankTask, TaskContext, TASK_CONTEXT_PUBKEY, + CrankTask, }; use solana_sdk::{ - account::ReadableAccount, instruction::Instruction, message::Message, - pubkey::Pubkey, signature::Signature, transaction::Transaction, + instruction::Instruction, message::Message, pubkey::Pubkey, + signature::Signature, transaction::Transaction, }; -use tokio::{select, time::Duration}; +use tokio::{select, task::JoinHandle, time::Duration}; use tokio_util::{ sync::CancellationToken, time::{delay_queue::Key, DelayQueue}, @@ -38,40 +34,36 @@ use crate::{ const NOOP_PROGRAM_ID: Pubkey = Pubkey::from_str_const("noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV"); -pub struct TaskSchedulerService { +pub struct TaskSchedulerService { /// Database for persisting tasks db: SchedulerDatabase, - /// Bank for executing tasks - bank: Arc, /// Used to send transactions for execution tx_scheduler: TransactionSchedulerHandle, + /// Used to receive scheduled tasks from the transaction executor + scheduled_tasks: ScheduledTasksRx, /// Provides latest blockhash for signing transactions block: LatestBlock, - /// Interval at which the task scheduler will check for requests in the context - tick_interval: Duration, /// Queue of tasks to execute task_queue: DelayQueue, /// Map of task IDs to their corresponding keys in the task queue task_queue_keys: HashMap, /// Counter used to make each transaction unique tx_counter: AtomicU64, + /// Token used to cancel the task scheduler + token: CancellationToken, } -unsafe impl Send for TaskSchedulerService {} -unsafe impl Sync for TaskSchedulerService {} -impl TaskSchedulerService { - pub fn start( +unsafe impl Send for TaskSchedulerService {} +unsafe impl Sync for TaskSchedulerService {} +impl TaskSchedulerService { + pub fn new( path: &Path, config: &TaskSchedulerConfig, - bank: Arc, tx_scheduler: TransactionSchedulerHandle, + scheduled_tasks: ScheduledTasksRx, block: LatestBlock, token: CancellationToken, - ) -> Result< - tokio::task::JoinHandle>, - TaskSchedulerError, - > { - debug!("Initializing task scheduler service"); + ) -> Result { if config.reset { match std::fs::remove_file(path) { Ok(_) => {} @@ -87,72 +79,78 @@ impl TaskSchedulerService { // Reschedule all persisted tasks let db = SchedulerDatabase::new(path)?; - let tasks = db.get_tasks()?; - let mut service = Self { + Ok(Self { db, - bank, tx_scheduler, + scheduled_tasks, block, - tick_interval: Duration::from_millis(config.millis_per_tick), task_queue: DelayQueue::new(), task_queue_keys: HashMap::new(), tx_counter: AtomicU64::default(), - }; + token, + }) + } + + pub fn start( + mut self, + ) -> TaskSchedulerResult>> { + let tasks = self.db.get_tasks()?; let now = chrono::Utc::now().timestamp_millis() as u64; - debug!("Task scheduler started at {}", now); + debug!( + "Task scheduler starting at {} with {} tasks", + now, + tasks.len() + ); for task in tasks { let next_execution = task.last_execution_millis + task.execution_interval_millis; let timeout = Duration::from_millis(next_execution.saturating_sub(now)); let task_id = task.id; - let key = service.task_queue.insert(task, timeout); - service.task_queue_keys.insert(task_id, key); + let key = self.task_queue.insert(task, timeout); + self.task_queue_keys.insert(task_id, key); } - Ok(tokio::spawn(service.run(token))) + Ok(tokio::spawn(async move { self.run().await })) } - fn process_context_requests( + fn process_context_request( &mut self, - requests: &Vec, - ) -> TaskSchedulerResult> { - let mut errors = Vec::with_capacity(requests.len()); - for request in requests { - match request { - TaskRequest::Schedule(schedule_request) => { - if let Err(e) = - self.process_schedule_request(schedule_request) - { - self.db.insert_failed_scheduling( - schedule_request.id, - format!("{:?}", e), - )?; - error!( - "Failed to process schedule request {}: {}", - schedule_request.id, e - ); - errors.push(e); - } + request: &TaskRequest, + ) -> TaskSchedulerResult> { + match request { + TaskRequest::Schedule(schedule_request) => { + if let Err(e) = self.process_schedule_request(schedule_request) + { + self.db.insert_failed_scheduling( + schedule_request.id, + format!("{:?}", e), + )?; + error!( + "Failed to process schedule request {}: {}", + schedule_request.id, e + ); + + return Ok(Err(e)); } - TaskRequest::Cancel(cancel_request) => { - if let Err(e) = self.process_cancel_request(cancel_request) - { - self.db.insert_failed_scheduling( - cancel_request.task_id, - format!("{:?}", e), - )?; - error!( - "Failed to process cancel request for task {}: {}", - cancel_request.task_id, e - ); - errors.push(e); - } + } + TaskRequest::Cancel(cancel_request) => { + if let Err(e) = self.process_cancel_request(cancel_request) { + self.db.insert_failed_scheduling( + cancel_request.task_id, + format!("{:?}", e), + )?; + error!( + "Failed to process cancel request for task {}: {}", + cancel_request.task_id, e + ); + + return Ok(Err(e)); } - }; - } + } + }; - Ok(errors) + Ok(Ok(())) } fn process_schedule_request( @@ -261,11 +259,7 @@ impl TaskSchedulerService { Ok(()) } - async fn run( - mut self, - token: CancellationToken, - ) -> TaskSchedulerResult<()> { - let mut interval = tokio::time::interval(self.tick_interval); + pub async fn run(&mut self) -> TaskSchedulerResult<()> { loop { select! { Some(task) = self.task_queue.next() => { @@ -279,48 +273,19 @@ impl TaskSchedulerService { self.db.insert_failed_task(task.id, format!("{:?}", e))?; } } - _ = interval.tick() => { - // HACK: we deserialize the context on every tick avoid using geyser. This will be fixed once the channel to the transaction executor is implemented. - // Performance should not be too bad because the context should be small. - // https://github.com/magicblock-labs/magicblock-validator/issues/523 - - // Process any existing requests from the context - let Some(context_account) = self.bank.get_account(&TASK_CONTEXT_PUBKEY) else { - error!("Task context account not found"); - return Err(TaskSchedulerError::TaskContextNotFound); - }; - - let task_context = bincode::deserialize::(context_account.data()).unwrap_or_default(); - - if task_context.requests.is_empty() { - // Nothing to do because there are no requests in the context - continue; - } - - match self.process_context_requests(&task_context.requests) { - Ok(errors) => { - if !errors.is_empty() { - warn!("Failed to process {} requests out of {}", errors.len(), task_context.requests.len()); - } - - // All requests were processed, reset the context - if let Err(e) = self.process_transaction(vec![ - InstructionUtils::process_tasks_instruction( - &validator_authority_id(), - ), - ]).await { - error!("Failed to reset task context: {}", e); - return Err(e); - } - debug!("Processed {} requests", task_context.requests.len()); + Some(task) = self.scheduled_tasks.recv() => { + match self.process_context_request(&task) { + Ok(Err(e)) => { + warn!("Failed to process request ID={}: {e:?}", task.id()); } Err(e) => { error!("Failed to process context requests: {}", e); return Err(e); } + _ => {} } } - _ = token.cancelled() => { + _ = self.token.cancelled() => { break; } } diff --git a/test-integration/configs/schedule-task.ephem.toml b/test-integration/configs/schedule-task.ephem.toml index bf7e143f9..3f316cb32 100644 --- a/test-integration/configs/schedule-task.ephem.toml +++ b/test-integration/configs/schedule-task.ephem.toml @@ -17,5 +17,4 @@ port = 8899 port = 10001 [task-scheduler] -millis-per-tick = 50 reset = true diff --git a/test-integration/test-task-scheduler/src/lib.rs b/test-integration/test-task-scheduler/src/lib.rs index 36db600ea..a7b82983c 100644 --- a/test-integration/test-task-scheduler/src/lib.rs +++ b/test-integration/test-task-scheduler/src/lib.rs @@ -43,10 +43,7 @@ pub fn setup_validator() -> (TempDir, Child, IntegrationTestContext) { let config = EphemeralConfig { accounts: accounts_config, - task_scheduler: TaskSchedulerConfig { - reset: true, - millis_per_tick: TASK_SCHEDULER_TICK_MILLIS, - }, + task_scheduler: TaskSchedulerConfig { reset: true }, validator: ValidatorConfig { millis_per_slot: TASK_SCHEDULER_TICK_MILLIS, ..Default::default() From 920a9b1fd311413575b688006f25b0ccfb178073 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Wed, 5 Nov 2025 12:47:37 +0100 Subject: [PATCH 03/17] feat: remove task context --- magicblock-api/src/fund_account.rs | 18 +- magicblock-api/src/magic_validator.rs | 4 +- .../src/chainlink/blacklisted_accounts.rs | 1 - .../src/instruction.rs | 7 - magicblock-magic-program-api/src/lib.rs | 10 - magicblock-task-scheduler/src/service.rs | 23 +- programs/magicblock/src/lib.rs | 2 - .../magicblock/src/magicblock_processor.rs | 5 +- programs/magicblock/src/schedule_task/mod.rs | 4 +- .../src/schedule_task/process_cancel_task.rs | 114 ++-------- .../schedule_task/process_process_tasks.rs | 214 ------------------ .../schedule_task/process_schedule_task.rs | 60 ++--- .../magicblock/src/schedule_task/utils.rs | 26 --- programs/magicblock/src/task_context.rs | 111 --------- .../magicblock/src/utils/instruction_utils.rs | 40 +--- test-integration/Cargo.lock | 1 + .../programs/flexi-counter/src/instruction.rs | 4 - .../programs/flexi-counter/src/processor.rs | 14 +- .../tests/test_cancel_ongoing_task.rs | 4 +- .../tests/test_reschedule_task.rs | 5 +- .../tests/test_schedule_error.rs | 4 +- .../tests/test_schedule_task.rs | 4 +- .../tests/test_schedule_task_signed.rs | 3 +- .../tests/test_unauthorized_reschedule.rs | 4 +- 24 files changed, 62 insertions(+), 620 deletions(-) delete mode 100644 programs/magicblock/src/schedule_task/process_process_tasks.rs delete mode 100644 programs/magicblock/src/schedule_task/utils.rs delete mode 100644 programs/magicblock/src/task_context.rs diff --git a/magicblock-api/src/fund_account.rs b/magicblock-api/src/fund_account.rs index e4cec42b7..9bdb5696a 100644 --- a/magicblock-api/src/fund_account.rs +++ b/magicblock-api/src/fund_account.rs @@ -3,8 +3,7 @@ use std::path::Path; use magicblock_accounts_db::AccountsDb; use magicblock_core::traits::AccountsBank; use magicblock_magic_program_api as magic_program; -use magicblock_magic_program_api::TASK_CONTEXT_PUBKEY; -use magicblock_program::{MagicContext, TaskContext}; +use magicblock_program::MagicContext; use solana_sdk::{ account::{AccountSharedData, WritableAccount}, pubkey::Pubkey, @@ -86,18 +85,3 @@ pub(crate) fn fund_magic_context(accountsdb: &AccountsDb) { accountsdb .insert_account(&magic_program::MAGIC_CONTEXT_PUBKEY, &magic_context); } - -pub(crate) fn fund_task_context(accountsdb: &AccountsDb) { - fund_account_with_data( - accountsdb, - &TASK_CONTEXT_PUBKEY, - u64::MAX, - TaskContext::SIZE, - ); - let mut task_context = accountsdb - .get_account(&magic_program::TASK_CONTEXT_PUBKEY) - .unwrap(); - task_context.set_delegated(true); - accountsdb - .insert_account(&magic_program::TASK_CONTEXT_PUBKEY, &task_context); -} diff --git a/magicblock-api/src/magic_validator.rs b/magicblock-api/src/magic_validator.rs index 7a9882c13..9bb8eb199 100644 --- a/magicblock-api/src/magic_validator.rs +++ b/magicblock-api/src/magic_validator.rs @@ -84,8 +84,7 @@ use crate::{ remote_cluster_from_remote, try_convert_accounts_config, }, fund_account::{ - fund_magic_context, fund_task_context, funded_faucet, - init_validator_identity, + fund_magic_context, funded_faucet, init_validator_identity, }, genesis_utils::{create_genesis_config_with_leader, GenesisConfigInfo}, ledger::{ @@ -203,7 +202,6 @@ impl MagicValidator { init_validator_identity(&accountsdb, &validator_pubkey); fund_magic_context(&accountsdb); - fund_task_context(&accountsdb); let faucet_keypair = funded_faucet(&accountsdb, ledger.ledger_path().as_path())?; diff --git a/magicblock-chainlink/src/chainlink/blacklisted_accounts.rs b/magicblock-chainlink/src/chainlink/blacklisted_accounts.rs index 5db5cea80..f596c6ad4 100644 --- a/magicblock-chainlink/src/chainlink/blacklisted_accounts.rs +++ b/magicblock-chainlink/src/chainlink/blacklisted_accounts.rs @@ -24,7 +24,6 @@ pub fn blacklisted_accounts( blacklisted_accounts.insert(magic_program::ID); blacklisted_accounts.insert(magic_program::MAGIC_CONTEXT_PUBKEY); - blacklisted_accounts.insert(magic_program::TASK_CONTEXT_PUBKEY); blacklisted_accounts.insert(*validator_id); blacklisted_accounts.insert(*faucet_id); blacklisted_accounts diff --git a/magicblock-magic-program-api/src/instruction.rs b/magicblock-magic-program-api/src/instruction.rs index 6ff29bee2..8cd525396 100644 --- a/magicblock-magic-program-api/src/instruction.rs +++ b/magicblock-magic-program-api/src/instruction.rs @@ -87,13 +87,6 @@ pub enum MagicBlockInstruction { task_id: u64, }, - /// Process all tasks - /// - /// # Account references - /// - **0.** `[SIGNER]` Validator authority - /// - **1.** `[WRITE]` Task context account - ProcessTasks, - /// Disables the executable check, needed to modify the data of a program /// in preparation to deploying it via LoaderV4 and to modify its authority. /// diff --git a/magicblock-magic-program-api/src/lib.rs b/magicblock-magic-program-api/src/lib.rs index 9aa584220..be1028a75 100644 --- a/magicblock-magic-program-api/src/lib.rs +++ b/magicblock-magic-program-api/src/lib.rs @@ -15,13 +15,3 @@ pub const MAGIC_CONTEXT_PUBKEY: Pubkey = /// NOTE: the default max accumulated account size per transaction is 64MB. /// See: MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES inside program-runtime/src/compute_budget_processor.rs pub const MAGIC_CONTEXT_SIZE: usize = 1024 * 1024 * 5; // 5 MB - -pub const TASK_CONTEXT_PUBKEY: Pubkey = - pubkey!("TaskContext11111111111111111111111111111111"); - -/// Requests are ix data, so they cannot exceed ~1kB. -/// With 1000 schedules per slot, that's 1MB per slot. -/// The task scheduler ticking once every 4 slots, that's 4MB. -/// This can be drastically reduced once we have a channel to the transaction executor. -/// https://github.com/magicblock-labs/magicblock-validator/issues/523 -pub const TASK_CONTEXT_SIZE: usize = 1024 * 1024 * 4; // 4 MB diff --git a/magicblock-task-scheduler/src/service.rs b/magicblock-task-scheduler/src/service.rs index 6d40b964e..8fa4463bf 100644 --- a/magicblock-task-scheduler/src/service.rs +++ b/magicblock-task-scheduler/src/service.rs @@ -14,7 +14,6 @@ use magicblock_ledger::LatestBlock; use magicblock_program::{ args::{CancelTaskRequest, ScheduleTaskRequest, TaskRequest}, validator::{validator_authority, validator_authority_id}, - CrankTask, }; use solana_sdk::{ instruction::Instruction, message::Message, pubkey::Pubkey, @@ -114,14 +113,13 @@ impl TaskSchedulerService { Ok(tokio::spawn(async move { self.run().await })) } - fn process_context_request( + fn process_request( &mut self, request: &TaskRequest, ) -> TaskSchedulerResult> { match request { TaskRequest::Schedule(schedule_request) => { - if let Err(e) = self.process_schedule_request(schedule_request) - { + if let Err(e) = self.register_task(schedule_request) { self.db.insert_failed_scheduling( schedule_request.id, format!("{:?}", e), @@ -153,17 +151,6 @@ impl TaskSchedulerService { Ok(Ok(())) } - fn process_schedule_request( - &mut self, - schedule_request: &ScheduleTaskRequest, - ) -> TaskSchedulerResult<()> { - // Convert request to task and register in database - let task = CrankTask::from(schedule_request); - self.register_task(&task)?; - - Ok(()) - } - fn process_cancel_request( &mut self, cancel_request: &CancelTaskRequest, @@ -222,7 +209,7 @@ impl TaskSchedulerService { pub fn register_task( &mut self, - task: &CrankTask, + task: &ScheduleTaskRequest, ) -> TaskSchedulerResult<()> { let db_task = DbTask { id: task.id, @@ -274,12 +261,12 @@ impl TaskSchedulerService { } } Some(task) = self.scheduled_tasks.recv() => { - match self.process_context_request(&task) { + match self.process_request(&task) { Ok(Err(e)) => { warn!("Failed to process request ID={}: {e:?}", task.id()); } Err(e) => { - error!("Failed to process context requests: {}", e); + error!("Failed to process request: {}", e); return Err(e); } _ => {} diff --git a/programs/magicblock/src/lib.rs b/programs/magicblock/src/lib.rs index bedcc414e..76d21bde8 100644 --- a/programs/magicblock/src/lib.rs +++ b/programs/magicblock/src/lib.rs @@ -6,8 +6,6 @@ mod schedule_transactions; mod toggle_executable_check; pub use magic_context::MagicContext; pub mod magic_scheduled_base_intent; -pub mod task_context; -pub use task_context::{CrankTask, TaskContext}; pub mod magicblock_processor; pub mod test_utils; mod utils; diff --git a/programs/magicblock/src/magicblock_processor.rs b/programs/magicblock/src/magicblock_processor.rs index 60cc13486..433e3a6cf 100644 --- a/programs/magicblock/src/magicblock_processor.rs +++ b/programs/magicblock/src/magicblock_processor.rs @@ -5,9 +5,7 @@ use solana_sdk::program_utils::limited_deserialize; use crate::{ mutate_accounts::process_mutate_accounts, process_scheduled_commit_sent, - schedule_task::{ - process_cancel_task, process_process_tasks, process_schedule_task, - }, + schedule_task::{process_cancel_task, process_schedule_task}, schedule_transactions::{ process_accept_scheduled_commits, process_schedule_base_intent, process_schedule_commit, ProcessScheduleCommitOptions, @@ -73,7 +71,6 @@ declare_process_instruction!( CancelTask { task_id } => { process_cancel_task(signers, invoke_context, task_id) } - ProcessTasks => process_process_tasks(signers, invoke_context), DisableExecutableCheck => { process_toggle_executable_check(signers, invoke_context, false) } diff --git a/programs/magicblock/src/schedule_task/mod.rs b/programs/magicblock/src/schedule_task/mod.rs index 874d7a012..18252f7de 100644 --- a/programs/magicblock/src/schedule_task/mod.rs +++ b/programs/magicblock/src/schedule_task/mod.rs @@ -1,7 +1,5 @@ mod process_cancel_task; -mod process_process_tasks; mod process_schedule_task; -mod utils; + pub(crate) use process_cancel_task::*; -pub(crate) use process_process_tasks::*; pub(crate) use process_schedule_task::*; diff --git a/programs/magicblock/src/schedule_task/process_cancel_task.rs b/programs/magicblock/src/schedule_task/process_cancel_task.rs index 0a3a5cc34..92fb6b095 100644 --- a/programs/magicblock/src/schedule_task/process_cancel_task.rs +++ b/programs/magicblock/src/schedule_task/process_cancel_task.rs @@ -1,17 +1,14 @@ use std::collections::HashSet; -use magicblock_magic_program_api::args::{CancelTaskRequest, TaskRequest}; +use magicblock_magic_program_api::{ + args::{CancelTaskRequest, TaskRequest}, + tls::EXECUTION_TLS_STASH, +}; use solana_log_collector::ic_msg; use solana_program_runtime::invoke_context::InvokeContext; use solana_sdk::{instruction::InstructionError, pubkey::Pubkey}; -use crate::{ - schedule_task::utils::check_task_context_id, - task_context::TaskContext, - utils::accounts::{ - get_instruction_account_with_idx, get_instruction_pubkey_with_idx, - }, -}; +use crate::utils::accounts::get_instruction_pubkey_with_idx; pub(crate) fn process_cancel_task( signers: HashSet, @@ -19,9 +16,6 @@ pub(crate) fn process_cancel_task( task_id: u64, ) -> Result<(), InstructionError> { const TASK_AUTHORITY_IDX: u16 = 0; - const TASK_CONTEXT_IDX: u16 = TASK_AUTHORITY_IDX + 1; - - check_task_context_id(invoke_context, TASK_CONTEXT_IDX)?; let transaction_context = &invoke_context.transaction_context.clone(); @@ -45,12 +39,13 @@ pub(crate) fn process_cancel_task( authority: *task_authority_pubkey, }; - // Get the task context account - let context_acc = get_instruction_account_with_idx( - transaction_context, - TASK_CONTEXT_IDX, - )?; - TaskContext::add_request(context_acc, TaskRequest::Cancel(cancel_request))?; + // Add cancel request to execution TLS stash + EXECUTION_TLS_STASH.with(|stash| { + stash + .borrow_mut() + .tasks + .push_back(TaskRequest::Cancel(cancel_request)) + }); ic_msg!( invoke_context, @@ -63,9 +58,7 @@ pub(crate) fn process_cancel_task( #[cfg(test)] mod test { - use magicblock_magic_program_api::{ - instruction::MagicBlockInstruction, TASK_CONTEXT_PUBKEY, - }; + use magicblock_magic_program_api::instruction::MagicBlockInstruction; use solana_sdk::{ account::AccountSharedData, instruction::{AccountMeta, Instruction, InstructionError}, @@ -76,7 +69,6 @@ mod test { use crate::{ instruction_utils::InstructionUtils, test_utils::process_instruction, - TaskContext, }; #[test] @@ -86,20 +78,10 @@ mod test { let ix = InstructionUtils::cancel_task_instruction(&payer.pubkey(), task_id); - let transaction_accounts = vec![ - ( - payer.pubkey(), - AccountSharedData::new(u64::MAX, 0, &system_program::id()), - ), - ( - TASK_CONTEXT_PUBKEY, - AccountSharedData::new( - u64::MAX, - TaskContext::SIZE, - &system_program::id(), - ), - ), - ]; + let transaction_accounts = vec![( + payer.pubkey(), + AccountSharedData::new(u64::MAX, 0, &system_program::id()), + )]; let expected_result = Ok(()); process_instruction( @@ -110,73 +92,21 @@ mod test { ); } - #[test] - fn fail_process_cancel_task_wrong_context() { - let payer = Keypair::new(); - let wrong_context = Keypair::new().pubkey(); - let task_id = 1; - - let account_metas = vec![ - AccountMeta::new(payer.pubkey(), true), - AccountMeta::new(wrong_context, false), - ]; - let ix = Instruction::new_with_bincode( - crate::id(), - &MagicBlockInstruction::CancelTask { task_id }, - account_metas, - ); - let transaction_accounts = vec![ - ( - payer.pubkey(), - AccountSharedData::new(u64::MAX, 0, &system_program::id()), - ), - ( - wrong_context, - AccountSharedData::new( - u64::MAX, - TaskContext::SIZE, - &system_program::id(), - ), - ), - ]; - let expected_result = Err(InstructionError::MissingAccount); - - process_instruction( - &ix.data, - transaction_accounts, - ix.accounts, - expected_result, - ); - } - #[test] fn fail_unsigned_process_cancel_task() { let payer = Keypair::new(); let task_id = 1; - let account_metas = vec![ - AccountMeta::new(payer.pubkey(), false), - AccountMeta::new(TASK_CONTEXT_PUBKEY, false), - ]; + let account_metas = vec![AccountMeta::new(payer.pubkey(), false)]; let ix = Instruction::new_with_bincode( crate::id(), &MagicBlockInstruction::CancelTask { task_id }, account_metas, ); - let transaction_accounts = vec![ - ( - payer.pubkey(), - AccountSharedData::new(u64::MAX, 0, &system_program::id()), - ), - ( - TASK_CONTEXT_PUBKEY, - AccountSharedData::new( - u64::MAX, - TaskContext::SIZE, - &system_program::id(), - ), - ), - ]; + let transaction_accounts = vec![( + payer.pubkey(), + AccountSharedData::new(u64::MAX, 0, &system_program::id()), + )]; let expected_result = Err(InstructionError::MissingRequiredSignature); process_instruction( diff --git a/programs/magicblock/src/schedule_task/process_process_tasks.rs b/programs/magicblock/src/schedule_task/process_process_tasks.rs deleted file mode 100644 index 7e690bcf3..000000000 --- a/programs/magicblock/src/schedule_task/process_process_tasks.rs +++ /dev/null @@ -1,214 +0,0 @@ -use std::collections::HashSet; - -use solana_log_collector::ic_msg; -use solana_program_runtime::invoke_context::InvokeContext; -use solana_sdk::{instruction::InstructionError, pubkey::Pubkey}; - -use crate::{ - schedule_task::utils::check_task_context_id, - task_context::TaskContext, - utils::accounts::{ - get_instruction_account_with_idx, get_instruction_pubkey_with_idx, - }, - validator::validator_authority_id, -}; - -pub(crate) fn process_process_tasks( - signers: HashSet, - invoke_context: &mut InvokeContext, -) -> Result<(), InstructionError> { - const PROCESSOR_AUTHORITY_IDX: u16 = 0; - const TASK_CONTEXT_IDX: u16 = PROCESSOR_AUTHORITY_IDX + 1; - - check_task_context_id(invoke_context, TASK_CONTEXT_IDX)?; - - let transaction_context = &invoke_context.transaction_context.clone(); - - // Validate that the task authority is a signer - let processor_authority_pubkey = get_instruction_pubkey_with_idx( - transaction_context, - PROCESSOR_AUTHORITY_IDX, - )?; - if !signers.contains(processor_authority_pubkey) { - ic_msg!( - invoke_context, - "ProcessTasks ERR: processor authority {} not in signers", - processor_authority_pubkey - ); - return Err(InstructionError::MissingRequiredSignature); - } - - // Validate that the processor authority is the validator authority - if processor_authority_pubkey.ne(&validator_authority_id()) { - ic_msg!( - invoke_context, - "ProcessTasks ERR: processor authority {} is not the validator authority", - processor_authority_pubkey - ); - return Err(InstructionError::MissingRequiredSignature); - } - - // Get the task context account - let context_acc = get_instruction_account_with_idx( - transaction_context, - TASK_CONTEXT_IDX, - )?; - TaskContext::clear_requests(context_acc)?; - - ic_msg!( - invoke_context, - "Successfully cleared requests from task context", - ); - - Ok(()) -} - -#[cfg(test)] -mod test { - use magicblock_magic_program_api::{ - instruction::MagicBlockInstruction, TASK_CONTEXT_PUBKEY, - }; - use solana_sdk::{ - account::AccountSharedData, - instruction::{AccountMeta, Instruction, InstructionError}, - signature::Keypair, - signer::Signer, - system_program, - }; - - use crate::{ - instruction_utils::InstructionUtils, - test_utils::process_instruction, - validator::{ - generate_validator_authority_if_needed, validator_authority_id, - }, - TaskContext, - }; - - #[test] - fn test_process_tasks() { - generate_validator_authority_if_needed(); - - let ix = InstructionUtils::process_tasks_instruction( - &validator_authority_id(), - ); - let transaction_accounts = vec![ - ( - validator_authority_id(), - AccountSharedData::new(u64::MAX, 0, &system_program::id()), - ), - ( - TASK_CONTEXT_PUBKEY, - AccountSharedData::new( - u64::MAX, - TaskContext::SIZE, - &system_program::id(), - ), - ), - ]; - - process_instruction( - &ix.data, - transaction_accounts, - ix.accounts, - Ok(()), - ); - } - - #[test] - fn fail_process_tasks_wrong_context() { - generate_validator_authority_if_needed(); - - let ix = InstructionUtils::process_tasks_instruction( - &validator_authority_id(), - ); - let wrong_context = Keypair::new().pubkey(); - let transaction_accounts = vec![ - ( - validator_authority_id(), - AccountSharedData::new(u64::MAX, 0, &system_program::id()), - ), - ( - wrong_context, - AccountSharedData::new( - u64::MAX, - TaskContext::SIZE, - &system_program::id(), - ), - ), - ]; - - process_instruction( - &ix.data, - transaction_accounts, - ix.accounts, - Err(InstructionError::MissingAccount), - ); - } - - #[test] - fn fail_process_tasks_wrong_authority() { - generate_validator_authority_if_needed(); - - let wrong_authority = Keypair::new().pubkey(); - let ix = InstructionUtils::process_tasks_instruction(&wrong_authority); - let transaction_accounts = vec![ - ( - wrong_authority, - AccountSharedData::new(u64::MAX, 0, &system_program::id()), - ), - ( - TASK_CONTEXT_PUBKEY, - AccountSharedData::new( - u64::MAX, - TaskContext::SIZE, - &system_program::id(), - ), - ), - ]; - - process_instruction( - &ix.data, - transaction_accounts, - ix.accounts, - Err(InstructionError::MissingRequiredSignature), - ); - } - - #[test] - fn fail_unsigned_process_tasks() { - generate_validator_authority_if_needed(); - - let account_metas = vec![ - AccountMeta::new(validator_authority_id(), false), - AccountMeta::new(TASK_CONTEXT_PUBKEY, false), - ]; - let ix = Instruction::new_with_bincode( - crate::id(), - &MagicBlockInstruction::ProcessTasks, - account_metas, - ); - - let transaction_accounts = vec![ - ( - validator_authority_id(), - AccountSharedData::new(u64::MAX, 0, &system_program::id()), - ), - ( - TASK_CONTEXT_PUBKEY, - AccountSharedData::new( - u64::MAX, - TaskContext::SIZE, - &system_program::id(), - ), - ), - ]; - - process_instruction( - &ix.data, - transaction_accounts, - ix.accounts, - Err(InstructionError::MissingRequiredSignature), - ); - } -} diff --git a/programs/magicblock/src/schedule_task/process_schedule_task.rs b/programs/magicblock/src/schedule_task/process_schedule_task.rs index febfc251b..2fc1147f0 100644 --- a/programs/magicblock/src/schedule_task/process_schedule_task.rs +++ b/programs/magicblock/src/schedule_task/process_schedule_task.rs @@ -1,35 +1,31 @@ use std::collections::HashSet; -use magicblock_magic_program_api::args::{ - ScheduleTaskArgs, ScheduleTaskRequest, TaskRequest, +use magicblock_magic_program_api::{ + args::{ScheduleTaskArgs, ScheduleTaskRequest, TaskRequest}, + tls::EXECUTION_TLS_STASH, }; use solana_log_collector::ic_msg; use solana_program_runtime::invoke_context::InvokeContext; use solana_sdk::{instruction::InstructionError, pubkey::Pubkey}; use crate::{ - schedule_task::utils::check_task_context_id, - task_context::{TaskContext, MIN_EXECUTION_INTERVAL}, - utils::accounts::{ - get_instruction_account_with_idx, get_instruction_pubkey_with_idx, - }, + utils::accounts::get_instruction_pubkey_with_idx, validator::validator_authority_id, }; +const MIN_EXECUTION_INTERVAL: u64 = 10; + pub(crate) fn process_schedule_task( signers: HashSet, invoke_context: &mut InvokeContext, args: ScheduleTaskArgs, ) -> Result<(), InstructionError> { const PAYER_IDX: u16 = 0; - const TASK_CONTEXT_IDX: u16 = PAYER_IDX + 1; - - check_task_context_id(invoke_context, TASK_CONTEXT_IDX)?; let transaction_context = &invoke_context.transaction_context.clone(); let ix_ctx = transaction_context.get_current_instruction_context()?; let ix_accs_len = ix_ctx.get_number_of_instruction_accounts() as usize; - const ACCOUNTS_START: usize = TASK_CONTEXT_IDX as usize + 1; + const ACCOUNTS_START: usize = PAYER_IDX as usize + 1; // Assert MagicBlock program ix_ctx @@ -114,14 +110,13 @@ pub(crate) fn process_schedule_task( iterations: args.iterations, }; - let context_acc = get_instruction_account_with_idx( - transaction_context, - TASK_CONTEXT_IDX, - )?; - TaskContext::add_request( - context_acc, - TaskRequest::Schedule(schedule_request), - )?; + // Add schedule request to execution TLS stash + EXECUTION_TLS_STASH.with(|stash| { + stash + .borrow_mut() + .tasks + .push_back(TaskRequest::Schedule(schedule_request)); + }); ic_msg!( invoke_context, @@ -134,9 +129,7 @@ pub(crate) fn process_schedule_task( #[cfg(test)] mod test { - use magicblock_magic_program_api::{ - instruction::MagicBlockInstruction, TASK_CONTEXT_PUBKEY, - }; + use magicblock_magic_program_api::instruction::MagicBlockInstruction; use solana_sdk::{ account::AccountSharedData, instruction::{AccountMeta, Instruction}, @@ -186,20 +179,10 @@ mod test { let pdas = (0..n_pdas) .map(|_| Keypair::new().pubkey()) .collect::>(); - let mut transaction_accounts = vec![ - ( - payer.pubkey(), - AccountSharedData::new(u64::MAX, 0, &system_program::id()), - ), - ( - TASK_CONTEXT_PUBKEY, - AccountSharedData::new( - u64::MAX, - TaskContext::SIZE, - &system_program::id(), - ), - ), - ]; + let mut transaction_accounts = vec![( + payer.pubkey(), + AccountSharedData::new(u64::MAX, 0, &system_program::id()), + )]; transaction_accounts.extend( pdas.iter() .map(|pda| { @@ -324,10 +307,7 @@ mod test { iterations: 1, instructions: vec![create_simple_ix()], }; - let account_metas = vec![ - AccountMeta::new(payer.pubkey(), false), - AccountMeta::new(TASK_CONTEXT_PUBKEY, false), - ]; + let account_metas = vec![AccountMeta::new(payer.pubkey(), false)]; let ix = Instruction::new_with_bincode( crate::id(), &MagicBlockInstruction::ScheduleTask(args), diff --git a/programs/magicblock/src/schedule_task/utils.rs b/programs/magicblock/src/schedule_task/utils.rs deleted file mode 100644 index fd01c557b..000000000 --- a/programs/magicblock/src/schedule_task/utils.rs +++ /dev/null @@ -1,26 +0,0 @@ -use magicblock_magic_program_api::TASK_CONTEXT_PUBKEY; -use solana_log_collector::ic_msg; -use solana_program_runtime::invoke_context::InvokeContext; -use solana_sdk::instruction::InstructionError; - -use crate::utils::accounts::get_instruction_pubkey_with_idx; - -pub(crate) fn check_task_context_id( - invoke_context: &InvokeContext, - idx: u16, -) -> Result<(), InstructionError> { - let provided_magic_context = get_instruction_pubkey_with_idx( - invoke_context.transaction_context, - idx, - )?; - if !provided_magic_context.eq(&TASK_CONTEXT_PUBKEY) { - ic_msg!( - invoke_context, - "ERR: invalid task context account {}", - provided_magic_context - ); - return Err(InstructionError::MissingAccount); - } - - Ok(()) -} diff --git a/programs/magicblock/src/task_context.rs b/programs/magicblock/src/task_context.rs deleted file mode 100644 index 6aa4b0b01..000000000 --- a/programs/magicblock/src/task_context.rs +++ /dev/null @@ -1,111 +0,0 @@ -use std::cell::RefCell; - -use magicblock_magic_program_api::{ - args::{ScheduleTaskRequest, TaskRequest}, - TASK_CONTEXT_SIZE, -}; -use serde::{Deserialize, Serialize}; -use solana_sdk::{ - account::{AccountSharedData, ReadableAccount}, - instruction::{Instruction, InstructionError}, - pubkey::Pubkey, -}; - -pub const MIN_EXECUTION_INTERVAL: u64 = 10; - -#[derive(Debug, Default, Serialize, Deserialize)] -pub struct TaskContext { - /// List of requests - pub requests: Vec, -} - -impl TaskContext { - pub const SIZE: usize = TASK_CONTEXT_SIZE; - pub const ZERO: [u8; Self::SIZE] = [0; Self::SIZE]; - - pub fn add_request( - context_acc: &RefCell, - request: TaskRequest, - ) -> Result<(), InstructionError> { - Self::update_context(context_acc, |context| { - context.requests.push(request) - }) - } - - pub fn clear_requests( - context_acc: &RefCell, - ) -> Result<(), InstructionError> { - Self::update_context(context_acc, |context| context.requests.clear()) - } - - fn update_context( - context_acc: &RefCell, - update_fn: impl FnOnce(&mut TaskContext), - ) -> Result<(), InstructionError> { - let mut context = Self::deserialize(&context_acc.borrow()) - .map_err(|_| InstructionError::GenericError)?; - update_fn(&mut context); - - let serialized_data = bincode::serialize(&context) - .map_err(|_| InstructionError::InvalidAccountData)?; - let mut context_data = context_acc.borrow_mut(); - context_data.resize(serialized_data.len(), 0); - context_data.set_data_from_slice(&serialized_data); - Ok(()) - } - - pub(crate) fn deserialize( - data: &AccountSharedData, - ) -> Result { - if data.data().is_empty() { - Ok(Self::default()) - } else { - data.deserialize_data() - } - } -} - -// Keep the old Task struct for backward compatibility and database storage -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct CrankTask { - /// Unique identifier for this task - pub id: u64, - /// Unsigned instructions to execute when triggered - pub instructions: Vec, - /// Authority that can modify or cancel this task - pub authority: Pubkey, - /// How frequently the task should be executed, in milliseconds - pub execution_interval_millis: u64, - /// Number of times this task will be executed - pub iterations: u64, -} - -impl CrankTask { - pub fn new( - id: u64, - instructions: Vec, - authority: Pubkey, - execution_interval_millis: u64, - iterations: u64, - ) -> Self { - Self { - id, - instructions, - authority, - execution_interval_millis, - iterations, - } - } -} - -impl From<&ScheduleTaskRequest> for CrankTask { - fn from(request: &ScheduleTaskRequest) -> Self { - Self { - id: request.id, - instructions: request.instructions.clone(), - authority: request.authority, - execution_interval_millis: request.execution_interval_millis, - iterations: request.iterations, - } - } -} diff --git a/programs/magicblock/src/utils/instruction_utils.rs b/programs/magicblock/src/utils/instruction_utils.rs index 647b1b400..722bcec47 100644 --- a/programs/magicblock/src/utils/instruction_utils.rs +++ b/programs/magicblock/src/utils/instruction_utils.rs @@ -213,12 +213,7 @@ impl InstructionUtils { args: ScheduleTaskArgs, accounts: &[Pubkey], ) -> Instruction { - use magicblock_magic_program_api::TASK_CONTEXT_PUBKEY; - - let mut account_metas = vec![ - AccountMeta::new(*payer, true), - AccountMeta::new(TASK_CONTEXT_PUBKEY, false), - ]; + let mut account_metas = vec![AccountMeta::new(*payer, true)]; for account in accounts { account_metas.push(AccountMeta::new_readonly(*account, true)); } @@ -246,12 +241,7 @@ impl InstructionUtils { authority: &Pubkey, task_id: u64, ) -> Instruction { - use magicblock_magic_program_api::TASK_CONTEXT_PUBKEY; - - let account_metas = vec![ - AccountMeta::new(*authority, true), - AccountMeta::new(TASK_CONTEXT_PUBKEY, false), - ]; + let account_metas = vec![AccountMeta::new(*authority, true)]; Instruction::new_with_bincode( crate::id(), @@ -260,32 +250,6 @@ impl InstructionUtils { ) } - // ----------------- - // Process Tasks - // ----------------- - pub fn process_tasks( - authority: &Keypair, - recent_blockhash: Hash, - ) -> Transaction { - let ix = Self::process_tasks_instruction(&authority.pubkey()); - Self::into_transaction(authority, ix, recent_blockhash) - } - - pub fn process_tasks_instruction(authority: &Pubkey) -> Instruction { - use magicblock_magic_program_api::TASK_CONTEXT_PUBKEY; - - let account_metas = vec![ - AccountMeta::new(*authority, true), - AccountMeta::new(TASK_CONTEXT_PUBKEY, false), - ]; - - Instruction::new_with_bincode( - crate::id(), - &MagicBlockInstruction::ProcessTasks, - account_metas, - ) - } - // ----------------- // Executable Check // ----------------- diff --git a/test-integration/Cargo.lock b/test-integration/Cargo.lock index 00b1f92e4..5cf894420 100644 --- a/test-integration/Cargo.lock +++ b/test-integration/Cargo.lock @@ -3836,6 +3836,7 @@ dependencies = [ "magicblock-accounts-db", "magicblock-core", "magicblock-ledger", + "magicblock-magic-program-api 0.2.3", "magicblock-metrics", "magicblock-program", "parking_lot 0.12.4", diff --git a/test-integration/programs/flexi-counter/src/instruction.rs b/test-integration/programs/flexi-counter/src/instruction.rs index 1389f7641..2388a417d 100644 --- a/test-integration/programs/flexi-counter/src/instruction.rs +++ b/test-integration/programs/flexi-counter/src/instruction.rs @@ -417,7 +417,6 @@ pub fn create_intent_ix( #[allow(clippy::too_many_arguments)] pub fn create_schedule_task_ix( payer: Pubkey, - task_context: Pubkey, magic_program: Pubkey, task_id: u64, execution_interval_millis: u64, @@ -430,7 +429,6 @@ pub fn create_schedule_task_ix( let accounts = vec![ AccountMeta::new_readonly(magic_program, false), AccountMeta::new(payer, true), - AccountMeta::new(task_context, false), AccountMeta::new(pda, false), ]; Instruction::new_with_borsh( @@ -448,7 +446,6 @@ pub fn create_schedule_task_ix( pub fn create_cancel_task_ix( payer: Pubkey, - task_context: Pubkey, magic_program: Pubkey, task_id: u64, ) -> Instruction { @@ -456,7 +453,6 @@ pub fn create_cancel_task_ix( let accounts = vec![ AccountMeta::new_readonly(magic_program, false), AccountMeta::new(payer, true), - AccountMeta::new(task_context, false), ]; Instruction::new_with_borsh( *program_id, diff --git a/test-integration/programs/flexi-counter/src/processor.rs b/test-integration/programs/flexi-counter/src/processor.rs index a81021d9a..22d242e83 100644 --- a/test-integration/programs/flexi-counter/src/processor.rs +++ b/test-integration/programs/flexi-counter/src/processor.rs @@ -407,7 +407,6 @@ fn process_schedule_task( let account_info_iter = &mut accounts.iter(); let _magic_program_info = next_account_info(account_info_iter)?; let payer_info = next_account_info(account_info_iter)?; - let task_context_info = next_account_info(account_info_iter)?; let counter_pda_info = next_account_info(account_info_iter)?; let (counter_pda, bump) = FlexiCounter::pda(payer_info.key); @@ -443,18 +442,13 @@ fn process_schedule_task( &ix_data, vec![ AccountMeta::new(*payer_info.key, true), - AccountMeta::new(*task_context_info.key, false), AccountMeta::new(*counter_pda_info.key, true), ], ); invoke_signed( &ix, - &[ - payer_info.clone(), - task_context_info.clone(), - counter_pda_info.clone(), - ], + &[payer_info.clone(), counter_pda_info.clone()], &[&seeds], )?; @@ -470,7 +464,6 @@ fn process_cancel_task( let account_info_iter = &mut accounts.iter(); let _magic_program_info = next_account_info(account_info_iter)?; let payer_info = next_account_info(account_info_iter)?; - let task_context_info = next_account_info(account_info_iter)?; let ix_data = bincode::serialize(&MagicBlockInstruction::CancelTask { task_id: args.task_id, @@ -483,10 +476,7 @@ fn process_cancel_task( let ix = Instruction::new_with_bytes( MAGIC_PROGRAM_ID, &ix_data, - vec![ - AccountMeta::new(*payer_info.key, true), - AccountMeta::new(*task_context_info.key, false), - ], + vec![AccountMeta::new(*payer_info.key, true)], ); invoke(&ix, &[payer_info.clone(), task_context_info.clone()])?; diff --git a/test-integration/test-task-scheduler/tests/test_cancel_ongoing_task.rs b/test-integration/test-task-scheduler/tests/test_cancel_ongoing_task.rs index 9e389757e..7529d70cd 100644 --- a/test-integration/test-task-scheduler/tests/test_cancel_ongoing_task.rs +++ b/test-integration/test-task-scheduler/tests/test_cancel_ongoing_task.rs @@ -1,6 +1,6 @@ use cleanass::{assert, assert_eq}; use integration_test_tools::{expect, validator::cleanup}; -use magicblock_program::{ID as MAGIC_PROGRAM_ID, TASK_CONTEXT_PUBKEY}; +use magicblock_program::ID as MAGIC_PROGRAM_ID; use magicblock_task_scheduler::SchedulerDatabase; use program_flexi_counter::{ instruction::{create_cancel_task_ix, create_schedule_task_ix}, @@ -41,7 +41,6 @@ fn test_cancel_ongoing_task() { &mut Transaction::new_signed_with_payer( &[create_schedule_task_ix( payer.pubkey(), - TASK_CONTEXT_PUBKEY, MAGIC_PROGRAM_ID, task_id, execution_interval_millis, @@ -80,7 +79,6 @@ fn test_cancel_ongoing_task() { &mut Transaction::new_signed_with_payer( &[create_cancel_task_ix( payer.pubkey(), - TASK_CONTEXT_PUBKEY, MAGIC_PROGRAM_ID, task_id, )], diff --git a/test-integration/test-task-scheduler/tests/test_reschedule_task.rs b/test-integration/test-task-scheduler/tests/test_reschedule_task.rs index 83bbc13d5..4d4c3b3cc 100644 --- a/test-integration/test-task-scheduler/tests/test_reschedule_task.rs +++ b/test-integration/test-task-scheduler/tests/test_reschedule_task.rs @@ -1,6 +1,6 @@ use cleanass::{assert, assert_eq}; use integration_test_tools::{expect, validator::cleanup}; -use magicblock_program::{ID as MAGIC_PROGRAM_ID, TASK_CONTEXT_PUBKEY}; +use magicblock_program::ID as MAGIC_PROGRAM_ID; use magicblock_task_scheduler::{db::DbTask, SchedulerDatabase}; use program_flexi_counter::{ instruction::{create_cancel_task_ix, create_schedule_task_ix}, @@ -41,7 +41,6 @@ fn test_reschedule_task() { &mut Transaction::new_signed_with_payer( &[create_schedule_task_ix( payer.pubkey(), - TASK_CONTEXT_PUBKEY, MAGIC_PROGRAM_ID, task_id, execution_interval_millis, @@ -77,7 +76,6 @@ fn test_reschedule_task() { &mut Transaction::new_signed_with_payer( &[create_schedule_task_ix( payer.pubkey(), - TASK_CONTEXT_PUBKEY, MAGIC_PROGRAM_ID, task_id, new_execution_interval_millis, @@ -169,7 +167,6 @@ fn test_reschedule_task() { &mut Transaction::new_signed_with_payer( &[create_cancel_task_ix( payer.pubkey(), - TASK_CONTEXT_PUBKEY, MAGIC_PROGRAM_ID, task_id, )], diff --git a/test-integration/test-task-scheduler/tests/test_schedule_error.rs b/test-integration/test-task-scheduler/tests/test_schedule_error.rs index af629963e..06a6495fb 100644 --- a/test-integration/test-task-scheduler/tests/test_schedule_error.rs +++ b/test-integration/test-task-scheduler/tests/test_schedule_error.rs @@ -1,6 +1,6 @@ use cleanass::{assert, assert_eq}; use integration_test_tools::{expect, validator::cleanup}; -use magicblock_program::{ID as MAGIC_PROGRAM_ID, TASK_CONTEXT_PUBKEY}; +use magicblock_program::ID as MAGIC_PROGRAM_ID; use magicblock_task_scheduler::SchedulerDatabase; use program_flexi_counter::{ instruction::{create_cancel_task_ix, create_schedule_task_ix}, @@ -42,7 +42,6 @@ fn test_schedule_error() { &mut Transaction::new_signed_with_payer( &[create_schedule_task_ix( payer.pubkey(), - TASK_CONTEXT_PUBKEY, MAGIC_PROGRAM_ID, task_id, execution_interval_millis, @@ -128,7 +127,6 @@ fn test_schedule_error() { &mut Transaction::new_signed_with_payer( &[create_cancel_task_ix( payer.pubkey(), - TASK_CONTEXT_PUBKEY, MAGIC_PROGRAM_ID, task_id, )], diff --git a/test-integration/test-task-scheduler/tests/test_schedule_task.rs b/test-integration/test-task-scheduler/tests/test_schedule_task.rs index 382978c00..c6bfd1de8 100644 --- a/test-integration/test-task-scheduler/tests/test_schedule_task.rs +++ b/test-integration/test-task-scheduler/tests/test_schedule_task.rs @@ -1,6 +1,6 @@ use cleanass::{assert, assert_eq}; use integration_test_tools::{expect, validator::cleanup}; -use magicblock_program::{ID as MAGIC_PROGRAM_ID, TASK_CONTEXT_PUBKEY}; +use magicblock_program::ID as MAGIC_PROGRAM_ID; use magicblock_task_scheduler::{db::DbTask, SchedulerDatabase}; use program_flexi_counter::{ instruction::{create_cancel_task_ix, create_schedule_task_ix}, @@ -41,7 +41,6 @@ fn test_schedule_task() { &mut Transaction::new_signed_with_payer( &[create_schedule_task_ix( payer.pubkey(), - TASK_CONTEXT_PUBKEY, MAGIC_PROGRAM_ID, task_id, execution_interval_millis, @@ -133,7 +132,6 @@ fn test_schedule_task() { &mut Transaction::new_signed_with_payer( &[create_cancel_task_ix( payer.pubkey(), - TASK_CONTEXT_PUBKEY, MAGIC_PROGRAM_ID, task_id, )], diff --git a/test-integration/test-task-scheduler/tests/test_schedule_task_signed.rs b/test-integration/test-task-scheduler/tests/test_schedule_task_signed.rs index e85463ccf..69d190129 100644 --- a/test-integration/test-task-scheduler/tests/test_schedule_task_signed.rs +++ b/test-integration/test-task-scheduler/tests/test_schedule_task_signed.rs @@ -1,5 +1,5 @@ use integration_test_tools::{expect, validator::cleanup}; -use magicblock_program::{ID as MAGIC_PROGRAM_ID, TASK_CONTEXT_PUBKEY}; +use magicblock_program::ID as MAGIC_PROGRAM_ID; use program_flexi_counter::instruction::create_schedule_task_ix; use solana_sdk::{ instruction::InstructionError, @@ -37,7 +37,6 @@ fn test_schedule_task_signed() { &mut Transaction::new_signed_with_payer( &[create_schedule_task_ix( payer.pubkey(), - TASK_CONTEXT_PUBKEY, MAGIC_PROGRAM_ID, task_id, execution_interval_millis, diff --git a/test-integration/test-task-scheduler/tests/test_unauthorized_reschedule.rs b/test-integration/test-task-scheduler/tests/test_unauthorized_reschedule.rs index ccbb31c16..add2f4c99 100644 --- a/test-integration/test-task-scheduler/tests/test_unauthorized_reschedule.rs +++ b/test-integration/test-task-scheduler/tests/test_unauthorized_reschedule.rs @@ -1,6 +1,6 @@ use cleanass::{assert, assert_eq}; use integration_test_tools::{expect, validator::cleanup}; -use magicblock_program::{ID as MAGIC_PROGRAM_ID, TASK_CONTEXT_PUBKEY}; +use magicblock_program::ID as MAGIC_PROGRAM_ID; use magicblock_task_scheduler::{db::DbTask, SchedulerDatabase}; use program_flexi_counter::{ instruction::create_schedule_task_ix, state::FlexiCounter, @@ -46,7 +46,6 @@ fn test_unauthorized_reschedule() { &mut Transaction::new_signed_with_payer( &[create_schedule_task_ix( payer.pubkey(), - TASK_CONTEXT_PUBKEY, MAGIC_PROGRAM_ID, task_id, execution_interval_millis, @@ -82,7 +81,6 @@ fn test_unauthorized_reschedule() { &mut Transaction::new_signed_with_payer( &[create_schedule_task_ix( different_payer.pubkey(), - TASK_CONTEXT_PUBKEY, MAGIC_PROGRAM_ID, task_id, new_execution_interval_millis, From 3b171825e7607bf500ed24563f3360a5d373f9ce Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Wed, 5 Nov 2025 15:52:29 +0100 Subject: [PATCH 04/17] test: task scheduler with test-kit --- Cargo.lock | 7 + magicblock-api/src/magic_validator.rs | 7 +- magicblock-core/src/link.rs | 4 +- magicblock-task-scheduler/Cargo.toml | 8 + magicblock-task-scheduler/tests/service.rs | 178 ++++++++++++++++++ programs/elfs/guinea.so | Bin 113320 -> 143600 bytes programs/guinea/Cargo.toml | 1 + programs/guinea/src/lib.rs | 66 ++++++- .../schedule_task/process_schedule_task.rs | 2 +- test-integration/Cargo.lock | 1 + test-kit/src/lib.rs | 9 + 11 files changed, 277 insertions(+), 6 deletions(-) create mode 100644 magicblock-task-scheduler/tests/service.rs diff --git a/Cargo.lock b/Cargo.lock index 1700adacd..036ffcc31 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2248,6 +2248,7 @@ name = "guinea" version = "0.2.3" dependencies = [ "bincode", + "magicblock-magic-program-api", "serde", "solana-program", ] @@ -3950,19 +3951,25 @@ dependencies = [ "bincode", "chrono", "futures-util", + "guinea", "log", "magicblock-config", "magicblock-core", "magicblock-ledger", + "magicblock-magic-program-api", "magicblock-processor", "magicblock-program", "rusqlite", "serde", + "solana-account", "solana-program", + "solana-pubkey", "solana-pubsub-client", "solana-sdk", + "solana-signature", "solana-svm", "solana-timings", + "test-kit", "thiserror 1.0.69", "tokio", "tokio-util 0.7.15", diff --git a/magicblock-api/src/magic_validator.rs b/magicblock-api/src/magic_validator.rs index 9bb8eb199..df797f988 100644 --- a/magicblock-api/src/magic_validator.rs +++ b/magicblock-api/src/magic_validator.rs @@ -233,7 +233,7 @@ impl MagicValidator { let accounts_config = try_get_remote_accounts_config(&config.accounts)?; - let (dispatch, validator_channels) = link(); + let (mut dispatch, validator_channels) = link(); let committor_persist_path = storage_path.join("committor_service.sqlite"); @@ -333,7 +333,10 @@ impl MagicValidator { &task_scheduler_db_path, &config.task_scheduler, dispatch.transaction_scheduler.clone(), - dispatch.tasks_service, + dispatch + .tasks_service + .take() + .expect("tasks_service should be initialized"), ledger.latest_block().clone(), token.clone(), )?; diff --git a/magicblock-core/src/link.rs b/magicblock-core/src/link.rs index c429b6505..1950dd424 100644 --- a/magicblock-core/src/link.rs +++ b/magicblock-core/src/link.rs @@ -28,7 +28,7 @@ pub struct DispatchEndpoints { /// Receives notifications when a new block is produced. pub block_update: BlockUpdateRx, /// Receives scheduled (crank) tasks from transactions executor. - pub tasks_service: ScheduledTasksRx, + pub tasks_service: Option, } /// A collection of channel endpoints for the **validator's internal core**. @@ -73,7 +73,7 @@ pub fn link() -> (DispatchEndpoints, ValidatorChannelEndpoints) { transaction_status: transaction_status_rx, account_update: account_update_rx, block_update: block_update_rx, - tasks_service: tasks_rx, + tasks_service: Some(tasks_rx), }; // Bundle the corresponding channel ends for the validator's internal core. diff --git a/magicblock-task-scheduler/Cargo.toml b/magicblock-task-scheduler/Cargo.toml index 9c1fcc9df..3fa924ce0 100644 --- a/magicblock-task-scheduler/Cargo.toml +++ b/magicblock-task-scheduler/Cargo.toml @@ -28,3 +28,11 @@ solana-pubsub-client = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } tokio-util = { workspace = true, features = ["time"] } + +[dev-dependencies] +magicblock-magic-program-api = { workspace = true } +test-kit = { workspace = true } +guinea = { workspace = true } +solana-account = { workspace = true } +solana-pubkey = { workspace = true } +solana-signature = { workspace = true } diff --git a/magicblock-task-scheduler/tests/service.rs b/magicblock-task-scheduler/tests/service.rs new file mode 100644 index 000000000..29b6ef448 --- /dev/null +++ b/magicblock-task-scheduler/tests/service.rs @@ -0,0 +1,178 @@ +use std::time::Duration; + +use guinea::GuineaInstruction; +use magicblock_config::TaskSchedulerConfig; +use magicblock_program::{ + args::ScheduleTaskArgs, validator::init_validator_authority, +}; +use magicblock_task_scheduler::{ + errors::TaskSchedulerResult, SchedulerDatabase, TaskSchedulerService, +}; +use solana_account::ReadableAccount; +use solana_program::{ + instruction::{AccountMeta, Instruction}, + native_token::LAMPORTS_PER_SOL, +}; +use test_kit::{ExecutionTestEnv, Signer}; +use tokio_util::sync::CancellationToken; + +#[tokio::test] +pub async fn test_schedule_task() -> TaskSchedulerResult<()> { + let mut env = ExecutionTestEnv::new(); + let account = + env.create_account_with_config(LAMPORTS_PER_SOL, 1, guinea::ID); + + init_validator_authority(env.payer.insecure_clone()); + + let token = CancellationToken::new(); + let task_scheduler_db_path = SchedulerDatabase::path( + env.ledger + .ledger_path() + .parent() + .expect("ledger_path didn't have a parent, should never happen"), + ); + let handle = TaskSchedulerService::new( + &task_scheduler_db_path, + &TaskSchedulerConfig::default(), + env.transaction_scheduler.clone(), + env.dispatch + .tasks_service + .take() + .expect("Tasks service should be initialized"), + env.ledger.latest_block().clone(), + token.clone(), + )? + .start()?; + + // Schedule a task + let ix = Instruction::new_with_bincode( + guinea::ID, + &GuineaInstruction::ScheduleTask(ScheduleTaskArgs { + task_id: 1, + execution_interval_millis: 10, + iterations: 1, + instructions: vec![Instruction::new_with_bincode( + guinea::ID, + &GuineaInstruction::Increment, + vec![AccountMeta::new(account.pubkey(), false)], + )], + }), + vec![ + AccountMeta::new_readonly(magicblock_magic_program_api::ID, false), + AccountMeta::new(env.payer.pubkey(), true), + AccountMeta::new(account.pubkey(), false), + ], + ); + let txn = env.build_transaction(&[ix]); + let result = env.execute_transaction(txn).await; + assert!( + result.is_ok(), + "failed to execute schedule task transaction: {:?}", + result + ); + + // Wait the task scheduler to receive the task + tokio::time::sleep(Duration::from_millis(10)).await; + + assert_eq!( + env.get_account(account.pubkey()).data().first(), + Some(&1), + "the first byte of the account data should have been modified" + ); + + token.cancel(); + handle.await.unwrap().unwrap(); + + Ok(()) +} + +#[tokio::test] +pub async fn test_cancel_task() -> TaskSchedulerResult<()> { + let mut env = ExecutionTestEnv::new(); + let account = + env.create_account_with_config(LAMPORTS_PER_SOL, 1, guinea::ID); + + init_validator_authority(env.payer.insecure_clone()); + + let token = CancellationToken::new(); + let task_scheduler_db_path = SchedulerDatabase::path( + env.ledger + .ledger_path() + .parent() + .expect("ledger_path didn't have a parent, should never happen"), + ); + let handle = TaskSchedulerService::new( + &task_scheduler_db_path, + &TaskSchedulerConfig::default(), + env.transaction_scheduler.clone(), + env.dispatch + .tasks_service + .take() + .expect("Tasks service should be initialized"), + env.ledger.latest_block().clone(), + token.clone(), + )? + .start()?; + + // Schedule a task + let interval = 100; + let ix = Instruction::new_with_bincode( + guinea::ID, + &GuineaInstruction::ScheduleTask(ScheduleTaskArgs { + task_id: 1, + execution_interval_millis: interval, + iterations: 100, + instructions: vec![Instruction::new_with_bincode( + guinea::ID, + &GuineaInstruction::Increment, + vec![AccountMeta::new(account.pubkey(), false)], + )], + }), + vec![ + AccountMeta::new_readonly(magicblock_magic_program_api::ID, false), + AccountMeta::new(env.payer.pubkey(), true), + AccountMeta::new(account.pubkey(), false), + ], + ); + let txn = env.build_transaction(&[ix]); + let result = env.execute_transaction(txn).await; + assert!( + result.is_ok(), + "failed to execute schedule task transaction: {:?}", + result + ); + + // Wait the task scheduler to execute 10 times (+some to register the task) + tokio::time::sleep(Duration::from_millis(5 * interval)).await; + + // Cancel the task + let ix = Instruction::new_with_bincode( + guinea::ID, + &GuineaInstruction::CancelTask(1), + vec![ + AccountMeta::new_readonly(magicblock_magic_program_api::ID, false), + AccountMeta::new(env.payer.pubkey(), true), + ], + ); + let txn = env.build_transaction(&[ix]); + let result = env.execute_transaction(txn).await; + assert!( + result.is_ok(), + "failed to execute cancel task transaction: {:?}", + result + ); + + // Wait the task scheduler to cancel the task and make sure it didn't execute again + tokio::time::sleep(Duration::from_millis(2 * interval)).await; + + assert_eq!( + env.get_account(account.pubkey()).data().first(), + Some(&5), + "the first byte of the account data should have been modified" + ); + + token.cancel(); + handle.await.unwrap().unwrap(); + + Ok(()) +} diff --git a/programs/elfs/guinea.so b/programs/elfs/guinea.so index c60e6ed010657bf2c6dbdd7c7c06f762e0575b21..b9c6c4dbd7e43bebc5965d5bfb3c9a5d539835cd 100755 GIT binary patch literal 143600 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
    tBM{PJ_NjZ{b*rGq^H|NK1j zIx7D{nnTpwD`m1i57MIX_Fh?jTb_XQT~Rr{H@b$1dpg#*HC4v*dci(VET?j?PY1Uf zLV!=@b&wA``!?|uNQPA-{&xsZtgDevw5tq~{aWo$2K;Hp0gA6seBoRc^22@{1zYb` zM}IOYyvthm$@Af>6hHRoP*5K4@7no$4l=v4gQ&#!zp%c@A&mOKhg~Z8f&!I8z3!*) zX2eEzh!8xdJ&k_xJn&fga>>9|5nYIi^&`OR>DhvuXg}D&FpLU0u77~mzrbuq`T1sG zIBu_4c89>Ry}1Ns1IF`pORCS}KbmXf_e2%jJF`{J>l7Ic(<(IqOMYR`%$%0zBOyYPgZgkI1e z;S;~e!RyJcd2&8lN?{^g_22kCEJ--vNfYwmdp1`4DEV{$T0@0^PBQ6(onG6b4hp4m zVN}R*JMcZcW9h?R=(&xrqHnA5ofbtuHmO6D6LU$Q?=OKK$Mr=sO(RQ}7L8 zkqb|e{!WrN`jsyKaIuW%b51^IKB~PbKj>a`75-8=gyBk6d1+_>ay_lm$G*KuKQW%c zXTB~{YhKnntG(5^1ne4EPo_V@=XQutAi1vNk^he*fmbF6lGoZ1ER z>!1Hg)(IJr9`-nqJs1Ofp!!=U^=DxZl>ItZIbCRf!4>);sFR$6GN6h)VJBD*RlBi# zprEBEzws0jZv5iCd_hO!`UPSj@iyhODzsy_e)DJFWQZ znkpH>K3)#xtyo2W$XWeErE7C_h;Y_lOa4?0@*PV)|M-~nUe$x^*NF-c&uE`FwD!p) zdWqWS51^y;4|+%0={-gr^cQl+OpN5A>_gfX0mXYKAb&ph;&~;`6-9VNrsIAGJB@yX z`#-SX&{Oz0pM*a6j1?jSuj_a{i**&`;4_Lvd|1t&DzXI>&%@a1aXEMn6Xo%F6u%D^ z^S~_1ihK1Y`TSfY*08V3Q$VLbbQ;$q~%)B7IH=zNkqI{(cyL-xlqH@P(sf1$qK9pWd$Aj;yy}8}q78$}QVVcFek)W&ZFv z>i(!H@ul_BU!nRz1?dGG)8%&Lc|g}Fd4Cx8(cVtn9(+%f%j5YG@2mKXUx@hu&qMgR zC5$gO#J{ZSIIZ5 z<`HR88P58ec9p~xYCJ>#0>3Or^jlFsm9K^BYa*W14tACA&G5baScl}B+atD1@)^5d z)(iUw>>#JEGiAeC`;@A^;`PDmoc0$f94*0m%+DWHsBwqie-pb`%7^!p`99Rl<0N6$ zgV;_ff1mNBlrs^B{1T0S>FD38oGRkqq;y!f!Vh_6|HF9tPi;KK31T98|DcPqvFr|t zrFNs|b*7Quvq->Wj^{Vf^WQvw33!$LHDx}u6WRm(K~CsLh~oC*c|D&a!d@eu=Q}G@ z`}Zh4IaT#Lp7-P`oW*)V<%ssD`&4i|j#iv46|ZfA2#)vn<0R&K$MQNvz|2K5Ki9w0 zP=)&?|=)tD{tx*&@5M;_&F77XKkLu*M>!rxzUuetFt2a7@d^2~ zq8(KGeigaoQ`tPvIFnfU|BGCc zzCC^*K|H^&7x|y~lK9mL%ID`yVV|Hqpab9HBOvQpO7>Os3*T`@B?aXZhTpSF#!sbw z1)Q}=VvKK~Q%Zku?-=9ck+%^d9ow;*MG~(cC-)<{e{z52^Bj!hbj&r7wnV09{fa97 ze4@9~{DR7X-R$%o65lERSNH(8Y{z#JAk#zhE_v>2w_AcQRS*6y3G>bCLClXqkLw3H zzn-8y__;RTcjNJy_nDui%9%tbLiUNoF>n=#l0*B!fo?VV{2T$ov6I_Eaxuy8^k&QN zy{q*0^$y~B{m<>1Px;bm?%GpCk>u#7z*(wXwtKLN*p-J3sHA^9K2N1_2skTDf5_Rt zPlA2xv?(&Yiae0Sb5RaGAq-bjFa7oEy>*UurE+2Y9r;KOa5ak0^`qrDj`=;tId0d< zzPAPj`abg^9CG4tUH&Y;$uvKxpmcms9`X_OReCv9^@}Vu4l$qbP0H7ze4Ot%_*Hrg zKCrH|>z9zz1j}seaP~g5;)B zKd2->t3g1=_k@Ie&XnO*ln?dCXD=P?3Zp=-=1S?WM?!L}7s5`XA5pL)pW=(($F_cq z>PJ*>(T|iKPNwgvihjg$v+2J{<*?pQrgfd{KWe_OAJN(L~liS}C} zvDH76T&(_~_#U)>Sn=wfk)nUB@n>j)ei0iX`H0ZG8hl=*~q? z>ODd6r|BDy3rFcQ@!{w%$FTPf`io^b>laDi(r4ledSZW`Tja}@=`4M=*?Y+8SoYpt z?*EOwe^)fQbSXV1Il=M#$kJnF57~aRoysZ~$*peSh zd=IIV_=ZM=)O)2%sehV;K6UB>AT1}WBt`3eC&60+t%g$e2#tI z+(-3Ew6C&Dgva+)ocE=j`zm&N9U@Ma8;rn{EG!?}LOdp-6H_2fFZ0@_E1C zx__X;`QFJ)bzg*^@5ow3naEA{elQ^T$(9)FYf(P&=_p_I^RUW?bfSFnP0HG$e0^Vf zH^qVeZdLD0b=Mylu8XmRMj8qymv)ZyC%fku6X+-R4~svlpPfIflkJ$t4eT>~L^Q+o zk{;F}Fi`>8F92QUBOIj)ki&kujHe&u_3P1#_rj|vtw~WW z%AYxsevrfX25!3M%J=xC6(Lma;Ue6!M-xP_*hhi zmhN4oQ}YDmgS>d3-6~JT!#=~H43>`HZ)BfeVZ7$_;SSkO82?TB_pI`{9NfQx>zaMF z@cA48`iuJWdstSSA)>AGD8xg8iNev8iNaA-qHq#H!f?|TzLWF-a>D!@_bbU0KA(>< zzhx+#T$bV&`i6U!2!qe*t^2q{7xm!hnOVR2{(lR_gCvJz{Y%#_#o_>dStTe-`(%Hv zIJZfL;~ck$euBRO@&oQkB7(o~(n9e<&N(93Sg~FD{N7l^3ppnU2VaTO3ppo9FXWsc z+_cfzNtq#Md->q6jQD`#_Z)J3*?BS?_hf_|S4*7sE$R0tMQfpU5OP%M%W7nNZcODT z`UUhe>lT%7iS%J#PNkoG&#A6NhC^?`7wi@MnI`_m?HBB#Nf>g6&-R`7DY1?O-8u-1 z9G`#J5k0spWpBG?PZgNo*TV1F;d@3A-v}Wu*byN&rFWR$gFf5)x^h8A?4N>D&YuNF zfGIs3-iH7>yEV+|S6nI6@pJq9z09tBHD9M_k>jr;E=E<$c*ws;6j=Mv>gORM9V^@dj=M3=KKHKh-ut3b<{v3}?I3Rw(CJ zd_K)~vx?X-DJky<+20#1!l@ki_WSb!t9s=gN0H=mnU5j?>&okwYk~nv= z^2?>4)~@^|%HO7Z)gSnMCoEr<55F(uSz^oHZVyX!F|w4;_gSoT6_WnLN}l|F6Z?F( zpY#mlOg{M!t9cmTYvBFPt0WnEe~gq1hg*EAaV`^_T zV{ZP!(I?J1dHP9(;aW4?*cgtPb!*KHk=m-JYIDx)Xk$fgWhA#|WBrEctj1`xsj<1D z$y{11f7H~~nk_X*Xht?fHb!cjq7~r{8zPlv`8knj^MaOTrtPBcQ#ZW=)*zqKb;T=GvyON1DPq+1yYx(iqKcY_6@1H0HL{HLlOC-wa0=hNuqfnwuzfvlvE_A5E1T=<>l&%pl@zqBW@Am$%1A?Vq_!gRb&^VP_>#yn(Z;6} zs@o`7Xl$%&6ug%;*OxWbMS#nQS>i16wVK+>_4IFL-NuM{@wx+fz(pVebfJRm25i@5{vsSLE*%+x@(cEO_tcf*6qUPdAxRRP=eWmhM>yyOR zBDrZ+hofd?m~%PlQognMumK7^Y0o`fx>LQDtQ#wbi0a zsGgv2Hr7;CH}aly!rVB3&LwEDkEzz36la+ge*M> zEvYe2HVfk2{NLH(xD)?3Q*UZSjj8p~460XDQqE(RLtgGD+BLdKDle<=wENZ2> zFKVs0 zIPo^6;qSlZXb|*xeM!lFl`SQ8$k1WK)6xeop$=-C1;#jeAd^NeSLY!+2^cW6*{+c^?Bz@)vv0q zS+{;;ZC!msW3;LHl9txkrvJh?2&*QCUMNolgJe1EXjP=qEQc>m;LyXX*5-OSKlN_nKXj5}NsjLDr1EX2N|Bg;tZgwu|fQ+Un1m#OxkQ13zHOq{~ zHg2e?U2h`Yv}jY9+Knp(D%9g^E8{~!O>I35U-hIh)B$Nepr#@ikI?za3N$yhG}e&1 zZHhG3MI({*n3Xlx;+HTOplRc-p}CHR-$-jkBvKiv6a~^`i^kqY(#=S;uDP)yVy2 zTY;2f2^8}jQp^=Jf}CNgW&GDN(yf(HEmbsPiiKXO+2~TJmHNGybg2|7bUta&N;!Wx zqj|#yDrlwBqB!NVp+8HM`XHFh|1v1gIZAm>Bi&hI=?+pLbh z1XFTcXi_iUKppy&g=Wy2Ez^`Fx3VTyCZ>19i}BOCkFT6WGsevyJ#@tdi#E>ukoG%V zS?hW#|58x#-d|1|Vy^#~wf%yte)Cl0-M>3J9GbL}XudhAprL+Uzexwu52y3aq08lzxD7?flFU}?4sDnlP`PZ@F;=v z&;9)C^KVK2*)I=|7kI_?;)1$A-MZ~R4`&PfY~f{-e|bgIZ;c}}1pf54p4(n{>B^4d zkIWVLj}J{Bvpse0NAr%HEb#R|m^c0Ns)8Ss99b;zNvA&Z>$hIszh(WAWdc9`!f(b+ zUHrzrD~_xZ_}1qp|K!EH*WcW6qkFI{o+3azUHj|*x)N$bJgT8wg|j3_|v)N15Z46 z>KE4v9Gdd@jn`hW{MPfoxJBS6zd5ZwyX7BmHGT0Nfp6IJ$31sH`PTh6d~u(^w|t{% z>q})19D3l3hXj7<&RsX$T)+6oPk-^K!0Q8PJ%{@JSHJbe69PY$mlp|5?tbAPUpy`F zLzlMoKJ(70w~rclUf`sYPMrR9`i1Y#8hBaY-`o_fxjj4kdrJpi7kKzDjLSd%(N#w( z2Hq9;&*MhiRO$cY6W<*8K;Y-Mmei#@|HQSo4}2={jL6t`R*%{Js~-*=78q?#JNw^x zQM{c`qV&#}4;~zl&+izUOkKmBsv zJr#es$5<@zuCD#p7DV3ceo}om>-Uid$6fD#_VNS9DiME2X2WU!$h_$3kHyzd3}f|s z$KOAr=Ib{Nb5)A?KV4TpuJS|IYg1ht1U{?fqdRWDcjG;$xtavNV*NKBZEH>W_(Ioz z2;7qKMr_Bss~&A}Z4vlq`|iBprYkSF@+Q}{0x!Ji*8Tfd&fEWx>lT53*m2uGFMM?R zx1V)=N8mq{b=>~s-DkY{w(CBDCtthZ_j7-CXXjzpLjqrT_x5`m?|9~Kquq}R{NhvZ zuUgsj`tCgU69T`pXyV6XJ~_N~nfqyhJ6kXO&7Cir&#!eqFYsF{C$xQX{^;$S-7gDV z7|gou^&fS-bEo@tfp7icA1++^tKaQ<%>AywCokT4-|zn8&d+<@9|)Yj_`aRXmR<6b zKf6B_c-(i29{cF}2d_!;92R)SmeTvvKg#MgJ@SE|X?Zuk|Hu2%?>xyfRBS!mHS{|> zKEE^ly^v>=z|%fGY0dAu9?iw@a<<~qXi}(kQ|HGp<{eJ9sKl3aW_;)RDJzm>$>z~uS z%LG35SW(qj6Ky`$P;sIu?JR9!JS+O-J=M8~+cG2BXH+_YJifTDIW`N`{%?JlHtJ|BG->CeFqPH}H2J6bjNCt>rPjiQ6Bg2>h(^q)b1y2D zw*P3ytk6uA;rO9K3Qe<(b`Z_B)ETMYFu#$LlRCqaBW=Uw(rkz17i?!TexBa6w)$FE4L)-kiL-dGqq-=jG=WT@H)sBw{5b`47R(LK&6_)W?wq-E=gylue{TNVg1HOk1?T0>n>}yN zyt(t{&6__je_p}71@nXR^XAW}y+`n?aJoJ^6SKYsLzvk-mz3qO_d(i!%+3!8% z{-^hI*U+gaow9tz4d4Cl-EG(0eCs_wc>Kyol9E&AFFfVkPkMjl9hEUZ|J>3`zxVxz zpPbh>@~UrL|6T8};Uh*)%bQ!cI8PuE(EvI%&wzKxW~oOU}N3=kqUgB##|8aq=mrzW2dL1N)xwnv*6^$(d77SbWy9 zl9i!zSD%03#pM-|wdv%NX~xkHwF<__JQpE5etpK;QX1)d6jN^rDqg6DXj ztEj;HHD9hbH8~}@$eijOnlj&0=o^>p9hzLSG=I*pImxs9sYz2jnSOVB?>Xt7{2Bh~ zqsL7dKWa?Maw=-^u(8RhNyYxDDa}JpJ7s#(Nxsyivy)uDbdPVyd46~1niGossqOb) zJaNg8)TCj@6(*%7jT%40JEr}|Cs(c z_Y9{}7bK@{olr^Hz=i1ymz5Ue_y{^rD?SG%P%$w?UUp`{-vW4wGJ2}ba z4f&3r>)t$khPQI)xvA~nFUTA=!<&-q9-h>G$K?mSBRs=AE#9)Eppo@ks-c;lR9`{z_@tEN_J3|kGF%>?FDc2Loa9eV8Id}E$k?IdhNTZ5n&wUSj2wAf z%4pXZZ-#5EXI%2}uJP`y(Wd7_&&(mSTtRQ1d$wz*_W}3!ybt>S>Hgez#QlY5Af>A{ zcFlG71WQ+6bM3bAZwycS+Op3N&&oaZ!i&n@*>c_WH{SHXBai=N-!sqs;>~yIO*~%F z0rCq^TDbJAi?&=(k&iz9lV^VM;!E%T(XhJ5N$4LJS4Otnc;{Wuz4+3w5z`7!TC%kC zf(tJ$tBhQC;{%js-*a!h`^WxaBbF?!jI?k0!4psQyz<-rzg~XDHTT{BRL{O=UwZBL z#oM2J;h7g-TDp8i>3J8Iee3!g9(inc&rhFu_O~NOkGbH&zy1A-f%c6JZ@x7=tF~@@ zX4yBs`Td8#{*xz0k2xXhj5C+7KqvX;r9ayD@++_R|Mj!R=nYNHw@#UryYu1QJTTYR~`lSt7rQre%I*fg~L z)p48CeeG|jeEeNce#+(xN44+qx4#iM$&;E?;6KAZG^uIG37+%4=cTk?o-sakbjmVs z`_)Mg-ZylNH*be`^J`O*hx&Z&_or|EEZJpFPomiCyzNhUj`ySuv#dC_Yu40M$$8At z6TR9xITt%Rrn080rYTAj(YXm`d~$mfyCY4sV_lD3SlUIp1Y^sZnAj|)0@u(EDs3xa z!yWT9dEOYDHEUrS4?8jlCtc!r4&x8a9dVODVmYSKHa5*hG3yg^J{t1`z{8`Ldrjj; zU%7G7aUI6UF=p0Kvpnmg8Rm)8gB>T<-CsV#y{m7=37_}PG`=wBe|MmK{t?&P^IfSE z^0SA%o&VtQ@Pgcow-*G*zkSvxC%nD9Xzq@4P6)rfvVUE8)rz{eSKajlJ^BAq_}s{^ zcARUxHlft$+p+rl?@T!VgLmJ)p!Z<-Leu#0!hYAK7tu$Nl8u=(Cvnq%uHqrVK)NeJ z%H?*uypvof9Dl)(!ju$OhS!xsL#OXV&q*oMGhAjqW$^k*iIP*@nXW=)_xdSGs(YNv z?Os4*tk+FS>pH>haSZ|XQ3BT}_h=e{DK*jdyOKSr?h{-mQQo1HGl$3%RgaIR;n>rwmD6>OP+6xm@|fT~wlPh$}nA zwbtuOBBtD9-Cj?+cNqOla?w|5J%)F@C)0g`yU6WI_Pg9eQe4!PT+Qx@u1h>#cZw^? z^Lt`|xJ?E{w?8S>?Fwe)d4s85&y)pVD*$>tK@W8FU2ZSHX+hqq?e`Zm4HfNJT@p*3kIm9dB}<*)FbF(l91 z=9-^2jcS_e$s@YSu7#d#pUZ!WYp8o}3dzjtc9nVDUJ{GzE|v@-Pt_nYq_N3LL zPDUw(%ctCAY%ZW!tLoh2&-#!$Zq_Au)4k}@E*cG@k{=BlJIaF zUP74E$8y0JUE&#N4k)4b{$?;BznU=X!wyndj;q;Ph~Hb1DobBzlhSCpH^oX=oPkT5evHGq(oVtdk5BBgMQ@E*U!sGJTM_BO>@K-tx zXCI**kElrUIGlaN1XcVU#2o9vY??oqh*U0J`SyH$s$4zaG|pu6sT!8+PLMB;%FDA- z#AReJ@6b;Pyc(8!mfkT?^ETkJm9j%3dB?r9wn0)_Hm?@!ntqbm0)93 z8oOubqk#B;DTIP+tt|2c)Tjj@N*Yom<$6bEMy-$XbOpRqy0hY+1a>4y#y zK5UTiMhedcGvw_2;J$jP40eQXq3}ffuch!r^<75c6BE!$qwqxh54VN)z2!2dsJ!1G zKcIfw!ylk9rehEP-XP%*4if(TLBby%Bs`V){!I#JJwNGfmw4ARszYRsav_ShhwC_< z?4W7Obpxfh&nFSZ;{*E$JMg5W+HSL>2!Dw3+imdA33HnvUQcgo*%3^!KlnzV8q5@#AEgIA9n{WP-;;DL; z`!agJqV%J!)P|v_kJ{vhQ);zbHQu9qnJORT!FJjnj(OvWDoro(X{z7ot2qh6&!;fv zrHcl6{{1}?2+Jh!=lJ*x60->NniyEulY^u;Ja4#`;+c*;{5o5Buht%4Bl;6TShbDq6rJ0#V?c7vdcb>lB8O%k?HrVY{OOfp^Icdw^DS>m$(ipsqi4TMB!-rMB%3n z625Yf@UlU|*AEhIRES4$%EYm_LHA)BW8z5*BU-niY(rgDnIUdL3rIJ6={9p&ls1Ct zE*YM462Wu}wM^a=6c4?KFuIZ0*Kmi1_h>jc$4Rd{--%Z*aN_inoVfL5C+^np zehv3&IDMfrf02f}HQcA+^i!Pq_m(*E_SH_@t>J?jE;!GbzC^?A8s4ShehvF!D9LgA zmT0(I!`n36r{O~y-hYvkes#GMZ`W|QhU+Vx=}XWd$uYkjO-|ge3>L@ly-bVOaKRPM z_<9X**YGa&fGE@7ui=9l4sLbQFVb+kvWrZwTf-T*I`g-0cj8_R*Wd1pKX8W=7u==c z?>g~T4ex)z8Q=b(6L)L4Ps8b05Rl{gwQBf)hW%a6^aUDj)$lG2AJA~l_nq`sYj}@_ z4{A8$VQ2ng4R6)(9t|JVa6z||UbTj|Yk0qg_depxpRV@0SiU(L-lO3|8qWEllU|XA z+cmsh!@)TQ$62!-q7S^Q1F>xrVoCm@eQ7$NV1DaK=;4_yP@A zYk0ecdo^tIIO%QGaKT<@e6@!AHEjOWnZ8xS-5NgdoHKpDhJ(Lw##d{2yN35`*!ZP0 ze~yMrG~BA;T^jDy@F5MG&pXR2)^MwacWHR9h7V}CU&HAyILixaxLCv08gAF{b`9^* zaIc2@HSB-U$&abw0u8U$aI1#5X}DX%y&68KVdEu@9}O32xLm^rH0pyBF|obl-&J8_AIw`sUn!|9(m^RL$ME)DxXb*48pT&&@84R6(O zhlaNgIO)r;9L4Q3?<%sqbJQ*)<01{O)^NRsw`#as!~GieyIg`nE-$FzVhz`8c&mn6 zeNKAmL!7t+2Y=+aJOc+&Hr%e^?HcaZ@O};VY1kO9poWc+PI}#=oj51MiHkH`t>LX2?$Gca4fksJpoTNXI?E4gc)xsH%xcd*4X0;1 z^XF)|Si|KSHnN=gizhhofk{q$NW(dko$=KgE}G(u@6B=IgBnhs=8VtLaEXSiHGDwB z<yN0_p+^69~8a^0HGC-FnZBsNiQ6^2U&F=%XZm^# zcNaS24{3PoNzVBF8V<_GGp+u*TEq1k-lgGw4d*O$me---gBmtZai%ZQ@O}*+(r}JC zqvi3Vf02`3$LUVoy4Z=gX*jsV8Q-np>NA}22Q^%NrZYZR?8Ma?&RFV<4{G>;hPR!i zrT>}}AJT9+F0$Ch(~jj%ylaIMcb91RYz?2|#2qW0xL3o~HO}~58tz}`j4!Tr;&Kfe zmpJ41;=+%ue*4>=X_iH%)I%j;jhTE@q#%J8<#N`^^ zuHpR}KBVE+o1FCaYWR?byT9#Be?Y_i8m_<9nZ85AdohKkK6UaJ)Nr|mt2JD&;Z_ZA)$n!=@6zxd4e!_R0SzD2@F5NR zKXcZvK*OswT(03(4R6=*9u4tNW&!>F4u6qhTAo~O~V}; z?$+>L4Ij{OzlQnSNxUD$-&|r`@P$(zB^oZ*aJ`24oAykPzva%DzahtXua=*`&BpP4 zT711aKTM@P6>wS*#_o!I2O2#>Y1CgXpm>fG*u2t-gV#E7xs4vu?^+*^$5rOl>Vp_4 z54hEq9=Kh@{okQLa!8Lm>-+Blk{DN^t9J+};*;rTw_1N7J@8f=h79^_IEAn=t0~gj zWX!6J;S&Mmi_Z~6=o1Nb#;iv2DMH4^HVQwyv8KY96|JV!^qwY4Q->^|^e;Kc9{<$! zZo>p1$A0&7DnJHX`?m@jMf3wHMyi0`U&!?A=Vz-3<*`0+Xd3-M4zwAZpVvjoKZpc2 zr{|wZap0Zv_bWYPzgy+y`f>hr6+W5#A@t9!Fh94ze!mJwe(=fpGgLU*8f9~Sey)N2 y7L|@YDIG@g-3ZVJ&d<+Xu+Pt3KnLNteCB^Xg>l_Q0`;eQmZVs052W9E`Tqx}L9|=| literal 113320 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 diff --git a/programs/guinea/Cargo.toml b/programs/guinea/Cargo.toml index 9885dbb73..faf9d0756 100644 --- a/programs/guinea/Cargo.toml +++ b/programs/guinea/Cargo.toml @@ -16,3 +16,4 @@ bincode = { workspace = true } serde = { workspace = true } solana-program = { workspace = true } +magicblock-magic-program-api = { workspace = true } diff --git a/programs/guinea/src/lib.rs b/programs/guinea/src/lib.rs index 190341e5c..1dcddcd86 100644 --- a/programs/guinea/src/lib.rs +++ b/programs/guinea/src/lib.rs @@ -1,13 +1,17 @@ #![allow(unexpected_cfgs)] use core::slice; +use magicblock_magic_program_api::{ + args::ScheduleTaskArgs, instruction::MagicBlockInstruction, +}; use serde::{Deserialize, Serialize}; use solana_program::{ account_info::{next_account_info, AccountInfo}, declare_id, entrypoint::{self, ProgramResult}, + instruction::{AccountMeta, Instruction}, log, - program::set_return_data, + program::{invoke, set_return_data}, program_error::ProgramError, pubkey::Pubkey, }; @@ -20,8 +24,11 @@ pub enum GuineaInstruction { ComputeBalances, PrintSizes, WriteByteToData(u8), + Increment, Transfer(u64), Resize(usize), + ScheduleTask(ScheduleTaskArgs), + CancelTask(u64), } fn compute_balances(accounts: slice::Iter) { @@ -57,6 +64,16 @@ fn write_byte_to_data( Ok(()) } +fn increment(accounts: slice::Iter) -> ProgramResult { + for a in accounts { + let mut data = a.try_borrow_mut_data()?; + let first = + data.first_mut().ok_or(ProgramError::AccountDataTooSmall)?; + *first += 1; + } + Ok(()) +} + fn transfer( mut accounts: slice::Iter, lamports: u64, @@ -80,6 +97,46 @@ fn transfer( Ok(()) } +fn schedule_task( + mut accounts: slice::Iter, + args: ScheduleTaskArgs, +) -> ProgramResult { + let _magic_program_info = next_account_info(&mut accounts)?; + let payer_info = next_account_info(&mut accounts)?; + let counter_pda_info = next_account_info(&mut accounts)?; + + let ix = Instruction::new_with_bincode( + magicblock_magic_program_api::ID, + &MagicBlockInstruction::ScheduleTask(args), + vec![ + AccountMeta::new(*payer_info.key, true), + AccountMeta::new(*counter_pda_info.key, false), + ], + ); + + invoke(&ix, &[payer_info.clone(), counter_pda_info.clone()])?; + + Ok(()) +} + +fn cancel_task( + mut accounts: slice::Iter, + task_id: u64, +) -> ProgramResult { + let _magic_program_info = next_account_info(&mut accounts)?; + let payer_info = next_account_info(&mut accounts)?; + + let ix = Instruction::new_with_bincode( + magicblock_magic_program_api::ID, + &MagicBlockInstruction::CancelTask { task_id }, + vec![AccountMeta::new(*payer_info.key, true)], + ); + + invoke(&ix, &[payer_info.clone()])?; + + Ok(()) +} + fn process_instruction( _program_id: &Pubkey, accounts: &[AccountInfo], @@ -100,8 +157,15 @@ fn process_instruction( GuineaInstruction::WriteByteToData(byte) => { write_byte_to_data(accounts, byte)? } + GuineaInstruction::Increment => increment(accounts)?, GuineaInstruction::Transfer(lamports) => transfer(accounts, lamports)?, GuineaInstruction::Resize(size) => resize_account(accounts, size)?, + GuineaInstruction::ScheduleTask(request) => { + schedule_task(accounts, request)? + } + GuineaInstruction::CancelTask(task_id) => { + cancel_task(accounts, task_id)? + } } Ok(()) } diff --git a/programs/magicblock/src/schedule_task/process_schedule_task.rs b/programs/magicblock/src/schedule_task/process_schedule_task.rs index 2fc1147f0..182f1b0e0 100644 --- a/programs/magicblock/src/schedule_task/process_schedule_task.rs +++ b/programs/magicblock/src/schedule_task/process_schedule_task.rs @@ -70,7 +70,7 @@ pub(crate) fn process_schedule_task( return Err(InstructionError::InvalidInstructionData); } - // Enforce minimal number of executions + // Enforce minimal number of instructions if args.instructions.is_empty() { ic_msg!( invoke_context, diff --git a/test-integration/Cargo.lock b/test-integration/Cargo.lock index 5cf894420..68a746ac7 100644 --- a/test-integration/Cargo.lock +++ b/test-integration/Cargo.lock @@ -2271,6 +2271,7 @@ name = "guinea" version = "0.2.3" dependencies = [ "bincode", + "magicblock-magic-program-api 0.2.3", "serde", "solana-program", ] diff --git a/test-kit/src/lib.rs b/test-kit/src/lib.rs index 555542ae9..7290f82f3 100644 --- a/test-kit/src/lib.rs +++ b/test-kit/src/lib.rs @@ -37,6 +37,9 @@ use solana_transaction::Transaction; use solana_transaction_status_client_types::TransactionStatusMeta; use tempfile::TempDir; +const NOOP_PROGRAM_ID: Pubkey = + Pubkey::from_str_const("noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV"); + /// A simulated validator backend for integration tests. /// /// This struct encapsulates all the core components of a validator, including @@ -132,6 +135,12 @@ impl ExecutionTestEnv { "../programs/elfs/guinea.so".into(), )]) .expect("failed to load test programs into test env"); + scheduler_state + .load_upgradeable_programs(&[( + NOOP_PROGRAM_ID, + "../test-integration/programs/noop/noop.so".into(), + )]) + .expect("failed to load test programs into test env"); // Start the transaction processing backend. TransactionScheduler::new(1, scheduler_state).spawn(); From 57ec93af0114184f3dd72fbac65dfea4e45baa3f Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Wed, 5 Nov 2025 16:04:13 +0100 Subject: [PATCH 05/17] fix: integration tests --- magicblock-committor-program/Cargo.toml | 2 ++ test-integration/programs/flexi-counter/src/processor.rs | 2 +- test-integration/programs/schedulecommit/Cargo.toml | 2 ++ test-integration/test-ledger-restore/src/lib.rs | 5 +---- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/magicblock-committor-program/Cargo.toml b/magicblock-committor-program/Cargo.toml index 15164f42a..b37ba7c44 100644 --- a/magicblock-committor-program/Cargo.toml +++ b/magicblock-committor-program/Cargo.toml @@ -29,3 +29,5 @@ doctest = false [features] no-entrypoint = [] default = [] +custom-heap = [] +custom-panic = [] diff --git a/test-integration/programs/flexi-counter/src/processor.rs b/test-integration/programs/flexi-counter/src/processor.rs index 22d242e83..8bf826635 100644 --- a/test-integration/programs/flexi-counter/src/processor.rs +++ b/test-integration/programs/flexi-counter/src/processor.rs @@ -479,7 +479,7 @@ fn process_cancel_task( vec![AccountMeta::new(*payer_info.key, true)], ); - invoke(&ix, &[payer_info.clone(), task_context_info.clone()])?; + invoke(&ix, &[payer_info.clone()])?; Ok(()) } diff --git a/test-integration/programs/schedulecommit/Cargo.toml b/test-integration/programs/schedulecommit/Cargo.toml index d850f923f..59c45c378 100644 --- a/test-integration/programs/schedulecommit/Cargo.toml +++ b/test-integration/programs/schedulecommit/Cargo.toml @@ -16,3 +16,5 @@ crate-type = ["cdylib", "lib"] no-entrypoint = [] cpi = ["no-entrypoint"] default = [] +custom-heap = [] +custom-panic = [] diff --git a/test-integration/test-ledger-restore/src/lib.rs b/test-integration/test-ledger-restore/src/lib.rs index c60734075..64acf8e38 100644 --- a/test-integration/test-ledger-restore/src/lib.rs +++ b/test-integration/test-ledger-restore/src/lib.rs @@ -152,10 +152,7 @@ pub fn setup_validator_with_local_remote_and_resume_strategy( }, accounts: accounts_config.clone(), programs, - task_scheduler: TaskSchedulerConfig { - reset: true, - ..Default::default() - }, + task_scheduler: TaskSchedulerConfig { reset: true }, ..Default::default() }; // Fund validator on chain From 7a9aced0ac7f9df3aa213d3b82f2a41756bd27ed Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Wed, 5 Nov 2025 20:45:47 +0400 Subject: [PATCH 06/17] feat: improve the TLS stash API --- magicblock-magic-program-api/src/tls.rs | 23 ++++++++++++++----- .../src/executor/processing.rs | 22 ++++++++---------- .../src/schedule_task/process_cancel_task.rs | 9 ++------ .../schedule_task/process_schedule_task.rs | 9 ++------ 4 files changed, 31 insertions(+), 32 deletions(-) diff --git a/magicblock-magic-program-api/src/tls.rs b/magicblock-magic-program-api/src/tls.rs index 8f2c6ba70..464f02417 100644 --- a/magicblock-magic-program-api/src/tls.rs +++ b/magicblock-magic-program-api/src/tls.rs @@ -4,18 +4,29 @@ use crate::args::TaskRequest; #[derive(Default, Debug)] pub struct ExecutionTlsStash { - pub tasks: VecDeque, + tasks: VecDeque, // TODO(bmuddha/taco-paco): intents should go in here - pub intents: VecDeque<()>, + intents: VecDeque<()>, } thread_local! { - pub static EXECUTION_TLS_STASH: RefCell = RefCell::default(); + static EXECUTION_TLS_STASH: RefCell = RefCell::default(); } impl ExecutionTlsStash { - pub fn clear(&mut self) { - self.tasks.clear(); - self.intents.clear(); + pub fn register_task(task: TaskRequest) { + EXECUTION_TLS_STASH + .with_borrow_mut(|stash| stash.tasks.push_back(task)); + } + + pub fn next_task() -> Option { + EXECUTION_TLS_STASH.with_borrow_mut(|stash| stash.tasks.pop_front()) + } + + pub fn clear() { + EXECUTION_TLS_STASH.with_borrow_mut(|stash| { + stash.tasks.clear(); + stash.intents.clear(); + }) } } diff --git a/magicblock-processor/src/executor/processing.rs b/magicblock-processor/src/executor/processing.rs index ef4b78a37..2ee2b6d57 100644 --- a/magicblock-processor/src/executor/processing.rs +++ b/magicblock-processor/src/executor/processing.rs @@ -7,7 +7,7 @@ use magicblock_core::link::{ }, }; use magicblock_metrics::metrics::FAILED_TRANSACTIONS_COUNT; -use magicblock_program::tls::EXECUTION_TLS_STASH; +use magicblock_program::tls::ExecutionTlsStash; use solana_pubkey::Pubkey; use solana_svm::{ account_loader::{AccountsBalances, CheckedTransactionDetails}, @@ -83,22 +83,20 @@ impl super::TransactionExecutor { // If the transaction succeeded, check for potential tasks/intents // that may have been scheduled during the transaction execution if result.is_ok() { - EXECUTION_TLS_STASH.with(|stash| { - for task in stash.borrow_mut().tasks.drain(..) { - // This is a best effort send, if the tasks service has terminated - // for some reason, logging is the best we can do at this point - let _ = self.tasks_tx.send(task).inspect_err(|_| - warn!("Scheduled tasks service has hung up and longer running") - ); - } - }); + while let Some(task) = ExecutionTlsStash::next_task() { + // This is a best effort send, if the tasks service has terminated + // for some reason, logging is the best we can do at this point + let _ = self.tasks_tx.send(task).inspect_err(|_| + warn!("Scheduled tasks service has hung up and longer running") + ); + } } } result }); // Make sure that no matter what happened to the transaction we clear the stash - EXECUTION_TLS_STASH.with(|s| s.borrow_mut().clear()); + ExecutionTlsStash::clear(); // Send the final result back to the caller if they are waiting. tx.map(|tx| tx.send(result)); @@ -146,7 +144,7 @@ impl super::TransactionExecutor { }; // Make sure that we clear the stash, so that simulations // don't interfere with actual transaction executions - EXECUTION_TLS_STASH.with(|s| s.borrow_mut().clear()); + ExecutionTlsStash::clear(); let _ = tx.send(result); } diff --git a/programs/magicblock/src/schedule_task/process_cancel_task.rs b/programs/magicblock/src/schedule_task/process_cancel_task.rs index 92fb6b095..f8aeec33d 100644 --- a/programs/magicblock/src/schedule_task/process_cancel_task.rs +++ b/programs/magicblock/src/schedule_task/process_cancel_task.rs @@ -2,7 +2,7 @@ use std::collections::HashSet; use magicblock_magic_program_api::{ args::{CancelTaskRequest, TaskRequest}, - tls::EXECUTION_TLS_STASH, + tls::ExecutionTlsStash, }; use solana_log_collector::ic_msg; use solana_program_runtime::invoke_context::InvokeContext; @@ -40,12 +40,7 @@ pub(crate) fn process_cancel_task( }; // Add cancel request to execution TLS stash - EXECUTION_TLS_STASH.with(|stash| { - stash - .borrow_mut() - .tasks - .push_back(TaskRequest::Cancel(cancel_request)) - }); + ExecutionTlsStash::register_task(TaskRequest::Cancel(cancel_request)); ic_msg!( invoke_context, diff --git a/programs/magicblock/src/schedule_task/process_schedule_task.rs b/programs/magicblock/src/schedule_task/process_schedule_task.rs index 182f1b0e0..825b6200d 100644 --- a/programs/magicblock/src/schedule_task/process_schedule_task.rs +++ b/programs/magicblock/src/schedule_task/process_schedule_task.rs @@ -2,7 +2,7 @@ use std::collections::HashSet; use magicblock_magic_program_api::{ args::{ScheduleTaskArgs, ScheduleTaskRequest, TaskRequest}, - tls::EXECUTION_TLS_STASH, + tls::ExecutionTlsStash, }; use solana_log_collector::ic_msg; use solana_program_runtime::invoke_context::InvokeContext; @@ -111,12 +111,7 @@ pub(crate) fn process_schedule_task( }; // Add schedule request to execution TLS stash - EXECUTION_TLS_STASH.with(|stash| { - stash - .borrow_mut() - .tasks - .push_back(TaskRequest::Schedule(schedule_request)); - }); + ExecutionTlsStash::register_task(TaskRequest::Schedule(schedule_request)); ic_msg!( invoke_context, From 93f27c98bd0cb677e165e5fccd8b702564fe88ca Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Fri, 7 Nov 2025 19:13:25 +0100 Subject: [PATCH 07/17] test: fix flakyness --- magicblock-task-scheduler/tests/service.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/magicblock-task-scheduler/tests/service.rs b/magicblock-task-scheduler/tests/service.rs index 29b6ef448..f8cdc3f29 100644 --- a/magicblock-task-scheduler/tests/service.rs +++ b/magicblock-task-scheduler/tests/service.rs @@ -74,11 +74,17 @@ pub async fn test_schedule_task() -> TaskSchedulerResult<()> { // Wait the task scheduler to receive the task tokio::time::sleep(Duration::from_millis(10)).await; - assert_eq!( - env.get_account(account.pubkey()).data().first(), - Some(&1), - "the first byte of the account data should have been modified" - ); + // Wait until the task scheduler actually mutates the account (with an upper bound to avoid hangs) + tokio::time::timeout(Duration::from_secs(1), async { + loop { + if env.get_account(account.pubkey()).data().first() == Some(&1) { + break; + } + tokio::time::sleep(Duration::from_millis(20)).await; + } + }) + .await + .expect("task scheduler never incremented the account within 1s"); token.cancel(); handle.await.unwrap().unwrap(); From ebfee5be7ae8edb6db32dd2ca9c088572ae56dfc Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Fri, 7 Nov 2025 19:19:29 +0100 Subject: [PATCH 08/17] test: fix flakyness --- magicblock-task-scheduler/tests/service.rs | 49 ++++++++++++++++++---- programs/magicblock/src/validator.rs | 10 +++++ 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/magicblock-task-scheduler/tests/service.rs b/magicblock-task-scheduler/tests/service.rs index f8cdc3f29..9b7b51cb0 100644 --- a/magicblock-task-scheduler/tests/service.rs +++ b/magicblock-task-scheduler/tests/service.rs @@ -3,7 +3,7 @@ use std::time::Duration; use guinea::GuineaInstruction; use magicblock_config::TaskSchedulerConfig; use magicblock_program::{ - args::ScheduleTaskArgs, validator::init_validator_authority, + args::ScheduleTaskArgs, validator::init_validator_authority_if_needed, }; use magicblock_task_scheduler::{ errors::TaskSchedulerResult, SchedulerDatabase, TaskSchedulerService, @@ -22,7 +22,7 @@ pub async fn test_schedule_task() -> TaskSchedulerResult<()> { let account = env.create_account_with_config(LAMPORTS_PER_SOL, 1, guinea::ID); - init_validator_authority(env.payer.insecure_clone()); + init_validator_authority_if_needed(env.payer.insecure_clone()); let token = CancellationToken::new(); let task_scheduler_db_path = SchedulerDatabase::path( @@ -98,7 +98,7 @@ pub async fn test_cancel_task() -> TaskSchedulerResult<()> { let account = env.create_account_with_config(LAMPORTS_PER_SOL, 1, guinea::ID); - init_validator_authority(env.payer.insecure_clone()); + init_validator_authority_if_needed(env.payer.insecure_clone()); let token = CancellationToken::new(); let task_scheduler_db_path = SchedulerDatabase::path( @@ -148,8 +148,22 @@ pub async fn test_cancel_task() -> TaskSchedulerResult<()> { result ); - // Wait the task scheduler to execute 10 times (+some to register the task) - tokio::time::sleep(Duration::from_millis(5 * interval)).await; + // Wait until we actually observe at least five executions + let executed_before_cancel = + tokio::time::timeout(Duration::from_secs(2), async { + loop { + if let Some(value) = + env.get_account(account.pubkey()).data().first() + { + if *value >= 5 { + break *value; + } + } + tokio::time::sleep(Duration::from_millis(20)).await; + } + }) + .await + .expect("task scheduler never reached five executions within 2s"); // Cancel the task let ix = Instruction::new_with_bincode( @@ -168,13 +182,30 @@ pub async fn test_cancel_task() -> TaskSchedulerResult<()> { result ); - // Wait the task scheduler to cancel the task and make sure it didn't execute again + let value_at_cancel = env + .get_account(account.pubkey()) + .data() + .first() + .copied() + .unwrap_or_default(); + assert!( + value_at_cancel >= executed_before_cancel, + "value regressed before cancellation" + ); + + // Ensure the scheduler stops issuing executions after cancellation tokio::time::sleep(Duration::from_millis(2 * interval)).await; + let value_after_cancel = env + .get_account(account.pubkey()) + .data() + .first() + .copied() + .unwrap_or_default(); + assert_eq!( - env.get_account(account.pubkey()).data().first(), - Some(&5), - "the first byte of the account data should have been modified" + value_after_cancel, value_at_cancel, + "task scheduler kept executing after cancellation" ); token.cancel(); diff --git a/programs/magicblock/src/validator.rs b/programs/magicblock/src/validator.rs index 8a750c95d..2af460fdb 100644 --- a/programs/magicblock/src/validator.rs +++ b/programs/magicblock/src/validator.rs @@ -51,6 +51,16 @@ pub fn init_validator_authority(keypair: Keypair) { validator_authority_lock.replace(keypair); } +pub fn init_validator_authority_if_needed(keypair: Keypair) { + let mut validator_authority_lock = VALIDATOR_AUTHORITY + .write() + .expect("RwLock VALIDATOR_AUTHORITY poisoned"); + if validator_authority_lock.as_ref().is_some() { + return; + } + validator_authority_lock.replace(keypair); +} + pub fn generate_validator_authority_if_needed() { let mut validator_authority_lock = VALIDATOR_AUTHORITY .write() From 649f069cf1552cb3b47d4bfd8a3401b132cf3153 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Fri, 7 Nov 2025 19:21:28 +0100 Subject: [PATCH 09/17] feat: add validation --- programs/guinea/src/lib.rs | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/programs/guinea/src/lib.rs b/programs/guinea/src/lib.rs index 1dcddcd86..33b39859f 100644 --- a/programs/guinea/src/lib.rs +++ b/programs/guinea/src/lib.rs @@ -101,10 +101,18 @@ fn schedule_task( mut accounts: slice::Iter, args: ScheduleTaskArgs, ) -> ProgramResult { - let _magic_program_info = next_account_info(&mut accounts)?; + let magic_program_info = next_account_info(&mut accounts)?; let payer_info = next_account_info(&mut accounts)?; let counter_pda_info = next_account_info(&mut accounts)?; + if magic_program_info.key != &magicblock_magic_program_api::ID { + return Err(ProgramError::InvalidAccountData); + } + + if !payer_info.is_signer { + return Err(ProgramError::MissingRequiredSignature); + } + let ix = Instruction::new_with_bincode( magicblock_magic_program_api::ID, &MagicBlockInstruction::ScheduleTask(args), @@ -123,9 +131,17 @@ fn cancel_task( mut accounts: slice::Iter, task_id: u64, ) -> ProgramResult { - let _magic_program_info = next_account_info(&mut accounts)?; + let magic_program_info = next_account_info(&mut accounts)?; let payer_info = next_account_info(&mut accounts)?; + if magic_program_info.key != &magicblock_magic_program_api::ID { + return Err(ProgramError::InvalidAccountData); + } + + if !payer_info.is_signer { + return Err(ProgramError::MissingRequiredSignature); + } + let ix = Instruction::new_with_bincode( magicblock_magic_program_api::ID, &MagicBlockInstruction::CancelTask { task_id }, From b28286885c01662b9f3a1bd64fb039612b4d3154 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Fri, 7 Nov 2025 19:22:32 +0100 Subject: [PATCH 10/17] docs: fix typo --- magicblock-processor/src/executor/processing.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/magicblock-processor/src/executor/processing.rs b/magicblock-processor/src/executor/processing.rs index 2ee2b6d57..4fa2323ed 100644 --- a/magicblock-processor/src/executor/processing.rs +++ b/magicblock-processor/src/executor/processing.rs @@ -87,7 +87,7 @@ impl super::TransactionExecutor { // This is a best effort send, if the tasks service has terminated // for some reason, logging is the best we can do at this point let _ = self.tasks_tx.send(task).inspect_err(|_| - warn!("Scheduled tasks service has hung up and longer running") + warn!("Scheduled tasks service has hung up and is longer running") ); } } From 6b055f083460ec21630802bee4a7fae1f990e995 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Sun, 9 Nov 2025 23:55:13 +0100 Subject: [PATCH 11/17] docs: add todo --- magicblock-processor/src/executor/processing.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/magicblock-processor/src/executor/processing.rs b/magicblock-processor/src/executor/processing.rs index 4fa2323ed..df86b4911 100644 --- a/magicblock-processor/src/executor/processing.rs +++ b/magicblock-processor/src/executor/processing.rs @@ -77,7 +77,8 @@ impl super::TransactionExecutor { // Otherwise commit the account state changes self.commit_accounts(feepayer, &processed, is_replay); - // Commit transaction to the ledger and schedule tasks and intents (if any) + // Commit transaction to the ledger and schedule tasks + // TODO: send intents here as well once implemented if !is_replay { self.commit_transaction(txn, processed, balances); // If the transaction succeeded, check for potential tasks/intents From 0e60434505f7750b6e4068a24dbcfb16478e5e70 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Mon, 10 Nov 2025 00:07:48 +0100 Subject: [PATCH 12/17] docs: clarify error --- magicblock-task-scheduler/tests/service.rs | 56 ++++++++++------------ 1 file changed, 25 insertions(+), 31 deletions(-) diff --git a/magicblock-task-scheduler/tests/service.rs b/magicblock-task-scheduler/tests/service.rs index 9b7b51cb0..7d8638a5e 100644 --- a/magicblock-task-scheduler/tests/service.rs +++ b/magicblock-task-scheduler/tests/service.rs @@ -6,7 +6,8 @@ use magicblock_program::{ args::ScheduleTaskArgs, validator::init_validator_authority_if_needed, }; use magicblock_task_scheduler::{ - errors::TaskSchedulerResult, SchedulerDatabase, TaskSchedulerService, + errors::TaskSchedulerResult, SchedulerDatabase, TaskSchedulerError, + TaskSchedulerService, }; use solana_account::ReadableAccount; use solana_program::{ @@ -14,13 +15,15 @@ use solana_program::{ native_token::LAMPORTS_PER_SOL, }; use test_kit::{ExecutionTestEnv, Signer}; +use tokio::task::JoinHandle; use tokio_util::sync::CancellationToken; -#[tokio::test] -pub async fn test_schedule_task() -> TaskSchedulerResult<()> { +fn setup() -> TaskSchedulerResult<( + ExecutionTestEnv, + CancellationToken, + JoinHandle>, +)> { let mut env = ExecutionTestEnv::new(); - let account = - env.create_account_with_config(LAMPORTS_PER_SOL, 1, guinea::ID); init_validator_authority_if_needed(env.payer.insecure_clone()); @@ -44,6 +47,16 @@ pub async fn test_schedule_task() -> TaskSchedulerResult<()> { )? .start()?; + Ok((env, token, handle)) +} + +#[tokio::test] +pub async fn test_schedule_task() -> TaskSchedulerResult<()> { + let (env, token, handle) = setup()?; + + let account = + env.create_account_with_config(LAMPORTS_PER_SOL, 1, guinea::ID); + // Schedule a task let ix = Instruction::new_with_bincode( guinea::ID, @@ -94,32 +107,11 @@ pub async fn test_schedule_task() -> TaskSchedulerResult<()> { #[tokio::test] pub async fn test_cancel_task() -> TaskSchedulerResult<()> { - let mut env = ExecutionTestEnv::new(); + let (env, token, handle) = setup()?; + let account = env.create_account_with_config(LAMPORTS_PER_SOL, 1, guinea::ID); - init_validator_authority_if_needed(env.payer.insecure_clone()); - - let token = CancellationToken::new(); - let task_scheduler_db_path = SchedulerDatabase::path( - env.ledger - .ledger_path() - .parent() - .expect("ledger_path didn't have a parent, should never happen"), - ); - let handle = TaskSchedulerService::new( - &task_scheduler_db_path, - &TaskSchedulerConfig::default(), - env.transaction_scheduler.clone(), - env.dispatch - .tasks_service - .take() - .expect("Tasks service should be initialized"), - env.ledger.latest_block().clone(), - token.clone(), - )? - .start()?; - // Schedule a task let interval = 100; let ix = Instruction::new_with_bincode( @@ -189,9 +181,11 @@ pub async fn test_cancel_task() -> TaskSchedulerResult<()> { .copied() .unwrap_or_default(); assert!( - value_at_cancel >= executed_before_cancel, - "value regressed before cancellation" - ); + value_at_cancel >= executed_before_cancel, + "unexpected: value at cancellation ({}) < value when 5 executions were observed ({})", + value_at_cancel, + executed_before_cancel + ); // Ensure the scheduler stops issuing executions after cancellation tokio::time::sleep(Duration::from_millis(2 * interval)).await; From 1b4db562506bd1041bf3c483268ba098b4376ee3 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Mon, 10 Nov 2025 00:10:09 +0100 Subject: [PATCH 13/17] feat: checked add --- programs/guinea/src/lib.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/programs/guinea/src/lib.rs b/programs/guinea/src/lib.rs index 33b39859f..c64284876 100644 --- a/programs/guinea/src/lib.rs +++ b/programs/guinea/src/lib.rs @@ -69,7 +69,9 @@ fn increment(accounts: slice::Iter) -> ProgramResult { let mut data = a.try_borrow_mut_data()?; let first = data.first_mut().ok_or(ProgramError::AccountDataTooSmall)?; - *first += 1; + *first = first + .checked_add(1) + .ok_or(ProgramError::ArithmeticOverflow)?; } Ok(()) } From a83897fc8184bea5ab0e6d3b774caba903f9a8eb Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Mon, 10 Nov 2025 00:36:51 +0100 Subject: [PATCH 14/17] feat: update guinea --- programs/elfs/guinea.so | Bin 143600 -> 143720 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/programs/elfs/guinea.so b/programs/elfs/guinea.so index b9c6c4dbd7e43bebc5965d5bfb3c9a5d539835cd..62c6cedb4b653ad5f28a2473eb54d35b33c51701 100755 GIT binary patch delta 23551 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 Dr8gE# delta 24074 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 From 973781c9280a12c6bb38c01b395d44a00bda1c28 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Mon, 10 Nov 2025 00:51:06 +0100 Subject: [PATCH 15/17] fix: fund validator authority --- magicblock-task-scheduler/tests/service.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/magicblock-task-scheduler/tests/service.rs b/magicblock-task-scheduler/tests/service.rs index 7d8638a5e..c651ec415 100644 --- a/magicblock-task-scheduler/tests/service.rs +++ b/magicblock-task-scheduler/tests/service.rs @@ -3,7 +3,8 @@ use std::time::Duration; use guinea::GuineaInstruction; use magicblock_config::TaskSchedulerConfig; use magicblock_program::{ - args::ScheduleTaskArgs, validator::init_validator_authority_if_needed, + args::ScheduleTaskArgs, + validator::{init_validator_authority_if_needed, validator_authority_id}, }; use magicblock_task_scheduler::{ errors::TaskSchedulerResult, SchedulerDatabase, TaskSchedulerError, @@ -26,6 +27,9 @@ fn setup() -> TaskSchedulerResult<( let mut env = ExecutionTestEnv::new(); init_validator_authority_if_needed(env.payer.insecure_clone()); + // NOTE: validator authority is unique for all tests in this file, but the payer changes for each test + // Airdrop some SOL to the validator authority, which is used to pay task fees + env.fund_account(validator_authority_id(), LAMPORTS_PER_SOL); let token = CancellationToken::new(); let task_scheduler_db_path = SchedulerDatabase::path( @@ -84,9 +88,6 @@ pub async fn test_schedule_task() -> TaskSchedulerResult<()> { result ); - // Wait the task scheduler to receive the task - tokio::time::sleep(Duration::from_millis(10)).await; - // Wait until the task scheduler actually mutates the account (with an upper bound to avoid hangs) tokio::time::timeout(Duration::from_secs(1), async { loop { @@ -113,11 +114,12 @@ pub async fn test_cancel_task() -> TaskSchedulerResult<()> { env.create_account_with_config(LAMPORTS_PER_SOL, 1, guinea::ID); // Schedule a task + let task_id = 2; let interval = 100; let ix = Instruction::new_with_bincode( guinea::ID, &GuineaInstruction::ScheduleTask(ScheduleTaskArgs { - task_id: 1, + task_id, execution_interval_millis: interval, iterations: 100, instructions: vec![Instruction::new_with_bincode( @@ -160,7 +162,7 @@ pub async fn test_cancel_task() -> TaskSchedulerResult<()> { // Cancel the task let ix = Instruction::new_with_bincode( guinea::ID, - &GuineaInstruction::CancelTask(1), + &GuineaInstruction::CancelTask(task_id), vec![ AccountMeta::new_readonly(magicblock_magic_program_api::ID, false), AccountMeta::new(env.payer.pubkey(), true), From f94b077d04574ac7add1beb7ea0a37cdf46bacd2 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Mon, 10 Nov 2025 10:06:44 +0100 Subject: [PATCH 16/17] style: define setup type --- magicblock-task-scheduler/tests/service.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/magicblock-task-scheduler/tests/service.rs b/magicblock-task-scheduler/tests/service.rs index c651ec415..4c2d50799 100644 --- a/magicblock-task-scheduler/tests/service.rs +++ b/magicblock-task-scheduler/tests/service.rs @@ -19,11 +19,13 @@ use test_kit::{ExecutionTestEnv, Signer}; use tokio::task::JoinHandle; use tokio_util::sync::CancellationToken; -fn setup() -> TaskSchedulerResult<( +type SetupResult = TaskSchedulerResult<( ExecutionTestEnv, CancellationToken, JoinHandle>, -)> { +)>; + +fn setup() -> SetupResult { let mut env = ExecutionTestEnv::new(); init_validator_authority_if_needed(env.payer.insecure_clone()); From b52a038ce8c80fa0df6f59f9a29c6a2df2f98ba3 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Mon, 10 Nov 2025 10:08:31 +0100 Subject: [PATCH 17/17] style: replace unwrap with expect --- magicblock-task-scheduler/tests/service.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/magicblock-task-scheduler/tests/service.rs b/magicblock-task-scheduler/tests/service.rs index 4c2d50799..4c8f04809 100644 --- a/magicblock-task-scheduler/tests/service.rs +++ b/magicblock-task-scheduler/tests/service.rs @@ -103,7 +103,7 @@ pub async fn test_schedule_task() -> TaskSchedulerResult<()> { .expect("task scheduler never incremented the account within 1s"); token.cancel(); - handle.await.unwrap().unwrap(); + handle.await.expect("task service join handle failed")?; Ok(()) } @@ -207,7 +207,7 @@ pub async fn test_cancel_task() -> TaskSchedulerResult<()> { ); token.cancel(); - handle.await.unwrap().unwrap(); + handle.await.expect("task service join handle failed")?; Ok(()) }