Skip to main content

1. Arcium.toml Backends (no change required)

Arcium.toml stays the same for this migration. Only the Cerberus backend is supported at the moment; Manticore is unavailable. If your config already uses Cerberus, no action is needed.

2. Update Arcium Rust dependencies

Update your dependencies to v0.5.1:
# Update program dependencies
cd programs/your-program-name
cargo update --package arcium-client --precise 0.5.1
cargo update --package arcium-macros --precise 0.5.1
cargo update --package arcium-anchor --precise 0.5.1

# Update encrypted-ixs dependencies
cd ../../encrypted-ixs
cargo update --package arcis-imports --precise 0.5.1

3. Update Arcium TS dependencies

npm install @arcium-hq/[email protected]

4. Update TypeScript environment variable usage

The environment variable and type have changed from a public key to a cluster offset:
// Before v0.5.1
const arciumEnv = getArciumEnv();
console.log(arciumEnv.arciumClusterPubkey);  // PublicKey
// v0.5.1
const arciumEnv = getArciumEnv();
console.log(arciumEnv.arciumClusterOffset);  // number
Environment variable change:
  • Before: ARCIUM_CLUSTER_PUBKEY (base58 pubkey string)
  • After: ARCIUM_CLUSTER_OFFSET (integer)

5. Update TypeScript SDK Function Names

Several functions have been renamed for consistency: Program Address Function:
// Before v0.5.1
import { getArciumProgAddress } from "@arcium-hq/client";
const address = getArciumProgAddress();
// v0.5.1
import { getArciumProgramId } from "@arcium-hq/client";
const address = getArciumProgramId();
Account Data Functions:
// Before v0.5.1
const mempool = await getMempoolAccData(provider, address);
const execpool = await getExecutingPoolAccData(provider, address);
// v0.5.1
const mempool = await getMempoolAccInfo(provider, address);
const execpool = await getExecutingPoolAccInfo(provider, address);
Parameter Name Updates: Functions now use mxeProgramId (camelCase) instead of mxeProgramID:
// Before v0.5.1
await getMXEPublicKey(provider, mxeProgramID);
await uploadCircuit(provider, name, mxeProgramID, circuit);
// v0.5.1
await getMXEPublicKey(provider, mxeProgramId);
await uploadCircuit(provider, name, mxeProgramId, circuit);
Old FunctionNew Function
getArciumProgAddress()getArciumProgramId()
getMempoolAccData()getMempoolAccInfo()
getExecutingPoolAccData()getExecutingPoolAccInfo()
getArciumProgramReadonly()getArciumProgram()
getArxAccPDA()getArxNodeAccAddress()

6. Update TypeScript PDA derivation functions

Several PDA derivation functions now take clusterOffset instead of programId.
Note: getMXEAccAddress() still uses program.programId - only cluster-related PDAs changed.
// Before v0.5.1
import {
  getMXEAccAddress,
  getMempoolAccAddress,
  getExecutingPoolAccAddress,
  getComputationAccAddress,
} from "@arcium-hq/client";

.accountsPartial({
  computationAccount: getComputationAccAddress(
    program.programId,
    computationOffset
  ),
  clusterAccount: arciumEnv.arciumClusterPubkey,
  mxeAccount: getMXEAccAddress(program.programId),
  mempoolAccount: getMempoolAccAddress(program.programId),
  executingPool: getExecutingPoolAccAddress(program.programId),
})
// v0.5.1
import {
  getMXEAccAddress,
  getMempoolAccAddress,
  getExecutingPoolAccAddress,
  getComputationAccAddress,
  getClusterAccAddress,  // New import
} from "@arcium-hq/client";

.accountsPartial({
  computationAccount: getComputationAccAddress(
    arciumEnv.arciumClusterOffset,  // Changed from program.programId
    computationOffset
  ),
  clusterAccount: getClusterAccAddress(arciumEnv.arciumClusterOffset),  // Now derived
  mxeAccount: getMXEAccAddress(program.programId),  // Unchanged
  mempoolAccount: getMempoolAccAddress(arciumEnv.arciumClusterOffset),  // Changed
  executingPool: getExecutingPoolAccAddress(arciumEnv.arciumClusterOffset),  // Changed
})

