diff --git a/Cargo.lock b/Cargo.lock index c4614768c..c5d0e4ae1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -97,7 +97,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if 1.0.1", - "getrandom 0.3.3", + "getrandom 0.3.4", "once_cell", "version_check", "zerocopy", @@ -1804,7 +1804,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27cea6e7f512d43b098939ff4d5a5d6fe3db07971e1d05176fe26c642d33f5b8" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "rand 0.9.1", "siphasher 1.0.1", "wide", @@ -2147,15 +2147,15 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if 1.0.1", "js-sys", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasip2", "wasm-bindgen", ] @@ -3020,7 +3020,7 @@ version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "libc", ] @@ -3482,6 +3482,8 @@ dependencies = [ "magicblock-magic-program-api", "magicblock-program", "magicblock-rpc-client", + "rand 0.8.5", + "solana-instruction", "solana-sdk", "thiserror 1.0.69", "tokio", @@ -3662,6 +3664,7 @@ dependencies = [ "solana-rpc-client-api", "solana-sdk", "solana-sdk-ids", + "solana-signer", "solana-system-interface", "solana-transaction-error", "thiserror 1.0.69", @@ -5146,7 +5149,7 @@ checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" dependencies = [ "bytes", "fastbloom", - "getrandom 0.3.3", + "getrandom 0.3.4", "lru-slab", "rand 0.9.1", "ring", @@ -5287,7 +5290,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", ] [[package]] @@ -10343,7 +10346,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" dependencies = [ "fastrand", - "getrandom 0.3.3", + "getrandom 0.3.4", "once_cell", "rustix 1.0.7", "windows-sys 0.59.0", @@ -11187,12 +11190,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" +name = "wasip2" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] @@ -11802,13 +11805,10 @@ dependencies = [ ] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags 2.9.1", -] +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" diff --git a/magicblock-account-cloner/Cargo.toml b/magicblock-account-cloner/Cargo.toml index 482f17509..8768bb607 100644 --- a/magicblock-account-cloner/Cargo.toml +++ b/magicblock-account-cloner/Cargo.toml @@ -20,6 +20,8 @@ magicblock-ledger = { workspace = true } magicblock-program = { workspace = true } magicblock-magic-program-api = { workspace = true } magicblock-rpc-client = { workspace = true } +rand = { workspace = true } +solana-instruction = { workspace = true } solana-sdk = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } diff --git a/magicblock-account-cloner/src/lib.rs b/magicblock-account-cloner/src/lib.rs index 28021a237..d65508d9b 100644 --- a/magicblock-account-cloner/src/lib.rs +++ b/magicblock-account-cloner/src/lib.rs @@ -9,7 +9,7 @@ use magicblock_accounts_db::AccountsDb; use magicblock_chainlink::{ cloner::{ errors::{ClonerError, ClonerResult}, - Cloner, + AccountCloneRequest, Cloner, }, remote_account_provider::program_account::{ DeployableV4Program, LoadedProgram, RemoteProgramLoader, @@ -24,15 +24,22 @@ use magicblock_core::link::transactions::TransactionSchedulerHandle; use magicblock_ledger::LatestBlock; use magicblock_magic_program_api::instruction::AccountModification; use magicblock_program::{ - instruction_utils::InstructionUtils, validator::validator_authority, + args::ScheduleTaskArgs, + instruction::MagicBlockInstruction, + instruction_utils::InstructionUtils, + validator::{validator_authority, validator_authority_id}, + MAGIC_CONTEXT_PUBKEY, }; +use solana_instruction::Instruction; use solana_sdk::{ - account::{AccountSharedData, ReadableAccount}, + account::ReadableAccount, hash::Hash, + instruction::AccountMeta, loader_v4, pubkey::Pubkey, rent::Rent, signature::{Signature, Signer}, + signer::SignerError, transaction::Transaction, }; use tokio::sync::oneshot; @@ -81,23 +88,63 @@ impl ChainlinkCloner { fn transaction_to_clone_regular_account( &self, - pubkey: &Pubkey, - account: &AccountSharedData, + request: &AccountCloneRequest, recent_blockhash: Hash, - ) -> Transaction { + ) -> Result { let account_modification = AccountModification { - pubkey: *pubkey, - lamports: Some(account.lamports()), - owner: Some(*account.owner()), - rent_epoch: Some(account.rent_epoch()), - data: Some(account.data().to_owned()), - executable: Some(account.executable()), - delegated: Some(account.delegated()), + pubkey: request.pubkey, + lamports: Some(request.account.lamports()), + owner: Some(*request.account.owner()), + rent_epoch: Some(request.account.rent_epoch()), + data: Some(request.account.data().to_owned()), + executable: Some(request.account.executable()), + delegated: Some(request.account.delegated()), }; - InstructionUtils::modify_accounts( - vec![account_modification], - recent_blockhash, - ) + + let modify_ix = InstructionUtils::modify_accounts_instruction(vec![ + account_modification, + ]); + // Defined positive commit frequency means commits should be scheduled + let ixs = match request.commit_frequency_ms { + Some(commit_frequency_ms) if commit_frequency_ms > 0 => { + // The task ID is randomly generated to avoid conflicts with other tasks + // TODO: remove once the program handles generating tasks instead of the client + // https://github.com/magicblock-labs/magicblock-validator/issues/625 + let task_id = rand::random(); + let schedule_commit_ix = Instruction::new_with_bincode( + magicblock_program::ID, + &MagicBlockInstruction::ScheduleCommit, + vec![ + AccountMeta::new(validator_authority_id(), true), + AccountMeta::new(MAGIC_CONTEXT_PUBKEY, false), + AccountMeta::new_readonly(request.pubkey, false), + ], + ); + let crank_commits_ix = + InstructionUtils::schedule_task_instruction( + &validator_authority_id(), + ScheduleTaskArgs { + task_id, + execution_interval_millis: commit_frequency_ms + as i64, + iterations: i64::MAX, + instructions: vec![schedule_commit_ix.clone()], + }, + &[ + request.pubkey, + MAGIC_CONTEXT_PUBKEY, + validator_authority_id(), + ], + ); + vec![modify_ix, crank_commits_ix] + } + _ => vec![modify_ix], + }; + + let mut tx = + Transaction::new_with_payer(&ixs, Some(&validator_authority_id())); + tx.try_sign(&[&validator_authority()], recent_blockhash)?; + Ok(tx) } /// Creates a transaction to clone the given program into the validator. @@ -319,20 +366,22 @@ impl ChainlinkCloner { impl Cloner for ChainlinkCloner { async fn clone_account( &self, - pubkey: Pubkey, - account: AccountSharedData, + request: AccountCloneRequest, ) -> ClonerResult { let recent_blockhash = self.block.load().blockhash; - let tx = self.transaction_to_clone_regular_account( - &pubkey, - &account, - recent_blockhash, - ); - if account.delegated() { - self.maybe_prepare_lookup_tables(pubkey, *account.owner()); + let tx = self + .transaction_to_clone_regular_account(&request, recent_blockhash)?; + if request.account.delegated() { + self.maybe_prepare_lookup_tables( + request.pubkey, + *request.account.owner(), + ); } self.send_transaction(tx).await.map_err(|err| { - ClonerError::FailedToCloneRegularAccount(pubkey, Box::new(err)) + ClonerError::FailedToCloneRegularAccount( + request.pubkey, + Box::new(err), + ) }) } diff --git a/magicblock-chainlink/Cargo.toml b/magicblock-chainlink/Cargo.toml index ea342b02f..2f1415f45 100644 --- a/magicblock-chainlink/Cargo.toml +++ b/magicblock-chainlink/Cargo.toml @@ -14,7 +14,7 @@ lru = { workspace = true } magicblock-core = { workspace = true } magicblock-magic-program-api = { workspace = true } magicblock-metrics = { workspace = true } - magicblock-delegation-program = { workspace = true } +magicblock-delegation-program = { workspace = true } serde_json = { workspace = true } solana-account = { workspace = true } solana-account-decoder = { workspace = true } @@ -27,6 +27,7 @@ solana-rpc-client = { workspace = true } solana-rpc-client-api = { workspace = true } solana-sdk = { workspace = true } solana-sdk-ids = { workspace = true } +solana-signer = { workspace = true } solana-system-interface = { workspace = true } solana-transaction-error = { workspace = true } thiserror = { workspace = true } diff --git a/magicblock-chainlink/src/chainlink/fetch_cloner.rs b/magicblock-chainlink/src/chainlink/fetch_cloner.rs index f2266f96e..f31abda59 100644 --- a/magicblock-chainlink/src/chainlink/fetch_cloner.rs +++ b/magicblock-chainlink/src/chainlink/fetch_cloner.rs @@ -28,7 +28,7 @@ use crate::{ account_still_undelegating_on_chain::account_still_undelegating_on_chain, blacklisted_accounts::blacklisted_accounts, }, - cloner::{errors::ClonerResult, Cloner}, + cloner::{errors::ClonerResult, AccountCloneRequest, Cloner}, remote_account_provider::{ program_account::{ get_loaderv3_get_program_data_address, ProgramAccountResolver, @@ -259,8 +259,14 @@ where if account.executable() { self.handle_executable_sub_update(pubkey, account) .await; - } else if let Err(err) = - self.cloner.clone_account(pubkey, account).await + } else if let Err(err) = self + .cloner + .clone_account(AccountCloneRequest { + pubkey, + account, + commit_frequency_ms: None, + }) + .await { error!( "Failed to clone account {pubkey} into bank: {err}" @@ -661,10 +667,11 @@ where ); } } else { - plain.push(( + plain.push(AccountCloneRequest { pubkey, - account_shared_data, - )); + account: account_shared_data, + commit_frequency_ms: None, + }); } } ResolvedAccount::Bank((pubkey, slot)) => { @@ -682,8 +689,10 @@ where .iter() .map(|(pubkey, slot)| (pubkey.to_string(), *slot)) .collect::>(); - let plain = - plain.iter().map(|(p, _)| p.to_string()).collect::>(); + let plain = plain + .iter() + .map(|p| p.pubkey.to_string()) + .collect::>(); let owned_by_deleg = owned_by_deleg .iter() .map(|(pubkey, _, slot)| (pubkey.to_string(), *slot)) @@ -811,7 +820,9 @@ where record_subs.push(delegation_record_pubkey); // If the account is delegated we set the owner and delegation state - if let Some(delegation_record_data) = delegation_record { + let commit_frequency_ms = if let Some(delegation_record_data) = + delegation_record + { // NOTE: failing here is fine when resolving all accounts for a transaction // since if something is off we better not run it anyways // However we may consider a different behavior when user is getting @@ -852,12 +863,21 @@ where account .set_owner(delegation_record.owner) .set_delegated(is_delegated_to_us); + if is_delegated_to_us { + Some(delegation_record.commit_frequency_ms) + } else { + None + } } else { missing_delegation_record .push((pubkey, account.remote_slot())); - } - accounts_to_clone - .push((pubkey, account.into_account_shared_data())); + None + }; + accounts_to_clone.push(AccountCloneRequest { + pubkey, + account: account.into_account_shared_data(), + commit_frequency_ms, + }); } (accounts_to_clone, record_subs) @@ -1037,16 +1057,18 @@ where // Cancel new subs for accounts we don't clone let acc_subs = pubkeys.iter().filter(|pubkey| { - !accounts_to_clone.iter().any(|(p, _)| p.eq(pubkey)) + !accounts_to_clone + .iter() + .any(|request| request.pubkey.eq(pubkey)) && !loaded_programs.iter().any(|p| p.program_id.eq(pubkey)) }); // Cancel subs for delegated accounts (accounts we clone but don't need to watch) let delegated_acc_subs: HashSet = accounts_to_clone .iter() - .filter_map(|(pubkey, account)| { - if account.delegated() { - Some(*pubkey) + .filter_map(|request| { + if request.account.delegated() { + Some(request.pubkey) } else { None } @@ -1070,20 +1092,18 @@ where .await; let mut join_set = JoinSet::new(); - for acc in accounts_to_clone { - let (pubkey, account) = acc; + for request in accounts_to_clone { if log::log_enabled!(log::Level::Trace) { trace!( - "Cloning account: {pubkey} (remote slot {}, owner: {})", - account.remote_slot(), - account.owner() + "Cloning account: {} (remote slot {}, owner: {})", + request.pubkey, + request.account.remote_slot(), + request.account.owner() ); }; let cloner = self.cloner.clone(); - join_set.spawn(async move { - cloner.clone_account(pubkey, account).await - }); + join_set.spawn(async move { cloner.clone_account(request).await }); } for acc in loaded_programs { @@ -1497,7 +1517,14 @@ where "Auto-airdropping {} lamports to new/empty account {}", lamports, pubkey ); - let _sig = self.cloner.clone_account(pubkey, account).await?; + let _sig = self + .cloner + .clone_account(AccountCloneRequest { + pubkey, + account, + commit_frequency_ms: None, + }) + .await?; Ok(()) } } diff --git a/magicblock-chainlink/src/cloner/errors.rs b/magicblock-chainlink/src/cloner/errors.rs index 21891f4b9..32bc8f1c7 100644 --- a/magicblock-chainlink/src/cloner/errors.rs +++ b/magicblock-chainlink/src/cloner/errors.rs @@ -10,6 +10,8 @@ pub enum ClonerError { #[error(transparent)] TryFromIntError(#[from] std::num::TryFromIntError), #[error(transparent)] + SignerError(#[from] solana_signer::SignerError), + #[error(transparent)] TransactionError(#[from] solana_transaction_error::TransactionError), #[error(transparent)] RemoteAccountProviderError( diff --git a/magicblock-chainlink/src/cloner/mod.rs b/magicblock-chainlink/src/cloner/mod.rs index cc96d3d98..cb6994969 100644 --- a/magicblock-chainlink/src/cloner/mod.rs +++ b/magicblock-chainlink/src/cloner/mod.rs @@ -8,6 +8,12 @@ use crate::remote_account_provider::program_account::LoadedProgram; pub mod errors; +pub struct AccountCloneRequest { + pub pubkey: Pubkey, + pub account: AccountSharedData, + pub commit_frequency_ms: Option, +} + #[async_trait] pub trait Cloner: Send + Sync + 'static { /// Overrides the account in the bank to make sure it's a PDA that can be used as readonly @@ -17,8 +23,7 @@ pub trait Cloner: Send + Sync + 'static { /// successfully. async fn clone_account( &self, - pubkey: Pubkey, - account: AccountSharedData, + request: AccountCloneRequest, ) -> ClonerResult; // Overrides the accounts in the bank to make sure the program is usable normally (and upgraded) diff --git a/magicblock-chainlink/src/testing/cloner_stub.rs b/magicblock-chainlink/src/testing/cloner_stub.rs index 6ee165861..a2ad7c988 100644 --- a/magicblock-chainlink/src/testing/cloner_stub.rs +++ b/magicblock-chainlink/src/testing/cloner_stub.rs @@ -11,6 +11,8 @@ use solana_loader_v4_interface::state::LoaderV4State; use solana_pubkey::Pubkey; use solana_sdk::{instruction::InstructionError, signature::Signature}; +#[cfg(any(test, feature = "dev-context"))] +use crate::cloner::AccountCloneRequest; use crate::{ accounts_bank::mock::AccountsBankStub, cloner::{errors::ClonerResult, Cloner}, @@ -68,12 +70,14 @@ impl ClonerStub { #[cfg(any(test, feature = "dev-context"))] #[async_trait] impl Cloner for ClonerStub { + // NOTE: commit_frequency_ms is intentionally ignored by this test stub. + // This stub only performs cloning, not commit scheduling, so future readers/tests + // should understand this limitation. async fn clone_account( &self, - pubkey: Pubkey, - account: AccountSharedData, + request: AccountCloneRequest, ) -> ClonerResult { - self.accounts_bank.insert(pubkey, account); + self.accounts_bank.insert(request.pubkey, request.account); Ok(Signature::default()) } diff --git a/magicblock-magic-program-api/src/args.rs b/magicblock-magic-program-api/src/args.rs index c4679b634..06dab4721 100644 --- a/magicblock-magic-program-api/src/args.rs +++ b/magicblock-magic-program-api/src/args.rs @@ -125,9 +125,9 @@ impl<'a> From<&AccountInfo<'a>> for ShortAccountMeta { #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] pub struct ScheduleTaskArgs { - pub task_id: u64, - pub execution_interval_millis: u64, - pub iterations: u64, + pub task_id: i64, + pub execution_interval_millis: i64, + pub iterations: i64, pub instructions: Vec, } @@ -140,27 +140,27 @@ pub enum TaskRequest { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct ScheduleTaskRequest { /// Unique identifier for this task - pub id: u64, + pub id: i64, /// 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, + pub execution_interval_millis: i64, /// Number of times this task will be executed - pub iterations: u64, + pub iterations: i64, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct CancelTaskRequest { /// Unique identifier for the task to cancel - pub task_id: u64, + pub task_id: i64, /// Authority that can cancel this task pub authority: Pubkey, } impl TaskRequest { - pub fn id(&self) -> u64 { + pub fn id(&self) -> i64 { match self { Self::Schedule(request) => request.id, Self::Cancel(request) => request.task_id, diff --git a/magicblock-magic-program-api/src/instruction.rs b/magicblock-magic-program-api/src/instruction.rs index 8cd525396..289716063 100644 --- a/magicblock-magic-program-api/src/instruction.rs +++ b/magicblock-magic-program-api/src/instruction.rs @@ -84,7 +84,7 @@ pub enum MagicBlockInstruction { /// - **0.** `[WRITE, SIGNER]` Task authority /// - **1.** `[WRITE]` Task context account CancelTask { - task_id: u64, + task_id: i64, }, /// Disables the executable check, needed to modify the data of a program diff --git a/magicblock-task-scheduler/src/db.rs b/magicblock-task-scheduler/src/db.rs index a0b938fb2..a39fb23f2 100644 --- a/magicblock-task-scheduler/src/db.rs +++ b/magicblock-task-scheduler/src/db.rs @@ -7,20 +7,22 @@ use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; use crate::errors::TaskSchedulerError; +/// Represents a task in the database +/// Uses i64 for all timestamps and IDs to avoid overflows #[derive(Debug, Clone, PartialEq, Eq)] pub struct DbTask { /// Unique identifier for this task - pub id: u64, + pub id: i64, /// 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, + pub execution_interval_millis: i64, /// Number of times this task still needs to be executed. - pub executions_left: u64, + pub executions_left: i64, /// Timestamp of the last execution of this task in milliseconds since UNIX epoch - pub last_execution_millis: u64, + pub last_execution_millis: i64, } impl<'a> From<&'a ScheduleTaskRequest> for DbTask { @@ -38,17 +40,17 @@ impl<'a> From<&'a ScheduleTaskRequest> for DbTask { #[derive(Debug, Clone)] pub struct FailedScheduling { - pub id: u64, - pub timestamp: u64, - pub task_id: u64, + pub id: i64, + pub timestamp: i64, + pub task_id: i64, pub error: String, } #[derive(Debug, Clone)] pub struct FailedTask { - pub id: u64, - pub timestamp: u64, - pub task_id: u64, + pub id: i64, + pub timestamp: i64, + pub task_id: i64, pub error: String, } @@ -128,7 +130,7 @@ impl SchedulerDatabase { pub fn update_task_after_execution( &self, - task_id: u64, + task_id: i64, last_execution: i64, ) -> Result<(), TaskSchedulerError> { let now = Utc::now().timestamp_millis(); @@ -147,7 +149,7 @@ impl SchedulerDatabase { pub fn insert_failed_scheduling( &self, - task_id: u64, + task_id: i64, error: String, ) -> Result<(), TaskSchedulerError> { self.conn.execute( @@ -160,7 +162,7 @@ impl SchedulerDatabase { pub fn insert_failed_task( &self, - task_id: u64, + task_id: i64, error: String, ) -> Result<(), TaskSchedulerError> { self.conn.execute( @@ -173,7 +175,7 @@ impl SchedulerDatabase { pub fn unschedule_task( &self, - task_id: u64, + task_id: i64, ) -> Result<(), TaskSchedulerError> { self.conn.execute( "UPDATE tasks SET executions_left = 0 WHERE id = ?", @@ -183,7 +185,7 @@ impl SchedulerDatabase { Ok(()) } - pub fn remove_task(&self, task_id: u64) -> Result<(), TaskSchedulerError> { + pub fn remove_task(&self, task_id: i64) -> Result<(), TaskSchedulerError> { self.conn .execute("DELETE FROM tasks WHERE id = ?", [task_id])?; @@ -192,7 +194,7 @@ impl SchedulerDatabase { pub fn get_task( &self, - task_id: u64, + task_id: i64, ) -> Result, TaskSchedulerError> { let mut stmt = self.conn.prepare( "SELECT id, instructions, authority, execution_interval_millis, executions_left, last_execution_millis @@ -270,7 +272,7 @@ impl SchedulerDatabase { Ok(tasks) } - pub fn get_task_ids(&self) -> Result, TaskSchedulerError> { + pub fn get_task_ids(&self) -> Result, TaskSchedulerError> { let mut stmt = self.conn.prepare( "SELECT id FROM tasks", @@ -278,7 +280,7 @@ impl SchedulerDatabase { let rows = stmt.query_map([], |row| row.get(0))?; - Ok(rows.collect::, rusqlite::Error>>()?) + Ok(rows.collect::, rusqlite::Error>>()?) } pub fn get_failed_schedulings( diff --git a/magicblock-task-scheduler/src/errors.rs b/magicblock-task-scheduler/src/errors.rs index adee26215..e76cea504 100644 --- a/magicblock-task-scheduler/src/errors.rs +++ b/magicblock-task-scheduler/src/errors.rs @@ -21,7 +21,7 @@ pub enum TaskSchedulerError { Rpc(#[from] solana_rpc_client_api::client_error::ClientError), #[error("Task not found: {0}")] - TaskNotFound(u64), + TaskNotFound(i64), #[error(transparent)] Transaction(#[from] solana_sdk::transaction::TransactionError), @@ -42,5 +42,5 @@ pub enum TaskSchedulerError { ContextDeserialization(Vec), #[error("Task {0} already exists and is owned by {1}, not {2}")] - UnauthorizedReplacing(u64, String, String), + UnauthorizedReplacing(i64, String, String), } diff --git a/magicblock-task-scheduler/src/service.rs b/magicblock-task-scheduler/src/service.rs index d21374cea..46cb3db30 100644 --- a/magicblock-task-scheduler/src/service.rs +++ b/magicblock-task-scheduler/src/service.rs @@ -45,7 +45,7 @@ pub struct TaskSchedulerService { /// Queue of tasks to execute task_queue: DelayQueue, /// Map of task IDs to their corresponding keys in the task queue - task_queue_keys: HashMap, + task_queue_keys: HashMap, /// Counter used to make each transaction unique tx_counter: AtomicU64, /// Token used to cancel the task scheduler @@ -103,7 +103,7 @@ impl TaskSchedulerService { mut self, ) -> TaskSchedulerResult>> { let tasks = self.db.get_tasks()?; - let now = chrono::Utc::now().timestamp_millis() as u64; + let now = chrono::Utc::now().timestamp_millis(); debug!( "Task scheduler starting at {} with {} tasks", now, @@ -112,8 +112,9 @@ impl TaskSchedulerService { 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 timeout = Duration::from_millis( + next_execution.saturating_sub(now) as u64, + ); let task_id = task.id; let key = self.task_queue.insert(task, timeout); self.task_queue_keys.insert(task_id, key); @@ -205,7 +206,7 @@ impl TaskSchedulerService { }; let key = self.task_queue.insert( new_task, - Duration::from_millis(task.execution_interval_millis), + Duration::from_millis(task.execution_interval_millis as u64), ); self.task_queue_keys.insert(task.id, key); } @@ -241,7 +242,7 @@ impl TaskSchedulerService { Ok(()) } - pub fn unregister_task(&self, task_id: u64) -> TaskSchedulerResult<()> { + pub fn unregister_task(&self, task_id: i64) -> TaskSchedulerResult<()> { self.db.remove_task(task_id)?; debug!("Removed task {} from database", task_id); @@ -283,7 +284,7 @@ impl TaskSchedulerService { Ok(()) } - fn remove_task_from_queue(&mut self, task_id: u64) { + fn remove_task_from_queue(&mut self, task_id: i64) { if let Some(key) = self.task_queue_keys.remove(&task_id) { self.task_queue.remove(&key); } diff --git a/magicblock-task-scheduler/tests/service.rs b/magicblock-task-scheduler/tests/service.rs index 64a81b7cc..e85bafbdf 100644 --- a/magicblock-task-scheduler/tests/service.rs +++ b/magicblock-task-scheduler/tests/service.rs @@ -145,8 +145,9 @@ pub async fn test_cancel_task() -> TaskSchedulerResult<()> { ); // Wait until we actually observe at least five executions - let executed_before_cancel = - tokio::time::timeout(Duration::from_millis(10 * interval), async { + let executed_before_cancel = tokio::time::timeout( + Duration::from_millis(10 * interval as u64), + async { loop { if let Some(value) = env.get_account(account.pubkey()).data().first() @@ -157,11 +158,10 @@ pub async fn test_cancel_task() -> TaskSchedulerResult<()> { } tokio::time::sleep(Duration::from_millis(20)).await; } - }) - .await - .expect( - "task scheduler never reached five executions within 10 intervals", - ); + }, + ) + .await + .expect("task scheduler never reached five executions within 10 intervals"); // Cancel the task let ix = Instruction::new_with_bincode( @@ -194,7 +194,7 @@ pub async fn test_cancel_task() -> TaskSchedulerResult<()> { ); // Ensure the scheduler stops issuing executions after cancellation - tokio::time::sleep(Duration::from_millis(2 * interval)).await; + tokio::time::sleep(Duration::from_millis(2 * interval as u64)).await; let value_after_cancel = env .get_account(account.pubkey()) diff --git a/programs/guinea/src/lib.rs b/programs/guinea/src/lib.rs index e141af857..322ca4cc3 100644 --- a/programs/guinea/src/lib.rs +++ b/programs/guinea/src/lib.rs @@ -30,7 +30,7 @@ pub enum GuineaInstruction { Transfer(u64), Resize(usize), ScheduleTask(ScheduleTaskArgs), - CancelTask(u64), + CancelTask(i64), } fn compute_balances(accounts: slice::Iter) { @@ -141,7 +141,7 @@ fn schedule_task( fn cancel_task( mut accounts: slice::Iter, - task_id: u64, + task_id: i64, ) -> ProgramResult { let magic_program_info = next_account_info(&mut accounts)?; let payer_info = next_account_info(&mut accounts)?; diff --git a/programs/magicblock/src/schedule_task/process_cancel_task.rs b/programs/magicblock/src/schedule_task/process_cancel_task.rs index 867faf043..380cb9975 100644 --- a/programs/magicblock/src/schedule_task/process_cancel_task.rs +++ b/programs/magicblock/src/schedule_task/process_cancel_task.rs @@ -11,7 +11,7 @@ use crate::utils::accounts::get_instruction_pubkey_with_idx; pub(crate) fn process_cancel_task( signers: HashSet, invoke_context: &mut InvokeContext, - task_id: u64, + task_id: i64, ) -> Result<(), InstructionError> { const TASK_AUTHORITY_IDX: u16 = 0; diff --git a/programs/magicblock/src/schedule_task/process_schedule_task.rs b/programs/magicblock/src/schedule_task/process_schedule_task.rs index 936738bbf..11f11b90c 100644 --- a/programs/magicblock/src/schedule_task/process_schedule_task.rs +++ b/programs/magicblock/src/schedule_task/process_schedule_task.rs @@ -13,7 +13,7 @@ use crate::{ validator::validator_authority_id, }; -const MIN_EXECUTION_INTERVAL: u64 = 10; +const MIN_EXECUTION_INTERVAL: i64 = 10; pub(crate) fn process_schedule_task( signers: HashSet, @@ -70,6 +70,15 @@ pub(crate) fn process_schedule_task( return Err(InstructionError::InvalidInstructionData); } + // Enforce minimal number of iterations + if args.iterations < 1 { + ic_msg!( + invoke_context, + "ScheduleTask ERR: iterations must be at least 1" + ); + return Err(InstructionError::InvalidInstructionData); + } + // Enforce minimal number of instructions if args.instructions.is_empty() { ic_msg!( @@ -88,15 +97,25 @@ pub(crate) fn process_schedule_task( get_instruction_pubkey_with_idx(transaction_context, i as u16) .copied() }) - .collect::, _>>()?; + .collect::, _>>()?; + let val_id = validator_authority_id(); for instruction in &args.instructions { for account in &instruction.accounts { - let val_id = validator_authority_id(); if account.is_signer && account.pubkey.ne(&val_id) { + ic_msg!( + invoke_context, + "ScheduleTask: signer account '{}' is not the validator authority.", + account.pubkey, + ); return Err(InstructionError::MissingRequiredSignature); } if !ix_accounts.contains(&account.pubkey) { + ic_msg!( + invoke_context, + "ScheduleTask: missing account '{}'.", + account.pubkey, + ); return Err(InstructionError::MissingAccount); } } @@ -360,4 +379,26 @@ mod test { Err(InstructionError::InvalidInstructionData), ); } + + #[test] + fn test_process_schedule_task_with_invalid_iterations() { + let (payer, pdas, transaction_accounts) = setup_accounts(0); + let args = ScheduleTaskArgs { + task_id: 1, + execution_interval_millis: 1000, + iterations: -100, + instructions: vec![create_simple_ix()], + }; + let ix = InstructionUtils::schedule_task_instruction( + &payer.pubkey(), + args, + &pdas, + ); + process_instruction( + &ix.data, + transaction_accounts, + ix.accounts, + Err(InstructionError::InvalidInstructionData), + ); + } } diff --git a/programs/magicblock/src/schedule_transactions/process_schedule_commit.rs b/programs/magicblock/src/schedule_transactions/process_schedule_commit.rs index 0ab615b92..18ea8cc7c 100644 --- a/programs/magicblock/src/schedule_transactions/process_schedule_commit.rs +++ b/programs/magicblock/src/schedule_transactions/process_schedule_commit.rs @@ -23,6 +23,7 @@ use crate::{ }, instruction_utils::InstructionUtils, }, + validator::validator_authority_id, MagicContext, }; @@ -122,10 +123,12 @@ pub(crate) fn process_schedule_commit( let parent_program_id = Some(&first_committee_owner); // Assert all accounts are delegated, owned by invoking program OR are signers + // Also works if the validator authority is a signer // NOTE: we don't require PDAs to be signers as in our case verifying that the // program owning the PDAs invoked us via CPI is sufficient // Thus we can be `invoke`d unsigned and no seeds need to be provided let mut committed_accounts: Vec = Vec::new(); + let val_id = validator_authority_id(); for idx in COMMITTEES_START..ix_accs_len { let acc_pubkey = get_instruction_pubkey_with_idx(transaction_context, idx as u16)?; @@ -158,6 +161,7 @@ pub(crate) fn process_schedule_commit( let acc_owner = *acc.borrow().owner(); if parent_program_id != Some(&acc_owner) && !signers.contains(acc_pubkey) + && !signers.contains(&val_id) { return match parent_program_id { None => { @@ -170,7 +174,7 @@ pub(crate) fn process_schedule_commit( Some(parent_id) => { ic_msg!( invoke_context, - "ScheduleCommit ERR: account {} needs to be owned by the invoking program {} or be a signer to be committed, but is owned by {}", + "ScheduleCommit ERR: account {} needs to be owned by the invoking program {}, be a signer, or ix must be signed by the validator to be committed, but is owned by {}", acc_pubkey, parent_id, acc_owner ); Err(InstructionError::InvalidAccountOwner) diff --git a/programs/magicblock/src/utils/instruction_utils.rs b/programs/magicblock/src/utils/instruction_utils.rs index 95c00fa89..d7cacf18b 100644 --- a/programs/magicblock/src/utils/instruction_utils.rs +++ b/programs/magicblock/src/utils/instruction_utils.rs @@ -215,7 +215,7 @@ impl InstructionUtils { ) -> Instruction { let mut account_metas = vec![AccountMeta::new(*payer, true)]; for account in accounts { - account_metas.push(AccountMeta::new_readonly(*account, true)); + account_metas.push(AccountMeta::new_readonly(*account, false)); } Instruction::new_with_bincode( @@ -230,7 +230,7 @@ impl InstructionUtils { // ----------------- pub fn cancel_task( authority: &Keypair, - task_id: u64, + task_id: i64, recent_blockhash: Hash, ) -> Transaction { let ix = Self::cancel_task_instruction(&authority.pubkey(), task_id); @@ -239,7 +239,7 @@ impl InstructionUtils { pub fn cancel_task_instruction( authority: &Pubkey, - task_id: u64, + task_id: i64, ) -> Instruction { let account_metas = vec![AccountMeta::new(*authority, true)]; diff --git a/test-integration/Cargo.lock b/test-integration/Cargo.lock index 710c49968..ab8729f0c 100644 --- a/test-integration/Cargo.lock +++ b/test-integration/Cargo.lock @@ -3468,6 +3468,8 @@ dependencies = [ "magicblock-magic-program-api 0.3.1", "magicblock-program", "magicblock-rpc-client", + "rand 0.8.5", + "solana-instruction", "solana-sdk", "thiserror 1.0.69", "tokio", @@ -3638,6 +3640,7 @@ dependencies = [ "solana-rpc-client-api", "solana-sdk", "solana-sdk-ids", + "solana-signer", "solana-system-interface", "solana-transaction-error", "thiserror 1.0.69", diff --git a/test-integration/programs/flexi-counter/src/instruction.rs b/test-integration/programs/flexi-counter/src/instruction.rs index 2388a417d..8986f4850 100644 --- a/test-integration/programs/flexi-counter/src/instruction.rs +++ b/test-integration/programs/flexi-counter/src/instruction.rs @@ -19,16 +19,16 @@ pub struct DelegateArgs { #[derive(BorshSerialize, BorshDeserialize, Debug, Clone)] pub struct ScheduleArgs { - pub task_id: u64, - pub execution_interval_millis: u64, - pub iterations: u64, + pub task_id: i64, + pub execution_interval_millis: i64, + pub iterations: i64, pub error: bool, pub signer: bool, } #[derive(BorshSerialize, BorshDeserialize, Debug, Clone)] pub struct CancelArgs { - pub task_id: u64, + pub task_id: i64, } pub const MAX_ACCOUNT_ALLOC_PER_INSTRUCTION_SIZE: u16 = 10_240; @@ -272,6 +272,13 @@ pub fn create_mul_ix(payer: Pubkey, multiplier: u8) -> Instruction { } pub fn create_delegate_ix(payer: Pubkey) -> Instruction { + create_delegate_ix_with_commit_frequency_ms(payer, 0) +} + +pub fn create_delegate_ix_with_commit_frequency_ms( + payer: Pubkey, + commit_frequency_ms: u32, +) -> Instruction { let program_id = &crate::id(); let (pda, _) = FlexiCounter::pda(&payer); @@ -290,7 +297,7 @@ pub fn create_delegate_ix(payer: Pubkey) -> Instruction { let args = DelegateArgs { valid_until: i64::MAX, - commit_frequency_ms: 1_000_000_000, + commit_frequency_ms, }; Instruction::new_with_borsh( @@ -414,20 +421,18 @@ pub fn create_intent_ix( ) } -#[allow(clippy::too_many_arguments)] pub fn create_schedule_task_ix( payer: Pubkey, - magic_program: Pubkey, - task_id: u64, - execution_interval_millis: u64, - iterations: u64, + task_id: i64, + execution_interval_millis: i64, + iterations: i64, error: bool, signer: bool, ) -> Instruction { let program_id = &crate::id(); let (pda, _) = FlexiCounter::pda(&payer); let accounts = vec![ - AccountMeta::new_readonly(magic_program, false), + AccountMeta::new_readonly(MAGIC_PROGRAM_ID, false), AccountMeta::new(payer, true), AccountMeta::new(pda, false), ]; @@ -444,14 +449,10 @@ pub fn create_schedule_task_ix( ) } -pub fn create_cancel_task_ix( - payer: Pubkey, - magic_program: Pubkey, - task_id: u64, -) -> Instruction { +pub fn create_cancel_task_ix(payer: Pubkey, task_id: i64) -> Instruction { let program_id = &crate::id(); let accounts = vec![ - AccountMeta::new_readonly(magic_program, false), + AccountMeta::new_readonly(MAGIC_PROGRAM_ID, false), AccountMeta::new(payer, true), ]; Instruction::new_with_borsh( diff --git a/test-integration/test-chainlink/src/ixtest_context.rs b/test-integration/test-chainlink/src/ixtest_context.rs index bb5ca5e51..6526cc346 100644 --- a/test-integration/test-chainlink/src/ixtest_context.rs +++ b/test-integration/test-chainlink/src/ixtest_context.rs @@ -6,7 +6,7 @@ use integration_test_tools::dlp_interface; use log::*; use magicblock_chainlink::{ accounts_bank::mock::AccountsBankStub, - cloner::Cloner, + cloner::{AccountCloneRequest, Cloner}, config::{ChainlinkConfig, LifecycleMode}, fetch_cloner::FetchCloner, native_program_accounts, @@ -100,7 +100,11 @@ impl IxtestContext { ); for pubkey in native_programs { cloner - .clone_account(pubkey, program_stub.clone()) + .clone_account(AccountCloneRequest { + pubkey, + account: program_stub.clone(), + commit_frequency_ms: None, + }) .await .unwrap(); } diff --git a/test-integration/test-task-scheduler/src/lib.rs b/test-integration/test-task-scheduler/src/lib.rs index a7b82983c..f1259eb3b 100644 --- a/test-integration/test-task-scheduler/src/lib.rs +++ b/test-integration/test-task-scheduler/src/lib.rs @@ -15,7 +15,9 @@ use magicblock_config::{ LedgerResumeStrategyType, LifecycleMode, RemoteCluster, RemoteConfig, TaskSchedulerConfig, ValidatorConfig, }; -use program_flexi_counter::instruction::{create_delegate_ix, create_init_ix}; +use program_flexi_counter::instruction::{ + create_delegate_ix_with_commit_frequency_ms, create_init_ix, +}; use solana_sdk::{ hash::Hash, instruction::Instruction, pubkey::Pubkey, signature::Keypair, signer::Signer, transaction::Transaction, @@ -80,6 +82,7 @@ pub fn create_delegated_counter( ctx: &IntegrationTestContext, payer: &Keypair, validator: &mut Child, + commit_frequency_ms: u32, ) { // Initialize the counter let blockhash = expect!( @@ -109,7 +112,10 @@ pub fn create_delegated_counter( expect!( ctx.send_transaction_chain( &mut Transaction::new_signed_with_payer( - &[create_delegate_ix(payer.pubkey())], + &[create_delegate_ix_with_commit_frequency_ms( + payer.pubkey(), + commit_frequency_ms + )], Some(&payer.pubkey()), &[&payer], blockhash, 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 7529d70cd..245dcd510 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,5 @@ use cleanass::{assert, assert_eq}; use integration_test_tools::{expect, validator::cleanup}; -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}, @@ -27,7 +26,7 @@ fn test_cancel_ongoing_task() { validator ); - create_delegated_counter(&ctx, &payer, &mut validator); + create_delegated_counter(&ctx, &payer, &mut validator, 0); // Noop tx to make sure the noop program is cloned let ephem_blockhash = send_noop_tx(&ctx, &payer, &mut validator); @@ -41,7 +40,6 @@ fn test_cancel_ongoing_task() { &mut Transaction::new_signed_with_payer( &[create_schedule_task_ix( payer.pubkey(), - MAGIC_PROGRAM_ID, task_id, execution_interval_millis, iterations, @@ -77,11 +75,7 @@ fn test_cancel_ongoing_task() { let sig = expect!( ctx.send_transaction_ephem_with_preflight( &mut Transaction::new_signed_with_payer( - &[create_cancel_task_ix( - payer.pubkey(), - MAGIC_PROGRAM_ID, - task_id, - )], + &[create_cancel_task_ix(payer.pubkey(), task_id,)], Some(&payer.pubkey()), &[&payer], ephem_blockhash, @@ -150,7 +144,7 @@ fn test_cancel_ongoing_task() { let counter = expect!(FlexiCounter::try_decode(&counter_account.data), validator); assert!( - counter.count < iterations, + counter.count < iterations as u64, cleanup(&mut validator), "counter.count: {}", counter.count, 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 4d4c3b3cc..f2532bef7 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,5 @@ use cleanass::{assert, assert_eq}; use integration_test_tools::{expect, validator::cleanup}; -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}, @@ -27,7 +26,7 @@ fn test_reschedule_task() { validator ); - create_delegated_counter(&ctx, &payer, &mut validator); + create_delegated_counter(&ctx, &payer, &mut validator, 0); // Noop tx to make sure the noop program is cloned let ephem_blockhash = send_noop_tx(&ctx, &payer, &mut validator); @@ -41,7 +40,6 @@ fn test_reschedule_task() { &mut Transaction::new_signed_with_payer( &[create_schedule_task_ix( payer.pubkey(), - MAGIC_PROGRAM_ID, task_id, execution_interval_millis, iterations, @@ -76,7 +74,6 @@ fn test_reschedule_task() { &mut Transaction::new_signed_with_payer( &[create_schedule_task_ix( payer.pubkey(), - MAGIC_PROGRAM_ID, task_id, new_execution_interval_millis, iterations, @@ -155,7 +152,7 @@ fn test_reschedule_task() { let counter = expect!(FlexiCounter::try_decode(&counter_account.data), validator); assert!( - counter.count == 2 * iterations, + counter.count == 2 * iterations as u64, cleanup(&mut validator), "counter.count: {}", counter.count @@ -165,11 +162,7 @@ fn test_reschedule_task() { let sig = expect!( ctx.send_transaction_ephem_with_preflight( &mut Transaction::new_signed_with_payer( - &[create_cancel_task_ix( - payer.pubkey(), - MAGIC_PROGRAM_ID, - task_id, - )], + &[create_cancel_task_ix(payer.pubkey(), task_id,)], Some(&payer.pubkey()), &[&payer], ephem_blockhash, 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 06a6495fb..970cf7acc 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,5 @@ use cleanass::{assert, assert_eq}; use integration_test_tools::{expect, validator::cleanup}; -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}, @@ -28,7 +27,7 @@ fn test_schedule_error() { validator ); - create_delegated_counter(&ctx, &payer, &mut validator); + create_delegated_counter(&ctx, &payer, &mut validator, 0); // Noop tx to make sure the noop program is cloned let ephem_blockhash = send_noop_tx(&ctx, &payer, &mut validator); @@ -42,7 +41,6 @@ fn test_schedule_error() { &mut Transaction::new_signed_with_payer( &[create_schedule_task_ix( payer.pubkey(), - MAGIC_PROGRAM_ID, task_id, execution_interval_millis, iterations, @@ -125,11 +123,7 @@ fn test_schedule_error() { let sig = expect!( ctx.send_transaction_ephem_with_preflight( &mut Transaction::new_signed_with_payer( - &[create_cancel_task_ix( - payer.pubkey(), - MAGIC_PROGRAM_ID, - task_id, - )], + &[create_cancel_task_ix(payer.pubkey(), task_id,)], Some(&payer.pubkey()), &[&payer], ephem_blockhash, 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 c6bfd1de8..4040d074d 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,5 @@ use cleanass::{assert, assert_eq}; use integration_test_tools::{expect, validator::cleanup}; -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}, @@ -27,7 +26,7 @@ fn test_schedule_task() { validator ); - create_delegated_counter(&ctx, &payer, &mut validator); + create_delegated_counter(&ctx, &payer, &mut validator, 0); // Noop tx to make sure the noop program is cloned let ephem_blockhash = send_noop_tx(&ctx, &payer, &mut validator); @@ -41,7 +40,6 @@ fn test_schedule_task() { &mut Transaction::new_signed_with_payer( &[create_schedule_task_ix( payer.pubkey(), - MAGIC_PROGRAM_ID, task_id, execution_interval_millis, iterations, @@ -120,7 +118,7 @@ fn test_schedule_task() { let counter = expect!(FlexiCounter::try_decode(&counter_account.data), validator); assert!( - counter.count == iterations, + counter.count == iterations as u64, cleanup(&mut validator), "counter.count: {}", counter.count @@ -130,11 +128,7 @@ fn test_schedule_task() { let sig = expect!( ctx.send_transaction_ephem_with_preflight( &mut Transaction::new_signed_with_payer( - &[create_cancel_task_ix( - payer.pubkey(), - MAGIC_PROGRAM_ID, - task_id, - )], + &[create_cancel_task_ix(payer.pubkey(), task_id,)], Some(&payer.pubkey()), &[&payer], ephem_blockhash, 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 69d190129..0626aba42 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,4 @@ use integration_test_tools::{expect, validator::cleanup}; -use magicblock_program::ID as MAGIC_PROGRAM_ID; use program_flexi_counter::instruction::create_schedule_task_ix; use solana_sdk::{ instruction::InstructionError, @@ -23,7 +22,7 @@ fn test_schedule_task_signed() { validator ); - create_delegated_counter(&ctx, &payer, &mut validator); + create_delegated_counter(&ctx, &payer, &mut validator, 0); // Noop tx to make sure the noop program is cloned let ephem_blockhash = send_noop_tx(&ctx, &payer, &mut validator); @@ -37,7 +36,6 @@ fn test_schedule_task_signed() { &mut Transaction::new_signed_with_payer( &[create_schedule_task_ix( payer.pubkey(), - MAGIC_PROGRAM_ID, task_id, execution_interval_millis, iterations, diff --git a/test-integration/test-task-scheduler/tests/test_scheduled_commits.rs b/test-integration/test-task-scheduler/tests/test_scheduled_commits.rs new file mode 100644 index 000000000..3007e76c3 --- /dev/null +++ b/test-integration/test-task-scheduler/tests/test_scheduled_commits.rs @@ -0,0 +1,127 @@ +use cleanass::assert; +use integration_test_tools::{expect, validator::cleanup}; +use program_flexi_counter::{ + instruction::create_schedule_task_ix, state::FlexiCounter, +}; +use solana_sdk::{ + native_token::LAMPORTS_PER_SOL, signature::Keypair, signer::Signer, + transaction::Transaction, +}; +use test_task_scheduler::{ + create_delegated_counter, send_noop_tx, setup_validator, +}; + +#[test] +fn test_scheduled_commits() { + let (_temp_dir, mut validator, ctx) = setup_validator(); + + let payer = Keypair::new(); + let (counter_pda, _) = FlexiCounter::pda(&payer.pubkey()); + + expect!( + ctx.airdrop_chain(&payer.pubkey(), 10 * LAMPORTS_PER_SOL), + validator + ); + + // Noop tx to make sure the noop program is cloned + let ephem_blockhash = send_noop_tx(&ctx, &payer, &mut validator); + + let commit_frequency_ms = 400; + create_delegated_counter(&ctx, &payer, &mut validator, commit_frequency_ms); + + eprintln!("Delegated counter: {:?}", counter_pda); + + // Schedule a task + let task_id = 5; + // Interval matching mainnet block time + let execution_interval_millis = 400; + let iterations = 3; + let sig = expect!( + ctx.send_transaction_ephem_with_preflight( + &mut Transaction::new_signed_with_payer( + &[create_schedule_task_ix( + payer.pubkey(), + task_id, + execution_interval_millis, + iterations, + false, + false, + )], + Some(&payer.pubkey()), + &[&payer], + ephem_blockhash, + ), + &[&payer] + ), + validator + ); + let status = expect!(ctx.get_transaction_ephem(&sig), validator); + expect!( + status + .transaction + .meta + .and_then(|m| m.status.ok()) + .ok_or_else(|| anyhow::anyhow!("Transaction failed")), + validator + ); + + // Check that the counter value is properly incremented on mainnet + const MAX_TRIES: u32 = 30; + let mut tries = 0; + let mut chain_values = vec![1, 2]; + let mut ephem_values = vec![1, 2]; + loop { + let chain_counter_account = expect!( + ctx.try_chain_client().and_then(|client| client + .get_account(&counter_pda) + .map_err(|e| anyhow::anyhow!("Failed to get account: {}", e))), + validator + ); + let chain_counter = expect!( + FlexiCounter::try_decode(&chain_counter_account.data), + validator + ); + + let ephem_counter_account = expect!( + ctx.try_ephem_client().and_then(|client| client + .get_account(&counter_pda) + .map_err(|e| anyhow::anyhow!("Failed to get account: {}", e))), + validator + ); + let ephem_counter = expect!( + FlexiCounter::try_decode(&ephem_counter_account.data), + validator + ); + + let chain_value_index = + chain_values.iter().position(|x| x == &chain_counter.count); + if let Some(index) = chain_value_index { + chain_values.remove(index); + } + + let ephem_value_index = + ephem_values.iter().position(|x| x == &ephem_counter.count); + if let Some(index) = ephem_value_index { + ephem_values.remove(index); + } + + if chain_values.is_empty() && ephem_values.is_empty() { + break; + } + + tries += 1; + if tries > MAX_TRIES { + assert!( + false, + cleanup(&mut validator), + "Missed some values: ephem_values: {:?}, chain_values: {:?}", + ephem_values, + chain_values + ); + } + + std::thread::sleep(std::time::Duration::from_millis(50)); + } + + cleanup(&mut validator); +} 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 add2f4c99..37eb25125 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,5 @@ use cleanass::{assert, assert_eq}; use integration_test_tools::{expect, validator::cleanup}; -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, @@ -31,8 +30,8 @@ fn test_unauthorized_reschedule() { validator ); - create_delegated_counter(&ctx, &payer, &mut validator); - create_delegated_counter(&ctx, &different_payer, &mut validator); + create_delegated_counter(&ctx, &payer, &mut validator, 0); + create_delegated_counter(&ctx, &different_payer, &mut validator, 0); // Noop tx to make sure the noop program is cloned let ephem_blockhash = send_noop_tx(&ctx, &payer, &mut validator); @@ -46,7 +45,6 @@ fn test_unauthorized_reschedule() { &mut Transaction::new_signed_with_payer( &[create_schedule_task_ix( payer.pubkey(), - MAGIC_PROGRAM_ID, task_id, execution_interval_millis, iterations, @@ -81,7 +79,6 @@ fn test_unauthorized_reschedule() { &mut Transaction::new_signed_with_payer( &[create_schedule_task_ix( different_payer.pubkey(), - MAGIC_PROGRAM_ID, task_id, new_execution_interval_millis, iterations, @@ -160,7 +157,7 @@ fn test_unauthorized_reschedule() { let counter = expect!(FlexiCounter::try_decode(&counter_account.data), validator); assert!( - counter.count == iterations, + counter.count == iterations as u64, cleanup(&mut validator), "counter.count: {}", counter.count diff --git a/test-integration/test-tools/src/transactions.rs b/test-integration/test-tools/src/transactions.rs index a783cf5c2..44c350254 100644 --- a/test-integration/test-tools/src/transactions.rs +++ b/test-integration/test-tools/src/transactions.rs @@ -54,7 +54,7 @@ pub fn send_transaction( skip_preflight: bool, ) -> Result { let blockhash = rpc_client.get_latest_blockhash()?; - tx.sign(signers, blockhash); + tx.try_sign(signers, blockhash)?; let sig = rpc_client.send_transaction_with_config( tx, RpcSendTransactionConfig {