Refactor - Use builtin function interface from rbpf (#31221)
* Removes test_program_entry_debug(). * Uses declare_process_instruction!() in all tests and benchmarks. * Replaces with ProcessInstructionWithContext with solana_rbpf::BuiltInFunction.
This commit is contained in:
parent
cb65a785bc
commit
e5490b8d09
|
@ -1808,7 +1808,7 @@ pub mod tests {
|
|||
matches::assert_matches,
|
||||
rand::{thread_rng, Rng},
|
||||
solana_entry::entry::{create_ticks, next_entry, next_entry_mut},
|
||||
solana_program_runtime::invoke_context::InvokeContext,
|
||||
solana_program_runtime::declare_process_instruction,
|
||||
solana_runtime::{
|
||||
genesis_utils::{
|
||||
self, create_genesis_config_with_vote_accounts, ValidatorVoteKeypairs,
|
||||
|
@ -2962,12 +2962,10 @@ pub mod tests {
|
|||
]
|
||||
}
|
||||
|
||||
fn mock_processor_ok(
|
||||
invoke_context: &mut InvokeContext,
|
||||
) -> std::result::Result<(), Box<dyn std::error::Error>> {
|
||||
invoke_context.consume_checked(1)?;
|
||||
declare_process_instruction!(mock_processor_ok, 1, |_invoke_context| {
|
||||
// Always succeeds
|
||||
Ok(())
|
||||
}
|
||||
});
|
||||
|
||||
let mock_program_id = solana_sdk::pubkey::new_rand();
|
||||
|
||||
|
@ -2993,12 +2991,9 @@ pub mod tests {
|
|||
let bankhash_ok = bank.hash();
|
||||
assert!(result.is_ok());
|
||||
|
||||
fn mock_processor_err(
|
||||
invoke_context: &mut InvokeContext,
|
||||
) -> std::result::Result<(), Box<dyn std::error::Error>> {
|
||||
declare_process_instruction!(mock_processor_err, 1, |invoke_context| {
|
||||
let instruction_errors = get_instruction_errors();
|
||||
|
||||
invoke_context.consume_checked(1)?;
|
||||
let err = invoke_context
|
||||
.transaction_context
|
||||
.get_current_instruction_context()
|
||||
|
@ -3006,13 +3001,11 @@ pub mod tests {
|
|||
.get_instruction_data()
|
||||
.first()
|
||||
.expect("Failed to get instruction data");
|
||||
Err(Box::new(
|
||||
instruction_errors
|
||||
.get(*err as usize)
|
||||
.expect("Invalid error index")
|
||||
.clone(),
|
||||
))
|
||||
}
|
||||
Err(instruction_errors
|
||||
.get(*err as usize)
|
||||
.expect("Invalid error index")
|
||||
.clone())
|
||||
});
|
||||
|
||||
let mut bankhash_err = None;
|
||||
|
||||
|
|
|
@ -11,7 +11,11 @@ use {
|
|||
timings::{ExecuteDetailsTimings, ExecuteTimings},
|
||||
},
|
||||
solana_measure::measure::Measure,
|
||||
solana_rbpf::{ebpf::MM_HEAP_START, vm::ContextObject},
|
||||
solana_rbpf::{
|
||||
ebpf::MM_HEAP_START,
|
||||
memory_region::MemoryMapping,
|
||||
vm::{BuiltInFunction, Config, ContextObject, ProgramResult},
|
||||
},
|
||||
solana_sdk::{
|
||||
account::{AccountSharedData, ReadableAccount},
|
||||
bpf_loader_deprecated,
|
||||
|
@ -45,26 +49,40 @@ macro_rules! declare_process_instruction {
|
|||
($process_instruction:ident, $cu_to_consume:expr, |$invoke_context:ident| $inner:tt) => {
|
||||
pub fn $process_instruction(
|
||||
invoke_context: &mut $crate::invoke_context::InvokeContext,
|
||||
) -> std::result::Result<(), Box<dyn std::error::Error>> {
|
||||
_arg0: u64,
|
||||
_arg1: u64,
|
||||
_arg2: u64,
|
||||
_arg3: u64,
|
||||
_arg4: u64,
|
||||
_memory_mapping: &mut $crate::solana_rbpf::memory_region::MemoryMapping,
|
||||
result: &mut $crate::solana_rbpf::vm::ProgramResult,
|
||||
) {
|
||||
fn process_instruction_inner(
|
||||
$invoke_context: &mut $crate::invoke_context::InvokeContext,
|
||||
) -> std::result::Result<(), solana_sdk::instruction::InstructionError> {
|
||||
$inner
|
||||
}
|
||||
if invoke_context
|
||||
.feature_set
|
||||
.is_active(&solana_sdk::feature_set::native_programs_consume_cu::id())
|
||||
let consumption_result = if $cu_to_consume > 0
|
||||
&& invoke_context
|
||||
.feature_set
|
||||
.is_active(&solana_sdk::feature_set::native_programs_consume_cu::id())
|
||||
{
|
||||
invoke_context.consume_checked($cu_to_consume)?;
|
||||
}
|
||||
process_instruction_inner(invoke_context)
|
||||
.map_err(|err| Box::new(err) as Box<dyn std::error::Error>)
|
||||
invoke_context.consume_checked($cu_to_consume)
|
||||
} else {
|
||||
Ok(())
|
||||
};
|
||||
*result = consumption_result
|
||||
.and_then(|_| {
|
||||
process_instruction_inner(invoke_context)
|
||||
.map(|_| 0)
|
||||
.map_err(|err| Box::new(err) as Box<dyn std::error::Error>)
|
||||
})
|
||||
.into();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub type ProcessInstructionWithContext =
|
||||
fn(&mut InvokeContext) -> Result<(), Box<dyn std::error::Error>>;
|
||||
pub type ProcessInstructionWithContext = BuiltInFunction<InvokeContext<'static>>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct BuiltinProgram {
|
||||
|
@ -74,15 +92,11 @@ pub struct BuiltinProgram {
|
|||
|
||||
impl std::fmt::Debug for BuiltinProgram {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
// These are just type aliases for work around of Debug-ing above pointers
|
||||
type ErasedProcessInstructionWithContext =
|
||||
fn(&'static mut InvokeContext<'static>) -> Result<(), Box<dyn std::error::Error>>;
|
||||
|
||||
// rustc doesn't compile due to bug without this work around
|
||||
// https://github.com/rust-lang/rust/issues/50280
|
||||
// https://users.rust-lang.org/t/display-function-pointer/17073/2
|
||||
let erased_instruction: ErasedProcessInstructionWithContext = self.process_instruction;
|
||||
write!(f, "{}: {:p}", self.program_id, erased_instruction)
|
||||
write!(
|
||||
f,
|
||||
"{}: {:p}",
|
||||
self.program_id, self.process_instruction as *const ProcessInstructionWithContext,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -724,21 +738,37 @@ impl<'a> InvokeContext<'a> {
|
|||
self.transaction_context
|
||||
.set_return_data(program_id, Vec::new())?;
|
||||
|
||||
let pre_remaining_units = self.get_remaining();
|
||||
let logger = self.get_log_collector();
|
||||
stable_log::program_invoke(&logger, &program_id, self.get_stack_height());
|
||||
let result = (entry.process_instruction)(self)
|
||||
.map(|()| {
|
||||
let pre_remaining_units = self.get_remaining();
|
||||
let mock_config = Config::default();
|
||||
let mut mock_memory_mapping = MemoryMapping::new(Vec::new(), &mock_config).unwrap();
|
||||
let mut result = ProgramResult::Ok(0);
|
||||
(entry.process_instruction)(
|
||||
// Removes lifetime tracking
|
||||
unsafe { std::mem::transmute::<&mut InvokeContext, &mut InvokeContext>(self) },
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
&mut mock_memory_mapping,
|
||||
&mut result,
|
||||
);
|
||||
let result = match result {
|
||||
ProgramResult::Ok(_) => {
|
||||
stable_log::program_success(&logger, &program_id);
|
||||
})
|
||||
.map_err(|err| {
|
||||
Ok(())
|
||||
}
|
||||
ProgramResult::Err(err) => {
|
||||
stable_log::program_failure(&logger, &program_id, err.as_ref());
|
||||
if let Some(err) = err.downcast_ref::<InstructionError>() {
|
||||
err.clone()
|
||||
Err(err.clone())
|
||||
} else {
|
||||
InstructionError::ProgramFailedToComplete
|
||||
Err(InstructionError::ProgramFailedToComplete)
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
let post_remaining_units = self.get_remaining();
|
||||
*compute_units_consumed = pre_remaining_units.saturating_sub(post_remaining_units);
|
||||
|
||||
|
@ -997,31 +1027,6 @@ mod tests {
|
|||
},
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_program_entry_debug() {
|
||||
fn mock_process_instruction(
|
||||
_invoke_context: &mut InvokeContext,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
Ok(())
|
||||
}
|
||||
fn mock_ix_processor(
|
||||
_invoke_context: &mut InvokeContext,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
Ok(())
|
||||
}
|
||||
let builtin_programs = &[
|
||||
BuiltinProgram {
|
||||
program_id: solana_sdk::pubkey::new_rand(),
|
||||
process_instruction: mock_process_instruction,
|
||||
},
|
||||
BuiltinProgram {
|
||||
program_id: solana_sdk::pubkey::new_rand(),
|
||||
process_instruction: mock_ix_processor,
|
||||
},
|
||||
];
|
||||
assert!(!format!("{builtin_programs:?}").is_empty());
|
||||
}
|
||||
|
||||
const MOCK_BUILTIN_COMPUTE_UNIT_COST: u64 = 1;
|
||||
|
||||
declare_process_instruction!(
|
||||
|
|
|
@ -9,6 +9,7 @@ extern crate eager;
|
|||
#[macro_use]
|
||||
extern crate solana_metrics;
|
||||
|
||||
pub use solana_rbpf;
|
||||
pub mod accounts_data_meter;
|
||||
pub mod compute_budget;
|
||||
pub mod executor_cache;
|
||||
|
|
|
@ -180,9 +180,13 @@ pub fn builtin_process_instruction(
|
|||
#[macro_export]
|
||||
macro_rules! processor {
|
||||
($process_instruction:expr) => {
|
||||
Some(|invoke_context: &mut solana_program_test::InvokeContext| {
|
||||
$crate::builtin_process_instruction($process_instruction, invoke_context)
|
||||
})
|
||||
Some(
|
||||
|invoke_context, _arg0, _arg1, _arg2, _arg3, _arg4, _memory_mapping, result| {
|
||||
*result = $crate::builtin_process_instruction($process_instruction, invoke_context)
|
||||
.map(|_| 0)
|
||||
.into();
|
||||
},
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -439,20 +439,34 @@ fn create_memory_mapping<'a, 'b, C: ContextObject>(
|
|||
|
||||
pub fn process_instruction(
|
||||
invoke_context: &mut InvokeContext,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
process_instruction_inner(invoke_context, false)
|
||||
_arg0: u64,
|
||||
_arg1: u64,
|
||||
_arg2: u64,
|
||||
_arg3: u64,
|
||||
_arg4: u64,
|
||||
_memory_mapping: &mut MemoryMapping,
|
||||
result: &mut ProgramResult,
|
||||
) {
|
||||
*result = process_instruction_inner(invoke_context, false).into();
|
||||
}
|
||||
|
||||
pub fn process_instruction_jit(
|
||||
invoke_context: &mut InvokeContext,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
process_instruction_inner(invoke_context, true)
|
||||
_arg0: u64,
|
||||
_arg1: u64,
|
||||
_arg2: u64,
|
||||
_arg3: u64,
|
||||
_arg4: u64,
|
||||
_memory_mapping: &mut MemoryMapping,
|
||||
result: &mut ProgramResult,
|
||||
) {
|
||||
*result = process_instruction_inner(invoke_context, true).into();
|
||||
}
|
||||
|
||||
fn process_instruction_inner(
|
||||
invoke_context: &mut InvokeContext,
|
||||
use_jit: bool,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
) -> Result<u64, Box<dyn std::error::Error>> {
|
||||
let log_collector = invoke_context.get_log_collector();
|
||||
let transaction_context = &invoke_context.transaction_context;
|
||||
let instruction_context = transaction_context.get_current_instruction_context()?;
|
||||
|
@ -564,6 +578,7 @@ fn process_instruction_inner(
|
|||
ic_logger_msg!(log_collector, "Invalid BPF loader id");
|
||||
Err(InstructionError::IncorrectProgramId)
|
||||
}
|
||||
.map(|_| 0)
|
||||
.map_err(|error| Box::new(error) as Box<dyn std::error::Error>);
|
||||
}
|
||||
|
||||
|
@ -612,11 +627,14 @@ fn process_instruction_inner(
|
|||
match &executor.program {
|
||||
LoadedProgramType::FailedVerification
|
||||
| LoadedProgramType::Closed
|
||||
| LoadedProgramType::DelayVisibility => Err(Box::new(InstructionError::InvalidAccountData)),
|
||||
| LoadedProgramType::DelayVisibility => {
|
||||
Err(Box::new(InstructionError::InvalidAccountData) as Box<dyn std::error::Error>)
|
||||
}
|
||||
LoadedProgramType::LegacyV0(executable) => execute(executable, invoke_context),
|
||||
LoadedProgramType::LegacyV1(executable) => execute(executable, invoke_context),
|
||||
_ => Err(Box::new(InstructionError::IncorrectProgramId)),
|
||||
_ => Err(Box::new(InstructionError::IncorrectProgramId) as Box<dyn std::error::Error>),
|
||||
}
|
||||
.map(|_| 0)
|
||||
}
|
||||
|
||||
fn process_loader_upgradeable_instruction(
|
||||
|
|
|
@ -538,7 +538,20 @@ pub fn process_instruction_transfer_authority(
|
|||
|
||||
pub fn process_instruction(
|
||||
invoke_context: &mut InvokeContext,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
_arg0: u64,
|
||||
_arg1: u64,
|
||||
_arg2: u64,
|
||||
_arg3: u64,
|
||||
_arg4: u64,
|
||||
_memory_mapping: &mut MemoryMapping,
|
||||
result: &mut ProgramResult,
|
||||
) {
|
||||
*result = process_instruction_inner(invoke_context).into();
|
||||
}
|
||||
|
||||
pub fn process_instruction_inner(
|
||||
invoke_context: &mut InvokeContext,
|
||||
) -> Result<u64, Box<dyn std::error::Error>> {
|
||||
let use_jit = true;
|
||||
let log_collector = invoke_context.get_log_collector();
|
||||
let transaction_context = &invoke_context.transaction_context;
|
||||
|
@ -602,12 +615,13 @@ pub fn process_instruction(
|
|||
LoadedProgramType::FailedVerification
|
||||
| LoadedProgramType::Closed
|
||||
| LoadedProgramType::DelayVisibility => {
|
||||
Err(Box::new(InstructionError::InvalidAccountData))
|
||||
Err(Box::new(InstructionError::InvalidAccountData) as Box<dyn std::error::Error>)
|
||||
}
|
||||
LoadedProgramType::Typed(executable) => execute(invoke_context, executable),
|
||||
_ => Err(Box::new(InstructionError::IncorrectProgramId)),
|
||||
_ => Err(Box::new(InstructionError::IncorrectProgramId) as Box<dyn std::error::Error>),
|
||||
}
|
||||
}
|
||||
.map(|_| 0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -3,15 +3,16 @@
|
|||
extern crate test;
|
||||
|
||||
use {
|
||||
solana_program_runtime::with_mock_invoke_context,
|
||||
solana_program_runtime::invoke_context::mock_process_instruction,
|
||||
solana_sdk::{
|
||||
account::{create_account_for_test, Account, AccountSharedData},
|
||||
clock::{Clock, Slot},
|
||||
hash::Hash,
|
||||
instruction::AccountMeta,
|
||||
pubkey::Pubkey,
|
||||
slot_hashes::{SlotHashes, MAX_ENTRIES},
|
||||
sysvar,
|
||||
transaction_context::{IndexOfAccount, InstructionAccount, TransactionAccount},
|
||||
transaction_context::TransactionAccount,
|
||||
},
|
||||
solana_vote_program::{
|
||||
vote_instruction::VoteInstruction,
|
||||
|
@ -22,12 +23,7 @@ use {
|
|||
test::Bencher,
|
||||
};
|
||||
|
||||
fn create_accounts() -> (
|
||||
Slot,
|
||||
SlotHashes,
|
||||
Vec<TransactionAccount>,
|
||||
Vec<InstructionAccount>,
|
||||
) {
|
||||
fn create_accounts() -> (Slot, SlotHashes, Vec<TransactionAccount>, Vec<AccountMeta>) {
|
||||
// vote accounts are usually almost full of votes in normal operation
|
||||
let num_initial_votes = MAX_LOCKOUT_HISTORY as Slot;
|
||||
|
||||
|
@ -80,51 +76,48 @@ fn create_accounts() -> (
|
|||
),
|
||||
(authority_pubkey, AccountSharedData::default()),
|
||||
];
|
||||
let mut instruction_accounts = (0..4)
|
||||
.map(|index_in_callee| InstructionAccount {
|
||||
index_in_transaction: (1 as IndexOfAccount).saturating_add(index_in_callee),
|
||||
index_in_caller: index_in_callee,
|
||||
index_in_callee,
|
||||
let mut instruction_account_metas = (0..4)
|
||||
.map(|index_in_callee| AccountMeta {
|
||||
pubkey: transaction_accounts[1usize.saturating_add(index_in_callee)].0,
|
||||
is_signer: false,
|
||||
is_writable: false,
|
||||
})
|
||||
.collect::<Vec<InstructionAccount>>();
|
||||
instruction_accounts[0].is_writable = true;
|
||||
instruction_accounts[3].is_signer = true;
|
||||
.collect::<Vec<AccountMeta>>();
|
||||
instruction_account_metas[0].is_writable = true;
|
||||
instruction_account_metas[3].is_signer = true;
|
||||
|
||||
(
|
||||
num_initial_votes,
|
||||
slot_hashes,
|
||||
transaction_accounts,
|
||||
instruction_accounts,
|
||||
instruction_account_metas,
|
||||
)
|
||||
}
|
||||
|
||||
fn bench_process_vote_instruction(
|
||||
bencher: &mut Bencher,
|
||||
transaction_accounts: Vec<TransactionAccount>,
|
||||
instruction_accounts: Vec<InstructionAccount>,
|
||||
instruction_account_metas: Vec<AccountMeta>,
|
||||
instruction_data: Vec<u8>,
|
||||
) {
|
||||
bencher.iter(|| {
|
||||
let transaction_accounts = transaction_accounts.clone();
|
||||
with_mock_invoke_context!(invoke_context, transaction_context, transaction_accounts);
|
||||
invoke_context
|
||||
.transaction_context
|
||||
.get_next_instruction_context()
|
||||
.unwrap()
|
||||
.configure(&[0], &instruction_accounts, &instruction_data);
|
||||
invoke_context.push().unwrap();
|
||||
assert!(
|
||||
solana_vote_program::vote_processor::process_instruction(&mut invoke_context).is_ok()
|
||||
mock_process_instruction(
|
||||
&solana_vote_program::id(),
|
||||
Vec::new(),
|
||||
&instruction_data,
|
||||
transaction_accounts.clone(),
|
||||
instruction_account_metas.clone(),
|
||||
Ok(()),
|
||||
solana_vote_program::vote_processor::process_instruction,
|
||||
|_invoke_context| {},
|
||||
|_invoke_context| {},
|
||||
);
|
||||
invoke_context.pop().unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn bench_process_vote(bencher: &mut Bencher) {
|
||||
let (num_initial_votes, slot_hashes, transaction_accounts, instruction_accounts) =
|
||||
let (num_initial_votes, slot_hashes, transaction_accounts, instruction_account_metas) =
|
||||
create_accounts();
|
||||
|
||||
let num_vote_slots = 4;
|
||||
|
@ -145,14 +138,14 @@ fn bench_process_vote(bencher: &mut Bencher) {
|
|||
bench_process_vote_instruction(
|
||||
bencher,
|
||||
transaction_accounts,
|
||||
instruction_accounts,
|
||||
instruction_account_metas,
|
||||
instruction_data,
|
||||
);
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn bench_process_vote_state_update(bencher: &mut Bencher) {
|
||||
let (num_initial_votes, slot_hashes, transaction_accounts, instruction_accounts) =
|
||||
let (num_initial_votes, slot_hashes, transaction_accounts, instruction_account_metas) =
|
||||
create_accounts();
|
||||
|
||||
let num_vote_slots = MAX_LOCKOUT_HISTORY as Slot;
|
||||
|
@ -175,7 +168,7 @@ fn bench_process_vote_state_update(bencher: &mut Bencher) {
|
|||
bench_process_vote_instruction(
|
||||
bencher,
|
||||
transaction_accounts,
|
||||
instruction_accounts,
|
||||
instruction_account_metas,
|
||||
instruction_data,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ extern crate test;
|
|||
|
||||
use {
|
||||
log::*,
|
||||
solana_program_runtime::invoke_context::InvokeContext,
|
||||
solana_program_runtime::declare_process_instruction,
|
||||
solana_runtime::{
|
||||
bank::{test_utils::goto_end_of_slot, *},
|
||||
bank_client::BankClient,
|
||||
|
@ -34,12 +34,6 @@ const NOOP_PROGRAM_ID: [u8; 32] = [
|
|||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
|
||||
];
|
||||
|
||||
fn process_instruction(
|
||||
_invoke_context: &mut InvokeContext,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn create_builtin_transactions(
|
||||
bank_client: &BankClient,
|
||||
mint_keypair: &Keypair,
|
||||
|
@ -131,6 +125,11 @@ fn do_bench_transactions(
|
|||
// freeze bank so that slot hashes is populated
|
||||
bank.freeze();
|
||||
|
||||
declare_process_instruction!(process_instruction, 1, |_invoke_context| {
|
||||
// Do nothing
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let mut bank = Bank::new_from_parent(&Arc::new(bank), &Pubkey::default(), 1);
|
||||
bank.add_builtin(
|
||||
"builtin_program",
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
#[cfg(RUSTC_WITH_SPECIALIZATION)]
|
||||
use solana_frozen_abi::abi_example::AbiExample;
|
||||
use {
|
||||
solana_frozen_abi::abi_example::AbiExample, solana_program_runtime::declare_process_instruction,
|
||||
};
|
||||
use {
|
||||
solana_program_runtime::invoke_context::ProcessInstructionWithContext,
|
||||
solana_sdk::{feature_set, pubkey::Pubkey, stake},
|
||||
|
@ -36,10 +38,15 @@ impl fmt::Debug for Builtin {
|
|||
#[cfg(RUSTC_WITH_SPECIALIZATION)]
|
||||
impl AbiExample for Builtin {
|
||||
fn example() -> Self {
|
||||
declare_process_instruction!(process_instruction, 1, |_invoke_context| {
|
||||
// Do nothing
|
||||
Ok(())
|
||||
});
|
||||
|
||||
Self {
|
||||
name: String::default(),
|
||||
id: Pubkey::default(),
|
||||
process_instruction_with_context: |_invoke_context| Ok(()),
|
||||
process_instruction_with_context: process_instruction,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue