Callback Accounts
Last updated
Last updated
Callback accounts provide a way to define additional accounts to be used in the callback instruction for a computation. This is helpful when you want to use the output of a computation to modify an onchain account.
When to use callback accounts:
Storing computation results in persistent accounts
Updating game state, user balances, or protocol data
Writing results that exceed transaction size limits
Expanding on our example from before, let's say we want to save the result of our addition in an account for later use. Let's define an account first to save our data and an instruction to initialize it, as callback accounts must 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: Using `create_program_address` with the bump would be more efficient than `find_program_address`.
// Since this PDA is constant, you could also derive it at compile time and save it as a constant.
// We use find_program_address here for 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<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(),
});
// Save the result in our callback account too
ctx.accounts.add_result_account.sum = o.ciphertexts[0];
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.