Skip to main content

What this solves

When you write encrypted instructions in Arcium, the results come back as structured data. Previously, developers had to manually parse raw bytes - tracking offsets, sizes, and converting back to the right types. This was error-prone and tedious. Arcium’s type generation system analyzes your circuit’s return type and automatically creates typed Rust structs. This means you can work directly with structured data instead of byte arrays.

Mental model: from functions to structs

Here’s the transformation that happens automatically:
The generated struct gives you typed access to encrypted results, with predictable naming and field patterns you can rely on.

What you’ll learn

After reading this guide, you’ll know how to:
  • Work with automatically generated Rust structs for encrypted computation outputs
  • Predict what struct names and fields Arcium will create
  • Handle different encryption types (Shared vs MXE) in callbacks
  • Debug type generation issues when they arise

30-second quick start

  1. Write your circuit:
  2. Generate types: arcium build
  3. Use in callback:

Your first generated type: simple addition

Here’s a concrete example. Consider this encrypted instruction that adds two numbers:
When Arcium sees that your function returns Enc<Shared, u16>, it automatically generates this output struct:
Notice the pattern:
  • Name: add_together becomes AddTogetherOutput
  • Field: Always field_0 for single return values
  • Type: SharedEncryptedStruct<1> because it’s shared-encrypted with 1 value (the u16)
Now you can use this in your callback with full type safety:

How type generation works

Previously, decoding computation results required manual byte parsing. Now Arcium auto-generates typed structs, letting you focus on application logic.

How output types are generated

Type generation is a two-step process:
  1. arcium build compiles your Arcis circuits and writes .idarc interface files describing each circuit’s return type
  2. #[callback_accounts] macro reads the .idarc file at compile time and generates the corresponding Rust struct in your program crate
For example, given this encrypted instruction:
After running arcium build, the #[callback_accounts("add_together")] macro reads build/add_together.idarc and generates:
This is why you can reference AddTogetherOutput in your callback even though you never explicitly defined it.

Build dependency

You must run arcium build before compiling your program so the .idarc files exist for the macro to read. Run arcium build again whenever you change an encrypted instruction’s return type.

Behind the scenes: the two-step pipeline

Understanding how types flow from your circuit to your Solana program helps explain why arcium build must run first.

Step 1: arcium build produces .idarc files

When you run arcium build, the compiler processes each #[instruction] function inside your #[encrypted] module:
  1. Parse the return type: The compiler examines your function signature and extracts the return type
  2. Analyze the structure: It breaks down complex types (tuples, structs, encryption wrappers) into components
  3. Write the interface file: It serializes the type information to build/{circuit_name}.idarc

Step 2: #[callback_accounts] generates Rust structs

When you compile your Solana program, the #[callback_accounts("circuit_name")] macro:
  1. Reads the .idarc file: Loads the circuit interface from build/{circuit_name}.idarc
  2. Generates struct definitions: Creates typed Rust structs matching the circuit’s return type
  3. Injects into scope: The generated types become available in your program module

What gets generated

For different return types, the pipeline generates different struct patterns:

Why this approach works

This two-step approach provides several benefits:
  • Type safety: You get compile-time type checking for encrypted results
  • No manual definition: You don’t need to define output structs yourself
  • Consistency: All generated types follow the same predictable patterns
  • Automatic updates: If you change your function’s return type, re-run arcium build and the structs update automatically
The key insight is that these structs exist in your compiled program but not in your source code: arcium build produces the interface files, and #[callback_accounts] turns them into Rust types during compilation.

Understanding LEN parameters

In our add_together example, you saw SharedEncryptedStruct<1>. The <LEN> number tells you how many encrypted scalar values are stored inside. The <LEN> number represents the count of individual encrypted scalar values: For custom structs, LEN equals total scalar fields:

Type availability and scope

Where generated types live

Generated types are scoped to the module where #[callback_accounts] is used. After running arcium build, the types become available during program compilation:

No import required

Unlike external types, you don’t need to import generated types. #[callback_accounts] injects them directly into your module’s namespace:

Generated struct properties

All generated structs automatically receive standard derives that make them work with Anchor:
This is why you can use generated types in Anchor contexts without additional setup.

Multiple instructions, multiple types

Each #[instruction] produces its own .idarc file, so #[callback_accounts] generates a separate output type per circuit:
All generated types coexist in the same module scope without conflicts.

Generation process

When you define an encrypted instruction:
  1. arcium build compiles your circuit and writes a .idarc interface file describing its return type
  2. #[callback_accounts] reads the .idarc file at compile time and generates corresponding Rust structs with predictable names
  3. Encryption patterns are detected automatically and produce specialized types
  4. The generated types are available in your #[arcium_callback] functions

