Before reading this, we recommend having read the section and the section.
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 u8s, 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<...> and output: ComputationOutputs,.
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 (Ciphertext, Ciphertext, u8)
let args = vec![
Argument::ArcisPubkey(pub_key),
Argument::PlaintextU128(nonce),
Argument::EncryptedU8(ciphertext_0),
Argument::EncryptedU8(ciphertext_1),
];
// 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,
// Accounts needed for the callback instruction
vec![],
// 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
)?;
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,) -> Result<()> {
let bytes = if let ComputationOutputs::Bytes(bytes) = output {
bytes
} else {
return Err(ErrorCode::AbortedComputation.into());
};
emit!(SumEvent {
sum: bytes[48..80].try_into().unwrap(),
nonce: bytes[32..48].try_into().unwrap(),
});
Ok(())
}
Let's also have a look at the Accounts structs for each of these instructions:
That's a lot of accounts to remember! But don't worry, these can just be copy-pasted for any confidential instruction you want to invoke. The only thing you need to change is COMP_DEF_OFFSET_ADD_TOGETHER as a parameter for the derive_comp_def_pda macro and the name of the confidential instruction in the queue_computation_accounts macro. How about the accounts for the callback instruction?
#[callback_accounts("add_together", payer)]
#[derive(Accounts)]
pub struct AddTogetherCallback<'info> {
#[account(mut)]
pub payer: Signer<'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 to 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. But what if we don't just want to return a raw value and need some additional accounts? Check out for how to handle encrypted data and for returning additional accounts in the callback.