7. Update queue_computation Call

The queue_computation function has two changes:
  1. New parameter: cu_price_micro: u64 for priority fees
  2. Updated callback_ix: Now requires computation_offset and mxe_account
// Before v0.5.1
queue_computation(
    ctx.accounts,
    computation_offset,
    args,
    None,
    vec![MyCallback::callback_ix(&[])],
    1,
)?;
// v0.5.1
queue_computation(
    ctx.accounts,
    computation_offset,
    args,
    None,
    vec![MyCallback::callback_ix(
        computation_offset,
        &ctx.accounts.mxe_account,
        &[]
    )?],
    1,
    0,  // cu_price_micro: priority fee in microlamports (0 = no priority fee)
)?;
Priority Fee Notes:
  • Set cu_price_micro to 0 for standard processing
  • Use higher values (e.g., 50000) for faster processing during network congestion

8. Update on-chain Argument API

Replace the vec![Argument::...] pattern with the new ArgBuilder API:
// Before v0.5.1
let args = vec![
    Argument::ArcisPubkey(pub_key),
    Argument::PlaintextU128(nonce),
    Argument::EncryptedU8(ciphertext_0),
    Argument::EncryptedU8(ciphertext_1),
];
// v0.5.1
let args = ArgBuilder::new()
    .x25519_pubkey(pub_key)
    .plaintext_u128(nonce)
    .encrypted_u8(ciphertext_0)
    .encrypted_u8(ciphertext_1)
    .build();

Available ArgBuilder Methods

MethodInput TypeDescription
plaintext_bool(value)boolBoolean value
plaintext_u8(value)u8Unsigned 8-bit integer
plaintext_i8(value)i8Signed 8-bit integer
plaintext_u16(value)u16Unsigned 16-bit integer
plaintext_i16(value)i16Signed 16-bit integer
plaintext_u32(value)u32Unsigned 32-bit integer
plaintext_i32(value)i32Signed 32-bit integer
plaintext_u64(value)u64Unsigned 64-bit integer
plaintext_i64(value)i64Signed 64-bit integer
plaintext_u128(value)u128Unsigned 128-bit integer
plaintext_i128(value)i128Signed 128-bit integer
plaintext_float(value)f64Floating point
plaintext_point(value)[u8; 32]Elliptic curve point (new in v0.5.1)
MethodInput TypeDescription
encrypted_bool(value)[u8; 32]Encrypted boolean
encrypted_u8(value)[u8; 32]Encrypted unsigned 8-bit
encrypted_i8(value)[u8; 32]Encrypted signed 8-bit
encrypted_u16(value)[u8; 32]Encrypted unsigned 16-bit
encrypted_i16(value)[u8; 32]Encrypted signed 16-bit
encrypted_u32(value)[u8; 32]Encrypted unsigned 32-bit
encrypted_i32(value)[u8; 32]Encrypted signed 32-bit
encrypted_u64(value)[u8; 32]Encrypted unsigned 64-bit
encrypted_i64(value)[u8; 32]Encrypted signed 64-bit
encrypted_u128(value)[u8; 32]Encrypted unsigned 128-bit
encrypted_i128(value)[u8; 32]Encrypted signed 128-bit
encrypted_float(value)[u8; 32]Encrypted floating point
MethodInput TypeDescription
x25519_pubkey(value)[u8; 32]X25519 public key for encryption
arcis_ed25519_signature(value)[u8; 64]Ed25519 signature
MethodInput TypeDescription
account(pubkey, offset, length)Pubkey, u32, u32Reference on-chain account data

9. Update on-chain PDA macros

PDA derivation macros now require mxe_account and an error code parameter:
// Before v0.5.1
#[account(
    mut,
    address = derive_mempool_pda!()
)]
pub mempool_account: UncheckedAccount<'info>,

#[account(
    mut,
    address = derive_execpool_pda!()
)]
pub executing_pool: UncheckedAccount<'info>,

#[account(
    mut,
    address = derive_comp_pda!(computation_offset)
)]
pub computation_account: UncheckedAccount<'info>,
Don’t forget to update the cluster PDA as well:
// Before v0.5.1
#[account(
    mut,
    address = derive_cluster_pda!()
)]
pub cluster_account: Account<'info, Cluster>,
// v0.5.1
#[account(
    mut,
    address = derive_cluster_pda!(mxe_account, ErrorCode::ClusterNotSet)
)]
pub cluster_account: Account<'info, Cluster>,
// v0.5.1
#[account(
    mut,
    address = derive_mempool_pda!(mxe_account, ErrorCode::ClusterNotSet)
)]
pub mempool_account: UncheckedAccount<'info>,

#[account(
    mut,
    address = derive_execpool_pda!(mxe_account, ErrorCode::ClusterNotSet)
)]
pub executing_pool: UncheckedAccount<'info>,

#[account(
    mut,
    address = derive_comp_pda!(
        computation_offset,
        mxe_account,
        ErrorCode::ClusterNotSet
    )
)]
pub computation_account: UncheckedAccount<'info>,
Make sure you have the ClusterNotSet error code defined (this should already exist from v0.4.0):
#[error_code]
pub enum ErrorCode {
    #[msg("The computation was aborted")]
    AbortedComputation,
    #[msg("Cluster not set")]
    ClusterNotSet,
}

10. Update Callback Handling with BLS Verification

v0.5.1 introduces BLS signature verification for computation outputs. Use SignedComputationOutputs and call verify_output() to validate results:
// Before v0.5.1 - Direct output handling
#[arcium_callback(encrypted_ix = "my_circuit")]
pub fn my_circuit_callback(
    ctx: Context<MyCircuitCallback>,
    output: ComputationOutputs<MyCircuitOutput>,
) -> Result<()> {
    let result = output.unwrap();
    // Use result directly...
    Ok(())
}
// v0.5.1 - BLS verified outputs
#[arcium_callback(encrypted_ix = "my_circuit")]
pub fn my_circuit_callback(
    ctx: Context<MyCircuitCallback>,
    output: SignedComputationOutputs<MyCircuitOutput>,
) -> Result<()> {
    // verify_output() validates the BLS signature from the MXE cluster
    let result = match output.verify_output(
        &ctx.accounts.cluster_account,
        &ctx.accounts.computation_account
    ) {
        Ok(MyCircuitOutput { field_0 }) => field_0,
        Err(e) => {
            msg!("Computation verification failed: {}", e);
            return Err(ErrorCode::AbortedComputation.into())
        },
    };

    // Now use the verified result
    emit!(ResultEvent {
        value: result.ciphertexts[0],
        nonce: result.nonce.to_le_bytes(),
    });
    Ok(())
}
Key Changes:
  • Replace ComputationOutputs<T> with SignedComputationOutputs<T>
  • Always call verify_output() to validate BLS signatures
  • Handle the Result - verification can fail if signatures don’t match
  • verify_output() requires cluster_account and computation_account references
Error Codes:
  • BLSSignatureVerificationFailed - The BLS signature doesn’t match
  • InvalidClusterBLSPublicKey - Cluster’s BLS key is not set
  • AbortedComputation - The computation failed in the MXE

11. (Optional) Update callback macros for multi-transaction callbacks

If you need multiple callback functions for the same encrypted instruction, use the new skip_name_validation parameter:
// Primary callback (standard naming)
#[arcium_callback(encrypted_ix = "add_order")]
pub fn add_order_callback(
    ctx: Context<AddOrderCallback>,
    output: SignedComputationOutputs<AddOrderOutput>,
) -> Result<()> {
    // Handle primary callback with BLS verification
    let result = output.verify_output(
        &ctx.accounts.cluster_account,
        &ctx.accounts.computation_account
    )?;
    Ok(())
}