How the naming works

The naming follows predictable patterns:

Your circuit gets an output struct

If your encrypted instruction is called add_together, you get a struct called AddTogetherOutput. Arcium converts your circuit name to PascalCase and adds “Output” at the end.

Fields are numbered

Since Anchor doesn’t support tuple structs (yet), Arcium uses numbered fields instead. So if your function returns multiple values, you’ll get field_0, field_1, field_2, and so on. Not the prettiest names, but they’re consistent and predictable.

Complex types get their own structs

When your function returns complex nested data (like tuples or custom structs), Arcium generates additional helper structs with a unified naming convention:
  • All output structs use {CircuitName}OutputStruct{index} pattern
  • Nested structs within outputs use {ParentName}OutputStruct{parent_index}{field_index} pattern
  • The naming ensures uniqueness while maintaining consistency

Encryption types: Shared vs MXE

Arcium automatically detects different encryption patterns and generates the right struct type. Understanding when each type is used helps you predict the generated structs.

SharedEncryptedStruct<N>

When your circuit returns Enc<Shared, T>, Arcium knows this is data that both the client and the MXE can decrypt. It generates a struct that includes everything needed for decryption:
The <N> part tells you how many encrypted values are packed inside. So SharedEncryptedStruct<1> has one encrypted value, SharedEncryptedStruct<3> has three, and so on. In your callback, you can access everything you need:

MXEEncryptedStruct<N>

For Enc<Mxe, T> data, only the MXE cluster can decrypt it - clients can’t. Since there’s no shared secret needed, the struct is simpler:
Notice there’s no encryption_key field here - that’s because clients don’t get to decrypt MXE data.

EncDataStruct<N>

For encrypted data without key exchange metadata (used when the observer tracks encryption context client-side):
Note: EncDataStruct<N> is used when only ciphertext data is needed without additional metadata. See EncData<T> for when to use this pattern. Most applications use SharedEncryptedStruct<N> or MXEEncryptedStruct<N> instead.

Moving to real-world applications

Now that you understand the basics with our simple addition example, here’s how this works in real applications. The key difference is that real apps often:
  • Return multiple values: Functions return tuples or complex structs instead of single values
  • Mix encryption types: Some data for users (Shared), some for MXE only (Mxe)
  • Handle complex data: Custom structs with multiple fields instead of simple numbers
The type generation system handles all of this automatically - you just need to understand the patterns.

Real-world examples

Here’s how this type generation works in actual Arcium applications:

Simple tuple example

Let’s start with something in between - a function that returns two related values:
Since this returns a tuple (Enc<Shared, u32>, Enc<Shared, u32>), Arcium generates:
Notice how tuples get wrapped: the tuple itself becomes field_0, and its elements become field_0, field_1, etc.

Voting application

Now let’s look at a more realistic example. The encrypted voting example shows a perfect use case. You have poll data that only the MXE should see, and a user’s vote that should be shared between the user and the MXE:
Since this function returns a tuple (Enc<Mxe, PollData>, Enc<Shared, bool>), Arcium generates:
Now in your callback, you can work with properly typed data instead of raw bytes:

Coinflip application: back to basics

After seeing complex tuples and mixed encryption types, let’s look at the simplest possible case. The coinflip example returns just a single encrypted boolean:
Arcium sees this returns Enc<Shared, bool> and creates:
Your callback:

Blackjack application

From the blackjack example with complex game state:
Generated types:

Complex nested structures

For more complex outputs with nested data structures:
Generated types:
Notice the naming pattern: We have ComplexExampleOutputStruct00 and ComplexExampleOutputStruct02, but no ComplexExampleOutputStruct01. This is because:
  • field_0 (UserData) needs a custom struct → ComplexExampleOutputStruct00
  • field_1 (SharedEncryptedStruct) uses a predefined type → no custom struct needed
  • field_2 ((u64, f32) tuple) needs a custom struct → ComplexExampleOutputStruct02
  • field_3 (MXEEncryptedStruct) uses a predefined type → no custom struct needed
Only fields that contain custom structs or tuples get their own generated struct definitions.

Working with generated types

Pattern matching

Use destructuring to access nested data:

Error handling

Always handle computation failures:

Best practices

1. Use descriptive variable names

2. Document your circuit interfaces

3. Handle all computation states

4. Emit events for client tracking

When things go wrong

Here are the most common issues and how to fix them:

“Type not found” errors

