From 5929469eff5bedc174ff7e52753eb6a05f1f02b0 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Tue, 14 Oct 2025 11:07:26 +0530 Subject: [PATCH 01/13] feat: Add MagicBlockInstruction::ScheduleCommitDiffAndUndelegate to efficiently commit changes in delegated accounts --- Cargo.lock | 189 +++++++++++++-- Cargo.toml | 3 +- .../src/scheduled_commits_processor.rs | 3 + .../src/tasks/args_task.rs | 82 ++++++- magicblock-committor-service/src/tasks/mod.rs | 2 + .../src/tasks/task_builder.rs | 54 +++-- .../tasks/task_visitors/persistor_visitor.rs | 7 +- .../src/instruction.rs | 1 + .../src/magic_scheduled_base_intent.rs | 25 ++ .../magicblock/src/magicblock_processor.rs | 10 + .../process_schedule_commit.rs | 11 +- .../process_scheduled_commit_sent.rs | 7 + test-integration/Cargo.lock | 2 +- test-integration/Cargo.toml | 8 +- .../schedulecommit-security/src/lib.rs | 3 +- .../programs/schedulecommit/Cargo.toml | 2 + .../programs/schedulecommit/src/api.rs | 114 ++++++++- .../programs/schedulecommit/src/lib.rs | 210 ++++++++++++++++- .../programs/schedulecommit/src/order_book.rs | 216 ++++++++++++++++++ .../programs/schedulecommit/src/utils/mod.rs | 15 +- .../client/src/schedule_commit_context.rs | 67 ++++-- .../schedulecommit/client/src/verify.rs | 11 +- .../schedulecommit/test-scenarios/Cargo.toml | 2 + .../test-scenarios/tests/01_commits.rs | 6 +- .../tests/02_commit_and_undelegate.rs | 138 ++++++++++- .../tests/03_commits_fee_payer.rs | 135 +++++++++++ .../test-scenarios/tests/utils/mod.rs | 22 +- .../tests/08_commit_update.rs | 4 +- .../src/integration_test_context.rs | 8 +- .../test-tools/src/scheduled_commits.rs | 16 +- 30 files changed, 1269 insertions(+), 104 deletions(-) create mode 100644 test-integration/programs/schedulecommit/src/order_book.rs create mode 100644 test-integration/schedulecommit/test-scenarios/tests/03_commits_fee_payer.rs diff --git a/Cargo.lock b/Cargo.lock index 5285f84d8..f0fc2dd41 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -54,6 +54,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "version_check", +] + [[package]] name = "ahash" version = "0.8.12" @@ -513,6 +524,18 @@ dependencies = [ "serde", ] +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "blake3" version = "1.8.2" @@ -659,6 +682,28 @@ dependencies = [ "serde", ] +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta 0.1.4", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "bytemuck" version = "1.23.1" @@ -1345,7 +1390,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6503af7917fea18ffef8f7e8553fb8dff89e2e6837e94e09dd7fb069c82d62c" dependencies = [ "bytes", - "rkyv", + "rkyv 0.8.11", "serde", "simdutf8", ] @@ -1492,6 +1537,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.31" @@ -1741,6 +1792,9 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] [[package]] name = "hashbrown" @@ -1748,7 +1802,7 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" dependencies = [ - "ahash", + "ahash 0.8.12", ] [[package]] @@ -2927,8 +2981,8 @@ dependencies = [ [[package]] name = "magicblock-delegation-program" -version = "1.1.0" -source = "git+https://github.com/magicblock-labs/delegation-program.git?rev=aa1de56d90c#aa1de56d90c8a242377accd59899f272f0131f8c" +version = "1.1.2" +source = "git+https://github.com/magicblock-labs/delegation-program.git?rev=e8d03936#e8d039369ac1149e899ea94f31e0f9cc4e600a38" dependencies = [ "bincode", "borsh 1.5.7", @@ -2939,9 +2993,12 @@ dependencies = [ "pinocchio-log", "pinocchio-pubkey", "pinocchio-system", + "rkyv 0.7.45", "solana-curve25519", "solana-program", "solana-security-txt", + "static_assertions", + "strum", "thiserror 1.0.69", ] @@ -3996,13 +4053,33 @@ dependencies = [ "autotools", ] +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive 0.1.4", +] + [[package]] name = "ptr_meta" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe9e76f66d3f9606f44e45598d155cb13ecf09f4a28199e48daf8c8fc937ea90" dependencies = [ - "ptr_meta_derive", + "ptr_meta_derive 0.3.0", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", ] [[package]] @@ -4051,13 +4128,19 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rancor" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "caf5f7161924b9d1cea0e4cabc97c372cea92b5f927fc13c6bca67157a0ad947" dependencies = [ - "ptr_meta", + "ptr_meta 0.3.0", ] [[package]] @@ -4245,6 +4328,15 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + [[package]] name = "rend" version = "0.5.2" @@ -4327,6 +4419,24 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rkyv" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta 0.1.4", + "rend 0.4.2", + "rkyv_derive 0.7.45", + "seahash", + "tinyvec", + "uuid", +] + [[package]] name = "rkyv" version = "0.8.11" @@ -4337,14 +4447,25 @@ dependencies = [ "hashbrown 0.15.4", "indexmap 2.10.0", "munge", - "ptr_meta", + "ptr_meta 0.3.0", "rancor", - "rend", - "rkyv_derive", + "rend 0.5.2", + "rkyv_derive 0.8.11", "tinyvec", "uuid", ] +[[package]] +name = "rkyv_derive" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "rkyv_derive" version = "0.8.11" @@ -4539,6 +4660,12 @@ version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + [[package]] name = "security-framework" version = "2.11.1" @@ -5082,7 +5209,7 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4ee734c35b736e632aa3b1367f933d93ee7b4129dd1e20ca942205d4834054e" dependencies = [ - "ahash", + "ahash 0.8.12", "lazy_static", "log", "qualifier_attr", @@ -5395,7 +5522,7 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89e1d3b52b4a014efeaaab67f14e40af3972a4be61c523d612860db8e3145529" dependencies = [ - "ahash", + "ahash 0.8.12", "lazy_static", "solana-epoch-schedule", "solana-hash", @@ -6627,7 +6754,7 @@ name = "solana-svm" version = "2.2.1" source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=3e9456ec4#3e9456ec4d5798ad8281537501c1e777d6888ba3" dependencies = [ - "ahash", + "ahash 0.8.12", "log", "percentage", "qualifier_attr", @@ -7070,7 +7197,7 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd1adc42def3cb101f3ebef3cd2d642f9a21072bbcd4ec9423343ccaa6afa596" dependencies = [ - "ahash", + "ahash 0.8.12", "bumpalo", "bytes", "cfg-if", @@ -7509,6 +7636,27 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "subtle" version = "2.6.1" @@ -7590,6 +7738,12 @@ dependencies = [ "unicode-width 0.1.14", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "task-local-extensions" version = "0.1.4" @@ -8777,6 +8931,15 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "yansi" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index 05fe94cc5..32189da1b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -95,9 +95,10 @@ magicblock-committor-program = { path = "./magicblock-committor-program", featur magicblock-committor-service = { path = "./magicblock-committor-service" } magicblock-config = { path = "./magicblock-config" } magicblock-core = { path = "./magicblock-core" } -magicblock-delegation-program = { git = "https://github.com/magicblock-labs/delegation-program.git", rev = "aa1de56d90c", features = [ +magicblock-delegation-program = { git = "https://github.com/magicblock-labs/delegation-program.git", rev = "e8d03936", features = [ "no-entrypoint", ] } +magicblock-geyser-plugin = { path = "./magicblock-geyser-plugin" } magicblock-ledger = { path = "./magicblock-ledger" } magicblock-magic-program-api = { path = "./magicblock-magic-program-api" } magicblock-metrics = { path = "./magicblock-metrics" } diff --git a/magicblock-accounts/src/scheduled_commits_processor.rs b/magicblock-accounts/src/scheduled_commits_processor.rs index a4a78ce6c..b11ff44b4 100644 --- a/magicblock-accounts/src/scheduled_commits_processor.rs +++ b/magicblock-accounts/src/scheduled_commits_processor.rs @@ -314,6 +314,7 @@ impl ScheduledCommitsProcessorImpl { requested_undelegation: intent_meta.requested_undelegation, error_message, patched_errors, + commit_diff: intent_meta.commit_diff, } } } @@ -382,6 +383,7 @@ struct ScheduledBaseIntentMeta { included_pubkeys: Vec, intent_sent_transaction: Transaction, requested_undelegation: bool, + commit_diff: bool, } impl ScheduledBaseIntentMeta { @@ -395,6 +397,7 @@ impl ScheduledBaseIntentMeta { .unwrap_or_default(), intent_sent_transaction: intent.action_sent_transaction.clone(), requested_undelegation: intent.is_undelegate(), + commit_diff: intent.is_commit_diff(), } } } diff --git a/magicblock-committor-service/src/tasks/args_task.rs b/magicblock-committor-service/src/tasks/args_task.rs index 8919999c0..7b4ccf19f 100644 --- a/magicblock-committor-service/src/tasks/args_task.rs +++ b/magicblock-committor-service/src/tasks/args_task.rs @@ -1,21 +1,35 @@ -use dlp::args::{CallHandlerArgs, CommitStateArgs}; +use dlp::{ + args::{CallHandlerArgs, CommitDiffArgs, CommitStateArgs}, + compute_diff, +}; use magicblock_metrics::metrics::LabelValue; +use solana_account::ReadableAccount; use solana_instruction::{AccountMeta, Instruction}; use solana_pubkey::Pubkey; +use solana_rpc_client::rpc_client::RpcClient; +use solana_sdk::{ + commitment_config::CommitmentConfig, + instruction::{AccountMeta, Instruction}, +}; #[cfg(test)] use crate::tasks::TaskStrategy; -use crate::tasks::{ - buffer_task::{BufferTask, BufferTaskType}, - visitor::Visitor, - BaseActionTask, BaseTask, BaseTaskError, BaseTaskResult, CommitTask, - FinalizeTask, PreparationState, TaskType, UndelegateTask, +use crate::{ + config::ChainConfig, + tasks::{ + buffer_task::{BufferTask, BufferTaskType}, + visitor::Visitor, + BaseActionTask, BaseTask, BaseTaskError, BaseTaskResult, CommitTask, + FinalizeTask, PreparationState, TaskType, UndelegateTask, + }, + ComputeBudgetConfig, }; /// Task that will be executed on Base layer via arguments #[derive(Clone)] pub enum ArgsTaskType { Commit(CommitTask), + CommitDiff(CommitTask), Finalize(FinalizeTask), Undelegate(UndelegateTask), // Special action really BaseAction(BaseActionTask), @@ -59,6 +73,55 @@ impl BaseTask for ArgsTask { args, ) } + ArgsTaskType::CommitDiff(value) => { + let chain_config = + ChainConfig::local(ComputeBudgetConfig::new(1_000_000)); + + let rpc_client = RpcClient::new_with_commitment( + chain_config.rpc_uri.to_string(), + CommitmentConfig { + commitment: chain_config.commitment, + }, + ); + + let account = match rpc_client + .get_account(&value.committed_account.pubkey) + { + Ok(account) => account, + Err(e) => { + log::warn!("Fallback to commit_state and send full-bytes, as rpc failed to fetch the delegated-account from base chain, commmit_id: {} , error: {}", value.commit_id, e); + let args = CommitStateArgs { + nonce: value.commit_id, + lamports: value.committed_account.account.lamports, + data: value.committed_account.account.data.clone(), + allow_undelegation: value.allow_undelegation, + }; + return dlp::instruction_builder::commit_state( + *validator, + value.committed_account.pubkey, + value.committed_account.account.owner, + args, + ); + } + }; + + let args = CommitDiffArgs { + nonce: value.commit_id, + lamports: value.committed_account.account.lamports, + diff: compute_diff( + account.data(), + value.committed_account.account.data(), + ) + .to_vec(), + allow_undelegation: value.allow_undelegation, + }; + dlp::instruction_builder::commit_diff( + *validator, + value.committed_account.pubkey, + value.committed_account.account.owner, + args, + ) + } ArgsTaskType::Finalize(value) => { dlp::instruction_builder::finalize( *validator, @@ -107,6 +170,8 @@ impl BaseTask for ArgsTask { BufferTaskType::Commit(value), ))) } + // TODO (snawaz): discuss this with reviewers + ArgsTaskType::CommitDiff(_) => Err(self), ArgsTaskType::BaseAction(_) | ArgsTaskType::Finalize(_) | ArgsTaskType::Undelegate(_) => Err(self), @@ -133,6 +198,7 @@ impl BaseTask for ArgsTask { fn compute_units(&self) -> u32 { match &self.task_type { ArgsTaskType::Commit(_) => 70_000, + ArgsTaskType::CommitDiff(_) => 65_000, ArgsTaskType::BaseAction(task) => task.action.compute_units, ArgsTaskType::Undelegate(_) => 70_000, ArgsTaskType::Finalize(_) => 70_000, @@ -147,6 +213,9 @@ impl BaseTask for ArgsTask { fn task_type(&self) -> TaskType { match &self.task_type { ArgsTaskType::Commit(_) => TaskType::Commit, + // TODO (snawaz): What should we use here? Commit (in the sense of "category of task"), or add a + // new variant "CommitDiff" to indicate a specific instruction? + ArgsTaskType::CommitDiff(_) => TaskType::Commit, ArgsTaskType::BaseAction(_) => TaskType::Action, ArgsTaskType::Undelegate(_) => TaskType::Undelegate, ArgsTaskType::Finalize(_) => TaskType::Finalize, @@ -159,6 +228,7 @@ impl BaseTask for ArgsTask { } fn reset_commit_id(&mut self, commit_id: u64) { + // TODO (snawaz): handle CommitDiff as well? what is it about? let ArgsTaskType::Commit(commit_task) = &mut self.task_type else { return; }; diff --git a/magicblock-committor-service/src/tasks/mod.rs b/magicblock-committor-service/src/tasks/mod.rs index 43477e7ce..be61a5d40 100644 --- a/magicblock-committor-service/src/tasks/mod.rs +++ b/magicblock-committor-service/src/tasks/mod.rs @@ -54,6 +54,8 @@ pub enum TaskStrategy { pub trait BaseTask: Send + Sync + DynClone + LabelValue { /// Gets all pubkeys that involved in Task's instruction fn involved_accounts(&self, validator: &Pubkey) -> Vec { + // TODO (snawaz): rewrite it. + // currently it is slow as it discards heavy computations and memory allocations. self.instruction(validator) .accounts .iter() diff --git a/magicblock-committor-service/src/tasks/task_builder.rs b/magicblock-committor-service/src/tasks/task_builder.rs index bc134c6ea..bb76b260e 100644 --- a/magicblock-committor-service/src/tasks/task_builder.rs +++ b/magicblock-committor-service/src/tasks/task_builder.rs @@ -48,25 +48,29 @@ impl TasksBuilder for TaskBuilderImpl { base_intent: &ScheduledBaseIntent, persister: &Option

, ) -> TaskBuilderResult>> { - let (accounts, allow_undelegation) = match &base_intent.base_intent { - MagicBaseIntent::BaseActions(actions) => { - let tasks = actions - .iter() - .map(|el| { - let task = BaseActionTask { action: el.clone() }; - let task = - ArgsTask::new(ArgsTaskType::BaseAction(task)); - Box::new(task) as Box - }) - .collect(); - - return Ok(tasks); - } - MagicBaseIntent::Commit(t) => (t.get_committed_accounts(), false), - MagicBaseIntent::CommitAndUndelegate(t) => { - (t.commit_action.get_committed_accounts(), true) - } - }; + let (accounts, allow_undelegation, commit_diff) = + match &base_intent.base_intent { + MagicBaseIntent::BaseActions(actions) => { + let tasks = actions + .iter() + .map(|el| { + let task = BaseActionTask { action: el.clone() }; + let task = + ArgsTask::new(ArgsTaskType::BaseAction(task)); + Box::new(task) as Box + }) + .collect(); + return Ok(tasks); + } + MagicBaseIntent::Commit(t) => { + (t.get_committed_accounts(), false, t.is_commit_diff()) + } + MagicBaseIntent::CommitAndUndelegate(t) => ( + t.commit_action.get_committed_accounts(), + true, + t.commit_action.is_commit_diff(), + ), + }; let committed_pubkeys = accounts .iter() @@ -90,11 +94,16 @@ impl TasksBuilder for TaskBuilderImpl { .iter() .map(|account| { let commit_id = *commit_ids.get(&account.pubkey).expect("CommitIdFetcher provide commit ids for all listed pubkeys, or errors!"); - let task = ArgsTaskType::Commit(CommitTask { + let task = CommitTask { commit_id, allow_undelegation, committed_account: account.clone(), - }); + }; + let task = if commit_diff { + ArgsTaskType::CommitDiff(task) + } else { + ArgsTaskType::Commit(task) + }; Box::new(ArgsTask::new(task)) as Box }) @@ -135,6 +144,9 @@ impl TasksBuilder for TaskBuilderImpl { CommitType::Standalone(accounts) => { accounts.iter().map(finalize_task).collect() } + CommitType::StandaloneDiff(accounts) => { + accounts.iter().map(finalize_task).collect() + } CommitType::WithBaseActions { committed_accounts, base_actions, diff --git a/magicblock-committor-service/src/tasks/task_visitors/persistor_visitor.rs b/magicblock-committor-service/src/tasks/task_visitors/persistor_visitor.rs index c608f2ef9..1911db187 100644 --- a/magicblock-committor-service/src/tasks/task_visitors/persistor_visitor.rs +++ b/magicblock-committor-service/src/tasks/task_visitors/persistor_visitor.rs @@ -26,9 +26,10 @@ where fn visit_args_task(&mut self, task: &ArgsTask) { match self.context { PersistorContext::PersistStrategy { uses_lookup_tables } => { - let ArgsTaskType::Commit(ref commit_task) = task.task_type - else { - return; + let commit_task = match &task.task_type { + ArgsTaskType::Commit(commit_task) => commit_task, + ArgsTaskType::CommitDiff(commit_task) => commit_task, + _ => return, }; let commit_strategy = if uses_lookup_tables { diff --git a/magicblock-magic-program-api/src/instruction.rs b/magicblock-magic-program-api/src/instruction.rs index 108586ff1..d251f12ba 100644 --- a/magicblock-magic-program-api/src/instruction.rs +++ b/magicblock-magic-program-api/src/instruction.rs @@ -103,6 +103,7 @@ pub enum MagicBlockInstruction { /// Noop instruction Noop(u64), + ScheduleCommitDiffAndUndelegate, } impl MagicBlockInstruction { diff --git a/programs/magicblock/src/magic_scheduled_base_intent.rs b/programs/magicblock/src/magic_scheduled_base_intent.rs index b8e64380b..0a88817fe 100644 --- a/programs/magicblock/src/magic_scheduled_base_intent.rs +++ b/programs/magicblock/src/magic_scheduled_base_intent.rs @@ -102,6 +102,10 @@ impl ScheduledBaseIntent { self.base_intent.is_undelegate() } + pub fn is_commit_diff(&self) -> bool { + self.base_intent.is_commit_diff() + } + pub fn is_empty(&self) -> bool { self.base_intent.is_empty() } @@ -149,6 +153,16 @@ impl MagicBaseIntent { } } + pub fn is_commit_diff(&self) -> bool { + match &self { + MagicBaseIntent::BaseActions(_) => false, + MagicBaseIntent::Commit(c) => c.is_commit_diff(), + MagicBaseIntent::CommitAndUndelegate(c) => { + c.commit_action.is_commit_diff() + } + } + } + pub fn get_committed_accounts(&self) -> Option<&Vec> { match self { MagicBaseIntent::BaseActions(_) => None, @@ -309,6 +323,7 @@ impl BaseAction { } type CommittedAccountRef<'a> = (Pubkey, &'a RefCell); + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct CommittedAccount { pub pubkey: Pubkey, @@ -328,6 +343,7 @@ impl<'a> From> for CommittedAccount { pub enum CommitType { /// Regular commit without actions Standalone(Vec), // accounts to commit + StandaloneDiff(Vec), // accounts to commit /// Commits accounts and runs actions WithBaseActions { committed_accounts: Vec, @@ -451,9 +467,14 @@ impl CommitType { } } + pub fn is_commit_diff(&self) -> bool { + matches!(self, Self::StandaloneDiff(_)) + } + pub fn get_committed_accounts(&self) -> &Vec { match self { Self::Standalone(committed_accounts) => committed_accounts, + Self::StandaloneDiff(committed_accounts) => committed_accounts, Self::WithBaseActions { committed_accounts, .. } => committed_accounts, @@ -463,6 +484,7 @@ impl CommitType { pub fn get_committed_accounts_mut(&mut self) -> &mut Vec { match self { Self::Standalone(committed_accounts) => committed_accounts, + Self::StandaloneDiff(committed_accounts) => committed_accounts, Self::WithBaseActions { committed_accounts, .. } => committed_accounts, @@ -474,6 +496,9 @@ impl CommitType { Self::Standalone(committed_accounts) => { committed_accounts.is_empty() } + Self::StandaloneDiff(committed_accounts) => { + committed_accounts.is_empty() + } Self::WithBaseActions { committed_accounts, .. } => committed_accounts.is_empty(), diff --git a/programs/magicblock/src/magicblock_processor.rs b/programs/magicblock/src/magicblock_processor.rs index 53c132261..c25e5d83f 100644 --- a/programs/magicblock/src/magicblock_processor.rs +++ b/programs/magicblock/src/magicblock_processor.rs @@ -46,6 +46,7 @@ declare_process_instruction!( invoke_context, ProcessScheduleCommitOptions { request_undelegation: false, + request_diff: false, }, ), ScheduleCommitAndUndelegate => process_schedule_commit( @@ -53,6 +54,7 @@ declare_process_instruction!( invoke_context, ProcessScheduleCommitOptions { request_undelegation: true, + request_diff: false, }, ), AcceptScheduleCommits => { @@ -80,6 +82,14 @@ declare_process_instruction!( process_toggle_executable_check(signers, invoke_context, true) } Noop(_) => Ok(()), + ScheduleCommitDiffAndUndelegate => process_schedule_commit( + signers, + invoke_context, + ProcessScheduleCommitOptions { + request_undelegation: true, + request_diff: true, + }, + ), } } ); diff --git a/programs/magicblock/src/schedule_transactions/process_schedule_commit.rs b/programs/magicblock/src/schedule_transactions/process_schedule_commit.rs index 43ab4529f..041d8fd0e 100644 --- a/programs/magicblock/src/schedule_transactions/process_schedule_commit.rs +++ b/programs/magicblock/src/schedule_transactions/process_schedule_commit.rs @@ -26,6 +26,7 @@ use crate::{ #[derive(Default)] pub(crate) struct ProcessScheduleCommitOptions { pub request_undelegation: bool, + pub request_diff: bool, } pub(crate) fn process_schedule_commit( @@ -242,13 +243,19 @@ pub(crate) fn process_schedule_commit( InstructionUtils::scheduled_commit_sent(intent_id, blockhash); let commit_sent_sig = action_sent_transaction.signatures[0]; + let commit_action = if opts.request_diff { + CommitType::StandaloneDiff(committed_accounts) + } else { + CommitType::Standalone(committed_accounts) + }; + let base_intent = if opts.request_undelegation { MagicBaseIntent::CommitAndUndelegate(CommitAndUndelegate { - commit_action: CommitType::Standalone(committed_accounts), + commit_action, undelegate_action: UndelegateType::Standalone, }) } else { - MagicBaseIntent::Commit(CommitType::Standalone(committed_accounts)) + MagicBaseIntent::Commit(commit_action) }; let scheduled_base_intent = ScheduledBaseIntent { id: intent_id, diff --git a/programs/magicblock/src/schedule_transactions/process_scheduled_commit_sent.rs b/programs/magicblock/src/schedule_transactions/process_scheduled_commit_sent.rs index 4b886930f..530e05d0f 100644 --- a/programs/magicblock/src/schedule_transactions/process_scheduled_commit_sent.rs +++ b/programs/magicblock/src/schedule_transactions/process_scheduled_commit_sent.rs @@ -34,6 +34,7 @@ pub struct SentCommit { pub requested_undelegation: bool, pub error_message: Option, pub patched_errors: Vec, + pub commit_diff: bool, } /// This is a printable version of the SentCommit struct. @@ -50,6 +51,7 @@ struct SentCommitPrintable { requested_undelegation: bool, error_message: Option, patched_errors: Vec, + commit_diff: bool, } impl From for SentCommitPrintable { @@ -79,6 +81,7 @@ impl From for SentCommitPrintable { requested_undelegation: commit.requested_undelegation, error_message: commit.error_message, patched_errors: commit.patched_errors, + commit_diff: commit.commit_diff, } } } @@ -221,6 +224,9 @@ pub fn process_scheduled_commit_sent( if commit.requested_undelegation { ic_msg!(invoke_context, "ScheduledCommitSent requested undelegation",); } + if commit.commit_diff { + ic_msg!(invoke_context, "ScheduledCommitSent requested commit_diff",); + } for (idx, error) in commit.patched_errors.iter().enumerate() { ic_msg!( @@ -275,6 +281,7 @@ mod tests { requested_undelegation: false, error_message: None, patched_errors: vec![], + commit_diff: false, } } diff --git a/test-integration/Cargo.lock b/test-integration/Cargo.lock index f2f0eea2a..8e439cdbd 100644 --- a/test-integration/Cargo.lock +++ b/test-integration/Cargo.lock @@ -10956,4 +10956,4 @@ dependencies = [ "bindgen 0.71.1", "cc", "pkg-config", -] +] \ No newline at end of file diff --git a/test-integration/Cargo.toml b/test-integration/Cargo.toml index 6dd2ea45e..f17ae3f2c 100644 --- a/test-integration/Cargo.toml +++ b/test-integration/Cargo.toml @@ -36,8 +36,8 @@ chrono = "0.4" cleanass = "0.0.1" color-backtrace = { version = "0.7" } ctrlc = "3.4.7" -ephemeral-rollups-sdk = { git = "https://github.com/magicblock-labs/ephemeral-rollups-sdk.git", rev = "2d0f16b" } futures = "0.3.31" +ephemeral-rollups-sdk = { path = "../../ephemeral-rollups-sdk/rust/sdk"} integration-test-tools = { path = "test-tools" } isocountry = "0.3.2" lazy_static = "1.4.0" @@ -57,9 +57,7 @@ magicblock-config = { path = "../magicblock-config" } magicblock-core = { path = "../magicblock-core" } magic-domain-program = { git = "https://github.com/magicblock-labs/magic-domain-program.git", rev = "ea04d46", default-features = false } magicblock_magic_program_api = { package = "magicblock-magic-program-api", path = "../magicblock-magic-program-api" } -magicblock-delegation-program = { git = "https://github.com/magicblock-labs/delegation-program.git", rev = "aa1de56d90c", features = [ - "no-entrypoint", -] } +magicblock-delegation-program = { path="../../delegation-program", features = ["no-entrypoint"] } magicblock-program = { path = "../programs/magicblock" } magicblock-rpc-client = { path = "../magicblock-rpc-client" } magicblock-table-mania = { path = "../magicblock-table-mania" } @@ -71,6 +69,7 @@ program-schedulecommit-security = { path = "programs/schedulecommit-security" } rand = "0.8.5" random-port = "0.1.1" rayon = "1.10.0" +rkyv = "0.7.45" schedulecommit-client = { path = "schedulecommit/client" } serde = "1.0.217" serial_test = "3.2.0" @@ -89,6 +88,7 @@ solana-sdk-ids = { version = "2.2" } solana-system-interface = "1.0" solana-transaction-status = "2.2" spl-memo-interface = "1.0" +static_assertions = "1.1.0" teepee = "0.0.1" tempfile = "3.10.1" test-chainlink = { path = "./test-chainlink" } diff --git a/test-integration/programs/schedulecommit-security/src/lib.rs b/test-integration/programs/schedulecommit-security/src/lib.rs index 8236b40a7..8bac187a8 100644 --- a/test-integration/programs/schedulecommit-security/src/lib.rs +++ b/test-integration/programs/schedulecommit-security/src/lib.rs @@ -1,5 +1,5 @@ use borsh::{BorshDeserialize, BorshSerialize}; -use ephemeral_rollups_sdk::ephem::create_schedule_commit_ix; +use ephemeral_rollups_sdk::ephem::{create_schedule_commit_ix, CommitPolicy}; use program_schedulecommit::{ api::schedule_commit_cpi_instruction, process_schedulecommit_cpi, ProcessSchedulecommitCpiArgs, @@ -146,6 +146,7 @@ fn process_sibling_schedule_cpis( magic_context, magic_program, false, + CommitPolicy::UseFullBytes, ); invoke( &direct_ix, diff --git a/test-integration/programs/schedulecommit/Cargo.toml b/test-integration/programs/schedulecommit/Cargo.toml index 9067d6327..272fab194 100644 --- a/test-integration/programs/schedulecommit/Cargo.toml +++ b/test-integration/programs/schedulecommit/Cargo.toml @@ -9,6 +9,8 @@ ephemeral-rollups-sdk = { workspace = true } solana-program = { workspace = true } magicblock-delegation-program = { workspace = true } magicblock_magic_program_api = { workspace = true } +rkyv = { workspace = true } +static_assertions = { workspace = true } [lib] crate-type = ["cdylib", "lib"] diff --git a/test-integration/programs/schedulecommit/src/api.rs b/test-integration/programs/schedulecommit/src/api.rs index adecc662b..18e8d11a7 100644 --- a/test-integration/programs/schedulecommit/src/api.rs +++ b/test-integration/programs/schedulecommit/src/api.rs @@ -9,7 +9,8 @@ use solana_program::{ }; use crate::{ - DelegateCpiArgs, ScheduleCommitCpiArgs, ScheduleCommitInstruction, + BookUpdate, DelegateCpiArgs, DelegateOrderBookArgs, ScheduleCommitCpiArgs, + ScheduleCommitInstruction, }; pub fn init_account_instruction( @@ -32,6 +33,47 @@ pub fn init_account_instruction( ) } +pub fn init_order_book_instruction( + payer: Pubkey, + book_manager: Pubkey, + order_book: Pubkey, +) -> Instruction { + let program_id = crate::id(); + let account_metas = vec![ + AccountMeta::new(payer, true), + AccountMeta::new_readonly(book_manager, true), + AccountMeta::new(order_book, false), + AccountMeta::new_readonly(system_program::id(), false), + ]; + + Instruction::new_with_borsh( + program_id, + &ScheduleCommitInstruction::InitOrderBook, + account_metas, + ) +} + +pub fn grow_order_book_instruction( + payer: Pubkey, + book_manager: Pubkey, + order_book: Pubkey, + additional_space: u64, +) -> Instruction { + let program_id = crate::id(); + let account_metas = vec![ + AccountMeta::new(payer, true), + AccountMeta::new_readonly(book_manager, false), + AccountMeta::new(order_book, false), + AccountMeta::new_readonly(system_program::id(), false), + ]; + + Instruction::new_with_borsh( + program_id, + &ScheduleCommitInstruction::GrowOrderBook(additional_space), + account_metas, + ) +} + pub fn init_payer_escrow(payer: Pubkey) -> [Instruction; 2] { let top_up_ix = dlp::instruction_builder::top_up_ephemeral_balance( payer, @@ -58,17 +100,14 @@ pub fn init_payer_escrow(payer: Pubkey) -> [Instruction; 2] { pub fn delegate_account_cpi_instruction( payer: Pubkey, validator: Option, - player: Pubkey, + player_or_book_manager: Pubkey, + user_seed: &[u8], ) -> Instruction { let program_id = crate::id(); - let (pda, _) = pda_and_bump(&player); - - let args = DelegateCpiArgs { - valid_until: i64::MAX, - commit_frequency_ms: 1_000_000_000, - validator, - player, - }; + let (pda, _) = Pubkey::find_program_address( + &[user_seed, player_or_book_manager.as_ref()], + &crate::ID, + ); let delegate_accounts = DelegateAccounts::new(pda, program_id); let delegate_metas = DelegateAccountMetas::from(delegate_accounts); @@ -85,7 +124,21 @@ pub fn delegate_account_cpi_instruction( Instruction::new_with_borsh( program_id, - &ScheduleCommitInstruction::DelegateCpi(args), + &if user_seed == b"magic_schedule_commit" { + ScheduleCommitInstruction::DelegateCpi(DelegateCpiArgs { + valid_until: i64::MAX, + commit_frequency_ms: 1_000_000_000, + player: player_or_book_manager, + validator, + }) + } else { + ScheduleCommitInstruction::DelegateOrderBook( + DelegateOrderBookArgs { + commit_frequency_ms: 1_000_000_000, + book_manager: player_or_book_manager, + }, + ) + }, account_metas, ) } @@ -121,6 +174,45 @@ pub fn schedule_commit_cpi_instruction( ) } +pub fn update_order_book_instruction( + payer: Pubkey, + order_book: Pubkey, + update: BookUpdate, +) -> Instruction { + let program_id = crate::id(); + let account_metas = vec![ + AccountMeta::new(payer, true), + AccountMeta::new(order_book, false), + ]; + + Instruction::new_with_borsh( + program_id, + &ScheduleCommitInstruction::UpdateOrderBook(update), + account_metas, + ) +} + +pub fn schedule_commit_diff_instruction_for_order_book( + payer: Pubkey, + order_book: Pubkey, + magic_program_id: Pubkey, + magic_context_id: Pubkey, +) -> Instruction { + let program_id = crate::id(); + let account_metas = vec![ + AccountMeta::new(payer, true), + AccountMeta::new(order_book, false), + AccountMeta::new(magic_context_id, false), + AccountMeta::new_readonly(magic_program_id, false), + ]; + + Instruction::new_with_borsh( + program_id, + &ScheduleCommitInstruction::ScheduleCommitForOrderBook, + account_metas, + ) +} + pub fn schedule_commit_with_payer_cpi_instruction( payer: Pubkey, magic_program_id: Pubkey, diff --git a/test-integration/programs/schedulecommit/src/lib.rs b/test-integration/programs/schedulecommit/src/lib.rs index cdd207408..93c6976eb 100644 --- a/test-integration/programs/schedulecommit/src/lib.rs +++ b/test-integration/programs/schedulecommit/src/lib.rs @@ -6,7 +6,10 @@ use ephemeral_rollups_sdk::{ cpi::{ delegate_account, undelegate_account, DelegateAccounts, DelegateConfig, }, - ephem::{commit_accounts, commit_and_undelegate_accounts}, + ephem::{ + commit_accounts, commit_and_undelegate_accounts, + commit_diff_and_undelegate_accounts, + }, }; use magicblock_magic_program_api::instruction::MagicBlockInstruction; use solana_program::{ @@ -15,9 +18,13 @@ use solana_program::{ entrypoint::{self, ProgramResult}, instruction::{AccountMeta, Instruction}, msg, + program::invoke, program::invoke_signed, program_error::ProgramError, pubkey::Pubkey, + rent::Rent, + system_instruction, + sysvar::Sysvar, }; use crate::{ @@ -30,9 +37,13 @@ use crate::{ pub mod api; pub mod magicblock_program; +mod order_book; mod utils; pub const FAIL_UNDELEGATION_COUNT: u64 = u64::MAX - 1; +use order_book::*; + +pub use order_book::{BookUpdate, OrderBookOwned, OrderLevel}; declare_id!("9hgprgZiRWmy8KkfvUuaVkDGrqo9GzeXMohwq6BazgUY"); @@ -47,6 +58,12 @@ pub struct DelegateCpiArgs { validator: Option, } +#[derive(BorshSerialize, BorshDeserialize, Debug, Clone)] +pub struct DelegateOrderBookArgs { + commit_frequency_ms: u32, + book_manager: Pubkey, +} + #[derive(BorshSerialize, BorshDeserialize, Debug, Clone)] pub struct ScheduleCommitCpiArgs { /// Pubkeys of players from which PDAs were derived @@ -132,6 +149,19 @@ pub enum ScheduleCommitInstruction { // // It is not part of this enum as it has a custom discriminator // Undelegate, + /// Initialize an OrderBook + InitOrderBook, + + GrowOrderBook(u64), // additional_space + + /// Delegate order book to ER nodes + DelegateOrderBook(DelegateOrderBookArgs), + + /// Update order book + UpdateOrderBook(BookUpdate), + + /// ScheduleCommitDiffCpi + ScheduleCommitForOrderBook, } pub fn process_instruction<'a>( @@ -155,6 +185,7 @@ pub fn process_instruction<'a>( msg!("ERROR: failed to parse instruction data {:?}", err); ProgramError::InvalidArgument })?; + use ScheduleCommitInstruction::*; match ix { Init => process_init(program_id, accounts), @@ -180,6 +211,15 @@ pub fn process_instruction<'a>( } IncreaseCount => process_increase_count(accounts), SetCount(value) => process_set_count(accounts, value), + InitOrderBook => process_init_order_book(accounts), + GrowOrderBook(additional_space) => { + process_grow_order_book(accounts, additional_space) + } + DelegateOrderBook(args) => process_delegate_order_book(accounts, args), + UpdateOrderBook(args) => process_update_order_book(accounts, args), + ScheduleCommitForOrderBook => { + process_schedulecommit_for_orderbook(accounts) + } } } @@ -193,7 +233,7 @@ pub struct MainAccount { } impl MainAccount { - pub const SIZE: usize = std::mem::size_of::(); + pub const SIZE: u64 = std::mem::size_of::() as u64; pub fn try_decode(data: &[u8]) -> std::io::Result { Self::try_from_slice(data) @@ -267,6 +307,170 @@ fn process_init<'a>( Ok(()) } +// ----------------- +// InitOrderBook +// ----------------- +fn process_init_order_book<'a>( + accounts: &'a [AccountInfo<'a>], +) -> entrypoint::ProgramResult { + msg!("Init OrderBook account"); + let [payer, book_manager, order_book, _system_program] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + assert_is_signer(payer, "payer")?; + + let (pda, bump) = Pubkey::find_program_address( + &[b"order_book", book_manager.key.as_ref()], + &crate::ID, + ); + + assert_keys_equal(order_book.key, &pda, || { + format!( + "PDA for the account ('{}') and for book_manager ('{}') is incorrect", + order_book.key, book_manager.key + ) + })?; + + allocate_account_and_assign_owner(AllocateAndAssignAccountArgs { + payer_info: payer, + account_info: order_book, + owner: &crate::ID, + signer_seeds: &[b"order_book", book_manager.key.as_ref(), &[bump]], + size: 10 * 1024, + })?; + + Ok(()) +} + +fn process_grow_order_book<'a>( + accounts: &'a [AccountInfo<'a>], + additional_space: u64, +) -> entrypoint::ProgramResult { + msg!("Grow OrderBook account"); + let [payer, book_manager, order_book, system_program] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + assert_is_signer(payer, "payer")?; + + let (pda, _bump) = Pubkey::find_program_address( + &[b"order_book", book_manager.key.as_ref()], + &crate::ID, + ); + + assert_keys_equal(order_book.key, &pda, || { + format!( + "PDA for the account ('{}') and for book_manager ('{}') is incorrect", + order_book.key, payer.key + ) + })?; + + let new_size = order_book.data_len() + additional_space as usize; + + // Ideally, we should transfer some lamports from payer to order_book + // so that realloc could use it + + let rent = Rent::get()?; + let required = rent.minimum_balance(new_size); + let current = order_book.lamports(); + if current < required { + let diff = required - current; + invoke( + &system_instruction::transfer(payer.key, order_book.key, diff), + &[payer.clone(), order_book.clone(), system_program.clone()], + )?; + } + + order_book.realloc(new_size, true)?; + + Ok(()) +} + +// ----------------- +// Delegate OrderBook +// ----------------- +pub fn process_delegate_order_book( + accounts: &[AccountInfo], + args: DelegateOrderBookArgs, +) -> Result<(), ProgramError> { + msg!("Processing delegate_order_book instruction"); + + let [payer, order_book, owner_program, buffer, delegation_record, delegation_metadata, delegation_program, system_program] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + let seeds_no_bump = [b"order_book", args.book_manager.as_ref()]; + + delegate_account( + DelegateAccounts { + payer, + pda: order_book, + buffer, + delegation_record, + delegation_metadata, + owner_program, + delegation_program, + system_program, + }, + &seeds_no_bump, + DelegateConfig { + commit_frequency_ms: args.commit_frequency_ms, + ..DelegateConfig::default() + }, + )?; + + Ok(()) +} + +// ----------------- +// UpdateOrderBook +// ----------------- +fn process_update_order_book<'a>( + accounts: &'a [AccountInfo<'a>], + updates: BookUpdate, +) -> entrypoint::ProgramResult { + msg!("Update orderbook"); + let account_info_iter = &mut accounts.iter(); + let payer_info = next_account_info(account_info_iter)?; + let order_book_account = next_account_info(account_info_iter)?; + + assert_is_signer(payer_info, "payer")?; + + let mut book_raw = order_book_account.try_borrow_mut_data()?; + + OrderBook::new(&mut book_raw).update_from(updates); + + Ok(()) +} + +// ----------------- +// Schedule Commit +// ----------------- +pub fn process_schedulecommit_for_orderbook( + accounts: &[AccountInfo], +) -> Result<(), ProgramError> { + msg!("Processing schedulecommit (for orderbook) instruction"); + + let [payer, order_book_account, magic_context, magic_program] = accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + assert_is_signer(payer, "payer")?; + + commit_diff_and_undelegate_accounts( + payer, + vec![order_book_account], + magic_context, + magic_program, + )?; + + Ok(()) +} + // ----------------- // Delegate // ----------------- @@ -373,7 +577,7 @@ pub fn process_schedulecommit_cpi( ); if args.undelegate { - commit_and_undelegate_accounts( + commit_diff_and_undelegate_accounts( payer, committees, magic_context, diff --git a/test-integration/programs/schedulecommit/src/order_book.rs b/test-integration/programs/schedulecommit/src/order_book.rs new file mode 100644 index 000000000..3343064ca --- /dev/null +++ b/test-integration/programs/schedulecommit/src/order_book.rs @@ -0,0 +1,216 @@ +use std::{ + mem::{align_of, size_of}, + slice, +}; + +use borsh::{BorshDeserialize, BorshSerialize}; +use static_assertions::const_assert; + +#[repr(C)] +#[derive( + BorshSerialize, BorshDeserialize, Debug, Clone, Copy, Default, PartialEq, Eq, +)] +pub struct OrderLevel { + pub price: u64, // ideally both fields could be some decimal value + pub size: u64, +} + +const_assert!(align_of::() == align_of::()); +const_assert!(size_of::() == 16); + +#[derive(BorshSerialize, BorshDeserialize, Debug, Clone, Default)] +pub struct BookUpdate { + pub bids: Vec, + pub asks: Vec, +} + +#[repr(C)] +pub struct OrderBookHeader { + pub bids_len: u32, + pub asks_len: u32, +} + +const_assert!(align_of::() == align_of::()); +const_assert!(size_of::() == 8); + +const ORDER_LEVEL_SIZE: usize = std::mem::size_of::(); +const HEADER_SIZE: usize = std::mem::size_of::(); + +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub struct OrderBookOwned { + pub bids: Vec, + pub asks: Vec, +} + +impl From<&OrderBook<'_>> for OrderBookOwned { + fn from(book_ref: &OrderBook) -> Self { + let mut book = Self::default(); + book.bids.extend_from_slice(book_ref.bids()); + book.asks.extend(book_ref.asks_reversed().iter().rev()); + book + } +} + +impl borsh::de::BorshDeserialize for OrderBookOwned { + fn deserialize(buf: &mut &[u8]) -> Result { + let (book_bytes, rest) = buf.split_at(buf.len()); + *buf = rest; // rest is actually empty + + // I make a copy so that I can get mutable bytes in the unsafe block below. + // I could take mutable bytes from &[u8] as well and unsafe block will not + // stop me, but that would break aliasing rules and therefore would invoke UB. + // It's a test code, so copying should be OK. + let book_bytes = { + let mut aligned = rkyv::AlignedVec::with_capacity(book_bytes.len()); + aligned.extend_from_slice(book_bytes); + aligned + }; + + Ok(Self::from(&OrderBook::new(unsafe { + slice::from_raw_parts_mut( + book_bytes.as_ptr() as *mut u8, + book_bytes.len(), + ) + }))) + } + fn deserialize_reader( + _reader: &mut R, + ) -> ::core::result::Result { + unimplemented!("deserialize_reader() not implemented. Please use buffer version as it needs to know size of the buffer") + } +} +pub struct OrderBook<'a> { + header: &'a mut OrderBookHeader, + capacity: usize, + levels: *mut OrderLevel, +} + +impl<'a> OrderBook<'a> { + // + // ========= Zero-Copy Order Book ========== + // + // ----------------------------------------- + // | account data | + // ----------------------------------------- + // | header | levels | + // ----------------------------------------- + // | asks grows -> <- bids grows | + // ----------------------------------------- + // + // Note: + // + // - asks grows towards right + // - bids grows towards left + // + pub fn new(data: &'a mut [u8]) -> Self { + let (header_bytes, levels_bytes) = data.split_at_mut(HEADER_SIZE); + + assert!( + header_bytes + .as_ptr() + .align_offset(align_of::()) + == 0 + && levels_bytes.as_ptr().align_offset(align_of::()) + == 0, + "data is not properly aligned for OrderBook to be constructed" + ); + + Self { + header: unsafe { + &mut *(header_bytes.as_ptr() as *mut OrderBookHeader) + }, + capacity: levels_bytes.len() / ORDER_LEVEL_SIZE, + levels: levels_bytes.as_mut_ptr() as *mut OrderLevel, + } + } + + pub fn update_from(&mut self, updates: BookUpdate) { + self.add_bids(&updates.bids); + self.add_asks(&updates.asks); + } + + pub fn add_bids( + &mut self, + bids: &[OrderLevel], + ) -> Option<&'a [OrderLevel]> { + if self.remaining_capacity() < bids.len() { + return None; + } + let new_bids_len = self.bids_len() + bids.len(); + let bids_space = + unsafe { self.bids_with_uninitialized_slots(new_bids_len) }; + + bids_space[self.bids_len()..].copy_from_slice(bids); + self.header.bids_len = new_bids_len as u32; + + Some(bids_space) + } + + pub fn add_asks( + &mut self, + asks: &[OrderLevel], + ) -> Option<&'a [OrderLevel]> { + if self.remaining_capacity() < asks.len() { + return None; + } + let new_asks_len = self.asks_len() + asks.len(); + let asks_space = + unsafe { self.asks_with_uninitialized_slots(new_asks_len) }; + + // copy in the reverse order + for (dst, src) in + asks_space[..asks.len()].iter_mut().zip(asks.iter().rev()) + { + *dst = *src; + } + self.header.asks_len = new_asks_len as u32; + + Some(asks_space) + } + + pub fn bids(&self) -> &'a [OrderLevel] { + unsafe { slice::from_raw_parts(self.levels, self.bids_len()) } + } + + /// Note that the returned slice is in reverse order, means the first entry is the latest + /// entry and the last entry is the oldest entry. + pub fn asks_reversed(&self) -> &'a [OrderLevel] { + unsafe { + slice::from_raw_parts( + self.levels.add(self.capacity - self.asks_len()), + self.asks_len(), + ) + } + } + + pub fn bids_len(&self) -> usize { + self.header.bids_len as usize + } + + pub fn asks_len(&self) -> usize { + self.header.asks_len as usize + } + + unsafe fn bids_with_uninitialized_slots( + &mut self, + bids_len: usize, + ) -> &'a mut [OrderLevel] { + slice::from_raw_parts_mut(self.levels, bids_len) + } + + unsafe fn asks_with_uninitialized_slots( + &mut self, + asks_len: usize, + ) -> &'a mut [OrderLevel] { + slice::from_raw_parts_mut( + self.levels.add(self.capacity - asks_len), + asks_len as usize, + ) + } + + fn remaining_capacity(&self) -> usize { + self.capacity + .checked_sub((self.header.bids_len + self.header.asks_len) as usize) + .expect("remaining_capacity must exist") + } +} diff --git a/test-integration/programs/schedulecommit/src/utils/mod.rs b/test-integration/programs/schedulecommit/src/utils/mod.rs index 659e37842..fd60458e5 100644 --- a/test-integration/programs/schedulecommit/src/utils/mod.rs +++ b/test-integration/programs/schedulecommit/src/utils/mod.rs @@ -50,7 +50,7 @@ pub struct AllocateAndAssignAccountArgs<'a, 'b> { pub payer_info: &'a AccountInfo<'a>, pub account_info: &'a AccountInfo<'a>, pub owner: &'a Pubkey, - pub size: usize, + pub size: u64, pub signer_seeds: &'b [&'b [u8]], } @@ -68,10 +68,16 @@ pub fn allocate_account_and_assign_owner( } = args; let required_lamports = rent - .minimum_balance(size) + .minimum_balance(size as usize) .max(1) .saturating_sub(account_info.lamports()); + msg!( + "required_lamports: {}, payer has {}", + required_lamports, + payer_info.lamports() + ); + // 1. Transfer the required rent to the account if required_lamports > 0 { transfer_lamports(payer_info, account_info, required_lamports)?; @@ -81,10 +87,7 @@ pub fn allocate_account_and_assign_owner( // At this point the account is still owned by the system program msg!(" create_account() allocate space"); invoke_signed( - &system_instruction::allocate( - account_info.key, - size.try_into().unwrap(), - ), + &system_instruction::allocate(account_info.key, size), // 0. `[WRITE, SIGNER]` New account std::slice::from_ref(account_info), &[signer_seeds], diff --git a/test-integration/schedulecommit/client/src/schedule_commit_context.rs b/test-integration/schedulecommit/client/src/schedule_commit_context.rs index ac30d9aa7..cedf9f297 100644 --- a/test-integration/schedulecommit/client/src/schedule_commit_context.rs +++ b/test-integration/schedulecommit/client/src/schedule_commit_context.rs @@ -5,7 +5,7 @@ use integration_test_tools::IntegrationTestContext; use log::*; use program_schedulecommit::api::{ delegate_account_cpi_instruction, init_account_instruction, - init_payer_escrow, pda_and_bump, + init_order_book_instruction, init_payer_escrow, }; use solana_rpc_client::rpc_client::{RpcClient, SerializableTransaction}; use solana_rpc_client_api::config::RpcSendTransactionConfig; @@ -13,6 +13,7 @@ use solana_rpc_client_api::config::RpcSendTransactionConfig; use solana_sdk::signer::SeedDerivable; use solana_sdk::{ commitment_config::CommitmentConfig, + compute_budget::ComputeBudgetInstruction, hash::Hash, native_token::LAMPORTS_PER_SOL, pubkey::Pubkey, @@ -30,6 +31,7 @@ pub struct ScheduleCommitTestContext { pub payer_ephem: Keypair, // The Payer keypairs along with its PDA pubkey which we'll commit pub committees: Vec<(Keypair, Pubkey)>, + user_seed: Vec, common_ctx: IntegrationTestContext, } @@ -61,14 +63,21 @@ impl ScheduleCommitTestContext { // ----------------- // Init // ----------------- - pub fn try_new_random_keys(ncommittees: usize) -> Result { - Self::try_new_internal(ncommittees, true) + pub fn try_new_random_keys( + ncommittees: usize, + user_seed: &[u8], + ) -> Result { + Self::try_new_internal(ncommittees, true, user_seed) } - pub fn try_new(ncommittees: usize) -> Result { - Self::try_new_internal(ncommittees, false) + pub fn try_new(ncommittees: usize, user_seed: &[u8]) -> Result { + Self::try_new_internal(ncommittees, false, user_seed) } - fn try_new_internal(ncommittees: usize, random_keys: bool) -> Result { + fn try_new_internal( + ncommittees: usize, + random_keys: bool, + user_seed: &[u8], + ) -> Result { let ictx = IntegrationTestContext::try_new()?; let payer_chain = if random_keys { @@ -103,7 +112,10 @@ impl ScheduleCommitTestContext { lamports, ) .unwrap(); - let (pda, _) = pda_and_bump(&payer_ephem.pubkey()); + let (pda, _bump) = Pubkey::find_program_address( + &[user_seed, &payer_ephem.pubkey().as_ref()], + &program_schedulecommit::ID, + ); (payer_ephem, pda) }) .collect::>(); @@ -143,6 +155,7 @@ impl ScheduleCommitTestContext { payer_ephem, committees, common_ctx: ictx, + user_seed: user_seed.to_vec(), }) } @@ -150,17 +163,44 @@ impl ScheduleCommitTestContext { // Schedule Commit specific Transactions // ----------------- pub fn init_committees(&self) -> Result { - let ixs = self - .committees - .iter() - .map(|(player, committee)| { + let mut ixs = vec![ + ComputeBudgetInstruction::set_compute_unit_limit(1_400_000), + ComputeBudgetInstruction::set_compute_unit_price(10_000), + ]; + if self.user_seed == b"magic_schedule_commit" { + ixs.extend(self.committees.iter().map(|(player, committee)| { init_account_instruction( self.payer_chain.pubkey(), player.pubkey(), *committee, ) - }) - .collect::>(); + })); + } else { + ixs.extend(self.committees.iter().map( + |(book_manager, committee)| { + init_order_book_instruction( + self.payer_chain.pubkey(), + book_manager.pubkey(), + *committee, + ) + }, + )); + + //// TODO (snawaz): currently the size of delegatable-account cannot be + //// more than 10K, else delegation will fail. So Let's revisit this when + //// we relax the limit on the account size, then we can use larger + //// account, say even 10 MB, and execute CommitDiff. + // + // ixs.extend(self.committees.iter().flat_map( + // |(payer, committee)| { + // [grow_order_book_instruction( + // payer.pubkey(), + // *committee, + // 10 * 1024 + // )] + // }, + // )); + } let mut signers = self .committees @@ -224,6 +264,7 @@ impl ScheduleCommitTestContext { self.payer_chain.pubkey(), self.ephem_validator_identity, player.pubkey(), + &self.user_seed, ); ixs.push(ix); } diff --git a/test-integration/schedulecommit/client/src/verify.rs b/test-integration/schedulecommit/client/src/verify.rs index 11098f9c9..d383cbc2f 100644 --- a/test-integration/schedulecommit/client/src/verify.rs +++ b/test-integration/schedulecommit/client/src/verify.rs @@ -1,5 +1,5 @@ use integration_test_tools::scheduled_commits::ScheduledCommitResult; -use program_schedulecommit::MainAccount; +use program_schedulecommit::{MainAccount, OrderBookOwned}; use solana_sdk::signature::Signature; use crate::ScheduleCommitTestContext; @@ -12,3 +12,12 @@ pub fn fetch_and_verify_commit_result_from_logs( res.confirm_commit_transactions_on_chain(ctx).unwrap(); res } + +pub fn fetch_and_verify_order_book_commit_result_from_logs( + ctx: &ScheduleCommitTestContext, + sig: Signature, +) -> ScheduledCommitResult { + let res = ctx.fetch_schedule_commit_result(sig).unwrap(); + res.confirm_commit_transactions_on_chain(ctx).unwrap(); + res +} diff --git a/test-integration/schedulecommit/test-scenarios/Cargo.toml b/test-integration/schedulecommit/test-scenarios/Cargo.toml index 93d93863a..3cda55a8f 100644 --- a/test-integration/schedulecommit/test-scenarios/Cargo.toml +++ b/test-integration/schedulecommit/test-scenarios/Cargo.toml @@ -16,3 +16,5 @@ solana-rpc-client = { workspace = true } solana-rpc-client-api = { workspace = true } solana-sdk = { workspace = true } test-kit = { workspace = true } +rand = { workspace = true } +borsh = { workspace = true } diff --git a/test-integration/schedulecommit/test-scenarios/tests/01_commits.rs b/test-integration/schedulecommit/test-scenarios/tests/01_commits.rs index 8f07d51a7..ec0c00dce 100644 --- a/test-integration/schedulecommit/test-scenarios/tests/01_commits.rs +++ b/test-integration/schedulecommit/test-scenarios/tests/01_commits.rs @@ -45,7 +45,8 @@ mod utils; #[test] fn test_committing_one_account() { run_test!({ - let ctx = get_context_with_delegated_committees(1); + let ctx = + get_context_with_delegated_committees(1, b"magic_schedule_commit"); let ScheduleCommitTestContextFields { payer_ephem: payer, @@ -98,7 +99,8 @@ fn test_committing_one_account() { #[test] fn test_committing_two_accounts() { run_test!({ - let ctx = get_context_with_delegated_committees(2); + let ctx = + get_context_with_delegated_committees(2, b"magic_schedule_commit"); let ScheduleCommitTestContextFields { payer_ephem: payer, diff --git a/test-integration/schedulecommit/test-scenarios/tests/02_commit_and_undelegate.rs b/test-integration/schedulecommit/test-scenarios/tests/02_commit_and_undelegate.rs index 550cdd508..94c7dd44b 100644 --- a/test-integration/schedulecommit/test-scenarios/tests/02_commit_and_undelegate.rs +++ b/test-integration/schedulecommit/test-scenarios/tests/02_commit_and_undelegate.rs @@ -13,7 +13,13 @@ use program_schedulecommit::{ set_count_instruction, }, FAIL_UNDELEGATION_COUNT, + schedule_commit_and_undelegate_cpi_with_mod_after_instruction, + schedule_commit_diff_instruction_for_order_book, + update_order_book_instruction, + }, + BookUpdate, OrderLevel, }; +use rand::{RngCore, SeedableRng}; use schedulecommit_client::{ verify, ScheduleCommitTestContext, ScheduleCommitTestContextFields, }; @@ -51,7 +57,8 @@ fn commit_and_undelegate_one_account( Signature, Result, ) { - let ctx = get_context_with_delegated_committees(1); + let ctx = + get_context_with_delegated_committees(1, b"magic_schedule_commit"); let ScheduleCommitTestContextFields { payer_ephem: payer, committees, @@ -105,6 +112,62 @@ fn commit_and_undelegate_one_account( (ctx, *sig, tx_res) } +fn commit_and_undelegate_order_book_account( + update: BookUpdate, +) -> ( + ScheduleCommitTestContext, + Signature, + Result, +) { + let ctx = get_context_with_delegated_committees(1, b"order_book"); + let ScheduleCommitTestContextFields { + payer_ephem, + committees, + commitment, + ephem_client, + .. + } = ctx.fields(); + + assert_eq!(committees.len(), 1); + + let ixs = [ + update_order_book_instruction( + payer_ephem.pubkey(), + committees[0].1, + update, + ), + schedule_commit_diff_instruction_for_order_book( + payer_ephem.pubkey(), + committees[0].1, + magicblock_magic_program_api::id(), + magicblock_magic_program_api::MAGIC_CONTEXT_PUBKEY, + ), + ]; + + let ephem_blockhash = ephem_client.get_latest_blockhash().unwrap(); + let tx = Transaction::new_signed_with_payer( + &ixs, + Some(&payer_ephem.pubkey()), + &[&payer_ephem], + ephem_blockhash, + ); + + let sig = tx.get_signature(); + let tx_res = ephem_client + .send_and_confirm_transaction_with_spinner_and_config( + &tx, + *commitment, + RpcSendTransactionConfig { + skip_preflight: true, + ..Default::default() + }, + ); + println!("txhash (scheduled_commit): {:?}", tx_res); + + debug!("Commit and Undelegate Transaction result: '{:?}'", tx_res); + (ctx, *sig, tx_res) +} + fn commit_and_undelegate_two_accounts( modify_after: bool, ) -> ( @@ -112,7 +175,8 @@ fn commit_and_undelegate_two_accounts( Signature, Result, ) { - let ctx = get_context_with_delegated_committees(2); + let ctx = + get_context_with_delegated_committees(2, b"magic_schedule_commit"); let ScheduleCommitTestContextFields { payer_ephem: payer, committees, @@ -230,6 +294,76 @@ fn test_committing_and_undelegating_one_account() { }); } +#[test] +fn test_committing_and_undelegating_huge_order_book_account() { + run_test!({ + let (rng_seed, update) = { + use rand::{ + rngs::{OsRng, StdRng}, + Rng, + }; + let rng_seed = OsRng.next_u64(); + println!("Use {rng_seed} as seed to random generator"); + let mut random = StdRng::seed_from_u64(rng_seed); + let mut update = BookUpdate::default(); + update.bids.extend((0..random.gen_range(5..10)).map(|_| { + OrderLevel { + price: random.gen_range(75000..90000), + size: random.gen_range(1..10), + } + })); + update.asks.extend((0..random.gen_range(5..10)).map(|_| { + OrderLevel { + price: random.gen_range(125000..150000), + size: random.gen_range(1..10), + } + })); + println!( + "BookUpdate: total = {}, bids = {}, asks = {}", + update.bids.len() + update.asks.len(), + update.bids.len(), + update.asks.len() + ); + (rng_seed, update) + }; + let (ctx, sig, tx_res) = + commit_and_undelegate_order_book_account(update.clone()); + info!("'{}' {:?}", sig, tx_res); + + let res = verify::fetch_and_verify_order_book_commit_result_from_logs( + &ctx, sig, + ); + + let book = res + .included + .values() + .next() + .expect("one order-book must exist"); + + assert_eq!( + book.bids.len(), + update.bids.len(), + "Use {rng_seed} to generate the input and investigate" + ); + assert_eq!( + book.asks.len(), + update.asks.len(), + "Use {rng_seed} to generate the input and investigate" + ); + assert_eq!( + book.bids, update.bids, + "Use {rng_seed} to generate the input and investigate" + ); + assert_eq!( + book.asks, update.asks, + "Use {rng_seed} to generate the input and investigate" + ); + + assert_one_committee_was_committed(&ctx, &res, true); + assert_one_committee_account_was_undelegated_on_chain(&ctx); + }); +} + #[test] fn test_committing_and_undelegating_two_accounts_success() { run_test!({ diff --git a/test-integration/schedulecommit/test-scenarios/tests/03_commits_fee_payer.rs b/test-integration/schedulecommit/test-scenarios/tests/03_commits_fee_payer.rs new file mode 100644 index 000000000..b7118b26a --- /dev/null +++ b/test-integration/schedulecommit/test-scenarios/tests/03_commits_fee_payer.rs @@ -0,0 +1,135 @@ +use integration_test_tools::run_test; +use log::*; +use program_schedulecommit::api::schedule_commit_with_payer_cpi_instruction; +use schedulecommit_client::{verify, ScheduleCommitTestContextFields}; +use solana_rpc_client::rpc_client::SerializableTransaction; +use solana_rpc_client_api::config::RpcSendTransactionConfig; +use solana_sdk::{signer::Signer, transaction::Transaction}; +use test_tools_core::init_logger; +use utils::{ + assert_two_committees_synchronized_count, + assert_two_committees_were_committed, + get_context_with_delegated_committees, +}; + +use crate::utils::{ + assert_feepayer_was_committed, + get_context_with_delegated_committees_without_payer_escrow, +}; + +mod utils; + +#[test] +fn test_committing_fee_payer_without_escrowing_lamports() { + // NOTE: this test requires the following config + // [validator] + // base_fees = 1000 + // see ../../../configs/schedulecommit-conf-fees.ephem.toml + run_test!({ + let ctx = get_context_with_delegated_committees_without_payer_escrow( + 2, + b"magic_schedule_commit", + ); + + let ScheduleCommitTestContextFields { + payer, + committees, + commitment, + ephem_client, + ephem_blockhash, + .. + } = ctx.fields(); + + let ix = schedule_commit_with_payer_cpi_instruction( + payer.pubkey(), + magicblock_magic_program_api::id(), + magicblock_magic_program_api::MAGIC_CONTEXT_PUBKEY, + &committees + .iter() + .map(|(player, _)| player.pubkey()) + .collect::>(), + &committees.iter().map(|(_, pda)| *pda).collect::>(), + ); + + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer], + *ephem_blockhash, + ); + + let sig = tx.get_signature(); + let res = ephem_client + .send_and_confirm_transaction_with_spinner_and_config( + &tx, + *commitment, + RpcSendTransactionConfig { + skip_preflight: true, + ..Default::default() + }, + ); + info!("{} '{:?}'", sig, res); + + assert!(res.is_err()); + assert!(res + .err() + .unwrap() + .to_string() + .contains("DoesNotHaveEscrowAccount")); + }); +} + +#[test] +fn test_committing_fee_payer_escrowing_lamports() { + run_test!({ + let ctx = + get_context_with_delegated_committees(2, b"magic_schedule_commit"); + + let ScheduleCommitTestContextFields { + payer, + committees, + commitment, + ephem_client, + ephem_blockhash, + .. + } = ctx.fields(); + + let ix = schedule_commit_with_payer_cpi_instruction( + payer.pubkey(), + magicblock_magic_program_api::id(), + magicblock_magic_program_api::MAGIC_CONTEXT_PUBKEY, + &committees + .iter() + .map(|(player, _)| player.pubkey()) + .collect::>(), + &committees.iter().map(|(_, pda)| *pda).collect::>(), + ); + + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer], + *ephem_blockhash, + ); + + let sig = tx.get_signature(); + let res = ephem_client + .send_and_confirm_transaction_with_spinner_and_config( + &tx, + *commitment, + RpcSendTransactionConfig { + skip_preflight: true, + ..Default::default() + }, + ); + info!("{} '{:?}'", sig, res); + assert!(res.is_ok()); + + let res = verify::fetch_and_verify_commit_result_from_logs(&ctx, *sig); + assert_two_committees_were_committed(&ctx, &res, true); + assert_two_committees_synchronized_count(&ctx, &res, 1); + + // The fee payer should have been committed + assert_feepayer_was_committed(&ctx, &res, true); + }); +} diff --git a/test-integration/schedulecommit/test-scenarios/tests/utils/mod.rs b/test-integration/schedulecommit/test-scenarios/tests/utils/mod.rs index eb8f80dfb..cf1ae2c87 100644 --- a/test-integration/schedulecommit/test-scenarios/tests/utils/mod.rs +++ b/test-integration/schedulecommit/test-scenarios/tests/utils/mod.rs @@ -15,16 +15,22 @@ use solana_sdk::{ // ----------------- pub fn get_context_with_delegated_committees( ncommittees: usize, + user_seed: &[u8], ) -> ScheduleCommitTestContext { let ctx = if std::env::var("FIXED_KP").is_ok() { - ScheduleCommitTestContext::try_new(ncommittees) + ScheduleCommitTestContext::try_new(ncommittees, user_seed) } else { - ScheduleCommitTestContext::try_new_random_keys(ncommittees) + ScheduleCommitTestContext::try_new_random_keys(ncommittees, user_seed) } .unwrap(); + println!("get_context_with_delegated_committees inside"); + + let txhash = ctx.init_committees().unwrap(); + println!("txhash (init_committees): {}", txhash); + + let txhash = ctx.delegate_committees().unwrap(); + println!("txhash (delegate_committees): {}", txhash); - ctx.init_committees().unwrap(); - ctx.delegate_committees().unwrap(); ctx } @@ -32,11 +38,13 @@ pub fn get_context_with_delegated_committees( // Asserts // ----------------- #[allow(dead_code)] // used in 02_commit_and_undelegate.rs -pub fn assert_one_committee_was_committed( +pub fn assert_one_committee_was_committed( ctx: &ScheduleCommitTestContext, - res: &ScheduledCommitResult, + res: &ScheduledCommitResult, is_single_stage: bool, -) { +) +where + T: std::fmt::Debug + borsh::BorshDeserialize + PartialEq + Eq { let pda = ctx.committees[0].1; assert_eq!(res.included.len(), 1, "includes 1 pda"); diff --git a/test-integration/test-ledger-restore/tests/08_commit_update.rs b/test-integration/test-ledger-restore/tests/08_commit_update.rs index 699a7d03d..da24d2af2 100644 --- a/test-integration/test-ledger-restore/tests/08_commit_update.rs +++ b/test-integration/test-ledger-restore/tests/08_commit_update.rs @@ -52,7 +52,7 @@ fn test_restore_ledger_committed_and_updated_account() { fn write(ledger_path: &Path, payer: &Keypair) -> (Child, u64) { let programs = get_programs_with_flexi_counter(); - let (_, mut validator, ctx) = setup_validator_with_local_remote( + let (_tmpdir, mut validator, ctx) = setup_validator_with_local_remote( ledger_path, Some(programs), true, @@ -167,7 +167,7 @@ fn read(ledger_path: &Path, payer_kp: &Keypair) -> Child { let payer = &payer_kp.pubkey(); let programs = get_programs_with_flexi_counter(); - let (_, mut validator, ctx) = setup_validator_with_local_remote( + let (_tmpdir, mut validator, ctx) = setup_validator_with_local_remote( ledger_path, Some(programs), false, diff --git a/test-integration/test-tools/src/integration_test_context.rs b/test-integration/test-tools/src/integration_test_context.rs index fcfc346e5..35462161a 100644 --- a/test-integration/test-tools/src/integration_test_context.rs +++ b/test-integration/test-tools/src/integration_test_context.rs @@ -157,7 +157,8 @@ impl IntegrationTestContext { rpc_client: Option<&RpcClient>, label: &str, ) -> Option> { - let rpc_client = rpc_client.or(self.chain_client.as_ref())?; + let rpc_client = + rpc_client.expect("rpc_client for [{}] does not exist"); // Try this up to 50 times since devnet here returns the version response instead of // the EncodedConfirmedTransactionWithStatusMeta at times @@ -166,6 +167,11 @@ impl IntegrationTestContext { &sig, RpcTransactionConfig { commitment: Some(self.commitment), + max_supported_transaction_version: if label == "chain" { + Some(0) + } else { + None + }, ..Default::default() }, ) { diff --git a/test-integration/test-tools/src/scheduled_commits.rs b/test-integration/test-tools/src/scheduled_commits.rs index 038326a06..95260078f 100644 --- a/test-integration/test-tools/src/scheduled_commits.rs +++ b/test-integration/test-tools/src/scheduled_commits.rs @@ -179,7 +179,7 @@ impl IntegrationTestContext { { // 1. Find scheduled commit sent signature via // ScheduledCommitSent signature: - let (ephem_logs, scheduled_commmit_sent_sig) = { + let (ephem_logs_l1, scheduled_commmit_sent_sig) = { let logs = self.fetch_ephemeral_logs(sig).with_context(|| { format!( "Scheduled commit sent logs not found for sig {:?}", @@ -195,18 +195,22 @@ impl IntegrationTestContext { (logs, sig) }; + println!("Ephem Logs level-1: {:#?}", ephem_logs_l1); + // 2. Find chain commit signatures - let chain_logs = self + let ephem_logs_l2 = self .fetch_ephemeral_logs(scheduled_commmit_sent_sig) .with_context(|| { format!( "Logs {:#?}\nScheduled commit sent sig {:?}", - ephem_logs, scheduled_commmit_sent_sig + ephem_logs_l1, scheduled_commmit_sent_sig ) })?; + println!("Ephem Logs level-2: {:#?}", ephem_logs_l2); + let (included, excluded, feepayers, sigs) = - extract_sent_commit_info_from_logs(&chain_logs); + extract_sent_commit_info_from_logs(&ephem_logs_l2); let mut committed_accounts = HashMap::new(); for pubkey in included { @@ -226,6 +230,10 @@ impl IntegrationTestContext { }; } + for sig in sigs.iter() { + self.dump_chain_logs(sig.clone()); + } + Ok(ScheduledCommitResult { included: committed_accounts, excluded, From 7bc570e373dda97e22d161a40c52761b6384ac7d Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Sun, 2 Nov 2025 12:44:51 +0530 Subject: [PATCH 02/13] Redesign the implementation of CommitDiff support --- Cargo.lock | 1 + .../src/scheduled_commits_processor.rs | 3 - magicblock-committor-service/Cargo.toml | 1 + .../src/intent_executor/task_info_fetcher.rs | 53 +++- .../src/tasks/args_task.rs | 103 ++------ magicblock-committor-service/src/tasks/mod.rs | 241 ++++++++++++++---- .../src/tasks/task_builder.rs | 77 +++--- .../src/tasks/task_strategist.rs | 125 ++++----- .../tasks/task_visitors/persistor_visitor.rs | 7 +- .../src/instruction.rs | 1 - .../src/magic_scheduled_base_intent.rs | 24 -- .../magicblock/src/magicblock_processor.rs | 10 - .../process_schedule_commit.rs | 11 +- .../process_scheduled_commit_sent.rs | 7 - test-integration/Cargo.toml | 6 +- .../schedulecommit-security/src/lib.rs | 3 +- .../programs/schedulecommit/src/api.rs | 1 + .../programs/schedulecommit/src/lib.rs | 13 +- .../programs/schedulecommit/src/order_book.rs | 2 +- .../client/src/schedule_commit_context.rs | 78 +++--- test-integration/schedulecommit/elfs/dlp.so | Bin 341272 -> 347672 bytes .../test-scenarios/tests/01_commits.rs | 1 + .../tests/02_commit_and_undelegate.rs | 2 +- .../tests/03_commits_fee_payer.rs | 135 ---------- .../test-scenarios/tests/utils/mod.rs | 6 +- .../test-security/tests/01_invocations.rs | 7 +- .../test-committor-service/tests/common.rs | 37 ++- .../tests/test_delivery_preparator.rs | 41 ++- .../tests/test_ix_commit_local.rs | 38 +-- .../tests/test_transaction_preparator.rs | 62 +++-- .../tests/utils/transactions.rs | 12 +- .../src/integration_test_context.rs | 7 +- .../test-tools/src/scheduled_commits.rs | 2 +- 33 files changed, 549 insertions(+), 568 deletions(-) delete mode 100644 test-integration/schedulecommit/test-scenarios/tests/03_commits_fee_payer.rs diff --git a/Cargo.lock b/Cargo.lock index f0fc2dd41..7fe9c62b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2909,6 +2909,7 @@ dependencies = [ "log", "lru", "magicblock-committor-program", + "magicblock-config", "magicblock-delegation-program", "magicblock-metrics", "magicblock-program", diff --git a/magicblock-accounts/src/scheduled_commits_processor.rs b/magicblock-accounts/src/scheduled_commits_processor.rs index b11ff44b4..a4a78ce6c 100644 --- a/magicblock-accounts/src/scheduled_commits_processor.rs +++ b/magicblock-accounts/src/scheduled_commits_processor.rs @@ -314,7 +314,6 @@ impl ScheduledCommitsProcessorImpl { requested_undelegation: intent_meta.requested_undelegation, error_message, patched_errors, - commit_diff: intent_meta.commit_diff, } } } @@ -383,7 +382,6 @@ struct ScheduledBaseIntentMeta { included_pubkeys: Vec, intent_sent_transaction: Transaction, requested_undelegation: bool, - commit_diff: bool, } impl ScheduledBaseIntentMeta { @@ -397,7 +395,6 @@ impl ScheduledBaseIntentMeta { .unwrap_or_default(), intent_sent_transaction: intent.action_sent_transaction.clone(), requested_undelegation: intent.is_undelegate(), - commit_diff: intent.is_commit_diff(), } } } diff --git a/magicblock-committor-service/Cargo.toml b/magicblock-committor-service/Cargo.toml index 73db3b928..2bd13e61f 100644 --- a/magicblock-committor-service/Cargo.toml +++ b/magicblock-committor-service/Cargo.toml @@ -29,6 +29,7 @@ magicblock-metrics = { workspace = true } magicblock-program = { workspace = true } magicblock-rpc-client = { workspace = true } magicblock-table-mania = { workspace = true } +magicblock-config = { workspace = true } rusqlite = { workspace = true } solana-account = { workspace = true } solana-address-lookup-table-interface = { workspace = true } diff --git a/magicblock-committor-service/src/intent_executor/task_info_fetcher.rs b/magicblock-committor-service/src/intent_executor/task_info_fetcher.rs index a278a01fd..1d86400a6 100644 --- a/magicblock-committor-service/src/intent_executor/task_info_fetcher.rs +++ b/magicblock-committor-service/src/intent_executor/task_info_fetcher.rs @@ -9,7 +9,10 @@ use dlp::{ use log::warn; use lru::LruCache; use magicblock_metrics::metrics; -use magicblock_rpc_client::{MagicBlockRpcClientError, MagicblockRpcClient}; +use magicblock_rpc_client::{ + MagicBlockRpcClientError, MagicBlockRpcClientResult, MagicblockRpcClient, +}; +use solana_account::Account; use solana_pubkey::Pubkey; use solana_signature::Signature; @@ -36,6 +39,13 @@ pub trait TaskInfoFetcher: Send + Sync + 'static { /// Resets cache for some or all accounts fn reset(&self, reset_type: ResetType); + + async fn get_base_account( + &self, + _pubkey: &Pubkey, + ) -> MagicBlockRpcClientResult> { + Ok(None) // AccountNotFound + } } pub enum ResetType<'a> { @@ -263,6 +273,13 @@ impl TaskInfoFetcher for CacheTaskInfoFetcher { } } } + + async fn get_base_account( + &self, + pubkey: &Pubkey, + ) -> MagicBlockRpcClientResult> { + self.rpc_client.get_account(pubkey).await + } } #[derive(thiserror::Error, Debug)] @@ -292,3 +309,37 @@ impl TaskInfoFetcherError { } pub type TaskInfoFetcherResult = Result; + +#[cfg(any(test, feature = "dev-context-only-utils"))] +pub struct NullTaskInfoFetcher; + +#[cfg(any(test, feature = "dev-context-only-utils"))] +#[async_trait] +impl TaskInfoFetcher for NullTaskInfoFetcher { + async fn fetch_next_commit_ids( + &self, + _pubkeys: &[Pubkey], + ) -> TaskInfoFetcherResult> { + Ok(Default::default()) + } + + async fn fetch_rent_reimbursements( + &self, + _pubkeys: &[Pubkey], + ) -> TaskInfoFetcherResult> { + Ok(Default::default()) + } + + fn peek_commit_id(&self, _pubkey: &Pubkey) -> Option { + None + } + + fn reset(&self, _: ResetType) {} + + async fn get_base_account( + &self, + _pubkey: &Pubkey, + ) -> MagicBlockRpcClientResult> { + Ok(None) // AccountNotFound + } +} diff --git a/magicblock-committor-service/src/tasks/args_task.rs b/magicblock-committor-service/src/tasks/args_task.rs index 7b4ccf19f..75a960b04 100644 --- a/magicblock-committor-service/src/tasks/args_task.rs +++ b/magicblock-committor-service/src/tasks/args_task.rs @@ -6,30 +6,20 @@ use magicblock_metrics::metrics::LabelValue; use solana_account::ReadableAccount; use solana_instruction::{AccountMeta, Instruction}; use solana_pubkey::Pubkey; -use solana_rpc_client::rpc_client::RpcClient; -use solana_sdk::{ - commitment_config::CommitmentConfig, - instruction::{AccountMeta, Instruction}, -}; #[cfg(test)] use crate::tasks::TaskStrategy; -use crate::{ - config::ChainConfig, - tasks::{ - buffer_task::{BufferTask, BufferTaskType}, - visitor::Visitor, - BaseActionTask, BaseTask, BaseTaskError, BaseTaskResult, CommitTask, - FinalizeTask, PreparationState, TaskType, UndelegateTask, - }, - ComputeBudgetConfig, +use crate::tasks::{ + buffer_task::{BufferTask, BufferTaskType}, + visitor::Visitor, + BaseActionTask, BaseTask, BaseTaskError, BaseTaskResult, CommitTask, + FinalizeTask, PreparationState, TaskType, UndelegateTask, }; /// Task that will be executed on Base layer via arguments #[derive(Clone)] pub enum ArgsTaskType { Commit(CommitTask), - CommitDiff(CommitTask), Finalize(FinalizeTask), Undelegate(UndelegateTask), // Special action really BaseAction(BaseActionTask), @@ -59,69 +49,7 @@ impl ArgsTask { impl BaseTask for ArgsTask { fn instruction(&self, validator: &Pubkey) -> Instruction { match &self.task_type { - ArgsTaskType::Commit(value) => { - let args = CommitStateArgs { - nonce: value.commit_id, - lamports: value.committed_account.account.lamports, - data: value.committed_account.account.data.clone(), - allow_undelegation: value.allow_undelegation, - }; - dlp::instruction_builder::commit_state( - *validator, - value.committed_account.pubkey, - value.committed_account.account.owner, - args, - ) - } - ArgsTaskType::CommitDiff(value) => { - let chain_config = - ChainConfig::local(ComputeBudgetConfig::new(1_000_000)); - - let rpc_client = RpcClient::new_with_commitment( - chain_config.rpc_uri.to_string(), - CommitmentConfig { - commitment: chain_config.commitment, - }, - ); - - let account = match rpc_client - .get_account(&value.committed_account.pubkey) - { - Ok(account) => account, - Err(e) => { - log::warn!("Fallback to commit_state and send full-bytes, as rpc failed to fetch the delegated-account from base chain, commmit_id: {} , error: {}", value.commit_id, e); - let args = CommitStateArgs { - nonce: value.commit_id, - lamports: value.committed_account.account.lamports, - data: value.committed_account.account.data.clone(), - allow_undelegation: value.allow_undelegation, - }; - return dlp::instruction_builder::commit_state( - *validator, - value.committed_account.pubkey, - value.committed_account.account.owner, - args, - ); - } - }; - - let args = CommitDiffArgs { - nonce: value.commit_id, - lamports: value.committed_account.account.lamports, - diff: compute_diff( - account.data(), - value.committed_account.account.data(), - ) - .to_vec(), - allow_undelegation: value.allow_undelegation, - }; - dlp::instruction_builder::commit_diff( - *validator, - value.committed_account.pubkey, - value.committed_account.account.owner, - args, - ) - } + ArgsTaskType::Commit(value) => value.create_commit_ix(validator), ArgsTaskType::Finalize(value) => { dlp::instruction_builder::finalize( *validator, @@ -165,13 +93,23 @@ impl BaseTask for ArgsTask { self: Box, ) -> Result, Box> { match self.task_type { + ArgsTaskType::Commit(mut value) if value.is_commit_diff() => { + // TODO (snawaz): Currently, we do not support executing CommitDiff + // as BufferTask, which is why we're forcing CommitTask to use CommitState + // before converting this task into BufferTask. Once CommitDiff is supported + // by BufferTask, we do not have to force_commit_state and we can remove + // force_commit_state stuff, as it's essentially a downgrade. + + value.force_commit_state(); + Ok(Box::new(BufferTask::new_preparation_required( + BufferTaskType::Commit(value), + ))) + } ArgsTaskType::Commit(value) => { Ok(Box::new(BufferTask::new_preparation_required( BufferTaskType::Commit(value), ))) } - // TODO (snawaz): discuss this with reviewers - ArgsTaskType::CommitDiff(_) => Err(self), ArgsTaskType::BaseAction(_) | ArgsTaskType::Finalize(_) | ArgsTaskType::Undelegate(_) => Err(self), @@ -198,7 +136,6 @@ impl BaseTask for ArgsTask { fn compute_units(&self) -> u32 { match &self.task_type { ArgsTaskType::Commit(_) => 70_000, - ArgsTaskType::CommitDiff(_) => 65_000, ArgsTaskType::BaseAction(task) => task.action.compute_units, ArgsTaskType::Undelegate(_) => 70_000, ArgsTaskType::Finalize(_) => 70_000, @@ -213,9 +150,6 @@ impl BaseTask for ArgsTask { fn task_type(&self) -> TaskType { match &self.task_type { ArgsTaskType::Commit(_) => TaskType::Commit, - // TODO (snawaz): What should we use here? Commit (in the sense of "category of task"), or add a - // new variant "CommitDiff" to indicate a specific instruction? - ArgsTaskType::CommitDiff(_) => TaskType::Commit, ArgsTaskType::BaseAction(_) => TaskType::Action, ArgsTaskType::Undelegate(_) => TaskType::Undelegate, ArgsTaskType::Finalize(_) => TaskType::Finalize, @@ -228,7 +162,6 @@ impl BaseTask for ArgsTask { } fn reset_commit_id(&mut self, commit_id: u64) { - // TODO (snawaz): handle CommitDiff as well? what is it about? let ArgsTaskType::Commit(commit_task) = &mut self.task_type else { return; }; diff --git a/magicblock-committor-service/src/tasks/mod.rs b/magicblock-committor-service/src/tasks/mod.rs index be61a5d40..9b4377e63 100644 --- a/magicblock-committor-service/src/tasks/mod.rs +++ b/magicblock-committor-service/src/tasks/mod.rs @@ -1,3 +1,9 @@ +use std::sync::Arc; + +use dlp::{ + args::{CommitDiffArgs, CommitStateArgs}, + compute_diff, +}; use dyn_clone::DynClone; use magicblock_committor_program::{ instruction_builder::{ @@ -14,11 +20,15 @@ use magicblock_metrics::metrics::LabelValue; use magicblock_program::magic_scheduled_base_intent::{ BaseAction, CommittedAccount, }; +use solana_account::{Account, ReadableAccount}; use solana_instruction::Instruction; use solana_pubkey::Pubkey; use thiserror::Error; -use crate::tasks::visitor::Visitor; +use crate::{ + intent_executor::task_info_fetcher::TaskInfoFetcher, + tasks::visitor::Visitor, +}; pub mod args_task; pub mod buffer_task; @@ -105,6 +115,116 @@ pub struct CommitTask { pub commit_id: u64, pub allow_undelegation: bool, pub committed_account: CommittedAccount, + base_account: Option, + force_commit_state: bool, +} + +impl CommitTask { + // Accounts larger than COMMIT_STATE_SIZE_THRESHOLD, use CommitDiff to + // reduce instruction size. Below this, commit is sent as CommitState. + // Chose 256 as thresold seems good enough as it could hold 8 u32 fields + // or 4 u64 fields. + const COMMIT_STATE_SIZE_THRESHOLD: usize = 256; + + pub async fn new( + commit_id: u64, + allow_undelegation: bool, + committed_account: CommittedAccount, + task_info_fetcher: &Arc, + ) -> Self { + let base_account = if committed_account.account.data.len() + > CommitTask::COMMIT_STATE_SIZE_THRESHOLD + { + match task_info_fetcher + .get_base_account(&committed_account.pubkey) + .await + { + Ok(Some(account)) => Some(account), + Ok(None) => { + log::warn!("AccountNotFound for commit_diff, pubkey: {}, commit_id: {}, Falling back to commit_state.", + committed_account.pubkey, commit_id); + None + } + Err(e) => { + log::warn!("Failed to fetch base account for commit diff, pubkey: {}, commit_id: {}, error: {}. Falling back to commit_state.", + committed_account.pubkey, commit_id, e); + None + } + } + } else { + None + }; + + Self { + commit_id, + allow_undelegation, + committed_account, + base_account, + force_commit_state: false, + } + } + + pub fn is_commit_diff(&self) -> bool { + !self.force_commit_state + && self.committed_account.account.data.len() + > CommitTask::COMMIT_STATE_SIZE_THRESHOLD + && self.base_account.is_some() + } + + pub fn force_commit_state(&mut self) { + self.force_commit_state = true; + } + + pub fn create_commit_ix(&self, validator: &Pubkey) -> Instruction { + if let Some(fetched_account) = self.base_account.as_ref() { + self.create_commit_diff_ix(validator, fetched_account) + } else { + self.create_commit_state_ix(validator) + } + } + + fn create_commit_state_ix(&self, validator: &Pubkey) -> Instruction { + let args = CommitStateArgs { + nonce: self.commit_id, + lamports: self.committed_account.account.lamports, + data: self.committed_account.account.data.clone(), + allow_undelegation: self.allow_undelegation, + }; + dlp::instruction_builder::commit_state( + *validator, + self.committed_account.pubkey, + self.committed_account.account.owner, + args, + ) + } + + fn create_commit_diff_ix( + &self, + validator: &Pubkey, + fetched_account: &Account, + ) -> Instruction { + if self.force_commit_state { + return self.create_commit_state_ix(validator); + } + + let args = CommitDiffArgs { + nonce: self.commit_id, + lamports: self.committed_account.account.lamports, + diff: compute_diff( + fetched_account.data(), + self.committed_account.account.data(), + ) + .to_vec(), + allow_undelegation: self.allow_undelegation, + }; + + dlp::instruction_builder::commit_diff( + *validator, + self.committed_account.pubkey, + self.committed_account.account.owner, + args, + ) + } } #[derive(Clone)] @@ -299,32 +419,39 @@ mod serialization_safety_test { }; use solana_account::Account; - use crate::tasks::{ - args_task::{ArgsTask, ArgsTaskType}, - buffer_task::{BufferTask, BufferTaskType}, - *, + use crate::{ + intent_executor::task_info_fetcher::NullTaskInfoFetcher, + tasks::{ + args_task::{ArgsTask, ArgsTaskType}, + buffer_task::{BufferTask, BufferTaskType}, + *, + }, }; // Test all ArgsTask variants - #[test] - fn test_args_task_instruction_serialization() { + #[tokio::test] + async fn test_args_task_instruction_serialization() { let validator = Pubkey::new_unique(); // Test Commit variant - let commit_task: ArgsTask = ArgsTaskType::Commit(CommitTask { - commit_id: 123, - allow_undelegation: true, - committed_account: CommittedAccount { - pubkey: Pubkey::new_unique(), - account: Account { - lamports: 1000, - data: vec![1, 2, 3], - owner: Pubkey::new_unique(), - executable: false, - rent_epoch: 0, + let commit_task: ArgsTask = ArgsTaskType::Commit( + CommitTask::new( + 123, + true, + CommittedAccount { + pubkey: Pubkey::new_unique(), + account: Account { + lamports: 1000, + data: vec![1, 2, 3], + owner: Pubkey::new_unique(), + executable: false, + rent_epoch: 0, + }, }, - }, - }) + &Arc::new(NullTaskInfoFetcher), + ) + .await, + ) .into(); assert_serializable(&commit_task.instruction(&validator)); @@ -366,51 +493,57 @@ mod serialization_safety_test { } // Test BufferTask variants - #[test] - fn test_buffer_task_instruction_serialization() { + #[tokio::test] + async fn test_buffer_task_instruction_serialization() { let validator = Pubkey::new_unique(); - let buffer_task = BufferTask::new_preparation_required( - BufferTaskType::Commit(CommitTask { - commit_id: 456, - allow_undelegation: false, - committed_account: CommittedAccount { - pubkey: Pubkey::new_unique(), - account: Account { - lamports: 2000, - data: vec![7, 8, 9], - owner: Pubkey::new_unique(), - executable: false, - rent_epoch: 0, + let buffer_task = + BufferTask::new_preparation_required(BufferTaskType::Commit( + CommitTask::new( + 456, + false, + CommittedAccount { + pubkey: Pubkey::new_unique(), + account: Account { + lamports: 2000, + data: vec![7, 8, 9], + owner: Pubkey::new_unique(), + executable: false, + rent_epoch: 0, + }, }, - }, - }), - ); + &Arc::new(NullTaskInfoFetcher), + ) + .await, + )); assert_serializable(&buffer_task.instruction(&validator)); } // Test preparation instructions - #[test] - fn test_preparation_instructions_serialization() { + #[tokio::test] + async fn test_preparation_instructions_serialization() { let authority = Pubkey::new_unique(); // Test BufferTask preparation - let buffer_task = BufferTask::new_preparation_required( - BufferTaskType::Commit(CommitTask { - commit_id: 789, - allow_undelegation: true, - committed_account: CommittedAccount { - pubkey: Pubkey::new_unique(), - account: Account { - lamports: 3000, - data: vec![0; 1024], // Larger data to test chunking - owner: Pubkey::new_unique(), - executable: false, - rent_epoch: 0, + let buffer_task = + BufferTask::new_preparation_required(BufferTaskType::Commit( + CommitTask::new( + 789, + true, + CommittedAccount { + pubkey: Pubkey::new_unique(), + account: Account { + lamports: 3000, + data: vec![0; 1024], // Larger data to test chunking + owner: Pubkey::new_unique(), + executable: false, + rent_epoch: 0, + }, }, - }, - }), - ); + &Arc::new(NullTaskInfoFetcher), + ) + .await, + )); let PreparationState::Required(preparation_task) = buffer_task.preparation_state() diff --git a/magicblock-committor-service/src/tasks/task_builder.rs b/magicblock-committor-service/src/tasks/task_builder.rs index bb76b260e..ec5303b8e 100644 --- a/magicblock-committor-service/src/tasks/task_builder.rs +++ b/magicblock-committor-service/src/tasks/task_builder.rs @@ -1,6 +1,7 @@ use std::sync::Arc; use async_trait::async_trait; +use futures_util::future::join_all; use log::error; use magicblock_program::magic_scheduled_base_intent::{ CommitType, CommittedAccount, MagicBaseIntent, ScheduledBaseIntent, @@ -24,14 +25,14 @@ use crate::{ pub trait TasksBuilder { // Creates tasks for commit stage async fn commit_tasks( - commit_id_fetcher: &Arc, + task_info_fetcher: &Arc, base_intent: &ScheduledBaseIntent, persister: &Option

, ) -> TaskBuilderResult>>; // Create tasks for finalize stage async fn finalize_tasks( - info_fetcher: &Arc, + task_info_fetcher: &Arc, base_intent: &ScheduledBaseIntent, ) -> TaskBuilderResult>>; } @@ -44,39 +45,35 @@ pub struct TaskBuilderImpl; impl TasksBuilder for TaskBuilderImpl { /// Returns [`Task`]s for Commit stage async fn commit_tasks( - commit_id_fetcher: &Arc, + task_info_fetcher: &Arc, base_intent: &ScheduledBaseIntent, persister: &Option

, ) -> TaskBuilderResult>> { - let (accounts, allow_undelegation, commit_diff) = - match &base_intent.base_intent { - MagicBaseIntent::BaseActions(actions) => { - let tasks = actions - .iter() - .map(|el| { - let task = BaseActionTask { action: el.clone() }; - let task = - ArgsTask::new(ArgsTaskType::BaseAction(task)); - Box::new(task) as Box - }) - .collect(); - return Ok(tasks); - } - MagicBaseIntent::Commit(t) => { - (t.get_committed_accounts(), false, t.is_commit_diff()) - } - MagicBaseIntent::CommitAndUndelegate(t) => ( - t.commit_action.get_committed_accounts(), - true, - t.commit_action.is_commit_diff(), - ), - }; + let (accounts, allow_undelegation) = match &base_intent.base_intent { + MagicBaseIntent::BaseActions(actions) => { + let tasks = actions + .iter() + .map(|el| { + let task = BaseActionTask { action: el.clone() }; + let task = + ArgsTask::new(ArgsTaskType::BaseAction(task)); + Box::new(task) as Box + }) + .collect(); + + return Ok(tasks); + } + MagicBaseIntent::Commit(t) => (t.get_committed_accounts(), false), + MagicBaseIntent::CommitAndUndelegate(t) => { + (t.commit_action.get_committed_accounts(), true) + } + }; let committed_pubkeys = accounts .iter() .map(|account| account.pubkey) .collect::>(); - let commit_ids = commit_id_fetcher + let commit_ids = task_info_fetcher .fetch_next_commit_ids(&committed_pubkeys) .await .map_err(TaskBuilderError::CommitTasksBuildError)?; @@ -90,31 +87,26 @@ impl TasksBuilder for TaskBuilderImpl { } }); - let tasks = accounts + let tasks = join_all(accounts .iter() - .map(|account| { + .map(|account| async { let commit_id = *commit_ids.get(&account.pubkey).expect("CommitIdFetcher provide commit ids for all listed pubkeys, or errors!"); - let task = CommitTask { + let task = ArgsTaskType::Commit(CommitTask::new( commit_id, allow_undelegation, - committed_account: account.clone(), - }; - let task = if commit_diff { - ArgsTaskType::CommitDiff(task) - } else { - ArgsTaskType::Commit(task) - }; + account.clone(), + task_info_fetcher, + ).await); Box::new(ArgsTask::new(task)) as Box - }) - .collect(); + })).await; Ok(tasks) } /// Returns [`Task`]s for Finalize stage async fn finalize_tasks( - info_fetcher: &Arc, + task_info_fetcher: &Arc, base_intent: &ScheduledBaseIntent, ) -> TaskBuilderResult>> { // Helper to create a finalize task @@ -144,9 +136,6 @@ impl TasksBuilder for TaskBuilderImpl { CommitType::Standalone(accounts) => { accounts.iter().map(finalize_task).collect() } - CommitType::StandaloneDiff(accounts) => { - accounts.iter().map(finalize_task).collect() - } CommitType::WithBaseActions { committed_accounts, base_actions, @@ -180,7 +169,7 @@ impl TasksBuilder for TaskBuilderImpl { .iter() .map(|account| account.pubkey) .collect::>(); - let rent_reimbursements = info_fetcher + let rent_reimbursements = task_info_fetcher .fetch_rent_reimbursements(&pubkeys) .await .map_err(TaskBuilderError::FinalizedTasksBuildError)?; diff --git a/magicblock-committor-service/src/tasks/task_strategist.rs b/magicblock-committor-service/src/tasks/task_strategist.rs index 2cf7e3e13..3d442584e 100644 --- a/magicblock-committor-service/src/tasks/task_strategist.rs +++ b/magicblock-committor-service/src/tasks/task_strategist.rs @@ -275,6 +275,7 @@ impl TaskStrategist { ) -> Result { // Get initial transaction size let calculate_tx_length = |tasks: &[Box]| { + // TODO (snawaz): we seem to discard lots of heavy computations here match TransactionUtils::assemble_tasks_tx( &Keypair::new(), // placeholder tasks, @@ -369,6 +370,7 @@ pub type TaskStrategistResult = Result; mod tests { use std::{collections::HashMap, sync::Arc}; + use futures_util::future::join_all; use magicblock_program::magic_scheduled_base_intent::{ BaseAction, CommittedAccount, ProgramArgs, }; @@ -379,6 +381,7 @@ mod tests { use super::*; use crate::{ intent_execution_manager::intent_scheduler::create_test_intent, + intent_executor::task_info_fetcher::NullTaskInfoFetcher, intent_executor::task_info_fetcher::{ ResetType, TaskInfoFetcher, TaskInfoFetcherResult, }, @@ -414,21 +417,28 @@ mod tests { } // Helper to create a simple commit task - fn create_test_commit_task(commit_id: u64, data_size: usize) -> ArgsTask { - ArgsTask::new(ArgsTaskType::Commit(CommitTask { - commit_id, - allow_undelegation: false, - committed_account: CommittedAccount { - pubkey: Pubkey::new_unique(), - account: Account { - lamports: 1000, - data: vec![1; data_size], - owner: system_program_id(), - executable: false, - rent_epoch: 0, + async fn create_test_commit_task( + commit_id: u64, + data_size: usize, + ) -> ArgsTask { + ArgsTask::new(ArgsTaskType::Commit( + CommitTask::new( + commit_id, + false, + CommittedAccount { + pubkey: Pubkey::new_unique(), + account: Account { + lamports: 1000, + data: vec![1; data_size], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, }, - }, - })) + &Arc::new(NullTaskInfoFetcher), + ) + .await, + )) } // Helper to create a Base action task @@ -463,10 +473,10 @@ mod tests { })) } - #[test] - fn test_build_strategy_with_single_small_task() { + #[tokio::test] + async fn test_build_strategy_with_single_small_task() { let validator = Pubkey::new_unique(); - let task = create_test_commit_task(1, 100); + let task = create_test_commit_task(1, 100).await; let tasks = vec![Box::new(task) as Box]; let strategy = TaskStrategist::build_strategy( @@ -480,11 +490,11 @@ mod tests { assert!(strategy.lookup_tables_keys.is_empty()); } - #[test] - fn test_build_strategy_optimizes_to_buffer_when_needed() { + #[tokio::test] + async fn test_build_strategy_optimizes_to_buffer_when_needed() { let validator = Pubkey::new_unique(); - let task = create_test_commit_task(1, 1000); // Large task + let task = create_test_commit_task(1, 1000).await; // Large task let tasks = vec![Box::new(task) as Box]; let strategy = TaskStrategist::build_strategy( @@ -501,11 +511,11 @@ mod tests { )); } - #[test] - fn test_build_strategy_optimizes_to_buffer_u16_exceeded() { + #[tokio::test] + async fn test_build_strategy_optimizes_to_buffer_u16_exceeded() { let validator = Pubkey::new_unique(); - let task = create_test_commit_task(1, 66_000); // Large task + let task = create_test_commit_task(1, 66_000).await; // Large task let tasks = vec![Box::new(task) as Box]; let strategy = TaskStrategist::build_strategy( @@ -522,19 +532,18 @@ mod tests { )); } - #[test] - fn test_build_strategy_creates_multiple_buffers() { + #[tokio::test] + async fn test_build_strategy_creates_multiple_buffers() { // TODO: ALSO MAX NUM WITH PURE BUFFER commits, no alts const NUM_COMMITS: u64 = 3; let validator = Pubkey::new_unique(); - let tasks = (0..NUM_COMMITS) - .map(|i| { - let task = create_test_commit_task(i, 500); // Large task - Box::new(task) as Box - }) - .collect(); + let tasks = join_all((0..NUM_COMMITS).map(|i| async move { + let task = create_test_commit_task(i, 500).await; // Large task + Box::new(task) as Box + })) + .await; let strategy = TaskStrategist::build_strategy( tasks, @@ -549,20 +558,19 @@ mod tests { assert!(strategy.lookup_tables_keys.is_empty()); } - #[test] - fn test_build_strategy_with_lookup_tables_when_needed() { + #[tokio::test] + async fn test_build_strategy_with_lookup_tables_when_needed() { // Also max number of committed accounts fit with ALTs! const NUM_COMMITS: u64 = 22; let validator = Pubkey::new_unique(); - let tasks = (0..NUM_COMMITS) - .map(|i| { - // Large task - let task = create_test_commit_task(i, 10000); - Box::new(task) as Box - }) - .collect(); + let tasks = join_all((0..NUM_COMMITS).map(|i| async move { + // Large task + let task = create_test_commit_task(i, 10000).await; + Box::new(task) as Box + })) + .await; let strategy = TaskStrategist::build_strategy( tasks, @@ -577,19 +585,18 @@ mod tests { assert!(!strategy.lookup_tables_keys.is_empty()); } - #[test] - fn test_build_strategy_fails_when_cant_fit() { + #[tokio::test] + async fn test_build_strategy_fails_when_cant_fit() { const NUM_COMMITS: u64 = 23; let validator = Pubkey::new_unique(); - let tasks = (0..NUM_COMMITS) - .map(|i| { - // Large task - let task = create_test_commit_task(i, 1000); - Box::new(task) as Box - }) - .collect(); + let tasks = join_all((0..NUM_COMMITS).map(|i| async move { + // Large task + let task = create_test_commit_task(i, 1000).await; + Box::new(task) as Box + })) + .await; let result = TaskStrategist::build_strategy( tasks, @@ -599,12 +606,15 @@ mod tests { assert!(matches!(result, Err(TaskStrategistError::FailedToFitError))); } - #[test] - fn test_optimize_strategy_prioritizes_largest_tasks() { + #[tokio::test] + async fn test_optimize_strategy_prioritizes_largest_tasks() { let mut tasks = [ - Box::new(create_test_commit_task(1, 100)) as Box, - Box::new(create_test_commit_task(2, 1000)) as Box, // Larger task - Box::new(create_test_commit_task(3, 1000)) as Box, // Larger task + Box::new(create_test_commit_task(1, 100).await) + as Box, + Box::new(create_test_commit_task(2, 1000).await) + as Box, // Larger task + Box::new(create_test_commit_task(3, 1000).await) + as Box, // Larger task ]; let _ = TaskStrategist::optimize_strategy(&mut tasks); @@ -613,11 +623,12 @@ mod tests { assert!(matches!(tasks[1].strategy(), TaskStrategy::Buffer)); } - #[test] - fn test_mixed_task_types_with_optimization() { + #[tokio::test] + async fn test_mixed_task_types_with_optimization() { let validator = Pubkey::new_unique(); let tasks = vec![ - Box::new(create_test_commit_task(1, 1000)) as Box, + Box::new(create_test_commit_task(1, 1000).await) + as Box, Box::new(create_test_finalize_task()) as Box, Box::new(create_test_base_action_task(500)) as Box, Box::new(create_test_undelegate_task()) as Box, diff --git a/magicblock-committor-service/src/tasks/task_visitors/persistor_visitor.rs b/magicblock-committor-service/src/tasks/task_visitors/persistor_visitor.rs index 1911db187..c608f2ef9 100644 --- a/magicblock-committor-service/src/tasks/task_visitors/persistor_visitor.rs +++ b/magicblock-committor-service/src/tasks/task_visitors/persistor_visitor.rs @@ -26,10 +26,9 @@ where fn visit_args_task(&mut self, task: &ArgsTask) { match self.context { PersistorContext::PersistStrategy { uses_lookup_tables } => { - let commit_task = match &task.task_type { - ArgsTaskType::Commit(commit_task) => commit_task, - ArgsTaskType::CommitDiff(commit_task) => commit_task, - _ => return, + let ArgsTaskType::Commit(ref commit_task) = task.task_type + else { + return; }; let commit_strategy = if uses_lookup_tables { diff --git a/magicblock-magic-program-api/src/instruction.rs b/magicblock-magic-program-api/src/instruction.rs index d251f12ba..108586ff1 100644 --- a/magicblock-magic-program-api/src/instruction.rs +++ b/magicblock-magic-program-api/src/instruction.rs @@ -103,7 +103,6 @@ pub enum MagicBlockInstruction { /// Noop instruction Noop(u64), - ScheduleCommitDiffAndUndelegate, } impl MagicBlockInstruction { diff --git a/programs/magicblock/src/magic_scheduled_base_intent.rs b/programs/magicblock/src/magic_scheduled_base_intent.rs index 0a88817fe..2de827122 100644 --- a/programs/magicblock/src/magic_scheduled_base_intent.rs +++ b/programs/magicblock/src/magic_scheduled_base_intent.rs @@ -102,10 +102,6 @@ impl ScheduledBaseIntent { self.base_intent.is_undelegate() } - pub fn is_commit_diff(&self) -> bool { - self.base_intent.is_commit_diff() - } - pub fn is_empty(&self) -> bool { self.base_intent.is_empty() } @@ -153,16 +149,6 @@ impl MagicBaseIntent { } } - pub fn is_commit_diff(&self) -> bool { - match &self { - MagicBaseIntent::BaseActions(_) => false, - MagicBaseIntent::Commit(c) => c.is_commit_diff(), - MagicBaseIntent::CommitAndUndelegate(c) => { - c.commit_action.is_commit_diff() - } - } - } - pub fn get_committed_accounts(&self) -> Option<&Vec> { match self { MagicBaseIntent::BaseActions(_) => None, @@ -343,7 +329,6 @@ impl<'a> From> for CommittedAccount { pub enum CommitType { /// Regular commit without actions Standalone(Vec), // accounts to commit - StandaloneDiff(Vec), // accounts to commit /// Commits accounts and runs actions WithBaseActions { committed_accounts: Vec, @@ -467,14 +452,9 @@ impl CommitType { } } - pub fn is_commit_diff(&self) -> bool { - matches!(self, Self::StandaloneDiff(_)) - } - pub fn get_committed_accounts(&self) -> &Vec { match self { Self::Standalone(committed_accounts) => committed_accounts, - Self::StandaloneDiff(committed_accounts) => committed_accounts, Self::WithBaseActions { committed_accounts, .. } => committed_accounts, @@ -484,7 +464,6 @@ impl CommitType { pub fn get_committed_accounts_mut(&mut self) -> &mut Vec { match self { Self::Standalone(committed_accounts) => committed_accounts, - Self::StandaloneDiff(committed_accounts) => committed_accounts, Self::WithBaseActions { committed_accounts, .. } => committed_accounts, @@ -496,9 +475,6 @@ impl CommitType { Self::Standalone(committed_accounts) => { committed_accounts.is_empty() } - Self::StandaloneDiff(committed_accounts) => { - committed_accounts.is_empty() - } Self::WithBaseActions { committed_accounts, .. } => committed_accounts.is_empty(), diff --git a/programs/magicblock/src/magicblock_processor.rs b/programs/magicblock/src/magicblock_processor.rs index c25e5d83f..53c132261 100644 --- a/programs/magicblock/src/magicblock_processor.rs +++ b/programs/magicblock/src/magicblock_processor.rs @@ -46,7 +46,6 @@ declare_process_instruction!( invoke_context, ProcessScheduleCommitOptions { request_undelegation: false, - request_diff: false, }, ), ScheduleCommitAndUndelegate => process_schedule_commit( @@ -54,7 +53,6 @@ declare_process_instruction!( invoke_context, ProcessScheduleCommitOptions { request_undelegation: true, - request_diff: false, }, ), AcceptScheduleCommits => { @@ -82,14 +80,6 @@ declare_process_instruction!( process_toggle_executable_check(signers, invoke_context, true) } Noop(_) => Ok(()), - ScheduleCommitDiffAndUndelegate => process_schedule_commit( - signers, - invoke_context, - ProcessScheduleCommitOptions { - request_undelegation: true, - request_diff: true, - }, - ), } } ); diff --git a/programs/magicblock/src/schedule_transactions/process_schedule_commit.rs b/programs/magicblock/src/schedule_transactions/process_schedule_commit.rs index 041d8fd0e..43ab4529f 100644 --- a/programs/magicblock/src/schedule_transactions/process_schedule_commit.rs +++ b/programs/magicblock/src/schedule_transactions/process_schedule_commit.rs @@ -26,7 +26,6 @@ use crate::{ #[derive(Default)] pub(crate) struct ProcessScheduleCommitOptions { pub request_undelegation: bool, - pub request_diff: bool, } pub(crate) fn process_schedule_commit( @@ -243,19 +242,13 @@ pub(crate) fn process_schedule_commit( InstructionUtils::scheduled_commit_sent(intent_id, blockhash); let commit_sent_sig = action_sent_transaction.signatures[0]; - let commit_action = if opts.request_diff { - CommitType::StandaloneDiff(committed_accounts) - } else { - CommitType::Standalone(committed_accounts) - }; - let base_intent = if opts.request_undelegation { MagicBaseIntent::CommitAndUndelegate(CommitAndUndelegate { - commit_action, + commit_action: CommitType::Standalone(committed_accounts), undelegate_action: UndelegateType::Standalone, }) } else { - MagicBaseIntent::Commit(commit_action) + MagicBaseIntent::Commit(CommitType::Standalone(committed_accounts)) }; let scheduled_base_intent = ScheduledBaseIntent { id: intent_id, diff --git a/programs/magicblock/src/schedule_transactions/process_scheduled_commit_sent.rs b/programs/magicblock/src/schedule_transactions/process_scheduled_commit_sent.rs index 530e05d0f..4b886930f 100644 --- a/programs/magicblock/src/schedule_transactions/process_scheduled_commit_sent.rs +++ b/programs/magicblock/src/schedule_transactions/process_scheduled_commit_sent.rs @@ -34,7 +34,6 @@ pub struct SentCommit { pub requested_undelegation: bool, pub error_message: Option, pub patched_errors: Vec, - pub commit_diff: bool, } /// This is a printable version of the SentCommit struct. @@ -51,7 +50,6 @@ struct SentCommitPrintable { requested_undelegation: bool, error_message: Option, patched_errors: Vec, - commit_diff: bool, } impl From for SentCommitPrintable { @@ -81,7 +79,6 @@ impl From for SentCommitPrintable { requested_undelegation: commit.requested_undelegation, error_message: commit.error_message, patched_errors: commit.patched_errors, - commit_diff: commit.commit_diff, } } } @@ -224,9 +221,6 @@ pub fn process_scheduled_commit_sent( if commit.requested_undelegation { ic_msg!(invoke_context, "ScheduledCommitSent requested undelegation",); } - if commit.commit_diff { - ic_msg!(invoke_context, "ScheduledCommitSent requested commit_diff",); - } for (idx, error) in commit.patched_errors.iter().enumerate() { ic_msg!( @@ -281,7 +275,6 @@ mod tests { requested_undelegation: false, error_message: None, patched_errors: vec![], - commit_diff: false, } } diff --git a/test-integration/Cargo.toml b/test-integration/Cargo.toml index f17ae3f2c..f9820f806 100644 --- a/test-integration/Cargo.toml +++ b/test-integration/Cargo.toml @@ -37,7 +37,7 @@ cleanass = "0.0.1" color-backtrace = { version = "0.7" } ctrlc = "3.4.7" futures = "0.3.31" -ephemeral-rollups-sdk = { path = "../../ephemeral-rollups-sdk/rust/sdk"} +ephemeral-rollups-sdk = { git = "https://github.com/magicblock-labs/ephemeral-rollups-sdk.git", rev = "2d0f16b" } integration-test-tools = { path = "test-tools" } isocountry = "0.3.2" lazy_static = "1.4.0" @@ -57,7 +57,9 @@ magicblock-config = { path = "../magicblock-config" } magicblock-core = { path = "../magicblock-core" } magic-domain-program = { git = "https://github.com/magicblock-labs/magic-domain-program.git", rev = "ea04d46", default-features = false } magicblock_magic_program_api = { package = "magicblock-magic-program-api", path = "../magicblock-magic-program-api" } -magicblock-delegation-program = { path="../../delegation-program", features = ["no-entrypoint"] } +magicblock-delegation-program = { git = "https://github.com/magicblock-labs/delegation-program.git", rev = "e8d03936", features = [ + "no-entrypoint", +] } magicblock-program = { path = "../programs/magicblock" } magicblock-rpc-client = { path = "../magicblock-rpc-client" } magicblock-table-mania = { path = "../magicblock-table-mania" } diff --git a/test-integration/programs/schedulecommit-security/src/lib.rs b/test-integration/programs/schedulecommit-security/src/lib.rs index 8bac187a8..8236b40a7 100644 --- a/test-integration/programs/schedulecommit-security/src/lib.rs +++ b/test-integration/programs/schedulecommit-security/src/lib.rs @@ -1,5 +1,5 @@ use borsh::{BorshDeserialize, BorshSerialize}; -use ephemeral_rollups_sdk::ephem::{create_schedule_commit_ix, CommitPolicy}; +use ephemeral_rollups_sdk::ephem::create_schedule_commit_ix; use program_schedulecommit::{ api::schedule_commit_cpi_instruction, process_schedulecommit_cpi, ProcessSchedulecommitCpiArgs, @@ -146,7 +146,6 @@ fn process_sibling_schedule_cpis( magic_context, magic_program, false, - CommitPolicy::UseFullBytes, ); invoke( &direct_ix, diff --git a/test-integration/programs/schedulecommit/src/api.rs b/test-integration/programs/schedulecommit/src/api.rs index 18e8d11a7..65931d4c9 100644 --- a/test-integration/programs/schedulecommit/src/api.rs +++ b/test-integration/programs/schedulecommit/src/api.rs @@ -136,6 +136,7 @@ pub fn delegate_account_cpi_instruction( DelegateOrderBookArgs { commit_frequency_ms: 1_000_000_000, book_manager: player_or_book_manager, + validator, }, ) }, diff --git a/test-integration/programs/schedulecommit/src/lib.rs b/test-integration/programs/schedulecommit/src/lib.rs index 93c6976eb..009c51281 100644 --- a/test-integration/programs/schedulecommit/src/lib.rs +++ b/test-integration/programs/schedulecommit/src/lib.rs @@ -6,10 +6,7 @@ use ephemeral_rollups_sdk::{ cpi::{ delegate_account, undelegate_account, DelegateAccounts, DelegateConfig, }, - ephem::{ - commit_accounts, commit_and_undelegate_accounts, - commit_diff_and_undelegate_accounts, - }, + ephem::{commit_accounts, commit_and_undelegate_accounts}, }; use magicblock_magic_program_api::instruction::MagicBlockInstruction; use solana_program::{ @@ -42,7 +39,6 @@ mod utils; pub const FAIL_UNDELEGATION_COUNT: u64 = u64::MAX - 1; use order_book::*; - pub use order_book::{BookUpdate, OrderBookOwned, OrderLevel}; declare_id!("9hgprgZiRWmy8KkfvUuaVkDGrqo9GzeXMohwq6BazgUY"); @@ -62,6 +58,7 @@ pub struct DelegateCpiArgs { pub struct DelegateOrderBookArgs { commit_frequency_ms: u32, book_manager: Pubkey, + validator: Option, } #[derive(BorshSerialize, BorshDeserialize, Debug, Clone)] @@ -418,7 +415,7 @@ pub fn process_delegate_order_book( &seeds_no_bump, DelegateConfig { commit_frequency_ms: args.commit_frequency_ms, - ..DelegateConfig::default() + validator: args.validator, }, )?; @@ -461,7 +458,7 @@ pub fn process_schedulecommit_for_orderbook( assert_is_signer(payer, "payer")?; - commit_diff_and_undelegate_accounts( + commit_and_undelegate_accounts( payer, vec![order_book_account], magic_context, @@ -577,7 +574,7 @@ pub fn process_schedulecommit_cpi( ); if args.undelegate { - commit_diff_and_undelegate_accounts( + commit_and_undelegate_accounts( payer, committees, magic_context, diff --git a/test-integration/programs/schedulecommit/src/order_book.rs b/test-integration/programs/schedulecommit/src/order_book.rs index 3343064ca..d081482ca 100644 --- a/test-integration/programs/schedulecommit/src/order_book.rs +++ b/test-integration/programs/schedulecommit/src/order_book.rs @@ -204,7 +204,7 @@ impl<'a> OrderBook<'a> { ) -> &'a mut [OrderLevel] { slice::from_raw_parts_mut( self.levels.add(self.capacity - asks_len), - asks_len as usize, + asks_len, ) } diff --git a/test-integration/schedulecommit/client/src/schedule_commit_context.rs b/test-integration/schedulecommit/client/src/schedule_commit_context.rs index cedf9f297..f74d86707 100644 --- a/test-integration/schedulecommit/client/src/schedule_commit_context.rs +++ b/test-integration/schedulecommit/client/src/schedule_commit_context.rs @@ -113,7 +113,7 @@ impl ScheduleCommitTestContext { ) .unwrap(); let (pda, _bump) = Pubkey::find_program_address( - &[user_seed, &payer_ephem.pubkey().as_ref()], + &[user_seed, payer_ephem.pubkey().as_ref()], &program_schedulecommit::ID, ); (payer_ephem, pda) @@ -167,40 +167,48 @@ impl ScheduleCommitTestContext { ComputeBudgetInstruction::set_compute_unit_limit(1_400_000), ComputeBudgetInstruction::set_compute_unit_price(10_000), ]; - if self.user_seed == b"magic_schedule_commit" { - ixs.extend(self.committees.iter().map(|(player, committee)| { - init_account_instruction( - self.payer_chain.pubkey(), - player.pubkey(), - *committee, - ) - })); - } else { - ixs.extend(self.committees.iter().map( - |(book_manager, committee)| { - init_order_book_instruction( - self.payer_chain.pubkey(), - book_manager.pubkey(), - *committee, - ) - }, - )); - - //// TODO (snawaz): currently the size of delegatable-account cannot be - //// more than 10K, else delegation will fail. So Let's revisit this when - //// we relax the limit on the account size, then we can use larger - //// account, say even 10 MB, and execute CommitDiff. - // - // ixs.extend(self.committees.iter().flat_map( - // |(payer, committee)| { - // [grow_order_book_instruction( - // payer.pubkey(), - // *committee, - // 10 * 1024 - // )] - // }, - // )); - } + match self.user_seed.as_slice() { + b"magic_schedule_commit" => { + ixs.extend(self.committees.iter().map( + |(player, committee)| { + init_account_instruction( + self.payer_chain.pubkey(), + player.pubkey(), + *committee, + ) + }, + )); + } + b"order_book" => { + ixs.extend(self.committees.iter().map( + |(book_manager, committee)| { + init_order_book_instruction( + self.payer_chain.pubkey(), + book_manager.pubkey(), + *committee, + ) + }, + )); + + //// TODO (snawaz): currently the size of delegatable-account cannot be + //// more than 10K, else delegation will fail. So Let's revisit this when + //// we relax the limit on the account size, then we can use larger + //// account, say even 10 MB, and execute CommitDiff. + // + // ixs.extend(self.committees.iter().flat_map( + // |(payer, committee)| { + // [grow_order_book_instruction( + // payer.pubkey(), + // *committee, + // 10 * 1024 + // )] + // }, + // )); + } + _ => { + return Err(anyhow::anyhow!("Unsupported user_seed: {:?} ; expected b\"magic_schedule_commit\" or b\"order_book\"", self.user_seed)); + } + }; let mut signers = self .committees diff --git a/test-integration/schedulecommit/elfs/dlp.so b/test-integration/schedulecommit/elfs/dlp.so index f07df31f3e7ba6d920d4d2284d76d1d52047a724..decfd0f00a8a87543a65cd002d05a380f7c91d5d 100755 GIT binary patch delta 85820 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 delta 80909 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, - &committees.iter().map(|(_, pda)| *pda).collect::>(), - ); - - let tx = Transaction::new_signed_with_payer( - &[ix], - Some(&payer.pubkey()), - &[&payer], - *ephem_blockhash, - ); - - let sig = tx.get_signature(); - let res = ephem_client - .send_and_confirm_transaction_with_spinner_and_config( - &tx, - *commitment, - RpcSendTransactionConfig { - skip_preflight: true, - ..Default::default() - }, - ); - info!("{} '{:?}'", sig, res); - - assert!(res.is_err()); - assert!(res - .err() - .unwrap() - .to_string() - .contains("DoesNotHaveEscrowAccount")); - }); -} - -#[test] -fn test_committing_fee_payer_escrowing_lamports() { - run_test!({ - let ctx = - get_context_with_delegated_committees(2, b"magic_schedule_commit"); - - let ScheduleCommitTestContextFields { - payer, - committees, - commitment, - ephem_client, - ephem_blockhash, - .. - } = ctx.fields(); - - let ix = schedule_commit_with_payer_cpi_instruction( - payer.pubkey(), - magicblock_magic_program_api::id(), - magicblock_magic_program_api::MAGIC_CONTEXT_PUBKEY, - &committees - .iter() - .map(|(player, _)| player.pubkey()) - .collect::>(), - &committees.iter().map(|(_, pda)| *pda).collect::>(), - ); - - let tx = Transaction::new_signed_with_payer( - &[ix], - Some(&payer.pubkey()), - &[&payer], - *ephem_blockhash, - ); - - let sig = tx.get_signature(); - let res = ephem_client - .send_and_confirm_transaction_with_spinner_and_config( - &tx, - *commitment, - RpcSendTransactionConfig { - skip_preflight: true, - ..Default::default() - }, - ); - info!("{} '{:?}'", sig, res); - assert!(res.is_ok()); - - let res = verify::fetch_and_verify_commit_result_from_logs(&ctx, *sig); - assert_two_committees_were_committed(&ctx, &res, true); - assert_two_committees_synchronized_count(&ctx, &res, 1); - - // The fee payer should have been committed - assert_feepayer_was_committed(&ctx, &res, true); - }); -} diff --git a/test-integration/schedulecommit/test-scenarios/tests/utils/mod.rs b/test-integration/schedulecommit/test-scenarios/tests/utils/mod.rs index cf1ae2c87..9d8952c9a 100644 --- a/test-integration/schedulecommit/test-scenarios/tests/utils/mod.rs +++ b/test-integration/schedulecommit/test-scenarios/tests/utils/mod.rs @@ -42,9 +42,9 @@ pub fn assert_one_committee_was_committed( ctx: &ScheduleCommitTestContext, res: &ScheduledCommitResult, is_single_stage: bool, -) -where - T: std::fmt::Debug + borsh::BorshDeserialize + PartialEq + Eq { +) where + T: std::fmt::Debug + borsh::BorshDeserialize + PartialEq + Eq, +{ let pda = ctx.committees[0].1; assert_eq!(res.included.len(), 1, "includes 1 pda"); diff --git a/test-integration/schedulecommit/test-security/tests/01_invocations.rs b/test-integration/schedulecommit/test-security/tests/01_invocations.rs index b05168035..068c65193 100644 --- a/test-integration/schedulecommit/test-security/tests/01_invocations.rs +++ b/test-integration/schedulecommit/test-security/tests/01_invocations.rs @@ -27,9 +27,12 @@ const NEEDS_TO_BE_OWNED_BY_INVOKING_PROGRAM: &str = fn prepare_ctx_with_account_to_commit() -> ScheduleCommitTestContext { let ctx = if std::env::var("FIXED_KP").is_ok() { - ScheduleCommitTestContext::try_new(2) + ScheduleCommitTestContext::try_new(2, b"magic_schedule_commit") } else { - ScheduleCommitTestContext::try_new_random_keys(2) + ScheduleCommitTestContext::try_new_random_keys( + 2, + b"magic_schedule_commit", + ) } .unwrap(); ctx.init_committees().unwrap(); diff --git a/test-integration/test-committor-service/tests/common.rs b/test-integration/test-committor-service/tests/common.rs index 9966ff56a..9ea8edc68 100644 --- a/test-integration/test-committor-service/tests/common.rs +++ b/test-integration/test-committor-service/tests/common.rs @@ -10,7 +10,8 @@ use async_trait::async_trait; use magicblock_committor_service::{ intent_executor::{ task_info_fetcher::{ - ResetType, TaskInfoFetcher, TaskInfoFetcherResult, + NullTaskInfoFetcher, ResetType, TaskInfoFetcher, + TaskInfoFetcherResult, }, IntentExecutorImpl, }, @@ -21,7 +22,7 @@ use magicblock_committor_service::{ ComputeBudgetConfig, }; use magicblock_program::magic_scheduled_base_intent::CommittedAccount; -use magicblock_rpc_client::MagicblockRpcClient; +use magicblock_rpc_client::{MagicBlockRpcClientResult, MagicblockRpcClient}; use magicblock_table_mania::{GarbageCollectorConfig, TableMania}; use solana_account::Account; use solana_pubkey::Pubkey; @@ -104,17 +105,22 @@ impl TestFixture { ) -> IntentExecutorImpl { let transaction_preparator = self.create_transaction_preparator(); - let task_info_fetcher = Arc::new(MockTaskInfoFetcher); IntentExecutorImpl::new( self.rpc_client.clone(), transaction_preparator, - task_info_fetcher, + self.create_task_info_fetcher(), ) } + + #[allow(dead_code)] + pub fn create_task_info_fetcher(&self) -> Arc { + Arc::new(MockTaskInfoFetcher(self.rpc_client.clone())) + } } -pub struct MockTaskInfoFetcher; +pub struct MockTaskInfoFetcher(MagicblockRpcClient); + #[async_trait] impl TaskInfoFetcher for MockTaskInfoFetcher { async fn fetch_next_commit_ids( @@ -136,6 +142,13 @@ impl TaskInfoFetcher for MockTaskInfoFetcher { } fn reset(&self, _: ResetType) {} + + async fn get_base_account( + &self, + pubkey: &Pubkey, + ) -> MagicBlockRpcClientResult> { + self.0.get_account(pubkey).await + } } #[allow(dead_code)] @@ -147,12 +160,12 @@ pub fn generate_random_bytes(length: usize) -> Vec { } #[allow(dead_code)] -pub fn create_commit_task(data: &[u8]) -> CommitTask { +pub async fn create_commit_task(data: &[u8]) -> CommitTask { static COMMIT_ID: AtomicU64 = AtomicU64::new(0); - CommitTask { - commit_id: COMMIT_ID.fetch_add(1, Ordering::Relaxed), - allow_undelegation: false, - committed_account: CommittedAccount { + CommitTask::new( + COMMIT_ID.fetch_add(1, Ordering::Relaxed), + false, + CommittedAccount { pubkey: Pubkey::new_unique(), account: Account { lamports: 1000, @@ -162,7 +175,9 @@ pub fn create_commit_task(data: &[u8]) -> CommitTask { rent_epoch: 0, }, }, - } + &Arc::new(NullTaskInfoFetcher), + ) + .await } #[allow(dead_code)] diff --git a/test-integration/test-committor-service/tests/test_delivery_preparator.rs b/test-integration/test-committor-service/tests/test_delivery_preparator.rs index 5c48fa22b..35f9de4b3 100644 --- a/test-integration/test-committor-service/tests/test_delivery_preparator.rs +++ b/test-integration/test-committor-service/tests/test_delivery_preparator.rs @@ -1,4 +1,5 @@ use borsh::BorshDeserialize; +use futures::future::join_all; use magicblock_committor_program::Chunks; use magicblock_committor_service::{ persist::IntentPersisterImpl, @@ -21,7 +22,7 @@ async fn test_prepare_10kb_buffer() { let preparator = fixture.create_delivery_preparator(); let data = generate_random_bytes(10 * 1024); - let buffer_task = BufferTaskType::Commit(create_commit_task(&data)); + let buffer_task = BufferTaskType::Commit(create_commit_task(&data).await); let mut strategy = TransactionStrategy { optimized_tasks: vec![Box::new(BufferTask::new_preparation_required( buffer_task, @@ -86,15 +87,13 @@ async fn test_prepare_multiple_buffers() { generate_random_bytes(10), generate_random_bytes(500 * 1024), ]; - let buffer_tasks = datas - .iter() - .map(|data| { - let task = - BufferTaskType::Commit(create_commit_task(data.as_slice())); - Box::new(BufferTask::new_preparation_required(task)) - as Box - }) - .collect(); + let buffer_tasks = join_all(datas.iter().map(|data| async { + let task = + BufferTaskType::Commit(create_commit_task(data.as_slice()).await); + Box::new(BufferTask::new_preparation_required(task)) + as Box + })) + .await; let mut strategy = TransactionStrategy { optimized_tasks: buffer_tasks, lookup_tables_keys: vec![], @@ -165,14 +164,12 @@ async fn test_lookup_tables() { generate_random_bytes(20), generate_random_bytes(30), ]; - let tasks = datas - .iter() - .map(|data| { - let task = - ArgsTaskType::Commit(create_commit_task(data.as_slice())); - Box::::new(task.into()) as Box - }) - .collect::>(); + let tasks = join_all(datas.iter().map(|data| async { + let task = + ArgsTaskType::Commit(create_commit_task(data.as_slice()).await); + Box::::new(task.into()) as Box + })) + .await; let lookup_tables_keys = TaskStrategist::collect_lookup_table_keys( &fixture.authority.pubkey(), @@ -212,7 +209,7 @@ async fn test_already_initialized_error_handled() { let preparator = fixture.create_delivery_preparator(); let data = generate_random_bytes(10 * 1024); - let mut task = create_commit_task(&data); + let mut task = create_commit_task(&data).await; let buffer_task = BufferTaskType::Commit(task.clone()); let mut strategy = TransactionStrategy { optimized_tasks: vec![Box::new(BufferTask::new_preparation_required( @@ -301,9 +298,9 @@ async fn test_prepare_cleanup_and_reprepare_mixed_tasks() { let buf_b_data = generate_random_bytes(64 * 1024 + 3); // Keep these around to modify data later (same commit IDs, different data) - let mut commit_args = create_commit_task(&args_data); - let mut commit_a = create_commit_task(&buf_a_data); - let mut commit_b = create_commit_task(&buf_b_data); + let mut commit_args = create_commit_task(&args_data).await; + let mut commit_a = create_commit_task(&buf_a_data).await; + let mut commit_b = create_commit_task(&buf_b_data).await; let mut strategy = TransactionStrategy { optimized_tasks: vec![ diff --git a/test-integration/test-committor-service/tests/test_ix_commit_local.rs b/test-integration/test-committor-service/tests/test_ix_commit_local.rs index 16ea10441..a6c7994d3 100644 --- a/test-integration/test-committor-service/tests/test_ix_commit_local.rs +++ b/test-integration/test-committor-service/tests/test_ix_commit_local.rs @@ -63,6 +63,7 @@ fn expect_strategies( // ----------------- // Single Account Commits // ----------------- + #[tokio::test] async fn test_ix_commit_single_account_100_bytes() { commit_single_account(100, CommitStrategy::Args, false).await; @@ -158,12 +159,13 @@ async fn commit_single_account( .await; } -// TODO(thlorenz): once delegation program supports larger commits +// TODO(thlorenz/snawaz): once delegation program supports larger commits // add 1MB and 10MB tests // ----------------- // Multiple Account Commits // ----------------- + #[tokio::test] async fn test_ix_commit_two_accounts_1kb_2kb() { init_logger!(); @@ -171,7 +173,7 @@ async fn test_ix_commit_two_accounts_1kb_2kb() { &[1024, 2048], 1, false, - expect_strategies(&[(CommitStrategy::FromBuffer, 2)]), + expect_strategies(&[(CommitStrategy::Args, 2)]), ) .await; } @@ -219,25 +221,22 @@ async fn test_ix_commit_four_accounts_1kb_2kb_5kb_10kb_single_bundle() { &[1024, 2 * 1024, 5 * 1024, 10 * 1024], 1, false, - expect_strategies(&[(CommitStrategy::FromBuffer, 4)]), + expect_strategies(&[(CommitStrategy::Args, 4)]), ) .await; } #[tokio::test] async fn test_commit_20_accounts_1kb_bundle_size_2() { - commit_20_accounts_1kb( - 2, - expect_strategies(&[(CommitStrategy::FromBuffer, 20)]), - ) - .await; + commit_20_accounts_1kb(2, expect_strategies(&[(CommitStrategy::Args, 20)])) + .await; } #[tokio::test] async fn test_commit_5_accounts_1kb_bundle_size_3() { commit_5_accounts_1kb( 3, - expect_strategies(&[(CommitStrategy::FromBuffer, 5)]), + expect_strategies(&[(CommitStrategy::Args, 5)]), false, ) .await; @@ -247,7 +246,11 @@ async fn test_commit_5_accounts_1kb_bundle_size_3() { async fn test_commit_5_accounts_1kb_bundle_size_3_undelegate_all() { commit_5_accounts_1kb( 3, - expect_strategies(&[(CommitStrategy::FromBuffer, 5)]), + expect_strategies(&[ + // Intent fits in 1 TX only with ALT, see IntentExecutorImpl::try_unite_tasks + (CommitStrategy::FromBufferWithLookupTable, 3), + (CommitStrategy::Args, 2), + ]), true, ) .await; @@ -258,7 +261,7 @@ async fn test_commit_5_accounts_1kb_bundle_size_4() { commit_5_accounts_1kb( 4, expect_strategies(&[ - (CommitStrategy::FromBuffer, 1), + (CommitStrategy::Args, 1), (CommitStrategy::FromBufferWithLookupTable, 4), ]), false, @@ -271,7 +274,7 @@ async fn test_commit_5_accounts_1kb_bundle_size_4_undelegate_all() { commit_5_accounts_1kb( 4, expect_strategies(&[ - (CommitStrategy::FromBuffer, 1), + (CommitStrategy::Args, 1), (CommitStrategy::FromBufferWithLookupTable, 4), ]), true, @@ -291,11 +294,8 @@ async fn test_commit_5_accounts_1kb_bundle_size_5_undelegate_all() { #[tokio::test] async fn test_commit_20_accounts_1kb_bundle_size_3() { - commit_20_accounts_1kb( - 3, - expect_strategies(&[(CommitStrategy::FromBuffer, 20)]), - ) - .await; + commit_20_accounts_1kb(3, expect_strategies(&[(CommitStrategy::Args, 20)])) + .await; } #[tokio::test] @@ -314,7 +314,7 @@ async fn test_commit_20_accounts_1kb_bundle_size_6() { expect_strategies(&[ (CommitStrategy::FromBufferWithLookupTable, 18), // Two accounts don't make it into the bundles of size 6 - (CommitStrategy::FromBuffer, 2), + (CommitStrategy::Args, 2), ]), ) .await; @@ -476,7 +476,7 @@ async fn commit_multiple_accounts( ix_commit_local(service, intents, expected_strategies).await; } -// TODO(thlorenz): once delegation program supports larger commits add the following +// TODO(thlorenz/snawaz): once delegation program supports larger commits add the following // tests // // ## Scenario 1 diff --git a/test-integration/test-committor-service/tests/test_transaction_preparator.rs b/test-integration/test-committor-service/tests/test_transaction_preparator.rs index de2f4ea57..52037239e 100644 --- a/test-integration/test-committor-service/tests/test_transaction_preparator.rs +++ b/test-integration/test-committor-service/tests/test_transaction_preparator.rs @@ -35,11 +35,15 @@ async fn test_prepare_commit_tx_with_single_account() { let committed_account = create_committed_account(&account_data); let tasks = vec![ - Box::new(ArgsTask::new(ArgsTaskType::Commit(CommitTask { - commit_id: 1, - committed_account: committed_account.clone(), - allow_undelegation: true, - }))) as Box, + Box::new(ArgsTask::new(ArgsTaskType::Commit( + CommitTask::new( + 1, + true, + committed_account.clone(), + &fixture.create_task_info_fetcher(), + ) + .await, + ))) as Box, Box::new(ArgsTask::new(ArgsTaskType::Finalize(FinalizeTask { delegated_account: committed_account.pubkey, }))), @@ -89,21 +93,28 @@ async fn test_prepare_commit_tx_with_multiple_accounts() { let account2_data = generate_random_bytes(12); let committed_account2 = create_committed_account(&account2_data); - let buffer_commit_task = BufferTask::new_preparation_required( - BufferTaskType::Commit(CommitTask { - commit_id: 1, - committed_account: committed_account2.clone(), - allow_undelegation: true, - }), - ); + let buffer_commit_task = + BufferTask::new_preparation_required(BufferTaskType::Commit( + CommitTask::new( + 1, + true, + committed_account2.clone(), + &fixture.create_task_info_fetcher(), + ) + .await, + )); // Create test data let tasks = vec![ // account 1 - Box::new(ArgsTask::new(ArgsTaskType::Commit(CommitTask { - commit_id: 1, - committed_account: committed_account1.clone(), - allow_undelegation: true, - }))) as Box, + Box::new(ArgsTask::new(ArgsTaskType::Commit( + CommitTask::new( + 1, + true, + committed_account1.clone(), + &fixture.create_task_info_fetcher(), + ) + .await, + ))) as Box, // account 2 Box::new(buffer_commit_task), // finalize account 1 @@ -186,13 +197,16 @@ async fn test_prepare_commit_tx_with_base_actions() { }], }; - let buffer_commit_task = BufferTask::new_preparation_required( - BufferTaskType::Commit(CommitTask { - commit_id: 1, - committed_account: committed_account.clone(), - allow_undelegation: true, - }), - ); + let buffer_commit_task = + BufferTask::new_preparation_required(BufferTaskType::Commit( + CommitTask::new( + 1, + true, + committed_account.clone(), + &fixture.create_task_info_fetcher(), + ) + .await, + )); let tasks = vec![ // commit account Box::new(buffer_commit_task.clone()) as Box, diff --git a/test-integration/test-committor-service/tests/utils/transactions.rs b/test-integration/test-committor-service/tests/utils/transactions.rs index 851be7e39..8d0da41f9 100644 --- a/test-integration/test-committor-service/tests/utils/transactions.rs +++ b/test-integration/test-committor-service/tests/utils/transactions.rs @@ -120,7 +120,17 @@ pub async fn tx_logs_contain( .log_messages .clone() .unwrap_or_else(Vec::new); - logs.iter().any(|log| log.contains(needle)) + logs.iter().any(|log| { + // Lots of existing tests pass "CommitState" as needle argument to this function, but since now CommitTask + // could invoke CommitState or CommitDiff depending on the size of the account, we also look for "CommitDiff" + // in the logs when needle == CommitState. It's easier to make this little adjustment here than computing + // the decision and passing either CommitState or CommitDiff from the tests themselves. + if needle == "CommitState" { + log.contains(needle) || log.contains("CommitDiff") + } else { + log.contains(needle) + } + }) } /// This needs to be run for each test that required a new counter to be delegated diff --git a/test-integration/test-tools/src/integration_test_context.rs b/test-integration/test-tools/src/integration_test_context.rs index 35462161a..d1678ee10 100644 --- a/test-integration/test-tools/src/integration_test_context.rs +++ b/test-integration/test-tools/src/integration_test_context.rs @@ -157,8 +157,9 @@ impl IntegrationTestContext { rpc_client: Option<&RpcClient>, label: &str, ) -> Option> { - let rpc_client = - rpc_client.expect("rpc_client for [{}] does not exist"); + let rpc_client = rpc_client.unwrap_or_else(|| { + panic!("rpc_client for [{label}] does not exist") + }); // Try this up to 50 times since devnet here returns the version response instead of // the EncodedConfirmedTransactionWithStatusMeta at times @@ -168,6 +169,8 @@ impl IntegrationTestContext { RpcTransactionConfig { commitment: Some(self.commitment), max_supported_transaction_version: if label == "chain" { + // base chain cluster requires explicit v0 support, + // while ephemeral uses default version handling Some(0) } else { None diff --git a/test-integration/test-tools/src/scheduled_commits.rs b/test-integration/test-tools/src/scheduled_commits.rs index 95260078f..aae357654 100644 --- a/test-integration/test-tools/src/scheduled_commits.rs +++ b/test-integration/test-tools/src/scheduled_commits.rs @@ -231,7 +231,7 @@ impl IntegrationTestContext { } for sig in sigs.iter() { - self.dump_chain_logs(sig.clone()); + self.dump_chain_logs(*sig); } Ok(ScheduledCommitResult { From 78c6f5bb0d6e26fa844f0a78bf16e1bc90dbffbb Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Fri, 21 Nov 2025 18:07:02 +0400 Subject: [PATCH 03/13] Address @taco-paco's feedback --- Cargo.lock | 1 - magicblock-committor-service/Cargo.toml | 1 - .../intent_execution_engine.rs | 22 +++++ .../src/intent_executor/mod.rs | 93 +++++++++++++++++++ .../intent_executor/null_task_info_fetcher.rs | 42 +++++++++ .../src/intent_executor/task_info_fetcher.rs | 38 +------- magicblock-committor-service/src/tasks/mod.rs | 40 ++++---- .../src/tasks/task_builder.rs | 5 +- .../src/tasks/task_strategist.rs | 4 +- .../programs/schedulecommit/src/order_book.rs | 19 +++- .../test-committor-service/tests/common.rs | 9 +- .../tests/test_transaction_preparator.rs | 12 +-- .../test-tools/src/scheduled_commits.rs | 4 - 13 files changed, 211 insertions(+), 79 deletions(-) create mode 100644 magicblock-committor-service/src/intent_executor/null_task_info_fetcher.rs diff --git a/Cargo.lock b/Cargo.lock index 7fe9c62b0..f0fc2dd41 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2909,7 +2909,6 @@ dependencies = [ "log", "lru", "magicblock-committor-program", - "magicblock-config", "magicblock-delegation-program", "magicblock-metrics", "magicblock-program", diff --git a/magicblock-committor-service/Cargo.toml b/magicblock-committor-service/Cargo.toml index 2bd13e61f..73db3b928 100644 --- a/magicblock-committor-service/Cargo.toml +++ b/magicblock-committor-service/Cargo.toml @@ -29,7 +29,6 @@ magicblock-metrics = { workspace = true } magicblock-program = { workspace = true } magicblock-rpc-client = { workspace = true } magicblock-table-mania = { workspace = true } -magicblock-config = { workspace = true } rusqlite = { workspace = true } solana-account = { workspace = true } solana-address-lookup-table-interface = { workspace = true } diff --git a/magicblock-committor-service/src/intent_execution_manager/intent_execution_engine.rs b/magicblock-committor-service/src/intent_execution_manager/intent_execution_engine.rs index 2303ae86b..e2f01745c 100644 --- a/magicblock-committor-service/src/intent_execution_manager/intent_execution_engine.rs +++ b/magicblock-committor-service/src/intent_execution_manager/intent_execution_engine.rs @@ -371,6 +371,8 @@ mod tests { use async_trait::async_trait; use magicblock_program::magic_scheduled_base_intent::ScheduledBaseIntent; + use magicblock_rpc_client::MagicBlockRpcClientResult; + use solana_account::Account; use solana_pubkey::{pubkey, Pubkey}; use solana_signature::Signature; use solana_signer::SignerError; @@ -821,5 +823,25 @@ mod tests { async fn cleanup(self) -> Result<(), BufferExecutionError> { Ok(()) } + + async fn fetch_rent_reimbursements( + &self, + pubkeys: &[Pubkey], + ) -> TaskInfoFetcherResult> { + Ok(pubkeys.iter().map(|_| Pubkey::new_unique()).collect()) + } + + fn peek_commit_id(&self, _pubkey: &Pubkey) -> Option { + None + } + + fn reset(&self, _: ResetType) {} + + async fn get_base_account( + &self, + _pubkey: &Pubkey, + ) -> MagicBlockRpcClientResult> { + Ok(None) // AccountNotFound + } } } diff --git a/magicblock-committor-service/src/intent_executor/mod.rs b/magicblock-committor-service/src/intent_executor/mod.rs index 4c26ac8e0..0dedd5ef8 100644 --- a/magicblock-committor-service/src/intent_executor/mod.rs +++ b/magicblock-committor-service/src/intent_executor/mod.rs @@ -6,6 +6,9 @@ pub mod two_stage_executor; use std::{mem, ops::ControlFlow, sync::Arc, time::Duration}; +#[cfg(any(test, feature = "dev-context-only-utils"))] +mod null_task_info_fetcher; + use async_trait::async_trait; use futures_util::future::{join, try_join_all}; use log::{trace, warn}; @@ -22,8 +25,13 @@ use magicblock_rpc_client::{ MagicBlockRpcClientError, MagicBlockSendTransactionConfig, MagicBlockSendTransactionOutcome, MagicblockRpcClient, }; + use solana_keypair::Keypair; use solana_message::VersionedMessage; + +#[cfg(any(test, feature = "dev-context-only-utils"))] +pub use null_task_info_fetcher::*; + use solana_pubkey::Pubkey; use solana_rpc_client_api::config::RpcTransactionConfig; use solana_signature::Signature; @@ -807,3 +815,88 @@ where try_join_all(cleanup_futs).await.map(|_| ()) } } + +#[cfg(test)] +mod tests { + use std::{collections::HashMap, sync::Arc}; + + use magicblock_rpc_client::MagicBlockRpcClientResult; + use solana_account::Account; + use solana_pubkey::Pubkey; + + use crate::{ + intent_execution_manager::intent_scheduler::create_test_intent, + intent_executor::{ + task_info_fetcher::{ + ResetType, TaskInfoFetcher, TaskInfoFetcherResult, + }, + IntentExecutorImpl, + }, + persist::IntentPersisterImpl, + tasks::task_builder::{TaskBuilderImpl, TasksBuilder}, + transaction_preparator::TransactionPreparatorImpl, + }; + + struct MockInfoFetcher; + #[async_trait::async_trait] + impl TaskInfoFetcher for MockInfoFetcher { + async fn fetch_next_commit_ids( + &self, + pubkeys: &[Pubkey], + ) -> TaskInfoFetcherResult> { + Ok(pubkeys.iter().map(|pubkey| (*pubkey, 0)).collect()) + } + + async fn fetch_rent_reimbursements( + &self, + pubkeys: &[Pubkey], + ) -> TaskInfoFetcherResult> { + Ok(pubkeys.iter().map(|_| Pubkey::new_unique()).collect()) + } + + fn peek_commit_id(&self, _pubkey: &Pubkey) -> Option { + Some(0) + } + + fn reset(&self, _: ResetType) {} + + async fn get_base_account( + &self, + _pubkey: &Pubkey, + ) -> MagicBlockRpcClientResult> { + Ok(None) // AccountNotFound + } + } + + #[tokio::test] + async fn test_try_unite() { + let pubkey = [Pubkey::new_unique()]; + let intent = create_test_intent(0, &pubkey); + + let info_fetcher = Arc::new(MockInfoFetcher); + let commit_task = TaskBuilderImpl::commit_tasks( + &info_fetcher, + &intent, + &None::, + ) + .await + .unwrap(); + let finalize_task = + TaskBuilderImpl::finalize_tasks(&info_fetcher, &intent) + .await + .unwrap(); + + let result = IntentExecutorImpl::< + TransactionPreparatorImpl, + MockInfoFetcher, + >::try_unite_tasks( + &commit_task, + &finalize_task, + &Pubkey::new_unique(), + &None::, + ); + + let strategy = result.unwrap().unwrap(); + assert!(strategy.lookup_tables_keys.is_empty()); + } +} diff --git a/magicblock-committor-service/src/intent_executor/null_task_info_fetcher.rs b/magicblock-committor-service/src/intent_executor/null_task_info_fetcher.rs new file mode 100644 index 000000000..f00c77a17 --- /dev/null +++ b/magicblock-committor-service/src/intent_executor/null_task_info_fetcher.rs @@ -0,0 +1,42 @@ +use std::collections::HashMap; + +use async_trait::async_trait; +use magicblock_rpc_client::MagicBlockRpcClientResult; +use solana_account::Account; +use solana_pubkey::Pubkey; + +use super::task_info_fetcher::{ + ResetType, TaskInfoFetcher, TaskInfoFetcherResult, +}; + +pub struct NullTaskInfoFetcher; + +#[async_trait] +impl TaskInfoFetcher for NullTaskInfoFetcher { + async fn fetch_next_commit_ids( + &self, + _pubkeys: &[Pubkey], + ) -> TaskInfoFetcherResult> { + Ok(Default::default()) + } + + async fn fetch_rent_reimbursements( + &self, + _pubkeys: &[Pubkey], + ) -> TaskInfoFetcherResult> { + Ok(Default::default()) + } + + fn peek_commit_id(&self, _pubkey: &Pubkey) -> Option { + None + } + + fn reset(&self, _: ResetType) {} + + async fn get_base_account( + &self, + _pubkey: &Pubkey, + ) -> MagicBlockRpcClientResult> { + Ok(None) // AccountNotFound + } +} diff --git a/magicblock-committor-service/src/intent_executor/task_info_fetcher.rs b/magicblock-committor-service/src/intent_executor/task_info_fetcher.rs index 1d86400a6..b124f8667 100644 --- a/magicblock-committor-service/src/intent_executor/task_info_fetcher.rs +++ b/magicblock-committor-service/src/intent_executor/task_info_fetcher.rs @@ -43,9 +43,7 @@ pub trait TaskInfoFetcher: Send + Sync + 'static { async fn get_base_account( &self, _pubkey: &Pubkey, - ) -> MagicBlockRpcClientResult> { - Ok(None) // AccountNotFound - } + ) -> MagicBlockRpcClientResult>; } pub enum ResetType<'a> { @@ -309,37 +307,3 @@ impl TaskInfoFetcherError { } pub type TaskInfoFetcherResult = Result; - -#[cfg(any(test, feature = "dev-context-only-utils"))] -pub struct NullTaskInfoFetcher; - -#[cfg(any(test, feature = "dev-context-only-utils"))] -#[async_trait] -impl TaskInfoFetcher for NullTaskInfoFetcher { - async fn fetch_next_commit_ids( - &self, - _pubkeys: &[Pubkey], - ) -> TaskInfoFetcherResult> { - Ok(Default::default()) - } - - async fn fetch_rent_reimbursements( - &self, - _pubkeys: &[Pubkey], - ) -> TaskInfoFetcherResult> { - Ok(Default::default()) - } - - fn peek_commit_id(&self, _pubkey: &Pubkey) -> Option { - None - } - - fn reset(&self, _: ResetType) {} - - async fn get_base_account( - &self, - _pubkey: &Pubkey, - ) -> MagicBlockRpcClientResult> { - Ok(None) // AccountNotFound - } -} diff --git a/magicblock-committor-service/src/tasks/mod.rs b/magicblock-committor-service/src/tasks/mod.rs index 9b4377e63..2c7365470 100644 --- a/magicblock-committor-service/src/tasks/mod.rs +++ b/magicblock-committor-service/src/tasks/mod.rs @@ -110,30 +110,23 @@ pub trait BaseTask: Send + Sync + DynClone + LabelValue { dyn_clone::clone_trait_object!(BaseTask); -#[derive(Clone)] -pub struct CommitTask { - pub commit_id: u64, - pub allow_undelegation: bool, - pub committed_account: CommittedAccount, - base_account: Option, - force_commit_state: bool, -} +pub struct CommitTaskBuilder; -impl CommitTask { +impl CommitTaskBuilder { // Accounts larger than COMMIT_STATE_SIZE_THRESHOLD, use CommitDiff to // reduce instruction size. Below this, commit is sent as CommitState. // Chose 256 as thresold seems good enough as it could hold 8 u32 fields // or 4 u64 fields. const COMMIT_STATE_SIZE_THRESHOLD: usize = 256; - pub async fn new( + pub async fn create_commit_task( commit_id: u64, allow_undelegation: bool, committed_account: CommittedAccount, task_info_fetcher: &Arc, - ) -> Self { + ) -> CommitTask { let base_account = if committed_account.account.data.len() - > CommitTask::COMMIT_STATE_SIZE_THRESHOLD + > CommitTaskBuilder::COMMIT_STATE_SIZE_THRESHOLD { match task_info_fetcher .get_base_account(&committed_account.pubkey) @@ -155,7 +148,7 @@ impl CommitTask { None }; - Self { + CommitTask { commit_id, allow_undelegation, committed_account, @@ -163,11 +156,22 @@ impl CommitTask { force_commit_state: false, } } +} +#[derive(Clone)] +pub struct CommitTask { + pub commit_id: u64, + pub allow_undelegation: bool, + pub committed_account: CommittedAccount, + base_account: Option, + force_commit_state: bool, +} + +impl CommitTask { pub fn is_commit_diff(&self) -> bool { !self.force_commit_state && self.committed_account.account.data.len() - > CommitTask::COMMIT_STATE_SIZE_THRESHOLD + > CommitTaskBuilder::COMMIT_STATE_SIZE_THRESHOLD && self.base_account.is_some() } @@ -420,7 +424,7 @@ mod serialization_safety_test { use solana_account::Account; use crate::{ - intent_executor::task_info_fetcher::NullTaskInfoFetcher, + intent_executor::NullTaskInfoFetcher, tasks::{ args_task::{ArgsTask, ArgsTaskType}, buffer_task::{BufferTask, BufferTaskType}, @@ -435,7 +439,7 @@ mod serialization_safety_test { // Test Commit variant let commit_task: ArgsTask = ArgsTaskType::Commit( - CommitTask::new( + CommitTaskBuilder::create_commit_task( 123, true, CommittedAccount { @@ -499,7 +503,7 @@ mod serialization_safety_test { let buffer_task = BufferTask::new_preparation_required(BufferTaskType::Commit( - CommitTask::new( + CommitTaskBuilder::create_commit_task( 456, false, CommittedAccount { @@ -527,7 +531,7 @@ mod serialization_safety_test { // Test BufferTask preparation let buffer_task = BufferTask::new_preparation_required(BufferTaskType::Commit( - CommitTask::new( + CommitTaskBuilder::create_commit_task( 789, true, CommittedAccount { diff --git a/magicblock-committor-service/src/tasks/task_builder.rs b/magicblock-committor-service/src/tasks/task_builder.rs index ec5303b8e..f3631bf32 100644 --- a/magicblock-committor-service/src/tasks/task_builder.rs +++ b/magicblock-committor-service/src/tasks/task_builder.rs @@ -17,7 +17,8 @@ use crate::{ persist::IntentPersister, tasks::{ args_task::{ArgsTask, ArgsTaskType}, - BaseActionTask, BaseTask, CommitTask, FinalizeTask, UndelegateTask, + BaseActionTask, BaseTask, CommitTaskBuilder, FinalizeTask, + UndelegateTask, }, }; @@ -91,7 +92,7 @@ impl TasksBuilder for TaskBuilderImpl { .iter() .map(|account| async { let commit_id = *commit_ids.get(&account.pubkey).expect("CommitIdFetcher provide commit ids for all listed pubkeys, or errors!"); - let task = ArgsTaskType::Commit(CommitTask::new( + let task = ArgsTaskType::Commit(CommitTaskBuilder::create_commit_task( commit_id, allow_undelegation, account.clone(), diff --git a/magicblock-committor-service/src/tasks/task_strategist.rs b/magicblock-committor-service/src/tasks/task_strategist.rs index 3d442584e..4814118ef 100644 --- a/magicblock-committor-service/src/tasks/task_strategist.rs +++ b/magicblock-committor-service/src/tasks/task_strategist.rs @@ -387,6 +387,8 @@ mod tests { }, persist::IntentPersisterImpl, tasks::{ + intent_executor::NullTaskInfoFetcher, + persist::IntentPersisterImpl, task_builder::{TaskBuilderImpl, TasksBuilder}, BaseActionTask, CommitTask, TaskStrategy, UndelegateTask, }, @@ -422,7 +424,7 @@ mod tests { data_size: usize, ) -> ArgsTask { ArgsTask::new(ArgsTaskType::Commit( - CommitTask::new( + CommitTaskBuilder::create_commit_task( commit_id, false, CommittedAccount { diff --git a/test-integration/programs/schedulecommit/src/order_book.rs b/test-integration/programs/schedulecommit/src/order_book.rs index d081482ca..12d655080 100644 --- a/test-integration/programs/schedulecommit/src/order_book.rs +++ b/test-integration/programs/schedulecommit/src/order_book.rs @@ -115,11 +115,22 @@ impl<'a> OrderBook<'a> { "data is not properly aligned for OrderBook to be constructed" ); + let capacity = levels_bytes.len() / ORDER_LEVEL_SIZE; + let header = + unsafe { &mut *(header_bytes.as_ptr() as *mut OrderBookHeader) }; + + // Ensure header lengths never exceed backing storage; if they do, treat it as corrupted + // data and panic rather than creating out-of-bounds slices in `bids()`/`asks_reversed()`. + let used = header.bids_len as usize + header.asks_len as usize; + + assert!( + used <= capacity, + "OrderBook header lengths (bids_len + asks_len) exceed capacity" + ); + Self { - header: unsafe { - &mut *(header_bytes.as_ptr() as *mut OrderBookHeader) - }, - capacity: levels_bytes.len() / ORDER_LEVEL_SIZE, + header, + capacity, levels: levels_bytes.as_mut_ptr() as *mut OrderLevel, } } diff --git a/test-integration/test-committor-service/tests/common.rs b/test-integration/test-committor-service/tests/common.rs index 9ea8edc68..ecef5d3e2 100644 --- a/test-integration/test-committor-service/tests/common.rs +++ b/test-integration/test-committor-service/tests/common.rs @@ -10,12 +10,11 @@ use async_trait::async_trait; use magicblock_committor_service::{ intent_executor::{ task_info_fetcher::{ - NullTaskInfoFetcher, ResetType, TaskInfoFetcher, - TaskInfoFetcherResult, + ResetType, TaskInfoFetcher, TaskInfoFetcherResult, }, - IntentExecutorImpl, + IntentExecutorImpl, NullTaskInfoFetcher, }, - tasks::CommitTask, + tasks::{CommitTask, CommitTaskBuilder}, transaction_preparator::{ delivery_preparator::DeliveryPreparator, TransactionPreparatorImpl, }, @@ -162,7 +161,7 @@ pub fn generate_random_bytes(length: usize) -> Vec { #[allow(dead_code)] pub async fn create_commit_task(data: &[u8]) -> CommitTask { static COMMIT_ID: AtomicU64 = AtomicU64::new(0); - CommitTask::new( + CommitTaskBuilder::create_commit_task( COMMIT_ID.fetch_add(1, Ordering::Relaxed), false, CommittedAccount { diff --git a/test-integration/test-committor-service/tests/test_transaction_preparator.rs b/test-integration/test-committor-service/tests/test_transaction_preparator.rs index 52037239e..123f13c38 100644 --- a/test-integration/test-committor-service/tests/test_transaction_preparator.rs +++ b/test-integration/test-committor-service/tests/test_transaction_preparator.rs @@ -7,8 +7,8 @@ use magicblock_committor_service::{ buffer_task::{BufferTask, BufferTaskType}, task_strategist::{TaskStrategist, TransactionStrategy}, utils::TransactionUtils, - BaseActionTask, BaseTask, CommitTask, FinalizeTask, PreparationState, - UndelegateTask, + BaseActionTask, BaseTask, CommitTaskBuilder, FinalizeTask, + PreparationState, UndelegateTask, }, transaction_preparator::TransactionPreparator, }; @@ -36,7 +36,7 @@ async fn test_prepare_commit_tx_with_single_account() { let tasks = vec![ Box::new(ArgsTask::new(ArgsTaskType::Commit( - CommitTask::new( + CommitTaskBuilder::create_commit_task( 1, true, committed_account.clone(), @@ -95,7 +95,7 @@ async fn test_prepare_commit_tx_with_multiple_accounts() { let buffer_commit_task = BufferTask::new_preparation_required(BufferTaskType::Commit( - CommitTask::new( + CommitTaskBuilder::create_commit_task( 1, true, committed_account2.clone(), @@ -107,7 +107,7 @@ async fn test_prepare_commit_tx_with_multiple_accounts() { let tasks = vec![ // account 1 Box::new(ArgsTask::new(ArgsTaskType::Commit( - CommitTask::new( + CommitTaskBuilder::create_commit_task( 1, true, committed_account1.clone(), @@ -199,7 +199,7 @@ async fn test_prepare_commit_tx_with_base_actions() { let buffer_commit_task = BufferTask::new_preparation_required(BufferTaskType::Commit( - CommitTask::new( + CommitTaskBuilder::create_commit_task( 1, true, committed_account.clone(), diff --git a/test-integration/test-tools/src/scheduled_commits.rs b/test-integration/test-tools/src/scheduled_commits.rs index aae357654..0b64cb7c8 100644 --- a/test-integration/test-tools/src/scheduled_commits.rs +++ b/test-integration/test-tools/src/scheduled_commits.rs @@ -195,8 +195,6 @@ impl IntegrationTestContext { (logs, sig) }; - println!("Ephem Logs level-1: {:#?}", ephem_logs_l1); - // 2. Find chain commit signatures let ephem_logs_l2 = self .fetch_ephemeral_logs(scheduled_commmit_sent_sig) @@ -207,8 +205,6 @@ impl IntegrationTestContext { ) })?; - println!("Ephem Logs level-2: {:#?}", ephem_logs_l2); - let (included, excluded, feepayers, sigs) = extract_sent_commit_info_from_logs(&ephem_logs_l2); From dc395bd337c8aaaed1f38a882eed0d480de0b0c2 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Tue, 16 Dec 2025 19:29:52 +0530 Subject: [PATCH 04/13] fix/remove merge-related issues --- .../intent_execution_engine.rs | 20 ----- .../src/intent_executor/mod.rs | 85 ------------------- .../src/tasks/args_task.rs | 6 +- 3 files changed, 1 insertion(+), 110 deletions(-) diff --git a/magicblock-committor-service/src/intent_execution_manager/intent_execution_engine.rs b/magicblock-committor-service/src/intent_execution_manager/intent_execution_engine.rs index e2f01745c..2507b0b3b 100644 --- a/magicblock-committor-service/src/intent_execution_manager/intent_execution_engine.rs +++ b/magicblock-committor-service/src/intent_execution_manager/intent_execution_engine.rs @@ -823,25 +823,5 @@ mod tests { async fn cleanup(self) -> Result<(), BufferExecutionError> { Ok(()) } - - async fn fetch_rent_reimbursements( - &self, - pubkeys: &[Pubkey], - ) -> TaskInfoFetcherResult> { - Ok(pubkeys.iter().map(|_| Pubkey::new_unique()).collect()) - } - - fn peek_commit_id(&self, _pubkey: &Pubkey) -> Option { - None - } - - fn reset(&self, _: ResetType) {} - - async fn get_base_account( - &self, - _pubkey: &Pubkey, - ) -> MagicBlockRpcClientResult> { - Ok(None) // AccountNotFound - } } } diff --git a/magicblock-committor-service/src/intent_executor/mod.rs b/magicblock-committor-service/src/intent_executor/mod.rs index 0dedd5ef8..0e6dc554a 100644 --- a/magicblock-committor-service/src/intent_executor/mod.rs +++ b/magicblock-committor-service/src/intent_executor/mod.rs @@ -815,88 +815,3 @@ where try_join_all(cleanup_futs).await.map(|_| ()) } } - -#[cfg(test)] -mod tests { - use std::{collections::HashMap, sync::Arc}; - - use magicblock_rpc_client::MagicBlockRpcClientResult; - use solana_account::Account; - use solana_pubkey::Pubkey; - - use crate::{ - intent_execution_manager::intent_scheduler::create_test_intent, - intent_executor::{ - task_info_fetcher::{ - ResetType, TaskInfoFetcher, TaskInfoFetcherResult, - }, - IntentExecutorImpl, - }, - persist::IntentPersisterImpl, - tasks::task_builder::{TaskBuilderImpl, TasksBuilder}, - transaction_preparator::TransactionPreparatorImpl, - }; - - struct MockInfoFetcher; - #[async_trait::async_trait] - impl TaskInfoFetcher for MockInfoFetcher { - async fn fetch_next_commit_ids( - &self, - pubkeys: &[Pubkey], - ) -> TaskInfoFetcherResult> { - Ok(pubkeys.iter().map(|pubkey| (*pubkey, 0)).collect()) - } - - async fn fetch_rent_reimbursements( - &self, - pubkeys: &[Pubkey], - ) -> TaskInfoFetcherResult> { - Ok(pubkeys.iter().map(|_| Pubkey::new_unique()).collect()) - } - - fn peek_commit_id(&self, _pubkey: &Pubkey) -> Option { - Some(0) - } - - fn reset(&self, _: ResetType) {} - - async fn get_base_account( - &self, - _pubkey: &Pubkey, - ) -> MagicBlockRpcClientResult> { - Ok(None) // AccountNotFound - } - } - - #[tokio::test] - async fn test_try_unite() { - let pubkey = [Pubkey::new_unique()]; - let intent = create_test_intent(0, &pubkey); - - let info_fetcher = Arc::new(MockInfoFetcher); - let commit_task = TaskBuilderImpl::commit_tasks( - &info_fetcher, - &intent, - &None::, - ) - .await - .unwrap(); - let finalize_task = - TaskBuilderImpl::finalize_tasks(&info_fetcher, &intent) - .await - .unwrap(); - - let result = IntentExecutorImpl::< - TransactionPreparatorImpl, - MockInfoFetcher, - >::try_unite_tasks( - &commit_task, - &finalize_task, - &Pubkey::new_unique(), - &None::, - ); - - let strategy = result.unwrap().unwrap(); - assert!(strategy.lookup_tables_keys.is_empty()); - } -} diff --git a/magicblock-committor-service/src/tasks/args_task.rs b/magicblock-committor-service/src/tasks/args_task.rs index 75a960b04..70c45c764 100644 --- a/magicblock-committor-service/src/tasks/args_task.rs +++ b/magicblock-committor-service/src/tasks/args_task.rs @@ -1,9 +1,5 @@ -use dlp::{ - args::{CallHandlerArgs, CommitDiffArgs, CommitStateArgs}, - compute_diff, -}; +use dlp::args::CallHandlerArgs; use magicblock_metrics::metrics::LabelValue; -use solana_account::ReadableAccount; use solana_instruction::{AccountMeta, Instruction}; use solana_pubkey::Pubkey; From a0ae8e373e7da3ac30ad86febe510c665822ee6f Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Fri, 19 Dec 2025 21:04:30 +0530 Subject: [PATCH 05/13] Update the tests --- .../src/magic_scheduled_base_intent.rs | 1 - test-integration/Cargo.lock | 251 +++++++++++++++--- .../programs/schedulecommit/src/lib.rs | 27 +- .../programs/schedulecommit/src/order_book.rs | 43 +-- .../tests/02_commit_and_undelegate.rs | 14 +- 5 files changed, 268 insertions(+), 68 deletions(-) diff --git a/programs/magicblock/src/magic_scheduled_base_intent.rs b/programs/magicblock/src/magic_scheduled_base_intent.rs index 2de827122..b8e64380b 100644 --- a/programs/magicblock/src/magic_scheduled_base_intent.rs +++ b/programs/magicblock/src/magic_scheduled_base_intent.rs @@ -309,7 +309,6 @@ impl BaseAction { } type CommittedAccountRef<'a> = (Pubkey, &'a RefCell); - #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct CommittedAccount { pub pubkey: Pubkey, diff --git a/test-integration/Cargo.lock b/test-integration/Cargo.lock index 8e439cdbd..64948fb16 100644 --- a/test-integration/Cargo.lock +++ b/test-integration/Cargo.lock @@ -79,6 +79,17 @@ dependencies = [ "solana-svm-transaction", ] +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "version_check", +] + [[package]] name = "ahash" version = "0.8.12" @@ -591,6 +602,18 @@ dependencies = [ "typenum", ] +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "blake3" version = "1.8.2" @@ -737,6 +760,28 @@ dependencies = [ "serde", ] +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta 0.1.4", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "bytemuck" version = "1.23.1" @@ -1542,7 +1587,7 @@ dependencies = [ "ephemeral-rollups-sdk-attribute-commit", "ephemeral-rollups-sdk-attribute-delegate", "ephemeral-rollups-sdk-attribute-ephemeral", - "magicblock-delegation-program", + "magicblock-delegation-program 1.1.0", "magicblock-magic-program-api 0.2.1", "solana-program", ] @@ -1656,7 +1701,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6503af7917fea18ffef8f7e8553fb8dff89e2e6837e94e09dd7fb069c82d62c" dependencies = [ "bytes", - "rkyv", + "rkyv 0.8.11", "serde", "simdutf8", ] @@ -1840,6 +1885,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.31" @@ -2109,6 +2160,9 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] [[package]] name = "hashbrown" @@ -2116,7 +2170,7 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" dependencies = [ - "ahash", + "ahash 0.8.12", ] [[package]] @@ -2619,7 +2673,7 @@ dependencies = [ "log", "magicblock-config", "magicblock-core", - "magicblock-delegation-program", + "magicblock-delegation-program 1.1.2", "random-port", "rayon", "serde", @@ -3190,7 +3244,7 @@ dependencies = [ "lru", "magicblock-config", "magicblock-core", - "magicblock-delegation-program", + "magicblock-delegation-program 1.1.2", "magicblock-magic-program-api 0.5.0", "magicblock-metrics", "solana-account", @@ -3249,7 +3303,7 @@ dependencies = [ "log", "lru", "magicblock-committor-program", - "magicblock-delegation-program", + "magicblock-delegation-program 1.1.2", "magicblock-metrics", "magicblock-program", "magicblock-rpc-client", @@ -3335,6 +3389,29 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "magicblock-delegation-program" +version = "1.1.2" +source = "git+https://github.com/magicblock-labs/delegation-program.git?rev=e8d03936#e8d039369ac1149e899ea94f31e0f9cc4e600a38" +dependencies = [ + "bincode", + "borsh 1.5.7", + "bytemuck", + "num_enum", + "paste", + "pinocchio", + "pinocchio-log", + "pinocchio-pubkey", + "pinocchio-system", + "rkyv 0.7.45", + "solana-curve25519", + "solana-program", + "solana-security-txt", + "static_assertions", + "strum 0.27.2", + "thiserror 1.0.69", +] + [[package]] name = "magicblock-ledger" version = "0.5.0" @@ -3553,7 +3630,7 @@ name = "magicblock-validator-admin" version = "0.5.0" dependencies = [ "log", - "magicblock-delegation-program", + "magicblock-delegation-program 1.1.2", "magicblock-program", "magicblock-rpc-client", "solana-commitment-config", @@ -4426,9 +4503,11 @@ version = "0.0.0" dependencies = [ "borsh 1.5.7", "ephemeral-rollups-sdk", - "magicblock-delegation-program", + "magicblock-delegation-program 1.1.2", "magicblock-magic-program-api 0.5.0", + "rkyv 0.7.45", "solana-program", + "static_assertions", ] [[package]] @@ -4525,13 +4604,33 @@ dependencies = [ "autotools", ] +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive 0.1.4", +] + [[package]] name = "ptr_meta" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe9e76f66d3f9606f44e45598d155cb13ecf09f4a28199e48daf8c8fc937ea90" dependencies = [ - "ptr_meta_derive", + "ptr_meta_derive 0.3.0", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", ] [[package]] @@ -4652,13 +4751,19 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rancor" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "caf5f7161924b9d1cea0e4cabc97c372cea92b5f927fc13c6bca67157a0ad947" dependencies = [ - "ptr_meta", + "ptr_meta 0.3.0", ] [[package]] @@ -4880,6 +4985,15 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + [[package]] name = "rend" version = "0.5.2" @@ -4959,6 +5073,24 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rkyv" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta 0.1.4", + "rend 0.4.2", + "rkyv_derive 0.7.45", + "seahash", + "tinyvec", + "uuid", +] + [[package]] name = "rkyv" version = "0.8.11" @@ -4969,14 +5101,25 @@ dependencies = [ "hashbrown 0.15.4", "indexmap 2.10.0", "munge", - "ptr_meta", + "ptr_meta 0.3.0", "rancor", - "rend", - "rkyv_derive", + "rend 0.5.2", + "rkyv_derive 0.8.11", "tinyvec", "uuid", ] +[[package]] +name = "rkyv_derive" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "rkyv_derive" version = "0.8.11" @@ -5236,7 +5379,7 @@ dependencies = [ "integration-test-tools", "log", "magicblock-core", - "magicblock-delegation-program", + "magicblock-delegation-program 1.1.2", "program-schedulecommit", "solana-program", "solana-rpc-client", @@ -5254,7 +5397,7 @@ dependencies = [ "log", "magicblock-committor-program", "magicblock-committor-service", - "magicblock-delegation-program", + "magicblock-delegation-program 1.1.2", "magicblock-program", "magicblock-rpc-client", "magicblock-table-mania", @@ -5273,12 +5416,14 @@ dependencies = [ name = "schedulecommit-test-scenarios" version = "0.0.0" dependencies = [ + "borsh 1.5.7", "ephemeral-rollups-sdk", "integration-test-tools", "log", "magicblock-core", "magicblock-magic-program-api 0.5.0", "program-schedulecommit", + "rand 0.8.5", "schedulecommit-client", "solana-program", "solana-rpc-client", @@ -5347,6 +5492,12 @@ version = "3.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "584e070911c7017da6cb2eb0788d09f43d789029b5877d3e5ecc8acf86ceee21" +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + [[package]] name = "security-framework" version = "3.2.0" @@ -5747,7 +5898,7 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d65a1a23a53cae19cb92bab2cbdd9e289e5210bb12175ce27642c94adf74b220" dependencies = [ - "ahash", + "ahash 0.8.12", "bincode", "blake3", "bv", @@ -6048,7 +6199,7 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4ee734c35b736e632aa3b1367f933d93ee7b4129dd1e20ca942205d4834054e" dependencies = [ - "ahash", + "ahash 0.8.12", "lazy_static", "log", "qualifier_attr", @@ -6274,7 +6425,7 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a675ead1473b32a7a5735801608b35cbd8d3f5057ca8dbafdd5976146bb7e9e4" dependencies = [ - "ahash", + "ahash 0.8.12", "lazy_static", "log", "solana-bincode", @@ -6460,7 +6611,7 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89e1d3b52b4a014efeaaab67f14e40af3972a4be61c523d612860db8e3145529" dependencies = [ - "ahash", + "ahash 0.8.12", "lazy_static", "solana-epoch-schedule", "solana-hash", @@ -6939,7 +7090,7 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f0962d3818fc942a888f7c2d530896aeaf6f2da2187592a67bbdc8cf8a54192" dependencies = [ - "ahash", + "ahash 0.8.12", "bincode", "bv", "caps", @@ -7494,7 +7645,7 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5335e7925f6dc8d2fdcdc6ead3b190aca65f191a11cef74709a7a6ab5d0d5877" dependencies = [ - "ahash", + "ahash 0.8.12", "aquamarine", "arrayref", "base64 0.22.1", @@ -7565,8 +7716,8 @@ dependencies = [ "solana-vote", "solana-vote-program", "static_assertions", - "strum", - "strum_macros", + "strum 0.24.1", + "strum_macros 0.24.3", "symlink", "tar", "tempfile", @@ -8046,7 +8197,7 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0850baf834aba4a94a7558fa6cf6ca93fad284abf0363dec5fb9cab173a11fc4" dependencies = [ - "ahash", + "ahash 0.8.12", "itertools 0.12.1", "log", "percentage", @@ -8090,7 +8241,7 @@ name = "solana-svm" version = "2.2.1" source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=3e9456ec4#3e9456ec4d5798ad8281537501c1e777d6888ba3" dependencies = [ - "ahash", + "ahash 0.8.12", "log", "percentage", "qualifier_attr", @@ -8749,7 +8900,7 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd1adc42def3cb101f3ebef3cd2d642f9a21072bbcd4ec9423343ccaa6afa596" dependencies = [ - "ahash", + "ahash 0.8.12", "bumpalo", "bytes", "cfg-if", @@ -9183,7 +9334,16 @@ version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" dependencies = [ - "strum_macros", + "strum_macros 0.24.3", +] + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros 0.27.2", ] [[package]] @@ -9199,6 +9359,18 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "subtle" version = "2.6.1" @@ -9290,6 +9462,12 @@ dependencies = [ "solana-program", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tar" version = "0.4.44" @@ -9389,7 +9567,7 @@ dependencies = [ "log", "magicblock-chainlink", "magicblock-config", - "magicblock-delegation-program", + "magicblock-delegation-program 1.1.2", "program-flexi-counter", "program-mini", "solana-account", @@ -9471,7 +9649,7 @@ dependencies = [ "log", "magicblock-accounts-db", "magicblock-config", - "magicblock-delegation-program", + "magicblock-delegation-program 1.1.2", "program-flexi-counter", "solana-rpc-client", "solana-sdk", @@ -9492,7 +9670,7 @@ dependencies = [ "magic-domain-program", "magicblock-api", "magicblock-config", - "magicblock-delegation-program", + "magicblock-delegation-program 1.1.2", "magicblock-program", "magicblock-validator-admin", "solana-rpc-client", @@ -9532,7 +9710,7 @@ version = "0.0.0" dependencies = [ "integration-test-tools", "log", - "magicblock-delegation-program", + "magicblock-delegation-program 1.1.2", "program-flexi-counter", "solana-rpc-client-api", "solana-sdk", @@ -10777,6 +10955,15 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "x509-parser" version = "0.14.0" @@ -10956,4 +11143,4 @@ dependencies = [ "bindgen 0.71.1", "cc", "pkg-config", -] \ No newline at end of file +] diff --git a/test-integration/programs/schedulecommit/src/lib.rs b/test-integration/programs/schedulecommit/src/lib.rs index 009c51281..11fbd7d99 100644 --- a/test-integration/programs/schedulecommit/src/lib.rs +++ b/test-integration/programs/schedulecommit/src/lib.rs @@ -166,6 +166,7 @@ pub fn process_instruction<'a>( accounts: &'a [AccountInfo<'a>], instruction_data: &[u8], ) -> ProgramResult { + msg!("process_instruction: {}", instruction_data[0]); // Undelegate Instruction if instruction_data.len() >= EXTERNAL_UNDELEGATE_DISCRIMINATOR.len() { let (disc, seeds_data) = @@ -438,7 +439,7 @@ fn process_update_order_book<'a>( let mut book_raw = order_book_account.try_borrow_mut_data()?; - OrderBook::new(&mut book_raw).update_from(updates); + OrderBook::try_new(&mut book_raw)?.update_from(updates); Ok(()) } @@ -820,15 +821,23 @@ fn process_undelegate_request( )?; { - let data = delegated_account.try_borrow_data()?; - match MainAccount::try_from_slice(&data) { - Ok(counter) => { - msg!("counter: {:?}", counter); - if counter.count == FAIL_UNDELEGATION_COUNT { - return Err(ProgramError::Custom(111)); + let mut data = delegated_account.try_borrow_mut_data()?; + match data.len() as u64 { + MainAccount::SIZE => match MainAccount::try_from_slice(&data) { + Ok(counter) => { + msg!("counter: {:?}", counter); + if counter.count == FAIL_UNDELEGATION_COUNT { + return Err(ProgramError::Custom(111)); + } } - } - Err(err) => msg!("Failed to deserialize: {:?}", err), + Err(err) => { + msg!("Failed to deserialize MainAccount: {:?}", err) + } + }, + _ => match OrderBook::try_new(&mut data) { + Ok(_) => {} + Err(err) => msg!("Failed to deserialize OrderBook: {:?}", err), + }, } }; Ok(()) diff --git a/test-integration/programs/schedulecommit/src/order_book.rs b/test-integration/programs/schedulecommit/src/order_book.rs index 12d655080..d36298305 100644 --- a/test-integration/programs/schedulecommit/src/order_book.rs +++ b/test-integration/programs/schedulecommit/src/order_book.rs @@ -4,6 +4,7 @@ use std::{ }; use borsh::{BorshDeserialize, BorshSerialize}; +use solana_program::{msg, program_error::ProgramError}; use static_assertions::const_assert; #[repr(C)] @@ -66,13 +67,16 @@ impl borsh::de::BorshDeserialize for OrderBookOwned { aligned }; - Ok(Self::from(&OrderBook::new(unsafe { + OrderBook::try_new(unsafe { slice::from_raw_parts_mut( book_bytes.as_ptr() as *mut u8, book_bytes.len(), ) - }))) + }) + .map(|book| OrderBookOwned::from(&book)) + .map_err(|_| borsh::io::Error::from(borsh::io::ErrorKind::InvalidData)) } + fn deserialize_reader( _reader: &mut R, ) -> ::core::result::Result { @@ -102,18 +106,21 @@ impl<'a> OrderBook<'a> { // - asks grows towards right // - bids grows towards left // - pub fn new(data: &'a mut [u8]) -> Self { + pub fn try_new(data: &'a mut [u8]) -> Result { let (header_bytes, levels_bytes) = data.split_at_mut(HEADER_SIZE); - assert!( - header_bytes - .as_ptr() - .align_offset(align_of::()) - == 0 - && levels_bytes.as_ptr().align_offset(align_of::()) - == 0, - "data is not properly aligned for OrderBook to be constructed" - ); + if !(header_bytes + .as_ptr() + .align_offset(align_of::()) + == 0 + && levels_bytes.as_ptr().align_offset(align_of::()) + == 0) + { + msg!( + "data is not properly aligned for OrderBook to be constructed" + ); + return Err(ProgramError::InvalidAccountData); + } let capacity = levels_bytes.len() / ORDER_LEVEL_SIZE; let header = @@ -123,16 +130,16 @@ impl<'a> OrderBook<'a> { // data and panic rather than creating out-of-bounds slices in `bids()`/`asks_reversed()`. let used = header.bids_len as usize + header.asks_len as usize; - assert!( - used <= capacity, - "OrderBook header lengths (bids_len + asks_len) exceed capacity" - ); + if used > capacity { + msg!("OrderBook header lengths (bids_len + asks_len) exceed capacity"); + return Err(ProgramError::InvalidAccountData); + } - Self { + Ok(Self { header, capacity, levels: levels_bytes.as_mut_ptr() as *mut OrderLevel, - } + }) } pub fn update_from(&mut self, updates: BookUpdate) { diff --git a/test-integration/schedulecommit/test-scenarios/tests/02_commit_and_undelegate.rs b/test-integration/schedulecommit/test-scenarios/tests/02_commit_and_undelegate.rs index f96a3806d..daaea5506 100644 --- a/test-integration/schedulecommit/test-scenarios/tests/02_commit_and_undelegate.rs +++ b/test-integration/schedulecommit/test-scenarios/tests/02_commit_and_undelegate.rs @@ -10,14 +10,10 @@ use program_schedulecommit::{ schedule_commit_and_undelegate_cpi_instruction, schedule_commit_and_undelegate_cpi_twice, schedule_commit_and_undelegate_cpi_with_mod_after_instruction, - set_count_instruction, - }, - FAIL_UNDELEGATION_COUNT, - schedule_commit_and_undelegate_cpi_with_mod_after_instruction, - schedule_commit_diff_instruction_for_order_book, + schedule_commit_diff_instruction_for_order_book, set_count_instruction, update_order_book_instruction, }, - BookUpdate, OrderLevel, + BookUpdate, OrderLevel, FAIL_UNDELEGATION_COUNT, }; use rand::{RngCore, SeedableRng}; use schedulecommit_client::{ @@ -236,7 +232,8 @@ fn commit_and_undelegate_two_accounts_twice() -> ( Signature, Result, ) { - let ctx = get_context_with_delegated_committees(2); + let ctx = + get_context_with_delegated_committees(2, b"magic_schedule_commit"); let ScheduleCommitTestContextFields { payer_ephem: payer, committees, @@ -741,7 +738,8 @@ fn test_committing_and_undelegating_two_accounts_twice() { #[test] fn test_committing_after_failed_undelegation() { run_test!({ - let ctx = get_context_with_delegated_committees(1); + let ctx = + get_context_with_delegated_committees(1, b"magic_schedule_commit"); let ScheduleCommitTestContextFields { payer_ephem: payer, committees, From a939ccba8fa7714efedbdb44d02dd19c0cf3e5c1 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Fri, 19 Dec 2025 23:55:02 +0530 Subject: [PATCH 06/13] Remove CommitTaskBuilder --- magicblock-committor-service/src/tasks/mod.rs | 63 ++++--------------- .../src/tasks/task_builder.rs | 47 +++++++++++++- .../src/tasks/task_strategist.rs | 2 +- .../test-committor-service/tests/common.rs | 4 +- .../tests/test_transaction_preparator.rs | 12 ++-- 5 files changed, 64 insertions(+), 64 deletions(-) diff --git a/magicblock-committor-service/src/tasks/mod.rs b/magicblock-committor-service/src/tasks/mod.rs index 2c7365470..4506df094 100644 --- a/magicblock-committor-service/src/tasks/mod.rs +++ b/magicblock-committor-service/src/tasks/mod.rs @@ -110,54 +110,6 @@ pub trait BaseTask: Send + Sync + DynClone + LabelValue { dyn_clone::clone_trait_object!(BaseTask); -pub struct CommitTaskBuilder; - -impl CommitTaskBuilder { - // Accounts larger than COMMIT_STATE_SIZE_THRESHOLD, use CommitDiff to - // reduce instruction size. Below this, commit is sent as CommitState. - // Chose 256 as thresold seems good enough as it could hold 8 u32 fields - // or 4 u64 fields. - const COMMIT_STATE_SIZE_THRESHOLD: usize = 256; - - pub async fn create_commit_task( - commit_id: u64, - allow_undelegation: bool, - committed_account: CommittedAccount, - task_info_fetcher: &Arc, - ) -> CommitTask { - let base_account = if committed_account.account.data.len() - > CommitTaskBuilder::COMMIT_STATE_SIZE_THRESHOLD - { - match task_info_fetcher - .get_base_account(&committed_account.pubkey) - .await - { - Ok(Some(account)) => Some(account), - Ok(None) => { - log::warn!("AccountNotFound for commit_diff, pubkey: {}, commit_id: {}, Falling back to commit_state.", - committed_account.pubkey, commit_id); - None - } - Err(e) => { - log::warn!("Failed to fetch base account for commit diff, pubkey: {}, commit_id: {}, error: {}. Falling back to commit_state.", - committed_account.pubkey, commit_id, e); - None - } - } - } else { - None - }; - - CommitTask { - commit_id, - allow_undelegation, - committed_account, - base_account, - force_commit_state: false, - } - } -} - #[derive(Clone)] pub struct CommitTask { pub commit_id: u64, @@ -168,10 +120,16 @@ pub struct CommitTask { } impl CommitTask { + // Accounts larger than COMMIT_STATE_SIZE_THRESHOLD, use CommitDiff to + // reduce instruction size. Below this, commit is sent as CommitState. + // Chose 256 as thresold seems good enough as it could hold 8 u32 fields + // or 4 u64 fields. + const COMMIT_STATE_SIZE_THRESHOLD: usize = 256; + pub fn is_commit_diff(&self) -> bool { !self.force_commit_state && self.committed_account.account.data.len() - > CommitTaskBuilder::COMMIT_STATE_SIZE_THRESHOLD + > Self::COMMIT_STATE_SIZE_THRESHOLD && self.base_account.is_some() } @@ -428,6 +386,7 @@ mod serialization_safety_test { tasks::{ args_task::{ArgsTask, ArgsTaskType}, buffer_task::{BufferTask, BufferTaskType}, + task_builder::TaskBuilderImpl, *, }, }; @@ -439,7 +398,7 @@ mod serialization_safety_test { // Test Commit variant let commit_task: ArgsTask = ArgsTaskType::Commit( - CommitTaskBuilder::create_commit_task( + TaskBuilderImpl::create_commit_task( 123, true, CommittedAccount { @@ -503,7 +462,7 @@ mod serialization_safety_test { let buffer_task = BufferTask::new_preparation_required(BufferTaskType::Commit( - CommitTaskBuilder::create_commit_task( + TaskBuilderImpl::create_commit_task( 456, false, CommittedAccount { @@ -531,7 +490,7 @@ mod serialization_safety_test { // Test BufferTask preparation let buffer_task = BufferTask::new_preparation_required(BufferTaskType::Commit( - CommitTaskBuilder::create_commit_task( + TaskBuilderImpl::create_commit_task( 789, true, CommittedAccount { diff --git a/magicblock-committor-service/src/tasks/task_builder.rs b/magicblock-committor-service/src/tasks/task_builder.rs index f3631bf32..689697d97 100644 --- a/magicblock-committor-service/src/tasks/task_builder.rs +++ b/magicblock-committor-service/src/tasks/task_builder.rs @@ -17,11 +17,12 @@ use crate::{ persist::IntentPersister, tasks::{ args_task::{ArgsTask, ArgsTaskType}, - BaseActionTask, BaseTask, CommitTaskBuilder, FinalizeTask, - UndelegateTask, + BaseActionTask, BaseTask, FinalizeTask, UndelegateTask, }, }; +use super::CommitTask; + #[async_trait] pub trait TasksBuilder { // Creates tasks for commit stage @@ -42,6 +43,46 @@ pub trait TasksBuilder { /// V1: Actions are part of finalize tx pub struct TaskBuilderImpl; +impl TaskBuilderImpl { + pub async fn create_commit_task( + commit_id: u64, + allow_undelegation: bool, + committed_account: CommittedAccount, + task_info_fetcher: &Arc, + ) -> CommitTask { + let base_account = if committed_account.account.data.len() + > CommitTask::COMMIT_STATE_SIZE_THRESHOLD + { + match task_info_fetcher + .get_base_account(&committed_account.pubkey) + .await + { + Ok(Some(account)) => Some(account), + Ok(None) => { + log::warn!("AccountNotFound for commit_diff, pubkey: {}, commit_id: {}, Falling back to commit_state.", + committed_account.pubkey, commit_id); + None + } + Err(e) => { + log::warn!("Failed to fetch base account for commit diff, pubkey: {}, commit_id: {}, error: {}. Falling back to commit_state.", + committed_account.pubkey, commit_id, e); + None + } + } + } else { + None + }; + + CommitTask { + commit_id, + allow_undelegation, + committed_account, + base_account, + force_commit_state: false, + } + } +} + #[async_trait] impl TasksBuilder for TaskBuilderImpl { /// Returns [`Task`]s for Commit stage @@ -92,7 +133,7 @@ impl TasksBuilder for TaskBuilderImpl { .iter() .map(|account| async { let commit_id = *commit_ids.get(&account.pubkey).expect("CommitIdFetcher provide commit ids for all listed pubkeys, or errors!"); - let task = ArgsTaskType::Commit(CommitTaskBuilder::create_commit_task( + let task = ArgsTaskType::Commit(Self::create_commit_task( commit_id, allow_undelegation, account.clone(), diff --git a/magicblock-committor-service/src/tasks/task_strategist.rs b/magicblock-committor-service/src/tasks/task_strategist.rs index 4814118ef..9ee7acb85 100644 --- a/magicblock-committor-service/src/tasks/task_strategist.rs +++ b/magicblock-committor-service/src/tasks/task_strategist.rs @@ -424,7 +424,7 @@ mod tests { data_size: usize, ) -> ArgsTask { ArgsTask::new(ArgsTaskType::Commit( - CommitTaskBuilder::create_commit_task( + TaskBuilderImpl::create_commit_task( commit_id, false, CommittedAccount { diff --git a/test-integration/test-committor-service/tests/common.rs b/test-integration/test-committor-service/tests/common.rs index ecef5d3e2..61380daf4 100644 --- a/test-integration/test-committor-service/tests/common.rs +++ b/test-integration/test-committor-service/tests/common.rs @@ -14,7 +14,7 @@ use magicblock_committor_service::{ }, IntentExecutorImpl, NullTaskInfoFetcher, }, - tasks::{CommitTask, CommitTaskBuilder}, + tasks::{CommitTask, TaskBuilderImpl}, transaction_preparator::{ delivery_preparator::DeliveryPreparator, TransactionPreparatorImpl, }, @@ -161,7 +161,7 @@ pub fn generate_random_bytes(length: usize) -> Vec { #[allow(dead_code)] pub async fn create_commit_task(data: &[u8]) -> CommitTask { static COMMIT_ID: AtomicU64 = AtomicU64::new(0); - CommitTaskBuilder::create_commit_task( + TaskBuilderImpl::create_commit_task( COMMIT_ID.fetch_add(1, Ordering::Relaxed), false, CommittedAccount { diff --git a/test-integration/test-committor-service/tests/test_transaction_preparator.rs b/test-integration/test-committor-service/tests/test_transaction_preparator.rs index 123f13c38..46058e01b 100644 --- a/test-integration/test-committor-service/tests/test_transaction_preparator.rs +++ b/test-integration/test-committor-service/tests/test_transaction_preparator.rs @@ -7,8 +7,8 @@ use magicblock_committor_service::{ buffer_task::{BufferTask, BufferTaskType}, task_strategist::{TaskStrategist, TransactionStrategy}, utils::TransactionUtils, - BaseActionTask, BaseTask, CommitTaskBuilder, FinalizeTask, - PreparationState, UndelegateTask, + BaseActionTask, BaseTask, FinalizeTask, PreparationState, + TaskBuilderImpl, UndelegateTask, }, transaction_preparator::TransactionPreparator, }; @@ -36,7 +36,7 @@ async fn test_prepare_commit_tx_with_single_account() { let tasks = vec![ Box::new(ArgsTask::new(ArgsTaskType::Commit( - CommitTaskBuilder::create_commit_task( + TaskBuilderImpl::create_commit_task( 1, true, committed_account.clone(), @@ -95,7 +95,7 @@ async fn test_prepare_commit_tx_with_multiple_accounts() { let buffer_commit_task = BufferTask::new_preparation_required(BufferTaskType::Commit( - CommitTaskBuilder::create_commit_task( + TaskBuilderImpl::create_commit_task( 1, true, committed_account2.clone(), @@ -107,7 +107,7 @@ async fn test_prepare_commit_tx_with_multiple_accounts() { let tasks = vec![ // account 1 Box::new(ArgsTask::new(ArgsTaskType::Commit( - CommitTaskBuilder::create_commit_task( + TaskBuilderImpl::create_commit_task( 1, true, committed_account1.clone(), @@ -199,7 +199,7 @@ async fn test_prepare_commit_tx_with_base_actions() { let buffer_commit_task = BufferTask::new_preparation_required(BufferTaskType::Commit( - CommitTaskBuilder::create_commit_task( + TaskBuilderImpl::create_commit_task( 1, true, committed_account.clone(), From b26fedbfcd5611403733bba2131f61321ebb5d10 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Sat, 20 Dec 2025 09:06:11 +0530 Subject: [PATCH 07/13] Use existing style of tasks --- .../intent_execution_engine.rs | 2 - .../src/tasks/args_task.rs | 69 ++++++++-- magicblock-committor-service/src/tasks/mod.rs | 122 ++++-------------- .../src/tasks/task_builder.rs | 67 +++++----- .../src/tasks/task_strategist.rs | 1 - 5 files changed, 113 insertions(+), 148 deletions(-) diff --git a/magicblock-committor-service/src/intent_execution_manager/intent_execution_engine.rs b/magicblock-committor-service/src/intent_execution_manager/intent_execution_engine.rs index 2507b0b3b..2303ae86b 100644 --- a/magicblock-committor-service/src/intent_execution_manager/intent_execution_engine.rs +++ b/magicblock-committor-service/src/intent_execution_manager/intent_execution_engine.rs @@ -371,8 +371,6 @@ mod tests { use async_trait::async_trait; use magicblock_program::magic_scheduled_base_intent::ScheduledBaseIntent; - use magicblock_rpc_client::MagicBlockRpcClientResult; - use solana_account::Account; use solana_pubkey::{pubkey, Pubkey}; use solana_signature::Signature; use solana_signer::SignerError; diff --git a/magicblock-committor-service/src/tasks/args_task.rs b/magicblock-committor-service/src/tasks/args_task.rs index 70c45c764..a7e6ca81f 100644 --- a/magicblock-committor-service/src/tasks/args_task.rs +++ b/magicblock-committor-service/src/tasks/args_task.rs @@ -1,5 +1,9 @@ -use dlp::args::CallHandlerArgs; +use dlp::{ + args::{CallHandlerArgs, CommitDiffArgs, CommitStateArgs}, + compute_diff, +}; use magicblock_metrics::metrics::LabelValue; +use solana_account::ReadableAccount; use solana_instruction::{AccountMeta, Instruction}; use solana_pubkey::Pubkey; @@ -8,14 +12,15 @@ use crate::tasks::TaskStrategy; use crate::tasks::{ buffer_task::{BufferTask, BufferTaskType}, visitor::Visitor, - BaseActionTask, BaseTask, BaseTaskError, BaseTaskResult, CommitTask, - FinalizeTask, PreparationState, TaskType, UndelegateTask, + BaseActionTask, BaseTask, BaseTaskError, BaseTaskResult, CommitDiffTask, + CommitTask, FinalizeTask, PreparationState, TaskType, UndelegateTask, }; /// Task that will be executed on Base layer via arguments #[derive(Clone)] pub enum ArgsTaskType { Commit(CommitTask), + CommitDiff(CommitDiffTask), Finalize(FinalizeTask), Undelegate(UndelegateTask), // Special action really BaseAction(BaseActionTask), @@ -45,7 +50,39 @@ impl ArgsTask { impl BaseTask for ArgsTask { fn instruction(&self, validator: &Pubkey) -> Instruction { match &self.task_type { - ArgsTaskType::Commit(value) => value.create_commit_ix(validator), + ArgsTaskType::Commit(value) => { + let args = CommitStateArgs { + nonce: value.commit_id, + lamports: value.committed_account.account.lamports, + data: value.committed_account.account.data.clone(), + allow_undelegation: value.allow_undelegation, + }; + dlp::instruction_builder::commit_state( + *validator, + value.committed_account.pubkey, + value.committed_account.account.owner, + args, + ) + } + ArgsTaskType::CommitDiff(value) => { + let args = CommitDiffArgs { + nonce: value.commit_id, + lamports: value.committed_account.account.lamports, + diff: compute_diff( + value.base_account.data(), + value.committed_account.account.data(), + ) + .to_vec(), + allow_undelegation: value.allow_undelegation, + }; + + dlp::instruction_builder::commit_diff( + *validator, + value.committed_account.pubkey, + value.committed_account.account.owner, + args, + ) + } ArgsTaskType::Finalize(value) => { dlp::instruction_builder::finalize( *validator, @@ -89,21 +126,22 @@ impl BaseTask for ArgsTask { self: Box, ) -> Result, Box> { match self.task_type { - ArgsTaskType::Commit(mut value) if value.is_commit_diff() => { - // TODO (snawaz): Currently, we do not support executing CommitDiff - // as BufferTask, which is why we're forcing CommitTask to use CommitState - // before converting this task into BufferTask. Once CommitDiff is supported - // by BufferTask, we do not have to force_commit_state and we can remove - // force_commit_state stuff, as it's essentially a downgrade. - - value.force_commit_state(); + ArgsTaskType::Commit(value) => { Ok(Box::new(BufferTask::new_preparation_required( BufferTaskType::Commit(value), ))) } - ArgsTaskType::Commit(value) => { + ArgsTaskType::CommitDiff(value) => { + // TODO (snawaz): Currently, we do not support executing CommitDiff + // as BufferTask, which is why we're forcing CommitDiffTask to become CommitTask + // before converting this task into BufferTask. Once CommitDiff is supported + // by BufferTask, we do not have to do this, as it's essentially a downgrade. Ok(Box::new(BufferTask::new_preparation_required( - BufferTaskType::Commit(value), + BufferTaskType::Commit(CommitTask { + commit_id: value.commit_id, + allow_undelegation: value.allow_undelegation, + committed_account: value.committed_account, + }), ))) } ArgsTaskType::BaseAction(_) @@ -132,6 +170,7 @@ impl BaseTask for ArgsTask { fn compute_units(&self) -> u32 { match &self.task_type { ArgsTaskType::Commit(_) => 70_000, + ArgsTaskType::CommitDiff(_) => 70_000, ArgsTaskType::BaseAction(task) => task.action.compute_units, ArgsTaskType::Undelegate(_) => 70_000, ArgsTaskType::Finalize(_) => 70_000, @@ -146,6 +185,7 @@ impl BaseTask for ArgsTask { fn task_type(&self) -> TaskType { match &self.task_type { ArgsTaskType::Commit(_) => TaskType::Commit, + ArgsTaskType::CommitDiff(_) => TaskType::Commit, ArgsTaskType::BaseAction(_) => TaskType::Action, ArgsTaskType::Undelegate(_) => TaskType::Undelegate, ArgsTaskType::Finalize(_) => TaskType::Finalize, @@ -170,6 +210,7 @@ impl LabelValue for ArgsTask { fn value(&self) -> &str { match self.task_type { ArgsTaskType::Commit(_) => "args_commit", + ArgsTaskType::CommitDiff(_) => "args_commit_diff", ArgsTaskType::BaseAction(_) => "args_action", ArgsTaskType::Finalize(_) => "args_finalize", ArgsTaskType::Undelegate(_) => "args_undelegate", diff --git a/magicblock-committor-service/src/tasks/mod.rs b/magicblock-committor-service/src/tasks/mod.rs index 4506df094..037c6dbf4 100644 --- a/magicblock-committor-service/src/tasks/mod.rs +++ b/magicblock-committor-service/src/tasks/mod.rs @@ -1,9 +1,3 @@ -use std::sync::Arc; - -use dlp::{ - args::{CommitDiffArgs, CommitStateArgs}, - compute_diff, -}; use dyn_clone::DynClone; use magicblock_committor_program::{ instruction_builder::{ @@ -20,15 +14,12 @@ use magicblock_metrics::metrics::LabelValue; use magicblock_program::magic_scheduled_base_intent::{ BaseAction, CommittedAccount, }; -use solana_account::{Account, ReadableAccount}; +use solana_account::Account; use solana_instruction::Instruction; use solana_pubkey::Pubkey; use thiserror::Error; -use crate::{ - intent_executor::task_info_fetcher::TaskInfoFetcher, - tasks::visitor::Visitor, -}; +use crate::tasks::visitor::Visitor; pub mod args_task; pub mod buffer_task; @@ -64,8 +55,6 @@ pub enum TaskStrategy { pub trait BaseTask: Send + Sync + DynClone + LabelValue { /// Gets all pubkeys that involved in Task's instruction fn involved_accounts(&self, validator: &Pubkey) -> Vec { - // TODO (snawaz): rewrite it. - // currently it is slow as it discards heavy computations and memory allocations. self.instruction(validator) .accounts .iter() @@ -115,78 +104,14 @@ pub struct CommitTask { pub commit_id: u64, pub allow_undelegation: bool, pub committed_account: CommittedAccount, - base_account: Option, - force_commit_state: bool, } -impl CommitTask { - // Accounts larger than COMMIT_STATE_SIZE_THRESHOLD, use CommitDiff to - // reduce instruction size. Below this, commit is sent as CommitState. - // Chose 256 as thresold seems good enough as it could hold 8 u32 fields - // or 4 u64 fields. - const COMMIT_STATE_SIZE_THRESHOLD: usize = 256; - - pub fn is_commit_diff(&self) -> bool { - !self.force_commit_state - && self.committed_account.account.data.len() - > Self::COMMIT_STATE_SIZE_THRESHOLD - && self.base_account.is_some() - } - - pub fn force_commit_state(&mut self) { - self.force_commit_state = true; - } - - pub fn create_commit_ix(&self, validator: &Pubkey) -> Instruction { - if let Some(fetched_account) = self.base_account.as_ref() { - self.create_commit_diff_ix(validator, fetched_account) - } else { - self.create_commit_state_ix(validator) - } - } - - fn create_commit_state_ix(&self, validator: &Pubkey) -> Instruction { - let args = CommitStateArgs { - nonce: self.commit_id, - lamports: self.committed_account.account.lamports, - data: self.committed_account.account.data.clone(), - allow_undelegation: self.allow_undelegation, - }; - dlp::instruction_builder::commit_state( - *validator, - self.committed_account.pubkey, - self.committed_account.account.owner, - args, - ) - } - - fn create_commit_diff_ix( - &self, - validator: &Pubkey, - fetched_account: &Account, - ) -> Instruction { - if self.force_commit_state { - return self.create_commit_state_ix(validator); - } - - let args = CommitDiffArgs { - nonce: self.commit_id, - lamports: self.committed_account.account.lamports, - diff: compute_diff( - fetched_account.data(), - self.committed_account.account.data(), - ) - .to_vec(), - allow_undelegation: self.allow_undelegation, - }; - - dlp::instruction_builder::commit_diff( - *validator, - self.committed_account.pubkey, - self.committed_account.account.owner, - args, - ) - } +#[derive(Clone)] +pub struct CommitDiffTask { + pub commit_id: u64, + pub allow_undelegation: bool, + pub committed_account: CommittedAccount, + pub base_account: Account, } #[derive(Clone)] @@ -397,25 +322,22 @@ mod serialization_safety_test { let validator = Pubkey::new_unique(); // Test Commit variant - let commit_task: ArgsTask = ArgsTaskType::Commit( - TaskBuilderImpl::create_commit_task( - 123, - true, - CommittedAccount { - pubkey: Pubkey::new_unique(), - account: Account { - lamports: 1000, - data: vec![1, 2, 3], - owner: Pubkey::new_unique(), - executable: false, - rent_epoch: 0, - }, + let commit_task: ArgsTask = TaskBuilderImpl::create_commit_task( + 123, + true, + CommittedAccount { + pubkey: Pubkey::new_unique(), + account: Account { + lamports: 1000, + data: vec![1, 2, 3], + owner: Pubkey::new_unique(), + executable: false, + rent_epoch: 0, }, - &Arc::new(NullTaskInfoFetcher), - ) - .await, + }, + &Arc::new(NullTaskInfoFetcher), ) - .into(); + .await; assert_serializable(&commit_task.instruction(&validator)); // Test Finalize variant diff --git a/magicblock-committor-service/src/tasks/task_builder.rs b/magicblock-committor-service/src/tasks/task_builder.rs index 689697d97..eae55b7a8 100644 --- a/magicblock-committor-service/src/tasks/task_builder.rs +++ b/magicblock-committor-service/src/tasks/task_builder.rs @@ -21,20 +21,20 @@ use crate::{ }, }; -use super::CommitTask; +use super::{CommitDiffTask, CommitTask}; #[async_trait] pub trait TasksBuilder { // Creates tasks for commit stage async fn commit_tasks( - task_info_fetcher: &Arc, + commit_id_fetcher: &Arc, base_intent: &ScheduledBaseIntent, persister: &Option

, ) -> TaskBuilderResult>>; // Create tasks for finalize stage async fn finalize_tasks( - task_info_fetcher: &Arc, + info_fetcher: &Arc, base_intent: &ScheduledBaseIntent, ) -> TaskBuilderResult>>; } @@ -43,29 +43,32 @@ pub trait TasksBuilder { /// V1: Actions are part of finalize tx pub struct TaskBuilderImpl; +// Accounts larger than COMMIT_STATE_SIZE_THRESHOLD, use CommitDiff to +// reduce instruction size. Below this, commit is sent as CommitState. +// Chose 256 as thresold seems good enough as it could hold 8 u32 fields +// or 4 u64 fields. +const COMMIT_STATE_SIZE_THRESHOLD: usize = 256; + impl TaskBuilderImpl { pub async fn create_commit_task( commit_id: u64, allow_undelegation: bool, - committed_account: CommittedAccount, + account: CommittedAccount, task_info_fetcher: &Arc, - ) -> CommitTask { - let base_account = if committed_account.account.data.len() - > CommitTask::COMMIT_STATE_SIZE_THRESHOLD + ) -> ArgsTask { + let base_account = if account.account.data.len() + > COMMIT_STATE_SIZE_THRESHOLD { - match task_info_fetcher - .get_base_account(&committed_account.pubkey) - .await - { + match task_info_fetcher.get_base_account(&account.pubkey).await { Ok(Some(account)) => Some(account), Ok(None) => { log::warn!("AccountNotFound for commit_diff, pubkey: {}, commit_id: {}, Falling back to commit_state.", - committed_account.pubkey, commit_id); + account.pubkey, commit_id); None } Err(e) => { log::warn!("Failed to fetch base account for commit diff, pubkey: {}, commit_id: {}, error: {}. Falling back to commit_state.", - committed_account.pubkey, commit_id, e); + account.pubkey, commit_id, e); None } } @@ -73,13 +76,21 @@ impl TaskBuilderImpl { None }; - CommitTask { - commit_id, - allow_undelegation, - committed_account, - base_account, - force_commit_state: false, + if let Some(base_account) = base_account { + ArgsTaskType::CommitDiff(CommitDiffTask { + commit_id: commit_id, + allow_undelegation, + committed_account: account, + base_account, + }) + } else { + ArgsTaskType::Commit(CommitTask { + commit_id: commit_id, + allow_undelegation, + committed_account: account, + }) } + .into() } } @@ -87,7 +98,7 @@ impl TaskBuilderImpl { impl TasksBuilder for TaskBuilderImpl { /// Returns [`Task`]s for Commit stage async fn commit_tasks( - task_info_fetcher: &Arc, + commit_id_fetcher: &Arc, base_intent: &ScheduledBaseIntent, persister: &Option

, ) -> TaskBuilderResult>> { @@ -115,7 +126,7 @@ impl TasksBuilder for TaskBuilderImpl { .iter() .map(|account| account.pubkey) .collect::>(); - let commit_ids = task_info_fetcher + let commit_ids = commit_id_fetcher .fetch_next_commit_ids(&committed_pubkeys) .await .map_err(TaskBuilderError::CommitTasksBuildError)?; @@ -133,14 +144,8 @@ impl TasksBuilder for TaskBuilderImpl { .iter() .map(|account| async { let commit_id = *commit_ids.get(&account.pubkey).expect("CommitIdFetcher provide commit ids for all listed pubkeys, or errors!"); - let task = ArgsTaskType::Commit(Self::create_commit_task( - commit_id, - allow_undelegation, - account.clone(), - task_info_fetcher, - ).await); - - Box::new(ArgsTask::new(task)) as Box + let task = Self::create_commit_task(commit_id, allow_undelegation, account.clone(), commit_id_fetcher).await; + Box::new(task) as Box })).await; Ok(tasks) @@ -148,7 +153,7 @@ impl TasksBuilder for TaskBuilderImpl { /// Returns [`Task`]s for Finalize stage async fn finalize_tasks( - task_info_fetcher: &Arc, + info_fetcher: &Arc, base_intent: &ScheduledBaseIntent, ) -> TaskBuilderResult>> { // Helper to create a finalize task @@ -211,7 +216,7 @@ impl TasksBuilder for TaskBuilderImpl { .iter() .map(|account| account.pubkey) .collect::>(); - let rent_reimbursements = task_info_fetcher + let rent_reimbursements = info_fetcher .fetch_rent_reimbursements(&pubkeys) .await .map_err(TaskBuilderError::FinalizedTasksBuildError)?; diff --git a/magicblock-committor-service/src/tasks/task_strategist.rs b/magicblock-committor-service/src/tasks/task_strategist.rs index 9ee7acb85..5503b4df0 100644 --- a/magicblock-committor-service/src/tasks/task_strategist.rs +++ b/magicblock-committor-service/src/tasks/task_strategist.rs @@ -275,7 +275,6 @@ impl TaskStrategist { ) -> Result { // Get initial transaction size let calculate_tx_length = |tasks: &[Box]| { - // TODO (snawaz): we seem to discard lots of heavy computations here match TransactionUtils::assemble_tasks_tx( &Keypair::new(), // placeholder tasks, From 1d86f60f20710bac3c6364c1c6c9fb7cbaabfc5e Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Sat, 20 Dec 2025 19:31:24 +0530 Subject: [PATCH 08/13] fix tests --- .../src/intent_executor/mod.rs | 7 +- .../src/tasks/buffer_task.rs | 13 +++ magicblock-committor-service/src/tasks/mod.rs | 80 ++++++++++--------- .../src/tasks/task_builder.rs | 17 ++-- .../src/tasks/task_strategist.rs | 54 +++++++------ .../programs/schedulecommit/src/lib.rs | 3 +- .../test-committor-service/tests/common.rs | 18 ++--- .../tests/test_delivery_preparator.rs | 16 ++-- .../tests/test_transaction_preparator.rs | 52 ++++++------ 9 files changed, 141 insertions(+), 119 deletions(-) diff --git a/magicblock-committor-service/src/intent_executor/mod.rs b/magicblock-committor-service/src/intent_executor/mod.rs index 0e6dc554a..83ebf166f 100644 --- a/magicblock-committor-service/src/intent_executor/mod.rs +++ b/magicblock-committor-service/src/intent_executor/mod.rs @@ -25,13 +25,10 @@ use magicblock_rpc_client::{ MagicBlockRpcClientError, MagicBlockSendTransactionConfig, MagicBlockSendTransactionOutcome, MagicblockRpcClient, }; - -use solana_keypair::Keypair; -use solana_message::VersionedMessage; - #[cfg(any(test, feature = "dev-context-only-utils"))] pub use null_task_info_fetcher::*; - +use solana_keypair::Keypair; +use solana_message::VersionedMessage; use solana_pubkey::Pubkey; use solana_rpc_client_api::config::RpcTransactionConfig; use solana_signature::Signature; diff --git a/magicblock-committor-service/src/tasks/buffer_task.rs b/magicblock-committor-service/src/tasks/buffer_task.rs index ccb54b715..96920bcd3 100644 --- a/magicblock-committor-service/src/tasks/buffer_task.rs +++ b/magicblock-committor-service/src/tasks/buffer_task.rs @@ -4,6 +4,8 @@ use magicblock_metrics::metrics::LabelValue; use solana_instruction::Instruction; use solana_pubkey::Pubkey; +#[cfg(any(test, feature = "dev-context-only-utils"))] +use super::args_task::ArgsTaskType; #[cfg(test)] use crate::tasks::TaskStrategy; use crate::{ @@ -62,6 +64,17 @@ impl BufferTask { } } +#[cfg(any(test, feature = "dev-context-only-utils"))] +impl From for BufferTaskType { + fn from(value: ArgsTaskType) -> Self { + match value { + ArgsTaskType::Commit(task) => BufferTaskType::Commit(task), + ArgsTaskType::CommitDiff(_) => panic!("BufferTask doesn't support CommitDiff yet. Disable your tests temporarily till the next PR"), + _ => unimplemented!("Only commit task can be BufferTask currently. Fix your tests"), + } + } +} + impl BaseTask for BufferTask { fn instruction(&self, validator: &Pubkey) -> Instruction { let BufferTaskType::Commit(ref value) = self.task_type; diff --git a/magicblock-committor-service/src/tasks/mod.rs b/magicblock-committor-service/src/tasks/mod.rs index 037c6dbf4..70694114c 100644 --- a/magicblock-committor-service/src/tasks/mod.rs +++ b/magicblock-committor-service/src/tasks/mod.rs @@ -29,6 +29,8 @@ pub(crate) mod task_visitors; pub mod utils; pub mod visitor; +pub use task_builder::TaskBuilderImpl; + #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum TaskType { Commit, @@ -301,6 +303,8 @@ pub type BaseTaskResult = Result; #[cfg(test)] mod serialization_safety_test { + use std::sync::Arc; + use magicblock_program::{ args::ShortAccountMeta, magic_scheduled_base_intent::ProgramArgs, }; @@ -310,7 +314,7 @@ mod serialization_safety_test { intent_executor::NullTaskInfoFetcher, tasks::{ args_task::{ArgsTask, ArgsTaskType}, - buffer_task::{BufferTask, BufferTaskType}, + buffer_task::BufferTask, task_builder::TaskBuilderImpl, *, }, @@ -382,25 +386,26 @@ mod serialization_safety_test { async fn test_buffer_task_instruction_serialization() { let validator = Pubkey::new_unique(); - let buffer_task = - BufferTask::new_preparation_required(BufferTaskType::Commit( - TaskBuilderImpl::create_commit_task( - 456, - false, - CommittedAccount { - pubkey: Pubkey::new_unique(), - account: Account { - lamports: 2000, - data: vec![7, 8, 9], - owner: Pubkey::new_unique(), - executable: false, - rent_epoch: 0, - }, + let buffer_task = BufferTask::new_preparation_required( + TaskBuilderImpl::create_commit_task( + 456, + false, + CommittedAccount { + pubkey: Pubkey::new_unique(), + account: Account { + lamports: 2000, + data: vec![7, 8, 9], + owner: Pubkey::new_unique(), + executable: false, + rent_epoch: 0, }, - &Arc::new(NullTaskInfoFetcher), - ) - .await, - )); + }, + &Arc::new(NullTaskInfoFetcher), + ) + .await + .task_type + .into(), + ); assert_serializable(&buffer_task.instruction(&validator)); } @@ -410,25 +415,26 @@ mod serialization_safety_test { let authority = Pubkey::new_unique(); // Test BufferTask preparation - let buffer_task = - BufferTask::new_preparation_required(BufferTaskType::Commit( - TaskBuilderImpl::create_commit_task( - 789, - true, - CommittedAccount { - pubkey: Pubkey::new_unique(), - account: Account { - lamports: 3000, - data: vec![0; 1024], // Larger data to test chunking - owner: Pubkey::new_unique(), - executable: false, - rent_epoch: 0, - }, + let buffer_task = BufferTask::new_preparation_required( + TaskBuilderImpl::create_commit_task( + 789, + true, + CommittedAccount { + pubkey: Pubkey::new_unique(), + account: Account { + lamports: 3000, + data: vec![0; 1024], // Larger data to test chunking + owner: Pubkey::new_unique(), + executable: false, + rent_epoch: 0, }, - &Arc::new(NullTaskInfoFetcher), - ) - .await, - )); + }, + &Arc::new(NullTaskInfoFetcher), + ) + .await + .task_type + .into(), + ); let PreparationState::Required(preparation_task) = buffer_task.preparation_state() diff --git a/magicblock-committor-service/src/tasks/task_builder.rs b/magicblock-committor-service/src/tasks/task_builder.rs index eae55b7a8..0fdb45103 100644 --- a/magicblock-committor-service/src/tasks/task_builder.rs +++ b/magicblock-committor-service/src/tasks/task_builder.rs @@ -10,6 +10,7 @@ use magicblock_program::magic_scheduled_base_intent::{ use solana_pubkey::Pubkey; use solana_signature::Signature; +use super::{CommitDiffTask, CommitTask}; use crate::{ intent_executor::task_info_fetcher::{ TaskInfoFetcher, TaskInfoFetcherError, @@ -21,8 +22,6 @@ use crate::{ }, }; -use super::{CommitDiffTask, CommitTask}; - #[async_trait] pub trait TasksBuilder { // Creates tasks for commit stage @@ -43,10 +42,12 @@ pub trait TasksBuilder { /// V1: Actions are part of finalize tx pub struct TaskBuilderImpl; -// Accounts larger than COMMIT_STATE_SIZE_THRESHOLD, use CommitDiff to -// reduce instruction size. Below this, commit is sent as CommitState. -// Chose 256 as thresold seems good enough as it could hold 8 u32 fields -// or 4 u64 fields. +// Accounts larger than COMMIT_STATE_SIZE_THRESHOLD use CommitDiff to +// reduce instruction size. Below this threshold, the commit is sent +// as CommitState. The value (256) is chosen because it is sufficient +// for small accounts, which typically could hold up to 8 u32 fields or +// 4 u64 fields. These integers are expected to be on the hot path +// and updated continuously. const COMMIT_STATE_SIZE_THRESHOLD: usize = 256; impl TaskBuilderImpl { @@ -78,14 +79,14 @@ impl TaskBuilderImpl { if let Some(base_account) = base_account { ArgsTaskType::CommitDiff(CommitDiffTask { - commit_id: commit_id, + commit_id, allow_undelegation, committed_account: account, base_account, }) } else { ArgsTaskType::Commit(CommitTask { - commit_id: commit_id, + commit_id, allow_undelegation, committed_account: account, }) diff --git a/magicblock-committor-service/src/tasks/task_strategist.rs b/magicblock-committor-service/src/tasks/task_strategist.rs index 5503b4df0..fe2fd0a1b 100644 --- a/magicblock-committor-service/src/tasks/task_strategist.rs +++ b/magicblock-committor-service/src/tasks/task_strategist.rs @@ -373,27 +373,30 @@ mod tests { use magicblock_program::magic_scheduled_base_intent::{ BaseAction, CommittedAccount, ProgramArgs, }; + use magicblock_rpc_client::MagicBlockRpcClientResult; use solana_account::Account; + use solana_program::system_program; use solana_pubkey::Pubkey; use solana_system_program::id as system_program_id; use super::*; use crate::{ intent_execution_manager::intent_scheduler::create_test_intent, - intent_executor::task_info_fetcher::NullTaskInfoFetcher, - intent_executor::task_info_fetcher::{ - ResetType, TaskInfoFetcher, TaskInfoFetcherResult, + intent_executor::{ + task_info_fetcher::{ + ResetType, TaskInfoFetcher, TaskInfoFetcherResult, + }, + NullTaskInfoFetcher, }, persist::IntentPersisterImpl, tasks::{ - intent_executor::NullTaskInfoFetcher, - persist::IntentPersisterImpl, task_builder::{TaskBuilderImpl, TasksBuilder}, - BaseActionTask, CommitTask, TaskStrategy, UndelegateTask, + BaseActionTask, TaskStrategy, UndelegateTask, }, }; struct MockInfoFetcher; + #[async_trait::async_trait] impl TaskInfoFetcher for MockInfoFetcher { async fn fetch_next_commit_ids( @@ -415,6 +418,13 @@ mod tests { } fn reset(&self, _: ResetType) {} + + async fn get_base_account( + &self, + _pubkey: &Pubkey, + ) -> MagicBlockRpcClientResult> { + Ok(None) // Account Not Found + } } // Helper to create a simple commit task @@ -422,24 +432,22 @@ mod tests { commit_id: u64, data_size: usize, ) -> ArgsTask { - ArgsTask::new(ArgsTaskType::Commit( - TaskBuilderImpl::create_commit_task( - commit_id, - false, - CommittedAccount { - pubkey: Pubkey::new_unique(), - account: Account { - lamports: 1000, - data: vec![1; data_size], - owner: system_program::id(), - executable: false, - rent_epoch: 0, - }, + TaskBuilderImpl::create_commit_task( + commit_id, + false, + CommittedAccount { + pubkey: Pubkey::new_unique(), + account: Account { + lamports: 1000, + data: vec![1; data_size], + owner: system_program::id(), + executable: false, + rent_epoch: 0, }, - &Arc::new(NullTaskInfoFetcher), - ) - .await, - )) + }, + &Arc::new(NullTaskInfoFetcher), + ) + .await } // Helper to create a Base action task diff --git a/test-integration/programs/schedulecommit/src/lib.rs b/test-integration/programs/schedulecommit/src/lib.rs index 11fbd7d99..ee45268e1 100644 --- a/test-integration/programs/schedulecommit/src/lib.rs +++ b/test-integration/programs/schedulecommit/src/lib.rs @@ -15,8 +15,7 @@ use solana_program::{ entrypoint::{self, ProgramResult}, instruction::{AccountMeta, Instruction}, msg, - program::invoke, - program::invoke_signed, + program::{invoke, invoke_signed}, program_error::ProgramError, pubkey::Pubkey, rent::Rent, diff --git a/test-integration/test-committor-service/tests/common.rs b/test-integration/test-committor-service/tests/common.rs index 61380daf4..b5ad36775 100644 --- a/test-integration/test-committor-service/tests/common.rs +++ b/test-integration/test-committor-service/tests/common.rs @@ -12,9 +12,9 @@ use magicblock_committor_service::{ task_info_fetcher::{ ResetType, TaskInfoFetcher, TaskInfoFetcherResult, }, - IntentExecutorImpl, NullTaskInfoFetcher, + IntentExecutorImpl, }, - tasks::{CommitTask, TaskBuilderImpl}, + tasks::CommitTask, transaction_preparator::{ delivery_preparator::DeliveryPreparator, TransactionPreparatorImpl, }, @@ -159,12 +159,12 @@ pub fn generate_random_bytes(length: usize) -> Vec { } #[allow(dead_code)] -pub async fn create_commit_task(data: &[u8]) -> CommitTask { +pub fn create_commit_task(data: &[u8]) -> CommitTask { static COMMIT_ID: AtomicU64 = AtomicU64::new(0); - TaskBuilderImpl::create_commit_task( - COMMIT_ID.fetch_add(1, Ordering::Relaxed), - false, - CommittedAccount { + CommitTask { + commit_id: COMMIT_ID.fetch_add(1, Ordering::Relaxed), + allow_undelegation: false, + committed_account: CommittedAccount { pubkey: Pubkey::new_unique(), account: Account { lamports: 1000, @@ -174,9 +174,7 @@ pub async fn create_commit_task(data: &[u8]) -> CommitTask { rent_epoch: 0, }, }, - &Arc::new(NullTaskInfoFetcher), - ) - .await + } } #[allow(dead_code)] diff --git a/test-integration/test-committor-service/tests/test_delivery_preparator.rs b/test-integration/test-committor-service/tests/test_delivery_preparator.rs index 35f9de4b3..dded722ee 100644 --- a/test-integration/test-committor-service/tests/test_delivery_preparator.rs +++ b/test-integration/test-committor-service/tests/test_delivery_preparator.rs @@ -22,7 +22,7 @@ async fn test_prepare_10kb_buffer() { let preparator = fixture.create_delivery_preparator(); let data = generate_random_bytes(10 * 1024); - let buffer_task = BufferTaskType::Commit(create_commit_task(&data).await); + let buffer_task = BufferTaskType::Commit(create_commit_task(&data)); let mut strategy = TransactionStrategy { optimized_tasks: vec![Box::new(BufferTask::new_preparation_required( buffer_task, @@ -88,8 +88,7 @@ async fn test_prepare_multiple_buffers() { generate_random_bytes(500 * 1024), ]; let buffer_tasks = join_all(datas.iter().map(|data| async { - let task = - BufferTaskType::Commit(create_commit_task(data.as_slice()).await); + let task = BufferTaskType::Commit(create_commit_task(data.as_slice())); Box::new(BufferTask::new_preparation_required(task)) as Box })) @@ -165,8 +164,7 @@ async fn test_lookup_tables() { generate_random_bytes(30), ]; let tasks = join_all(datas.iter().map(|data| async { - let task = - ArgsTaskType::Commit(create_commit_task(data.as_slice()).await); + let task = ArgsTaskType::Commit(create_commit_task(data.as_slice())); Box::::new(task.into()) as Box })) .await; @@ -209,7 +207,7 @@ async fn test_already_initialized_error_handled() { let preparator = fixture.create_delivery_preparator(); let data = generate_random_bytes(10 * 1024); - let mut task = create_commit_task(&data).await; + let mut task = create_commit_task(&data); let buffer_task = BufferTaskType::Commit(task.clone()); let mut strategy = TransactionStrategy { optimized_tasks: vec![Box::new(BufferTask::new_preparation_required( @@ -298,9 +296,9 @@ async fn test_prepare_cleanup_and_reprepare_mixed_tasks() { let buf_b_data = generate_random_bytes(64 * 1024 + 3); // Keep these around to modify data later (same commit IDs, different data) - let mut commit_args = create_commit_task(&args_data).await; - let mut commit_a = create_commit_task(&buf_a_data).await; - let mut commit_b = create_commit_task(&buf_b_data).await; + let mut commit_args = create_commit_task(&args_data); + let mut commit_a = create_commit_task(&buf_a_data); + let mut commit_b = create_commit_task(&buf_b_data); let mut strategy = TransactionStrategy { optimized_tasks: vec![ diff --git a/test-integration/test-committor-service/tests/test_transaction_preparator.rs b/test-integration/test-committor-service/tests/test_transaction_preparator.rs index 46058e01b..16e55121a 100644 --- a/test-integration/test-committor-service/tests/test_transaction_preparator.rs +++ b/test-integration/test-committor-service/tests/test_transaction_preparator.rs @@ -4,7 +4,7 @@ use magicblock_committor_service::{ persist::IntentPersisterImpl, tasks::{ args_task::{ArgsTask, ArgsTaskType}, - buffer_task::{BufferTask, BufferTaskType}, + buffer_task::BufferTask, task_strategist::{TaskStrategist, TransactionStrategy}, utils::TransactionUtils, BaseActionTask, BaseTask, FinalizeTask, PreparationState, @@ -35,7 +35,7 @@ async fn test_prepare_commit_tx_with_single_account() { let committed_account = create_committed_account(&account_data); let tasks = vec![ - Box::new(ArgsTask::new(ArgsTaskType::Commit( + Box::new( TaskBuilderImpl::create_commit_task( 1, true, @@ -43,7 +43,7 @@ async fn test_prepare_commit_tx_with_single_account() { &fixture.create_task_info_fetcher(), ) .await, - ))) as Box, + ) as Box, Box::new(ArgsTask::new(ArgsTaskType::Finalize(FinalizeTask { delegated_account: committed_account.pubkey, }))), @@ -93,20 +93,21 @@ async fn test_prepare_commit_tx_with_multiple_accounts() { let account2_data = generate_random_bytes(12); let committed_account2 = create_committed_account(&account2_data); - let buffer_commit_task = - BufferTask::new_preparation_required(BufferTaskType::Commit( - TaskBuilderImpl::create_commit_task( - 1, - true, - committed_account2.clone(), - &fixture.create_task_info_fetcher(), - ) - .await, - )); + let buffer_commit_task = BufferTask::new_preparation_required( + TaskBuilderImpl::create_commit_task( + 1, + true, + committed_account2.clone(), + &fixture.create_task_info_fetcher(), + ) + .await + .task_type + .into(), + ); // Create test data let tasks = vec![ // account 1 - Box::new(ArgsTask::new(ArgsTaskType::Commit( + Box::new( TaskBuilderImpl::create_commit_task( 1, true, @@ -114,7 +115,7 @@ async fn test_prepare_commit_tx_with_multiple_accounts() { &fixture.create_task_info_fetcher(), ) .await, - ))) as Box, + ) as Box, // account 2 Box::new(buffer_commit_task), // finalize account 1 @@ -197,16 +198,17 @@ async fn test_prepare_commit_tx_with_base_actions() { }], }; - let buffer_commit_task = - BufferTask::new_preparation_required(BufferTaskType::Commit( - TaskBuilderImpl::create_commit_task( - 1, - true, - committed_account.clone(), - &fixture.create_task_info_fetcher(), - ) - .await, - )); + let buffer_commit_task = BufferTask::new_preparation_required( + TaskBuilderImpl::create_commit_task( + 1, + true, + committed_account.clone(), + &fixture.create_task_info_fetcher(), + ) + .await + .task_type + .into(), + ); let tasks = vec![ // commit account Box::new(buffer_commit_task.clone()) as Box, From ea5769993515af4d63f4dcc516b79d772f009939 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Sun, 21 Dec 2025 21:00:42 +0530 Subject: [PATCH 09/13] add new tests --- Cargo.lock | 5 +- Cargo.toml | 3 +- .../src/tasks/args_task.rs | 6 ++ magicblock-committor-service/src/tasks/mod.rs | 2 +- .../src/tasks/task_builder.rs | 8 ++ .../src/tasks/task_strategist.rs | 11 ++ test-integration/Cargo.lock | 28 ++--- test-integration/Cargo.toml | 2 +- .../programs/schedulecommit/src/api.rs | 6 ++ test-integration/schedulecommit/elfs/dlp.so | Bin 347672 -> 348536 bytes .../test-committor-service/Cargo.toml | 1 + .../tests/test_ix_commit_local.rs | 99 +++++++++++++++++- .../tests/utils/instructions.rs | 45 +++++++- .../tests/utils/transactions.rs | 70 ++++++++++++- 14 files changed, 258 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f0fc2dd41..09df25098 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2981,14 +2981,13 @@ dependencies = [ [[package]] name = "magicblock-delegation-program" -version = "1.1.2" -source = "git+https://github.com/magicblock-labs/delegation-program.git?rev=e8d03936#e8d039369ac1149e899ea94f31e0f9cc4e600a38" +version = "1.1.3" +source = "git+https://github.com/magicblock-labs/delegation-program.git?rev=3edb41022#3edb41022afb0e813607ffd4e79d1548131eab2b" dependencies = [ "bincode", "borsh 1.5.7", "bytemuck", "num_enum", - "paste", "pinocchio", "pinocchio-log", "pinocchio-pubkey", diff --git a/Cargo.toml b/Cargo.toml index 32189da1b..e0673688f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -95,10 +95,9 @@ magicblock-committor-program = { path = "./magicblock-committor-program", featur magicblock-committor-service = { path = "./magicblock-committor-service" } magicblock-config = { path = "./magicblock-config" } magicblock-core = { path = "./magicblock-core" } -magicblock-delegation-program = { git = "https://github.com/magicblock-labs/delegation-program.git", rev = "e8d03936", features = [ +magicblock-delegation-program = { git = "https://github.com/magicblock-labs/delegation-program.git", rev = "3edb41022" , features = [ "no-entrypoint", ] } -magicblock-geyser-plugin = { path = "./magicblock-geyser-plugin" } magicblock-ledger = { path = "./magicblock-ledger" } magicblock-magic-program-api = { path = "./magicblock-magic-program-api" } magicblock-metrics = { path = "./magicblock-metrics" } diff --git a/magicblock-committor-service/src/tasks/args_task.rs b/magicblock-committor-service/src/tasks/args_task.rs index a7e6ca81f..22a2bffe5 100644 --- a/magicblock-committor-service/src/tasks/args_task.rs +++ b/magicblock-committor-service/src/tasks/args_task.rs @@ -76,6 +76,12 @@ impl BaseTask for ArgsTask { allow_undelegation: value.allow_undelegation, }; + log::info!( + "DIFF SIZE: {} / {}", + args.diff.len(), + value.base_account.data.len() + ); + dlp::instruction_builder::commit_diff( *validator, value.committed_account.pubkey, diff --git a/magicblock-committor-service/src/tasks/mod.rs b/magicblock-committor-service/src/tasks/mod.rs index 70694114c..135affc6d 100644 --- a/magicblock-committor-service/src/tasks/mod.rs +++ b/magicblock-committor-service/src/tasks/mod.rs @@ -108,7 +108,7 @@ pub struct CommitTask { pub committed_account: CommittedAccount, } -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct CommitDiffTask { pub commit_id: u64, pub allow_undelegation: bool, diff --git a/magicblock-committor-service/src/tasks/task_builder.rs b/magicblock-committor-service/src/tasks/task_builder.rs index 0fdb45103..0f7363c94 100644 --- a/magicblock-committor-service/src/tasks/task_builder.rs +++ b/magicblock-committor-service/src/tasks/task_builder.rs @@ -57,6 +57,8 @@ impl TaskBuilderImpl { account: CommittedAccount, task_info_fetcher: &Arc, ) -> ArgsTask { + log::warn!("create_commit_task entered"); + let base_account = if account.account.data.len() > COMMIT_STATE_SIZE_THRESHOLD { @@ -78,6 +80,11 @@ impl TaskBuilderImpl { }; if let Some(base_account) = base_account { + log::warn!( + "create_commit_task: base_account: {:#?}, {:#?}", + base_account, + account.account + ); ArgsTaskType::CommitDiff(CommitDiffTask { commit_id, allow_undelegation, @@ -85,6 +92,7 @@ impl TaskBuilderImpl { base_account, }) } else { + log::warn!("create_commit_task: {:#?}", account.account); ArgsTaskType::Commit(CommitTask { commit_id, allow_undelegation, diff --git a/magicblock-committor-service/src/tasks/task_strategist.rs b/magicblock-committor-service/src/tasks/task_strategist.rs index fe2fd0a1b..a030e9131 100644 --- a/magicblock-committor-service/src/tasks/task_strategist.rs +++ b/magicblock-committor-service/src/tasks/task_strategist.rs @@ -289,6 +289,12 @@ impl TaskStrategist { // Get initial transaction size let mut current_tx_length = calculate_tx_length(tasks)?; + log::info!( + "optimze initial size: {} / limit {}", + current_tx_length, + MAX_ENCODED_TRANSACTION_SIZE + ); + if current_tx_length <= MAX_ENCODED_TRANSACTION_SIZE { return Ok(current_tx_length); } @@ -340,6 +346,11 @@ impl TaskStrategist { let new_ix_size = usize::try_from(new_ix_size).unwrap_or(usize::MAX); current_tx_length = calculate_tx_length(tasks)?; + log::info!( + "optimze updated size: {} / limit {}", + current_tx_length, + MAX_ENCODED_TRANSACTION_SIZE + ); map.push((new_ix_size, index)); } // That means el-t can't be optimized further diff --git a/test-integration/Cargo.lock b/test-integration/Cargo.lock index 64948fb16..c8490af54 100644 --- a/test-integration/Cargo.lock +++ b/test-integration/Cargo.lock @@ -2673,7 +2673,7 @@ dependencies = [ "log", "magicblock-config", "magicblock-core", - "magicblock-delegation-program 1.1.2", + "magicblock-delegation-program 1.1.3", "random-port", "rayon", "serde", @@ -3244,7 +3244,7 @@ dependencies = [ "lru", "magicblock-config", "magicblock-core", - "magicblock-delegation-program 1.1.2", + "magicblock-delegation-program 1.1.3", "magicblock-magic-program-api 0.5.0", "magicblock-metrics", "solana-account", @@ -3303,7 +3303,7 @@ dependencies = [ "log", "lru", "magicblock-committor-program", - "magicblock-delegation-program 1.1.2", + "magicblock-delegation-program 1.1.3", "magicblock-metrics", "magicblock-program", "magicblock-rpc-client", @@ -3391,14 +3391,13 @@ dependencies = [ [[package]] name = "magicblock-delegation-program" -version = "1.1.2" -source = "git+https://github.com/magicblock-labs/delegation-program.git?rev=e8d03936#e8d039369ac1149e899ea94f31e0f9cc4e600a38" +version = "1.1.3" +source = "git+https://github.com/magicblock-labs/delegation-program.git?rev=3edb41022#3edb41022afb0e813607ffd4e79d1548131eab2b" dependencies = [ "bincode", "borsh 1.5.7", "bytemuck", "num_enum", - "paste", "pinocchio", "pinocchio-log", "pinocchio-pubkey", @@ -3630,7 +3629,7 @@ name = "magicblock-validator-admin" version = "0.5.0" dependencies = [ "log", - "magicblock-delegation-program 1.1.2", + "magicblock-delegation-program 1.1.3", "magicblock-program", "magicblock-rpc-client", "solana-commitment-config", @@ -4503,7 +4502,7 @@ version = "0.0.0" dependencies = [ "borsh 1.5.7", "ephemeral-rollups-sdk", - "magicblock-delegation-program 1.1.2", + "magicblock-delegation-program 1.1.3", "magicblock-magic-program-api 0.5.0", "rkyv 0.7.45", "solana-program", @@ -5379,7 +5378,7 @@ dependencies = [ "integration-test-tools", "log", "magicblock-core", - "magicblock-delegation-program 1.1.2", + "magicblock-delegation-program 1.1.3", "program-schedulecommit", "solana-program", "solana-rpc-client", @@ -5397,11 +5396,12 @@ dependencies = [ "log", "magicblock-committor-program", "magicblock-committor-service", - "magicblock-delegation-program 1.1.2", + "magicblock-delegation-program 1.1.3", "magicblock-program", "magicblock-rpc-client", "magicblock-table-mania", "program-flexi-counter", + "program-schedulecommit", "rand 0.8.5", "solana-account", "solana-pubkey", @@ -9567,7 +9567,7 @@ dependencies = [ "log", "magicblock-chainlink", "magicblock-config", - "magicblock-delegation-program 1.1.2", + "magicblock-delegation-program 1.1.3", "program-flexi-counter", "program-mini", "solana-account", @@ -9649,7 +9649,7 @@ dependencies = [ "log", "magicblock-accounts-db", "magicblock-config", - "magicblock-delegation-program 1.1.2", + "magicblock-delegation-program 1.1.3", "program-flexi-counter", "solana-rpc-client", "solana-sdk", @@ -9670,7 +9670,7 @@ dependencies = [ "magic-domain-program", "magicblock-api", "magicblock-config", - "magicblock-delegation-program 1.1.2", + "magicblock-delegation-program 1.1.3", "magicblock-program", "magicblock-validator-admin", "solana-rpc-client", @@ -9710,7 +9710,7 @@ version = "0.0.0" dependencies = [ "integration-test-tools", "log", - "magicblock-delegation-program 1.1.2", + "magicblock-delegation-program 1.1.3", "program-flexi-counter", "solana-rpc-client-api", "solana-sdk", diff --git a/test-integration/Cargo.toml b/test-integration/Cargo.toml index f9820f806..06999a97d 100644 --- a/test-integration/Cargo.toml +++ b/test-integration/Cargo.toml @@ -57,7 +57,7 @@ magicblock-config = { path = "../magicblock-config" } magicblock-core = { path = "../magicblock-core" } magic-domain-program = { git = "https://github.com/magicblock-labs/magic-domain-program.git", rev = "ea04d46", default-features = false } magicblock_magic_program_api = { package = "magicblock-magic-program-api", path = "../magicblock-magic-program-api" } -magicblock-delegation-program = { git = "https://github.com/magicblock-labs/delegation-program.git", rev = "e8d03936", features = [ +magicblock-delegation-program = { git = "https://github.com/magicblock-labs/delegation-program.git", rev = "3edb41022" , features = [ "no-entrypoint", ] } magicblock-program = { path = "../programs/magicblock" } diff --git a/test-integration/programs/schedulecommit/src/api.rs b/test-integration/programs/schedulecommit/src/api.rs index 65931d4c9..4cbbb5f31 100644 --- a/test-integration/programs/schedulecommit/src/api.rs +++ b/test-integration/programs/schedulecommit/src/api.rs @@ -110,6 +110,12 @@ pub fn delegate_account_cpi_instruction( ); let delegate_accounts = DelegateAccounts::new(pda, program_id); + println!("delegate_accounts: {:#?}", delegate_accounts); + println!( + "config: {:#?}", + dlp::pda::program_config_from_program_id(&crate::ID) + ); + let delegate_metas = DelegateAccountMetas::from(delegate_accounts); let account_metas = vec![ AccountMeta::new(payer, true), diff --git a/test-integration/schedulecommit/elfs/dlp.so b/test-integration/schedulecommit/elfs/dlp.so index decfd0f00a8a87543a65cd002d05a380f7c91d5d..1a169270bddb7ba9d0fea7d39f8c1cf0afb5c178 100755 GIT binary patch delta 65733 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

0Pxh|GaK?MOl&sgZm=-H+Eb)DVqP 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

%qThq-kON5Y&2@Tou!5^Bf%$rL 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 wS}wj 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 LIn?Rfz7zi+fi)u9 diff --git a/test-integration/test-committor-service/Cargo.toml b/test-integration/test-committor-service/Cargo.toml index 7d74150f1..c6b448d1e 100644 --- a/test-integration/test-committor-service/Cargo.toml +++ b/test-integration/test-committor-service/Cargo.toml @@ -23,6 +23,7 @@ magicblock-table-mania = { workspace = true, features = [ "randomize_lookup_table_slot", ] } program-flexi-counter = { workspace = true, features = ["no-entrypoint"] } +program-schedulecommit = { workspace = true, features = ["no-entrypoint"] } rand = { workspace = true } solana-account = { workspace = true } solana-pubkey = { workspace = true } diff --git a/test-integration/test-committor-service/tests/test_ix_commit_local.rs b/test-integration/test-committor-service/tests/test_ix_commit_local.rs index a6c7994d3..6af953d47 100644 --- a/test-integration/test-committor-service/tests/test_ix_commit_local.rs +++ b/test-integration/test-committor-service/tests/test_ix_commit_local.rs @@ -31,6 +31,7 @@ use test_kit::init_logger; use tokio::task::JoinSet; use utils::transactions::tx_logs_contain; +use self::utils::transactions::init_and_delegate_order_book_on_chain; use crate::utils::{ ensure_validator_authority, transactions::{ @@ -76,22 +77,47 @@ async fn test_ix_commit_single_account_100_bytes_and_undelegate() { #[tokio::test] async fn test_ix_commit_single_account_800_bytes() { - commit_single_account(800, CommitStrategy::FromBuffer, false).await; + commit_single_account(800, CommitStrategy::Args, false).await; } #[tokio::test] async fn test_ix_commit_single_account_800_bytes_and_undelegate() { - commit_single_account(800, CommitStrategy::FromBuffer, true).await; + commit_single_account(800, CommitStrategy::Args, true).await; } #[tokio::test] async fn test_ix_commit_single_account_one_kb() { - commit_single_account(1024, CommitStrategy::FromBuffer, false).await; + commit_single_account(1024, CommitStrategy::Args, false).await; } #[tokio::test] async fn test_ix_commit_single_account_ten_kb() { - commit_single_account(10 * 1024, CommitStrategy::FromBuffer, false).await; + commit_single_account(10 * 1024, CommitStrategy::Args, false).await; +} + +#[tokio::test] +async fn test_ix_commit_order_book_change_100_bytes() { + commit_book_order_account(100, CommitStrategy::Args, false).await; +} + +#[tokio::test] +async fn test_ix_commit_order_book_change_679_bytes() { + commit_book_order_account(679, CommitStrategy::Args, false).await; +} + +#[tokio::test] +async fn test_ix_commit_order_book_change_680_bytes() { + // We cannot use 680 as changed_len because that both 679 and 680 produce encoded tx + // of size 1644 (which is the max limit), but while the size of raw bytes for 679 is within + // 1232 limit, the size for 680 execeds by 1 (1233). That is why we used + // 681 as changed_len where CommitStrategy goes from Args to FromBuffer. + commit_book_order_account(681, CommitStrategy::FromBuffer, false).await; +} + +#[tokio::test] +async fn test_ix_commit_order_book_change_10k_bytes() { + commit_book_order_account(10 * 1024, CommitStrategy::FromBuffer, false) + .await; } async fn commit_single_account( @@ -159,6 +185,71 @@ async fn commit_single_account( .await; } +async fn commit_book_order_account( + changed_len: usize, + expected_strategy: CommitStrategy, + undelegate: bool, +) { + init_logger!(); + + let validator_auth = ensure_validator_authority(); + fund_validator_auth_and_ensure_validator_fees_vault(&validator_auth).await; + + // Run each test with and without finalizing + let service = CommittorService::try_start( + validator_auth.insecure_clone(), + ":memory:", + ChainConfig::local(ComputeBudgetConfig::new(1_000_000)), + ) + .unwrap(); + let service = CommittorServiceExt::new(Arc::new(service)); + + let payer = Keypair::new(); + let (order_book_pk, mut order_book_ac) = + init_and_delegate_order_book_on_chain(&payer).await; + + // Modify bytes so that a diff is produced and is sent to DLP + let data = &mut order_book_ac.data; + assert!(changed_len <= data.len()); + for byte in &mut order_book_ac.data[..changed_len] { + *byte = byte.wrapping_add(1); + } + order_book_ac.owner = program_schedulecommit::id(); + + // We should always be able to Commit & Finalize 1 account either with Args or Buffers + let account = CommittedAccount { + pubkey: order_book_pk, + account: order_book_ac, + }; + let base_intent = if undelegate { + MagicBaseIntent::CommitAndUndelegate(CommitAndUndelegate { + commit_action: CommitType::Standalone(vec![account]), + undelegate_action: UndelegateType::Standalone, + }) + } else { + MagicBaseIntent::Commit(CommitType::Standalone(vec![account])) + }; + + let intent = ScheduledBaseIntentWrapper { + trigger_type: TriggerType::OnChain, + inner: ScheduledBaseIntent { + id: 0, + slot: 10, + blockhash: Hash::new_unique(), + action_sent_transaction: Transaction::default(), + payer: payer.pubkey(), + base_intent, + }, + }; + + ix_commit_local( + service, + vec![intent], + expect_strategies(&[(expected_strategy, 1)]), + ) + .await; +} + // TODO(thlorenz/snawaz): once delegation program supports larger commits // add 1MB and 10MB tests diff --git a/test-integration/test-committor-service/tests/utils/instructions.rs b/test-integration/test-committor-service/tests/utils/instructions.rs index 9b7e3ffbd..9a8622968 100644 --- a/test-integration/test-committor-service/tests/utils/instructions.rs +++ b/test-integration/test-committor-service/tests/utils/instructions.rs @@ -1,5 +1,6 @@ use solana_pubkey::Pubkey; -use solana_sdk::{instruction::Instruction, rent::Rent}; +use solana_sdk::{instruction::Instruction, rent::Rent, signature::Keypair}; +use test_kit::Signer; pub fn init_validator_fees_vault_ix(validator_auth: Pubkey) -> Instruction { dlp::instruction_builder::init_validator_fees_vault( @@ -49,3 +50,45 @@ pub fn init_account_and_delegate_ixs( rent_excempt: rent_exempt, } } + +pub struct InitOrderBookAndDelegateIxs { + pub init: Instruction, + pub delegate: Instruction, + pub book_manager: Keypair, + pub order_book: Pubkey, +} + +pub fn init_order_book_account_and_delegate_ixs( + payer: Pubkey, +) -> InitOrderBookAndDelegateIxs { + use program_schedulecommit::{api, ID}; + + let book_manager = Keypair::new(); + + println!("schedulecommit ID: {}", ID); + + let (order_book, _bump) = Pubkey::find_program_address( + &[b"order_book", book_manager.pubkey().as_ref()], + &ID, + ); + + let init_ix = api::init_order_book_instruction( + payer, + book_manager.pubkey(), + order_book, + ); + + let delegate_ix = api::delegate_account_cpi_instruction( + payer, + None, + book_manager.pubkey(), + b"order_book", + ); + + InitOrderBookAndDelegateIxs { + init: init_ix, + delegate: delegate_ix, + book_manager, + order_book, + } +} diff --git a/test-integration/test-committor-service/tests/utils/transactions.rs b/test-integration/test-committor-service/tests/utils/transactions.rs index 8d0da41f9..29a80a260 100644 --- a/test-integration/test-committor-service/tests/utils/transactions.rs +++ b/test-integration/test-committor-service/tests/utils/transactions.rs @@ -13,8 +13,9 @@ use solana_sdk::{ }; use crate::utils::instructions::{ - init_account_and_delegate_ixs, init_validator_fees_vault_ix, - InitAccountAndDelegateIxs, + init_account_and_delegate_ixs, init_order_book_account_and_delegate_ixs, + init_validator_fees_vault_ix, InitAccountAndDelegateIxs, + InitOrderBookAndDelegateIxs, }; #[macro_export] @@ -134,6 +135,7 @@ pub async fn tx_logs_contain( } /// This needs to be run for each test that required a new counter to be delegated +#[allow(dead_code)] pub async fn init_and_delegate_account_on_chain( counter_auth: &Keypair, bytes: u64, @@ -233,6 +235,70 @@ pub async fn init_and_delegate_account_on_chain( (pda, pda_acc) } +/// This needs to be run for each test that required a new counter to be delegated +#[allow(dead_code)] +pub async fn init_and_delegate_order_book_on_chain( + payer: &Keypair, +) -> (Pubkey, Account) { + let rpc_client = RpcClient::new("http://localhost:7799".to_string()); + + rpc_client + .request_airdrop(&payer.pubkey(), 777 * LAMPORTS_PER_SOL) + .await + .unwrap(); + debug!("Airdropped to counter auth: {} SOL", 777 * LAMPORTS_PER_SOL); + + let InitOrderBookAndDelegateIxs { + init, + delegate, + book_manager, + order_book, + } = init_order_book_account_and_delegate_ixs(payer.pubkey()); + + let latest_block_hash = rpc_client.get_latest_blockhash().await.unwrap(); + + // Init account + rpc_client + .send_and_confirm_transaction_with_spinner_and_config( + &Transaction::new_signed_with_payer( + &[init], + Some(&payer.pubkey()), + &[payer, &book_manager], + latest_block_hash, + ), + CommitmentConfig::confirmed(), + RpcSendTransactionConfig { + skip_preflight: true, + ..Default::default() + }, + ) + .await + .expect("Failed to init account"); + + // Delegate account + rpc_client + .send_and_confirm_transaction_with_spinner_and_config( + &Transaction::new_signed_with_payer( + &[delegate], + Some(&payer.pubkey()), + &[&payer], + latest_block_hash, + ), + CommitmentConfig::confirmed(), + RpcSendTransactionConfig { + skip_preflight: true, + ..Default::default() + }, + ) + .await + .expect("Failed to delegate"); + // debug!("Delegated account: {:?}", pda); + + let order_book_acc = get_account!(rpc_client, order_book, "order_book"); + + (order_book, order_book_acc) +} + /// This needs to be run once for all tests pub async fn fund_validator_auth_and_ensure_validator_fees_vault( validator_auth: &Keypair, From 74b995df4d93f5435eeea59a857857ea9c12b474 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Mon, 22 Dec 2025 13:38:51 +0530 Subject: [PATCH 10/13] Add one test and cleanup --- .../src/tasks/args_task.rs | 6 - .../src/tasks/buffer_task.rs | 2 +- .../src/tasks/task_builder.rs | 10 +- .../src/tasks/task_strategist.rs | 163 +++++++++++++----- .../tests/utils/transactions.rs | 4 +- 5 files changed, 124 insertions(+), 61 deletions(-) diff --git a/magicblock-committor-service/src/tasks/args_task.rs b/magicblock-committor-service/src/tasks/args_task.rs index 22a2bffe5..a7e6ca81f 100644 --- a/magicblock-committor-service/src/tasks/args_task.rs +++ b/magicblock-committor-service/src/tasks/args_task.rs @@ -76,12 +76,6 @@ impl BaseTask for ArgsTask { allow_undelegation: value.allow_undelegation, }; - log::info!( - "DIFF SIZE: {} / {}", - args.diff.len(), - value.base_account.data.len() - ); - dlp::instruction_builder::commit_diff( *validator, value.committed_account.pubkey, diff --git a/magicblock-committor-service/src/tasks/buffer_task.rs b/magicblock-committor-service/src/tasks/buffer_task.rs index 96920bcd3..7a1fb1890 100644 --- a/magicblock-committor-service/src/tasks/buffer_task.rs +++ b/magicblock-committor-service/src/tasks/buffer_task.rs @@ -69,7 +69,7 @@ impl From for BufferTaskType { fn from(value: ArgsTaskType) -> Self { match value { ArgsTaskType::Commit(task) => BufferTaskType::Commit(task), - ArgsTaskType::CommitDiff(_) => panic!("BufferTask doesn't support CommitDiff yet. Disable your tests temporarily till the next PR"), + ArgsTaskType::CommitDiff(_) => panic!("BufferTask doesn't support CommitDiff yet. Disable your tests (if any) temporarily till the next PR"), _ => unimplemented!("Only commit task can be BufferTask currently. Fix your tests"), } } diff --git a/magicblock-committor-service/src/tasks/task_builder.rs b/magicblock-committor-service/src/tasks/task_builder.rs index 0f7363c94..a46c820c7 100644 --- a/magicblock-committor-service/src/tasks/task_builder.rs +++ b/magicblock-committor-service/src/tasks/task_builder.rs @@ -48,7 +48,7 @@ pub struct TaskBuilderImpl; // for small accounts, which typically could hold up to 8 u32 fields or // 4 u64 fields. These integers are expected to be on the hot path // and updated continuously. -const COMMIT_STATE_SIZE_THRESHOLD: usize = 256; +pub const COMMIT_STATE_SIZE_THRESHOLD: usize = 256; impl TaskBuilderImpl { pub async fn create_commit_task( @@ -57,8 +57,6 @@ impl TaskBuilderImpl { account: CommittedAccount, task_info_fetcher: &Arc, ) -> ArgsTask { - log::warn!("create_commit_task entered"); - let base_account = if account.account.data.len() > COMMIT_STATE_SIZE_THRESHOLD { @@ -80,11 +78,6 @@ impl TaskBuilderImpl { }; if let Some(base_account) = base_account { - log::warn!( - "create_commit_task: base_account: {:#?}, {:#?}", - base_account, - account.account - ); ArgsTaskType::CommitDiff(CommitDiffTask { commit_id, allow_undelegation, @@ -92,7 +85,6 @@ impl TaskBuilderImpl { base_account, }) } else { - log::warn!("create_commit_task: {:#?}", account.account); ArgsTaskType::Commit(CommitTask { commit_id, allow_undelegation, diff --git a/magicblock-committor-service/src/tasks/task_strategist.rs b/magicblock-committor-service/src/tasks/task_strategist.rs index a030e9131..bc55ab2a9 100644 --- a/magicblock-committor-service/src/tasks/task_strategist.rs +++ b/magicblock-committor-service/src/tasks/task_strategist.rs @@ -289,11 +289,6 @@ impl TaskStrategist { // Get initial transaction size let mut current_tx_length = calculate_tx_length(tasks)?; - log::info!( - "optimze initial size: {} / limit {}", - current_tx_length, - MAX_ENCODED_TRANSACTION_SIZE - ); if current_tx_length <= MAX_ENCODED_TRANSACTION_SIZE { return Ok(current_tx_length); @@ -346,11 +341,6 @@ impl TaskStrategist { let new_ix_size = usize::try_from(new_ix_size).unwrap_or(usize::MAX); current_tx_length = calculate_tx_length(tasks)?; - log::info!( - "optimze updated size: {} / limit {}", - current_tx_length, - MAX_ENCODED_TRANSACTION_SIZE - ); map.push((new_ix_size, index)); } // That means el-t can't be optimized further @@ -401,12 +391,15 @@ mod tests { }, persist::IntentPersisterImpl, tasks::{ - task_builder::{TaskBuilderImpl, TasksBuilder}, + task_builder::{ + TaskBuilderImpl, TasksBuilder, COMMIT_STATE_SIZE_THRESHOLD, + }, BaseActionTask, TaskStrategy, UndelegateTask, }, }; - struct MockInfoFetcher; + #[derive(Default)] + struct MockInfoFetcher(HashMap); #[async_trait::async_trait] impl TaskInfoFetcher for MockInfoFetcher { @@ -432,9 +425,9 @@ mod tests { async fn get_base_account( &self, - _pubkey: &Pubkey, + pubkey: &Pubkey, ) -> MagicBlockRpcClientResult> { - Ok(None) // Account Not Found + Ok(self.0.get(pubkey).cloned()) } } @@ -442,23 +435,44 @@ mod tests { async fn create_test_commit_task( commit_id: u64, data_size: usize, + diff_len: usize, ) -> ArgsTask { - TaskBuilderImpl::create_commit_task( - commit_id, - false, - CommittedAccount { - pubkey: Pubkey::new_unique(), - account: Account { - lamports: 1000, - data: vec![1; data_size], - owner: system_program::id(), - executable: false, - rent_epoch: 0, - }, + let committed_account = CommittedAccount { + pubkey: Pubkey::new_unique(), + account: Account { + lamports: 1000, + data: vec![1; data_size], + owner: system_program::id(), + executable: false, + rent_epoch: 0, }, - &Arc::new(NullTaskInfoFetcher), - ) - .await + }; + + if diff_len == 0 { + TaskBuilderImpl::create_commit_task( + commit_id, + false, + committed_account, + &Arc::new(NullTaskInfoFetcher), + ) + .await + } else { + let originals: HashMap = { + let mut acc = committed_account.account.clone(); + assert!(diff_len <= acc.data.len()); + for byte in &mut acc.data[..diff_len] { + *byte = byte.wrapping_add(1); + } + [(committed_account.pubkey, acc)].iter().cloned().collect() + }; + TaskBuilderImpl::create_commit_task( + commit_id, + false, + committed_account, + &Arc::new(MockInfoFetcher(originals)), + ) + .await + } } // Helper to create a Base action task @@ -496,7 +510,7 @@ mod tests { #[tokio::test] async fn test_build_strategy_with_single_small_task() { let validator = Pubkey::new_unique(); - let task = create_test_commit_task(1, 100).await; + let task = create_test_commit_task(1, 100, 0).await; let tasks = vec![Box::new(task) as Box]; let strategy = TaskStrategist::build_strategy( @@ -514,7 +528,7 @@ mod tests { async fn test_build_strategy_optimizes_to_buffer_when_needed() { let validator = Pubkey::new_unique(); - let task = create_test_commit_task(1, 1000).await; // Large task + let task = create_test_commit_task(1, 1000, 0).await; // Large task let tasks = vec![Box::new(task) as Box]; let strategy = TaskStrategist::build_strategy( @@ -535,7 +549,7 @@ mod tests { async fn test_build_strategy_optimizes_to_buffer_u16_exceeded() { let validator = Pubkey::new_unique(); - let task = create_test_commit_task(1, 66_000).await; // Large task + let task = create_test_commit_task(1, 66_000, 0).await; // Large task let tasks = vec![Box::new(task) as Box]; let strategy = TaskStrategist::build_strategy( @@ -552,6 +566,71 @@ mod tests { )); } + #[tokio::test] + async fn test_build_strategy_does_not_optimize_large_account_but_small_diff( + ) { + let validator = Pubkey::new_unique(); + + let task = + create_test_commit_task(1, 66_000, COMMIT_STATE_SIZE_THRESHOLD) + .await; // large account but small diff + let tasks = vec![Box::new(task) as Box]; + + let strategy = TaskStrategist::build_strategy( + tasks, + &validator, + &None::, + ) + .expect("Should build strategy with buffer optimization"); + + assert_eq!(strategy.optimized_tasks.len(), 1); + assert_eq!(strategy.optimized_tasks[0].strategy(), TaskStrategy::Args); + } + + #[tokio::test] + async fn test_build_strategy_does_not_optimize_large_account_and_above_threshold_diff( + ) { + let validator = Pubkey::new_unique(); + + let task = + create_test_commit_task(1, 66_000, COMMIT_STATE_SIZE_THRESHOLD + 1) + .await; // large account but small diff + let tasks = vec![Box::new(task) as Box]; + + let strategy = TaskStrategist::build_strategy( + tasks, + &validator, + &None::, + ) + .expect("Should build strategy with buffer optimization"); + + assert_eq!(strategy.optimized_tasks.len(), 1); + assert_eq!(strategy.optimized_tasks[0].strategy(), TaskStrategy::Args); + } + + #[tokio::test] + async fn test_build_strategy_does_optimize_large_account_and_large_diff() { + let validator = Pubkey::new_unique(); + + let task = + create_test_commit_task(1, 66_000, COMMIT_STATE_SIZE_THRESHOLD * 4) + .await; // large account but small diff + let tasks = vec![Box::new(task) as Box]; + + let strategy = TaskStrategist::build_strategy( + tasks, + &validator, + &None::, + ) + .expect("Should build strategy with buffer optimization"); + + assert_eq!(strategy.optimized_tasks.len(), 1); + assert_eq!( + strategy.optimized_tasks[0].strategy(), + TaskStrategy::Buffer + ); + } + #[tokio::test] async fn test_build_strategy_creates_multiple_buffers() { // TODO: ALSO MAX NUM WITH PURE BUFFER commits, no alts @@ -560,7 +639,7 @@ mod tests { let validator = Pubkey::new_unique(); let tasks = join_all((0..NUM_COMMITS).map(|i| async move { - let task = create_test_commit_task(i, 500).await; // Large task + let task = create_test_commit_task(i, 500, 0).await; // Large task Box::new(task) as Box })) .await; @@ -587,7 +666,7 @@ mod tests { let tasks = join_all((0..NUM_COMMITS).map(|i| async move { // Large task - let task = create_test_commit_task(i, 10000).await; + let task = create_test_commit_task(i, 10000, 0).await; Box::new(task) as Box })) .await; @@ -613,7 +692,7 @@ mod tests { let tasks = join_all((0..NUM_COMMITS).map(|i| async move { // Large task - let task = create_test_commit_task(i, 1000).await; + let task = create_test_commit_task(i, 1000, 0).await; Box::new(task) as Box })) .await; @@ -629,11 +708,11 @@ mod tests { #[tokio::test] async fn test_optimize_strategy_prioritizes_largest_tasks() { let mut tasks = [ - Box::new(create_test_commit_task(1, 100).await) + Box::new(create_test_commit_task(1, 100, 0).await) as Box, - Box::new(create_test_commit_task(2, 1000).await) + Box::new(create_test_commit_task(2, 1000, 0).await) as Box, // Larger task - Box::new(create_test_commit_task(3, 1000).await) + Box::new(create_test_commit_task(3, 1000, 0).await) as Box, // Larger task ]; @@ -647,7 +726,7 @@ mod tests { async fn test_mixed_task_types_with_optimization() { let validator = Pubkey::new_unique(); let tasks = vec![ - Box::new(create_test_commit_task(1, 1000).await) + Box::new(create_test_commit_task(1, 1000, 0).await) as Box, Box::new(create_test_finalize_task()) as Box, Box::new(create_test_base_action_task(500)) as Box, @@ -689,7 +768,7 @@ mod tests { let pubkey = [Pubkey::new_unique()]; let intent = create_test_intent(0, &pubkey, false); - let info_fetcher = Arc::new(MockInfoFetcher); + let info_fetcher = Arc::new(MockInfoFetcher::default()); let commit_task = TaskBuilderImpl::commit_tasks( &info_fetcher, &intent, @@ -721,7 +800,7 @@ mod tests { let pubkeys: [_; 3] = std::array::from_fn(|_| Pubkey::new_unique()); let intent = create_test_intent(0, &pubkeys, true); - let info_fetcher = Arc::new(MockInfoFetcher); + let info_fetcher = Arc::new(MockInfoFetcher::default()); let commit_task = TaskBuilderImpl::commit_tasks( &info_fetcher, &intent, @@ -758,7 +837,7 @@ mod tests { let pubkeys: [_; 8] = std::array::from_fn(|_| Pubkey::new_unique()); let intent = create_test_intent(0, &pubkeys, false); - let info_fetcher = Arc::new(MockInfoFetcher); + let info_fetcher = Arc::new(MockInfoFetcher::default()); let commit_task = TaskBuilderImpl::commit_tasks( &info_fetcher, &intent, diff --git a/test-integration/test-committor-service/tests/utils/transactions.rs b/test-integration/test-committor-service/tests/utils/transactions.rs index 29a80a260..166850104 100644 --- a/test-integration/test-committor-service/tests/utils/transactions.rs +++ b/test-integration/test-committor-service/tests/utils/transactions.rs @@ -235,7 +235,7 @@ pub async fn init_and_delegate_account_on_chain( (pda, pda_acc) } -/// This needs to be run for each test that required a new counter to be delegated +/// This needs to be run for each test that required a new order_book to be delegated #[allow(dead_code)] pub async fn init_and_delegate_order_book_on_chain( payer: &Keypair, @@ -246,7 +246,6 @@ pub async fn init_and_delegate_order_book_on_chain( .request_airdrop(&payer.pubkey(), 777 * LAMPORTS_PER_SOL) .await .unwrap(); - debug!("Airdropped to counter auth: {} SOL", 777 * LAMPORTS_PER_SOL); let InitOrderBookAndDelegateIxs { init, @@ -292,7 +291,6 @@ pub async fn init_and_delegate_order_book_on_chain( ) .await .expect("Failed to delegate"); - // debug!("Delegated account: {:?}", pda); let order_book_acc = get_account!(rpc_client, order_book, "order_book"); From 08fc8ed66ebd6741c422f9f2af67c2801ef1d4d9 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Mon, 22 Dec 2025 20:08:47 +0530 Subject: [PATCH 11/13] Address taco's feedback --- .../src/intent_executor/mod.rs | 4 +- magicblock-committor-service/src/tasks/mod.rs | 66 ++++++++----------- .../programs/schedulecommit/src/api.rs | 53 +++++++++------ .../programs/schedulecommit/src/lib.rs | 4 +- .../programs/schedulecommit/src/utils/mod.rs | 9 ++- .../client/src/schedule_commit_context.rs | 25 ++++--- .../test-scenarios/tests/01_commits.rs | 16 +++-- .../tests/02_commit_and_undelegate.rs | 36 +++++----- .../test-scenarios/tests/utils/mod.rs | 4 +- .../test-security/tests/01_invocations.rs | 6 +- .../tests/utils/instructions.rs | 2 +- 11 files changed, 115 insertions(+), 110 deletions(-) diff --git a/magicblock-committor-service/src/intent_executor/mod.rs b/magicblock-committor-service/src/intent_executor/mod.rs index 83ebf166f..fed13019e 100644 --- a/magicblock-committor-service/src/intent_executor/mod.rs +++ b/magicblock-committor-service/src/intent_executor/mod.rs @@ -6,7 +6,7 @@ pub mod two_stage_executor; use std::{mem, ops::ControlFlow, sync::Arc, time::Duration}; -#[cfg(any(test, feature = "dev-context-only-utils"))] +#[cfg(test)] mod null_task_info_fetcher; use async_trait::async_trait; @@ -25,7 +25,7 @@ use magicblock_rpc_client::{ MagicBlockRpcClientError, MagicBlockSendTransactionConfig, MagicBlockSendTransactionOutcome, MagicblockRpcClient, }; -#[cfg(any(test, feature = "dev-context-only-utils"))] +#[cfg(test)] pub use null_task_info_fetcher::*; use solana_keypair::Keypair; use solana_message::VersionedMessage; diff --git a/magicblock-committor-service/src/tasks/mod.rs b/magicblock-committor-service/src/tasks/mod.rs index 135affc6d..ea8b5c17a 100644 --- a/magicblock-committor-service/src/tasks/mod.rs +++ b/magicblock-committor-service/src/tasks/mod.rs @@ -303,33 +303,28 @@ pub type BaseTaskResult = Result; #[cfg(test)] mod serialization_safety_test { - use std::sync::Arc; use magicblock_program::{ args::ShortAccountMeta, magic_scheduled_base_intent::ProgramArgs, }; use solana_account::Account; - use crate::{ - intent_executor::NullTaskInfoFetcher, - tasks::{ - args_task::{ArgsTask, ArgsTaskType}, - buffer_task::BufferTask, - task_builder::TaskBuilderImpl, - *, - }, + use crate::tasks::{ + args_task::{ArgsTask, ArgsTaskType}, + buffer_task::{BufferTask, BufferTaskType}, + *, }; // Test all ArgsTask variants - #[tokio::test] - async fn test_args_task_instruction_serialization() { + #[test] + fn test_args_task_instruction_serialization() { let validator = Pubkey::new_unique(); // Test Commit variant - let commit_task: ArgsTask = TaskBuilderImpl::create_commit_task( - 123, - true, - CommittedAccount { + let commit_task: ArgsTask = ArgsTaskType::Commit(CommitTask { + commit_id: 123, + allow_undelegation: true, + committed_account: CommittedAccount { pubkey: Pubkey::new_unique(), account: Account { lamports: 1000, @@ -339,9 +334,8 @@ mod serialization_safety_test { rent_epoch: 0, }, }, - &Arc::new(NullTaskInfoFetcher), - ) - .await; + }) + .into(); assert_serializable(&commit_task.instruction(&validator)); // Test Finalize variant @@ -382,15 +376,15 @@ mod serialization_safety_test { } // Test BufferTask variants - #[tokio::test] - async fn test_buffer_task_instruction_serialization() { + #[test] + fn test_buffer_task_instruction_serialization() { let validator = Pubkey::new_unique(); let buffer_task = BufferTask::new_preparation_required( - TaskBuilderImpl::create_commit_task( - 456, - false, - CommittedAccount { + BufferTaskType::Commit(CommitTask { + commit_id: 456, + allow_undelegation: false, + committed_account: CommittedAccount { pubkey: Pubkey::new_unique(), account: Account { lamports: 2000, @@ -400,26 +394,22 @@ mod serialization_safety_test { rent_epoch: 0, }, }, - &Arc::new(NullTaskInfoFetcher), - ) - .await - .task_type - .into(), + }), ); assert_serializable(&buffer_task.instruction(&validator)); } // Test preparation instructions - #[tokio::test] - async fn test_preparation_instructions_serialization() { + #[test] + fn test_preparation_instructions_serialization() { let authority = Pubkey::new_unique(); // Test BufferTask preparation let buffer_task = BufferTask::new_preparation_required( - TaskBuilderImpl::create_commit_task( - 789, - true, - CommittedAccount { + BufferTaskType::Commit(CommitTask { + commit_id: 789, + allow_undelegation: true, + committed_account: CommittedAccount { pubkey: Pubkey::new_unique(), account: Account { lamports: 3000, @@ -429,11 +419,7 @@ mod serialization_safety_test { rent_epoch: 0, }, }, - &Arc::new(NullTaskInfoFetcher), - ) - .await - .task_type - .into(), + }), ); let PreparationState::Required(preparation_task) = diff --git a/test-integration/programs/schedulecommit/src/api.rs b/test-integration/programs/schedulecommit/src/api.rs index 4cbbb5f31..8f359a56c 100644 --- a/test-integration/programs/schedulecommit/src/api.rs +++ b/test-integration/programs/schedulecommit/src/api.rs @@ -13,6 +13,21 @@ use crate::{ ScheduleCommitInstruction, }; +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum UserSeeds { + MagicScheduleCommit, + OrderBook, +} + +impl UserSeeds { + pub fn bytes(&self) -> &'static [u8] { + match self { + UserSeeds::MagicScheduleCommit => b"magic_schedule_commit", + UserSeeds::OrderBook => b"order_book", + } + } +} + pub fn init_account_instruction( payer: Pubkey, player: Pubkey, @@ -101,20 +116,15 @@ pub fn delegate_account_cpi_instruction( payer: Pubkey, validator: Option, player_or_book_manager: Pubkey, - user_seed: &[u8], + user_seed: UserSeeds, ) -> Instruction { let program_id = crate::id(); let (pda, _) = Pubkey::find_program_address( - &[user_seed, player_or_book_manager.as_ref()], + &[user_seed.bytes(), player_or_book_manager.as_ref()], &crate::ID, ); let delegate_accounts = DelegateAccounts::new(pda, program_id); - println!("delegate_accounts: {:#?}", delegate_accounts); - println!( - "config: {:#?}", - dlp::pda::program_config_from_program_id(&crate::ID) - ); let delegate_metas = DelegateAccountMetas::from(delegate_accounts); let account_metas = vec![ @@ -130,21 +140,24 @@ pub fn delegate_account_cpi_instruction( Instruction::new_with_borsh( program_id, - &if user_seed == b"magic_schedule_commit" { - ScheduleCommitInstruction::DelegateCpi(DelegateCpiArgs { - valid_until: i64::MAX, - commit_frequency_ms: 1_000_000_000, - player: player_or_book_manager, - validator, - }) - } else { - ScheduleCommitInstruction::DelegateOrderBook( - DelegateOrderBookArgs { + &match user_seed { + UserSeeds::MagicScheduleCommit => { + ScheduleCommitInstruction::DelegateCpi(DelegateCpiArgs { + valid_until: i64::MAX, commit_frequency_ms: 1_000_000_000, - book_manager: player_or_book_manager, + player: player_or_book_manager, validator, - }, - ) + }) + } + UserSeeds::OrderBook => { + ScheduleCommitInstruction::DelegateOrderBook( + DelegateOrderBookArgs { + commit_frequency_ms: 1_000_000_000, + book_manager: player_or_book_manager, + validator, + }, + ) + } }, account_metas, ) diff --git a/test-integration/programs/schedulecommit/src/lib.rs b/test-integration/programs/schedulecommit/src/lib.rs index ee45268e1..32d2e39f5 100644 --- a/test-integration/programs/schedulecommit/src/lib.rs +++ b/test-integration/programs/schedulecommit/src/lib.rs @@ -230,7 +230,7 @@ pub struct MainAccount { } impl MainAccount { - pub const SIZE: u64 = std::mem::size_of::() as u64; + pub const SIZE: usize = std::mem::size_of::(); pub fn try_decode(data: &[u8]) -> std::io::Result { Self::try_from_slice(data) @@ -821,7 +821,7 @@ fn process_undelegate_request( { let mut data = delegated_account.try_borrow_mut_data()?; - match data.len() as u64 { + match data.len() { MainAccount::SIZE => match MainAccount::try_from_slice(&data) { Ok(counter) => { msg!("counter: {:?}", counter); diff --git a/test-integration/programs/schedulecommit/src/utils/mod.rs b/test-integration/programs/schedulecommit/src/utils/mod.rs index fd60458e5..6cd4f4c20 100644 --- a/test-integration/programs/schedulecommit/src/utils/mod.rs +++ b/test-integration/programs/schedulecommit/src/utils/mod.rs @@ -50,7 +50,7 @@ pub struct AllocateAndAssignAccountArgs<'a, 'b> { pub payer_info: &'a AccountInfo<'a>, pub account_info: &'a AccountInfo<'a>, pub owner: &'a Pubkey, - pub size: u64, + pub size: usize, pub signer_seeds: &'b [&'b [u8]], } @@ -68,7 +68,7 @@ pub fn allocate_account_and_assign_owner( } = args; let required_lamports = rent - .minimum_balance(size as usize) + .minimum_balance(size) .max(1) .saturating_sub(account_info.lamports()); @@ -87,7 +87,10 @@ pub fn allocate_account_and_assign_owner( // At this point the account is still owned by the system program msg!(" create_account() allocate space"); invoke_signed( - &system_instruction::allocate(account_info.key, size), + &system_instruction::allocate( + account_info.key, + size.try_into().unwrap(), + ), // 0. `[WRITE, SIGNER]` New account std::slice::from_ref(account_info), &[signer_seeds], diff --git a/test-integration/schedulecommit/client/src/schedule_commit_context.rs b/test-integration/schedulecommit/client/src/schedule_commit_context.rs index f74d86707..2fc1d1892 100644 --- a/test-integration/schedulecommit/client/src/schedule_commit_context.rs +++ b/test-integration/schedulecommit/client/src/schedule_commit_context.rs @@ -5,7 +5,7 @@ use integration_test_tools::IntegrationTestContext; use log::*; use program_schedulecommit::api::{ delegate_account_cpi_instruction, init_account_instruction, - init_order_book_instruction, init_payer_escrow, + init_order_book_instruction, init_payer_escrow, UserSeeds, }; use solana_rpc_client::rpc_client::{RpcClient, SerializableTransaction}; use solana_rpc_client_api::config::RpcSendTransactionConfig; @@ -31,7 +31,7 @@ pub struct ScheduleCommitTestContext { pub payer_ephem: Keypair, // The Payer keypairs along with its PDA pubkey which we'll commit pub committees: Vec<(Keypair, Pubkey)>, - user_seed: Vec, + user_seed: UserSeeds, common_ctx: IntegrationTestContext, } @@ -65,18 +65,18 @@ impl ScheduleCommitTestContext { // ----------------- pub fn try_new_random_keys( ncommittees: usize, - user_seed: &[u8], + user_seed: UserSeeds, ) -> Result { Self::try_new_internal(ncommittees, true, user_seed) } - pub fn try_new(ncommittees: usize, user_seed: &[u8]) -> Result { + pub fn try_new(ncommittees: usize, user_seed: UserSeeds) -> Result { Self::try_new_internal(ncommittees, false, user_seed) } fn try_new_internal( ncommittees: usize, random_keys: bool, - user_seed: &[u8], + user_seed: UserSeeds, ) -> Result { let ictx = IntegrationTestContext::try_new()?; @@ -113,7 +113,7 @@ impl ScheduleCommitTestContext { ) .unwrap(); let (pda, _bump) = Pubkey::find_program_address( - &[user_seed, payer_ephem.pubkey().as_ref()], + &[user_seed.bytes(), payer_ephem.pubkey().as_ref()], &program_schedulecommit::ID, ); (payer_ephem, pda) @@ -155,7 +155,7 @@ impl ScheduleCommitTestContext { payer_ephem, committees, common_ctx: ictx, - user_seed: user_seed.to_vec(), + user_seed, }) } @@ -167,8 +167,8 @@ impl ScheduleCommitTestContext { ComputeBudgetInstruction::set_compute_unit_limit(1_400_000), ComputeBudgetInstruction::set_compute_unit_price(10_000), ]; - match self.user_seed.as_slice() { - b"magic_schedule_commit" => { + match self.user_seed { + UserSeeds::MagicScheduleCommit => { ixs.extend(self.committees.iter().map( |(player, committee)| { init_account_instruction( @@ -179,7 +179,7 @@ impl ScheduleCommitTestContext { }, )); } - b"order_book" => { + UserSeeds::OrderBook => { ixs.extend(self.committees.iter().map( |(book_manager, committee)| { init_order_book_instruction( @@ -205,9 +205,6 @@ impl ScheduleCommitTestContext { // }, // )); } - _ => { - return Err(anyhow::anyhow!("Unsupported user_seed: {:?} ; expected b\"magic_schedule_commit\" or b\"order_book\"", self.user_seed)); - } }; let mut signers = self @@ -272,7 +269,7 @@ impl ScheduleCommitTestContext { self.payer_chain.pubkey(), self.ephem_validator_identity, player.pubkey(), - &self.user_seed, + self.user_seed, ); ixs.push(ix); } diff --git a/test-integration/schedulecommit/test-scenarios/tests/01_commits.rs b/test-integration/schedulecommit/test-scenarios/tests/01_commits.rs index ef0595b0c..4c7456b43 100644 --- a/test-integration/schedulecommit/test-scenarios/tests/01_commits.rs +++ b/test-integration/schedulecommit/test-scenarios/tests/01_commits.rs @@ -5,7 +5,7 @@ use log::*; use program_schedulecommit::{ api::{ delegate_account_cpi_instruction, init_account_instruction, - pda_and_bump, schedule_commit_cpi_instruction, + pda_and_bump, schedule_commit_cpi_instruction, UserSeeds, }, ScheduleCommitCpiArgs, ScheduleCommitInstruction, }; @@ -45,8 +45,10 @@ mod utils; #[test] fn test_committing_one_account() { run_test!({ - let ctx = - get_context_with_delegated_committees(1, b"magic_schedule_commit"); + let ctx = get_context_with_delegated_committees( + 1, + UserSeeds::MagicScheduleCommit, + ); let ScheduleCommitTestContextFields { payer_ephem: payer, @@ -99,8 +101,10 @@ fn test_committing_one_account() { #[test] fn test_committing_two_accounts() { run_test!({ - let ctx = - get_context_with_delegated_committees(2, b"magic_schedule_commit"); + let ctx = get_context_with_delegated_committees( + 2, + UserSeeds::MagicScheduleCommit, + ); let ScheduleCommitTestContextFields { payer_ephem: payer, @@ -231,7 +235,7 @@ fn init_and_delegate_player( payer.pubkey(), validator, player.pubkey(), - b"magic_schedule_commit", + UserSeeds::MagicScheduleCommit, ); // Send transaction diff --git a/test-integration/schedulecommit/test-scenarios/tests/02_commit_and_undelegate.rs b/test-integration/schedulecommit/test-scenarios/tests/02_commit_and_undelegate.rs index daaea5506..f2d9c7ba0 100644 --- a/test-integration/schedulecommit/test-scenarios/tests/02_commit_and_undelegate.rs +++ b/test-integration/schedulecommit/test-scenarios/tests/02_commit_and_undelegate.rs @@ -11,7 +11,7 @@ use program_schedulecommit::{ schedule_commit_and_undelegate_cpi_twice, schedule_commit_and_undelegate_cpi_with_mod_after_instruction, schedule_commit_diff_instruction_for_order_book, set_count_instruction, - update_order_book_instruction, + update_order_book_instruction, UserSeeds, }, BookUpdate, OrderLevel, FAIL_UNDELEGATION_COUNT, }; @@ -53,8 +53,10 @@ fn commit_and_undelegate_one_account( Signature, Result, ) { - let ctx = - get_context_with_delegated_committees(1, b"magic_schedule_commit"); + let ctx = get_context_with_delegated_committees( + 1, + UserSeeds::MagicScheduleCommit, + ); let ScheduleCommitTestContextFields { payer_ephem: payer, committees, @@ -115,7 +117,7 @@ fn commit_and_undelegate_order_book_account( Signature, Result, ) { - let ctx = get_context_with_delegated_committees(1, b"order_book"); + let ctx = get_context_with_delegated_committees(1, UserSeeds::OrderBook); let ScheduleCommitTestContextFields { payer_ephem, committees, @@ -158,7 +160,7 @@ fn commit_and_undelegate_order_book_account( ..Default::default() }, ); - println!("txhash (scheduled_commit): {:?}", tx_res); + debug!("txhash (scheduled_commit): {:?}", tx_res); debug!("Commit and Undelegate Transaction result: '{:?}'", tx_res); (ctx, *sig, tx_res) @@ -171,8 +173,10 @@ fn commit_and_undelegate_two_accounts( Signature, Result, ) { - let ctx = - get_context_with_delegated_committees(2, b"magic_schedule_commit"); + let ctx = get_context_with_delegated_committees( + 2, + UserSeeds::MagicScheduleCommit, + ); let ScheduleCommitTestContextFields { payer_ephem: payer, committees, @@ -232,8 +236,10 @@ fn commit_and_undelegate_two_accounts_twice() -> ( Signature, Result, ) { - let ctx = - get_context_with_delegated_committees(2, b"magic_schedule_commit"); + let ctx = get_context_with_delegated_committees( + 2, + UserSeeds::MagicScheduleCommit, + ); let ScheduleCommitTestContextFields { payer_ephem: payer, committees, @@ -315,12 +321,6 @@ fn test_committing_and_undelegating_huge_order_book_account() { size: random.gen_range(1..10), } })); - println!( - "BookUpdate: total = {}, bids = {}, asks = {}", - update.bids.len() + update.asks.len(), - update.bids.len(), - update.asks.len() - ); (rng_seed, update) }; let (ctx, sig, tx_res) = @@ -738,8 +738,10 @@ fn test_committing_and_undelegating_two_accounts_twice() { #[test] fn test_committing_after_failed_undelegation() { run_test!({ - let ctx = - get_context_with_delegated_committees(1, b"magic_schedule_commit"); + let ctx = get_context_with_delegated_committees( + 1, + UserSeeds::MagicScheduleCommit, + ); let ScheduleCommitTestContextFields { payer_ephem: payer, committees, diff --git a/test-integration/schedulecommit/test-scenarios/tests/utils/mod.rs b/test-integration/schedulecommit/test-scenarios/tests/utils/mod.rs index 9d8952c9a..342f94ed2 100644 --- a/test-integration/schedulecommit/test-scenarios/tests/utils/mod.rs +++ b/test-integration/schedulecommit/test-scenarios/tests/utils/mod.rs @@ -1,6 +1,6 @@ use ephemeral_rollups_sdk::consts::DELEGATION_PROGRAM_ID; use integration_test_tools::scheduled_commits::ScheduledCommitResult; -use program_schedulecommit::MainAccount; +use program_schedulecommit::{api::UserSeeds, MainAccount}; use schedulecommit_client::ScheduleCommitTestContext; use solana_rpc_client_api::client_error; use solana_sdk::{ @@ -15,7 +15,7 @@ use solana_sdk::{ // ----------------- pub fn get_context_with_delegated_committees( ncommittees: usize, - user_seed: &[u8], + user_seed: UserSeeds, ) -> ScheduleCommitTestContext { let ctx = if std::env::var("FIXED_KP").is_ok() { ScheduleCommitTestContext::try_new(ncommittees, user_seed) diff --git a/test-integration/schedulecommit/test-security/tests/01_invocations.rs b/test-integration/schedulecommit/test-security/tests/01_invocations.rs index 068c65193..8fa8ed255 100644 --- a/test-integration/schedulecommit/test-security/tests/01_invocations.rs +++ b/test-integration/schedulecommit/test-security/tests/01_invocations.rs @@ -1,4 +1,4 @@ -use program_schedulecommit::api::schedule_commit_cpi_instruction; +use program_schedulecommit::api::{schedule_commit_cpi_instruction, UserSeeds}; use schedulecommit_client::{ ScheduleCommitTestContext, ScheduleCommitTestContextFields, }; @@ -27,11 +27,11 @@ const NEEDS_TO_BE_OWNED_BY_INVOKING_PROGRAM: &str = fn prepare_ctx_with_account_to_commit() -> ScheduleCommitTestContext { let ctx = if std::env::var("FIXED_KP").is_ok() { - ScheduleCommitTestContext::try_new(2, b"magic_schedule_commit") + ScheduleCommitTestContext::try_new(2, UserSeeds::MagicScheduleCommit) } else { ScheduleCommitTestContext::try_new_random_keys( 2, - b"magic_schedule_commit", + UserSeeds::MagicScheduleCommit, ) } .unwrap(); diff --git a/test-integration/test-committor-service/tests/utils/instructions.rs b/test-integration/test-committor-service/tests/utils/instructions.rs index 9a8622968..0fcb37f97 100644 --- a/test-integration/test-committor-service/tests/utils/instructions.rs +++ b/test-integration/test-committor-service/tests/utils/instructions.rs @@ -82,7 +82,7 @@ pub fn init_order_book_account_and_delegate_ixs( payer, None, book_manager.pubkey(), - b"order_book", + api::UserSeeds::OrderBook, ); InitOrderBookAndDelegateIxs { From 3406ed980fc092b3c990ee22b5cd36df9a8e57b5 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Tue, 23 Dec 2025 18:22:44 +0530 Subject: [PATCH 12/13] Fetch base_accounts and commit_ids in parallel --- .../src/intent_executor/mod.rs | 5 - .../intent_executor/null_task_info_fetcher.rs | 42 ----- .../src/intent_executor/task_info_fetcher.rs | 30 ++-- .../src/tasks/task_builder.rs | 82 ++++++---- .../src/tasks/task_strategist.rs | 147 ++++++++---------- .../programs/schedulecommit/src/order_book.rs | 7 +- .../test-committor-service/tests/common.rs | 25 ++- .../tests/test_transaction_preparator.rs | 36 ++--- 8 files changed, 170 insertions(+), 204 deletions(-) delete mode 100644 magicblock-committor-service/src/intent_executor/null_task_info_fetcher.rs diff --git a/magicblock-committor-service/src/intent_executor/mod.rs b/magicblock-committor-service/src/intent_executor/mod.rs index fed13019e..4c26ac8e0 100644 --- a/magicblock-committor-service/src/intent_executor/mod.rs +++ b/magicblock-committor-service/src/intent_executor/mod.rs @@ -6,9 +6,6 @@ pub mod two_stage_executor; use std::{mem, ops::ControlFlow, sync::Arc, time::Duration}; -#[cfg(test)] -mod null_task_info_fetcher; - use async_trait::async_trait; use futures_util::future::{join, try_join_all}; use log::{trace, warn}; @@ -25,8 +22,6 @@ use magicblock_rpc_client::{ MagicBlockRpcClientError, MagicBlockSendTransactionConfig, MagicBlockSendTransactionOutcome, MagicblockRpcClient, }; -#[cfg(test)] -pub use null_task_info_fetcher::*; use solana_keypair::Keypair; use solana_message::VersionedMessage; use solana_pubkey::Pubkey; diff --git a/magicblock-committor-service/src/intent_executor/null_task_info_fetcher.rs b/magicblock-committor-service/src/intent_executor/null_task_info_fetcher.rs deleted file mode 100644 index f00c77a17..000000000 --- a/magicblock-committor-service/src/intent_executor/null_task_info_fetcher.rs +++ /dev/null @@ -1,42 +0,0 @@ -use std::collections::HashMap; - -use async_trait::async_trait; -use magicblock_rpc_client::MagicBlockRpcClientResult; -use solana_account::Account; -use solana_pubkey::Pubkey; - -use super::task_info_fetcher::{ - ResetType, TaskInfoFetcher, TaskInfoFetcherResult, -}; - -pub struct NullTaskInfoFetcher; - -#[async_trait] -impl TaskInfoFetcher for NullTaskInfoFetcher { - async fn fetch_next_commit_ids( - &self, - _pubkeys: &[Pubkey], - ) -> TaskInfoFetcherResult> { - Ok(Default::default()) - } - - async fn fetch_rent_reimbursements( - &self, - _pubkeys: &[Pubkey], - ) -> TaskInfoFetcherResult> { - Ok(Default::default()) - } - - fn peek_commit_id(&self, _pubkey: &Pubkey) -> Option { - None - } - - fn reset(&self, _: ResetType) {} - - async fn get_base_account( - &self, - _pubkey: &Pubkey, - ) -> MagicBlockRpcClientResult> { - Ok(None) // AccountNotFound - } -} diff --git a/magicblock-committor-service/src/intent_executor/task_info_fetcher.rs b/magicblock-committor-service/src/intent_executor/task_info_fetcher.rs index b124f8667..be7ea5161 100644 --- a/magicblock-committor-service/src/intent_executor/task_info_fetcher.rs +++ b/magicblock-committor-service/src/intent_executor/task_info_fetcher.rs @@ -9,9 +9,7 @@ use dlp::{ use log::warn; use lru::LruCache; use magicblock_metrics::metrics; -use magicblock_rpc_client::{ - MagicBlockRpcClientError, MagicBlockRpcClientResult, MagicblockRpcClient, -}; +use magicblock_rpc_client::{MagicBlockRpcClientError, MagicblockRpcClient}; use solana_account::Account; use solana_pubkey::Pubkey; use solana_signature::Signature; @@ -40,10 +38,10 @@ pub trait TaskInfoFetcher: Send + Sync + 'static { /// Resets cache for some or all accounts fn reset(&self, reset_type: ResetType); - async fn get_base_account( + async fn get_base_accounts( &self, - _pubkey: &Pubkey, - ) -> MagicBlockRpcClientResult>; + pubkeys: &[Pubkey], + ) -> TaskInfoFetcherResult>; } pub enum ResetType<'a> { @@ -272,11 +270,23 @@ impl TaskInfoFetcher for CacheTaskInfoFetcher { } } - async fn get_base_account( + async fn get_base_accounts( &self, - pubkey: &Pubkey, - ) -> MagicBlockRpcClientResult> { - self.rpc_client.get_account(pubkey).await + pubkeys: &[Pubkey], + ) -> TaskInfoFetcherResult> { + self.rpc_client + .get_multiple_accounts(pubkeys, None) + .await + .map_err(|err| { + TaskInfoFetcherError::MagicBlockRpcClientError(Box::new(err)) + }) + .map(|accounts| { + pubkeys + .iter() + .zip(accounts.into_iter()) + .filter_map(|(key, value)| value.map(|value| (*key, value))) + .collect() + }) } } diff --git a/magicblock-committor-service/src/tasks/task_builder.rs b/magicblock-committor-service/src/tasks/task_builder.rs index a46c820c7..7a9787fe2 100644 --- a/magicblock-committor-service/src/tasks/task_builder.rs +++ b/magicblock-committor-service/src/tasks/task_builder.rs @@ -1,12 +1,12 @@ use std::sync::Arc; use async_trait::async_trait; -use futures_util::future::join_all; use log::error; use magicblock_program::magic_scheduled_base_intent::{ CommitType, CommittedAccount, MagicBaseIntent, ScheduledBaseIntent, UndelegateType, }; +use solana_account::Account; use solana_pubkey::Pubkey; use solana_signature::Signature; @@ -51,31 +51,18 @@ pub struct TaskBuilderImpl; pub const COMMIT_STATE_SIZE_THRESHOLD: usize = 256; impl TaskBuilderImpl { - pub async fn create_commit_task( + pub fn create_commit_task( commit_id: u64, allow_undelegation: bool, account: CommittedAccount, - task_info_fetcher: &Arc, + base_account: Option, ) -> ArgsTask { - let base_account = if account.account.data.len() - > COMMIT_STATE_SIZE_THRESHOLD - { - match task_info_fetcher.get_base_account(&account.pubkey).await { - Ok(Some(account)) => Some(account), - Ok(None) => { - log::warn!("AccountNotFound for commit_diff, pubkey: {}, commit_id: {}, Falling back to commit_state.", - account.pubkey, commit_id); - None - } - Err(e) => { - log::warn!("Failed to fetch base account for commit diff, pubkey: {}, commit_id: {}, error: {}. Falling back to commit_state.", - account.pubkey, commit_id, e); - None - } - } - } else { - None - }; + let base_account = + if account.account.data.len() > COMMIT_STATE_SIZE_THRESHOLD { + base_account + } else { + None + }; if let Some(base_account) = base_account { ArgsTaskType::CommitDiff(CommitDiffTask { @@ -123,14 +110,37 @@ impl TasksBuilder for TaskBuilderImpl { } }; - let committed_pubkeys = accounts - .iter() - .map(|account| account.pubkey) - .collect::>(); - let commit_ids = commit_id_fetcher - .fetch_next_commit_ids(&committed_pubkeys) - .await - .map_err(TaskBuilderError::CommitTasksBuildError)?; + let (commit_ids, base_accounts) = { + let committed_pubkeys = accounts + .iter() + .map(|account| account.pubkey) + .collect::>(); + + let diffable_pubkeys = accounts + .iter() + .filter(|account| { + account.account.data.len() > COMMIT_STATE_SIZE_THRESHOLD + }) + .map(|account| account.pubkey) + .collect::>(); + + tokio::join!( + commit_id_fetcher.fetch_next_commit_ids(&committed_pubkeys), + commit_id_fetcher + .get_base_accounts(diffable_pubkeys.as_slice()) + ) + }; + + let commit_ids = + commit_ids.map_err(TaskBuilderError::CommitTasksBuildError)?; + + let base_accounts = match base_accounts { + Ok(map) => map, + Err(err) => { + log::warn!("Failed to fetch base accounts for CommitDiff (id={}): {}; falling back to CommitState", base_intent.id, err); + Default::default() + } + }; // Persist commit ids for commitees commit_ids @@ -141,13 +151,17 @@ impl TasksBuilder for TaskBuilderImpl { } }); - let tasks = join_all(accounts + let tasks = accounts .iter() - .map(|account| async { + .map(|account| { let commit_id = *commit_ids.get(&account.pubkey).expect("CommitIdFetcher provide commit ids for all listed pubkeys, or errors!"); - let task = Self::create_commit_task(commit_id, allow_undelegation, account.clone(), commit_id_fetcher).await; + // TODO (snawaz): if accounts do not have duplicate, then we can use remove + // instead: + // let base_account = base_accounts.remove(&account.pubkey); + let base_account = base_accounts.get(&account.pubkey).cloned(); + let task = Self::create_commit_task(commit_id, allow_undelegation, account.clone(), base_account); Box::new(task) as Box - })).await; + }).collect(); Ok(tasks) } diff --git a/magicblock-committor-service/src/tasks/task_strategist.rs b/magicblock-committor-service/src/tasks/task_strategist.rs index bc55ab2a9..edbbd74d3 100644 --- a/magicblock-committor-service/src/tasks/task_strategist.rs +++ b/magicblock-committor-service/src/tasks/task_strategist.rs @@ -370,11 +370,9 @@ pub type TaskStrategistResult = Result; mod tests { use std::{collections::HashMap, sync::Arc}; - use futures_util::future::join_all; use magicblock_program::magic_scheduled_base_intent::{ BaseAction, CommittedAccount, ProgramArgs, }; - use magicblock_rpc_client::MagicBlockRpcClientResult; use solana_account::Account; use solana_program::system_program; use solana_pubkey::Pubkey; @@ -383,11 +381,8 @@ mod tests { use super::*; use crate::{ intent_execution_manager::intent_scheduler::create_test_intent, - intent_executor::{ - task_info_fetcher::{ - ResetType, TaskInfoFetcher, TaskInfoFetcherResult, - }, - NullTaskInfoFetcher, + intent_executor::task_info_fetcher::{ + ResetType, TaskInfoFetcher, TaskInfoFetcherResult, }, persist::IntentPersisterImpl, tasks::{ @@ -398,8 +393,7 @@ mod tests { }, }; - #[derive(Default)] - struct MockInfoFetcher(HashMap); + struct MockInfoFetcher; #[async_trait::async_trait] impl TaskInfoFetcher for MockInfoFetcher { @@ -423,16 +417,16 @@ mod tests { fn reset(&self, _: ResetType) {} - async fn get_base_account( + async fn get_base_accounts( &self, - pubkey: &Pubkey, - ) -> MagicBlockRpcClientResult> { - Ok(self.0.get(pubkey).cloned()) + _pubkeys: &[Pubkey], + ) -> TaskInfoFetcherResult> { + Ok(Default::default()) } } // Helper to create a simple commit task - async fn create_test_commit_task( + fn create_test_commit_task( commit_id: u64, data_size: usize, diff_len: usize, @@ -453,25 +447,23 @@ mod tests { commit_id, false, committed_account, - &Arc::new(NullTaskInfoFetcher), + None, ) - .await } else { - let originals: HashMap = { + let base_account = { let mut acc = committed_account.account.clone(); assert!(diff_len <= acc.data.len()); for byte in &mut acc.data[..diff_len] { *byte = byte.wrapping_add(1); } - [(committed_account.pubkey, acc)].iter().cloned().collect() + acc }; TaskBuilderImpl::create_commit_task( commit_id, false, committed_account, - &Arc::new(MockInfoFetcher(originals)), + Some(base_account), ) - .await } } @@ -507,10 +499,10 @@ mod tests { })) } - #[tokio::test] - async fn test_build_strategy_with_single_small_task() { + #[test] + fn test_build_strategy_with_single_small_task() { let validator = Pubkey::new_unique(); - let task = create_test_commit_task(1, 100, 0).await; + let task = create_test_commit_task(1, 100, 0); let tasks = vec![Box::new(task) as Box]; let strategy = TaskStrategist::build_strategy( @@ -524,11 +516,11 @@ mod tests { assert!(strategy.lookup_tables_keys.is_empty()); } - #[tokio::test] - async fn test_build_strategy_optimizes_to_buffer_when_needed() { + #[test] + fn test_build_strategy_optimizes_to_buffer_when_needed() { let validator = Pubkey::new_unique(); - let task = create_test_commit_task(1, 1000, 0).await; // Large task + let task = create_test_commit_task(1, 1000, 0); // Large task let tasks = vec![Box::new(task) as Box]; let strategy = TaskStrategist::build_strategy( @@ -545,11 +537,11 @@ mod tests { )); } - #[tokio::test] - async fn test_build_strategy_optimizes_to_buffer_u16_exceeded() { + #[test] + fn test_build_strategy_optimizes_to_buffer_u16_exceeded() { let validator = Pubkey::new_unique(); - let task = create_test_commit_task(1, 66_000, 0).await; // Large task + let task = create_test_commit_task(1, 66_000, 0); // Large task let tasks = vec![Box::new(task) as Box]; let strategy = TaskStrategist::build_strategy( @@ -566,14 +558,12 @@ mod tests { )); } - #[tokio::test] - async fn test_build_strategy_does_not_optimize_large_account_but_small_diff( - ) { + #[test] + fn test_build_strategy_does_not_optimize_large_account_but_small_diff() { let validator = Pubkey::new_unique(); let task = - create_test_commit_task(1, 66_000, COMMIT_STATE_SIZE_THRESHOLD) - .await; // large account but small diff + create_test_commit_task(1, 66_000, COMMIT_STATE_SIZE_THRESHOLD); // large account but small diff let tasks = vec![Box::new(task) as Box]; let strategy = TaskStrategist::build_strategy( @@ -587,14 +577,13 @@ mod tests { assert_eq!(strategy.optimized_tasks[0].strategy(), TaskStrategy::Args); } - #[tokio::test] - async fn test_build_strategy_does_not_optimize_large_account_and_above_threshold_diff( + #[test] + fn test_build_strategy_does_not_optimize_large_account_and_above_threshold_diff( ) { let validator = Pubkey::new_unique(); let task = - create_test_commit_task(1, 66_000, COMMIT_STATE_SIZE_THRESHOLD + 1) - .await; // large account but small diff + create_test_commit_task(1, 66_000, COMMIT_STATE_SIZE_THRESHOLD + 1); // large account but small diff let tasks = vec![Box::new(task) as Box]; let strategy = TaskStrategist::build_strategy( @@ -608,13 +597,12 @@ mod tests { assert_eq!(strategy.optimized_tasks[0].strategy(), TaskStrategy::Args); } - #[tokio::test] - async fn test_build_strategy_does_optimize_large_account_and_large_diff() { + #[test] + fn test_build_strategy_does_optimize_large_account_and_large_diff() { let validator = Pubkey::new_unique(); let task = - create_test_commit_task(1, 66_000, COMMIT_STATE_SIZE_THRESHOLD * 4) - .await; // large account but small diff + create_test_commit_task(1, 66_000, COMMIT_STATE_SIZE_THRESHOLD * 4); // large account but small diff let tasks = vec![Box::new(task) as Box]; let strategy = TaskStrategist::build_strategy( @@ -631,18 +619,19 @@ mod tests { ); } - #[tokio::test] - async fn test_build_strategy_creates_multiple_buffers() { + #[test] + fn test_build_strategy_creates_multiple_buffers() { // TODO: ALSO MAX NUM WITH PURE BUFFER commits, no alts const NUM_COMMITS: u64 = 3; let validator = Pubkey::new_unique(); - let tasks = join_all((0..NUM_COMMITS).map(|i| async move { - let task = create_test_commit_task(i, 500, 0).await; // Large task - Box::new(task) as Box - })) - .await; + let tasks = (0..NUM_COMMITS) + .map(|i| { + let task = create_test_commit_task(i, 500, 0); // Large task + Box::new(task) as Box + }) + .collect(); let strategy = TaskStrategist::build_strategy( tasks, @@ -657,19 +646,20 @@ mod tests { assert!(strategy.lookup_tables_keys.is_empty()); } - #[tokio::test] - async fn test_build_strategy_with_lookup_tables_when_needed() { + #[test] + fn test_build_strategy_with_lookup_tables_when_needed() { // Also max number of committed accounts fit with ALTs! const NUM_COMMITS: u64 = 22; let validator = Pubkey::new_unique(); - let tasks = join_all((0..NUM_COMMITS).map(|i| async move { - // Large task - let task = create_test_commit_task(i, 10000, 0).await; - Box::new(task) as Box - })) - .await; + let tasks = (0..NUM_COMMITS) + .map(|i| { + // Large task + let task = create_test_commit_task(i, 10000, 0); + Box::new(task) as Box + }) + .collect(); let strategy = TaskStrategist::build_strategy( tasks, @@ -684,18 +674,19 @@ mod tests { assert!(!strategy.lookup_tables_keys.is_empty()); } - #[tokio::test] - async fn test_build_strategy_fails_when_cant_fit() { + #[test] + fn test_build_strategy_fails_when_cant_fit() { const NUM_COMMITS: u64 = 23; let validator = Pubkey::new_unique(); - let tasks = join_all((0..NUM_COMMITS).map(|i| async move { - // Large task - let task = create_test_commit_task(i, 1000, 0).await; - Box::new(task) as Box - })) - .await; + let tasks = (0..NUM_COMMITS) + .map(|i| { + // Large task + let task = create_test_commit_task(i, 1000, 0); + Box::new(task) as Box + }) + .collect(); let result = TaskStrategist::build_strategy( tasks, @@ -705,15 +696,12 @@ mod tests { assert!(matches!(result, Err(TaskStrategistError::FailedToFitError))); } - #[tokio::test] - async fn test_optimize_strategy_prioritizes_largest_tasks() { + #[test] + fn test_optimize_strategy_prioritizes_largest_tasks() { let mut tasks = [ - Box::new(create_test_commit_task(1, 100, 0).await) - as Box, - Box::new(create_test_commit_task(2, 1000, 0).await) - as Box, // Larger task - Box::new(create_test_commit_task(3, 1000, 0).await) - as Box, // Larger task + Box::new(create_test_commit_task(1, 100, 0)) as Box, + Box::new(create_test_commit_task(2, 1000, 0)) as Box, // Larger task + Box::new(create_test_commit_task(3, 1000, 0)) as Box, // Larger task ]; let _ = TaskStrategist::optimize_strategy(&mut tasks); @@ -722,12 +710,11 @@ mod tests { assert!(matches!(tasks[1].strategy(), TaskStrategy::Buffer)); } - #[tokio::test] - async fn test_mixed_task_types_with_optimization() { + #[test] + fn test_mixed_task_types_with_optimization() { let validator = Pubkey::new_unique(); let tasks = vec![ - Box::new(create_test_commit_task(1, 1000, 0).await) - as Box, + Box::new(create_test_commit_task(1, 1000, 0)) as Box, Box::new(create_test_finalize_task()) as Box, Box::new(create_test_base_action_task(500)) as Box, Box::new(create_test_undelegate_task()) as Box, @@ -768,7 +755,7 @@ mod tests { let pubkey = [Pubkey::new_unique()]; let intent = create_test_intent(0, &pubkey, false); - let info_fetcher = Arc::new(MockInfoFetcher::default()); + let info_fetcher = Arc::new(MockInfoFetcher); let commit_task = TaskBuilderImpl::commit_tasks( &info_fetcher, &intent, @@ -800,7 +787,7 @@ mod tests { let pubkeys: [_; 3] = std::array::from_fn(|_| Pubkey::new_unique()); let intent = create_test_intent(0, &pubkeys, true); - let info_fetcher = Arc::new(MockInfoFetcher::default()); + let info_fetcher = Arc::new(MockInfoFetcher); let commit_task = TaskBuilderImpl::commit_tasks( &info_fetcher, &intent, @@ -837,7 +824,7 @@ mod tests { let pubkeys: [_; 8] = std::array::from_fn(|_| Pubkey::new_unique()); let intent = create_test_intent(0, &pubkeys, false); - let info_fetcher = Arc::new(MockInfoFetcher::default()); + let info_fetcher = Arc::new(MockInfoFetcher); let commit_task = TaskBuilderImpl::commit_tasks( &info_fetcher, &intent, diff --git a/test-integration/programs/schedulecommit/src/order_book.rs b/test-integration/programs/schedulecommit/src/order_book.rs index d36298305..5881387e4 100644 --- a/test-integration/programs/schedulecommit/src/order_book.rs +++ b/test-integration/programs/schedulecommit/src/order_book.rs @@ -61,17 +61,14 @@ impl borsh::de::BorshDeserialize for OrderBookOwned { // I could take mutable bytes from &[u8] as well and unsafe block will not // stop me, but that would break aliasing rules and therefore would invoke UB. // It's a test code, so copying should be OK. - let book_bytes = { + let mut book_bytes = { let mut aligned = rkyv::AlignedVec::with_capacity(book_bytes.len()); aligned.extend_from_slice(book_bytes); aligned }; OrderBook::try_new(unsafe { - slice::from_raw_parts_mut( - book_bytes.as_ptr() as *mut u8, - book_bytes.len(), - ) + slice::from_raw_parts_mut(book_bytes.as_mut_ptr(), book_bytes.len()) }) .map(|book| OrderBookOwned::from(&book)) .map_err(|_| borsh::io::Error::from(borsh::io::ErrorKind::InvalidData)) diff --git a/test-integration/test-committor-service/tests/common.rs b/test-integration/test-committor-service/tests/common.rs index b5ad36775..2c3e1b0e9 100644 --- a/test-integration/test-committor-service/tests/common.rs +++ b/test-integration/test-committor-service/tests/common.rs @@ -10,7 +10,8 @@ use async_trait::async_trait; use magicblock_committor_service::{ intent_executor::{ task_info_fetcher::{ - ResetType, TaskInfoFetcher, TaskInfoFetcherResult, + ResetType, TaskInfoFetcher, TaskInfoFetcherError, + TaskInfoFetcherResult, }, IntentExecutorImpl, }, @@ -21,7 +22,7 @@ use magicblock_committor_service::{ ComputeBudgetConfig, }; use magicblock_program::magic_scheduled_base_intent::CommittedAccount; -use magicblock_rpc_client::{MagicBlockRpcClientResult, MagicblockRpcClient}; +use magicblock_rpc_client::MagicblockRpcClient; use magicblock_table_mania::{GarbageCollectorConfig, TableMania}; use solana_account::Account; use solana_pubkey::Pubkey; @@ -142,11 +143,23 @@ impl TaskInfoFetcher for MockTaskInfoFetcher { fn reset(&self, _: ResetType) {} - async fn get_base_account( + async fn get_base_accounts( &self, - pubkey: &Pubkey, - ) -> MagicBlockRpcClientResult> { - self.0.get_account(pubkey).await + pubkeys: &[Pubkey], + ) -> TaskInfoFetcherResult> { + self.0 + .get_multiple_accounts(pubkeys, None) + .await + .map_err(|err| { + TaskInfoFetcherError::MagicBlockRpcClientError(Box::new(err)) + }) + .map(|accounts| { + pubkeys + .iter() + .zip(accounts.into_iter()) + .filter_map(|(key, value)| value.map(|value| (*key, value))) + .collect() + }) } } diff --git a/test-integration/test-committor-service/tests/test_transaction_preparator.rs b/test-integration/test-committor-service/tests/test_transaction_preparator.rs index 16e55121a..b82aa3aa5 100644 --- a/test-integration/test-committor-service/tests/test_transaction_preparator.rs +++ b/test-integration/test-committor-service/tests/test_transaction_preparator.rs @@ -35,15 +35,12 @@ async fn test_prepare_commit_tx_with_single_account() { let committed_account = create_committed_account(&account_data); let tasks = vec![ - Box::new( - TaskBuilderImpl::create_commit_task( - 1, - true, - committed_account.clone(), - &fixture.create_task_info_fetcher(), - ) - .await, - ) as Box, + Box::new(TaskBuilderImpl::create_commit_task( + 1, + true, + committed_account.clone(), + None, + )) as Box, Box::new(ArgsTask::new(ArgsTaskType::Finalize(FinalizeTask { delegated_account: committed_account.pubkey, }))), @@ -98,24 +95,20 @@ async fn test_prepare_commit_tx_with_multiple_accounts() { 1, true, committed_account2.clone(), - &fixture.create_task_info_fetcher(), + None, ) - .await .task_type .into(), ); // Create test data let tasks = vec![ // account 1 - Box::new( - TaskBuilderImpl::create_commit_task( - 1, - true, - committed_account1.clone(), - &fixture.create_task_info_fetcher(), - ) - .await, - ) as Box, + Box::new(TaskBuilderImpl::create_commit_task( + 1, + true, + committed_account1.clone(), + None, + )) as Box, // account 2 Box::new(buffer_commit_task), // finalize account 1 @@ -203,9 +196,8 @@ async fn test_prepare_commit_tx_with_base_actions() { 1, true, committed_account.clone(), - &fixture.create_task_info_fetcher(), + None, ) - .await .task_type .into(), ); From f092b0ba034f678f000ac79ea59870325b6c075e Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Wed, 24 Dec 2025 15:58:39 +0530 Subject: [PATCH 13/13] fix reset_commit_id() --- .../src/tasks/args_task.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/magicblock-committor-service/src/tasks/args_task.rs b/magicblock-committor-service/src/tasks/args_task.rs index a7e6ca81f..1bd5f1e5d 100644 --- a/magicblock-committor-service/src/tasks/args_task.rs +++ b/magicblock-committor-service/src/tasks/args_task.rs @@ -198,11 +198,17 @@ impl BaseTask for ArgsTask { } fn reset_commit_id(&mut self, commit_id: u64) { - let ArgsTaskType::Commit(commit_task) = &mut self.task_type else { - return; + match &mut self.task_type { + ArgsTaskType::Commit(task) => { + task.commit_id = commit_id; + } + ArgsTaskType::CommitDiff(task) => { + task.commit_id = commit_id; + } + ArgsTaskType::BaseAction(_) + | ArgsTaskType::Finalize(_) + | ArgsTaskType::Undelegate(_) => {} }; - - commit_task.commit_id = commit_id; } }