// Secondary callback (non-standard naming)
#[arcium_callback(
    encrypted_ix = "add_order",
    auto_serialize = false,
    skip_name_validation = true
)]
pub fn emit_order_event(
    ctx: Context<EmitOrderEvent>,
    output: RawComputationOutputs<ShortVec<u8>>
) -> Result<()> {
    // Handle secondary callback (e.g., emit event)
    Ok(())
}

12. Verify Migration

After completing all migration steps, verify that everything works correctly:

Build Test

# From your workspace root
arcium build

Type Checking

# Ensure all new types compile correctly
cargo check --all

Test Your Changes

# Run your existing tests to ensure functionality is preserved
arcium test

13. Complete Example

Here’s a complete before/after example of a typical computation function:

Before v0.5.1:

// Arcium.toml
[localnet]
nodes = 2
localnet_timeout_secs = 60

// programs/coinflip/src/lib.rs
pub fn flip(
    ctx: Context<Flip>,
    computation_offset: u64,
    user_choice: [u8; 32],
    pub_key: [u8; 32],
    nonce: u128,
) -> Result<()> {
    let args = vec![
        Argument::ArcisPubkey(pub_key),
        Argument::PlaintextU128(nonce),
        Argument::EncryptedU8(user_choice),
    ];

    ctx.accounts.sign_pda_account.bump = ctx.bumps.sign_pda_account;

    queue_computation(
        ctx.accounts,
        computation_offset,
        args,
        None,
        vec![FlipCallback::callback_ix(&[])],
        1,
    )?;

    Ok(())
}

#[queue_computation_accounts("flip", payer)]
#[derive(Accounts)]
#[instruction(computation_offset: u64)]
pub struct Flip<'info> {
    #[account(mut)]
    pub payer: Signer<'info>,
    #[account(
        mut,
        address = derive_mempool_pda!()
    )]
    pub mempool_account: UncheckedAccount<'info>,
    #[account(
        mut,
        address = derive_execpool_pda!()
    )]
    pub executing_pool: UncheckedAccount<'info>,
    #[account(
        mut,
        address = derive_comp_pda!(computation_offset)
    )]
    pub computation_account: UncheckedAccount<'info>,
    // ... other accounts
}
// tests/coinflip.ts
const arciumEnv = getArciumEnv();

await program.methods
  .flip(computationOffset, encryptedChoice, pubKey, nonce)
  .accountsPartial({
    computationAccount: getComputationAccAddress(program.programId, computationOffset),
    clusterAccount: arciumEnv.arciumClusterPubkey,
    mempoolAccount: getMempoolAccAddress(program.programId),
    executingPool: getExecutingPoolAccAddress(program.programId),
  })
  .rpc();

v0.5.1:

// Arcium.toml
[localnet]
nodes = 2
localnet_timeout_secs = 60
backends = ["Cerberus"]

// programs/coinflip/src/lib.rs
use arcium_anchor::prelude::*;

const COMP_DEF_OFFSET_FLIP: u32 = comp_def_offset("flip");

pub fn flip(
    ctx: Context<Flip>,
    computation_offset: u64,
    user_choice: [u8; 32],
    pub_key: [u8; 32],
    nonce: u128,
) -> Result<()> {
    let args = ArgBuilder::new()
        .x25519_pubkey(pub_key)
        .plaintext_u128(nonce)
        .encrypted_u8(user_choice)
        .build();

    ctx.accounts.sign_pda_account.bump = ctx.bumps.sign_pda_account;

    queue_computation(
        ctx.accounts,
        computation_offset,
        args,
        None,
        vec![FlipCallback::callback_ix(
            computation_offset,
            &ctx.accounts.mxe_account,
            &[]
        )?],
        1,
        0,  // cu_price_micro: priority fee (0 = no priority)
    )?;

    Ok(())
}

