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: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
-
Write your circuit:
-
Generate types:
arcium build -
Use in callback:
Your First Generated Type: Simple Addition
Here’s a concrete example. Consider this encrypted instruction that adds two numbers:Enc<Shared, u16>, it automatically generates this output struct:
- Name:
add_togetherbecomesAddTogetherOutput - Field: Always
field_0for single return values - Type:
SharedEncryptedStruct<1>because it’s shared-encrypted with 1 value (the u16)
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:arcium buildcompiles your Arcis circuits and writes.idarcinterface files describing each circuit’s return type#[callback_accounts]macro reads the.idarcfile at compile time and generates the corresponding Rust struct in your program crate
arcium build, the #[callback_accounts("add_together")] macro reads build/add_together.idarc and generates:
AddTogetherOutput in your callback even though you never explicitly defined it.
Build dependency
You must runarcium 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 whyarcium 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:
- Parse the return type: The compiler examines your function signature and extracts the return type
- Analyze the structure: It breaks down complex types (tuples, structs, encryption wrappers) into components
- 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:
- Reads the
.idarcfile: Loads the circuit interface frombuild/{circuit_name}.idarc - Generates struct definitions: Creates typed Rust structs matching the circuit’s return type
- 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 buildand the structs update automatically
arcium build produces the interface files, and #[callback_accounts] turns them into Rust types during compilation.
Understanding LEN Parameters
In ouradd_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:
| Return Type | LEN Value | Why |
|---|---|---|
Enc<Shared, u32> | 1 | Single scalar |
Enc<Shared, (u32, bool)> | 2 | Two scalars |
Enc<Shared, [u32; 5]> | 5 | Five array elements |
Enc<Shared, MyStruct> | field count | Count all scalar fields in struct |
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:Multiple Instructions, Multiple Types
Each#[instruction] produces its own .idarc file, so #[callback_accounts] generates a separate output type per circuit:
Generation process
When you define an encrypted instruction:arcium buildcompiles your circuit and writes a.idarcinterface file describing its return type#[callback_accounts]reads the.idarcfile at compile time and generates corresponding Rust structs with predictable names- Encryption patterns are detected automatically and produce specialized types
- 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 calledadd_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 getfield_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 returnsEnc<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:
<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>
ForEnc<Mxe, T> data, only the MXE cluster can decrypt it - clients can’t. Since there’s no shared secret needed, the struct is simpler:
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):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
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:(Enc<Shared, u32>, Enc<Shared, u32>), Arcium generates:
field_0, and its elements become field_0, field_1, etc.
Voting Application
Now let’s look at a more realistic example. The confidential 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:(Enc<Mxe, PollData>, Enc<Shared, bool>), Arcium generates:
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:Enc<Shared, bool> and creates:
Blackjack Application
From the blackjack example with complex game state:Complex Nested Structures
For more complex outputs with nested data structures:ComplexExampleOutputStruct00 and ComplexExampleOutputStruct02, but no ComplexExampleOutputStruct01. This is because:
field_0(UserData) needs a custom struct →ComplexExampleOutputStruct00field_1(SharedEncryptedStruct) uses a predefined type → no custom struct neededfield_2((u64, f32) tuple) needs a custom struct →ComplexExampleOutputStruct02field_3(MXEEncryptedStruct) uses a predefined type → no custom struct needed
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
- Typo in the circuit name - Check that
MyCircuitexactly matches your#[instruction]function name (case matters!) - You forgot to rebuild - Run
arcium buildagain after making changes to your encrypted instructions
”No field found” Errors
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
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 buildafter changing circuit - Handling all ComputationOutputs variants
- 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:Array and Complex Type Handling
Fixed-Size Arrays
When your circuit returns arrays, each element becomes a separate scalar in the LEN count:SharedEncryptedStruct<3> because the array has 3 elements:
Nested Structures
For deeply nested data, LEN counts all scalar values at any depth: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):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
- 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>orResult<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
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]becomesSharedEncryptedStruct<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
Mock the computation outputs for testing:Quick Reference
| Return Type | Generated Struct | Access Pattern |
|---|---|---|
Enc<Shared, T> | SharedEncryptedStruct<1> | result.ciphertexts[0], result.nonce |
Enc<Mxe, T> | MXEEncryptedStruct<1> | result.ciphertexts[0], result.nonce |
(T, U, V) | {Circuit}OutputStruct0 | result.field_0, result.field_1 |
| Custom struct | {Circuit}OutputStruct0 | result.field_0, result.field_1 |
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.