Callback Accounts
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.
Prerequisites: Before diving into callback accounts, make sure you've read:
Basic program invocation guide - fundamentals of queuing computations and defining callback instructions
Callback Type Generation - how output types like
AddTogetherOutput
are automatically generated from encrypted instructionsArcis inputs/outputs - handling encrypted data types
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
Complete Example
Expanding on our basic example, let's say we want to save the result of our addition in an account for later use. We'll walk through the complete implementation step by step.
Step 1: Define the Account Structure
First, define an account to store our computation result:
#[account]
#[derive(InitSpace)]
pub struct SecretAdditionResult {
pub sum: [u8; 32], // Store the encrypted result as ciphertext
}
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>,
}
Step 2: Modify the Queue Function
There are two ways to specify callback instructions in your queue_computation
call:
Recommended: Using callback_ix() Helper
The callback_ix()
helper method is the preferred approach because it automatically handles the 3 required standard accounts and is less error-prone.
What callback_ix() does automatically:
Creates a CallbackInstruction with the proper instruction data
Automatically includes 3 standard accounts:
arcium_program
,comp_def_account
,instructions_sysvar
Accepts custom accounts through the
&[CallbackAccount]
parameterEliminates boilerplate and prevents errors
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 (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,
// Using callback_ix() helper - automatically includes the 3 standard accounts
// (arcium_program, comp_def_account, instructions_sysvar) plus our custom account
vec![AddTogetherCallback::callback_ix(&[
CallbackAccount {
pubkey: addition_result_pda,
is_writable: true, // Tells nodes to mark this account as writable in the transaction
}
])],
)?;
Ok(())
}
/* The AddTogether accounts struct stays exactly the same as shown in the basic guide */
Understanding What Happens: Manual CallbackInstruction
For educational purposes, here's what callback_ix()
generates under the hood. This manual approach is functionally equivalent but more verbose and error-prone:
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 (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,
// Manual approach: Define which callback instruction to call when the computation is complete.
// We specify the program ID, instruction discriminator, and all accounts needed
// for the callback, including our result account which we want to be writable.
vec![CallbackInstruction {
program_id: ID_CONST,
discriminator: instruction::AddTogetherCallback::DISCRIMINATOR.to_vec(),
accounts: vec![
// Standard accounts (always required, in this order)
CallbackAccount {
pubkey: ARCIUM_PROGRAM_ID,
is_writable: false,
},
CallbackAccount {
pubkey: derive_comp_def_pda!(COMP_DEF_OFFSET_ADD_TOGETHER),
is_writable: false,
},
CallbackAccount {
pubkey: INSTRUCTIONS_SYSVAR_ID,
is_writable: false,
},
// Custom accounts (your callback-specific accounts)
CallbackAccount {
pubkey: addition_result_pda,
is_writable: true, // Tells nodes to mark this account as writable in the transaction
}
]
}],
)?;
Ok(())
}
/* The AddTogether accounts struct stays exactly the same as shown in the basic guide */
Key Point: Both approaches are functionally equivalent. The callback_ix()
method automatically generates the exact same CallbackInstruction
structure as the manual approach, but with less code and reduced chance for errors.
Important: We added the account to the callback (either via callback_ix()
parameter or CallbackInstruction.accounts
) but didn't include it in the AddTogether accounts struct because we don't read or write to it during the queue function - only during the callback.
Step 3: Implement the Callback Function
The callback instruction receives the accounts in the exact order specified in the queue function:
// 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(),
});
// Save the result in our callback account too
ctx.accounts.add_result_account.sum = o.ciphertexts[0];
Ok(())
}
#[callback_accounts("add_together")]
#[derive(Accounts)]
pub struct AddTogetherCallback<'info> {
// Standard accounts (match the first 3 in CallbackInstruction.accounts)
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>,
// Custom accounts (match remaining accounts in CallbackInstruction.accounts)
#[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>,
}
Key Requirements & Constraints
Account Ordering
The accounts in your callback struct must match exactly the order in CallbackInstruction.accounts
:
First three accounts are always standard (arcium_program, comp_def_account, instructions_sysvar)
Custom accounts follow in the exact sequence you specified
Account Creation Rules
Can create accounts in the queue computation function (user pays rent)
Cannot create accounts during callback execution (would require nodes to pay)
Accounts must exist before the callback executes
Account size cannot change during callback
Writability Requirements
Set
is_writable: true
in CallbackAccount to tell nodes to mark the account as writableThe account must have
#[account(mut)]
in the callback structWithout proper writability flags, mutations will fail
Troubleshooting
Account not found: Ensure the account exists before callback execution. Initialize it in the queue function or a separate instruction.
Order mismatch errors: Double-check that your callback struct accounts are in the exact same order as the CallbackInstruction.accounts vector.
Cannot modify account: Verify both is_writable: true
in CallbackAccount and #[account(mut)]
in the callback struct are set.
Size errors: Callback accounts cannot be resized. Allocate sufficient space when creating the account.
Understanding callback_ix() in Detail
The callback_ix()
method you see throughout these examples is a convenient helper that's automatically generated by the #[callback_accounts]
macro.
How callback_ix() Works
When you define a callback struct with #[callback_accounts("instruction_name")]
, the macro automatically generates a callback_ix()
method that:
Creates a CallbackInstruction with the proper instruction data
Automatically includes 3 standard accounts that every callback needs:
arcium_program
: The Arcium program that will invoke your callbackcomp_def_account
: The computation definition account for your encrypted instructioninstructions_sysvar
: Solana's instructions sysvar for transaction validation
Accepts custom accounts through the
&[CallbackAccount]
parameter
Usage Patterns
Basic usage (no custom accounts):
vec![AddTogetherCallback::callback_ix(&[])]
The empty array indicates no custom accounts needed beyond the 3 standard ones.
Advanced usage (with custom accounts):
vec![AddTogetherCallback::callback_ix(&[
CallbackAccount {
pubkey: my_account.key(),
is_writable: true,
},
// ... more custom accounts
])]
Why Use callback_ix()?
The callback_ix()
helper is the recommended approach because it:
Eliminates boilerplate: No need to manually construct CallbackInstruction
Prevents errors: Automatically includes all required standard accounts
Maintains consistency: Ensures your callback instructions follow the correct format
Simplifies maintenance: Changes to callback requirements are handled by the macro
Going Further
This guide covered the advanced patterns for working with callback accounts. To understand the fundamentals of callback instructions, see our basic program invocation guide.
For handling different types of encrypted data inputs and outputs, see Arcis inputs/outputs.
Last updated