Invoking a Computation from your Solana program
Before reading this, we recommend having read the Computation Lifecycle section, the Arcis inputs/outputs section, and the Callback Type Generation guide to understand how output types like AddTogetherOutput
are automatically generated from encrypted instructions.
The Basics
Let's say we have the following encrypted instruction and want to invoke it from our MXE.
#[encrypted]
mod circuits {
use arcis_imports::*;
pub struct InputValues {
v1: u8,
v2: u8,
}
#[instruction]
pub fn add_together(input_ctxt: Enc<Shared, InputValues>) -> Enc<Shared, u16> {
let input = input_ctxt.to_arcis();
let sum = input.v1 as u16 + input.v2 as u16;
input_ctxt.owner.from_arcis(sum)
}
}
To do this, we first need to receive the encrypted parameter of type InputValues
which contains two encrypted u8
s, then encode them into the Argument
format, and finally queue the computation for execution. Additionally, we need to define a callback instruction that will be invoked when the computation is complete. Callback instructions have a few requirements:
They must be defined with the
#[arcium_callback(encrypted_ix = "encrypted_ix_name")]
macro.They must have exactly two arguments:
ctx: Context<...>
andoutput: ComputationOutputs<T>
whereT
is named as{encrypted_ix_name}Output
.
For passing encrypted arguments, if the corresponding argument is Enc<Shared, T>
, then we need to pass the Argument::ArcisPubkey(pub_key)
and Argument::PlaintextU128(nonce)
, before the ciphertext. If the corresponding argument is Enc<Mxe, T>
, then we only need to pass the nonce as Argument::PlaintextU128(nonce)
and the ciphertext. Ciphertexts are passed as Argument::EncryptedXYZ(ciphertext)
where XYZ
is the type of the ciphertext, with the possibilities being EncryptedU8
, EncryptedU16
, EncryptedU32
, EncryptedU64
, EncryptedU128
, EncryptedBool
.
pub fn add_together(
ctx: Context<AddTogether>,
computation_offset: u64,
ciphertext_0: [u8; 32],
ciphertext_1: [u8; 32],
pub_key: [u8; 32],
nonce: u128,
) -> Result<()> {
// Build the args the confidential instruction expects (ArcisPubkey, nonce, EncryptedU8, EncryptedU8)
let args = vec![
Argument::ArcisPubkey(pub_key),
Argument::PlaintextU128(nonce),
Argument::EncryptedU8(ciphertext_0),
Argument::EncryptedU8(ciphertext_1),
];
// Set the bump for the sign_pda_account
ctx.accounts.sign_pda_account.bump = ctx.bumps.sign_pda_account;
// Build & queue our computation (via CPI to the Arcium program)
queue_computation(
ctx.accounts,
// Random offset for the computation
computation_offset,
// The one-time inputs our confidential instruction expects
args,
// Callback server address
// None here because the output of the confidential instruction can fit into a solana transaction
// as its just 1 ciphertext which is 32 bytes
None,
// Use callback_ix() helper to generate the callback instruction
vec![AddTogetherCallback::callback_ix(&[])], // Empty array = no custom accounts
)?;
Ok(())
}
// Macro provided by the Arcium SDK to define a callback instruction.
#[arcium_callback(encrypted_ix = "add_together")]
pub fn add_together_callback(
ctx: Context<AddTogetherCallback>,
output: ComputationOutputs<AddTogetherOutput>,
) -> Result<()> {
let o = match output {
ComputationOutputs::Success(AddTogetherOutput { field_0 }) => field_0,
_ => return Err(ErrorCode::AbortedComputation.into()),
};
emit!(SumEvent {
sum: o.ciphertexts[0],
nonce: o.nonce.to_le_bytes(),
});
Ok(())
}
Let's also have a look at the Accounts
structs for each of these instructions:
/// Accounts required to invoke the `add_together` encrypted instruction.
/// `add_together` must be the name of the encrypted instruction we're invoking.
#[queue_computation_accounts("add_together", payer)]
#[derive(Accounts)]
#[instruction(computation_offset: u64)]
pub struct AddTogether<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init_if_needed,
space = 9,
payer = payer,
seeds = [&SIGN_PDA_SEED],
bump,
address = derive_sign_pda!(),
)]
pub sign_pda_account: Account<'info, SignerAccount>,
#[account(
address = derive_mxe_pda!()
)]
pub mxe_account: Account<'info, MXEAccount>,
#[account(
mut,
address = derive_mempool_pda!()
)]
/// CHECK: mempool_account, checked by the arcium program.
pub mempool_account: UncheckedAccount<'info>,
#[account(
mut,
address = derive_execpool_pda!()
)]
/// CHECK: executing_pool, checked by the arcium program.
pub executing_pool: UncheckedAccount<'info>,
#[account(
mut,
address = derive_comp_pda!(computation_offset)
)]
/// CHECK: computation_account, checked by the arcium program.
pub computation_account: UncheckedAccount<'info>,
#[account(
address = derive_comp_def_pda!(COMP_DEF_OFFSET_ADD_TOGETHER)
)]
pub comp_def_account: Account<'info, ComputationDefinitionAccount>,
#[account(
mut,
address = derive_cluster_pda!(mxe_account)
)]
pub cluster_account: Account<'info, Cluster>,
#[account(
mut,
address = ARCIUM_FEE_POOL_ACCOUNT_ADDRESS,
)]
pub pool_account: Account<'info, FeePool>,
#[account(
address = ARCIUM_CLOCK_ACCOUNT_ADDRESS
)]
pub clock_account: Account<'info, ClockAccount>,
pub system_program: Program<'info, System>,
pub arcium_program: Program<'info, Arcium>,
}
That's a lot of accounts to remember! Here's what each one does:
Core MXE Accounts:
mxe_account
: Your MXE's metadata and configurationmempool_account
: Queue where computations wait to be processedexecuting_pool
: Tracks computations currently being executedcomputation_account
: Stores individual computation data and resultscomp_def_account
: Definition of your encrypted instruction (circuit)
Arcium Network Accounts:
cluster_account
: The MPC cluster that will process your computationpool_account
: Arcium's fee collection accountclock_account
: Network timing information
System Accounts:
payer
: Pays transaction fees and rentsign_pda_account
: PDA signer for the computationsystem_program
: Solana's system program for account creationarcium_program
: Arcium's core program that orchestrates MPC
The good news is these can be copy-pasted for any confidential instruction. You only need to change:
COMP_DEF_OFFSET_ADD_TOGETHER
to match your instruction nameThe instruction name in the
queue_computation_accounts
macro
How about the accounts for the callback instruction?
#[callback_accounts("add_together")]
#[derive(Accounts)]
pub struct AddTogetherCallback<'info> {
pub arcium_program: Program<'info, Arcium>,
/// Like above, COMP_DEF_PDA_SEED is a constant defined in the Arcium SDK.
/// COMP_DEF_OFFSET_ADD_TOGETHER is an encrypted instruction specific u32
/// offset which can be calculated with `comp_def_offset("add_together")`, where
/// comp_def_offset is a function provided by the Arcium SDK and `add_together`
/// is the name of the encrypted instruction we're invoking.
#[account(
address = derive_comp_def_pda!(COMP_DEF_OFFSET_ADD_TOGETHER)
)]
pub comp_def_account: Account<'info, ComputationDefinitionAccount>,
#[account(address = ::anchor_lang::solana_program::sysvar::instructions::ID)]
/// CHECK: instructions_sysvar, checked by the account constraint
pub instructions_sysvar: AccountInfo<'info>,
}
Here it's a lot fewer accounts fortunately! Like with the AddTogether
struct, we need to change the parameter for the derive_comp_def_pda
macro and in the callback_accounts
macro depending on the encrypted instruction we're invoking.
The callback_ix()
method is a convenient helper generated by the #[callback_accounts]
macro that automatically creates the proper callback instruction with all required accounts.
But what if we don't just want to return a raw value and need some additional accounts? Check out input/outputs for how to handle encrypted data and callback accounts for returning additional accounts in the callback.
Last updated