From 506909d17d82b23fbdc0746cef90ed81f3a3a3f1 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Tue, 14 Oct 2025 11:07:26 +0530 Subject: [PATCH 01/16] feat: Add MagicBlockInstruction::ScheduleCommitDiffAndUndelegate to efficiently commit changes in delegated accounts --- Cargo.lock | 184 ++++++++++-- Cargo.toml | 4 +- .../src/scheduled_commits_processor.rs | 3 + .../src/tasks/args_task.rs | 83 +++++- magicblock-committor-service/src/tasks/mod.rs | 2 + .../src/tasks/task_builder.rs | 54 ++-- .../tasks/task_visitors/persistor_visitor.rs | 7 +- .../src/instruction.rs | 2 + .../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 | 279 +++++++++++++----- 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 | 211 ++++++++++++- .../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 | 145 ++++++++- .../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, 1471 insertions(+), 185 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 5ff4a459f..a60f9e2b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -680,6 +680,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" @@ -836,6 +848,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" @@ -1823,7 +1857,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6503af7917fea18ffef8f7e8553fb8dff89e2e6837e94e09dd7fb069c82d62c" dependencies = [ "bytes", - "rkyv", + "rkyv 0.8.12", "serde", "simdutf8", ] @@ -1983,6 +2017,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.1.31" @@ -3730,7 +3770,7 @@ dependencies = [ "serde", "solana-keypair", "solana-pubkey", - "strum", + "strum 0.24.1", "thiserror 1.0.69", "toml 0.8.23", "url 2.5.4", @@ -3778,8 +3818,7 @@ 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" dependencies = [ "bincode", "borsh 1.5.7", @@ -3790,9 +3829,12 @@ dependencies = [ "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", ] @@ -5045,13 +5087,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]] @@ -5178,13 +5240,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]] @@ -5454,6 +5522,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" @@ -5538,27 +5615,56 @@ dependencies = [ [[package]] name = "rkyv" -version = "0.8.11" +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.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f5c3e5da784cd8c69d32cdc84673f3204536ca56e1fa01be31a74b92c932ac" +checksum = "35a640b26f007713818e9a9b65d34da1cf58538207b052916a83d80e43f3ffa4" dependencies = [ "bytes", "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.12", "tinyvec", "uuid", ] [[package]] name = "rkyv_derive" -version = "0.8.11" +version = "0.7.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4270433626cffc9c4c1d3707dd681f2a2718d3d7b09ad754bec137acecda8d22" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rkyv_derive" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd83f5f173ff41e00337d97f6572e416d022ef8a19f371817259ae960324c482" dependencies = [ "proc-macro2", "quote", @@ -5859,6 +5965,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" @@ -7481,8 +7593,8 @@ dependencies = [ "spl-token", "spl-token-2022 7.0.0", "static_assertions", - "strum", - "strum_macros", + "strum 0.24.1", + "strum_macros 0.24.3", "tar", "tempfile", "thiserror 2.0.12", @@ -8479,8 +8591,8 @@ dependencies = [ "solana-vote", "solana-vote-program", "static_assertions", - "strum", - "strum_macros", + "strum 0.24.1", + "strum_macros 0.24.3", "symlink", "tar", "tempfile", @@ -10144,7 +10256,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]] @@ -10160,6 +10281,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" @@ -10253,6 +10386,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 = "tar" version = "0.4.44" @@ -11794,6 +11933,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" diff --git a/Cargo.toml b/Cargo.toml index b7c9686d8..e7f9ae540 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -108,10 +108,8 @@ magicblock-config = { path = "./magicblock-config" } magicblock-config-helpers = { path = "./magicblock-config-helpers" } magicblock-config-macro = { path = "./magicblock-config-macro" } magicblock-core = { path = "./magicblock-core" } -magicblock-delegation-program = { git = "https://github.com/magicblock-labs/delegation-program.git", rev = "aa1de56d90c", features = [ - "no-entrypoint", -] } magicblock-aperture = { path = "./magicblock-aperture" } +magicblock-delegation-program = { path="../delegation-program", features = ["no-entrypoint"] } magicblock-geyser-plugin = { path = "./magicblock-geyser-plugin" } magicblock-ledger = { path = "./magicblock-ledger" } magicblock-metrics = { path = "./magicblock-metrics" } diff --git a/magicblock-accounts/src/scheduled_commits_processor.rs b/magicblock-accounts/src/scheduled_commits_processor.rs index 83a03c064..c9beb8336 100644 --- a/magicblock-accounts/src/scheduled_commits_processor.rs +++ b/magicblock-accounts/src/scheduled_commits_processor.rs @@ -343,6 +343,7 @@ impl ScheduledCommitsProcessorImpl { included_pubkeys: intent_meta.included_pubkeys, excluded_pubkeys: intent_meta.excluded_pubkeys, requested_undelegation: intent_meta.requested_undelegation, + commit_diff: intent_meta.commit_diff, } } } @@ -412,6 +413,7 @@ struct ScheduledBaseIntentMeta { excluded_pubkeys: Vec, intent_sent_transaction: Transaction, requested_undelegation: bool, + commit_diff: bool, } impl ScheduledBaseIntentMeta { @@ -429,6 +431,7 @@ impl ScheduledBaseIntentMeta { excluded_pubkeys, 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 301db3b63..26d00542d 100644 --- a/magicblock-committor-service/src/tasks/args_task.rs +++ b/magicblock-committor-service/src/tasks/args_task.rs @@ -1,20 +1,33 @@ -use dlp::args::{CallHandlerArgs, CommitStateArgs}; +use dlp::{ + args::{CallHandlerArgs, CommitDiffArgs, CommitStateArgs}, + compute_diff, +}; +use solana_account::ReadableAccount; use solana_pubkey::Pubkey; -use solana_sdk::instruction::{AccountMeta, Instruction}; +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), @@ -58,6 +71,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, @@ -106,6 +168,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), @@ -132,6 +196,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, @@ -146,6 +211,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, @@ -158,6 +226,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 a31e52c4a..f2fae6c86 100644 --- a/magicblock-committor-service/src/tasks/mod.rs +++ b/magicblock-committor-service/src/tasks/mod.rs @@ -53,6 +53,8 @@ pub enum TaskStrategy { pub trait BaseTask: Send + Sync + DynClone { /// 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 36c7315eb..e50f61851 100644 --- a/magicblock-committor-service/src/tasks/task_builder.rs +++ b/magicblock-committor-service/src/tasks/task_builder.rs @@ -47,25 +47,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() @@ -89,11 +93,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 }) @@ -134,6 +143,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 6ff29bee2..03d85d8a9 100644 --- a/magicblock-magic-program-api/src/instruction.rs +++ b/magicblock-magic-program-api/src/instruction.rs @@ -107,6 +107,8 @@ pub enum MagicBlockInstruction { /// # Account references /// - **0.** `[SIGNER]` Validator authority EnableExecutableCheck, + + ScheduleCommitDiffAndUndelegate, } impl MagicBlockInstruction { diff --git a/programs/magicblock/src/magic_scheduled_base_intent.rs b/programs/magicblock/src/magic_scheduled_base_intent.rs index 1fb050188..02f91d478 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, @@ -308,6 +322,7 @@ impl BaseAction { } type CommittedAccountRef<'a> = (Pubkey, &'a RefCell); + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct CommittedAccount { pub pubkey: Pubkey, @@ -327,6 +342,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, @@ -463,9 +479,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, @@ -475,6 +496,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, @@ -486,6 +508,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 60cc13486..6d012fb33 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!( EnableExecutableCheck => { process_toggle_executable_check(signers, invoke_context, true) } + 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 bf0662f8a..e0eda9a7c 100644 --- a/programs/magicblock/src/schedule_transactions/process_schedule_commit.rs +++ b/programs/magicblock/src/schedule_transactions/process_schedule_commit.rs @@ -29,6 +29,7 @@ use crate::{ #[derive(Default)] pub(crate) struct ProcessScheduleCommitOptions { pub request_undelegation: bool, + pub request_diff: bool, } pub(crate) fn process_schedule_commit( @@ -243,13 +244,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 7bb293d8a..40fa31367 100644 --- a/programs/magicblock/src/schedule_transactions/process_scheduled_commit_sent.rs +++ b/programs/magicblock/src/schedule_transactions/process_scheduled_commit_sent.rs @@ -26,6 +26,7 @@ pub struct SentCommit { pub included_pubkeys: Vec, pub excluded_pubkeys: Vec, pub requested_undelegation: bool, + pub commit_diff: bool, } /// This is a printable version of the SentCommit struct. @@ -40,6 +41,7 @@ struct SentCommitPrintable { included_pubkeys: String, excluded_pubkeys: String, requested_undelegation: bool, + commit_diff: bool, } impl From for SentCommitPrintable { @@ -67,6 +69,7 @@ impl From for SentCommitPrintable { .collect::>() .join(", "), requested_undelegation: commit.requested_undelegation, + commit_diff: commit.commit_diff, } } } @@ -209,6 +212,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",); + } Ok(()) } @@ -245,6 +251,7 @@ mod tests { included_pubkeys: vec![acc], excluded_pubkeys: Default::default(), requested_undelegation: false, + commit_diff: false, } } diff --git a/test-integration/Cargo.lock b/test-integration/Cargo.lock index 00b1f92e4..9fcab7f7d 100644 --- a/test-integration/Cargo.lock +++ b/test-integration/Cargo.lock @@ -680,6 +680,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" @@ -836,6 +848,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" @@ -1719,22 +1753,20 @@ dependencies = [ [[package]] name = "ephemeral-rollups-sdk" -version = "0.3.4" -source = "git+https://github.com/magicblock-labs/ephemeral-rollups-sdk.git?rev=2d0f16b#2d0f16bf18a8618dcac07a8dc271dd3a30096c7c" +version = "0.3.7" dependencies = [ "borsh 1.5.7", "ephemeral-rollups-sdk-attribute-commit", "ephemeral-rollups-sdk-attribute-delegate", "ephemeral-rollups-sdk-attribute-ephemeral", "magicblock-delegation-program", - "magicblock-magic-program-api 0.2.1", + "magicblock-magic-program-api", "solana-program", ] [[package]] name = "ephemeral-rollups-sdk-attribute-commit" -version = "0.3.4" -source = "git+https://github.com/magicblock-labs/ephemeral-rollups-sdk.git?rev=2d0f16b#2d0f16bf18a8618dcac07a8dc271dd3a30096c7c" +version = "0.3.7" dependencies = [ "quote", "syn 1.0.109", @@ -1742,8 +1774,7 @@ dependencies = [ [[package]] name = "ephemeral-rollups-sdk-attribute-delegate" -version = "0.3.4" -source = "git+https://github.com/magicblock-labs/ephemeral-rollups-sdk.git?rev=2d0f16b#2d0f16bf18a8618dcac07a8dc271dd3a30096c7c" +version = "0.3.7" dependencies = [ "proc-macro2", "quote", @@ -1752,8 +1783,7 @@ dependencies = [ [[package]] name = "ephemeral-rollups-sdk-attribute-ephemeral" -version = "0.3.4" -source = "git+https://github.com/magicblock-labs/ephemeral-rollups-sdk.git?rev=2d0f16b#2d0f16bf18a8618dcac07a8dc271dd3a30096c7c" +version = "0.3.7" dependencies = [ "proc-macro2", "quote", @@ -1844,12 +1874,12 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "faststr" -version = "0.2.31" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6503af7917fea18ffef8f7e8553fb8dff89e2e6837e94e09dd7fb069c82d62c" +checksum = "baec6a0289d7f1fe5665586ef7340af82e3037207bef60f5785e57569776f0c8" dependencies = [ "bytes", - "rkyv", + "rkyv 0.8.12", "serde", "simdutf8", ] @@ -2019,6 +2049,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.1.31" @@ -2653,9 +2689,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" dependencies = [ "bytes", "futures-core", @@ -3462,7 +3498,7 @@ dependencies = [ "magicblock-config", "magicblock-core", "magicblock-ledger", - "magicblock-magic-program-api 0.2.3", + "magicblock-magic-program-api", "magicblock-program", "magicblock-rpc-client", "solana-sdk", @@ -3485,7 +3521,7 @@ dependencies = [ "magicblock-core", "magicblock-delegation-program", "magicblock-ledger", - "magicblock-magic-program-api 0.2.3", + "magicblock-magic-program-api", "magicblock-metrics", "magicblock-processor", "magicblock-program", @@ -3586,7 +3622,7 @@ dependencies = [ "magicblock-core", "magicblock-delegation-program", "magicblock-ledger", - "magicblock-magic-program-api 0.2.3", + "magicblock-magic-program-api", "magicblock-metrics", "magicblock-processor", "magicblock-program", @@ -3619,7 +3655,7 @@ dependencies = [ "lru 0.16.0", "magicblock-core", "magicblock-delegation-program", - "magicblock-magic-program-api 0.2.3", + "magicblock-magic-program-api", "serde_json", "solana-account", "solana-account-decoder", @@ -3699,7 +3735,7 @@ dependencies = [ "serde", "solana-keypair", "solana-pubkey", - "strum", + "strum 0.24.1", "thiserror 1.0.69", "toml 0.8.23", "url 2.5.4", @@ -3727,7 +3763,7 @@ version = "0.2.3" dependencies = [ "bincode", "flume", - "magicblock-magic-program-api 0.2.3", + "magicblock-magic-program-api", "serde", "solana-account", "solana-account-decoder", @@ -3744,8 +3780,7 @@ 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" dependencies = [ "bincode", "borsh 1.5.7", @@ -3756,9 +3791,12 @@ dependencies = [ "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", ] @@ -3793,17 +3831,6 @@ dependencies = [ "tokio-util 0.7.15", ] -[[package]] -name = "magicblock-magic-program-api" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349b26eb6d819328dad699a6c9a26234548d366d9a30e7edf0d296180188ee27" -dependencies = [ - "bincode", - "serde", - "solana-program", -] - [[package]] name = "magicblock-magic-program-api" version = "0.2.3" @@ -3868,7 +3895,7 @@ dependencies = [ "bincode", "lazy_static", "magicblock-core", - "magicblock-magic-program-api 0.2.3", + "magicblock-magic-program-api", "magicblock-metrics", "num-derive", "num-traits", @@ -4121,18 +4148,18 @@ checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" [[package]] name = "munge" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7feb0b48aa0a25f9fe0899482c6e1379ee7a11b24a53073eacdecb9adb6dc60" +checksum = "5e17401f259eba956ca16491461b6e8f72913a0a114e39736ce404410f915a0c" dependencies = [ "munge_macro", ] [[package]] name = "munge_macro" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2e3795a5d2da581a8b252fec6022eee01aea10161a4d1bf237d4cbe47f7e988" +checksum = "4568f25ccbd45ab5d5603dc34318c1ec56b117531781260002151b8530a9f931" dependencies = [ "proc-macro2", "quote", @@ -4844,7 +4871,7 @@ dependencies = [ "bincode", "borsh 1.5.7", "ephemeral-rollups-sdk", - "magicblock-magic-program-api 0.2.3", + "magicblock-magic-program-api", "serde", "solana-program", ] @@ -4868,7 +4895,9 @@ dependencies = [ "borsh 1.5.7", "ephemeral-rollups-sdk", "magicblock-delegation-program", + "rkyv 0.7.45", "solana-program", + "static_assertions", ] [[package]] @@ -4987,18 +5016,38 @@ dependencies = [ [[package]] name = "ptr_meta" -version = "0.3.0" +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.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe9e76f66d3f9606f44e45598d155cb13ecf09f4a28199e48daf8c8fc937ea90" +checksum = "0b9a0cf95a1196af61d4f1cbdab967179516d9a4a4312af1f31948f8f6224a79" dependencies = [ - "ptr_meta_derive", + "ptr_meta_derive 0.3.1", ] [[package]] name = "ptr_meta_derive" -version = "0.3.0" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca414edb151b4c8d125c12566ab0d74dc9cdba36fb80eb7b848c15f495fd32d1" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7347867d0a7e1208d93b46767be83e2b8f978c3dad35f775ac8d8847551d6fe1" dependencies = [ "proc-macro2", "quote", @@ -5118,13 +5167,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" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf5f7161924b9d1cea0e4cabc97c372cea92b5f927fc13c6bca67157a0ad947" +checksum = "a063ea72381527c2a0561da9c80000ef822bdd7c3241b1cc1b12100e3df081ee" dependencies = [ - "ptr_meta", + "ptr_meta 0.3.1", ] [[package]] @@ -5331,18 +5386,18 @@ dependencies = [ [[package]] name = "ref-cast" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", @@ -5392,9 +5447,18 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rend" -version = "0.5.2" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "rend" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a35e8a6bf28cd121053a66aa2e6a2e3eaffad4a60012179f0e864aa5ffeff215" +checksum = "cadadef317c2f20755a64d7fdc48f9e7178ee6b0e1f7fce33fa60f1d68a276e6" [[package]] name = "reqwest" @@ -5474,27 +5538,56 @@ dependencies = [ [[package]] name = "rkyv" -version = "0.8.11" +version = "0.7.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f5c3e5da784cd8c69d32cdc84673f3204536ca56e1fa01be31a74b92c932ac" +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.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a640b26f007713818e9a9b65d34da1cf58538207b052916a83d80e43f3ffa4" dependencies = [ "bytes", "hashbrown 0.15.4", "indexmap 2.10.0", "munge", - "ptr_meta", + "ptr_meta 0.3.1", "rancor", - "rend", - "rkyv_derive", + "rend 0.5.3", + "rkyv_derive 0.8.12", "tinyvec", "uuid", ] [[package]] name = "rkyv_derive" -version = "0.8.11" +version = "0.7.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4270433626cffc9c4c1d3707dd681f2a2718d3d7b09ad754bec137acecda8d22" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rkyv_derive" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd83f5f173ff41e00337d97f6572e416d022ef8a19f371817259ae960324c482" dependencies = [ "proc-macro2", "quote", @@ -5819,12 +5912,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.2.3", + "magicblock-magic-program-api", "program-schedulecommit", + "rand 0.8.5", "schedulecommit-client", "solana-program", "solana-rpc-client", @@ -5839,7 +5934,7 @@ version = "0.0.0" dependencies = [ "integration-test-tools", "magicblock-core", - "magicblock-magic-program-api 0.2.3", + "magicblock-magic-program-api", "program-schedulecommit", "program-schedulecommit-security", "schedulecommit-client", @@ -5869,6 +5964,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 = "2.11.1" @@ -7508,8 +7609,8 @@ dependencies = [ "spl-token", "spl-token-2022 7.0.0", "static_assertions", - "strum", - "strum_macros", + "strum 0.24.1", + "strum_macros 0.24.3", "tar", "tempfile", "thiserror 2.0.12", @@ -8520,8 +8621,8 @@ dependencies = [ "solana-vote", "solana-vote-program", "static_assertions", - "strum", - "strum_macros", + "strum 0.24.1", + "strum_macros 0.24.3", "symlink", "tar", "tempfile", @@ -9760,9 +9861,9 @@ dependencies = [ [[package]] name = "sonic-rs" -version = "0.5.3" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd1adc42def3cb101f3ebef3cd2d642f9a21072bbcd4ec9423343ccaa6afa596" +checksum = "22540d56ba14521e4878ad436d498518c59698c39a89d5905c694932f0bf7134" dependencies = [ "ahash 0.8.12", "bumpalo", @@ -10215,7 +10316,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]] @@ -10231,6 +10341,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" @@ -10322,6 +10444,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" @@ -11239,9 +11367,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.18.0" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ "js-sys", "wasm-bindgen", @@ -11947,6 +12075,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" diff --git a/test-integration/Cargo.toml b/test-integration/Cargo.toml index 9720dd913..f84f54806 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 d850f923f..00e6468fe 100644 --- a/test-integration/programs/schedulecommit/Cargo.toml +++ b/test-integration/programs/schedulecommit/Cargo.toml @@ -8,6 +8,8 @@ borsh = { workspace = true } ephemeral-rollups-sdk = { workspace = true } solana-program = { workspace = true } magicblock-delegation-program = { 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 57475abed..af4ca61ba 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 a90da8c26..d7f524f05 100644 --- a/test-integration/programs/schedulecommit/src/lib.rs +++ b/test-integration/programs/schedulecommit/src/lib.rs @@ -4,15 +4,22 @@ 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 solana_program::{ account_info::{next_account_info, AccountInfo}, declare_id, entrypoint::{self, ProgramResult}, msg, + program::invoke, program_error::ProgramError, pubkey::Pubkey, + rent::Rent, + system_instruction, + sysvar::Sysvar, }; use crate::{ @@ -24,8 +31,13 @@ use crate::{ }; pub mod api; pub mod magicblock_program; +mod order_book; mod utils; +use order_book::*; + +pub use order_book::{BookUpdate, OrderBookOwned, OrderLevel}; + declare_id!("9hgprgZiRWmy8KkfvUuaVkDGrqo9GzeXMohwq6BazgUY"); #[cfg(not(feature = "no-entrypoint"))] @@ -39,6 +51,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 @@ -105,6 +123,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>( @@ -128,6 +159,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), @@ -147,6 +179,15 @@ pub fn process_instruction<'a>( ) } IncreaseCount => process_increase_count(accounts), + 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) + } } } @@ -160,7 +201,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) @@ -234,6 +275,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 // ----------------- @@ -340,7 +545,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 7de5b1250..bb1bd9086 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 &[account_info.clone()], &[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 de5383e8b..41054406e 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 e2869303a..bf486d3cd 100644 --- a/test-integration/schedulecommit/test-scenarios/tests/01_commits.rs +++ b/test-integration/schedulecommit/test-scenarios/tests/01_commits.rs @@ -43,7 +43,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, @@ -96,7 +97,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 120650a70..6c768362f 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 @@ -4,10 +4,17 @@ use integration_test_tools::{ transactions::send_and_confirm_instructions_with_payer, }; use log::*; -use program_schedulecommit::api::{ - increase_count_instruction, schedule_commit_and_undelegate_cpi_instruction, - schedule_commit_and_undelegate_cpi_with_mod_after_instruction, +use program_schedulecommit::{ + api::{ + increase_count_instruction, + schedule_commit_and_undelegate_cpi_instruction, + 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, }; @@ -45,7 +52,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, @@ -99,6 +107,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, ) -> ( @@ -106,7 +170,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, @@ -176,6 +241,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 f31287102..e9fd9f672 100644 --- a/test-integration/test-tools/src/integration_test_context.rs +++ b/test-integration/test-tools/src/integration_test_context.rs @@ -154,7 +154,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 @@ -163,6 +164,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 f61aa09902ace2a3e0a5ac088a06efdb6616c577 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Sun, 2 Nov 2025 12:44:51 +0530 Subject: [PATCH 02/16] Redesign the implementation of CommitDiff support --- .../src/scheduled_commits_processor.rs | 3 - .../src/tasks/args_task.rs | 106 ++++++++---------- .../src/tasks/task_builder.rs | 54 ++++----- .../src/tasks/task_strategist.rs | 1 + .../tasks/task_visitors/persistor_visitor.rs | 7 +- .../src/instruction.rs | 2 - .../src/magic_scheduled_base_intent.rs | 24 ---- .../magicblock/src/magicblock_processor.rs | 10 -- .../process_schedule_commit.rs | 11 +- .../process_scheduled_commit_sent.rs | 7 -- 10 files changed, 76 insertions(+), 149 deletions(-) diff --git a/magicblock-accounts/src/scheduled_commits_processor.rs b/magicblock-accounts/src/scheduled_commits_processor.rs index c9beb8336..83a03c064 100644 --- a/magicblock-accounts/src/scheduled_commits_processor.rs +++ b/magicblock-accounts/src/scheduled_commits_processor.rs @@ -343,7 +343,6 @@ impl ScheduledCommitsProcessorImpl { included_pubkeys: intent_meta.included_pubkeys, excluded_pubkeys: intent_meta.excluded_pubkeys, requested_undelegation: intent_meta.requested_undelegation, - commit_diff: intent_meta.commit_diff, } } } @@ -413,7 +412,6 @@ struct ScheduledBaseIntentMeta { excluded_pubkeys: Vec, intent_sent_transaction: Transaction, requested_undelegation: bool, - commit_diff: bool, } impl ScheduledBaseIntentMeta { @@ -431,7 +429,6 @@ impl ScheduledBaseIntentMeta { excluded_pubkeys, 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 26d00542d..6f837c1ef 100644 --- a/magicblock-committor-service/src/tasks/args_task.rs +++ b/magicblock-committor-service/src/tasks/args_task.rs @@ -27,7 +27,6 @@ use crate::{ #[derive(Clone)] pub enum ArgsTaskType { Commit(CommitTask), - CommitDiff(CommitTask), Finalize(FinalizeTask), Undelegate(UndelegateTask), // Special action really BaseAction(BaseActionTask), @@ -71,55 +70,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::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, @@ -168,8 +167,6 @@ 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), @@ -196,7 +193,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, @@ -211,9 +207,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, @@ -226,7 +219,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/task_builder.rs b/magicblock-committor-service/src/tasks/task_builder.rs index e50f61851..36c7315eb 100644 --- a/magicblock-committor-service/src/tasks/task_builder.rs +++ b/magicblock-committor-service/src/tasks/task_builder.rs @@ -47,29 +47,25 @@ impl TasksBuilder for TaskBuilderImpl { 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() @@ -93,16 +89,11 @@ 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 = CommitTask { + let task = ArgsTaskType::Commit(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 }) @@ -143,9 +134,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, diff --git a/magicblock-committor-service/src/tasks/task_strategist.rs b/magicblock-committor-service/src/tasks/task_strategist.rs index 406ba1a9d..b02e1e2d0 100644 --- a/magicblock-committor-service/src/tasks/task_strategist.rs +++ b/magicblock-committor-service/src/tasks/task_strategist.rs @@ -157,6 +157,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, 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 03d85d8a9..6ff29bee2 100644 --- a/magicblock-magic-program-api/src/instruction.rs +++ b/magicblock-magic-program-api/src/instruction.rs @@ -107,8 +107,6 @@ pub enum MagicBlockInstruction { /// # Account references /// - **0.** `[SIGNER]` Validator authority EnableExecutableCheck, - - ScheduleCommitDiffAndUndelegate, } impl MagicBlockInstruction { diff --git a/programs/magicblock/src/magic_scheduled_base_intent.rs b/programs/magicblock/src/magic_scheduled_base_intent.rs index 02f91d478..09bead405 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, @@ -342,7 +328,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, @@ -479,14 +464,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, @@ -496,7 +476,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, @@ -508,9 +487,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 6d012fb33..60cc13486 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!( EnableExecutableCheck => { process_toggle_executable_check(signers, invoke_context, true) } - 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 e0eda9a7c..bf0662f8a 100644 --- a/programs/magicblock/src/schedule_transactions/process_schedule_commit.rs +++ b/programs/magicblock/src/schedule_transactions/process_schedule_commit.rs @@ -29,7 +29,6 @@ use crate::{ #[derive(Default)] pub(crate) struct ProcessScheduleCommitOptions { pub request_undelegation: bool, - pub request_diff: bool, } pub(crate) fn process_schedule_commit( @@ -244,19 +243,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 40fa31367..7bb293d8a 100644 --- a/programs/magicblock/src/schedule_transactions/process_scheduled_commit_sent.rs +++ b/programs/magicblock/src/schedule_transactions/process_scheduled_commit_sent.rs @@ -26,7 +26,6 @@ pub struct SentCommit { pub included_pubkeys: Vec, pub excluded_pubkeys: Vec, pub requested_undelegation: bool, - pub commit_diff: bool, } /// This is a printable version of the SentCommit struct. @@ -41,7 +40,6 @@ struct SentCommitPrintable { included_pubkeys: String, excluded_pubkeys: String, requested_undelegation: bool, - commit_diff: bool, } impl From for SentCommitPrintable { @@ -69,7 +67,6 @@ impl From for SentCommitPrintable { .collect::>() .join(", "), requested_undelegation: commit.requested_undelegation, - commit_diff: commit.commit_diff, } } } @@ -212,9 +209,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",); - } Ok(()) } @@ -251,7 +245,6 @@ mod tests { included_pubkeys: vec![acc], excluded_pubkeys: Default::default(), requested_undelegation: false, - commit_diff: false, } } From fbb293485880ee7f7d6c0caf1cbbbf2cfc5e4625 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Sun, 2 Nov 2025 19:12:45 +0530 Subject: [PATCH 03/16] Make it work again --- Cargo.lock | 1 + Cargo.toml | 4 +- .../src/tasks/args_task.rs | 96 ++++--------------- magicblock-committor-service/src/tasks/mod.rs | 76 ++++++++++++++- test-integration/Cargo.lock | 92 ++++++++++++------ test-integration/Cargo.toml | 6 +- .../programs/schedulecommit/src/lib.rs | 10 +- .../client/src/schedule_commit_context.rs | 76 ++++++++------- .../tests/02_commit_and_undelegate.rs | 2 +- .../test-scenarios/tests/utils/mod.rs | 6 +- .../src/integration_test_context.rs | 4 +- .../test-tools/src/scheduled_commits.rs | 2 +- 12 files changed, 214 insertions(+), 161 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a60f9e2b1..912628723 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3819,6 +3819,7 @@ dependencies = [ [[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", diff --git a/Cargo.toml b/Cargo.toml index e7f9ae540..9b0b8e5e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -109,7 +109,9 @@ magicblock-config-helpers = { path = "./magicblock-config-helpers" } magicblock-config-macro = { path = "./magicblock-config-macro" } magicblock-core = { path = "./magicblock-core" } magicblock-aperture = { path = "./magicblock-aperture" } -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-geyser-plugin = { path = "./magicblock-geyser-plugin" } magicblock-ledger = { path = "./magicblock-ledger" } 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 6f837c1ef..b5f96abf8 100644 --- a/magicblock-committor-service/src/tasks/args_task.rs +++ b/magicblock-committor-service/src/tasks/args_task.rs @@ -1,26 +1,14 @@ -use dlp::{ - args::{CallHandlerArgs, CommitDiffArgs, CommitStateArgs}, - compute_diff, -}; -use solana_account::ReadableAccount; +use dlp::args::CallHandlerArgs; use solana_pubkey::Pubkey; -use solana_rpc_client::rpc_client::RpcClient; -use solana_sdk::{ - commitment_config::CommitmentConfig, - instruction::{AccountMeta, Instruction}, -}; +use solana_sdk::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 @@ -57,68 +45,12 @@ 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, - ) + if value.is_commit_diff() { + value.create_commit_diff_ix(validator) + } else { + value.create_commit_state_ix(validator) + } } - // 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, @@ -162,6 +94,10 @@ impl BaseTask for ArgsTask { self: Box, ) -> Result, Box> { match self.task_type { + ArgsTaskType::Commit(ref value) if value.is_commit_diff() => { + // We do not currently support executing CommitDiff as BufferTask + Err(self) + } ArgsTaskType::Commit(value) => { Ok(Box::new(BufferTask::new_preparation_required( BufferTaskType::Commit(value), diff --git a/magicblock-committor-service/src/tasks/mod.rs b/magicblock-committor-service/src/tasks/mod.rs index f2fae6c86..3782cb7dc 100644 --- a/magicblock-committor-service/src/tasks/mod.rs +++ b/magicblock-committor-service/src/tasks/mod.rs @@ -1,3 +1,7 @@ +use dlp::{ + args::{CommitDiffArgs, CommitStateArgs}, + compute_diff, +}; use dyn_clone::DynClone; use magicblock_committor_program::{ instruction_builder::{ @@ -13,11 +17,17 @@ use magicblock_committor_program::{ use magicblock_program::magic_scheduled_base_intent::{ BaseAction, CommittedAccount, }; +use solana_account::ReadableAccount; use solana_pubkey::Pubkey; -use solana_sdk::instruction::Instruction; +use solana_rpc_client::rpc_client::RpcClient; +use solana_sdk::{ + commitment_config::CommitmentConfig, instruction::Instruction, +}; use thiserror::Error; -use crate::tasks::visitor::Visitor; +use crate::{ + config::ChainConfig, tasks::visitor::Visitor, ComputeBudgetConfig, +}; pub mod args_task; pub mod buffer_task; @@ -106,6 +116,68 @@ pub struct CommitTask { pub committed_account: CommittedAccount, } +impl CommitTask { + const COMMIT_STATE_SIZE_THRESHOLD: usize = 200; + + pub fn is_commit_diff(&self) -> bool { + self.committed_account.account.data.len() + > CommitTask::COMMIT_STATE_SIZE_THRESHOLD + } + + pub 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, + ) + } + pub fn create_commit_diff_ix(&self, validator: &Pubkey) -> Instruction { + 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(&self.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: {}", self.commit_id, e); + return self.create_commit_state_ix(validator); + } + }; + + let args = CommitDiffArgs { + nonce: self.commit_id, + lamports: self.committed_account.account.lamports, + diff: compute_diff( + 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 UndelegateTask { pub delegated_account: Pubkey, diff --git a/test-integration/Cargo.lock b/test-integration/Cargo.lock index 9fcab7f7d..19fe098e8 100644 --- a/test-integration/Cargo.lock +++ b/test-integration/Cargo.lock @@ -1753,20 +1753,22 @@ dependencies = [ [[package]] name = "ephemeral-rollups-sdk" -version = "0.3.7" +version = "0.3.4" +source = "git+https://github.com/magicblock-labs/ephemeral-rollups-sdk.git?rev=2d0f16b#2d0f16bf18a8618dcac07a8dc271dd3a30096c7c" dependencies = [ "borsh 1.5.7", "ephemeral-rollups-sdk-attribute-commit", "ephemeral-rollups-sdk-attribute-delegate", "ephemeral-rollups-sdk-attribute-ephemeral", - "magicblock-delegation-program", - "magicblock-magic-program-api", + "magicblock-delegation-program 1.1.0", + "magicblock-magic-program-api 0.2.1", "solana-program", ] [[package]] name = "ephemeral-rollups-sdk-attribute-commit" -version = "0.3.7" +version = "0.3.4" +source = "git+https://github.com/magicblock-labs/ephemeral-rollups-sdk.git?rev=2d0f16b#2d0f16bf18a8618dcac07a8dc271dd3a30096c7c" dependencies = [ "quote", "syn 1.0.109", @@ -1774,7 +1776,8 @@ dependencies = [ [[package]] name = "ephemeral-rollups-sdk-attribute-delegate" -version = "0.3.7" +version = "0.3.4" +source = "git+https://github.com/magicblock-labs/ephemeral-rollups-sdk.git?rev=2d0f16b#2d0f16bf18a8618dcac07a8dc271dd3a30096c7c" dependencies = [ "proc-macro2", "quote", @@ -1783,7 +1786,8 @@ dependencies = [ [[package]] name = "ephemeral-rollups-sdk-attribute-ephemeral" -version = "0.3.7" +version = "0.3.4" +source = "git+https://github.com/magicblock-labs/ephemeral-rollups-sdk.git?rev=2d0f16b#2d0f16bf18a8618dcac07a8dc271dd3a30096c7c" dependencies = [ "proc-macro2", "quote", @@ -2959,7 +2963,7 @@ dependencies = [ "log", "magicblock-config", "magicblock-core", - "magicblock-delegation-program", + "magicblock-delegation-program 1.1.2", "random-port", "rayon", "serde", @@ -3498,7 +3502,7 @@ dependencies = [ "magicblock-config", "magicblock-core", "magicblock-ledger", - "magicblock-magic-program-api", + "magicblock-magic-program-api 0.2.3", "magicblock-program", "magicblock-rpc-client", "solana-sdk", @@ -3519,9 +3523,9 @@ dependencies = [ "magicblock-chainlink", "magicblock-committor-service", "magicblock-core", - "magicblock-delegation-program", + "magicblock-delegation-program 1.1.2", "magicblock-ledger", - "magicblock-magic-program-api", + "magicblock-magic-program-api 0.2.3", "magicblock-metrics", "magicblock-processor", "magicblock-program", @@ -3620,9 +3624,9 @@ dependencies = [ "magicblock-committor-service", "magicblock-config", "magicblock-core", - "magicblock-delegation-program", + "magicblock-delegation-program 1.1.2", "magicblock-ledger", - "magicblock-magic-program-api", + "magicblock-magic-program-api 0.2.3", "magicblock-metrics", "magicblock-processor", "magicblock-program", @@ -3654,8 +3658,8 @@ dependencies = [ "log", "lru 0.16.0", "magicblock-core", - "magicblock-delegation-program", - "magicblock-magic-program-api", + "magicblock-delegation-program 1.1.2", + "magicblock-magic-program-api 0.2.3", "serde_json", "solana-account", "solana-account-decoder", @@ -3703,7 +3707,7 @@ dependencies = [ "log", "lru 0.16.0", "magicblock-committor-program", - "magicblock-delegation-program", + "magicblock-delegation-program 1.1.2", "magicblock-metrics", "magicblock-program", "magicblock-rpc-client", @@ -3763,7 +3767,7 @@ version = "0.2.3" dependencies = [ "bincode", "flume", - "magicblock-magic-program-api", + "magicblock-magic-program-api 0.2.3", "serde", "solana-account", "solana-account-decoder", @@ -3778,9 +3782,30 @@ dependencies = [ "tokio", ] +[[package]] +name = "magicblock-delegation-program" +version = "1.1.0" +source = "git+https://github.com/magicblock-labs/delegation-program.git?rev=aa1de56d90c#aa1de56d90c8a242377accd59899f272f0131f8c" +dependencies = [ + "bincode", + "borsh 1.5.7", + "bytemuck", + "num_enum", + "paste", + "pinocchio", + "pinocchio-log", + "pinocchio-pubkey", + "pinocchio-system", + "solana-curve25519", + "solana-program", + "solana-security-txt", + "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", @@ -3831,6 +3856,17 @@ dependencies = [ "tokio-util 0.7.15", ] +[[package]] +name = "magicblock-magic-program-api" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349b26eb6d819328dad699a6c9a26234548d366d9a30e7edf0d296180188ee27" +dependencies = [ + "bincode", + "serde", + "solana-program", +] + [[package]] name = "magicblock-magic-program-api" version = "0.2.3" @@ -3895,7 +3931,7 @@ dependencies = [ "bincode", "lazy_static", "magicblock-core", - "magicblock-magic-program-api", + "magicblock-magic-program-api 0.2.3", "magicblock-metrics", "num-derive", "num-traits", @@ -3969,7 +4005,7 @@ dependencies = [ "anyhow", "log", "magicblock-config", - "magicblock-delegation-program", + "magicblock-delegation-program 1.1.2", "magicblock-program", "magicblock-rpc-client", "solana-rpc-client", @@ -4871,7 +4907,7 @@ dependencies = [ "bincode", "borsh 1.5.7", "ephemeral-rollups-sdk", - "magicblock-magic-program-api", + "magicblock-magic-program-api 0.2.3", "serde", "solana-program", ] @@ -4894,7 +4930,7 @@ version = "0.0.0" dependencies = [ "borsh 1.5.7", "ephemeral-rollups-sdk", - "magicblock-delegation-program", + "magicblock-delegation-program 1.1.2", "rkyv 0.7.45", "solana-program", "static_assertions", @@ -5875,7 +5911,7 @@ dependencies = [ "integration-test-tools", "log", "magicblock-core", - "magicblock-delegation-program", + "magicblock-delegation-program 1.1.2", "program-schedulecommit", "solana-program", "solana-rpc-client", @@ -5893,7 +5929,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", @@ -5917,7 +5953,7 @@ dependencies = [ "integration-test-tools", "log", "magicblock-core", - "magicblock-magic-program-api", + "magicblock-magic-program-api 0.2.3", "program-schedulecommit", "rand 0.8.5", "schedulecommit-client", @@ -5934,7 +5970,7 @@ version = "0.0.0" dependencies = [ "integration-test-tools", "magicblock-core", - "magicblock-magic-program-api", + "magicblock-magic-program-api 0.2.3", "program-schedulecommit", "program-schedulecommit-security", "schedulecommit-client", @@ -10548,7 +10584,7 @@ dependencies = [ "integration-test-tools", "log", "magicblock-chainlink", - "magicblock-delegation-program", + "magicblock-delegation-program 1.1.2", "program-flexi-counter", "program-mini", "solana-account", @@ -10630,7 +10666,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", @@ -10651,7 +10687,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", @@ -10691,7 +10727,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", diff --git a/test-integration/Cargo.toml b/test-integration/Cargo.toml index f84f54806..ae92124ce 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/src/lib.rs b/test-integration/programs/schedulecommit/src/lib.rs index d7f524f05..9e20bcb05 100644 --- a/test-integration/programs/schedulecommit/src/lib.rs +++ b/test-integration/programs/schedulecommit/src/lib.rs @@ -4,10 +4,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 solana_program::{ account_info::{next_account_info, AccountInfo}, @@ -35,7 +32,6 @@ mod order_book; mod utils; use order_book::*; - pub use order_book::{BookUpdate, OrderBookOwned, OrderLevel}; declare_id!("9hgprgZiRWmy8KkfvUuaVkDGrqo9GzeXMohwq6BazgUY"); @@ -429,7 +425,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, @@ -545,7 +541,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/schedulecommit/client/src/schedule_commit_context.rs b/test-integration/schedulecommit/client/src/schedule_commit_context.rs index 41054406e..8550d238f 100644 --- a/test-integration/schedulecommit/client/src/schedule_commit_context.rs +++ b/test-integration/schedulecommit/client/src/schedule_commit_context.rs @@ -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/test-scenarios/tests/02_commit_and_undelegate.rs b/test-integration/schedulecommit/test-scenarios/tests/02_commit_and_undelegate.rs index 6c768362f..59d6b3065 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 @@ -250,7 +250,7 @@ fn test_committing_and_undelegating_huge_order_book_account() { Rng, }; let rng_seed = OsRng.next_u64(); - println!("Use {rng_seed} as seed to random generator"); + println!("Important: use {rng_seed} as seed to regenerate the random inputs in case of test failure"); let mut random = StdRng::seed_from_u64(rng_seed); let mut update = BookUpdate::default(); update.bids.extend((0..random.gen_range(5..10)).map(|_| { 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/test-tools/src/integration_test_context.rs b/test-integration/test-tools/src/integration_test_context.rs index e9fd9f672..95e09676e 100644 --- a/test-integration/test-tools/src/integration_test_context.rs +++ b/test-integration/test-tools/src/integration_test_context.rs @@ -154,8 +154,8 @@ 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 + .expect(&format!("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 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 763d69e59494405bd7d33824b22c4b029dea14d6 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Mon, 3 Nov 2025 23:35:52 +0530 Subject: [PATCH 04/16] Make unit-tests work again --- .../src/tasks/args_task.rs | 8 +- magicblock-committor-service/src/tasks/mod.rs | 108 ++++++++++++------ .../src/tasks/task_builder.rs | 6 +- .../src/tasks/task_strategist.rs | 14 ++- .../schedulecommit-security/src/lib.rs | 3 +- .../programs/schedulecommit/src/order_book.rs | 2 +- 6 files changed, 85 insertions(+), 56 deletions(-) diff --git a/magicblock-committor-service/src/tasks/args_task.rs b/magicblock-committor-service/src/tasks/args_task.rs index b5f96abf8..2cb7ffd18 100644 --- a/magicblock-committor-service/src/tasks/args_task.rs +++ b/magicblock-committor-service/src/tasks/args_task.rs @@ -44,13 +44,7 @@ impl ArgsTask { impl BaseTask for ArgsTask { fn instruction(&self, validator: &Pubkey) -> Instruction { match &self.task_type { - ArgsTaskType::Commit(value) => { - if value.is_commit_diff() { - value.create_commit_diff_ix(validator) - } else { - value.create_commit_state_ix(validator) - } - } + ArgsTaskType::Commit(value) => value.create_commit_ix(validator), ArgsTaskType::Finalize(value) => { dlp::instruction_builder::finalize( *validator, diff --git a/magicblock-committor-service/src/tasks/mod.rs b/magicblock-committor-service/src/tasks/mod.rs index 3782cb7dc..35596dedc 100644 --- a/magicblock-committor-service/src/tasks/mod.rs +++ b/magicblock-committor-service/src/tasks/mod.rs @@ -17,7 +17,7 @@ use magicblock_committor_program::{ use magicblock_program::magic_scheduled_base_intent::{ BaseAction, CommittedAccount, }; -use solana_account::ReadableAccount; +use solana_account::{Account, ReadableAccount}; use solana_pubkey::Pubkey; use solana_rpc_client::rpc_client::RpcClient; use solana_sdk::{ @@ -114,23 +114,68 @@ pub struct CommitTask { pub commit_id: u64, pub allow_undelegation: bool, pub committed_account: CommittedAccount, + fetched_account: Option, } impl CommitTask { const COMMIT_STATE_SIZE_THRESHOLD: usize = 200; + pub fn new( + commit_id: u64, + allow_undelegation: bool, + committed_account: CommittedAccount, + ) -> Self { + 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 fetched_account = if committed_account.account.data.len() + > CommitTask::COMMIT_STATE_SIZE_THRESHOLD + { + rpc_client.get_account(&committed_account.pubkey).ok() + } else { + None + }; + + Self { + commit_id, + allow_undelegation, + committed_account, + fetched_account, + } + } + + // TODO (snawaz): it is infinitely bad implementation + // as it's making a network call, but we'll fix it soon once + // we start using caching and fetched accounts. pub fn is_commit_diff(&self) -> bool { self.committed_account.account.data.len() > CommitTask::COMMIT_STATE_SIZE_THRESHOLD + && self.fetched_account.is_some() } - pub fn create_commit_state_ix(&self, validator: &Pubkey) -> Instruction { + pub fn create_commit_ix(&self, validator: &Pubkey) -> Instruction { + if let Some(fetched_account) = self.fetched_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, }; + println!("create_commit_state_ix, data: {}", args.data.len()); dlp::instruction_builder::commit_state( *validator, self.committed_account.pubkey, @@ -138,37 +183,24 @@ impl CommitTask { args, ) } - pub fn create_commit_diff_ix(&self, validator: &Pubkey) -> Instruction { - 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(&self.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: {}", self.commit_id, e); - return self.create_commit_state_ix(validator); - } - }; + fn create_commit_diff_ix( + &self, + validator: &Pubkey, + fetched_account: &Account, + ) -> Instruction { let args = CommitDiffArgs { nonce: self.commit_id, lamports: self.committed_account.account.lamports, diff: compute_diff( - account.data(), + fetched_account.data(), self.committed_account.account.data(), ) .to_vec(), allow_undelegation: self.allow_undelegation, }; + println!("create_commit_diff_ix, diff: {}", args.diff.len()); + dlp::instruction_builder::commit_diff( *validator, self.committed_account.pubkey, @@ -382,10 +414,10 @@ mod serialization_safety_test { let validator = Pubkey::new_unique(); // Test Commit variant - let commit_task: ArgsTask = ArgsTaskType::Commit(CommitTask { - commit_id: 123, - allow_undelegation: true, - committed_account: CommittedAccount { + let commit_task: ArgsTask = ArgsTaskType::Commit(CommitTask::new( + 123, + true, + CommittedAccount { pubkey: Pubkey::new_unique(), account: Account { lamports: 1000, @@ -395,7 +427,7 @@ mod serialization_safety_test { rent_epoch: 0, }, }, - }) + )) .into(); assert_serializable(&commit_task.instruction(&validator)); @@ -442,10 +474,10 @@ mod serialization_safety_test { let validator = Pubkey::new_unique(); let buffer_task = BufferTask::new_preparation_required( - BufferTaskType::Commit(CommitTask { - commit_id: 456, - allow_undelegation: false, - committed_account: CommittedAccount { + BufferTaskType::Commit(CommitTask::new( + 456, + false, + CommittedAccount { pubkey: Pubkey::new_unique(), account: Account { lamports: 2000, @@ -455,7 +487,7 @@ mod serialization_safety_test { rent_epoch: 0, }, }, - }), + )), ); assert_serializable(&buffer_task.instruction(&validator)); } @@ -467,10 +499,10 @@ mod serialization_safety_test { // Test BufferTask preparation let buffer_task = BufferTask::new_preparation_required( - BufferTaskType::Commit(CommitTask { - commit_id: 789, - allow_undelegation: true, - committed_account: CommittedAccount { + BufferTaskType::Commit(CommitTask::new( + 789, + true, + CommittedAccount { pubkey: Pubkey::new_unique(), account: Account { lamports: 3000, @@ -480,7 +512,7 @@ mod serialization_safety_test { rent_epoch: 0, }, }, - }), + )), ); let PreparationState::Required(preparation_task) = diff --git a/magicblock-committor-service/src/tasks/task_builder.rs b/magicblock-committor-service/src/tasks/task_builder.rs index 36c7315eb..8268a64a4 100644 --- a/magicblock-committor-service/src/tasks/task_builder.rs +++ b/magicblock-committor-service/src/tasks/task_builder.rs @@ -89,11 +89,11 @@ 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 = ArgsTaskType::Commit(CommitTask::new( commit_id, allow_undelegation, - committed_account: account.clone(), - }); + account.clone(), + )); Box::new(ArgsTask::new(task)) as Box }) diff --git a/magicblock-committor-service/src/tasks/task_strategist.rs b/magicblock-committor-service/src/tasks/task_strategist.rs index b02e1e2d0..b74a20ddd 100644 --- a/magicblock-committor-service/src/tasks/task_strategist.rs +++ b/magicblock-committor-service/src/tasks/task_strategist.rs @@ -95,6 +95,7 @@ impl TaskStrategist { lookup_tables_keys, }) } else { + println!("snawaz: inside build_strategy"); Err(TaskStrategistError::FailedToFitError) } } @@ -165,7 +166,10 @@ impl TaskStrategist { &[], ) { Ok(tx) => Ok(serialize_and_encode_base64(&tx).len()), - Err(TaskStrategistError::FailedToFitError) => Ok(usize::MAX), + Err(TaskStrategistError::FailedToFitError) => { + println!("snawaz: inside optimize_strategy"); + Ok(usize::MAX) + } Err(TaskStrategistError::SignerError(err)) => Err(err), } }; @@ -264,10 +268,10 @@ 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 { + ArgsTask::new(ArgsTaskType::Commit(CommitTask::new( commit_id, - allow_undelegation: false, - committed_account: CommittedAccount { + false, + CommittedAccount { pubkey: Pubkey::new_unique(), account: Account { lamports: 1000, @@ -277,7 +281,7 @@ mod tests { rent_epoch: 0, }, }, - })) + ))) } // Helper to create a Base action task 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/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, ) } From 5afda80098871309bdf5cab04fae4fe9afa858c7 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Tue, 4 Nov 2025 00:41:19 +0530 Subject: [PATCH 05/16] fix ci lint etc --- .../client/src/schedule_commit_context.rs | 2 +- .../tests/03_commits_fee_payer.rs | 135 ------------------ .../test-security/tests/01_invocations.rs | 7 +- .../test-committor-service/tests/common.rs | 10 +- .../tests/test_transaction_preparator.rs | 42 +++--- .../src/integration_test_context.rs | 5 +- 6 files changed, 32 insertions(+), 169 deletions(-) delete mode 100644 test-integration/schedulecommit/test-scenarios/tests/03_commits_fee_payer.rs diff --git a/test-integration/schedulecommit/client/src/schedule_commit_context.rs b/test-integration/schedulecommit/client/src/schedule_commit_context.rs index 8550d238f..25dbbbe7f 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) 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 deleted file mode 100644 index b7118b26a..000000000 --- a/test-integration/schedulecommit/test-scenarios/tests/03_commits_fee_payer.rs +++ /dev/null @@ -1,135 +0,0 @@ -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-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..daf9a35f1 100644 --- a/test-integration/test-committor-service/tests/common.rs +++ b/test-integration/test-committor-service/tests/common.rs @@ -149,10 +149,10 @@ pub fn generate_random_bytes(length: usize) -> Vec { #[allow(dead_code)] pub 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 +162,7 @@ pub fn create_commit_task(data: &[u8]) -> CommitTask { rent_epoch: 0, }, }, - } + ) } #[allow(dead_code)] 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..033aa072e 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,11 @@ 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(), + )))) as Box, Box::new(ArgsTask::new(ArgsTaskType::Finalize(FinalizeTask { delegated_account: committed_account.pubkey, }))), @@ -89,21 +89,18 @@ 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()), + )); // 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(), + )))) as Box, // account 2 Box::new(buffer_commit_task), // finalize account 1 @@ -186,13 +183,10 @@ 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()), + )); let tasks = vec![ // commit account Box::new(buffer_commit_task.clone()) as Box, diff --git a/test-integration/test-tools/src/integration_test_context.rs b/test-integration/test-tools/src/integration_test_context.rs index 95e09676e..d2cad683e 100644 --- a/test-integration/test-tools/src/integration_test_context.rs +++ b/test-integration/test-tools/src/integration_test_context.rs @@ -154,8 +154,9 @@ impl IntegrationTestContext { rpc_client: Option<&RpcClient>, label: &str, ) -> Option> { - let rpc_client = rpc_client - .expect(&format!("rpc_client for [{label}] 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 From ddd15cf99373e9c66049c229676fce444eaab189 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Tue, 4 Nov 2025 13:03:46 +0530 Subject: [PATCH 06/16] Use #[tokio::test(flavor = "multi_thread", worker_threads = 2)] --- magicblock-committor-service/src/tasks/mod.rs | 49 ++++++++++--------- .../tests/test_delivery_preparator.rs | 18 +++++-- 2 files changed, 39 insertions(+), 28 deletions(-) diff --git a/magicblock-committor-service/src/tasks/mod.rs b/magicblock-committor-service/src/tasks/mod.rs index 35596dedc..34b9545ed 100644 --- a/magicblock-committor-service/src/tasks/mod.rs +++ b/magicblock-committor-service/src/tasks/mod.rs @@ -19,15 +19,10 @@ use magicblock_program::magic_scheduled_base_intent::{ }; use solana_account::{Account, ReadableAccount}; use solana_pubkey::Pubkey; -use solana_rpc_client::rpc_client::RpcClient; -use solana_sdk::{ - commitment_config::CommitmentConfig, instruction::Instruction, -}; +use solana_sdk::instruction::Instruction; use thiserror::Error; -use crate::{ - config::ChainConfig, tasks::visitor::Visitor, ComputeBudgetConfig, -}; +use crate::tasks::visitor::Visitor; pub mod args_task; pub mod buffer_task; @@ -114,7 +109,7 @@ pub struct CommitTask { pub commit_id: u64, pub allow_undelegation: bool, pub committed_account: CommittedAccount, - fetched_account: Option, + base_account: Option, } impl CommitTask { @@ -125,43 +120,49 @@ impl CommitTask { allow_undelegation: bool, committed_account: CommittedAccount, ) -> Self { - 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 fetched_account = if committed_account.account.data.len() > CommitTask::COMMIT_STATE_SIZE_THRESHOLD { + // TODO (snawaz): it is the most ugliest piece of code as it is making network call, + // and I'll soon fix it in a separate PR that will use caching of base-accounts. + use solana_rpc_client::rpc_client::RpcClient; + use solana_sdk::commitment_config::CommitmentConfig; + + use crate::{config::ChainConfig, ComputeBudgetConfig}; + + 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, + }, + ); + rpc_client.get_account(&committed_account.pubkey).ok() } else { None }; + println!("fetched_account: {:#?}", fetched_account); + Self { commit_id, allow_undelegation, committed_account, - fetched_account, + base_account: fetched_account, } } - // TODO (snawaz): it is infinitely bad implementation - // as it's making a network call, but we'll fix it soon once - // we start using caching and fetched accounts. pub fn is_commit_diff(&self) -> bool { self.committed_account.account.data.len() > CommitTask::COMMIT_STATE_SIZE_THRESHOLD - && self.fetched_account.is_some() + && self.base_account.is_some() } pub fn create_commit_ix(&self, validator: &Pubkey) -> Instruction { - if let Some(fetched_account) = self.fetched_account.as_ref() { + 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) 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..724859c62 100644 --- a/test-integration/test-committor-service/tests/test_delivery_preparator.rs +++ b/test-integration/test-committor-service/tests/test_delivery_preparator.rs @@ -15,9 +15,13 @@ use crate::common::{create_commit_task, generate_random_bytes, TestFixture}; mod common; -#[tokio::test] +// TODO (snawaz): use #[tokio::test] once CommitTask::new() stops using blocking RpcClient +// # see the PR #575 for more context. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_prepare_10kb_buffer() { + println!("TestFixture::new()"); let fixture = TestFixture::new().await; + println!("TestFixture::new() done"); let preparator = fixture.create_delivery_preparator(); let data = generate_random_bytes(10 * 1024); @@ -76,7 +80,9 @@ async fn test_prepare_10kb_buffer() { ); } -#[tokio::test] +// TODO (snawaz): use #[tokio::test] once CommitTask::new() stops using blocking RpcClient +// # see the PR #575 for more context. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_prepare_multiple_buffers() { let fixture = TestFixture::new().await; let preparator = fixture.create_delivery_preparator(); @@ -206,7 +212,9 @@ async fn test_lookup_tables() { } } -#[tokio::test] +// TODO (snawaz): use #[tokio::test] once CommitTask::new() stops using blocking RpcClient +// # see the PR #575 for more context. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_already_initialized_error_handled() { let fixture = TestFixture::new().await; let preparator = fixture.create_delivery_preparator(); @@ -288,7 +296,9 @@ async fn test_already_initialized_error_handled() { assert_eq!(account.data.as_slice(), data, "Unexpected account data"); } -#[tokio::test] +// TODO (snawaz): use #[tokio::test] once CommitTask::new() stops using blocking RpcClient +// # see the PR #575 for more context. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_prepare_cleanup_and_reprepare_mixed_tasks() { use borsh::BorshDeserialize; From 4dc563bf08dbd640402e7e2e25c325985831837a Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Tue, 4 Nov 2025 14:58:41 +0530 Subject: [PATCH 07/16] Update schedulecommit/elfs/dlp.so to use latest version of DLP --- test-integration/schedulecommit/elfs/dlp.so | Bin 341272 -> 347672 bytes .../tests/test_ix_commit_local.rs | 6 ++++-- .../tests/utils/transactions.rs | 12 +++++++++++- 3 files changed, 15 insertions(+), 3 deletions(-) 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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 Date: Tue, 4 Nov 2025 18:07:03 +0530 Subject: [PATCH 08/16] Use CommitStrategy::Args in place of CommitStrategy::FromBuffer for many tests --- .../tests/test_ix_commit_local.rs | 132 ++++++++++++------ 1 file changed, 86 insertions(+), 46 deletions(-) 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 f9296889d..371977577 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 @@ -61,34 +61,47 @@ fn expect_strategies( // ----------------- // Single Account Commits // ----------------- -#[tokio::test] + +// TODO (snawaz): use #[tokio::test] once CommitTask::new() stops using blocking RpcClient +// # see the PR #575 for more context. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_ix_commit_single_account_100_bytes() { commit_single_account(100, CommitStrategy::Args, false).await; } -#[tokio::test] +// TODO (snawaz): use #[tokio::test] once CommitTask::new() stops using blocking RpcClient +// # see the PR #575 for more context. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_ix_commit_single_account_100_bytes_and_undelegate() { commit_single_account(100, CommitStrategy::Args, true).await; } -#[tokio::test] +// TODO (snawaz): use #[tokio::test] once CommitTask::new() stops using blocking RpcClient +// # see the PR #575 for more context. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] 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] +// TODO (snawaz): use #[tokio::test] once CommitTask::new() stops using blocking RpcClient +// # see the PR #575 for more context. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] 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] +// TODO (snawaz): use #[tokio::test] once CommitTask::new() stops using blocking RpcClient +// # see the PR #575 for more context. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] 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] +// TODO (snawaz): use #[tokio::test] once CommitTask::new() stops using blocking RpcClient +// # see the PR #575 for more context. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] 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; } async fn commit_single_account( @@ -153,19 +166,24 @@ async fn commit_single_account( // ----------------- // Multiple Account Commits // ----------------- -#[tokio::test] + +// TODO (snawaz): use #[tokio::test] once CommitTask::new() stops using blocking RpcClient +// # see the PR #575 for more context. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_ix_commit_two_accounts_1kb_2kb() { init_logger!(); commit_multiple_accounts( &[1024, 2048], 1, false, - expect_strategies(&[(CommitStrategy::FromBuffer, 2)]), + expect_strategies(&[(CommitStrategy::Args, 2)]), ) .await; } -#[tokio::test] +// TODO (snawaz): use #[tokio::test] once CommitTask::new() stops using blocking RpcClient +// # see the PR #575 for more context. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_ix_commit_two_accounts_512kb() { init_logger!(); commit_multiple_accounts( @@ -177,7 +195,9 @@ async fn test_ix_commit_two_accounts_512kb() { .await; } -#[tokio::test] +// TODO (snawaz): use #[tokio::test] once CommitTask::new() stops using blocking RpcClient +// # see the PR #575 for more context. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_ix_commit_three_accounts_512kb() { init_logger!(); commit_multiple_accounts( @@ -189,7 +209,9 @@ async fn test_ix_commit_three_accounts_512kb() { .await; } -#[tokio::test] +// TODO (snawaz): use #[tokio::test] once CommitTask::new() stops using blocking RpcClient +// # see the PR #575 for more context. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_ix_commit_six_accounts_512kb() { init_logger!(); commit_multiple_accounts( @@ -201,14 +223,16 @@ async fn test_ix_commit_six_accounts_512kb() { .await; } -#[tokio::test] +// TODO (snawaz): use #[tokio::test] once CommitTask::new() stops using blocking RpcClient +// # see the PR #575 for more context. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_ix_commit_four_accounts_1kb_2kb_5kb_10kb_single_bundle() { init_logger!(); commit_multiple_accounts( &[1024, 2 * 1024, 5 * 1024, 10 * 1024], 1, false, - expect_strategies(&[(CommitStrategy::FromBuffer, 4)]), + expect_strategies(&[(CommitStrategy::Args, 4)]), ) .await; } @@ -217,43 +241,46 @@ async fn test_ix_commit_four_accounts_1kb_2kb_5kb_10kb_single_bundle() { // # see the PR #575 for more context. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] 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] +// TODO (snawaz): use #[tokio::test] once CommitTask::new() stops using blocking RpcClient +// # see the PR #575 for more context. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] 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; } -#[tokio::test] +// TODO (snawaz): use #[tokio::test] once CommitTask::new() stops using blocking RpcClient +// # see the PR #575 for more context. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_commit_5_accounts_1kb_bundle_size_3_undelegate_all() { commit_5_accounts_1kb( 3, expect_strategies(&[ // Intent fits in 1 TX only with ALT, see IntentExecutorImpl::try_unite_tasks - (CommitStrategy::FromBufferWithLookupTable, 3), - (CommitStrategy::FromBuffer, 2), + (CommitStrategy::ArgsWithLookupTable, 3), + (CommitStrategy::Args, 2), ]), true, ) .await; } -#[tokio::test] +// TODO (snawaz): use #[tokio::test] once CommitTask::new() stops using blocking RpcClient +// # see the PR #575 for more context. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] 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, @@ -261,12 +288,14 @@ async fn test_commit_5_accounts_1kb_bundle_size_4() { .await; } -#[tokio::test] +// TODO (snawaz): use #[tokio::test] once CommitTask::new() stops using blocking RpcClient +// # see the PR #575 for more context. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] 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, @@ -274,26 +303,29 @@ async fn test_commit_5_accounts_1kb_bundle_size_4_undelegate_all() { .await; } -#[tokio::test] +// TODO (snawaz): use #[tokio::test] once CommitTask::new() stops using blocking RpcClient +// # see the PR #575 for more context. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_commit_5_accounts_1kb_bundle_size_5_undelegate_all() { commit_5_accounts_1kb( 5, - expect_strategies(&[(CommitStrategy::FromBufferWithLookupTable, 5)]), + expect_strategies(&[(CommitStrategy::ArgsWithLookupTable, 5)]), true, ) .await; } -#[tokio::test] +// TODO (snawaz): use #[tokio::test] once CommitTask::new() stops using blocking RpcClient +// # see the PR #575 for more context. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] 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] +// TODO (snawaz): use #[tokio::test] once CommitTask::new() stops using blocking RpcClient +// # see the PR #575 for more context. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_commit_20_accounts_1kb_bundle_size_4() { commit_20_accounts_1kb( 4, @@ -302,20 +334,24 @@ async fn test_commit_20_accounts_1kb_bundle_size_4() { .await; } -#[tokio::test] +// TODO (snawaz): use #[tokio::test] once CommitTask::new() stops using blocking RpcClient +// # see the PR #575 for more context. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_commit_20_accounts_1kb_bundle_size_6() { commit_20_accounts_1kb( 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; } -#[tokio::test] +// TODO (snawaz): use #[tokio::test] once CommitTask::new() stops using blocking RpcClient +// # see the PR #575 for more context. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_commit_20_accounts_1kb_bundle_size_20() { commit_20_accounts_1kb( 20, @@ -324,7 +360,9 @@ async fn test_commit_20_accounts_1kb_bundle_size_20() { .await; } -#[tokio::test] +// TODO (snawaz): use #[tokio::test] once CommitTask::new() stops using blocking RpcClient +// # see the PR #575 for more context. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_commit_8_accounts_1kb_bundle_size_8() { commit_8_accounts_1kb( 8, @@ -337,7 +375,9 @@ async fn test_commit_8_accounts_1kb_bundle_size_8() { .await; } -#[tokio::test] +// TODO (snawaz): use #[tokio::test] once CommitTask::new() stops using blocking RpcClient +// # see the PR #575 for more context. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_commit_20_accounts_1kb_bundle_size_8() { commit_20_accounts_1kb( 8, @@ -468,7 +508,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 From 1505d6020104c601e33ab2243ef6ba463a9511e9 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Tue, 4 Nov 2025 19:26:19 +0530 Subject: [PATCH 09/16] Add force_commit_state --- .../src/tasks/args_task.rs | 12 +++++++++--- magicblock-committor-service/src/tasks/mod.rs | 18 ++++++++++++++++-- .../tests/test_ix_commit_local.rs | 12 ++++++------ 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/magicblock-committor-service/src/tasks/args_task.rs b/magicblock-committor-service/src/tasks/args_task.rs index 2cb7ffd18..fe0d10ad6 100644 --- a/magicblock-committor-service/src/tasks/args_task.rs +++ b/magicblock-committor-service/src/tasks/args_task.rs @@ -88,9 +88,15 @@ impl BaseTask for ArgsTask { self: Box, ) -> Result, Box> { match self.task_type { - ArgsTaskType::Commit(ref value) if value.is_commit_diff() => { - // We do not currently support executing CommitDiff as BufferTask - Err(self) + ArgsTaskType::Commit(mut value) if value.is_commit_diff() => { + //TODO (snawaz): We do not currently 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. + + value.force_commit_state(); + Ok(Box::new(BufferTask::new_preparation_required( + BufferTaskType::Commit(value), + ))) } ArgsTaskType::Commit(value) => { Ok(Box::new(BufferTask::new_preparation_required( diff --git a/magicblock-committor-service/src/tasks/mod.rs b/magicblock-committor-service/src/tasks/mod.rs index 34b9545ed..88afbf38b 100644 --- a/magicblock-committor-service/src/tasks/mod.rs +++ b/magicblock-committor-service/src/tasks/mod.rs @@ -110,6 +110,7 @@ pub struct CommitTask { pub allow_undelegation: bool, pub committed_account: CommittedAccount, base_account: Option, + force_commit_state: bool, } impl CommitTask { @@ -152,15 +153,21 @@ impl CommitTask { allow_undelegation, committed_account, base_account: fetched_account, + force_commit_state: false, } } pub fn is_commit_diff(&self) -> bool { - self.committed_account.account.data.len() - > CommitTask::COMMIT_STATE_SIZE_THRESHOLD + !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) @@ -190,6 +197,13 @@ impl CommitTask { validator: &Pubkey, fetched_account: &Account, ) -> Instruction { + if self.force_commit_state { + println!( + "force_commit_state is true, so call create_commit_state_ix" + ); + return self.create_commit_state_ix(validator); + } + let args = CommitDiffArgs { nonce: self.commit_id, lamports: self.committed_account.account.lamports, 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 371977577..8587f72e5 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 @@ -80,28 +80,28 @@ async fn test_ix_commit_single_account_100_bytes_and_undelegate() { // # see the PR #575 for more context. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_ix_commit_single_account_800_bytes() { - commit_single_account(800, CommitStrategy::Args, false).await; + commit_single_account(800, CommitStrategy::FromBuffer, false).await; } // TODO (snawaz): use #[tokio::test] once CommitTask::new() stops using blocking RpcClient // # see the PR #575 for more context. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_ix_commit_single_account_800_bytes_and_undelegate() { - commit_single_account(800, CommitStrategy::Args, true).await; + commit_single_account(800, CommitStrategy::FromBuffer, true).await; } // TODO (snawaz): use #[tokio::test] once CommitTask::new() stops using blocking RpcClient // # see the PR #575 for more context. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_ix_commit_single_account_one_kb() { - commit_single_account(1024, CommitStrategy::Args, false).await; + commit_single_account(1024, CommitStrategy::FromBuffer, false).await; } // TODO (snawaz): use #[tokio::test] once CommitTask::new() stops using blocking RpcClient // # see the PR #575 for more context. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_ix_commit_single_account_ten_kb() { - commit_single_account(10 * 1024, CommitStrategy::Args, false).await; + commit_single_account(10 * 1024, CommitStrategy::FromBuffer, false).await; } async fn commit_single_account( @@ -265,7 +265,7 @@ async fn test_commit_5_accounts_1kb_bundle_size_3_undelegate_all() { 3, expect_strategies(&[ // Intent fits in 1 TX only with ALT, see IntentExecutorImpl::try_unite_tasks - (CommitStrategy::ArgsWithLookupTable, 3), + (CommitStrategy::FromBufferWithLookupTable, 3), (CommitStrategy::Args, 2), ]), true, @@ -309,7 +309,7 @@ async fn test_commit_5_accounts_1kb_bundle_size_4_undelegate_all() { async fn test_commit_5_accounts_1kb_bundle_size_5_undelegate_all() { commit_5_accounts_1kb( 5, - expect_strategies(&[(CommitStrategy::ArgsWithLookupTable, 5)]), + expect_strategies(&[(CommitStrategy::FromBufferWithLookupTable, 5)]), true, ) .await; From 7ccb3be93c435b30cc50a1b45c8304506d370a57 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Tue, 4 Nov 2025 21:36:01 +0530 Subject: [PATCH 10/16] CI is green, so let's Cleanup --- .../src/tasks/args_task.rs | 8 +++++--- magicblock-committor-service/src/tasks/mod.rs | 14 +++++--------- .../src/tasks/task_strategist.rs | 6 +----- .../tests/test_delivery_preparator.rs | 2 -- .../test-tools/src/integration_test_context.rs | 2 ++ 5 files changed, 13 insertions(+), 19 deletions(-) diff --git a/magicblock-committor-service/src/tasks/args_task.rs b/magicblock-committor-service/src/tasks/args_task.rs index fe0d10ad6..3b7936a93 100644 --- a/magicblock-committor-service/src/tasks/args_task.rs +++ b/magicblock-committor-service/src/tasks/args_task.rs @@ -89,9 +89,11 @@ impl BaseTask for ArgsTask { ) -> Result, Box> { match self.task_type { ArgsTaskType::Commit(mut value) if value.is_commit_diff() => { - //TODO (snawaz): We do not currently 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. + // 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( diff --git a/magicblock-committor-service/src/tasks/mod.rs b/magicblock-committor-service/src/tasks/mod.rs index 88afbf38b..c0e8c3e67 100644 --- a/magicblock-committor-service/src/tasks/mod.rs +++ b/magicblock-committor-service/src/tasks/mod.rs @@ -114,6 +114,8 @@ 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. const COMMIT_STATE_SIZE_THRESHOLD: usize = 200; pub fn new( @@ -124,8 +126,9 @@ impl CommitTask { let fetched_account = if committed_account.account.data.len() > CommitTask::COMMIT_STATE_SIZE_THRESHOLD { - // TODO (snawaz): it is the most ugliest piece of code as it is making network call, - // and I'll soon fix it in a separate PR that will use caching of base-accounts. + // TODO (snawaz): it is the most ugliest piece of code as it is making + // network call and that too, a blocking one, and I'll soon fix it in a + // separate PR that will use caching of base-accounts. use solana_rpc_client::rpc_client::RpcClient; use solana_sdk::commitment_config::CommitmentConfig; @@ -146,8 +149,6 @@ impl CommitTask { None }; - println!("fetched_account: {:#?}", fetched_account); - Self { commit_id, allow_undelegation, @@ -183,7 +184,6 @@ impl CommitTask { data: self.committed_account.account.data.clone(), allow_undelegation: self.allow_undelegation, }; - println!("create_commit_state_ix, data: {}", args.data.len()); dlp::instruction_builder::commit_state( *validator, self.committed_account.pubkey, @@ -198,9 +198,6 @@ impl CommitTask { fetched_account: &Account, ) -> Instruction { if self.force_commit_state { - println!( - "force_commit_state is true, so call create_commit_state_ix" - ); return self.create_commit_state_ix(validator); } @@ -214,7 +211,6 @@ impl CommitTask { .to_vec(), allow_undelegation: self.allow_undelegation, }; - println!("create_commit_diff_ix, diff: {}", args.diff.len()); dlp::instruction_builder::commit_diff( *validator, diff --git a/magicblock-committor-service/src/tasks/task_strategist.rs b/magicblock-committor-service/src/tasks/task_strategist.rs index b74a20ddd..4a16db257 100644 --- a/magicblock-committor-service/src/tasks/task_strategist.rs +++ b/magicblock-committor-service/src/tasks/task_strategist.rs @@ -95,7 +95,6 @@ impl TaskStrategist { lookup_tables_keys, }) } else { - println!("snawaz: inside build_strategy"); Err(TaskStrategistError::FailedToFitError) } } @@ -166,10 +165,7 @@ impl TaskStrategist { &[], ) { Ok(tx) => Ok(serialize_and_encode_base64(&tx).len()), - Err(TaskStrategistError::FailedToFitError) => { - println!("snawaz: inside optimize_strategy"); - Ok(usize::MAX) - } + Err(TaskStrategistError::FailedToFitError) => Ok(usize::MAX), Err(TaskStrategistError::SignerError(err)) => Err(err), } }; 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 724859c62..3362c1b8c 100644 --- a/test-integration/test-committor-service/tests/test_delivery_preparator.rs +++ b/test-integration/test-committor-service/tests/test_delivery_preparator.rs @@ -19,9 +19,7 @@ mod common; // # see the PR #575 for more context. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_prepare_10kb_buffer() { - println!("TestFixture::new()"); let fixture = TestFixture::new().await; - println!("TestFixture::new() done"); let preparator = fixture.create_delivery_preparator(); let data = generate_random_bytes(10 * 1024); diff --git a/test-integration/test-tools/src/integration_test_context.rs b/test-integration/test-tools/src/integration_test_context.rs index d2cad683e..5362e0f19 100644 --- a/test-integration/test-tools/src/integration_test_context.rs +++ b/test-integration/test-tools/src/integration_test_context.rs @@ -166,6 +166,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 From cfa88dbe296bea75836ff7e853b572f0f18f7284 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Fri, 7 Nov 2025 18:30:15 +0530 Subject: [PATCH 11/16] Use MagicBlockConfig instead of ChainConfig::local() --- Cargo.lock | 1 + magicblock-committor-service/Cargo.toml | 1 + magicblock-committor-service/src/tasks/mod.rs | 17 ++++++++++++++--- test-integration/Cargo.lock | 1 + 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 912628723..306ecbb60 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3737,6 +3737,7 @@ dependencies = [ "log", "lru 0.16.0", "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 270939fa9..b4a2618bb 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-pubkey = { workspace = true } diff --git a/magicblock-committor-service/src/tasks/mod.rs b/magicblock-committor-service/src/tasks/mod.rs index c0e8c3e67..114dbdab3 100644 --- a/magicblock-committor-service/src/tasks/mod.rs +++ b/magicblock-committor-service/src/tasks/mod.rs @@ -14,6 +14,7 @@ use magicblock_committor_program::{ }, pdas, ChangesetChunks, Chunks, }; +use magicblock_config::MagicBlockConfig; use magicblock_program::magic_scheduled_base_intent::{ BaseAction, CommittedAccount, }; @@ -133,9 +134,19 @@ impl CommitTask { use solana_sdk::commitment_config::CommitmentConfig; use crate::{config::ChainConfig, ComputeBudgetConfig}; - - let chain_config = - ChainConfig::local(ComputeBudgetConfig::new(1_000_000)); + let mb_config = MagicBlockConfig::parse_config(); + + let chain_config = ChainConfig { + rpc_uri: mb_config + .config + .accounts + .remote + .url + .as_ref() + .unwrap() + .to_string(), + ..ChainConfig::mainnet(ComputeBudgetConfig::new(1_000_000)) + }; let rpc_client = RpcClient::new_with_commitment( chain_config.rpc_uri.to_string(), diff --git a/test-integration/Cargo.lock b/test-integration/Cargo.lock index 19fe098e8..cc0ae61f8 100644 --- a/test-integration/Cargo.lock +++ b/test-integration/Cargo.lock @@ -3707,6 +3707,7 @@ dependencies = [ "log", "lru 0.16.0", "magicblock-committor-program", + "magicblock-config", "magicblock-delegation-program 1.1.2", "magicblock-metrics", "magicblock-program", From 344305c68f64aa3738971f6cdc7ebc014a9a81f5 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Sun, 9 Nov 2025 19:42:10 +0530 Subject: [PATCH 12/16] Add AccountFetcher and move the fetching logic to AccountFetcher --- .../src/tasks/account_fetcher.rs | 45 +++++++++++++++++++ magicblock-committor-service/src/tasks/mod.rs | 45 +++++-------------- .../src/tasks/task_builder.rs | 13 +++--- 3 files changed, 65 insertions(+), 38 deletions(-) create mode 100644 magicblock-committor-service/src/tasks/account_fetcher.rs diff --git a/magicblock-committor-service/src/tasks/account_fetcher.rs b/magicblock-committor-service/src/tasks/account_fetcher.rs new file mode 100644 index 000000000..72c7e26a7 --- /dev/null +++ b/magicblock-committor-service/src/tasks/account_fetcher.rs @@ -0,0 +1,45 @@ +use magicblock_config::MagicBlockConfig; + +use solana_account::Account; +use solana_pubkey::Pubkey; +use solana_rpc_client::nonblocking::rpc_client::RpcClient; +use solana_sdk::commitment_config::CommitmentConfig; + +pub struct AccountFetcher { + rpc_client: RpcClient, +} + +impl AccountFetcher { + pub fn new() -> Self { + use crate::{config::ChainConfig, ComputeBudgetConfig}; + let mb_config = MagicBlockConfig::parse_config(); + + let chain_config = ChainConfig { + rpc_uri: mb_config + .config + .accounts + .remote + .url + .as_ref() + .unwrap() + .to_string(), + ..ChainConfig::mainnet(ComputeBudgetConfig::new(1_000_000)) + }; + + Self { + rpc_client: RpcClient::new_with_commitment( + chain_config.rpc_uri.to_string(), + CommitmentConfig { + commitment: chain_config.commitment, + }, + ), + } + } + + pub async fn fetch_account( + &self, + pubkey: &Pubkey, + ) -> Result { + self.rpc_client.get_account(pubkey).await + } +} diff --git a/magicblock-committor-service/src/tasks/mod.rs b/magicblock-committor-service/src/tasks/mod.rs index 114dbdab3..52d83b07c 100644 --- a/magicblock-committor-service/src/tasks/mod.rs +++ b/magicblock-committor-service/src/tasks/mod.rs @@ -14,7 +14,6 @@ use magicblock_committor_program::{ }, pdas, ChangesetChunks, Chunks, }; -use magicblock_config::MagicBlockConfig; use magicblock_program::magic_scheduled_base_intent::{ BaseAction, CommittedAccount, }; @@ -25,6 +24,8 @@ use thiserror::Error; use crate::tasks::visitor::Visitor; +use self::account_fetcher::AccountFetcher; + pub mod args_task; pub mod buffer_task; pub mod task_builder; @@ -33,6 +34,8 @@ pub(crate) mod task_visitors; pub mod utils; pub mod visitor; +mod account_fetcher; + #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum TaskType { Commit, @@ -119,43 +122,19 @@ impl CommitTask { // reduce instruction size. Below this, commit is sent as CommitState. const COMMIT_STATE_SIZE_THRESHOLD: usize = 200; - pub fn new( + pub async fn new( commit_id: u64, allow_undelegation: bool, committed_account: CommittedAccount, + account_fetcher: AccountFetcher, ) -> Self { - let fetched_account = if committed_account.account.data.len() + let base_account = if committed_account.account.data.len() > CommitTask::COMMIT_STATE_SIZE_THRESHOLD { - // TODO (snawaz): it is the most ugliest piece of code as it is making - // network call and that too, a blocking one, and I'll soon fix it in a - // separate PR that will use caching of base-accounts. - use solana_rpc_client::rpc_client::RpcClient; - use solana_sdk::commitment_config::CommitmentConfig; - - use crate::{config::ChainConfig, ComputeBudgetConfig}; - let mb_config = MagicBlockConfig::parse_config(); - - let chain_config = ChainConfig { - rpc_uri: mb_config - .config - .accounts - .remote - .url - .as_ref() - .unwrap() - .to_string(), - ..ChainConfig::mainnet(ComputeBudgetConfig::new(1_000_000)) - }; - - let rpc_client = RpcClient::new_with_commitment( - chain_config.rpc_uri.to_string(), - CommitmentConfig { - commitment: chain_config.commitment, - }, - ); - - rpc_client.get_account(&committed_account.pubkey).ok() + account_fetcher + .fetch_account(&committed_account.pubkey) + .await + .ok() } else { None }; @@ -164,7 +143,7 @@ impl CommitTask { commit_id, allow_undelegation, committed_account, - base_account: fetched_account, + base_account, force_commit_state: false, } } diff --git a/magicblock-committor-service/src/tasks/task_builder.rs b/magicblock-committor-service/src/tasks/task_builder.rs index 8268a64a4..2801096a0 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, @@ -19,6 +20,8 @@ use crate::{ }, }; +use super::account_fetcher::AccountFetcher; + #[async_trait] pub trait TasksBuilder { // Creates tasks for commit stage @@ -85,19 +88,19 @@ 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 = ArgsTaskType::Commit(CommitTask::new( commit_id, allow_undelegation, account.clone(), - )); + AccountFetcher::new(), + ).await); Box::new(ArgsTask::new(task)) as Box - }) - .collect(); + })).await; Ok(tasks) } From b74a70a466167ec688f3244bdbfaed7b85d4e52b Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Sun, 9 Nov 2025 21:42:33 +0530 Subject: [PATCH 13/16] Adjust tests with AccountFetcher and async new() --- .../src/tasks/account_fetcher.rs | 3 + magicblock-committor-service/src/tasks/mod.rs | 78 ++++++++++--------- .../test-committor-service/tests/common.rs | 6 +- .../tests/test_delivery_preparator.rs | 41 +++++----- .../tests/test_transaction_preparator.rs | 45 ++++++++--- 5 files changed, 101 insertions(+), 72 deletions(-) diff --git a/magicblock-committor-service/src/tasks/account_fetcher.rs b/magicblock-committor-service/src/tasks/account_fetcher.rs index 72c7e26a7..54945a3e7 100644 --- a/magicblock-committor-service/src/tasks/account_fetcher.rs +++ b/magicblock-committor-service/src/tasks/account_fetcher.rs @@ -5,6 +5,9 @@ use solana_pubkey::Pubkey; use solana_rpc_client::nonblocking::rpc_client::RpcClient; use solana_sdk::commitment_config::CommitmentConfig; +// +// AccountFetcher is used by CommitTask +// pub struct AccountFetcher { rpc_client: RpcClient, } diff --git a/magicblock-committor-service/src/tasks/mod.rs b/magicblock-committor-service/src/tasks/mod.rs index 52d83b07c..83e939ab5 100644 --- a/magicblock-committor-service/src/tasks/mod.rs +++ b/magicblock-committor-service/src/tasks/mod.rs @@ -24,7 +24,7 @@ use thiserror::Error; use crate::tasks::visitor::Visitor; -use self::account_fetcher::AccountFetcher; +use account_fetcher::*; pub mod args_task; pub mod buffer_task; @@ -34,7 +34,7 @@ pub(crate) mod task_visitors; pub mod utils; pub mod visitor; -mod account_fetcher; +pub mod account_fetcher; #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum TaskType { @@ -470,51 +470,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::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, + 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, + }, }, - }, - )), - ); + AccountFetcher::new(), + ) + .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::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, + 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, + }, }, - }, - )), - ); + AccountFetcher::new(), + ) + .await, + )); let PreparationState::Required(preparation_task) = buffer_task.preparation_state() diff --git a/test-integration/test-committor-service/tests/common.rs b/test-integration/test-committor-service/tests/common.rs index daf9a35f1..2ba5d3c6a 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, }, - tasks::CommitTask, + tasks::{account_fetcher::AccountFetcher, CommitTask}, transaction_preparator::{ delivery_preparator::DeliveryPreparator, TransactionPreparatorImpl, }, @@ -147,7 +147,7 @@ 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::new( COMMIT_ID.fetch_add(1, Ordering::Relaxed), @@ -162,7 +162,9 @@ pub fn create_commit_task(data: &[u8]) -> CommitTask { rent_epoch: 0, }, }, + AccountFetcher::new(), ) + .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 3362c1b8c..dcc1db525 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, @@ -23,7 +24,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, @@ -90,15 +91,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![], @@ -169,14 +168,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(), @@ -218,7 +215,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( @@ -309,9 +306,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_transaction_preparator.rs b/test-integration/test-committor-service/tests/test_transaction_preparator.rs index 033aa072e..e95710868 100644 --- a/test-integration/test-committor-service/tests/test_transaction_preparator.rs +++ b/test-integration/test-committor-service/tests/test_transaction_preparator.rs @@ -3,6 +3,7 @@ use magicblock_committor_program::Chunks; use magicblock_committor_service::{ persist::IntentPersisterImpl, tasks::{ + account_fetcher::AccountFetcher, args_task::{ArgsTask, ArgsTaskType}, buffer_task::{BufferTask, BufferTaskType}, task_strategist::{TaskStrategist, TransactionStrategy}, @@ -35,11 +36,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::new( - 1, - true, - committed_account.clone(), - )))) as Box, + Box::new(ArgsTask::new(ArgsTaskType::Commit( + CommitTask::new( + 1, + true, + committed_account.clone(), + AccountFetcher::new(), + ) + .await, + ))) as Box, Box::new(ArgsTask::new(ArgsTaskType::Finalize(FinalizeTask { delegated_account: committed_account.pubkey, }))), @@ -91,16 +96,26 @@ async fn test_prepare_commit_tx_with_multiple_accounts() { let buffer_commit_task = BufferTask::new_preparation_required(BufferTaskType::Commit( - CommitTask::new(1, true, committed_account2.clone()), + CommitTask::new( + 1, + true, + committed_account2.clone(), + AccountFetcher::new(), + ) + .await, )); // Create test data let tasks = vec![ // account 1 - Box::new(ArgsTask::new(ArgsTaskType::Commit(CommitTask::new( - 1, - true, - committed_account1.clone(), - )))) as Box, + Box::new(ArgsTask::new(ArgsTaskType::Commit( + CommitTask::new( + 1, + true, + committed_account1.clone(), + AccountFetcher::new(), + ) + .await, + ))) as Box, // account 2 Box::new(buffer_commit_task), // finalize account 1 @@ -185,7 +200,13 @@ async fn test_prepare_commit_tx_with_base_actions() { let buffer_commit_task = BufferTask::new_preparation_required(BufferTaskType::Commit( - CommitTask::new(1, true, committed_account.clone()), + CommitTask::new( + 1, + true, + committed_account.clone(), + AccountFetcher::new(), + ) + .await, )); let tasks = vec![ // commit account From b2b4043fc3e4237c9772693c30b78e0f3aabc49b Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Mon, 10 Nov 2025 02:09:17 +0530 Subject: [PATCH 14/16] Use dev-context-only-utils in AccountFetcher --- .../src/tasks/account_fetcher.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/magicblock-committor-service/src/tasks/account_fetcher.rs b/magicblock-committor-service/src/tasks/account_fetcher.rs index 54945a3e7..39b79fa96 100644 --- a/magicblock-committor-service/src/tasks/account_fetcher.rs +++ b/magicblock-committor-service/src/tasks/account_fetcher.rs @@ -1,5 +1,3 @@ -use magicblock_config::MagicBlockConfig; - use solana_account::Account; use solana_pubkey::Pubkey; use solana_rpc_client::nonblocking::rpc_client::RpcClient; @@ -15,10 +13,14 @@ pub struct AccountFetcher { impl AccountFetcher { pub fn new() -> Self { use crate::{config::ChainConfig, ComputeBudgetConfig}; - let mb_config = MagicBlockConfig::parse_config(); + #[cfg(feature = "dev-context-only-utils")] + let chain_config = + ChainConfig::local(ComputeBudgetConfig::new(1_000_000)); + + #[cfg(not(feature = "dev-context-only-utils"))] let chain_config = ChainConfig { - rpc_uri: mb_config + rpc_uri: magicblock_config::MagicBlockConfig::parse_config() .config .accounts .remote From 8dd69dcbc5d62b8945b0e936cfabc42379df9724 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Mon, 10 Nov 2025 11:56:21 +0530 Subject: [PATCH 15/16] Make rabbit happy --- .../src/tasks/account_fetcher.rs | 11 ++++++++--- magicblock-committor-service/src/tasks/mod.rs | 11 +++++++++-- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/magicblock-committor-service/src/tasks/account_fetcher.rs b/magicblock-committor-service/src/tasks/account_fetcher.rs index 39b79fa96..4922ad480 100644 --- a/magicblock-committor-service/src/tasks/account_fetcher.rs +++ b/magicblock-committor-service/src/tasks/account_fetcher.rs @@ -18,7 +18,7 @@ impl AccountFetcher { let chain_config = ChainConfig::local(ComputeBudgetConfig::new(1_000_000)); - #[cfg(not(feature = "dev-context-only-utils"))] + //#[cfg(not(feature = "dev-context-only-utils"))] let chain_config = ChainConfig { rpc_uri: magicblock_config::MagicBlockConfig::parse_config() .config @@ -26,8 +26,13 @@ impl AccountFetcher { .remote .url .as_ref() - .unwrap() - .to_string(), + .map(|url| url.to_string()) + .unwrap_or_else(|| { + log::error!( + "Remote URL not configured, falling back to mainnet" + ); + "https://api.mainnet-beta.solana.com".to_string() + }), ..ChainConfig::mainnet(ComputeBudgetConfig::new(1_000_000)) }; diff --git a/magicblock-committor-service/src/tasks/mod.rs b/magicblock-committor-service/src/tasks/mod.rs index 83e939ab5..df42529c3 100644 --- a/magicblock-committor-service/src/tasks/mod.rs +++ b/magicblock-committor-service/src/tasks/mod.rs @@ -131,10 +131,17 @@ impl CommitTask { let base_account = if committed_account.account.data.len() > CommitTask::COMMIT_STATE_SIZE_THRESHOLD { - account_fetcher + match account_fetcher .fetch_account(&committed_account.pubkey) .await - .ok() + { + Ok(account) => Some(account), + 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 }; From 41493c588f167881dbb758a6a73244a0f88896cf Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Mon, 10 Nov 2025 12:05:08 +0530 Subject: [PATCH 16/16] make changes after rebase --- magicblock-committor-service/src/tasks/mod.rs | 3 +-- magicblock-committor-service/src/tasks/task_builder.rs | 3 +-- test-integration/programs/schedulecommit/src/api.rs | 1 + test-integration/programs/schedulecommit/src/lib.rs | 3 ++- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/magicblock-committor-service/src/tasks/mod.rs b/magicblock-committor-service/src/tasks/mod.rs index df42529c3..073a92446 100644 --- a/magicblock-committor-service/src/tasks/mod.rs +++ b/magicblock-committor-service/src/tasks/mod.rs @@ -1,3 +1,4 @@ +use account_fetcher::*; use dlp::{ args::{CommitDiffArgs, CommitStateArgs}, compute_diff, @@ -24,8 +25,6 @@ use thiserror::Error; use crate::tasks::visitor::Visitor; -use account_fetcher::*; - pub mod args_task; pub mod buffer_task; pub mod task_builder; diff --git a/magicblock-committor-service/src/tasks/task_builder.rs b/magicblock-committor-service/src/tasks/task_builder.rs index 2801096a0..a99470fc7 100644 --- a/magicblock-committor-service/src/tasks/task_builder.rs +++ b/magicblock-committor-service/src/tasks/task_builder.rs @@ -9,6 +9,7 @@ use magicblock_program::magic_scheduled_base_intent::{ }; use solana_pubkey::Pubkey; +use super::account_fetcher::AccountFetcher; use crate::{ intent_executor::task_info_fetcher::{ TaskInfoFetcher, TaskInfoFetcherError, @@ -20,8 +21,6 @@ use crate::{ }, }; -use super::account_fetcher::AccountFetcher; - #[async_trait] pub trait TasksBuilder { // Creates tasks for commit stage diff --git a/test-integration/programs/schedulecommit/src/api.rs b/test-integration/programs/schedulecommit/src/api.rs index af4ca61ba..ec0316f74 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 9e20bcb05..44dfcb031 100644 --- a/test-integration/programs/schedulecommit/src/lib.rs +++ b/test-integration/programs/schedulecommit/src/lib.rs @@ -51,6 +51,7 @@ pub struct DelegateCpiArgs { pub struct DelegateOrderBookArgs { commit_frequency_ms: u32, book_manager: Pubkey, + validator: Option, } #[derive(BorshSerialize, BorshDeserialize, Debug, Clone)] @@ -382,7 +383,7 @@ pub fn process_delegate_order_book( &seeds_no_bump, DelegateConfig { commit_frequency_ms: args.commit_frequency_ms, - ..DelegateConfig::default() + validator: args.validator, }, )?;