Callback accounts provide a way to return define additional accounts to be used in the callback instruction for a computation. This is helpful for when you want to use the output of a computation to modify an onchain account. Expanding on our , let's say we want to now save the result of our addition in an account for later (We won't redefine the confidential instruction since that stays the same). Let's define an account first to save our data & an instruction to initialize it, as callback accounts have to already exist and cannot change in size when being used as part of a computation:
#[account]
#[derive(InitSpace)]
pub struct SecretAdditionResult{
pub sum: u8,
}
pub fn init(ctx: Context<Initialize>) -> Result<()> {
Ok(())
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub signer: Signer<'info>,
#[account(
init,
payer = signer,
seeds = [b"AdditionResult"],
space = 8 + SecretAdditionResult::INIT_SPACE,
// Note: In a real implementation you should usually save the bump too,
// but for the sake of simplicity in this example we skip that
bump
)]
pub add_result_account: Account<'info, SecretAdditionResult>,
pub system_program: Program<'info, System>,
}
Now that we have defined and initialized our account, let's use it in our existing example from before. Let's start with the queue step:
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<()> {
// Note here: If we have the bump, using `create_program_address` would be more efficient.
// Additionally, since this PDA is constant, technically we could also derive it at compile time already
// and save it as a constant. We do it like this here for the sake of simplicity.
let addition_result_pda = Pubkey::find_program_address(&[b"AdditionResult"], ctx.program_id).0;
// 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,
// Additional callback accounts we want to receive when the computation is complete,
// in this case our account from before. We specify it's pubkey and that we want it to be
// passed as writable in the callback since we plan to edit it.
vec![CallbackAccount{
pubkey: addition_result_pda,
is_writable: true,
}],
// 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(())
}
/* The AddTogether accounts struct stays exactly the same */
Note here how we added the account we need in the callback Vec inside queue_computation, but since we didn't actually read or write to the account itself we don't need to pass it as part of the accounts struct. Let's take a look at how the callback instruction changes next:
// Macro provided by the Arcium Macros 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(),
});
// Save the result in our callback account too
ctx.accounts.add_result_account.sum = bytes[48..80].try_into().unwrap();
Ok(())
}
#[callback_accounts("add_together", payer)]
#[derive(Accounts)]
pub struct AddTogetherCallback<'info> {
#[account(mut)]
pub payer: Signer<'info>,
pub arcium_program: Program<'info, Arcium>,
#[account(
address = derive_comp_def_pda!(COMP_DEF_OFFSET_ADD_TOGETHER)
)]
pub comp_def_account: Account<'info, ComputationDefinitionAccount>,
/// CHECK: instructions_sysvar, checked by the account constraint
#[account(address = ::anchor_lang::solana_program::sysvar::instructions::ID)]
pub instructions_sysvar: AccountInfo<'info>,
// Append the callback account(s) in the same order we provided them in the queue function
// call
#[account(
mut,
seeds = [b"AdditionResult"],
// Note: In a real implementation you should usually save the bump too,
// but for the sake of simplicity in this example we skip that
bump
)]
pub add_result_account: Account<'info, SecretAdditionResult>,
}
What did we change? We appended the callback account we plan to receive to the end of the accounts struct and that's it, Arcium takes care of the rest.