// Callback with BLS verification
#[arcium_callback(encrypted_ix = "flip")]
pub fn flip_callback(
    ctx: Context<FlipCallback>,
    output: SignedComputationOutputs<FlipOutput>,
) -> Result<()> {
    let result = match output.verify_output(
        &ctx.accounts.cluster_account,
        &ctx.accounts.computation_account
    ) {
        Ok(FlipOutput { field_0 }) => field_0,
        Err(e) => {
            msg!("Error: {}", e);
            return Err(ErrorCode::AbortedComputation.into())
        },
    };

    emit!(FlipResultEvent {
        result: result.ciphertexts[0],
        nonce: result.nonce.to_le_bytes(),
    });
    Ok(())
}

#[queue_computation_accounts("flip", payer)]
#[derive(Accounts)]
#[instruction(computation_offset: u64)]
pub struct Flip<'info> {
    #[account(mut)]
    pub payer: Signer<'info>,
    #[account(
        mut,
        address = derive_mempool_pda!(mxe_account, ErrorCode::ClusterNotSet)
    )]
    pub mempool_account: UncheckedAccount<'info>,
    #[account(
        mut,
        address = derive_execpool_pda!(mxe_account, ErrorCode::ClusterNotSet)
    )]
    pub executing_pool: UncheckedAccount<'info>,
    #[account(
        mut,
        address = derive_comp_pda!(
            computation_offset,
            mxe_account,
            ErrorCode::ClusterNotSet
        )
    )]
    pub computation_account: UncheckedAccount<'info>,
    // ... other accounts
}
// tests/coinflip.ts
import {
  getClusterAccAddress,
  getMXEAccAddress,
  getMempoolAccAddress,
  getExecutingPoolAccAddress,
  getComputationAccAddress,
  getCompDefAccAddress,
  getCompDefAccOffset,
  getArciumEnv,
} from "@arcium-hq/client";

const arciumEnv = getArciumEnv();

await program.methods
  .flip(computationOffset, encryptedChoice, pubKey, nonce)
  .accountsPartial({
    computationAccount: getComputationAccAddress(
      arciumEnv.arciumClusterOffset,
      computationOffset
    ),
    clusterAccount: getClusterAccAddress(arciumEnv.arciumClusterOffset),
    mxeAccount: getMXEAccAddress(program.programId),
    mempoolAccount: getMempoolAccAddress(arciumEnv.arciumClusterOffset),
    executingPool: getExecutingPoolAccAddress(arciumEnv.arciumClusterOffset),
    compDefAccount: getCompDefAccAddress(
      program.programId,
      Buffer.from(getCompDefAccOffset("flip")).readUInt32LE()
    ),
  })
  .rpc({ skipPreflight: true, commitment: "confirmed" });
That’s it! Your program should now be compatible with Arcium tooling v0.5.1.

14. Summary of Breaking Changes

ChangeBefore v0.5.1v0.5.1
queue_computation6 parameters7 parameters (add cu_price_micro)
callback_ixcallback_ix(&[])callback_ix(computation_offset, &mxe_account, &[])?
Callback outputComputationOutputs<T>SignedComputationOutputs<T> with verify_output()
PDA functionsprogram.programIdarciumEnv.arciumClusterOffset
EnvironmentARCIUM_CLUSTER_PUBKEYARCIUM_CLUSTER_OFFSET
PDA macrosderive_*_pda!()derive_*_pda!(mxe_account, ErrorCode::ClusterNotSet)
TS Program IDgetArciumProgAddress()getArciumProgramId()
TS Account DatagetMempoolAccData()getMempoolAccInfo()
TS ParametermxeProgramIDmxeProgramId (camelCase)
init_comp_definit_comp_def(accs, 0, None, None)init_comp_def(accs, None, None)
ArgBuilder pubkey.arcis_x25519_pubkey(key).x25519_pubkey(key)