This usually means one of two things:
  1. Typo in the circuit name - Check that MyCircuit exactly matches your #[instruction] function name (case matters!)
  2. You forgot to rebuild - Run arcium build again after making changes to your encrypted instructions

”No field found” errors

Remember, the generated structs use numbered fields like field_0, field_1, etc. There’s no field called result unless you specifically named your function that way. Try this instead:

Encryption type mismatches

This happens when your circuit returns Enc<Mxe, T> but your callback expects Enc<Shared, T> (or vice versa). Double-check your encrypted instruction’s return type - it needs to match what you’re expecting in the callback.

Callback not working? Check these:

  • Circuit name matches exactly (case sensitive)
  • Ran arcium build after changing circuit
  • Handling verify_output Ok/Err
  • Using correct field numbers (field_0, field_1, etc.)
  • Array access within bounds (ciphertexts.len())

Finding generated types

The best way to see generated types:
This shows exactly what structs were generated for your circuit. You can also search the full output:

Array and complex type handling

Fixed-size arrays

When your circuit returns arrays, each element becomes a separate scalar in the LEN count:
This generates SharedEncryptedStruct<3> because the array has 3 elements:
In your callback, access individual elements:

Nested structures

For deeply nested data, LEN counts all scalar values at any depth:
Result: SharedEncryptedStruct<4> because Entity contains 4 total scalar values.

Migration from v0.1.x

If you’re upgrading from an older version, the new type generation system replaces manual byte parsing: Old way (v0.1.x):
New way (current):
For version compatibility and required changes, see Versioning and compatibility and the release notes.

Type generation limitations

Supported return types

The type generation system works with most common Rust types, but has some constraints: ✅ Supported:
  • Primitive types: u8, u16, u32, u64, u128, i8, i16, i32, i64, i128, bool
  • Fixed-size arrays: [T; N] where N is a compile-time constant
  • Tuples: (T, U, V) with any number of elements
  • Custom structs with supported field types
  • Nested combinations of the above
❌ Not Supported:
  • Dynamic types: Vec<T>, String, HashMap<K, V>
  • Reference types: &T, &mut T (except for input parameters)
  • Generic types with lifetime parameters
  • Recursive or self-referencing structs
  • Option<T> or Result<T, E> as return types

Practical constraints

Size limitations:
  • Very large structs (1000+ fields) may impact compilation time
  • Arrays with thousands of elements create correspondingly large LEN values
  • Deep nesting (10+ levels) may cause macro expansion issues
Naming conflicts:
Function names must be unique when converted to PascalCase + “Output”.

Working within constraints

If you need unsupported types, consider these patterns:

Common patterns and performance tips

Choosing the right encryption type

  • Use Enc<Shared, T> when users need to decrypt and verify results (votes, game outcomes, personal data)
  • Use Enc<Mxe, T> for internal state that users shouldn’t access (system secrets, aggregate statistics, protocol data)

Performance considerations

  • Large arrays: [u8; 1000] becomes SharedEncryptedStruct<1000> - consider if you really need all elements encrypted
  • Complex nesting: Deep struct hierarchies increase LEN values - flatten when possible
  • Mixed returns: (Enc<Shared, T>, Enc<Mxe, U>) creates separate encrypted structs for optimal access patterns

Testing your callbacks

Test callbacks in two layers:
  1. Rust unit tests: extract the post-verify_output() business logic into a pure function and test that directly.
  2. TypeScript integration tests: queue a real computation with arcium test, wait for finalization with awaitComputationFinalization(...), then assert the callback result via emitted events or updated account state. See Building and testing for the arcium test runner and Tracking callbacks for awaitComputationFinalization.
verify_output() performs real BLS signature verification against the cluster’s BLS key, so a mocked SignedComputationOutputs with a dummy signature always fails verification in a unit test. That’s why unit tests cover the pure logic only. Exercise the full verification wiring through the arcium test flow instead: queue a computation, awaitComputationFinalization(...), then assert on the emitted event or updated account state.
Keep your callback thin: match on verify_output(), destructure the result, and delegate to the pure function that holds your business logic. The pure function is what you unit-test; the arcium test flow is what exercises the callback end-to-end.

Quick reference

Callback pattern:

The callback type generation system automatically handles encrypted computation results, eliminating manual byte parsing and offset tracking. With properly typed structs, you can work directly with structured data and focus on building your applications rather than handling low-level data conversion. These generated types provide type safety and predictable patterns that make working with encrypted computation outputs straightforward and reliable.

What’s next?

Callback accounts

Pass additional accounts to your callback for storing results.

Deployment

Deploy your MXE program to devnet or mainnet.