diff --git a/magicblock-accounts/src/remote_scheduled_commits_processor.rs b/magicblock-accounts/src/remote_scheduled_commits_processor.rs index 92a268fcd..975be02ee 100644 --- a/magicblock-accounts/src/remote_scheduled_commits_processor.rs +++ b/magicblock-accounts/src/remote_scheduled_commits_processor.rs @@ -17,8 +17,8 @@ use magicblock_committor_service::{ }; use magicblock_processor::execute_transaction::execute_legacy_transaction; use magicblock_program::{ - register_scheduled_commit_sent, FeePayerAccount, Pubkey, SentCommit, - TransactionScheduler, + register_scheduled_commit_sent, FeePayerAccount, Pubkey, ScheduledCommit, + SentCommit, TransactionScheduler, }; use magicblock_transaction_status::TransactionStatusSender; use solana_sdk::{ @@ -47,8 +47,19 @@ impl ScheduledCommitsProcessor for RemoteScheduledCommitsProcessor { IAP: InternalAccountProvider, CC: ChangesetCommittor, { - let scheduled_commits = - self.transaction_scheduler.take_scheduled_commits(); + let scheduled_actions = + self.transaction_scheduler.take_scheduled_actions(); + + // TODO(edwin): remove once actions are supported + let scheduled_commits: Vec = scheduled_actions + .into_iter() + .filter_map(|action| { + action + .try_into() + .inspect_err(|err| error!("Unexpected action: {:?}", err)) + .ok() + }) + .collect(); if scheduled_commits.is_empty() { return Ok(()); @@ -182,11 +193,11 @@ impl ScheduledCommitsProcessor for RemoteScheduledCommitsProcessor { } fn scheduled_commits_len(&self) -> usize { - self.transaction_scheduler.scheduled_commits_len() + self.transaction_scheduler.scheduled_actions_len() } fn clear_scheduled_commits(&self) { - self.transaction_scheduler.clear_scheduled_commits(); + self.transaction_scheduler.clear_scheduled_actions(); } } diff --git a/magicblock-api/src/magic_validator.rs b/magicblock-api/src/magic_validator.rs index a5925e2a6..df8744cea 100644 --- a/magicblock-api/src/magic_validator.rs +++ b/magicblock-api/src/magic_validator.rs @@ -868,4 +868,4 @@ fn try_get_remote_accounts_and_rpc_config( Some(CommitmentLevel::Confirmed), ); Ok((accounts_config, remote_rpc_config)) -} \ No newline at end of file +} diff --git a/magicblock-api/src/tickers.rs b/magicblock-api/src/tickers.rs index 90f356a49..00e3afa5f 100644 --- a/magicblock-api/src/tickers.rs +++ b/magicblock-api/src/tickers.rs @@ -14,9 +14,7 @@ use magicblock_core::magic_program; use magicblock_ledger::Ledger; use magicblock_metrics::metrics; use magicblock_processor::execute_transaction::execute_legacy_transaction; -use magicblock_program::{ - magicblock_instruction::accept_scheduled_commits, MagicContext, -}; +use magicblock_program::{instruction_utils::InstructionUtils, MagicContext}; use magicblock_transaction_status::TransactionStatusSender; use solana_sdk::account::ReadableAccount; use tokio_util::sync::CancellationToken; @@ -54,7 +52,9 @@ pub fn init_slot_ticker( if MagicContext::has_scheduled_commits(magic_context_acc.data()) { // 1. Send the transaction to move the scheduled commits from the MagicContext // to the global ScheduledCommit store - let tx = accept_scheduled_commits(bank.last_blockhash()); + let tx = InstructionUtils::accept_scheduled_commits( + bank.last_blockhash(), + ); if let Err(err) = execute_legacy_transaction( tx, &bank, diff --git a/magicblock-mutator/src/lib.rs b/magicblock-mutator/src/lib.rs index 531cb8862..a0044f733 100644 --- a/magicblock-mutator/src/lib.rs +++ b/magicblock-mutator/src/lib.rs @@ -7,6 +7,4 @@ pub mod transactions; pub use cluster::*; pub use fetch::transaction_to_clone_pubkey_from_cluster; -pub use magicblock_program::magicblock_instruction::{ - modify_accounts, AccountModification, -}; +pub use magicblock_program::magicblock_instruction::AccountModification; diff --git a/magicblock-mutator/src/transactions.rs b/magicblock-mutator/src/transactions.rs index e529c361b..8af6f6943 100644 --- a/magicblock-mutator/src/transactions.rs +++ b/magicblock-mutator/src/transactions.rs @@ -1,8 +1,6 @@ use magicblock_program::{ - magicblock_instruction::{ - modify_accounts, modify_accounts_instruction, AccountModification, - }, - validator, + instruction_utils::InstructionUtils, + magicblock_instruction::AccountModification, validator, }; use solana_sdk::{ account::Account, bpf_loader_upgradeable, hash::Hash, pubkey::Pubkey, @@ -35,7 +33,10 @@ pub fn transaction_to_clone_regular_account( } } // We only need a single transaction with a single mutation in this case - modify_accounts(vec![account_modification], recent_blockhash) + InstructionUtils::modify_accounts( + vec![account_modification], + recent_blockhash, + ) } pub fn transaction_to_clone_program( @@ -61,10 +62,14 @@ pub fn transaction_to_clone_program( // If the program does not exist yet, we just need to update it's data and don't // need to explicitly update using the BPF loader's Upgrade IX if !needs_upgrade { - return modify_accounts(account_modifications, recent_blockhash); + return InstructionUtils::modify_accounts( + account_modifications, + recent_blockhash, + ); } // First dump the necessary set of account to our bank/ledger - let modify_ix = modify_accounts_instruction(account_modifications); + let modify_ix = + InstructionUtils::modify_accounts_instruction(account_modifications); // The validator is marked as the upgrade authority of all program accounts let validator_pubkey = &validator::validator_authority_id(); // Then we run the official BPF upgrade IX to notify the system of the new program diff --git a/programs/magicblock/src/args.rs b/programs/magicblock/src/args.rs new file mode 100644 index 000000000..e2be68104 --- /dev/null +++ b/programs/magicblock/src/args.rs @@ -0,0 +1,42 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct HandlerArgs { + pub escrow_index: u8, + pub data: Vec, +} + +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] +pub struct CallHandlerArgs { + pub args: HandlerArgs, + pub destination_program: u8, // index of the account + pub accounts: Vec, // indices of account +} + +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] +pub enum CommitTypeArgs { + Standalone(Vec), // indices on accounts + WithHandler { + committed_accounts: Vec, // indices of accounts + call_handlers: Vec, + }, +} + +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] +pub enum UndelegateTypeArgs { + Standalone, + WithHandler { call_handlers: Vec }, +} + +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] +pub struct CommitAndUndelegateArgs { + pub commit_type: CommitTypeArgs, + pub undelegate_type: UndelegateTypeArgs, +} + +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] +pub enum MagicActionArgs { + L1Action(Vec), + Commit(CommitTypeArgs), + CommitAndUndelegate(CommitAndUndelegateArgs), +} diff --git a/programs/magicblock/src/errors.rs b/programs/magicblock/src/errors.rs index c5f0596a9..532c5cf1c 100644 --- a/programs/magicblock/src/errors.rs +++ b/programs/magicblock/src/errors.rs @@ -1,3 +1,8 @@ +use num_derive::{FromPrimitive, ToPrimitive}; +use serde::Serialize; +use solana_sdk::decode_error::DecodeError; +use thiserror::Error; + // ----------------- // Program CustomError Codes // ----------------- @@ -6,3 +11,47 @@ pub mod custom_error_codes { pub const UNABLE_TO_UNLOCK_SENT_COMMITS: u32 = 10_001; pub const CANNOT_FIND_SCHEDULED_COMMIT: u32 = 10_002; } + +#[derive( + Error, Debug, Serialize, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive, +)] +pub enum MagicBlockProgramError { + #[error("need at least one account to modify")] + NoAccountsToModify, + + #[error("number of accounts to modify needs to match number of account modifications")] + AccountsToModifyNotMatchingAccountModifications, + + #[error("The account modification for the provided key is missing.")] + AccountModificationMissing, + + #[error("first account needs to be MagicBlock authority")] + FirstAccountNeedsToBeMagicBlockAuthority, + + #[error("MagicBlock authority needs to be owned by system program")] + MagicBlockAuthorityNeedsToBeOwnedBySystemProgram, + + #[error("The account resolution for the provided key failed.")] + AccountDataResolutionFailed, + + #[error("The account data for the provided key is missing both from in-memory and ledger storage.")] + AccountDataMissing, + + #[error("The account data for the provided key is missing from in-memory and we are not replaying the ledger.")] + AccountDataMissingFromMemory, + + #[error("Tried to persist data that could not be resolved.")] + AttemptedToPersistUnresolvedData, + + #[error("Tried to persist data that was resolved from storage.")] + AttemptedToPersistDataFromStorage, + + #[error("Encountered an error when persisting account modification data.")] + FailedToPersistAccountModData, +} + +impl DecodeError for MagicBlockProgramError { + fn type_of() -> &'static str { + "MagicBlockProgramError" + } +} diff --git a/programs/magicblock/src/lib.rs b/programs/magicblock/src/lib.rs index ffe22905b..87d896a18 100644 --- a/programs/magicblock/src/lib.rs +++ b/programs/magicblock/src/lib.rs @@ -3,6 +3,8 @@ mod magic_context; mod mutate_accounts; mod schedule_transactions; pub use magic_context::{FeePayerAccount, MagicContext, ScheduledCommit}; +pub mod args; +mod magic_schedule_action; pub mod magicblock_instruction; pub mod magicblock_processor; #[cfg(test)] @@ -16,3 +18,4 @@ pub use schedule_transactions::{ process_scheduled_commit_sent, register_scheduled_commit_sent, transaction_scheduler::TransactionScheduler, SentCommit, }; +pub use utils::instruction_utils; diff --git a/programs/magicblock/src/magic_context.rs b/programs/magicblock/src/magic_context.rs index c50f2e90d..6e559888b 100644 --- a/programs/magicblock/src/magic_context.rs +++ b/programs/magicblock/src/magic_context.rs @@ -10,13 +10,10 @@ use solana_sdk::{ transaction::Transaction, }; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct CommittedAccount { - pub pubkey: Pubkey, - // TODO(GabrielePicco): We should read the owner from the delegation record rather - // than deriving/storing it. To remove once the cloning pipeline allow us to easily access the owner. - pub owner: Pubkey, -} +use crate::magic_schedule_action::{ + CommitAndUndelegate, CommitType, CommittedAccountV2, MagicAction, + ScheduledAction, ShortAccountMeta, UndelegateType, +}; #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct FeePayerAccount { @@ -24,20 +21,26 @@ pub struct FeePayerAccount { pub delegated_pda: Pubkey, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct ScheduledCommit { - pub id: u64, - pub slot: Slot, - pub blockhash: Hash, - pub accounts: Vec, - pub payer: Pubkey, - pub commit_sent_transaction: Transaction, - pub request_undelegation: bool, -} +// Q: can user initiate actions on arbitrary accounts? +// No, then he could call any handler on any porgram +// Inititating transfer for himself +// +// Answer: No + +// Q; can user call any program but using account that he owns? +// Far example, there could Transfer from that implements logix for transfer +// Here the fact that magicblock-program schedyled that call huarantess that user apporved this +// +// Answer: Yes + +// user has multiple actions that he wants to perform on owned accounts +// he may schedule +// Those actions may have contraints: Undelegate can come only After Commit +// Commit can't come after undelegate #[derive(Debug, Default, Serialize, Deserialize)] pub struct MagicContext { - pub scheduled_commits: Vec, + pub scheduled_commits: Vec, } impl MagicContext { @@ -53,11 +56,11 @@ impl MagicContext { } } - pub(crate) fn add_scheduled_commit(&mut self, commit: ScheduledCommit) { - self.scheduled_commits.push(commit); + pub(crate) fn add_scheduled_action(&mut self, action: ScheduledAction) { + self.scheduled_commits.push(action); } - pub(crate) fn take_scheduled_commits(&mut self) -> Vec { + pub(crate) fn take_scheduled_commits(&mut self) -> Vec { mem::take(&mut self.scheduled_commits) } @@ -81,3 +84,127 @@ fn is_zeroed(buf: &[u8]) -> bool { && chunks.remainder() == &ZEROS[..chunks.remainder().len()] } } + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ScheduledCommit { + pub id: u64, + pub slot: Slot, + pub blockhash: Hash, + pub accounts: Vec, + pub payer: Pubkey, + pub commit_sent_transaction: Transaction, + pub request_undelegation: bool, +} + +impl From for ScheduledAction { + fn from(value: ScheduledCommit) -> Self { + let commit_type = CommitType::Standalone( + value + .accounts + .into_iter() + .map(CommittedAccountV2::from) + .collect(), + ); + let action = if value.request_undelegation { + MagicAction::CommitAndUndelegate(CommitAndUndelegate { + commit_action: commit_type, + undelegate_action: UndelegateType::Standalone, + }) + } else { + MagicAction::Commit(commit_type) + }; + + Self { + id: value.id, + slot: value.slot, + blockhash: value.blockhash, + payer: value.payer, + action_sent_transaction: value.commit_sent_transaction, + action, + } + } +} + +impl TryFrom for ScheduledCommit { + type Error = MagicAction; + fn try_from(value: ScheduledAction) -> Result { + fn extract_accounts( + commit_type: CommitType, + ) -> Result, CommitType> { + match commit_type { + CommitType::Standalone(committed_accounts) => { + Ok(committed_accounts + .into_iter() + .map(CommittedAccount::from) + .collect()) + } + val @ CommitType::WithHandler { .. } => Err(val), + } + } + + let (accounts, request_undelegation) = match value.action { + MagicAction::Commit(commit_action) => { + let accounts = extract_accounts(commit_action) + .map_err(MagicAction::Commit)?; + Ok((accounts, false)) + } + MagicAction::CommitAndUndelegate(value) => { + if let UndelegateType::WithHandler(..) = + &value.undelegate_action + { + return Err(MagicAction::CommitAndUndelegate(value)); + }; + + let accounts = extract_accounts(value.commit_action).map_err( + |commit_type| { + MagicAction::CommitAndUndelegate(CommitAndUndelegate { + commit_action: commit_type, + undelegate_action: value.undelegate_action, + }) + }, + )?; + Ok((accounts, true)) + } + err @ MagicAction::CallHandler(_) => Err(err), + }?; + + Ok(Self { + id: value.id, + slot: value.slot, + blockhash: value.blockhash, + payer: value.payer, + commit_sent_transaction: value.action_sent_transaction, + accounts, + request_undelegation, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CommittedAccount { + pub pubkey: Pubkey, + // TODO(GabrielePicco): We should read the owner from the delegation record rather + // than deriving/storing it. To remove once the cloning pipeline allow us to easily access the owner. + pub owner: Pubkey, +} + +impl From for CommittedAccountV2 { + fn from(value: CommittedAccount) -> Self { + Self { + owner: value.owner, + short_meta: ShortAccountMeta { + pubkey: value.pubkey, + is_writable: false, + }, + } + } +} + +impl From for CommittedAccount { + fn from(value: CommittedAccountV2) -> Self { + Self { + pubkey: value.short_meta.pubkey, + owner: value.owner, + } + } +} diff --git a/programs/magicblock/src/magic_schedule_action.rs b/programs/magicblock/src/magic_schedule_action.rs new file mode 100644 index 000000000..4c33029aa --- /dev/null +++ b/programs/magicblock/src/magic_schedule_action.rs @@ -0,0 +1,357 @@ +use std::collections::HashSet; + +use serde::{Deserialize, Serialize}; +use solana_log_collector::ic_msg; +use solana_program_runtime::{ + __private::{Hash, InstructionError, ReadableAccount, TransactionContext}, + invoke_context::InvokeContext, +}; +use solana_sdk::{clock::Slot, transaction::Transaction}; + +use crate::{ + args::{ + CallHandlerArgs, CommitAndUndelegateArgs, CommitTypeArgs, HandlerArgs, + MagicActionArgs, UndelegateTypeArgs, + }, + instruction_utils::InstructionUtils, + utils::accounts::{ + get_instruction_account_short_meta_with_idx, + get_instruction_account_with_idx, get_instruction_pubkey_with_idx, + }, + Pubkey, +}; + +/// Context necessary for construction of Schedule Action +pub struct ConstructionContext<'a, 'ic> { + parent_program_id: Option, + signers: &'a HashSet, + transaction_context: &'a TransactionContext, + invoke_context: &'a mut InvokeContext<'ic>, +} + +impl<'a, 'ic> ConstructionContext<'a, 'ic> { + pub fn new( + parent_program_id: Option, + signers: &'a HashSet, + transaction_context: &'a TransactionContext, + invoke_context: &'a mut InvokeContext<'ic>, + ) -> Self { + Self { + parent_program_id, + signers, + transaction_context, + invoke_context, + } + } +} + +/// Scheduled action to be executed on base layer +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ScheduledAction { + pub id: u64, + pub slot: Slot, + pub blockhash: Hash, + pub action_sent_transaction: Transaction, + pub payer: Pubkey, + // Scheduled action + pub action: MagicAction, +} + +impl ScheduledAction { + pub fn try_new<'a>( + args: &MagicActionArgs, + commit_id: u64, + slot: Slot, + payer_pubkey: &Pubkey, + context: &ConstructionContext<'a, '_>, + ) -> Result { + let action = MagicAction::try_from_args(args, &context)?; + + let blockhash = context.invoke_context.environment_config.blockhash; + let action_sent_transaction = + InstructionUtils::scheduled_commit_sent(commit_id, blockhash); + Ok(ScheduledAction { + id: commit_id, + slot, + blockhash, + payer: *payer_pubkey, + action_sent_transaction, + action, + }) + } +} + +// Action that user wants to perform on base layer +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum MagicAction { + /// Actions without commitment or undelegation + CallHandler(Vec), + Commit(CommitType), + CommitAndUndelegate(CommitAndUndelegate), +} + +impl MagicAction { + pub fn try_from_args<'a>( + args: &MagicActionArgs, + context: &ConstructionContext<'a, '_>, + ) -> Result { + match args { + MagicActionArgs::L1Action(call_handlers_args) => { + let call_handlers = call_handlers_args + .iter() + .map(|args| CallHandler::try_from_args(args, context)) + .collect::, InstructionError>>()?; + Ok(MagicAction::CallHandler(call_handlers)) + } + MagicActionArgs::Commit(type_) => { + let commit = CommitType::try_from_args(type_, context)?; + Ok(MagicAction::Commit(commit)) + } + MagicActionArgs::CommitAndUndelegate(type_) => { + let commit_and_undelegate = + CommitAndUndelegate::try_from_args(type_, context)?; + Ok(MagicAction::CommitAndUndelegate(commit_and_undelegate)) + } + } + } +} + +impl CommitType { + fn validate_accounts<'a>( + account_indices: &[u8], + context: &ConstructionContext<'a, '_>, + ) -> Result<(), InstructionError> { + account_indices.iter().try_for_each(|index| { + let acc_pubkey = get_instruction_pubkey_with_idx(context.transaction_context, *index as u16)?; + let acc = get_instruction_account_with_idx(context.transaction_context, *index as u16)?; + let acc_owner = *acc.borrow().owner(); + + if context.parent_program_id != Some(acc_owner) && !context.signers.contains(acc_pubkey) { + match context.parent_program_id { + None => { + ic_msg!( + context.invoke_context, + "ScheduleCommit ERR: failed to find parent program id" + ); + Err(InstructionError::InvalidInstructionData) + } + Some(parent_id) => { + ic_msg!( + context.invoke_context, + "ScheduleCommit ERR: account {} must be owned by {} or be a signer, but is owned by {}", + acc_pubkey, parent_id, acc_owner + ); + Err(InstructionError::InvalidAccountOwner) + } + } + } else { + Ok(()) + } + }) + } + + fn extract_commit_accounts<'a>( + account_indices: &[u8], + context: &ConstructionContext<'a, '_>, + ) -> Result, InstructionError> { + account_indices + .iter() + .map(|i| { + let account = get_instruction_account_with_idx( + context.transaction_context, + *i as u16, + )?; + let owner = *account.borrow().owner(); + let short_meta = get_instruction_account_short_meta_with_idx( + context.transaction_context, + *i as u16, + )?; + + Ok(CommittedAccountV2 { + short_meta, + owner: context.parent_program_id.unwrap_or(owner), + }) + }) + .collect::, InstructionError>>() + } + + pub fn try_from_args<'a>( + args: &CommitTypeArgs, + context: &ConstructionContext<'a, '_>, + ) -> Result { + match args { + CommitTypeArgs::Standalone(accounts) => { + Self::validate_accounts(accounts, context)?; + let committed_accounts = + Self::extract_commit_accounts(accounts, context)?; + + Ok(CommitType::Standalone(committed_accounts)) + } + CommitTypeArgs::WithHandler { + committed_accounts, + call_handlers, + } => { + Self::validate_accounts(committed_accounts, context)?; + let committed_accounts = + Self::extract_commit_accounts(committed_accounts, context)?; + let call_handlers = call_handlers + .iter() + .map(|args| CallHandler::try_from_args(args, context)) + .collect::, InstructionError>>()?; + + Ok(CommitType::WithHandler { + committed_accounts, + call_handlers, + }) + } + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CommitAndUndelegate { + pub commit_action: CommitType, + pub undelegate_action: UndelegateType, +} + +impl CommitAndUndelegate { + pub fn try_from_args<'a>( + args: &CommitAndUndelegateArgs, + context: &ConstructionContext<'a, '_>, + ) -> Result { + let commit_action = + CommitType::try_from_args(&args.commit_type, context)?; + let undelegate_action = + UndelegateType::try_from_args(&args.undelegate_type, context)?; + + Ok(Self { + commit_action, + undelegate_action, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Handler { + pub escrow_index: u8, + pub data: Vec, +} + +impl From for Handler { + fn from(value: HandlerArgs) -> Self { + Self { + escrow_index: value.escrow_index, + data: value.data, + } + } +} + +impl From<&HandlerArgs> for Handler { + fn from(value: &HandlerArgs) -> Self { + value.clone().into() + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ShortAccountMeta { + pub pubkey: Pubkey, + pub is_writable: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CallHandler { + pub destination_program: Pubkey, + pub data_per_program: Handler, + pub account_metas_per_program: Vec, +} + +impl CallHandler { + pub fn try_from_args<'a>( + args: &CallHandlerArgs, + context: &ConstructionContext<'a, '_>, + ) -> Result { + let destination_program_pubkey = *get_instruction_pubkey_with_idx( + context.transaction_context, + args.destination_program as u16, + )?; + let destination_program = get_instruction_account_with_idx( + context.transaction_context, + args.destination_program as u16, + )?; + + if !destination_program.borrow().executable() { + ic_msg!( + context.invoke_context, + &format!( + "CallHandler: destination_program must be an executable. got: {}", + destination_program_pubkey + ) + ); + return Err(InstructionError::AccountNotExecutable); + } + + let account_metas = args + .accounts + .iter() + .map(|i| { + get_instruction_account_short_meta_with_idx( + context.transaction_context, + *i as u16, + ) + }) + .collect::, InstructionError>>()?; + + Ok(CallHandler { + destination_program: destination_program_pubkey, + data_per_program: args.args.clone().into(), + account_metas_per_program: account_metas, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CommittedAccountV2 { + pub short_meta: ShortAccountMeta, + // TODO(GabrielePicco): We should read the owner from the delegation record rather + // than deriving/storing it. To remove once the cloning pipeline allow us to easily access the owner. + pub owner: Pubkey, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum CommitType { + /// Regular commit without actions + /// TODO: feels like ShortMeta isn't needed + Standalone(Vec), // accounts to commit + /// Commits accounts and runs actions + WithHandler { + committed_accounts: Vec, + call_handlers: Vec, + }, +} + +/// No CommitedAccounts since it is only used with CommitAction. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum UndelegateType { + Standalone, + WithHandler(Vec), +} + +impl UndelegateType { + pub fn try_from_args<'a>( + args: &UndelegateTypeArgs, + context: &ConstructionContext<'a, '_>, + ) -> Result { + match args { + UndelegateTypeArgs::Standalone => Ok(UndelegateType::Standalone), + UndelegateTypeArgs::WithHandler { call_handlers } => { + let call_handlers = call_handlers + .iter() + .map(|call_handler| { + CallHandler::try_from_args(call_handler, context) + }) + .collect::, InstructionError>>()?; + Ok(UndelegateType::WithHandler(call_handlers)) + } + } + } +} diff --git a/programs/magicblock/src/magicblock_instruction.rs b/programs/magicblock/src/magicblock_instruction.rs index 2ecd39067..8ababb613 100644 --- a/programs/magicblock/src/magicblock_instruction.rs +++ b/programs/magicblock/src/magicblock_instruction.rs @@ -1,105 +1,12 @@ use std::collections::HashMap; -use magicblock_core::magic_program::MAGIC_CONTEXT_PUBKEY; -use num_derive::{FromPrimitive, ToPrimitive}; use serde::{Deserialize, Serialize}; -use solana_sdk::{ - account::Account, - decode_error::DecodeError, - hash::Hash, - instruction::{AccountMeta, Instruction}, - pubkey::Pubkey, - signature::Keypair, - signer::Signer, - transaction::Transaction, -}; -use thiserror::Error; +use solana_sdk::{account::Account, pubkey::Pubkey}; -use crate::{ - mutate_accounts::set_account_mod_data, - validator::{validator_authority, validator_authority_id}, -}; - -#[derive( - Error, Debug, Serialize, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive, -)] -pub enum MagicBlockProgramError { - #[error("need at least one account to modify")] - NoAccountsToModify, - - #[error("number of accounts to modify needs to match number of account modifications")] - AccountsToModifyNotMatchingAccountModifications, - - #[error("The account modification for the provided key is missing.")] - AccountModificationMissing, - - #[error("first account needs to be MagicBlock authority")] - FirstAccountNeedsToBeMagicBlockAuthority, - - #[error("MagicBlock authority needs to be owned by system program")] - MagicBlockAuthorityNeedsToBeOwnedBySystemProgram, - - #[error("The account resolution for the provided key failed.")] - AccountDataResolutionFailed, - - #[error("The account data for the provided key is missing both from in-memory and ledger storage.")] - AccountDataMissing, - - #[error("The account data for the provided key is missing from in-memory and we are not replaying the ledger.")] - AccountDataMissingFromMemory, - - #[error("Tried to persist data that could not be resolved.")] - AttemptedToPersistUnresolvedData, - - #[error("Tried to persist data that was resolved from storage.")] - AttemptedToPersistDataFromStorage, - - #[error("Encountered an error when persisting account modification data.")] - FailedToPersistAccountModData, -} - -impl DecodeError for MagicBlockProgramError { - fn type_of() -> &'static str { - "MagicBlockProgramError" - } -} - -#[derive(Default, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] -pub struct AccountModification { - pub pubkey: Pubkey, - pub lamports: Option, - pub owner: Option, - pub executable: Option, - pub data: Option>, - pub rent_epoch: Option, -} - -impl From<(&Pubkey, &Account)> for AccountModification { - fn from( - (account_pubkey, account): (&Pubkey, &Account), - ) -> AccountModification { - AccountModification { - pubkey: *account_pubkey, - lamports: Some(account.lamports), - owner: Some(account.owner), - executable: Some(account.executable), - data: Some(account.data.clone()), - rent_epoch: Some(account.rent_epoch), - } - } -} - -#[derive(Default, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] -pub(crate) struct AccountModificationForInstruction { - pub lamports: Option, - pub owner: Option, - pub executable: Option, - pub data_key: Option, - pub rent_epoch: Option, -} +use crate::args::MagicActionArgs; #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] -pub(crate) enum MagicBlockInstruction { +pub enum MagicBlockInstruction { /// Modify one or more accounts /// /// # Account references @@ -160,8 +67,10 @@ pub(crate) enum MagicBlockInstruction { /// We implement it this way so we can log the signature of this transaction /// as part of the [MagicBlockInstruction::ScheduleCommit] instruction. ScheduledCommitSent(u64), + ScheduleAction(MagicActionArgs), } +// TODO: why that exists? #[allow(unused)] impl MagicBlockInstruction { pub(crate) fn index(&self) -> u8 { @@ -172,6 +81,7 @@ impl MagicBlockInstruction { ScheduleCommitAndUndelegate => 2, AcceptScheduleCommits => 3, ScheduledCommitSent(_) => 4, + ScheduleAction(_) => 5, } } @@ -185,169 +95,36 @@ impl MagicBlockInstruction { } } -// ----------------- -// ModifyAccounts -// ----------------- -pub fn modify_accounts( - account_modifications: Vec, - recent_blockhash: Hash, -) -> Transaction { - let ix = modify_accounts_instruction(account_modifications); - into_transaction(&validator_authority(), ix, recent_blockhash) -} - -pub fn modify_accounts_instruction( - account_modifications: Vec, -) -> Instruction { - let mut account_metas = - vec![AccountMeta::new(validator_authority_id(), true)]; - let mut account_mods: HashMap = - HashMap::new(); - for account_modification in account_modifications { - account_metas - .push(AccountMeta::new(account_modification.pubkey, false)); - let account_mod_for_instruction = AccountModificationForInstruction { - lamports: account_modification.lamports, - owner: account_modification.owner, - executable: account_modification.executable, - data_key: account_modification.data.map(set_account_mod_data), - rent_epoch: account_modification.rent_epoch, - }; - account_mods - .insert(account_modification.pubkey, account_mod_for_instruction); - } - Instruction::new_with_bincode( - crate::id(), - &MagicBlockInstruction::ModifyAccounts(account_mods), - account_metas, - ) -} - -// ----------------- -// Schedule Commit -// ----------------- -pub fn schedule_commit( - payer: &Keypair, - pubkeys: Vec, - recent_blockhash: Hash, -) -> Transaction { - let ix = schedule_commit_instruction(&payer.pubkey(), pubkeys); - into_transaction(payer, ix, recent_blockhash) -} - -pub(crate) fn schedule_commit_instruction( - payer: &Pubkey, - pdas: Vec, -) -> Instruction { - let mut account_metas = vec![ - AccountMeta::new(*payer, true), - AccountMeta::new(MAGIC_CONTEXT_PUBKEY, false), - ]; - for pubkey in &pdas { - account_metas.push(AccountMeta::new_readonly(*pubkey, true)); - } - Instruction::new_with_bincode( - crate::id(), - &MagicBlockInstruction::ScheduleCommit, - account_metas, - ) -} - -// ----------------- -// Schedule Commit and Undelegate -// ----------------- -pub fn schedule_commit_and_undelegate( - payer: &Keypair, - pubkeys: Vec, - recent_blockhash: Hash, -) -> Transaction { - let ix = - schedule_commit_and_undelegate_instruction(&payer.pubkey(), pubkeys); - into_transaction(payer, ix, recent_blockhash) +#[derive(Default, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] +pub struct AccountModification { + pub pubkey: Pubkey, + pub lamports: Option, + pub owner: Option, + pub executable: Option, + pub data: Option>, + pub rent_epoch: Option, } -pub(crate) fn schedule_commit_and_undelegate_instruction( - payer: &Pubkey, - pdas: Vec, -) -> Instruction { - let mut account_metas = vec![ - AccountMeta::new(*payer, true), - AccountMeta::new(MAGIC_CONTEXT_PUBKEY, false), - ]; - for pubkey in &pdas { - account_metas.push(AccountMeta::new_readonly(*pubkey, true)); +impl From<(&Pubkey, &Account)> for AccountModification { + fn from( + (account_pubkey, account): (&Pubkey, &Account), + ) -> AccountModification { + AccountModification { + pubkey: *account_pubkey, + lamports: Some(account.lamports), + owner: Some(account.owner), + executable: Some(account.executable), + data: Some(account.data.clone()), + rent_epoch: Some(account.rent_epoch), + } } - Instruction::new_with_bincode( - crate::id(), - &MagicBlockInstruction::ScheduleCommitAndUndelegate, - account_metas, - ) } -// ----------------- -// Accept Scheduled Commits -// ----------------- -pub fn accept_scheduled_commits(recent_blockhash: Hash) -> Transaction { - let ix = accept_scheduled_commits_instruction(); - into_transaction(&validator_authority(), ix, recent_blockhash) -} - -pub(crate) fn accept_scheduled_commits_instruction() -> Instruction { - let account_metas = vec![ - AccountMeta::new_readonly(validator_authority_id(), true), - AccountMeta::new(MAGIC_CONTEXT_PUBKEY, false), - ]; - Instruction::new_with_bincode( - crate::id(), - &MagicBlockInstruction::AcceptScheduleCommits, - account_metas, - ) -} - -// ----------------- -// Scheduled Commit Sent -// ----------------- -pub fn scheduled_commit_sent( - scheduled_commit_id: u64, - recent_blockhash: Hash, -) -> Transaction { - let ix = scheduled_commit_sent_instruction( - &crate::id(), - &validator_authority_id(), - scheduled_commit_id, - ); - into_transaction(&validator_authority(), ix, recent_blockhash) -} - -pub(crate) fn scheduled_commit_sent_instruction( - magic_block_program: &Pubkey, - validator_authority: &Pubkey, - scheduled_commit_id: u64, -) -> Instruction { - let account_metas = vec![ - AccountMeta::new_readonly(*magic_block_program, false), - AccountMeta::new_readonly(*validator_authority, true), - ]; - Instruction::new_with_bincode( - *magic_block_program, - &MagicBlockInstruction::ScheduledCommitSent(scheduled_commit_id), - account_metas, - ) -} - -// ----------------- -// Utils -// ----------------- -pub(crate) fn into_transaction( - signer: &Keypair, - instruction: Instruction, - recent_blockhash: Hash, -) -> Transaction { - let signers = &[&signer]; - Transaction::new_signed_with_payer( - &[instruction], - Some(&signer.pubkey()), - signers, - recent_blockhash, - ) +#[derive(Default, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] +pub struct AccountModificationForInstruction { + pub lamports: Option, + pub owner: Option, + pub executable: Option, + pub data_key: Option, + pub rent_epoch: Option, } diff --git a/programs/magicblock/src/magicblock_processor.rs b/programs/magicblock/src/magicblock_processor.rs index 00293fcfd..bf4587999 100644 --- a/programs/magicblock/src/magicblock_processor.rs +++ b/programs/magicblock/src/magicblock_processor.rs @@ -6,8 +6,8 @@ use crate::{ mutate_accounts::process_mutate_accounts, process_scheduled_commit_sent, schedule_transactions::{ - process_accept_scheduled_commits, process_schedule_commit, - ProcessScheduleCommitOptions, + process_accept_scheduled_commits, process_schedule_action, + process_schedule_commit, ProcessScheduleCommitOptions, }, }; @@ -60,6 +60,9 @@ declare_process_instruction!( id, ) } + MagicBlockInstruction::ScheduleAction(args) => { + process_schedule_action(signers, invoke_context, args) + } } } ); diff --git a/programs/magicblock/src/mutate_accounts/account_mod_data.rs b/programs/magicblock/src/mutate_accounts/account_mod_data.rs index 006c39e96..484a86015 100644 --- a/programs/magicblock/src/mutate_accounts/account_mod_data.rs +++ b/programs/magicblock/src/mutate_accounts/account_mod_data.rs @@ -12,7 +12,7 @@ use magicblock_core::traits::PersistsAccountModData; use solana_log_collector::ic_msg; use solana_program_runtime::invoke_context::InvokeContext; -use crate::{magicblock_instruction::MagicBlockProgramError, validator}; +use crate::{errors::MagicBlockProgramError, validator}; lazy_static! { /// In order to modify large data chunks we cannot include all the data as part of the diff --git a/programs/magicblock/src/mutate_accounts/process_mutate_accounts.rs b/programs/magicblock/src/mutate_accounts/process_mutate_accounts.rs index 5b23bbec5..7ec81b5a7 100644 --- a/programs/magicblock/src/mutate_accounts/process_mutate_accounts.rs +++ b/programs/magicblock/src/mutate_accounts/process_mutate_accounts.rs @@ -11,9 +11,8 @@ use solana_sdk::{ }; use crate::{ - magicblock_instruction::{ - AccountModificationForInstruction, MagicBlockProgramError, - }, + errors::MagicBlockProgramError, + magicblock_instruction::AccountModificationForInstruction, mutate_accounts::account_mod_data::resolve_account_mod_data, validator::validator_authority_id, }; @@ -277,9 +276,8 @@ mod tests { use super::*; use crate::{ - magicblock_instruction::{ - modify_accounts_instruction, AccountModification, - }, + instruction_utils::InstructionUtils, + magicblock_instruction::AccountModification, test_utils::{ ensure_started_validator, process_instruction, AUTHORITY_BALANCE, }, @@ -309,7 +307,9 @@ mod tests { data: Some(vec![1, 2, 3, 4, 5]), rent_epoch: Some(88), }; - let ix = modify_accounts_instruction(vec![modification.clone()]); + let ix = InstructionUtils::modify_accounts_instruction(vec![ + modification.clone(), + ]); let transaction_accounts = ix .accounts .iter() @@ -376,7 +376,7 @@ mod tests { }; ensure_started_validator(&mut account_data); - let ix = modify_accounts_instruction(vec![ + let ix = InstructionUtils::modify_accounts_instruction(vec![ AccountModification { pubkey: mod_key1, lamports: Some(300), @@ -473,7 +473,7 @@ mod tests { }; ensure_started_validator(&mut account_data); - let ix = modify_accounts_instruction(vec![ + let ix = InstructionUtils::modify_accounts_instruction(vec![ AccountModification { pubkey: mod_key1, lamports: Some(1000), diff --git a/programs/magicblock/src/schedule_transactions/mod.rs b/programs/magicblock/src/schedule_transactions/mod.rs index 6da807982..89d7b470c 100644 --- a/programs/magicblock/src/schedule_transactions/mod.rs +++ b/programs/magicblock/src/schedule_transactions/mod.rs @@ -1,10 +1,45 @@ +mod process_accept_scheduled_commits; +mod process_schedule_action; mod process_schedule_commit; +#[cfg(test)] +mod process_schedule_commit_tests; mod process_scheduled_commit_sent; pub(crate) mod transaction_scheduler; + +use std::sync::atomic::AtomicU64; + +use magicblock_core::magic_program::MAGIC_CONTEXT_PUBKEY; +pub(crate) use process_accept_scheduled_commits::*; +pub(crate) use process_schedule_action::*; pub(crate) use process_schedule_commit::*; pub use process_scheduled_commit_sent::{ process_scheduled_commit_sent, register_scheduled_commit_sent, SentCommit, }; +use solana_log_collector::ic_msg; +use solana_program_runtime::{ + __private::InstructionError, invoke_context::InvokeContext, +}; -#[cfg(test)] -mod process_schedule_commit_tests; +use crate::utils::accounts::get_instruction_pubkey_with_idx; + +pub(crate) static COMMIT_ID: AtomicU64 = AtomicU64::new(0); + +pub fn check_magic_context_id( + invoke_context: &InvokeContext, + idx: u16, +) -> Result<(), InstructionError> { + let provided_magic_context = get_instruction_pubkey_with_idx( + invoke_context.transaction_context, + idx, + )?; + if !provided_magic_context.eq(&MAGIC_CONTEXT_PUBKEY) { + ic_msg!( + invoke_context, + "ERR: invalid magic context account {}", + provided_magic_context + ); + return Err(InstructionError::MissingAccount); + } + + Ok(()) +} diff --git a/programs/magicblock/src/schedule_transactions/process_accept_scheduled_commits.rs b/programs/magicblock/src/schedule_transactions/process_accept_scheduled_commits.rs new file mode 100644 index 000000000..19e424233 --- /dev/null +++ b/programs/magicblock/src/schedule_transactions/process_accept_scheduled_commits.rs @@ -0,0 +1,111 @@ +use std::collections::HashSet; + +use solana_log_collector::ic_msg; +use solana_program_runtime::{ + __private::{InstructionError, ReadableAccount}, + invoke_context::InvokeContext, +}; + +use crate::{ + schedule_transactions, + utils::accounts::{ + get_instruction_account_with_idx, get_instruction_pubkey_with_idx, + }, + validator::validator_authority_id, + MagicContext, Pubkey, TransactionScheduler, +}; + +pub fn process_accept_scheduled_commits( + signers: HashSet, + invoke_context: &mut InvokeContext, +) -> Result<(), InstructionError> { + const VALIDATOR_AUTHORITY_IDX: u16 = 0; + const MAGIC_CONTEXT_IDX: u16 = VALIDATOR_AUTHORITY_IDX + 1; + + let transaction_context = &invoke_context.transaction_context.clone(); + + // 1. Read all scheduled commits from the `MagicContext` account + // We do this first so we can skip all checks in case there is nothing + // to be processed + schedule_transactions::check_magic_context_id( + invoke_context, + MAGIC_CONTEXT_IDX, + )?; + let magic_context_acc = get_instruction_account_with_idx( + transaction_context, + MAGIC_CONTEXT_IDX, + )?; + let mut magic_context = + bincode::deserialize::(magic_context_acc.borrow().data()) + .map_err(|err| { + ic_msg!( + invoke_context, + "Failed to deserialize MagicContext: {}", + err + ); + InstructionError::InvalidAccountData + })?; + if magic_context.scheduled_commits.is_empty() { + ic_msg!( + invoke_context, + "AcceptScheduledCommits: no scheduled commits to accept" + ); + // NOTE: we should have not been called if no commits are scheduled + return Ok(()); + } + + // 2. Check that the validator authority (first account) is correct and signer + let provided_validator_auth = get_instruction_pubkey_with_idx( + transaction_context, + VALIDATOR_AUTHORITY_IDX, + )?; + let validator_auth = validator_authority_id(); + if !provided_validator_auth.eq(&validator_auth) { + ic_msg!( + invoke_context, + "AcceptScheduledCommits ERR: invalid validator authority {}, should be {}", + provided_validator_auth, + validator_auth + ); + return Err(InstructionError::InvalidArgument); + } + if !signers.contains(&validator_auth) { + ic_msg!( + invoke_context, + "AcceptScheduledCommits ERR: validator authority pubkey {} not in signers", + validator_auth + ); + return Err(InstructionError::MissingRequiredSignature); + } + + // 3. Move scheduled commits (without copying) + let scheduled_commits = magic_context.take_scheduled_commits(); + ic_msg!( + invoke_context, + "AcceptScheduledCommits: accepted {} scheduled commit(s)", + scheduled_commits.len() + ); + TransactionScheduler::default().accept_scheduled_actions(scheduled_commits); + + // 4. Serialize and store the updated `MagicContext` account + // Zero fill account before updating data + // NOTE: this may become expensive, but is a security measure and also prevents + // accidentally interpreting old data when deserializing + magic_context_acc + .borrow_mut() + .set_data_from_slice(&MagicContext::ZERO); + + magic_context_acc + .borrow_mut() + .serialize_data(&magic_context) + .map_err(|err| { + ic_msg!( + invoke_context, + "Failed to serialize MagicContext: {}", + err + ); + InstructionError::GenericError + })?; + + Ok(()) +} diff --git a/programs/magicblock/src/schedule_transactions/process_schedule_action.rs b/programs/magicblock/src/schedule_transactions/process_schedule_action.rs new file mode 100644 index 000000000..40d1abd5f --- /dev/null +++ b/programs/magicblock/src/schedule_transactions/process_schedule_action.rs @@ -0,0 +1,176 @@ +use std::{collections::HashSet, sync::atomic::Ordering}; + +use solana_log_collector::ic_msg; +use solana_program_runtime::invoke_context::InvokeContext; +use solana_sdk::{ + instruction::InstructionError, pubkey::Pubkey, + transaction_context::TransactionContext, +}; + +use crate::{ + args::MagicActionArgs, + magic_schedule_action::{ConstructionContext, ScheduledAction}, + schedule_transactions::{check_magic_context_id, COMMIT_ID}, + utils::accounts::{ + get_instruction_account_with_idx, get_instruction_pubkey_with_idx, + }, + TransactionScheduler, +}; + +const PAYER_IDX: u16 = 0; +const MAGIC_CONTEXT_IDX: u16 = PAYER_IDX + 1; +const ACTION_ACCOUNTS_OFFSET: usize = MAGIC_CONTEXT_IDX as usize + 1; +const ACTIONS_SUPPORTED: bool = false; + +pub(crate) fn process_schedule_action( + signers: HashSet, + invoke_context: &mut InvokeContext, + args: MagicActionArgs, +) -> Result<(), InstructionError> { + // TODO: remove once actions are supported + if !ACTIONS_SUPPORTED { + return Err(InstructionError::InvalidInstructionData); + } + + check_magic_context_id(invoke_context, MAGIC_CONTEXT_IDX)?; + + let transaction_context = &invoke_context.transaction_context.clone(); + let ix_ctx = transaction_context.get_current_instruction_context()?; + + // Assert MagicBlock program + ix_ctx + .find_index_of_program_account(transaction_context, &crate::id()) + .ok_or_else(|| { + ic_msg!( + invoke_context, + "ScheduleAction ERR: Magic program account not found" + ); + InstructionError::UnsupportedProgramId + })?; + + // Assert enough accounts + let ix_accs_len = ix_ctx.get_number_of_instruction_accounts() as usize; + if ix_accs_len <= ACTION_ACCOUNTS_OFFSET { + ic_msg!( + invoke_context, + "ScheduleCommit ERR: not enough accounts to schedule commit ({}), need payer, signing program an account for each pubkey to be committed", + ix_accs_len + ); + return Err(InstructionError::NotEnoughAccountKeys); + } + + // Assert Payer is signer + let payer_pubkey = + get_instruction_pubkey_with_idx(transaction_context, PAYER_IDX)?; + if !signers.contains(payer_pubkey) { + ic_msg!( + invoke_context, + "ScheduleCommit ERR: payer pubkey {} not in signers", + payer_pubkey + ); + return Err(InstructionError::MissingRequiredSignature); + } + + // + // Get the program_id of the parent instruction that invoked this one via CPI + // + + // We cannot easily simulate the transaction being invoked via CPI + // from the owning program during unit tests + // Instead the integration tests ensure that this works as expected + let parent_program_id = + get_parent_program_id(transaction_context, invoke_context)?; + + // It appears that in builtin programs `Clock::get` doesn't work as expected, thus + // we have to get it directly from the sysvar cache. + let clock = + invoke_context + .get_sysvar_cache() + .get_clock() + .map_err(|err| { + ic_msg!(invoke_context, "Failed to get clock sysvar: {}", err); + InstructionError::UnsupportedSysvar + })?; + + // Determine id and slot + let commit_id = COMMIT_ID.fetch_add(1, Ordering::Relaxed); + let construction_context = ConstructionContext::new( + parent_program_id, + &signers, + transaction_context, + invoke_context, + ); + let scheduled_action = ScheduledAction::try_new( + &args, + commit_id, + clock.slot, + payer_pubkey, + &construction_context, + )?; + let action_sent_signature = + scheduled_action.action_sent_transaction.signatures[0]; + + let context_acc = get_instruction_account_with_idx( + transaction_context, + MAGIC_CONTEXT_IDX, + )?; + TransactionScheduler::schedule_action( + invoke_context, + context_acc, + scheduled_action, + ) + .map_err(|err| { + ic_msg!( + invoke_context, + "ScheduleAction ERR: failed to schedule action: {}", + err + ); + InstructionError::GenericError + })?; + ic_msg!(invoke_context, "Scheduled commit with ID: {}", commit_id); + ic_msg!( + invoke_context, + "ScheduledCommitSent signature: {}", + action_sent_signature, + ); + + Ok(()) +} + +#[cfg(not(test))] +fn get_parent_program_id( + transaction_context: &TransactionContext, + invoke_context: &mut InvokeContext, +) -> Result, InstructionError> { + let frames = crate::utils::instruction_context_frames::InstructionContextFrames::try_from(transaction_context)?; + let parent_program_id = + frames.find_program_id_of_parent_of_current_instruction(); + + ic_msg!( + invoke_context, + "ScheduleCommit: parent program id: {}", + parent_program_id + .map_or_else(|| "None".to_string(), |id| id.to_string()) + ); + + Ok(parent_program_id.map(Clone::clone)) +} + +#[cfg(test)] +fn get_parent_program_id( + transaction_context: &TransactionContext, + _: &mut InvokeContext, +) -> Result, InstructionError> { + use solana_sdk::account::ReadableAccount; + + use crate::utils::accounts::get_instruction_account_with_idx; + + let first_committee_owner = *get_instruction_account_with_idx( + transaction_context, + ACTION_ACCOUNTS_OFFSET as u16, + )? + .borrow() + .owner(); + + Ok(Some(first_committee_owner.clone())) +} diff --git a/programs/magicblock/src/schedule_transactions/process_schedule_commit.rs b/programs/magicblock/src/schedule_transactions/process_schedule_commit.rs index 6016f374d..8c53a489d 100644 --- a/programs/magicblock/src/schedule_transactions/process_schedule_commit.rs +++ b/programs/magicblock/src/schedule_transactions/process_schedule_commit.rs @@ -1,9 +1,5 @@ -use std::{ - collections::HashSet, - sync::atomic::{AtomicU64, Ordering}, -}; +use std::{collections::HashSet, sync::atomic::Ordering}; -use magicblock_core::magic_program::MAGIC_CONTEXT_PUBKEY; use solana_log_collector::ic_msg; use solana_program_runtime::invoke_context::InvokeContext; use solana_sdk::{ @@ -11,16 +7,18 @@ use solana_sdk::{ }; use crate::{ - magic_context::{CommittedAccount, MagicContext, ScheduledCommit}, - magicblock_instruction::scheduled_commit_sent, - schedule_transactions::transaction_scheduler::TransactionScheduler, + magic_context::{CommittedAccount, ScheduledCommit}, + schedule_transactions, + schedule_transactions::{ + transaction_scheduler::TransactionScheduler, COMMIT_ID, + }, utils::{ account_actions::set_account_owner_to_delegation_program, accounts::{ get_instruction_account_with_idx, get_instruction_pubkey_with_idx, }, + instruction_utils::InstructionUtils, }, - validator::validator_authority_id, }; #[derive(Default)] @@ -33,12 +31,13 @@ pub(crate) fn process_schedule_commit( invoke_context: &mut InvokeContext, opts: ProcessScheduleCommitOptions, ) -> Result<(), InstructionError> { - static COMMIT_ID: AtomicU64 = AtomicU64::new(0); - const PAYER_IDX: u16 = 0; const MAGIC_CONTEXT_IDX: u16 = PAYER_IDX + 1; - check_magic_context_id(invoke_context, MAGIC_CONTEXT_IDX)?; + schedule_transactions::check_magic_context_id( + invoke_context, + MAGIC_CONTEXT_IDX, + )?; let transaction_context = &invoke_context.transaction_context.clone(); let ix_ctx = transaction_context.get_current_instruction_context()?; @@ -193,7 +192,8 @@ pub(crate) fn process_schedule_commit( })?; let blockhash = invoke_context.environment_config.blockhash; - let commit_sent_transaction = scheduled_commit_sent(commit_id, blockhash); + let commit_sent_transaction = + InstructionUtils::scheduled_commit_sent(commit_id, blockhash); let commit_sent_sig = commit_sent_transaction.signatures[0]; @@ -214,10 +214,10 @@ pub(crate) fn process_schedule_commit( transaction_context, MAGIC_CONTEXT_IDX, )?; - TransactionScheduler::schedule_commit( + TransactionScheduler::schedule_action( invoke_context, context_acc, - scheduled_commit, + scheduled_commit.into(), ) .map_err(|err| { ic_msg!( @@ -236,115 +236,3 @@ pub(crate) fn process_schedule_commit( Ok(()) } - -pub fn process_accept_scheduled_commits( - signers: HashSet, - invoke_context: &mut InvokeContext, -) -> Result<(), InstructionError> { - const VALIDATOR_AUTHORITY_IDX: u16 = 0; - const MAGIC_CONTEXT_IDX: u16 = VALIDATOR_AUTHORITY_IDX + 1; - - let transaction_context = &invoke_context.transaction_context.clone(); - - // 1. Read all scheduled commits from the `MagicContext` account - // We do this first so we can skip all checks in case there is nothing - // to be processed - check_magic_context_id(invoke_context, MAGIC_CONTEXT_IDX)?; - let magic_context_acc = get_instruction_account_with_idx( - transaction_context, - MAGIC_CONTEXT_IDX, - )?; - let mut magic_context = - bincode::deserialize::(magic_context_acc.borrow().data()) - .map_err(|err| { - ic_msg!( - invoke_context, - "Failed to deserialize MagicContext: {}", - err - ); - InstructionError::InvalidAccountData - })?; - if magic_context.scheduled_commits.is_empty() { - ic_msg!( - invoke_context, - "AcceptScheduledCommits: no scheduled commits to accept" - ); - // NOTE: we should have not been called if no commits are scheduled - return Ok(()); - } - - // 2. Check that the validator authority (first account) is correct and signer - let provided_validator_auth = get_instruction_pubkey_with_idx( - transaction_context, - VALIDATOR_AUTHORITY_IDX, - )?; - let validator_auth = validator_authority_id(); - if !provided_validator_auth.eq(&validator_auth) { - ic_msg!( - invoke_context, - "AcceptScheduledCommits ERR: invalid validator authority {}, should be {}", - provided_validator_auth, - validator_auth - ); - return Err(InstructionError::InvalidArgument); - } - if !signers.contains(&validator_auth) { - ic_msg!( - invoke_context, - "AcceptScheduledCommits ERR: validator authority pubkey {} not in signers", - validator_auth - ); - return Err(InstructionError::MissingRequiredSignature); - } - - // 3. Move scheduled commits (without copying) - let scheduled_commits = magic_context.take_scheduled_commits(); - ic_msg!( - invoke_context, - "AcceptScheduledCommits: accepted {} scheduled commit(s)", - scheduled_commits.len() - ); - TransactionScheduler::default().accept_scheduled_commits(scheduled_commits); - - // 4. Serialize and store the updated `MagicContext` account - // Zero fill account before updating data - // NOTE: this may become expensive, but is a security measure and also prevents - // accidentally interpreting old data when deserializing - magic_context_acc - .borrow_mut() - .set_data_from_slice(&MagicContext::ZERO); - - magic_context_acc - .borrow_mut() - .serialize_data(&magic_context) - .map_err(|err| { - ic_msg!( - invoke_context, - "Failed to serialize MagicContext: {}", - err - ); - InstructionError::GenericError - })?; - - Ok(()) -} - -fn check_magic_context_id( - invoke_context: &InvokeContext, - idx: u16, -) -> Result<(), InstructionError> { - let provided_magic_context = get_instruction_pubkey_with_idx( - invoke_context.transaction_context, - idx, - )?; - if !provided_magic_context.eq(&MAGIC_CONTEXT_PUBKEY) { - ic_msg!( - invoke_context, - "ERR: invalid magic context account {}", - provided_magic_context - ); - return Err(InstructionError::MissingAccount); - } - - Ok(()) -} diff --git a/programs/magicblock/src/schedule_transactions/process_schedule_commit_tests.rs b/programs/magicblock/src/schedule_transactions/process_schedule_commit_tests.rs index 2f93d2aa7..ecd110ffe 100644 --- a/programs/magicblock/src/schedule_transactions/process_schedule_commit_tests.rs +++ b/programs/magicblock/src/schedule_transactions/process_schedule_commit_tests.rs @@ -19,11 +19,8 @@ use test_tools_core::init_logger; use crate::{ magic_context::MagicContext, - magicblock_instruction::{ - accept_scheduled_commits_instruction, - schedule_commit_and_undelegate_instruction, - schedule_commit_instruction, MagicBlockInstruction, - }, + magic_schedule_action::ScheduledAction, + magicblock_instruction::MagicBlockInstruction, schedule_transactions::transaction_scheduler::TransactionScheduler, test_utils::{ensure_started_validator, process_instruction}, utils::DELEGATION_PROGRAM_ID, @@ -137,7 +134,7 @@ fn find_magic_context_account( .find(|acc| acc.owner() == &crate::id() && acc.lamports() == u64::MAX) } -fn assert_non_accepted_commits<'a>( +fn assert_non_accepted_actions<'a>( processed_scheduled: &'a [AccountSharedData], payer: &Pubkey, expected_non_accepted_commits: usize, @@ -147,34 +144,34 @@ fn assert_non_accepted_commits<'a>( let magic_context = bincode::deserialize::(magic_context_acc.data()).unwrap(); - let accepted_scheduled_commits = - TransactionScheduler::default().get_scheduled_commits_by_payer(payer); + let accepted_scheduled_actions = + TransactionScheduler::default().get_scheduled_actions_by_payer(payer); assert_eq!( magic_context.scheduled_commits.len(), expected_non_accepted_commits ); - assert_eq!(accepted_scheduled_commits.len(), 0); + assert_eq!(accepted_scheduled_actions.len(), 0); magic_context_acc } -fn assert_accepted_commits( +fn assert_accepted_actions( processed_accepted: &[AccountSharedData], payer: &Pubkey, - expected_scheduled_commits: usize, -) -> Vec { + expected_scheduled_actions: usize, +) -> Vec { let magic_context_acc = find_magic_context_account(processed_accepted) .expect("magic context account not found"); let magic_context = bincode::deserialize::(magic_context_acc.data()).unwrap(); - let scheduled_commits = - TransactionScheduler::default().get_scheduled_commits_by_payer(payer); + let scheduled_actions = + TransactionScheduler::default().get_scheduled_actions_by_payer(payer); assert_eq!(magic_context.scheduled_commits.len(), 0); - assert_eq!(scheduled_commits.len(), expected_scheduled_commits); + assert_eq!(scheduled_actions.len(), expected_scheduled_actions); - scheduled_commits + scheduled_actions } fn extend_transaction_accounts_from_ix( @@ -237,479 +234,536 @@ fn assert_first_commit( ); } -#[test] -fn test_schedule_commit_single_account_success() { - init_logger!(); - let payer = - Keypair::from_seed(b"schedule_commit_single_account_success").unwrap(); - let program = Pubkey::new_unique(); - let committee = Pubkey::new_unique(); +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + magic_schedule_action::MagicAction, + utils::instruction_utils::InstructionUtils, + }; - // 1. We run the transaction that registers the intent to schedule a commit - let (processed_scheduled, magic_context_acc) = { - let (mut account_data, mut transaction_accounts) = - prepare_transaction_with_single_committee( - &payer, program, committee, + #[test] + fn test_schedule_commit_single_account_success() { + init_logger!(); + let payer = + Keypair::from_seed(b"schedule_commit_single_account_success") + .unwrap(); + let program = Pubkey::new_unique(); + let committee = Pubkey::new_unique(); + + // 1. We run the transaction that registers the intent to schedule a commit + let (processed_scheduled, magic_context_acc) = { + let (mut account_data, mut transaction_accounts) = + prepare_transaction_with_single_committee( + &payer, program, committee, + ); + + let ix = InstructionUtils::schedule_commit_instruction( + &payer.pubkey(), + vec![committee], ); - let ix = schedule_commit_instruction(&payer.pubkey(), vec![committee]); + extend_transaction_accounts_from_ix( + &ix, + &mut account_data, + &mut transaction_accounts, + ); - extend_transaction_accounts_from_ix( - &ix, - &mut account_data, - &mut transaction_accounts, - ); + let processed_scheduled = process_instruction( + ix.data.as_slice(), + transaction_accounts.clone(), + ix.accounts, + Ok(()), + ); - let processed_scheduled = process_instruction( - ix.data.as_slice(), - transaction_accounts.clone(), - ix.accounts, - Ok(()), - ); + // At this point the intent to commit was added to the magic context account, + // but not yet accepted + let magic_context_acc = assert_non_accepted_actions( + &processed_scheduled, + &payer.pubkey(), + 1, + ); - // At this point the intent to commit was added to the magic context account, - // but not yet accepted - let magic_context_acc = assert_non_accepted_commits( - &processed_scheduled, - &payer.pubkey(), - 1, - ); + (processed_scheduled.clone(), magic_context_acc.clone()) + }; + + // 2. We run the transaction that accepts the scheduled commit + { + let (mut account_data, mut transaction_accounts) = + prepare_transaction_with_single_committee( + &payer, program, committee, + ); + + let ix = InstructionUtils::accept_scheduled_commits_instruction(); + extend_transaction_accounts_from_ix_adding_magic_context( + &ix, + &magic_context_acc, + &mut account_data, + &mut transaction_accounts, + ); - (processed_scheduled.clone(), magic_context_acc.clone()) - }; + let processed_accepted = process_instruction( + ix.data.as_slice(), + transaction_accounts, + ix.accounts, + Ok(()), + ); - // 2. We run the transaction that accepts the scheduled commit - { - let (mut account_data, mut transaction_accounts) = - prepare_transaction_with_single_committee( - &payer, program, committee, + // At this point the intended commits were accepted and moved to the global + let scheduled_commits = assert_accepted_actions( + &processed_accepted, + &payer.pubkey(), + 1, ); - let ix = accept_scheduled_commits_instruction(); - extend_transaction_accounts_from_ix_adding_magic_context( - &ix, - &magic_context_acc, - &mut account_data, - &mut transaction_accounts, - ); + let scheduled_commits = scheduled_commits + .into_iter() + .map(|el| el.try_into()) + .collect::, MagicAction>>() + .expect("only commit action"); + + assert_first_commit( + &scheduled_commits, + &payer.pubkey(), + &[committee], + false, + ); + } + let committed_account = processed_scheduled.last().unwrap(); + assert_eq!(*committed_account.owner(), program); + } - let processed_accepted = process_instruction( - ix.data.as_slice(), - transaction_accounts, - ix.accounts, - Ok(()), - ); + #[test] + fn test_schedule_commit_single_account_and_request_undelegate_success() { + init_logger!(); + let payer = + Keypair::from_seed(b"single_account_with_undelegate_success") + .unwrap(); + let program = Pubkey::new_unique(); + let committee = Pubkey::new_unique(); + + // 1. We run the transaction that registers the intent to schedule a commit + let (processed_scheduled, magic_context_acc) = { + let (mut account_data, mut transaction_accounts) = + prepare_transaction_with_single_committee( + &payer, program, committee, + ); + + let ix = + InstructionUtils::schedule_commit_and_undelegate_instruction( + &payer.pubkey(), + vec![committee], + ); + + extend_transaction_accounts_from_ix( + &ix, + &mut account_data, + &mut transaction_accounts, + ); - // At this point the intended commits were accepted and moved to the global - let scheduled_commits = - assert_accepted_commits(&processed_accepted, &payer.pubkey(), 1); + let processed_scheduled = process_instruction( + ix.data.as_slice(), + transaction_accounts.clone(), + ix.accounts, + Ok(()), + ); - assert_first_commit( - &scheduled_commits, - &payer.pubkey(), - &[committee], - false, - ); - } - let committed_account = processed_scheduled.last().unwrap(); - assert_eq!(*committed_account.owner(), program); -} + // At this point the intent to commit was added to the magic context account, + // but not yet accepted + let magic_context_acc = assert_non_accepted_actions( + &processed_scheduled, + &payer.pubkey(), + 1, + ); -#[test] -fn test_schedule_commit_single_account_and_request_undelegate_success() { - init_logger!(); - let payer = - Keypair::from_seed(b"single_account_with_undelegate_success").unwrap(); - let program = Pubkey::new_unique(); - let committee = Pubkey::new_unique(); + (processed_scheduled.clone(), magic_context_acc.clone()) + }; + + // 2. We run the transaction that accepts the scheduled commit + { + let (mut account_data, mut transaction_accounts) = + prepare_transaction_with_single_committee( + &payer, program, committee, + ); + + let ix = InstructionUtils::accept_scheduled_commits_instruction(); + extend_transaction_accounts_from_ix_adding_magic_context( + &ix, + &magic_context_acc, + &mut account_data, + &mut transaction_accounts, + ); - // 1. We run the transaction that registers the intent to schedule a commit - let (processed_scheduled, magic_context_acc) = { - let (mut account_data, mut transaction_accounts) = - prepare_transaction_with_single_committee( - &payer, program, committee, + let processed_accepted = process_instruction( + ix.data.as_slice(), + transaction_accounts, + ix.accounts, + Ok(()), ); - let ix = schedule_commit_and_undelegate_instruction( - &payer.pubkey(), - vec![committee], - ); + // At this point the intended commits were accepted and moved to the global + let scheduled_commits = assert_accepted_actions( + &processed_accepted, + &payer.pubkey(), + 1, + ); - extend_transaction_accounts_from_ix( - &ix, - &mut account_data, - &mut transaction_accounts, - ); + let scheduled_commits = scheduled_commits + .into_iter() + .map(|el| el.try_into()) + .collect::, MagicAction>>() + .expect("only commit action"); + + assert_first_commit( + &scheduled_commits, + &payer.pubkey(), + &[committee], + true, + ); + } + let committed_account = processed_scheduled.last().unwrap(); + assert_eq!(*committed_account.owner(), DELEGATION_PROGRAM_ID); + } - let processed_scheduled = process_instruction( - ix.data.as_slice(), - transaction_accounts.clone(), - ix.accounts, - Ok(()), - ); + #[test] + fn test_schedule_commit_three_accounts_success() { + init_logger!(); - // At this point the intent to commit was added to the magic context account, - // but not yet accepted - let magic_context_acc = assert_non_accepted_commits( - &processed_scheduled, - &payer.pubkey(), - 1, - ); + let payer = + Keypair::from_seed(b"schedule_commit_three_accounts_success") + .unwrap(); - (processed_scheduled.clone(), magic_context_acc.clone()) - }; + // 1. We run the transaction that registers the intent to schedule a commit + let ( + mut processed_scheduled, + magic_context_acc, + program, + committee_uno, + committee_dos, + committee_tres, + ) = { + let PreparedTransactionThreeCommittees { + mut accounts_data, + committee_uno, + committee_dos, + committee_tres, + mut transaction_accounts, + program, + .. + } = prepare_transaction_with_three_committees(&payer, None); + + let ix = InstructionUtils::schedule_commit_instruction( + &payer.pubkey(), + vec![committee_uno, committee_dos, committee_tres], + ); + extend_transaction_accounts_from_ix( + &ix, + &mut accounts_data, + &mut transaction_accounts, + ); - // 2. We run the transaction that accepts the scheduled commit - { - let (mut account_data, mut transaction_accounts) = - prepare_transaction_with_single_committee( - &payer, program, committee, + let processed_scheduled = process_instruction( + ix.data.as_slice(), + transaction_accounts, + ix.accounts, + Ok(()), ); - let ix = accept_scheduled_commits_instruction(); - extend_transaction_accounts_from_ix_adding_magic_context( - &ix, - &magic_context_acc, - &mut account_data, - &mut transaction_accounts, - ); + // At this point the intent to commit was added to the magic context account, + // but not yet accepted + let magic_context_acc = assert_non_accepted_actions( + &processed_scheduled, + &payer.pubkey(), + 1, + ); - let processed_accepted = process_instruction( - ix.data.as_slice(), - transaction_accounts, - ix.accounts, - Ok(()), - ); + ( + processed_scheduled.clone(), + magic_context_acc.clone(), + program, + committee_uno, + committee_dos, + committee_tres, + ) + }; + + // 2. We run the transaction that accepts the scheduled commit + { + let PreparedTransactionThreeCommittees { + mut accounts_data, + mut transaction_accounts, + .. + } = prepare_transaction_with_three_committees( + &payer, + Some((committee_uno, committee_dos, committee_tres)), + ); - // At this point the intended commits were accepted and moved to the global - let scheduled_commits = - assert_accepted_commits(&processed_accepted, &payer.pubkey(), 1); + let ix = InstructionUtils::accept_scheduled_commits_instruction(); + extend_transaction_accounts_from_ix_adding_magic_context( + &ix, + &magic_context_acc, + &mut accounts_data, + &mut transaction_accounts, + ); - assert_first_commit( - &scheduled_commits, - &payer.pubkey(), - &[committee], - true, - ); - } - let committed_account = processed_scheduled.last().unwrap(); - assert_eq!(*committed_account.owner(), DELEGATION_PROGRAM_ID); -} + let processed_accepted = process_instruction( + ix.data.as_slice(), + transaction_accounts, + ix.accounts, + Ok(()), + ); -#[test] -fn test_schedule_commit_three_accounts_success() { - init_logger!(); + // At this point the intended commits were accepted and moved to the global + let scheduled_commits = assert_accepted_actions( + &processed_accepted, + &payer.pubkey(), + 1, + ); - let payer = - Keypair::from_seed(b"schedule_commit_three_accounts_success").unwrap(); + let scheduled_commits = scheduled_commits + .into_iter() + .map(|el| el.try_into()) + .collect::, MagicAction>>() + .expect("only commit action"); + + assert_first_commit( + &scheduled_commits, + &payer.pubkey(), + &[committee_uno, committee_dos, committee_tres], + false, + ); + for _ in &[committee_uno, committee_dos, committee_tres] { + let committed_account = processed_scheduled.pop().unwrap(); + assert_eq!(*committed_account.owner(), program); + } + } + } - // 1. We run the transaction that registers the intent to schedule a commit - let ( - mut processed_scheduled, - magic_context_acc, - program, - committee_uno, - committee_dos, - committee_tres, - ) = { - let PreparedTransactionThreeCommittees { - mut accounts_data, + #[test] + fn test_schedule_commit_three_accounts_and_request_undelegate_success() { + let payer = Keypair::from_seed( + b"three_accounts_and_request_undelegate_success", + ) + .unwrap(); + + // 1. We run the transaction that registers the intent to schedule a commit + let ( + mut processed_scheduled, + magic_context_acc, + _program, committee_uno, committee_dos, committee_tres, - mut transaction_accounts, - program, - .. - } = prepare_transaction_with_three_committees(&payer, None); - - let ix = schedule_commit_instruction( - &payer.pubkey(), - vec![committee_uno, committee_dos, committee_tres], - ); - extend_transaction_accounts_from_ix( - &ix, - &mut accounts_data, - &mut transaction_accounts, - ); + ) = { + let PreparedTransactionThreeCommittees { + mut accounts_data, + committee_uno, + committee_dos, + committee_tres, + mut transaction_accounts, + program, + .. + } = prepare_transaction_with_three_committees(&payer, None); + + let ix = + InstructionUtils::schedule_commit_and_undelegate_instruction( + &payer.pubkey(), + vec![committee_uno, committee_dos, committee_tres], + ); + + extend_transaction_accounts_from_ix( + &ix, + &mut accounts_data, + &mut transaction_accounts, + ); - let processed_scheduled = process_instruction( - ix.data.as_slice(), - transaction_accounts, - ix.accounts, - Ok(()), - ); + let processed_scheduled = process_instruction( + ix.data.as_slice(), + transaction_accounts, + ix.accounts, + Ok(()), + ); - // At this point the intent to commit was added to the magic context account, - // but not yet accepted - let magic_context_acc = assert_non_accepted_commits( - &processed_scheduled, - &payer.pubkey(), - 1, - ); + // At this point the intent to commit was added to the magic context account, + // but not yet accepted + let magic_context_acc = assert_non_accepted_actions( + &processed_scheduled, + &payer.pubkey(), + 1, + ); - ( - processed_scheduled.clone(), - magic_context_acc.clone(), - program, - committee_uno, - committee_dos, - committee_tres, - ) - }; + ( + processed_scheduled.clone(), + magic_context_acc.clone(), + program, + committee_uno, + committee_dos, + committee_tres, + ) + }; + + // 2. We run the transaction that accepts the scheduled commit + { + let PreparedTransactionThreeCommittees { + mut accounts_data, + mut transaction_accounts, + .. + } = prepare_transaction_with_three_committees( + &payer, + Some((committee_uno, committee_dos, committee_tres)), + ); - // 2. We run the transaction that accepts the scheduled commit - { - let PreparedTransactionThreeCommittees { - mut accounts_data, - mut transaction_accounts, - .. - } = prepare_transaction_with_three_committees( - &payer, - Some((committee_uno, committee_dos, committee_tres)), - ); + let ix = InstructionUtils::accept_scheduled_commits_instruction(); + extend_transaction_accounts_from_ix_adding_magic_context( + &ix, + &magic_context_acc, + &mut accounts_data, + &mut transaction_accounts, + ); - let ix = accept_scheduled_commits_instruction(); - extend_transaction_accounts_from_ix_adding_magic_context( - &ix, - &magic_context_acc, - &mut accounts_data, - &mut transaction_accounts, - ); + let processed_accepted = process_instruction( + ix.data.as_slice(), + transaction_accounts, + ix.accounts, + Ok(()), + ); - let processed_accepted = process_instruction( - ix.data.as_slice(), - transaction_accounts, - ix.accounts, - Ok(()), - ); + // At this point the intended commits were accepted and moved to the global + let scheduled_commits = assert_accepted_actions( + &processed_accepted, + &payer.pubkey(), + 1, + ); - // At this point the intended commits were accepted and moved to the global - let scheduled_commits = - assert_accepted_commits(&processed_accepted, &payer.pubkey(), 1); + let scheduled_commits = scheduled_commits + .into_iter() + .map(|el| el.try_into()) + .collect::, MagicAction>>() + .expect("only commit action"); + + assert_first_commit( + &scheduled_commits, + &payer.pubkey(), + &[committee_uno, committee_dos, committee_tres], + true, + ); + for _ in &[committee_uno, committee_dos, committee_tres] { + let committed_account = processed_scheduled.pop().unwrap(); + assert_eq!(*committed_account.owner(), DELEGATION_PROGRAM_ID); + } + } + } - assert_first_commit( - &scheduled_commits, - &payer.pubkey(), - &[committee_uno, committee_dos, committee_tres], - false, - ); - for _ in &[committee_uno, committee_dos, committee_tres] { - let committed_account = processed_scheduled.pop().unwrap(); - assert_eq!(*committed_account.owner(), program); + // ----------------- + // Failure Cases + // ---------------- + fn get_account_metas_for_schedule_commit( + payer: &Pubkey, + pdas: Vec, + ) -> Vec { + let mut account_metas = vec![ + AccountMeta::new(*payer, true), + AccountMeta::new(MAGIC_CONTEXT_PUBKEY, false), + ]; + for pubkey in &pdas { + account_metas.push(AccountMeta::new_readonly(*pubkey, true)); } + account_metas } -} -#[test] -fn test_schedule_commit_three_accounts_and_request_undelegate_success() { - let payer = - Keypair::from_seed(b"three_accounts_and_request_undelegate_success") - .unwrap(); - - // 1. We run the transaction that registers the intent to schedule a commit - let ( - mut processed_scheduled, - magic_context_acc, - _program, - committee_uno, - committee_dos, - committee_tres, - ) = { + fn account_metas_last_committee_not_signer( + payer: &Pubkey, + pdas: Vec, + ) -> Vec { + let mut account_metas = + get_account_metas_for_schedule_commit(payer, pdas); + let last = account_metas.pop().unwrap(); + account_metas.push(AccountMeta::new_readonly(last.pubkey, false)); + account_metas + } + + fn instruction_from_account_metas( + account_metas: Vec, + ) -> solana_sdk::instruction::Instruction { + Instruction::new_with_bincode( + crate::id(), + &MagicBlockInstruction::ScheduleCommit, + account_metas, + ) + } + + #[test] + fn test_schedule_commit_no_pdas_provided_to_ix() { + init_logger!(); + + let payer = + Keypair::from_seed(b"schedule_commit_no_pdas_provided_to_ix") + .unwrap(); + let PreparedTransactionThreeCommittees { mut accounts_data, - committee_uno, - committee_dos, - committee_tres, mut transaction_accounts, - program, .. } = prepare_transaction_with_three_committees(&payer, None); - let ix = schedule_commit_and_undelegate_instruction( - &payer.pubkey(), - vec![committee_uno, committee_dos, committee_tres], + let ix = instruction_from_account_metas( + get_account_metas_for_schedule_commit(&payer.pubkey(), vec![]), ); - extend_transaction_accounts_from_ix( &ix, &mut accounts_data, &mut transaction_accounts, ); - let processed_scheduled = process_instruction( + process_instruction( ix.data.as_slice(), transaction_accounts, ix.accounts, - Ok(()), + Err(InstructionError::NotEnoughAccountKeys), ); + } - // At this point the intent to commit was added to the magic context account, - // but not yet accepted - let magic_context_acc = assert_non_accepted_commits( - &processed_scheduled, - &payer.pubkey(), - 1, - ); + #[test] + fn test_schedule_commit_three_accounts_second_not_owned_by_program_and_not_signer( + ) { + init_logger!(); - ( - processed_scheduled.clone(), - magic_context_acc.clone(), - program, - committee_uno, - committee_dos, - committee_tres, - ) - }; + let payer = + Keypair::from_seed(b"three_accounts_last_not_owned_by_program") + .unwrap(); - // 2. We run the transaction that accepts the scheduled commit - { let PreparedTransactionThreeCommittees { mut accounts_data, + committee_uno, + committee_dos, + committee_tres, mut transaction_accounts, .. - } = prepare_transaction_with_three_committees( - &payer, - Some((committee_uno, committee_dos, committee_tres)), + } = prepare_transaction_with_three_committees(&payer, None); + + accounts_data.insert( + committee_dos, + AccountSharedData::new(0, 0, &Pubkey::new_unique()), + ); + + let ix = instruction_from_account_metas( + account_metas_last_committee_not_signer( + &payer.pubkey(), + vec![committee_uno, committee_tres, committee_dos], + ), ); - let ix = accept_scheduled_commits_instruction(); - extend_transaction_accounts_from_ix_adding_magic_context( + extend_transaction_accounts_from_ix( &ix, - &magic_context_acc, &mut accounts_data, &mut transaction_accounts, ); - let processed_accepted = process_instruction( + process_instruction( ix.data.as_slice(), transaction_accounts, ix.accounts, - Ok(()), - ); - - // At this point the intended commits were accepted and moved to the global - let scheduled_commits = - assert_accepted_commits(&processed_accepted, &payer.pubkey(), 1); - - assert_first_commit( - &scheduled_commits, - &payer.pubkey(), - &[committee_uno, committee_dos, committee_tres], - true, + Err(InstructionError::InvalidAccountOwner), ); - for _ in &[committee_uno, committee_dos, committee_tres] { - let committed_account = processed_scheduled.pop().unwrap(); - assert_eq!(*committed_account.owner(), DELEGATION_PROGRAM_ID); - } } } - -// ----------------- -// Failure Cases -// ---------------- -fn get_account_metas_for_schedule_commit( - payer: &Pubkey, - pdas: Vec, -) -> Vec { - let mut account_metas = vec![ - AccountMeta::new(*payer, true), - AccountMeta::new(MAGIC_CONTEXT_PUBKEY, false), - ]; - for pubkey in &pdas { - account_metas.push(AccountMeta::new_readonly(*pubkey, true)); - } - account_metas -} - -fn account_metas_last_committee_not_signer( - payer: &Pubkey, - pdas: Vec, -) -> Vec { - let mut account_metas = get_account_metas_for_schedule_commit(payer, pdas); - let last = account_metas.pop().unwrap(); - account_metas.push(AccountMeta::new_readonly(last.pubkey, false)); - account_metas -} - -fn instruction_from_account_metas( - account_metas: Vec, -) -> solana_sdk::instruction::Instruction { - Instruction::new_with_bincode( - crate::id(), - &MagicBlockInstruction::ScheduleCommit, - account_metas, - ) -} - -#[test] -fn test_schedule_commit_no_pdas_provided_to_ix() { - init_logger!(); - - let payer = - Keypair::from_seed(b"schedule_commit_no_pdas_provided_to_ix").unwrap(); - - let PreparedTransactionThreeCommittees { - mut accounts_data, - mut transaction_accounts, - .. - } = prepare_transaction_with_three_committees(&payer, None); - - let ix = instruction_from_account_metas( - get_account_metas_for_schedule_commit(&payer.pubkey(), vec![]), - ); - extend_transaction_accounts_from_ix( - &ix, - &mut accounts_data, - &mut transaction_accounts, - ); - - process_instruction( - ix.data.as_slice(), - transaction_accounts, - ix.accounts, - Err(InstructionError::NotEnoughAccountKeys), - ); -} - -#[test] -fn test_schedule_commit_three_accounts_second_not_owned_by_program_and_not_signer( -) { - init_logger!(); - - let payer = Keypair::from_seed(b"three_accounts_last_not_owned_by_program") - .unwrap(); - - let PreparedTransactionThreeCommittees { - mut accounts_data, - committee_uno, - committee_dos, - committee_tres, - mut transaction_accounts, - .. - } = prepare_transaction_with_three_committees(&payer, None); - - accounts_data.insert( - committee_dos, - AccountSharedData::new(0, 0, &Pubkey::new_unique()), - ); - - let ix = instruction_from_account_metas( - account_metas_last_committee_not_signer( - &payer.pubkey(), - vec![committee_uno, committee_tres, committee_dos], - ), - ); - - extend_transaction_accounts_from_ix( - &ix, - &mut accounts_data, - &mut transaction_accounts, - ); - - process_instruction( - ix.data.as_slice(), - transaction_accounts, - ix.accounts, - Err(InstructionError::InvalidAccountOwner), - ); -} 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 58d76040f..b6701b187 100644 --- a/programs/magicblock/src/schedule_transactions/process_scheduled_commit_sent.rs +++ b/programs/magicblock/src/schedule_transactions/process_scheduled_commit_sent.rs @@ -240,7 +240,7 @@ mod tests { use super::*; use crate::{ - magicblock_instruction::scheduled_commit_sent_instruction, + instruction_utils::InstructionUtils, test_utils::{ensure_started_validator, process_instruction}, validator, }; @@ -292,7 +292,7 @@ mod tests { ensure_started_validator(&mut account_data); - let mut ix = scheduled_commit_sent_instruction( + let mut ix = InstructionUtils::scheduled_commit_sent_instruction( &crate::id(), &validator::validator_authority_id(), commit.commit_id, @@ -329,7 +329,7 @@ mod tests { }; ensure_started_validator(&mut account_data); - let ix = scheduled_commit_sent_instruction( + let ix = InstructionUtils::scheduled_commit_sent_instruction( &crate::id(), &fake_validator.pubkey(), commit.commit_id, @@ -364,7 +364,7 @@ mod tests { }; ensure_started_validator(&mut account_data); - let ix = scheduled_commit_sent_instruction( + let ix = InstructionUtils::scheduled_commit_sent_instruction( &fake_program.pubkey(), &validator::validator_authority_id(), commit.commit_id, @@ -393,7 +393,7 @@ mod tests { ensure_started_validator(&mut account_data); - let ix = scheduled_commit_sent_instruction( + let ix = InstructionUtils::scheduled_commit_sent_instruction( &crate::id(), &validator::validator_authority_id(), commit.commit_id, diff --git a/programs/magicblock/src/schedule_transactions/transaction_scheduler.rs b/programs/magicblock/src/schedule_transactions/transaction_scheduler.rs index d6b151e6d..a961e5965 100644 --- a/programs/magicblock/src/schedule_transactions/transaction_scheduler.rs +++ b/programs/magicblock/src/schedule_transactions/transaction_scheduler.rs @@ -12,11 +12,13 @@ use solana_sdk::{ instruction::InstructionError, pubkey::Pubkey, }; -use crate::magic_context::{MagicContext, ScheduledCommit}; +use crate::{ + magic_context::MagicContext, magic_schedule_action::ScheduledAction, +}; #[derive(Clone)] pub struct TransactionScheduler { - scheduled_commits: Arc>>, + scheduled_action: Arc>>, } impl Default for TransactionScheduler { @@ -25,20 +27,20 @@ impl Default for TransactionScheduler { /// This vec tracks commits that went through the entire process of first /// being scheduled into the MagicContext, and then being moved /// over to this global. - static ref SCHEDULED_COMMITS: Arc>> = + static ref SCHEDULED_ACTION: Arc>> = Default::default(); } Self { - scheduled_commits: SCHEDULED_COMMITS.clone(), + scheduled_action: SCHEDULED_ACTION.clone(), } } } impl TransactionScheduler { - pub fn schedule_commit( + pub fn schedule_action( invoke_context: &InvokeContext, context_account: &RefCell, - commit: ScheduledCommit, + action: ScheduledAction, ) -> Result<(), InstructionError> { let context_data = &mut context_account.borrow_mut(); let mut context = @@ -50,26 +52,26 @@ impl TransactionScheduler { ); InstructionError::GenericError })?; - context.add_scheduled_commit(commit); + context.add_scheduled_action(action); context_data.set_state(&context)?; Ok(()) } - pub fn accept_scheduled_commits(&self, commits: Vec) { - self.scheduled_commits + pub fn accept_scheduled_actions(&self, commits: Vec) { + self.scheduled_action .write() - .expect("scheduled_commits lock poisoned") + .expect("scheduled_action lock poisoned") .extend(commits); } - pub fn get_scheduled_commits_by_payer( + pub fn get_scheduled_actions_by_payer( &self, payer: &Pubkey, - ) -> Vec { + ) -> Vec { let commits = self - .scheduled_commits + .scheduled_action .read() - .expect("scheduled_commits lock poisoned"); + .expect("scheduled_action lock poisoned"); commits .iter() @@ -78,28 +80,28 @@ impl TransactionScheduler { .collect::>() } - pub fn take_scheduled_commits(&self) -> Vec { + pub fn take_scheduled_actions(&self) -> Vec { let mut lock = self - .scheduled_commits + .scheduled_action .write() - .expect("scheduled_commits lock poisoned"); + .expect("scheduled_action lock poisoned"); mem::take(&mut *lock) } - pub fn scheduled_commits_len(&self) -> usize { + pub fn scheduled_actions_len(&self) -> usize { let lock = self - .scheduled_commits + .scheduled_action .read() - .expect("scheduled_commits lock poisoned"); + .expect("scheduled_action lock poisoned"); lock.len() } - pub fn clear_scheduled_commits(&self) { + pub fn clear_scheduled_actions(&self) { let mut lock = self - .scheduled_commits + .scheduled_action .write() - .expect("scheduled_commits lock poisoned"); + .expect("scheduled_action lock poisoned"); lock.clear(); } } diff --git a/programs/magicblock/src/utils/accounts.rs b/programs/magicblock/src/utils/accounts.rs index 5b8c44136..eaaff189a 100644 --- a/programs/magicblock/src/utils/accounts.rs +++ b/programs/magicblock/src/utils/accounts.rs @@ -6,11 +6,13 @@ use solana_program_runtime::invoke_context::InvokeContext; use solana_sdk::{ account::{Account, AccountSharedData, ReadableAccount, WritableAccount}, account_info::{AccountInfo, IntoAccountInfo}, - instruction::InstructionError, + instruction::{AccountMeta, InstructionError}, pubkey::Pubkey, transaction_context::TransactionContext, }; +use crate::magic_schedule_action::ShortAccountMeta; + pub(crate) fn find_tx_index_of_instruction_account( invoke_context: &InvokeContext, transaction_context: &TransactionContext, @@ -100,6 +102,21 @@ pub(crate) fn get_instruction_pubkey_with_idx( Ok(pubkey) } +pub(crate) fn get_instruction_account_short_meta_with_idx( + transaction_context: &TransactionContext, + idx: u16, +) -> Result { + let ix_ctx = transaction_context.get_current_instruction_context()?; + let tx_idx = ix_ctx.get_index_of_instruction_account_in_transaction(idx)?; + + let pubkey = *transaction_context.get_key_of_account_at_index(tx_idx)?; + let is_writable = ix_ctx.is_instruction_account_writable(idx)?; + Ok(ShortAccountMeta { + pubkey, + is_writable, + }) +} + pub(crate) fn debit_instruction_account_at_index( transaction_context: &TransactionContext, idx: u16, diff --git a/programs/magicblock/src/utils/instruction_utils.rs b/programs/magicblock/src/utils/instruction_utils.rs new file mode 100644 index 000000000..60e9e5e2b --- /dev/null +++ b/programs/magicblock/src/utils/instruction_utils.rs @@ -0,0 +1,202 @@ +use std::collections::HashMap; + +use magicblock_core::magic_program::MAGIC_CONTEXT_PUBKEY; +use solana_program_runtime::__private::Hash; +use solana_sdk::{ + instruction::{AccountMeta, Instruction}, + signature::{Keypair, Signer}, + transaction::Transaction, +}; + +use crate::{ + magicblock_instruction::{ + AccountModification, AccountModificationForInstruction, + MagicBlockInstruction, + }, + mutate_accounts::set_account_mod_data, + validator::{validator_authority, validator_authority_id}, + Pubkey, +}; + +pub struct InstructionUtils; +impl InstructionUtils { + // ----------------- + // Schedule Commit + // ----------------- + #[cfg(test)] + pub fn schedule_commit( + payer: &Keypair, + pubkeys: Vec, + recent_blockhash: Hash, + ) -> Transaction { + let ix = Self::schedule_commit_instruction(&payer.pubkey(), pubkeys); + Self::into_transaction(payer, ix, recent_blockhash) + } + + #[cfg(test)] + pub(crate) fn schedule_commit_instruction( + payer: &Pubkey, + pdas: Vec, + ) -> Instruction { + let mut account_metas = vec![ + AccountMeta::new(*payer, true), + AccountMeta::new(MAGIC_CONTEXT_PUBKEY, false), + ]; + for pubkey in &pdas { + account_metas.push(AccountMeta::new_readonly(*pubkey, true)); + } + Instruction::new_with_bincode( + crate::id(), + &MagicBlockInstruction::ScheduleCommit, + account_metas, + ) + } + + // ----------------- + // Schedule Commit and Undelegate + // ----------------- + #[cfg(test)] + pub fn schedule_commit_and_undelegate( + payer: &Keypair, + pubkeys: Vec, + recent_blockhash: Hash, + ) -> Transaction { + let ix = Self::schedule_commit_and_undelegate_instruction( + &payer.pubkey(), + pubkeys, + ); + Self::into_transaction(payer, ix, recent_blockhash) + } + + #[cfg(test)] + pub(crate) fn schedule_commit_and_undelegate_instruction( + payer: &Pubkey, + pdas: Vec, + ) -> Instruction { + let mut account_metas = vec![ + AccountMeta::new(*payer, true), + AccountMeta::new(MAGIC_CONTEXT_PUBKEY, false), + ]; + for pubkey in &pdas { + account_metas.push(AccountMeta::new_readonly(*pubkey, true)); + } + Instruction::new_with_bincode( + crate::id(), + &MagicBlockInstruction::ScheduleCommitAndUndelegate, + account_metas, + ) + } + + // ----------------- + // Scheduled Commit Sent + // ----------------- + pub fn scheduled_commit_sent( + scheduled_commit_id: u64, + recent_blockhash: Hash, + ) -> Transaction { + let ix = Self::scheduled_commit_sent_instruction( + &crate::id(), + &validator_authority_id(), + scheduled_commit_id, + ); + Self::into_transaction(&validator_authority(), ix, recent_blockhash) + } + + pub(crate) fn scheduled_commit_sent_instruction( + magic_block_program: &Pubkey, + validator_authority: &Pubkey, + scheduled_commit_id: u64, + ) -> Instruction { + let account_metas = vec![ + AccountMeta::new_readonly(*magic_block_program, false), + AccountMeta::new_readonly(*validator_authority, true), + ]; + Instruction::new_with_bincode( + *magic_block_program, + &MagicBlockInstruction::ScheduledCommitSent(scheduled_commit_id), + account_metas, + ) + } + + // ----------------- + // Accept Scheduled Commits + // ----------------- + pub fn accept_scheduled_commits(recent_blockhash: Hash) -> Transaction { + let ix = Self::accept_scheduled_commits_instruction(); + Self::into_transaction(&validator_authority(), ix, recent_blockhash) + } + + pub(crate) fn accept_scheduled_commits_instruction() -> Instruction { + let account_metas = vec![ + AccountMeta::new_readonly(validator_authority_id(), true), + AccountMeta::new(MAGIC_CONTEXT_PUBKEY, false), + ]; + Instruction::new_with_bincode( + crate::id(), + &MagicBlockInstruction::AcceptScheduleCommits, + account_metas, + ) + } + + // ----------------- + // ModifyAccounts + // ----------------- + pub fn modify_accounts( + account_modifications: Vec, + recent_blockhash: Hash, + ) -> Transaction { + let ix = Self::modify_accounts_instruction(account_modifications); + Self::into_transaction(&validator_authority(), ix, recent_blockhash) + } + + pub fn modify_accounts_instruction( + account_modifications: Vec, + ) -> Instruction { + let mut account_metas = + vec![AccountMeta::new(validator_authority_id(), true)]; + let mut account_mods: HashMap< + Pubkey, + AccountModificationForInstruction, + > = HashMap::new(); + for account_modification in account_modifications { + account_metas + .push(AccountMeta::new(account_modification.pubkey, false)); + let account_mod_for_instruction = + AccountModificationForInstruction { + lamports: account_modification.lamports, + owner: account_modification.owner, + executable: account_modification.executable, + data_key: account_modification + .data + .map(set_account_mod_data), + rent_epoch: account_modification.rent_epoch, + }; + account_mods.insert( + account_modification.pubkey, + account_mod_for_instruction, + ); + } + Instruction::new_with_bincode( + crate::id(), + &MagicBlockInstruction::ModifyAccounts(account_mods), + account_metas, + ) + } + + // ----------------- + // Utils + // ----------------- + pub(crate) fn into_transaction( + signer: &Keypair, + instruction: Instruction, + recent_blockhash: Hash, + ) -> Transaction { + let signers = &[&signer]; + Transaction::new_signed_with_payer( + &[instruction], + Some(&signer.pubkey()), + signers, + recent_blockhash, + ) + } +} diff --git a/programs/magicblock/src/utils/mod.rs b/programs/magicblock/src/utils/mod.rs index 9dd1481bb..556ac28cc 100644 --- a/programs/magicblock/src/utils/mod.rs +++ b/programs/magicblock/src/utils/mod.rs @@ -4,6 +4,7 @@ pub mod account_actions; pub mod accounts; #[cfg(not(test))] pub(crate) mod instruction_context_frames; +pub mod instruction_utils; // NOTE: there is no low level SDK currently that exposes the program address // we hardcode it here to avoid either having to pull in the delegation program