# Configuration and security
Source: https://docs.arcium.com/arx-nodes/configuration-and-security
How Node Operators protect Arx node key shares, preprocessing material, hosts, and access credentials
Each Arx node registers its declared **CU capacity** as metadata. Stake requirements determine whether it can activate and join a Cluster; see [Staking overview](/staking/overview).
### Protect key shares
Arx nodes store MPC key shares and preprocessing material offchain. Node Operators are responsible for protecting this data, the host, and access credentials.
When selecting Cluster members, evaluate each Node Operator's hosting, hardware, access controls, and key-management practices offchain.
# Arx nodes overview
Source: https://docs.arcium.com/arx-nodes/overview
Arx node metadata, jurisdiction, and registration on the Arcium Network
When launching an Arx node, the operator provides node metadata and associates the node with a *Node Operator*.
**Node Operators** are metadata entities that represent the party operating one or more Arx nodes.
## Node operators
Node Operators have a one-to-many relationship with Arx nodes, meaning that Node Operators may run multiple nodes. The following metadata fields are associated with a Node Operator:
* **Jurisdiction**: Follows the ISO 3166-1 alpha-2 code standard for jurisdictions, such as `DE` for Germany or `FR` for France.
* **URL**: A webpage representing the Node Operator, such as a website, social profile, or operator reputation page.
These metadata fields are self-declared. The Arcium Network does not verify that the actual hardware location matches the jurisdiction field, but the community can evaluate location claims along with the operator's team, security approach, transparency, and cross-network reputation.
## Metadata
An Arx node's metadata is unique to that specific node and may vary from an associated Node Operator's metadata since a single operator may run multiple nodes in, for example, different jurisdictions.
This node-specific metadata consists of the following fields:
* **IP address**: The node's IPv4 address.
* **Peer ID**: The node's peer identifier in the Multi-Party Computation (MPC) network.
* **Jurisdiction**: The same ISO 3166-1 alpha-2 code standard that Node Operators follow.
As with the Node Operator jurisdiction field, an Arx node's jurisdiction field isn't verified and, therefore, will also be independently evaluated by the Arcium Network's community.
# Performance and incentives
Source: https://docs.arcium.com/arx-nodes/performance-and-incentives
How offchain reputation and stake-weighted rewards affect Node Operators
Customers and delegators evaluate an Arx node's reputation offchain using criteria such as uptime, response time, operational history, and the Node Operator's security practices.
Program rewards follow the Arx node's stake and its Cluster's reward pot.
### Node incentives and rewards structure
Primary stake and third-party delegation contribute to an Arx node's stake-weighted reward share. The Node Operator can also configure a percentage fee on delegated rewards.
Epoch settlement records each Arx node's claim from its Cluster reward pot. Delegators claim their portion after the Node Operator's fee. See [Staking overview](/staking/overview) for settlement details and [Pricing and incentives](/computations/pricing-and-incentives) for customer charges.
Operational reputation informs Cluster invitations and delegation decisions.
# Cluster migration
Source: https://docs.arcium.com/clusters/cluster-migration
How an MXE authority moves an MXE between Clusters through key recovery
An MXE authority can move an MXE from one active Cluster to another through key recovery.
## Migrate an MXE
An MXE binds to one active Cluster at a time. Migration is an explicit action that changes this binding from the current Cluster to a destination Cluster.
The MXE's [Recovery Peers](/getting-started/network-stakeholders#recovery-peers) participate in key recovery so the destination Cluster can continue using the MXE's key material.
Migration changes the MXE's Cluster binding, not an Arx node's membership. An Arx node belongs to one Cluster at a time.
# Incentivization
Source: https://docs.arcium.com/clusters/incentivization
How stake affects Cluster eligibility, reward distribution, and leader selection for Arx nodes
An Arx node's primary and delegated stake determine its stake-weighted reward share and leader weight.
The two types of delegation are:
1. **Self-delegation:** Stake that Node Operators commit to their own Arx nodes.
2. **Third-party delegation:** External stakeholders who delegate their assets to Arx nodes.
## Rewards distribution
Computation rewards accrue in a per-Cluster pot. Epoch settlement records each Arx node's claim as `pot × node_stake / cluster_total_stake`. An Arx node participates in one Cluster at a time.
**Nodes must stake a minimum amount of collateral to participate.**
Node activation and Cluster eligibility have separate primary-stake requirements. See [Staking overview](/staking/overview) for the current floors and settlement behavior.
# Clusters overview
Source: https://docs.arcium.com/clusters/overview
How Cluster authorities assemble Arx nodes and provide execution capacity for MXEs
Clusters are groups of Arx nodes that jointly execute encrypted computations for MPC eXecution Environments (MXEs).
A Cluster is created with an authority, target Arx node count, mempool size, and initial CU price. The mempool size determines the separate mempool and executing-pool account sizes.
The Cluster account records its authority, target node count, CU price, pending invitations, and joined Arx nodes.
The authority invites Arx nodes separately. Each invited Node Operator accepts or declines, and every joining Arx node must meet the applicable [stake floor](/staking/overview). The program does not maintain a node-priority order or automatically replace unavailable Arx nodes.
The authority evaluates reputation, jurisdiction, and hosting security offchain when choosing whom to invite.
### Cluster participation and MXE initialization
After enough Arx nodes join and agree on the aggregated BLS public key, the Cluster authority activates the Cluster.
An MXE binds to one active Cluster, while the Cluster authority manages Arx node membership separately.
### Parallelization and scalability
Every computation is jointly executed by the Arx nodes in its Cluster. A Cluster can execute different non-conflicting computations concurrently when resources and preprocessing data are available. For queue behavior, see [Solana integration: orchestration and execution](/solana-integration-and-multichain-coordination/solana-integration-orchestration-and-execution).
# Permissioned clusters
Source: https://docs.arcium.com/clusters/permissioned-clusters
How Cluster authorities control membership and assemble internal, mixed, or external node sets
Every Cluster uses authority-gated membership. The Cluster authority invites Arx nodes, and each invited Node Operator accepts or declines.
Organizations can use this mechanism to assemble different operating models:
1. **Internal:** Invite only Arx nodes controlled by the organization.
2. **Mixed:** Invite both internal and selected external Arx nodes.
3. **External:** Invite eligible Arx nodes operated by third parties.
All three operating patterns use the same authority-gated membership mechanism onchain.
### Fully-permissioned clusters
An organization can invite only self-operated Arx nodes, giving it operational control over the hosts. The organization remains responsible for assessing its legal and industry requirements.
Despite the isolation, fully permissioned Clusters still use the Arcium program for onchain coordination, Cluster-level base-price voting, and fee distribution.
### Partially-permissioned clusters
An organization can combine its own Arx nodes with selected external operators. All members participate as peers in the same MPC protocol.
### Use external clusters
An authority can invite Arx nodes operated by unrelated third parties. Membership remains invitation-based, and eligibility still requires registration and sufficient stake.
Every joining node must meet the network's stake requirements; see [Sybil resistance](/clusters/sybil-resistance) for how these gates raise the cost of coordinated control.
A broader operator set can reduce dependence on one organization. Authorities should verify operator independence during selection.
# Sybil resistance
Source: https://docs.arcium.com/clusters/sybil-resistance
How Proof of Stake requirements raise the cost of Sybil attacks in the Arcium Network
Sybil attacks involve a single entity creating multiple false identities to disproportionately influence the network. Arcium's defense is Proof of Stake: every identity an attacker controls must be backed by real stake.
The program enforces these stake gates today:
* **Node activation**: activating an Arx node requires a primary self-delegation of 1,000 ARX. See the [Staking overview](/staking/overview).
* **Cluster eligibility**: joining a Cluster requires meeting the primary-stake floor for its mempool size. Because activation already requires 1,000 ARX, the effective minimum ranges from 1,000 to 10,000 ARX. The program checks the floor when an Arx node is invited and when its operator reduces primary stake.
* **Authority-gated membership**: Cluster membership is invitation-based. The Cluster authority chooses which Arx nodes to invite, and each Node Operator accepts or declines. See [Permissioned Clusters](/clusters/permissioned-clusters).
These gates make each participating identity costly. Base-price voting gives each Cluster member one vote, so every additional voting identity must independently satisfy activation and Cluster eligibility.
These controls raise the cost of coordinated identities. Cluster authorities can add offchain operator due diligence when membership independence matters.
# Computation tasks
Source: https://docs.arcium.com/computations/computation-tasks
Computation Units (CUs), task structure, and how computations are measured on Arcium
The **Computation Unit (or CU)** is a Network-wide constant that represents a fixed amount of computational work.
The CU is the smallest unit of computation in the Arcium Network, and all types of arithmetic operations on the Arcium Network (that Computations are made up of) are measured in CUs.
In particular, Computation base pricing uses CUs to set a baseline for Arx node compensation.
## Types of computation tasks
In the Arcium Network, computations fall into two broad categories:
**1) System Computations,** and
**2) Customer Computations**.
These task types serve different purposes on the Network, the first relating to the network's internal operations, and the latter handling external customer requests.
## System computations
System Computations are critical processes automatically generated by the network to maintain its health and operation. They include:
* **Distributed Key Generation (DKG) Computations**: Each Arx node within a Cluster receives a fragment of the overall cryptographic key, known as a key share. These key shares collectively form the key for the MPC eXecution Environment (MXE) in that Cluster, enabling secure joint computation tasks without revealing individual inputs.
* **Key-recovery and migration computations**: Support MXE key recovery during Cluster migration (see [Cluster migration](/clusters/cluster-migration)). These computations let an MXE resume on a destination Cluster without re-keying from scratch.
## Customer computations
Customer Computations are tasks submitted by **Computation Customers** to execute private and confidential operations. These tasks are highly configurable to meet the diverse needs of developers and businesses.
# Defining and commissioning computations
Source: https://docs.arcium.com/computations/defining-and-commissioning-computations
How to register a circuit as a computation definition and commission it for execution on Arcium
Before commissioning a computation, register its circuit as a **Computation Definition** on an MXE.
The definition stores:
1. **Circuit source:** The onchain or offchain source used to load the compiled circuit. Local sources are reserved for internal MXE circuits.
2. **Circuit interface:** The circuit definition and parameter signature produced by the Arcis toolchain.
3. **CU amount:** The amount used to calculate base and priority charges.
4. **Lifecycle state:** The slot when the definition was explicitly deactivated, when applicable.
The MXE's Solana program defines authorization and validates callers and inputs before queueing a computation.
## Commissioning a computation
Commissioning creates a computation account and queues it against the MXE's Cluster.
Key steps in commissioning include:
1. **Provide arguments:** Pass the plaintext and encrypted inputs expected by the Arcis instruction.
2. **Choose a priority price:** Higher values improve queue position; queued computations remain subject to the mempool's 180-slot expiry.
3. **Configure callbacks:** Define the onchain instructions for successful and failed outcomes.
4. **Queue the computation:** Add the computation account to the Cluster's mempool.
When a valid leader choice is available, it can take one dispatch slot. Otherwise priority-fee and account-lock ordering apply. See [Leader weight](/staking/overview#leader-weight).
# Lifecycle of an Arcium computation
Source: https://docs.arcium.com/computations/lifecycle-of-an-arcium-computation
How computations transition from definition to commissioning, execution, and completion
The **Computation Lifecycle** in the Arcium Network governs how computations transition from definition to execution and completion. This structured process ensures reliable coordination, resource optimization, and precise post-execution handling.
## Lifecycle overview
The lifecycle of a computation in the Arcium Network follows a clear progression of steps. These steps ensure computations are securely defined, efficiently commissioned, and accurately executed.
Here are the five key stages:
1. **Definition**: A computation is defined within the context of an MXE, providing a blueprint for its execution (see the [Defining and commissioning computations section](/computations/defining-and-commissioning-computations) for details). This includes specifying inputs, outputs, logic, versioning, and access permissions.
2. **Commissioning**: A defined computation is instantiated by specifying arguments, callback requirements, and fee parameters required for queueing.
3. **Mempool Placement**: Commissioned computations are queued in the Cluster's mempool, where they stay eligible until a Node picks them up or they expire after 180 slots. If a computation is still queued when it expires, the payer can reclaim its computation fee.
4. **Execution**: Nodes execute the computation securely, ensuring privacy and accuracy.
5. **Post-Execution Callbacks**: Following execution, actions defined for success or failure cases are carried out so downstream programs can update state or notify clients.
## Post-execution callbacks
Once a computation is executed, the system triggers callbacks based on the outcome:
* **Success Callbacks**: Handle actions for successfully executed computations. These may include:
* **Dynamic Onchain Actions**: Triggering onchain processes based on computation results.
* **Static Onchain Actions**: Fixed onchain processes to address success scenarios.
* **Failure Callbacks**: Manage actions when a computation fails. These are always static and include:
* **Static Onchain Actions**: Fixed onchain processes to address failure scenarios.
Callbacks ensure that the system remains flexible and responsive, even in the event of unexpected outcomes.
# Pricing and incentives
Source: https://docs.arcium.com/computations/pricing-and-incentives
Base pricing, priority fee markets, and economic incentives for Node Operators
Computation charges combine the Cluster's base price, an optional priority price, and a fixed reserve for submitting the callback transaction.
## Base pricing
Each Cluster has a base price per [Computation Unit (CU)](/computations/computation-tasks). The program multiplies this price by the Computation Definition's CU amount and rounds down to lamports.
## Computational units
The execution cost of a computation is measured in CUs. The Computation Definition stores the CU amount used for pricing.
The cost per CU is determined for each epoch through a voting process. Node Operators cast one vote per Arx node to decide the price of a CU in the upcoming epoch. Abstaining counts as a vote for maintaining the current price.
Voting authority belongs to each Arx node's Node Operator.
The resulting CU price becomes the Cluster's base price for the next epoch. Customers can add a priority price when commissioning a computation.
## Priority fee markets
Priority prices raise a computation's position in its Cluster's mempool. Queued computations remain subject to the mempool's 180-slot expiry.
Priority-fee ordering is scoped to the Cluster's mempool. Computations compete only with other computations queued for the same Cluster, which prevents unrelated tasks from interfering with resource allocation.
When a valid leader choice is available, it can take one dispatch slot. Otherwise the program uses priority-fee and account-lock ordering. See [Leader weight](/staking/overview#leader-weight).
## Economic incentives for Arx nodes
Base and priority charges contribute to computation revenue. The program allocates this revenue to Cluster members, Recovery Peers, and the protocol fee pool as described in [Fee allocation](/staking/overview#fee-allocation).
Stake lockups commit Node Operators and delegators for a minimum period.
## Cluster-level competition
Fee competition is local to a Cluster. Computations in different Clusters have independent fee markets.
## How customers optimize costs
The customer charge includes:
* The Cluster's base price multiplied by the computation's CU amount
* The selected priority price multiplied by the computation's CU amount
* Callback priority cost based on the callback compute budget
* A fixed callback submission reserve that scales with the Cluster's Arx node count
Computation Customers can optimize their costs by carefully managing execution parameters:
1. **Choose submission timing:** Submit non-urgent work when demand is lower.
2. **Set the priority price:** Increase it when queue position matters, while accounting for the callback compute budget.
# Queue ordering and fault handling
Source: https://docs.arcium.com/computations/queue-ordering-and-fault-handling
How queue ordering, expiry, and Cerberus fault detection affect computation liveness and results
Queue rules determine which computation executes next, while Cerberus protects integrity after execution begins. Applications should handle queue expiry and failed execution as normal lifecycle outcomes.
## Order queued computations
Priority prices, account-lock ordering, and an optional leader choice determine which computation is selected from a Cluster's mempool. A queued computation can expire after 180 slots if it is not promoted for execution. The payer can then reclaim its computation charge.
These rules make ordering explicit. Applications can resubmit or apply their own fallback after queue expiry.
## Handle execution faults
Once execution begins, [Cerberus](/multi-party-execution-environments-mxes/mpc-protocols) detects protocol faults and aborts instead of producing a corrupted result under its stated trust assumption.
For a failed computation, the program allocates half of the base-plus-priority execution fee and the full callback reserve to rewards. It does not refund the unallocated half of the execution fee to the payer. See [Fee allocation](/staking/overview#fee-allocation).
After a recorded failure, the application decides whether to retry, choose another path, or initiate [Cluster migration](/clusters/cluster-migration).
# Intro to Arcium
Source: https://docs.arcium.com/developers
What Arcium enables, common use cases, and a guided path from setup to deployment
Arcium is a decentralized private computation network that processes encrypted data with Multi-Party Computation (MPC). It lets Solana applications run computations without revealing sensitive inputs to any single node.
## What Arcium enables
As a Solana developer, you can use Arcium to:
1. **Build privacy-preserving applications**: Add privacy without adopting a new blockchain or abandoning Anchor-style workflows.
2. **Use familiar tooling**: Write encrypted instructions in Rust with Arcis and integrate them with Solana programs.
3. **Process sensitive data**: Run computations over data such as balances, trade orders, game state, or personal information without exposing plaintext inputs.
## How it works
Your application uses an MXE to work with encrypted data in three steps:
1. The client encrypts data and sends it to your MXE program.
2. Your program submits the computation to Arcium's network of MPC nodes.
3. Nodes process the data while keeping it encrypted and return the result.
The entire process happens onchain through Solana, with each step verified and coordinated by Arcium's programs.
## Common use cases
1. **Confidential DeFi**: Build dark pools or private order books where trade sizes and prices remain hidden.
2. **Secure AI**: Enable AI model inference and training on sensitive data while keeping the data encrypted.
3. **Confidential gaming**: Build hidden information games where player moves and state remain private until revealed (e.g., card games, strategy games, auctions).
## Getting started
Arcium provides a familiar development experience for Solana developers:
* Use the `arcium` CLI (a wrapper over `anchor` CLI) to build Solana programs with Arcium
* Write encrypted instructions in Rust using the Arcis framework
* Integrate with your Solana programs using the TypeScript client library
Follow these steps to get started:
1. [Install Arcium](/developers/installation): Set up the development environment and tools.
2. [Hello World](/developers/hello-world): Create your first encrypted instruction.
3. [Core concepts](/developers/core-concepts): Learn how MXEs, Clusters, and encrypted instructions connect.
4. [Computation lifecycle](/developers/computation-lifecycle): Understand how encrypted computations work.
5. [Examples](/developers/examples): Explore complete applications and the concepts they demonstrate.
6. [Continuous integration](/developers/continuous-integration): Build and test each change in GitHub Actions.
7. [Deployment](/developers/deployment): Deploy your MXE to devnet or mainnet.
8. [TypeScript SDK reference](https://ts.arcium.com/api): Use the API reference for TypeScript client libraries.
The Arcium Network is live on Solana mainnet. Join [Discord](https://discord.com/invite/arcium) for developer support and community discussion.
# Arcis overview
Source: https://docs.arcium.com/developers/arcis
Introduction to Arcis, a Rust-based framework for writing secure MPC circuits on the Arcium Network
Arcis is a Rust-based framework for writing secure multi-party computation (MPC) circuits that run on the Arcium Network. Create privacy-preserving applications that compute over encrypted data using familiar Rust syntax.
## What Arcis code looks like
Arcis code is standard Rust with special annotations for MPC execution:
```rust theme={null}
use arcis::*;
#[encrypted]
mod my_circuit {
use arcis::*;
#[instruction]
pub fn add_private(a: Enc, b: Enc) -> Enc {
let x = a.to_arcis(); // Encrypted → secret shares
let y = b.to_arcis();
a.owner.from_arcis(x + y) // Secret shares → encrypted
}
}
```
This computes `a + b` where both inputs remain encrypted throughout. No node ever sees the plaintext values.
Understand why MPC circuits work differently and the mental model behind Arcis.
Build your first Arcis circuit with a hands-on tutorial.
Real-world circuits: voting, games, DeFi applications.
Quick reference cheatsheet for Arcis syntax and patterns.
## Key features
* **Rust-based**: Use Rust's type safety and performance for MPC development.
* **Circuit-oriented**: Write MPC circuits using familiar Rust syntax with constraints for fixed circuit structure.
* **Privacy-focused**: Compute over encrypted data without revealing the underlying information.
## What's next?
Security patterns, optimization tips, and common pitfalls to avoid.
Learn `Enc`, `EncData`, and supported data patterns.
Pass encrypted inputs and design callback-friendly outputs.
Check what Rust operations are supported in Arcis circuits.
# Best practices
Source: https://docs.arcium.com/developers/arcis/best-practices
Performance tips, debugging techniques, and testing strategies for Arcis circuits
This guide covers practical tips for writing efficient, debuggable, and testable Arcis circuits.
**Use this page when** you are optimizing circuit performance, debugging an issue, or setting up tests.
## Understanding execution flow
For conceptual background on why MPC circuits work differently (e.g., why both if/else branches execute), see [Thinking in MPC](/developers/arcis/mental-model).
## Performance optimization
### Operation costs
See [Thinking in MPC - Cost Model](/developers/arcis/mental-model#cost-model) for the full cost breakdown.
| Operation | Cost | Notes |
| ------------------------------------- | --------- | ------------------------------------------- |
| Addition, subtraction, multiplication | Cheap | Multiplications optimized via preprocessing |
| Comparisons | Expensive | Bit decomposition required |
| Division, modulo | Expensive | Multiple internal operations |
| Dynamic indexing | O(n) | Checks all positions |
### Optimization tips
**Batch encrypted outputs when possible**:
```rust theme={null}
// Multiple separate encryptions have overhead
let enc_a = owner.from_arcis(x);
let enc_b = owner.from_arcis(y);
// If you need both values encrypted together, use a tuple type
let enc_tuple: Enc = owner.from_arcis((x, y));
// ✗ Won't compile - Enc wraps the entire value,
// so destructuring patterns don't work on Enc types
// let (enc_a, enc_b) = owner.from_arcis((x, y));
```
**Reuse comparison results**:
```rust theme={null}
// ✗ Redundant - same comparison computed twice
if x > 1000 {
do_something();
}
if x > 1000 { // Expensive comparison done AGAIN
do_another_thing();
}
// ✓ Compute once, reuse the result
let is_large = x > 1000;
if is_large {
do_something();
}
if is_large { // Reuses the boolean, no recomputation
do_another_thing();
}
```
**Prefer public constants over secret-dependent values**:
```rust theme={null}
// ✓ Constant multiplier - compiler can optimize
fn double(x: u64) -> u64 {
x * 2 // Multiplication by constant is efficient
}
// ✓ Pass known values as public inputs
fn apply_rate(amount: u64, rate_percent: u64) -> u64 {
// If rate is known ahead of time, pass it as a public input
// rather than computing it inside the secure computation
amount * rate_percent / 100
}
```
In MPC, values known before the computation (public inputs and constants) can be handled more efficiently than values computed during secure execution.
## Debugging
Arcis provides familiar debugging macros that work during circuit development.
### Print debugging
```rust theme={null}
#[instruction]
fn debug_example(a: u32, b: u32) -> u32 {
println!("Inputs: a = {}, b = {}", a, b);
let result = a + b;
println!("Result: {}", result);
// Also available: print!, eprint!, eprintln!
eprintln!("Debug: computation complete");
result
}
```
Print macros do not change circuit behavior. They are for development only. Output appears during circuit execution on Arx nodes.
### Debug assertions
Use assertions to verify invariants during development:
```rust theme={null}
#[instruction]
fn with_assertions(x: u32, y: u32) -> u32 {
debug_assert!(x > 0, "x must be positive");
debug_assert_eq!(x, x, "sanity check");
debug_assert_ne!(x, y, "x and y should differ");
x + y
}
```
`debug_assert` macros are for development verification only. They do not enforce constraints in production: use explicit conditionals for actual validation logic.
### Common debugging patterns
**Trace loop iterations**:
```rust theme={null}
for i in 0..10 {
println!("Iteration {}: value = {}", i, arr[i]);
// ... processing
}
```
**Check intermediate values**:
```rust theme={null}
let step1 = compute_step1(input);
println!("After step1: {}", step1);
let step2 = compute_step2(step1);
println!("After step2: {}", step2);
```
## Testing
### What can be unit tested
You can test:
* **Helper functions** (non-`#[instruction]` functions)
* **`#[arcis_circuit]` functions** (builtin circuits)
* **Pure logic** extracted into testable units
You **cannot** directly unit test:
* **`#[instruction]` functions** (require MPC runtime)
### Testing strategy
Extract testable logic into helper functions:
```rust theme={null}
#[encrypted]
mod circuits {
use arcis::*;
// Testable: regular function
pub fn calculate_fee(amount: u64, rate: u64) -> u64 {
amount * rate / 10000 // basis points
}
// Testable: builtin circuit
#[arcis_circuit = "min"]
pub fn min(a: u128, b: u128) -> u128 {}
// NOT directly testable: requires MPC
#[instruction]
fn transfer_with_fee(amount: u64, rate: u64) -> u64 {
let fee = calculate_fee(amount, rate);
amount - fee
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fee_calculation() {
// 2.5% fee on 10000
assert_eq!(circuits::calculate_fee(10000, 250), 250);
// 1% fee on 5000
assert_eq!(circuits::calculate_fee(5000, 100), 50);
}
#[test]
fn test_builtin_circuit() {
assert_eq!(circuits::min(10, 20), 10);
assert_eq!(circuits::min(1, 0), 0);
assert_eq!(circuits::min(4, 4), 4);
}
}
```
### Integration testing
`#[instruction]` functions cannot be unit-tested in isolation: they require the full MPC runtime. For end-to-end testing, use the TypeScript SDK to invoke deployed circuits on a test cluster.
See the [JavaScript Client documentation](/developers/js-client-library) and the [Hello World tutorial](/developers/hello-world) for integration testing setup.
## Common pitfalls
### Conditionals don't guard execution
When a condition is not a compile-time constant, both branches execute. The condition selects which result to keep, but Arx nodes perform work for both paths.
See [Thinking in MPC](/developers/arcis/mental-model#both-branches-always-execute) for the full explanation.
```rust theme={null}
// Problematic: assumes the indexing won't happen when found_match is false
if found_match {
data[secret_idx] = new_value; // Executes regardless of found_match
}
// Safe: constant-index loop with conditional assignment
for i in 0..DATA_SIZE {
let should_update = found_match && (i == secret_idx);
if should_update {
data[i] = new_value;
}
}
```
### Reveal and encryption placement
`.reveal()` and `.from_arcis()` cannot appear inside conditional blocks. See [Thinking in MPC](/developers/arcis/mental-model#reveal-and-encryption-placement) for the correct pattern.
## Error handling
### Compile-time vs runtime
| Condition | Compile-Time | Runtime |
| ------------------------- | ---------------------------- | ----------------------- |
| Division by zero | Error if divisor is constant | Undefined behavior |
| Array index out of bounds | Error if index is constant | Error during evaluation |
| Float out of range | Error for literals | Silently clamped |
**Division by secret values:** If your divisor could be zero based on secret inputs, add explicit validation:
```rust theme={null}
let is_valid = divisor != 0;
let safe_divisor = if is_valid { divisor } else { 1 };
let result = if is_valid { numerator / safe_divisor } else { 0 };
```
**Best practices:**
1. Use constant array sizes where possible
2. Validate divisors before division when they depend on secret inputs
3. Keep floats within the supported range `[-2^75, 2^75)`
## What's next?
For the operation cost breakdown, see [Thinking in MPC - Cost Model](/developers/arcis/mental-model#cost-model).
Keep this open while coding for fast syntax lookup.
Understand MPC constraints and the cost model.
# Input/output
Source: https://docs.arcium.com/developers/arcis/input-output
Working with Enc types for encrypted inputs and outputs in Arcis circuits
Inputs and outputs in encrypted instructions are handled the same way. The Arcium Network does not mutate any state itself. Both can be encrypted or plaintext.
Encrypted data is passed as an `Enc` generic type. See [Types](/developers/arcis/types#encryption-types) for the full reference on `Enc` vs `Enc`.
## Data visibility
Parameters and return values have different visibility levels during MPC execution:
| Type | Who Sees Plaintext |
| ------------------------------- | ------------------------------- |
| Plaintext (`u64`, `bool`, etc.) | All Arx nodes |
| `Enc` | Client + MXE (after decryption) |
| `Enc` | MXE only |
Plaintext parameters are visible to all Arx nodes during computation. Use `Enc` for sensitive user data.
### Return value requirements
Values returned from an `#[instruction]` must be in a form that can leave the MPC circuit:
* **Encrypted:** Call `.from_arcis()` to produce `Enc`. The ciphertext is public bytes; the plaintext remains protected.
* **Revealed:** Call `.reveal()` to produce plaintext. The value becomes visible to everyone.
Secret-shared values (intermediate results from `.to_arcis()`) cannot be returned directly: they exist only within the MPC computation.
## Example
```rust theme={null}
use arcis::*;
#[encrypted]
mod order_book {
use arcis::*;
const ORDER_BOOK_SIZE: usize = 8;
#[derive(Copy, Clone)]
pub struct Order {
size: u64,
bid: bool,
owner: u128,
}
#[derive(Copy, Clone)]
pub struct OrderBook {
orders: [Order; ORDER_BOOK_SIZE],
}
#[instruction]
pub fn add_order(
order_ctxt: Enc,
ob_ctxt: Enc,
) -> Enc {
let order = order_ctxt.to_arcis();
let mut ob = ob_ctxt.to_arcis();
let mut found = false;
for i in 0..ORDER_BOOK_SIZE {
let overwrite = ob.orders[i].size == 0 && !found;
if overwrite {
ob.orders[i] = order;
}
found = overwrite || found;
}
ob_ctxt.owner.from_arcis(ob)
}
}
```
This example demonstrates how to pass inputs into encrypted instructions, compute on them, and return outputs. The goal is to add an order to an existing order book.
In this example, `order_ctxt: Enc` contains data encrypted with a shared secret between the client and MXE: both can decrypt it. In contrast, `ob_ctxt: Enc` is encrypted exclusively for the MXE, so only the MXE nodes (acting together) can decrypt it. This pattern is useful for storing protocol state that users shouldn't access directly.
**Why use Mxe?** If `ob_ctxt` were `Enc`, any user could decrypt the entire order book and see everyone else's orders. By using `Enc`, only the MXE cluster can access the aggregate state. Individual users can only see their own inputs and the revealed outputs.
To use the parameters `order_ctxt` and `ob_ctxt` for computation, we need to convert them to corresponding secret shares for the nodes to compute in MPC. This is done by calling the `to_arcis` function on any `Enc` generic parameter. This does not reveal the plaintext data underneath to the nodes during the process.
The order parameter is consumed after the encrypted instruction has been processed. To output the new order book, convert it back using `from_arcis` on the `ob_ctxt.owner` field (the party that encrypted the data) to get the new `Enc` type, and return it.
For more details on how to invoke these encrypted instructions from your Solana program, see [Invoking a computation](/developers/program).
## Efficient data packing
MPC operations work on field elements. For large arrays of small integers, `Pack` provides significant compression. See [Data packing](/developers/arcis/primitives#data-packing) for when to use it.
### Usage
```rust Arcis theme={null}
pub struct GameState {
// Pack large arrays for ~26x storage reduction
board: Pack<[u8; 256]>,
}
#[instruction]
pub fn sum_board(input: Enc>) -> u64 {
let data: [u8; 64] = input.to_arcis().unpack();
let mut sum: u64 = 0;
for i in 0..64 {
sum += data[i] as u64;
}
sum.reveal()
}
```
```typescript Client theme={null}
import { randomBytes } from 'crypto';
import { circuits } from './build/circuits'; // Generated by Arcium compiler
import { RescueCipher } from '@arcium-hq/client';
// Your game board data
const board = new Uint8Array(256).fill(0);
// Pack using generated packer (matches Arcis struct)
const packed = circuits.GameState.pack({
board: Array.from(board)
});
// Encrypt for MPC (sharedSecret from x25519 key exchange - see Encryption docs)
const cipher = new RescueCipher(sharedSecret);
const nonce = randomBytes(16);
const ciphertext = cipher.encrypt(packed, nonce);
```
The `sharedSecret` comes from x25519 key exchange with the MXE cluster. See [Encrypting inputs](/developers/js-client-library/encryption) for the full setup.
Field names and order in TypeScript must exactly match your Arcis struct definition. Mismatches cause silent data corruption.
The generated packers provide:
* Type-safe interfaces matching your Arcis struct
* Correct field ordering (must match Arcis definition)
* Compile-time validation with TypeScript
See [Primitives: Data Packing](/developers/arcis/primitives#data-packing) for full `Pack` API.
Complete example with client-side packing via circuits.VerifyingKey.pack().
## What's next?
RNG, cryptography, and data packing operations.
Queue computations from your Solana program.
# Thinking in MPC
Source: https://docs.arcium.com/developers/arcis/mental-model
Understand the mental model behind Arcis and why MPC circuits work differently from regular code
Arcis lets you write Rust that computes on encrypted data. But MPC (Multi-Party Computation) has fundamental constraints that affect how you write code. This page explains *why* these constraints exist so you can write effective Arcis programs.
**Use this page** to build intuition for how Arcis works. Understanding these concepts will help you write efficient circuits. If you prefer hands-on learning, try the [Hello World tutorial](/developers/hello-world) alongside this guide.
## How secret sharing works
When you call `.to_arcis()` on encrypted data, it does not decrypt the data. Instead, it converts the ciphertext into **secret shares** distributed across Arx nodes (Arcium's MPC execution nodes).
Think of it like splitting a secret number into random pieces:
```text theme={null}
Secret value: 42
Node A holds: 17 (random)
Node B holds: 93 (random)
Node C holds: -68 (calculated so shares sum to 42)
```
**Key insight:** Each node sees only random-looking data. No single node learns anything about the original value. But when nodes compute together following the MPC protocol, the math works out correctly.
**Security guarantee**: Under Arcium's dishonest majority model, privacy is maintained as long as at least one node remains honest, even if every other node colludes. For maximum assurance, you can [run your own node](/developers/node-setup) in a cluster. Since you trust yourself, this guarantees at least one honest participant.
```rust theme={null}
#[instruction]
pub fn double_secret(input: Enc) -> Enc {
// Convert encrypted data to secret shares across nodes
let value = input.to_arcis();
// Each node multiplies their share by 2
// The shares still reconstruct to the correct answer!
let result = value * 2;
// Convert secret shares to encrypted output
input.owner.from_arcis(result)
}
```
This is why Arcis code looks like normal Rust but runs on encrypted data: the MPC protocol handles the complexity of computing on shares.
## The circuit is compiled once
The important constraint is this: **your Arcis code compiles into a fixed circuit structure before any data flows through it.**
```mermaid theme={null}
flowchart LR
A["Rust Code
(compile)"] --> B["Fixed Circuit
Structure"] --> C["Secret Shares
Flow Through"]
```
The circuit structure (which operations happen, in what order, how many times) is locked in at compile time. Secret data flows through this fixed structure at runtime.
**This is the root cause of all Arcis constraints.** If the circuit structure could change based on secret data, observers could learn information by watching *how* the computation runs, not just *what* it outputs.
## Both branches always execute
In normal code, `if/else` picks one branch to run:
```rust theme={null}
// Normal Rust: only ONE branch executes
if condition {
do_expensive_thing(); // Runs if true
} else {
do_cheap_thing(); // Runs if false
}
```
In Arcis, when the condition is **not a compile-time constant**, both branches execute:
```rust theme={null}
// Arcis: BOTH branches execute, condition selects the result
let secret_value = encrypted_input.to_arcis(); // Now secret-shared across nodes
let is_large = secret_value > 1000; // Comparison result is also secret
if is_large { // No single node knows this value
expensive() // Always runs
} else {
cheap() // Always runs
}
// Cost = cost(expensive) + cost(cheap)
```
In the example above, after `.to_arcis()`, no individual node knows the actual value: each holds a random-looking share. The condition `is_large` is itself secret-shared, meaning no node can determine which branch "should" execute. The MPC protocol executes both branches, then uses the secret condition to select which result to keep, without revealing which branch applied.
**The rule:** If a condition is not a compile-time constant, Arcis executes both branches. This includes:
* Conditions derived from secret data (via `.to_arcis()`)
* Conditions using public runtime parameters
**The exception:** Compile-time constants like `if true { ... }` or `if CONST > 5 { ... }` allow single-branch execution because the value is known during circuit compilation.
**Compile-time constant**: A value the Arcis compiler can determine before circuit generation: literals like `10`, `const` declarations, or expressions involving only constants. Values from function parameters or `.to_arcis()` results are NOT compile-time constants.
**Cost implication**: The cost of an `if/else` is the sum of both branches, not the max. Keep branches balanced when possible.
## Fixed iteration counts
Loops must have iteration counts known at compile time:
```rust theme={null}
// ✓ Works: iteration count is fixed
for i in 0..100 {
process(data[i]);
}
// ✗ Won't compile: iteration count depends on runtime value
while secret_value < threshold {
secret_value += 1;
}
```
**Why no `while` loops?** The number of iterations would depend on secret data:
* Secret starts at 10 → 90 iterations → takes X time
* Secret starts at 99 → 1 iteration → takes X/90 time
Execution time would leak information about the secret value.
**Why no `break` or `continue`?** Same reason. Early exit based on secret data reveals information:
```rust theme={null}
// ✗ Won't compile
for i in 0..1000 {
if found_match { break; } // Would reveal when match occurred
}
```
## Fixed-size data only
Variable-length types like `Vec`, `String`, and `HashMap` are not supported:
```rust theme={null}
// ✗ Not supported
let items: Vec = vec![];
let name: String = String::new();
// ✓ Use fixed-size alternatives
let items: [u8; 100] = [0; 100];
let name: [u8; 32] = [0; 32];
```
**Why?** The circuit compiler must know exactly how much memory and how many operations your circuit needs. A `Vec` that might hold 10 or 10,000 elements would create a circuit of unknown size.
## Reveal and encryption placement
The `.reveal()` and `.from_arcis()` methods **cannot be called inside `if/else` blocks** when the condition is not a compile-time constant:
```rust theme={null}
// ✗ Won't compile - reveal inside conditional
if secret_condition {
value.reveal() // Error: cannot call reveal in conditional execution
}
// ✓ Works - select first, then reveal outside
let selected = if secret_condition { a } else { b };
selected.reveal()
```
**Why?** Both branches execute in isolation before results are merged. `.reveal()` broadcasts data to all parties. It is a global side effect that cannot be undone during the merge. If reveal happened inside a branch, it would leak which branch was taken.
The same applies to `.from_arcis()`:
```rust theme={null}
// ✗ Won't compile
if secret_condition {
owner.from_arcis(value) // Error
}
// ✓ Works
let result = if secret_condition { a } else { b };
owner.from_arcis(result)
```
## Dynamic indexing is O(n)
When the index is known at compile time, array access is O(1):
```rust theme={null}
let x = arr[5]; // Compile-time index: O(1)
```
When the index depends on secret data, it becomes O(n):
```rust theme={null}
let x = arr[secret_idx]; // Secret index: O(n)
```
**Why?** The circuit cannot reveal which index was accessed. It must check all positions and select the right one without leaking which position matched. For small arrays this is fine; for large arrays, consider your access patterns carefully.
## Cost model
Not all operations are equal in MPC. Here's a practical cost ranking:
| Operation | Relative Cost | Notes |
| ------------------------------ | ---------------------- | --------------------------- |
| Addition, subtraction | Nearly free | Local computation on shares |
| Multiplication by constant | Nearly free | Local computation |
| Multiplication | Cheap | Optimized via preprocessing |
| Comparisons (`<`, `>`, `==`) | Expensive | Bit-by-bit operations |
| Division, modulo by power of 2 | Expensive | Bit shift operations |
| Division, modulo (general) | Very expensive | Iterative algorithms |
| Dynamic array indexing | O(n) | Must check all positions |
| Sorting | O(n·log²(n)·bit\_size) | Fixed comparison pattern |
**Optimization tip:** Batch operations when possible. Multiple `.from_arcis()` calls have overhead. Restructure to minimize conversions between encrypted and secret-shared forms.
## Rust patterns that need adjustment
Arcis is Rust, but some common patterns need adaptation:
| Standard Rust | Arcis Equivalent | Why |
| ----------------------------- | ---------------------------- | ----------------------------------- |
| `Vec` | `[T; N]` | Fixed size required |
| `String` | `[u8; N]` | Fixed size required |
| `while condition { }` | `for i in 0..MAX { }` | Fixed iterations |
| `let ... else` | `if let` or `match` | `let ... else` is not supported yet |
| `break`, `continue`, `return` | Restructure logic | No early exit |
| `.filter()` | Manual loop with conditional | Would produce variable length |
| `HashMap` | Arrays with manual lookup | Fixed size required |
## Syntax constraints
A few syntax rules to keep in mind:
* **`if`, `else`, and `else if` all work normally**: when the condition is not a compile-time constant, both branches execute (MPC cost = sum of all branches)
* **`.reveal()` and `.from_arcis()` cannot be called inside `if/else` blocks when the condition is not a compile-time constant**: if the condition is a compile-time constant (like `if true`), only one branch runs and reveal is allowed inside
* **`match`, `if let`, let chains, and `matches!` are supported**: match arms may use guards, except an arm cannot combine a guard with an OR-pattern
* **No `let ... else`**: Use `if let` or `match` instead
* **No early `return`**: Functions must have a single exit point
* **No `while`, `loop`, `break`, `continue`**: Use `for` loops with fixed bounds
* **Enums and `Option` are supported**: but not as a circuit input or output, and enum discriminants cannot be set explicitly
## What you learned
* **Secret sharing splits data across nodes**: no single node sees the actual value
* **Circuits are fixed at compile time**: structure cannot depend on secret data
* **Both branches execute**: MPC cost is the sum, not max
* **Loops need fixed bounds**: no `while`, `break`, or `continue`
* **Use fixed-size types**: `[T; N]` instead of `Vec`
* **Reveal outside conditionals**: `.reveal()` and `.from_arcis()` are global operations
* **Pattern matching works within Arcis constraints**: use `match`, `if let`, let chains, and `matches!`, but avoid `let ... else`
* **Dynamic indexing is O(n)**: the circuit checks all positions
* **Comparisons are expensive**: additions and multiplications are cheap
## What's next?
Supported types including integers, arrays, and encrypted types.
Working with Enc types for encrypted inputs and outputs.
# Operations
Source: https://docs.arcium.com/developers/arcis/operations
Complete reference for supported operations, expressions, and patterns in Arcis MPC circuits
Arcis supports many of Rust's native operations and extends them for encrypted data, allowing you to write private computations using familiar Rust syntax. See the tables below for a detailed list of supported and unsupported operations.
**Use this page when** you need to check if a specific operation is supported in Arcis circuits.
## Quick summary
**Works:** `if/else`, `if let`, `match`, `for` loops, arithmetic, comparisons, iterators (except filter)
**Doesn't work:** `while`, `loop`, `break`, `continue`, `return`, `let ... else`, `.filter()`
See tables below for full details.
### Table of contents
* [Expression support](#expression-support)
* [Binary expressions](#binary-expressions)
* [Unary expressions](#unary-expressions)
* [Casts](#cast-expressions)
* [Literals](#literal-expressions)
* [Methods](#method-calls)
* [Paths](#paths)
* [Item support](#item-support)
* [Pattern support](#pattern-support)
* [Pattern matching](#pattern-matching)
## Expression support
| Expression Name | Example | Support | Comments |
| ----------------- | ------------------------------ | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Array literal | `[a, b]` | Supported | |
| Assignment | `a = b;` | Supported | |
| Async block | `async { ... }` | Unsupported | |
| Await | `foo().await` | Unsupported | |
| Binary expression | `a + b` | Partial Support | [See table below](#binary-expressions) for supported binary expressions. |
| Block expression | `{ ... }` | Supported | |
| Break | `break;` | Unsupported | |
| Function call | `f(a, b)` | Partial Support | [See table below](#function-calls) for supported functions. |
| Casts | `a as u16` | Partial Support | [See table below](#cast-expressions) for supported conversions. |
| Closures | `\|a, b \| a + b` | Supported | |
| Const block | `const { ... }` | Supported | |
| Continue | `continue;` | Unsupported | |
| Field access/set | `obj.field` | Supported | |
| For loop | `for i in expr { ... }` | Supported | `expr` must have a length known at compile-time. |
| If | `if cond { ... } else { ... }` | Supported | Complexity is O(`then_block + else_block`). |
| Indexing | `a[idx]` | Supported | Complexity is O(`a.len()`) if `idx` isn't compile-time known (all positions are checked to hide which index was accessed). Slicing accepts `a[2..5]`, `a[2..=4]`, `a[2..]`, `a[..5]`, `a[..=4]`, and `a[..]` when each specified bound is compile-time known. |
| If let | `if let Some(x) = ...` | Partial Support | [See pattern matching](#pattern-matching). Let chains require Rust edition 2024. |
| Literals | `1u128` | Partial Support | [See table below](#literal-expressions) for supported literals. |
| Loops | `loop { ... }` | Unsupported | MPC circuits have fixed structure: variable iteration counts would require dynamic circuit size. Use `for` with compile-time bounds instead. |
| Macros | `println!("{}", q)` | Partial Support | [See table below](#macros) for supported macros. |
| Match | `match n { ... }` | Partial Support | [See pattern matching](#pattern-matching). Last arms cannot have guards. |
| Method calls | `x.foo(a, b)` | Partial Support | [See table below](#method-calls) for supported methods. |
| Parentheses | `(a + b)` | Supported | |
| Paths | `Foo::bar` | Partial Support | [See table below](#paths) for supported paths. |
| Ranges | `4..5` | Partial Support | Specified bounds must be compile-time known. `Range` and `RangeInclusive` are fully iterable. `RangeFrom` iterates forward only. `RangeTo`, `RangeToInclusive`, and `RangeFull` are slice-index only. |
| Raw addresses | `&raw const foo` | Unsupported | |
| References | `&mut foo` | Supported | |
| Repeat arrays | `[4u8; 128]` | Supported | |
| Return | `return false;` | Unsupported | |
| Struct literals | `MyStruct { a: 12, b }` | Supported | |
| Try expression | `this_call_can_err()?;` | Unsupported | |
| Tuple literal | `(a, 4, c)` | Supported | |
| Unary expressions | `!x` | Partial Support | [See table below](#unary-expressions) for supported unary expressions. |
| Unsafe | `unsafe { ... }` | Unsupported | |
| While loops | `while x < 64 { ... }` | Unsupported | Cannot be supported as the number of iterations is not known. |
**Why branches count:** In MPC, both sides of non-constant conditional execution are evaluated, including `if`/`else` branches and non-constant `match` arms. The condition only selects which result to use. This ensures the execution pattern does not leak information about the condition value. See [Thinking in MPC](/developers/arcis/mental-model) for details.
### Binary expressions
User-defined binary operations are currently unsupported.
| Example | Supported types |
| ---------- | -------------------------------------------- |
| `a + b` | Integers, floats, `BaseField25519` |
| `a - b` | Integers, floats, `BaseField25519` |
| `a * b` | Integers, floats, `BaseField25519` |
| `a / b` | Integers, floats |
| `a % b` | Integers |
| `a && b` | Booleans |
| `a \|\| b` | Booleans |
| `a ^ b` | Booleans, integers |
| `a & b` | Booleans, integers |
| `a \| b` | Booleans, integers |
| `a << b` | Integers, if `b` is known at compile time. |
| `a >> b` | Integers, if `b` is known at compile time. |
| `a == b` | All. Use `derive(PartialEq)` for structs. |
| `a != b` | All. Use `derive(PartialEq)` for structs. |
| `a < b` | Booleans, integers, floats, `BaseField25519` |
| `a <= b` | Booleans, integers, floats, `BaseField25519` |
| `a >= b` | Booleans, integers, floats, `BaseField25519` |
| `a > b` | Booleans, integers, floats, `BaseField25519` |
| `a += b` | Integers, floats, `BaseField25519` |
| `a -= b` | Integers, floats, `BaseField25519` |
| `a *= b` | Integers, floats, `BaseField25519` |
| `a /= b` | Integers, floats |
| `a %= b` | Integers |
| `a ^= b` | Booleans, integers |
| `a &= b` | Booleans, integers |
| `a \|= b` | Booleans, integers |
| `a <<= b` | Integers, if `b` is known at compile time. |
| `a >>= b` | Integers, if `b` is known at compile time. |
`BaseField25519` does **not** support `/`, `%`, `>>`, `<<`, `&`, `|`, or `^`. Use `.field_division()` or `.euclidean_division()` for division on field elements. See [BaseField25519 Operations](/developers/arcis/primitives#basefield25519-operations).
### Unary expressions
User-defined unary operations are currently unsupported.
| Example | Supported types |
| ------- | ---------------------------------------------- |
| `!x` | Booleans (logical NOT), integers (bitwise NOT) |
| `-x` | Signed integers, floats, `BaseField25519` |
| `*x` | `&T`, `&mut T`, `Box` |
### Cast expressions
`a as MyType` is only supported:
| From Type | To Type |
| ------------ | ------------ |
| integer type | integer type |
| `bool` | integer type |
| integer type | `bool` |
| `&...&T` | `&T` |
### Function calls
The following function calls are supported:
* user-defined function calls (without recursion)
* `ArcisRNG::bool()` to generate a boolean.
* `ArcisRNG::gen_uniform::()` to generate a uniform value of type T (bool, integer, or combination). Requires explicit type parameter.
* `ArcisRNG::gen_integer_from_width(width: usize) -> u128`. Generates a secret integer between 0 and 2^width - 1 included.
* `ArcisRNG::gen_public_integer_from_width(width: usize) -> u128`. Generates a public integer between 0 and 2^width - 1 included.
* `ArcisRNG::gen_integer_in_range(min: u128, max: u128, n_attempts: usize) -> (u128, bool)`. Generates a random integer in `[min, max]` using rejection sampling. **`n_attempts` must be compile-time known**. Returns `(result, success)` where `success=false` indicates all attempts were rejected. With `n_attempts=24`, failure probability is `<2^-24`.
* `ArcisRNG::shuffle(slice)` on slices. Complexity is in `O(n*log³(n) + n*log²(n)*sizeof(T))`.
* `Mxe::get()` to be able to create MXE-owned secret data.
* `Shared::new(arcis_public_key)` to share private data with `arcis_public_key`.
* `ArcisX25519Pubkey::from_base58(base58_byte_string)` to create a public key from a base58-encoded address.
* `ArcisX25519Pubkey::from_uint8(u8_byte_slice)` to create a public key from a Uint8 array.
* `SolanaPublicKey::from_serialized(value)` to create a Solana public key from serialized form.
* `SolanaPublicKey::from_base58(byte_string)` to create a Solana public key from base58.
* `ArcisMath::sigmoid(x)` for the sigmoid activation function.
* `LogisticRegression::new(coef, intercept)` for logistic regression models.
* `LinearRegression::new(coef, intercept)` for linear regression models.
* `Pack::new(value)` to bit-pack data for onchain storage (multiple small values fit into fewer field elements).
* `ArcisX25519Pubkey::new_from_x(x: BaseField25519)` to create a public key from its Curve25519 Montgomery X-coordinate.
* `ArcisX25519Pubkey::to_x() -> BaseField25519` to extract the Montgomery X-coordinate from a public key.
* `BaseField25519::from_u8(x)` ... `BaseField25519::from_u128(x)` to convert unsigned integers to field elements. Signed variants (`from_i8` ... `from_i128`) and `from_bool`, `from_usize`, `from_isize` also available.
* `BaseField25519::power_of_two(exp)` to compute `2^exp` as a field element.
### Literal expressions
| Example | Support |
| ----------- | ----------- |
| `"foo"` | Unsupported |
| `b"foo"` | Supported |
| `c"foo"` | Unsupported |
| `b'f'` | Supported |
| `'a'` | Unsupported |
| `1` | Supported |
| `1u16` | Supported |
| `1f64` | Supported |
| `1.0e10f64` | Supported |
| `true` | Supported |
### Macros
The following macros are supported:
* `debug_assert!`, `debug_assert_ne!`, `debug_assert_eq!` to assert conditions during debugging. They do not change instruction behavior.
* `eprint!`, `eprintln!`, `print!`, `println!` to print debug output. They do not change instruction behavior.
* `matches!(expr, pattern)` to test a pattern and return `bool`. See [Pattern support](#pattern-support).
* `arcis_static_panic!(message)` to fail compilation when the branch is reached. Useful for enforcing constraints that must be known before circuit generation.
* `include_bytes!("file_path")` to include raw bytes from a file in Arcis circuits.
* `include!("file_path")` to include a file in item position (not expression position).
* `assert_current_module!(crate::path::to::module)` to enable `crate::` absolute paths within the current module. Place at the top of any module that needs to reference items via absolute paths.
* `encrypted_mod!("path/to/module.rs")` or `encrypted_mod!("path/to/module.rs", alias_name)` to use another file as a module within an `#[encrypted]` module. The target file must use the `#[encrypted_library]` attribute. Items are accessible via the filename stem (or alias) as a namespace, e.g., `module_name::ITEM`.
Example usage:
```rust theme={null}
const ARRAY_LEN: usize = 3; // Change to 1 and the example will not compile.
fn second_element(arr: &[u8]) -> u8 {
if arr.len() < 2 {
arcis_static_panic!("Array must have at least 2 elements");
}
arr[1]
}
#[instruction]
fn reveal_second_element(input: Enc>) -> u8 {
let array = input.to_arcis().unpack();
second_element(&array).reveal()
}
```
`arcis_static_panic!` triggers at compile time when the Arcis compiler evaluates the branch. Try changing `ARRAY_LEN` to `1` above: the compile error demonstrates how this macro enforces constraints that must be validated before circuit generation.
### Method calls
The following method calls are supported:
* user-defined method calls (with generics but without recursion)
* `.clone()` on all `Clone` objects.
* `.len()`, `.is_empty()`, `.swap(a, b)`, `.fill(value)`, `.reverse()`, `.iter()`, `.iter_mut()`, `.into_iter()`, `.windows(width)`, `.copy_from_slice(src)`, `.clone_from_slice(src)`, `.split_at(mid)`, `.split_at_mut(mid)`, `.rotate_left(mid)`, `.rotate_right(mid)`, `.contains(item)`, `.starts_with(needle)`, `.ends_with(needle)`, `.as_slice()`, `.as_mut_slice()` on arrays and slices.
* `.first()`, `.first_mut()`, `.last()`, `.last_mut()`, `.get(index)`, `.get_mut(index)`, `.split_first()`, `.split_first_mut()`, `.split_last()`, `.split_last_mut()`, `.first_chunk::()`, `.first_chunk_mut::()`, `.last_chunk::()`, `.last_chunk_mut::()`, `.split_first_chunk::()`, `.split_first_chunk_mut::()`, `.split_last_chunk::()`, `.split_last_chunk_mut::()` on arrays and slices. These return `Option`.
* `.as_array::()`, `.as_mut_array::()` on slices (requires Rust 1.93+). These return `Option`.
* `.map(f)`, `.each_ref()`, `.each_mut()` on arrays.
* `.sort()` on arrays of integers. Complexity is in `O(n*log²(n)*bit_size)`.
* `.enumerate()`, `.chain(other)`, `.cloned()`, `.copied()`, `.count()`, `.rev()`, `.zip(other)`, `.map(func)`, `.for_each(func)`, `.fold(init, func)`, `.sum()`, `.product()`, `.collect::>()` on iterators.
* `.take(n)`, `.skip(n)`, `.step_by(n)` on iterators when `n` is compile-time known.
* `.reveal()` if not inside a conditionally executed block (`if`/`else`, non-constant `match` arm, or guard)
* `.to_arcis()` on `Enc`s
* `.from_arcis(x)` on `Owner`s (objects of types `Mxe` or `Shared`) if not inside a conditionally executed block (`if`/`else`, non-constant `match` arm, or guard)
* `.abs()`, `.min(x)`, `.max(x)` on integers and floats
* `.abs_diff(other)`, `.is_positive()`, `.is_negative()`, `.div_ceil(other)` on integers
* `.to_le_bytes()`, `.to_be_bytes()`, `.wrapping_add(rhs)`, `.wrapping_sub(rhs)`, `.wrapping_mul(rhs)` on typed integers (does not work on integers whose type the interpreter does not know)
* `.exp()`, `.exp2()`, `.ln()`, `.log2()`, `.sqrt()` on floats.
* `.set(val)`, `.swap(other)`, `.update(f)`, `.replace(val)`, `.into_inner()`, `.get()`, `.get_mut()` on `Cell`.
* `.unpack()` on `Pack` to extract the original value from packed storage.
* `Option` combinators: `.is_some()`, `.is_none()`, `.is_some_and(f)`, `.is_none_or(f)`, `.unwrap()`, `.unwrap_or(default)`, `.unwrap_or_else(f)`, `.map(f)`, `.map_or(default, f)`, `.map_or_else(default_f, f)`, `.inspect(f)`, `.filter(predicate)`, `.and(other)`, `.and_then(f)`, `.or(other)`, `.or_else(f)`, `.xor(other)`, `.zip(other)`, `.unzip()`, `.flatten()`, `.copied()`, `.cloned()`, `.take()`, `.take_if(predicate)`, `.replace(value)`, `.insert(value)`, `.get_or_insert(value)`, `.get_or_insert_with(f)`.
* `.to_arcis_with_pubkey_and_nonce(pubkey, nonce)` on `EncData` to decrypt when the key is shared across inputs (avoids duplicate decryption gates). See [EncData](/developers/arcis/types#advanced-encdata%3Ct%3E) for details.
* `.data` on `Enc` to extract only the `EncData` ciphertext for smaller callback payloads.
* `.safe_inverse()` on `BaseField25519` to get the field inverse (returns 0 for inverse of 0).
* `.field_division(divisor)` on `BaseField25519` for field division (returns 0 for division by 0).
* `.euclidean_division(divisor)` on `BaseField25519` for signed Euclidean division (panics on division by 0).
* `.to_u8_unchecked()` ... `.to_u128_unchecked()` on `BaseField25519` to extract as unsigned int. Silently produces incorrect results if value exceeds target range. Signed variants (`to_i8_unchecked` ... `to_i128_unchecked`) and `to_bool_unchecked` also available.
### Paths
The following paths are supported:
* `IntType::BITS`, `IntType::MIN` and `IntType::MAX` where `IntType` is an integer type.
* Paths to user-defined constants, functions and structs, as long as they are inside the `#[encrypted]` area. Both `super::` paths and `crate::` paths (when `assert_current_module!` is declared) are supported.
* `std::mem::replace` and `std::mem::swap`
* `Box::leak`, `Box::new`, `Cell::new`, `Cell::from_mut`
### Code organization with modules
Arcis supports nested modules, `super::` parent references, `crate::` absolute paths, and multi-file projects. `#[instruction]` functions can be placed at any module depth, not just at the top level.
**Submodules with `super::` and `crate::` paths:**
```rust theme={null}
use arcis::*;
#[encrypted]
mod my_mxe {
use arcis::*;
assert_current_module!(crate::my_mxe);
const THRESHOLD: u64 = 1000;
// No assert_current_module! needed: this module only uses super::, not crate::
mod validation {
use arcis::*;
pub fn is_above_threshold(val: u64) -> bool {
val > super::THRESHOLD // Access parent constant via super::
}
}
mod processing {
use arcis::*;
assert_current_module!(crate::my_mxe::processing);
// #[instruction] works in nested modules
#[instruction]
pub fn process(vals: [u64; 3]) -> [u64; 3] {
let mut result = [0u64; 3];
for i in 0..3 {
// Use crate:: to reference a sibling module's function
let valid = crate::my_mxe::validation::is_above_threshold(vals[i]);
result[i] = if valid { vals[i] * 2 } else { vals[i] };
}
result
}
}
}
```
**Multi-file projects with `encrypted_mod!`:**
```rust theme={null}
// encrypted-ixs/src/lib.rs
use arcis::*;
#[encrypted]
mod my_mxe {
use arcis::*;
encrypted_mod!("helpers.rs");
type Config = helpers::Config;
#[instruction]
fn compute(cfg: Config) -> u64 {
cfg.value * helpers::MULTIPLIER
}
}
```
```rust theme={null}
// encrypted-ixs/src/helpers.rs
use arcis::*;
#[encrypted_library]
mod arcis_library {
pub const MULTIPLIER: u64 = 42;
pub struct Config {
pub value: u64,
}
}
```
Each file imported via `encrypted_mod!` must use `#[encrypted_library]` (not `#[encrypted]`). Items are accessible through the filename stem as a namespace (e.g., `helpers::Config`), or you can provide an alias: `encrypted_mod!("helpers.rs", utils)` makes items accessible as `utils::Config`.
## Item support
| Item Name | Example | Support | Comments |
| ----------------- | --------------------------- | --------------- | -------------------------------------------------------------------------------------- |
| Constant | `const MAX: u16 = 65535` | Supported | |
| Enum | `enum MyEnum { ... }` | Partial Support | Explicit discriminants are not supported. Cannot be a circuit input or output. |
| Extern | `extern ...` | Unsupported | |
| Functions | `fn foo() -> u8 { 0 }` | Partial Support | Recursive functions are not supported. |
| Impls | `impl MyType { ... }` | Supported | Generics and custom traits are supported. `MyType` must not be a reference. |
| Macro Definitions | `macro_rules! ...` | Unsupported | |
| Macro Invocations | `println!(...)` | Partial Support | [See table above](#macros) for supported macros. |
| Modules | `mod my_module { ... }` | Supported | |
| Statics | `static ...` | Unsupported | |
| Structs | `struct MyStruct { ... }` | Supported | |
| Traits | `trait MyTrait { ... }` | Partial Support | Custom traits with associated types and constants. Standard library traits forbidden.¹ |
| Type Aliases | `type MyId = usize;` | Supported | |
| Union | `union MyUnion { ... }` | Unsupported | |
| Use | `use arcis::*` | Partial Support | Only `use arcis::*` is supported. |
| Arcis Circuit | `#[arcis_circuit = "name"]` | Supported | Use a pre-built optimized circuit by name. For internal/advanced use. |
¹ **Forbidden trait implementations**: You cannot manually implement `Drop`, `Deref`, `AsRef`, `AsMut`, `From`, `Into`, `TryFrom`, `TryInto`, `PartialEq`, `Eq`, `PartialOrd`, `Ord`, `Clone`, `ToOwned`, `ToString`, `Iterator`, `IntoIterator`, `DoubleEndedIterator`, `ExactSizeIterator`, `Extend`, `FromIterator`, `Fn`, `FnMut`, `FnOnce`, `Future`, `IntoFuture`, `AsyncFn`, `AsyncFnMut`, or `AsyncFnOnce`.
**Why?** These traits have special runtime semantics (drop ordering, lazy evaluation, dynamic dispatch) that cannot be correctly translated to fixed MPC circuits. The Arcis compiler provides built-in implementations that work within MPC constraints.
Use `#[derive(...)]` for `Clone`, `PartialEq`, `Default`, etc., which generates MPC-compatible implementations. `Default` also supports a manual `impl Default for T`.
## Pattern support
The following patterns are supported in function arguments, `let` statements, `if let` conditions, `matches!` calls, and `match` expressions:
* simple idents: `let ident = ...;`
* mutable idents: `let mut ident = ...;`
* ref idents: `let ref ident = ...;`
* mutable ref idents: `let ref mut ident = ...;`
* parentheses around a supported pattern: `let (...) = ...;`
* reference of a supported pattern: `let &... = ...;`
* array of supported patterns: `let [...] = ...;`
* struct of supported patterns: `let MyStruct { ... } = ...;`
* tuple of supported patterns: `let (...) = ...;`
* tuple struct of supported patterns: `let MyStruct(...) = ...;`
* type pattern of a supported pattern: `let ...: ty = ...;`
* wild pattern: `let _ = ...;`
`|` patterns are supported only in `if let`, `matches!`, and `match`; they cannot be used on a `match` arm that also has a guard. The `..` pattern is only supported inside struct patterns with named fields, e.g. `MyStruct { x: 0, .. }`.
Literal, range, and path-constant patterns are only supported in `if let`, `matches!`, and `match`. Path constants must use a path such as `module::MY_CONST`; bare const identifiers are not supported as patterns.
## Pattern matching
Arcis supports `match` expressions, `if let` (including let chains), and the `matches!` macro for branching on patterns. Patterns can be literals, ranges, path constants like `module::MY_CONST`, OR-patterns, tuples, structs (with `..` rest), arrays/slices, references, bindings, and wildcards. Match arms can use `if`-guards, except an arm cannot combine a guard with an OR-pattern.
`let ... else` remains unsupported. In `match`, guards are supported, but an arm cannot combine a guard with an OR-pattern.
### `match` expressions
```rust theme={null}
#[encrypted]
mod circuits {
use arcis::*;
pub struct Point { x: i16, y: i16 }
#[instruction]
pub fn classify(p: Point) -> i16 {
match p {
Point { x: 0, y: 0 } => 0,
Point { x: 0, y } => y,
Point { x, y: 0 } => x * 10,
Point { x, y } => x + y,
}
}
#[instruction]
pub fn bucket(x: u8) -> u8 {
match x {
v if v < 5 => 0,
v if v < 10 => 1,
_ => 2,
}
}
}
```
### `if let` and let chains
```rust theme={null}
if let Point { x: 0, y } = p {
y
} else {
-1
}
// let chain: bind in the first condition, use in the second
if let (0, val) = (a, b) && val < 10 {
1
} else {
0
}
```
Let chains (`if let ... && let ... && ...`) require Rust edition 2024. The default `arcium init` scaffold sets `edition = "2021"` in `encrypted-ixs/Cargo.toml`. Bump it to `edition = "2024"` to use this syntax.
### `matches!` macro
`matches!` returns a `bool` and supports the same pattern surface:
```rust theme={null}
let in_range = matches!(x, 0..=9);
let is_origin = matches!(p, Point { x: 0, y: 0 });
let is_short = matches!(arr.as_slice(), [] | [_]);
```
### Slice patterns
Fixed-size arrays viewed as slices can be matched with arms of different lengths:
```rust theme={null}
match arr.as_slice() {
[] => 0,
[x] => *x + 1,
[x, y] => *x + *y + 2,
[_, _, _] => 3,
_ => 99,
}
```
Item shadowing of a `let` binding is rejected. Defining `fn f()` or `const F: _` after `let f = ...` (or `let F = ...`) errors at compile time with *"Cannot have an item with the same name as a variable in scope."* `let` shadowing another `let` is still allowed.
## Generics
Arcis supports Rust generics with some constraints. Generic types must be known at compile time. Runtime polymorphism is not supported.
### Generic functions
```rust theme={null}
#[encrypted]
mod generics_example {
use arcis::*;
// Use a pre-built optimized circuit by name
// The empty function body is intentional - the circuit implementation is built-in
#[arcis_circuit = "zero"]
fn make_zero(a: T) -> T {}
fn set_zero(a: &mut T) {
*a = make_zero(*a);
}
#[instruction]
fn zero_any_type(mut arr: [u8; 10], mut val: u64) -> ([u8; 10], u64) {
set_zero(&mut arr);
set_zero::(&mut val); // Turbofish syntax works
(arr, val)
}
}
```
### Generic structs
```rust theme={null}
struct Wrapper(T);
impl Wrapper {
fn new(value: T) -> Self {
Wrapper(value)
}
fn into_inner(self) -> T {
self.0
}
}
#[instruction]
fn use_generic_struct(a: u8) -> u8 {
Wrapper::new(a).into_inner()
}
```
### Custom traits
```rust theme={null}
trait Processable {
type Output;
fn process(&self) -> Self::Output;
}
impl Processable for u8 {
type Output = u16;
fn process(&self) -> u16 {
*self as u16 * 2
}
}
fn apply_process(val: &T) -> T::Output {
val.process()
}
#[instruction]
fn trait_example(x: u8) -> u16 {
apply_process(&x)
}
```
### Generic constraints
| Feature | Supported | Notes |
| ----------------------- | --------- | ------------------------------- |
| Type parameters `` | Yes | Must be known at compile time |
| Trait bounds `T: Trait` | Yes | Including `ArcisType` |
| Associated types | Yes | `type Output;` |
| Associated constants | Yes | `const SIZE: usize;` |
| Where clauses | Yes | `where T: Clone` |
| Turbofish `::` | Yes | For explicit type specification |
| Runtime polymorphism | No | No `dyn Trait` or trait objects |
## Iterators
Most iterator methods work in Arcis, with the notable exception of `.filter()`.
### Supported iterator methods
```rust theme={null}
#[instruction]
fn iterator_examples(arr: [u8; 10]) -> u16 {
// Basic iteration
let mut sum = 0u16;
for val in arr.iter() {
sum += *val as u16;
}
// Method chaining
arr.iter()
.map(|x| *x as u16)
.map(|x| x * 2)
.sum()
}
```
### Complete iterator support
| Method | Supported | Notes |
| ------------------------ | --------- | ---------------------------------------------- |
| `.iter()` | Yes | Creates iterator of references |
| `.iter_mut()` | Yes | Mutable references |
| `.into_iter()` | Yes | Consumes collection |
| `.map(f)` | Yes | Transform elements |
| `.enumerate()` | Yes | Add indices |
| `.zip(other)` | Yes | Pair with another iterator |
| `.chain(other)` | Yes | Concatenate iterators |
| `.rev()` | Yes | Reverse order |
| `.cloned()` | Yes | Clone elements |
| `.copied()` | Yes | Copy elements |
| `.fold(init, f)` | Yes | Reduce with accumulator |
| `.sum()` | Yes | Sum all elements |
| `.product()` | Yes | Multiply all elements |
| `.count()` | Yes | Count elements |
| `.take(n)` | Yes | n must be compile-time known |
| `.skip(n)` | Yes | n must be compile-time known |
| `.step_by(n)` | Yes | n must be compile-time known |
| `.for_each(f)` | Yes | Apply function to each |
| `.collect::>()` | Yes | Collect into `Box<[_]>` |
| `.next()` | Yes | Returns `Option` |
| `.nth(n)` | Yes | Returns `Option`; n must be compile-time known |
| `.last()` | Yes | Returns `Option` |
| `.reduce(f)` | Yes | Returns `Option` |
| `.max()` | Yes | Returns `Option` |
| `.min()` | Yes | Returns `Option` |
| `.max_by_key(f)` | Yes | Returns `Option` |
| `.min_by_key(f)` | Yes | Returns `Option` |
| `.filter(f)` | **No** | Would produce variable-length output |
| `.find(f)` | **No** | Would require early exit |
| `.any(f)` | **No** | Would require early exit |
| `.all(f)` | **No** | Would require early exit |
### Filter alternative
Since `.filter()` is not supported (it produces variable-length output), use a manual loop with conditionals:
```rust theme={null}
// ✗ Not supported
arr.iter().filter(|x| **x > threshold).sum()
// ✓ Manual filter pattern
#[instruction]
fn filter_sum(arr: [u8; 10], threshold: u8) -> u16 {
let mut sum = 0u16;
for val in arr.iter() {
if *val > threshold {
sum += *val as u16;
}
}
sum
}
```
This pattern checks all elements but only accumulates those meeting the condition: same result, fixed execution structure.
## What's next?
RNG, cryptography, and data packing operations.
Performance tips, debugging, and testing strategies.
# Primitives
Source: https://docs.arcium.com/developers/arcis/primitives
Random number generation, cryptographic operations, and data packing in Arcis
Arcis provides built-in primitives for randomness, cryptography, and efficient data storage. These operations are implemented as optimized MPC circuits.
## Random number generation
The `ArcisRNG` struct provides access to randomness within MPC circuits. All random values are generated within the MPC context.
### Basic usage
```rust theme={null}
use arcis::*;
#[encrypted]
mod randomness_example {
use arcis::*;
#[instruction]
pub fn random_operations() -> (bool, u128, [u8; 32]) {
// Generate a random boolean (50/50 probability)
let coin_flip = ArcisRNG::bool();
// Generate a random integer with specific bit width
// Returns u128 in range [0, 2^width - 1]
let random_byte = ArcisRNG::gen_integer_from_width(8); // 0-255
let random_u64 = ArcisRNG::gen_integer_from_width(64); // 0 to 2^64-1
// Generate a uniformly random value of any supported type
let random_array = ArcisRNG::gen_uniform::<[u8; 32]>();
(coin_flip.reveal(), random_byte.reveal(), random_array.reveal())
}
}
```
The `width` parameter in `gen_integer_from_width` must be known at compile time.
### Public vs secret random integers
```rust theme={null}
// Secret random integer (default) - only revealed when you call .reveal()
let secret_num = ArcisRNG::gen_integer_from_width(64);
// Public random integer - visible to all Arx nodes during circuit execution
let public_num = ArcisRNG::gen_public_integer_from_width(64);
```
Use `gen_public_integer_from_width` when you need randomness that does not need to stay secret within the MPC computation (for example, nonce generation). The value is visible to Arx nodes during execution but is not automatically included in the circuit output; you still control what gets returned.
### Range-based generation
To generate integers within a specific range, use `gen_integer_in_range`:
```rust theme={null}
#[instruction]
pub fn dice_roll() -> (u128, bool) {
// Generate integer between min and max (both inclusive)
// n_attempts controls the success probability
let (roll, success) = ArcisRNG::gen_integer_in_range(1, 6, 24);
// With 24 attempts, failure probability is below 2^-24
(roll.reveal(), success.reveal())
}
```
The function uses rejection sampling. Each attempt has >50% success probability, so `n_attempts=24` gives a failure probability below 2^-24.
The `n_attempts` parameter must be known at compile time.
### Shuffling
Shuffle arrays in-place with cryptographic uniformity:
```rust theme={null}
#[instruction]
pub fn shuffle_deck(mut cards: [u8; 52]) -> [u8; 52] {
ArcisRNG::shuffle(&mut cards);
cards.reveal()
}
```
**Complexity:** O(n·log³(n) + n·log²(n)·sizeof(T))
### What works and what doesn't
```rust theme={null}
// ✓ Works
let b: bool = ArcisRNG::bool();
let n: u128 = ArcisRNG::gen_integer_from_width(64);
let arr: [u8; 32] = ArcisRNG::gen_uniform::<[u8; 32]>();
// ✗ Doesn't work - type must be explicit
let b = ArcisRNG::gen_uniform(); // Error: type inference not supported
// ✗ Doesn't work - floats cannot be generated uniformly
let f: f64 = ArcisRNG::gen_uniform::(); // Error
```
## Cryptographic operations
### SHA3 hashing
Arcis provides SHA3-256 and SHA3-512 hash functions:
```rust theme={null}
#[instruction]
pub fn hash_message(message: [u8; 64]) -> [u8; 32] {
let hasher = SHA3_256::new();
hasher.digest(&message).reveal()
}
#[instruction]
pub fn hash_512(message: [u8; 128]) -> [u8; 64] {
let hasher = SHA3_512::new();
hasher.digest(&message).reveal()
}
```
Arcis uses SHA3 (Keccak) rather than SHA-2/SHA-512 because SHA3 has a more efficient circuit structure for MPC evaluation.
### Ed25519 signatures
Arcis provides Ed25519 signature operations using SHA3-512 internally (ArcisEd25519).
#### Signature verification
```rust theme={null}
#[instruction]
pub fn verify_signature(
verifying_key: Pack, // Public key from client
message: [u8; 32],
signature: [u8; 64],
) -> bool {
let vk = verifying_key.unpack();
let sig = ArcisEd25519Signature::from_bytes(signature);
vk.verify(&message, &sig).reveal()
}
```
#### Key generation
```rust theme={null}
#[instruction]
pub fn generate_keypair() -> VerifyingKey {
// Generate a random secret key (stays secret within MPC)
let secret_key = SecretKey::new_rand();
// Derive and return only the verifying (public) key
let verifying_key = VerifyingKey::from_secret_key(&secret_key);
verifying_key.reveal()
}
```
Only the public verifying key is revealed. The secret key is never revealed in plaintext; it exists only as secret shares distributed across Arx nodes. Arcium uses a **dishonest majority** model: privacy is maintained as long as at least one node remains honest, even if every other node colludes.
#### MXE cluster signing
Sign messages using the MXE cluster's collective key:
```rust theme={null}
#[instruction]
pub fn cluster_sign(message: [u8; 32]) -> ArcisEd25519Signature {
MXESigningKey::sign(&message).reveal()
}
```
### Public key operations
Work with X25519 public keys:
```rust theme={null}
#[instruction]
pub fn compare_keys(key1: [u8; 32], key2: [u8; 32]) -> bool {
let pk1 = ArcisX25519Pubkey::from_uint8(&key1);
let pk2 = ArcisX25519Pubkey::from_uint8(&key2);
(pk1 == pk2).reveal()
}
#[instruction]
pub fn key_from_base58() -> ArcisX25519Pubkey {
// Create public key from base58-encoded string
// Note: b"..." creates a byte string literal
ArcisX25519Pubkey::from_base58(b"2uKu51kQaLseu7FySMAGWU6hpnjNvgGr3PkvUCBVTTPD")
}
```
For advanced use, work with the Montgomery X coordinate directly:
```rust theme={null}
#[instruction]
pub fn extract_coordinate(pubkey: ArcisX25519Pubkey) -> BaseField25519 {
pubkey.to_x() // Extract Montgomery X-coordinate
}
#[instruction]
pub fn rebuild_from_coordinate(x: BaseField25519) -> ArcisX25519Pubkey {
ArcisX25519Pubkey::new_from_x(x) // Rebuild from X-coordinate
}
```
Coordinate extraction is for advanced cryptographic operations such as:
* **Custom ECDH key exchange** implementations
* **Key derivation** from shared secrets
* **Interoperability** with external systems that work with raw Curve25519 coordinates
* **Zero-knowledge proof** inputs that require field elements
Most applications should use `from_base58()` or `from_uint8()` for standard public key handling.
**About `.reveal()`:** Revealing cryptographic keys or signatures makes them public to all Arx nodes. Only reveal data that is intended to be public output. For internal computations, keep values in secret-shared form.
Learn where `.reveal()` and `.from_arcis()` can be called.
## BaseField25519 operations
`BaseField25519` (integers modulo `2^255 - 19`) is the native field element for Arcis MPC circuits. Use it for raw field arithmetic without truncation or overflow: cryptographic primitives, Pedersen commitments, curve coordinate work.
For bounded arithmetic, comparison-heavy logic, or when you need bitwise operations and division operators, use regular integers (`u8`..`u128`) instead.
### Construction
| Method | Description |
| ------------------------------------------------- | ------------------------------------- |
| `BaseField25519::from_u8(x)` ... `from_u128(x)` | Convert unsigned int to field element |
| `BaseField25519::from_i8(x)` ... `from_i128(x)` | Convert signed int to field element |
| `BaseField25519::from_usize(x)` / `from_isize(x)` | Platform-sized conversions |
| `BaseField25519::from_bool(x)` | `true` → 1, `false` → 0 |
| `BaseField25519::power_of_two(exp)` | Returns `2^exp` as a field element |
All `from_*` functions work both in plaintext Rust and inside `#[encrypted]` blocks.
### Extraction (unchecked)
| Method | Description |
| ----------------------------------------------- | ----------------------- |
| `.to_u8_unchecked()` ... `.to_u128_unchecked()` | Extract as unsigned int |
| `.to_i8_unchecked()` ... `.to_i128_unchecked()` | Extract as signed int |
| `.to_bool_unchecked()` | Extract as bool |
These methods are **unchecked**: if the field element value exceeds the target type's range, the result is undefined or otherwise incorrect. The circuit will not error; it will silently produce incorrect output.
### Arithmetic
All operations wrap modulo `2^255 - 19` (not at integer type boundaries).
| Operation | Syntax | Assign variant |
| -------------- | ------- | -------------- |
| Addition | `a + b` | `a += b` |
| Subtraction | `a - b` | `a -= b` |
| Multiplication | `a * b` | `a *= b` |
| Negation | `-a` | N/A |
### Comparisons
`==`, `!=`, `<`, `<=`, `>`, `>=` all produce `bool`.
Comparisons are **unsigned**: the field element is treated as a number in `[0, p-1]`. This means `BaseField25519::from_i8(-1)` wraps to `p - 1` and compares **greater than** `BaseField25519::from_u8(0)`.
### Serialization
| Method | Description |
| ---------------------------- | --------------------------------- |
| `.to_le_bytes() -> [u8; 32]` | Little-endian byte representation |
### Division methods
```rust theme={null}
#[instruction]
pub fn field_math(a: BaseField25519, b: BaseField25519) -> (BaseField25519, BaseField25519, BaseField25519) {
// Field inverse: returns 0 for inverse of 0 (no panic)
let inv = b.safe_inverse();
// Field division: uses safe_inverse internally, division by 0 returns 0
let quotient = a.field_division(b);
// Signed Euclidean division: panics on division by 0
let euclidean = a.euclidean_division(b);
(inv.reveal(), quotient.reveal(), euclidean.reveal())
}
```
| Method | Description | Division by Zero |
| ----------------------------- | -------------------------------- | ---------------- |
| `safe_inverse()` | Returns the field inverse | Returns 0 |
| `field_division(divisor)` | Field division (a \* divisor^-1) | Returns 0 |
| `euclidean_division(divisor)` | Signed Euclidean division | Panics |
`euclidean_division` will panic at runtime if the divisor is zero. Use `field_division` if you need safe handling of zero divisors.
### Differences from regular integers
`BaseField25519` is **not** an integer type. The following operations available on `u8`..`u128` are **not supported**:
* **No `/` or `%` operators**: use `.field_division()` or `.euclidean_division()` instead
* **No `>>`, `<<`, `&`, `|`, or `^` operators**
* **No `MIN`, `MAX`, `BITS` constants**
* **No `.min()`, `.max()`, `.abs()`** and no `.sort()` on arrays of field elements
* **No `.to_be_bytes()`**: only `.to_le_bytes()`
* **No `as` casts**: use `from_*` / `to_*_unchecked` methods
`Pack` provides **no compression**: each value already occupies one full field element. Only use `Pack` with smaller types like `[u8; N]`.
## Data packing
The `Pack` type provides bit-level compression for onchain storage efficiency.
### Why packing matters
In Arcis, all values are stored as field elements (\~255 bits / 32 bytes each). Without packing:
* A single `u8` (8 bits) uses one full field element
* `[u8; 256]` uses 256 field elements
With packing, multiple small values are combined into fewer field elements:
**The math:**
* `[u8; 256]` = 256 bytes total
* Each field element packs \~26 bytes (208 usable bits)
* Packed: ⌈256 / 26⌉ = **10 field elements**
* Compression: 256 → 10 = **\~26x fewer field elements**
Without packing, each `u8` would use a full field element (256 elements total). This significantly reduces onchain storage costs and transaction sizes.
### When to use Pack
* Large arrays of small integers (`[u8; N]`, `[u16; N]`)
* Data that needs to be stored onchain
* Input/output parameters approaching transaction size limits
### Basic usage
```rust theme={null}
// Pack data for efficient storage
let packed: Pack<[u8; 64]> = Pack::new(data);
// Unpack to use the data
let data: [u8; 64] = packed.unpack();
```
**Trade-off:** Packing/unpacking has compute cost. Use `Pack` when storage savings outweigh the computation overhead, typically for arrays of 32+ small integers.
How to use generated packers with encrypted inputs in TypeScript.
### Simple example
```rust theme={null}
#[instruction]
pub fn pack_data(data: [u8; 64]) -> Pack<[u8; 64]> {
Pack::new(data) // Compress 64 bytes into ~3 field elements
}
#[instruction]
pub fn unpack_data(packed: Pack<[u8; 64]>) -> [u8; 64] {
packed.unpack() // Restore original array
}
#[instruction]
pub fn process_packed(packed: Pack<[u8; 64]>) -> u8 {
let data = packed.unpack();
let mut max = data[0];
for i in 1..64 {
if data[i] > max {
max = data[i];
}
}
max.reveal()
}
```
These basic patterns cover most `Pack` use cases. The "Practical Example" below shows advanced usage with encrypted types.
### Practical example
```rust theme={null}
const ARRAY_SIZE: usize = 64;
#[instruction]
pub fn merge_and_sort(
player_min: Enc>,
player_max: Enc>,
) -> (Enc>, Enc>) {
// Unpack the encrypted data
let mut min_array = player_min.to_arcis().unpack();
let mut max_array = player_max.to_arcis().unpack();
// Combine, sort, and split
let mut full = [0u8; 2 * ARRAY_SIZE];
full[..ARRAY_SIZE].copy_from_slice(&min_array);
full[ARRAY_SIZE..].copy_from_slice(&max_array);
full.sort();
min_array.copy_from_slice(&full[..ARRAY_SIZE]);
max_array.copy_from_slice(&full[ARRAY_SIZE..]);
// Re-pack for output
(
player_min.owner.from_arcis(Pack::new(min_array)),
player_max.owner.from_arcis(Pack::new(max_array))
)
}
```
### Pack with crypto types
Cryptographic types like `VerifyingKey` are often passed as `Pack`:
```rust theme={null}
#[instruction]
pub fn verify_with_packed_key(
key: Pack, // Efficiently packed public key
message: [u8; 32],
signature: [u8; 64],
) -> bool {
let vk = key.unpack();
let sig = ArcisEd25519Signature::from_bytes(signature);
vk.verify(&message, &sig).reveal()
}
```
## Machine learning
Arcis includes basic ML primitives for privacy-preserving inference.
### Logistic regression
```rust theme={null}
#[instruction]
pub fn predict_class(
features: Enc,
coefficients: Enc,
intercept: Enc,
) -> Enc {
let x = features.to_arcis();
let coef = coefficients.to_arcis();
let bias = intercept.to_arcis();
let model = LogisticRegression::new(&coef, bias);
let prediction = model.predict(&x, 0.5); // threshold = 0.5
features.owner.from_arcis(prediction)
}
```
### Linear regression
```rust theme={null}
#[instruction]
pub fn predict_value(
features: Enc,
coefficients: [f64; 4], // Plaintext model weights
intercept: f64,
) -> Enc {
let x = features.to_arcis();
let model = LinearRegression::new(&coefficients, intercept);
let prediction = model.predict(&x);
features.owner.from_arcis(prediction)
}
```
### Available ML functions
| Function | Description |
| ------------------------------------------- | -------------------------------- |
| `LogisticRegression::new(coef, intercept)` | Create logistic regression model |
| `LogisticRegression::predict(x, threshold)` | Binary classification |
| `LogisticRegression::predict_proba(x)` | Probability output |
| `LinearRegression::new(coef, intercept)` | Create linear regression model |
| `LinearRegression::predict(x)` | Continuous prediction |
| `ArcisMath::sigmoid(x)` | Sigmoid activation function |
| `logit(p)` | Inverse of sigmoid |
| `expit(x)` | Alias for sigmoid |
The feature and coefficient arrays must have the same length. Arcis does not define a fixed maximum feature count, but larger arrays increase circuit cost.
## Summary
| Primitive | Use Case | Key Methods |
| -------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `ArcisRNG` | Random values | `bool()`, `gen_integer_from_width()`, `gen_public_integer_from_width()`, `gen_integer_in_range()`, `gen_uniform()`, `shuffle()` |
| `SHA3_256/512` | Hashing | `new()`, `digest()` |
| `SecretKey` | Ed25519 keys | `new_rand()`, `from_bytes()` |
| `VerifyingKey` | Signature verification | `from_secret_key()`, `verify()` |
| `MXESigningKey` | Cluster signing | `sign()` |
| `ArcisX25519Pubkey` | Public keys | `from_base58()`, `from_uint8()`, `to_x()`, `new_from_x()` |
| `BaseField25519` | Field arithmetic | `from_*()`, `to_*_unchecked()`, `power_of_two()`, `to_le_bytes()`, `safe_inverse()`, `field_division()`, `euclidean_division()` |
| `Pack` | Efficient storage | `new()`, `unpack()` |
| `LogisticRegression` | Binary classification | `new()`, `predict()`, `predict_proba()` |
| `LinearRegression` | Regression | `new()`, `predict()` |
| `ArcisMath` | Math functions | `sigmoid()` |
## What's next?
Performance optimization, debugging, and testing strategies.
Full function and method reference.
# Quick reference
Source: https://docs.arcium.com/developers/arcis/quick-reference
A quick reference cheatsheet for Arcis syntax and patterns
Arcis is a Rust framework for writing MPC circuits on Solana. This page is a **quick reference**. For conceptual understanding, see [Thinking in MPC](/developers/arcis/mental-model).
**Use this page when** you need quick syntax lookup while coding.
## Quick reference: limitations
| Category | Supported | Not supported |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| **Control flow** | `if`, `if/else`, `else if`, `if let`, `match`, `for` loops | `while`, `loop`, `break`, `continue`, `let ... else`, early `return` |
| **Types** | Integers, floats, `BaseField25519`, arrays, tuples, structs, enums and `Option` (inside circuits, not encrypted instruction inputs/outputs) | `Vec`, `String`, `HashMap` |
| **Functions** | Helpers, closures, generics, traits | Recursion, async/await |
| **Operations** | Arithmetic, comparisons, bitwise integer operations, left and right shifts (const) | Left and right shifts (variable) |
**Why?** MPC circuits must have fixed structure. See [Thinking in MPC](/developers/arcis/mental-model) for the full explanation.
## Basic structure
```rust theme={null}
use arcis::*;
#[encrypted]
mod my_circuit {
use arcis::*;
#[instruction]
pub fn add(a: u8, b: u8) -> u16 {
a as u16 + b as u16
}
}
```
* `#[encrypted]` marks modules containing MPC circuits
* `#[instruction]` marks entry points callable from Solana
## Working with encrypted data
```rust theme={null}
#[instruction]
pub fn process(input: Enc) -> Enc {
let value = input.to_arcis(); // Encrypted → secret shares
let result = value * 2 + 10; // Compute on shares
input.owner.from_arcis(result) // Secret shares → encrypted
}
```
| Owner | Who Can Decrypt |
| ---------------- | --------------- |
| `Enc` | Client AND MXE |
| `Enc` | MXE only |
## Types
```rust theme={null}
// Integers
let x: u8 = 255;
let y: i64 = -1000;
let z: u128 = 10000;
// Floats (emulated fixed-point)
let pi: f64 = 3.14159;
// Arrays (fixed-size only)
let arr: [u8; 10] = [0; 10];
// Tuples and structs
let pair: (u8, u16) = (1, 2);
#[derive(Copy, Clone)]
struct Point { x: i16, y: i16 }
```
See [Types](/developers/arcis/types) for complete reference.
## Control flow
```rust theme={null}
// if/else: when condition is not a compile-time constant, both branches execute
let result = if condition { a } else { b };
// if without else (for side effects)
if should_update {
counter += 1;
}
// else if chains work normally
let category = if value < 10 {
0
} else if value < 100 {
1
} else {
2
};
// for loops: fixed iteration count required
for i in 0..10 {
process(arr[i]);
}
// match expressions with literal, range, struct, tuple, array patterns and guards
let bucket = match x {
v if v < 5 => 0,
v if v < 10 => 1,
_ => 2,
};
// plain if let
if let Point { x: 0, y } = p {
y
} else {
-1
}
// if let combined with && requires edition = "2024" in encrypted-ixs/Cargo.toml
if let Point { x: 0, y } = p && y > 0 {
y
} else {
-1
}
// matches! macro
let in_range = matches!(x, 0..=9);
let is_small = matches!(x, 0 | 1 | 2);
```
## Functions
```rust theme={null}
// Helper function
fn helper(a: u8, b: u8) -> u16 {
a as u16 + b as u16
}
// Closures
let double = |x: u8| x * 2;
// Generics
fn set_zero(a: &mut T) {
*a = make_zero(*a);
}
```
See [Operations](/developers/arcis/operations#generics) for generics and traits.
## Arrays
```rust theme={null}
let arr: [u8; 10] = [0; 10];
// Constant index: O(1)
let x = arr[5];
// Secret index: O(n)
let y = arr[secret_idx];
// Methods
arr.swap(0, 1);
arr.reverse();
arr.fill(42);
arr.sort(); // O(n·log²(n)·bit_size)
```
## Iterators
```rust theme={null}
// Basic iteration
for val in arr.iter() {
sum += *val;
}
// Chaining
arr.iter().map(|x| *x * 2).sum()
```
See [Operations](/developers/arcis/operations#iterators) for complete iterator support. Note: `.filter()` is not supported.
## Encryption patterns
```rust theme={null}
// Shared: client + MXE can decrypt
fn process(input: Enc) -> Enc
// MXE-owned: only MXE can decrypt
fn process_state(state: Enc) -> Enc
// Reveal (use carefully)
let plain = secret.reveal();
// Create MXE-owned data
let mxe_data = Mxe::get().from_arcis(value);
// EncData output (smaller callback payload)
fn verify(a: Enc, observer: Shared) -> EncData {
observer.from_arcis(result).data
}
```
See [Input/Output](/developers/arcis/input-output) for details. For `EncData` usage, see [Types](/developers/arcis/types#advanced-encdata%3Ct%3E).
## Randomness
```rust theme={null}
let coin = ArcisRNG::bool();
let num = ArcisRNG::gen_integer_from_width(64);
let uniform = ArcisRNG::gen_uniform::<[u8; 32]>();
ArcisRNG::shuffle(&mut arr);
let (val, ok) = ArcisRNG::gen_integer_in_range(1, 100, 24);
```
See [Primitives](/developers/arcis/primitives#random-number-generation) for complete RNG reference.
## Cryptography
```rust theme={null}
// Hashing
let hash = SHA3_256::new().digest(&data).reveal();
// Signature verification
let valid = vk.verify(&message, &signature).reveal();
// Key generation
let sk = SecretKey::new_rand();
let vk = VerifyingKey::from_secret_key(&sk);
// MXE signing
let sig = MXESigningKey::sign(&message).reveal();
```
See [Primitives](/developers/arcis/primitives#cryptographic-operations) for complete crypto reference.
## Field arithmetic
```rust theme={null}
// Construct
let a = BaseField25519::from_u64(42);
let b = BaseField25519::power_of_two(8); // 256
// Arithmetic (mod 2^255 - 19)
let c = a + b;
let d = a * b;
let e = -a;
// Division (no / operator: use methods)
let inv = b.safe_inverse(); // 0 if b == 0
let quot = a.field_division(b); // 0 if b == 0
let euc = a.euclidean_division(b); // panics if b == 0
// Extract
let n: u64 = c.to_u64_unchecked(); // incorrect result if value exceeds u64 range
let bytes = c.to_le_bytes(); // [u8; 32]
```
See [Primitives](/developers/arcis/primitives#basefield25519-operations) for complete reference.
## Data packing
```rust theme={null}
// Pack for efficient storage
let packed = Pack::new(data);
// Unpack to use
let data: [u8; 64] = packed.unpack();
```
See [Primitives](/developers/arcis/primitives#data-packing) for details.
## Debugging
```rust theme={null}
println!("value = {}", x);
debug_assert!(x > 0, "x must be positive");
```
See [Best practices](/developers/arcis/best-practices#debugging) for debugging strategies.
## Testing
```rust theme={null}
#[cfg(test)]
mod tests {
#[test]
fn test_helper() {
// Only non-#[instruction] functions can be unit tested
assert_eq!(helper(1, 2), 3);
}
}
```
See [Best practices](/developers/arcis/best-practices#testing) for testing strategies.
## What's next?
Ready to build? Start here:
Build your first Arcis circuit step-by-step.
Invoke circuits from your Solana program.
Call circuits from TypeScript.
# Types
Source: https://docs.arcium.com/developers/arcis/types
Supported types in Arcis: integers, floats, arrays, structs, and encrypted types
The following types are supported:
* `u8`, `u16`, `u32`, `u64`, `u128`, `usize`, `i8`, `i16`, `i32`, `i64`, `i128`, `isize`
* `f64`, `f32` (emulated as fixed-point with 52 fractional bits; supported range is `[-2^75, 2^75)`. Values outside this range are unsupported.)
* tuples of supported types, including `()`
* fixed-length arrays of a supported type
* slices with compile-time known length (e.g., `&arr[..]` from fixed arrays, or `&[u8]` parameters in stdlib APIs like SHA3)
* compile-time known ranges
* (mutable) references to a supported type
* user-defined structs of supported types
* user-defined enums of supported types (but not as input or output of an encrypted instruction; explicit discriminants are unsupported)
* `Option` of a supported type (but not as input or output of an encrypted instruction)
* functions (but not as input or output of an encrypted instruction)
* `ArcisX25519Pubkey`, an Arcis public key wrapper.
* Arcis-defined `Enc`, `Mxe` and `Shared`.
* `Pack`, a wrapper for bit-packing data into fewer field elements for onchain storage.
* `EncData`, encrypted data without embedded cipher info. Use with `.to_arcis_with_pubkey_and_nonce()` when multiple values share the same key.
* `BaseField25519`, integers modulo `2^255 - 19`. The native field element for Arcis MPC circuits. Supports arithmetic (`+`, `-`, `*`, negation), unsigned comparisons, and field division methods. Construct via `BaseField25519::from_u64(x)` (and similar `from_*` methods); extract via `.to_u64_unchecked()` (only when the value fits the target type -- out-of-range values silently produce incorrect results). See [Primitives](/developers/arcis/primitives#basefield25519-operations).
**Float emulation**: Arcis emulates `f64`/`f32` as fixed-point with 52 fractional bits. This differs from IEEE 754 floats:
* Different precision characteristics than standard floats
* Supported range: `[-2^75, 2^75)`
* **Float literals outside this range produce a compile-time error:** `"Arcis only supports inputs in the range [-2**75, 2**75)"`
* **Computed values outside this range are silently clamped** to the boundary values
## Encryption types
| Type | Description |
| ---------------- | -------------------------------------------- |
| `Enc` | Encrypted data shared between client and MXE |
| `Enc` | Encrypted data for MXE only |
| `EncData` | Raw encrypted data (advanced use, see below) |
| `Shared` | Owner type for client-shared encryption |
| `Mxe` | Owner type for MXE-only encryption |
The `Owner` type parameter determines who can decrypt:
* **`Shared`**: Both client and MXE can decrypt. Use for user inputs/outputs that need client-side verification.
* **`Mxe`**: Only the MXE cluster can decrypt. Use for internal protocol state that users should not access.
## Public key types
| Type | Description |
| --------------------------- | -------------------------------------------- |
| `ArcisX25519Pubkey` | Arcis X25519 public key wrapper |
| `SolanaPublicKey` | Solana public key (32 bytes) |
| `SerializedSolanaPublicKey` | Serialized form using `{lo: u128, hi: u128}` |
## Example
```rust theme={null}
use arcis::*;
#[encrypted]
mod types_example {
use arcis::*;
#[derive(Copy, Clone)]
struct GameState {
score: u64,
level: u8,
}
#[instruction]
fn example(
user_data: Enc,
state: Enc,
) -> Enc {
let value = user_data.to_arcis();
user_data.owner.from_arcis(value * 2)
}
}
```
## Advanced: EncData\
`EncData` stores just the encrypted ciphertext without encryption metadata (pubkey + nonce).
| Type | Contains | Size |
| ------------------- | --------------------------------------------- | ---------- |
| `Enc` | pubkey (32B) + nonce (16B) + ciphertext (32B) | \~80 bytes |
| `EncData` | ciphertext only | \~32 bytes |
`EncData` omits pubkey and nonce metadata, useful for multiple outputs where callback payload size matters.
### Primary use: smaller callback payloads
Use `EncData` when returning encrypted data to observers to reduce callback payload size:
```rust theme={null}
#[instruction]
pub fn check_solana_public_key_equality(
encrypted_pk1: Enc,
encrypted_pk2: Enc,
observer: Shared,
) -> EncData {
let pk1 = SolanaPublicKey::from_serialized(encrypted_pk1.to_arcis());
let pk2 = SolanaPublicKey::from_serialized(encrypted_pk2.to_arcis());
let res = pk1 == pk2;
observer.from_arcis(res).data // Extract .data from Enc
}
```
**When to use `EncData` output:** Multiple return values where encryption metadata would be redundant.
When returned from circuits, `EncData` generates `EncDataStruct` in your Solana program, where N is the number of field elements in T (e.g., `EncData` → `EncDataStruct<1>`). See [Callback type generation](/developers/program/callback-type-generation).
The MXE encrypts outputs with `nonce + 1`. See [Encryption overview](/developers/encryption) for nonce handling.
### Secondary use: shared key input optimization
When multiple inputs share the same encryption key, `Enc` duplicates the key-derivation circuit for each input. Use `EncData` with explicit key/nonce to avoid this:
```rust theme={null}
#[instruction]
fn optimized_sum(
key: ArcisX25519Pubkey,
t_nonce: u128, t: EncData,
u_nonce: u128, u: EncData,
) -> u64 {
let t_val = t.to_arcis_with_pubkey_and_nonce(key, t_nonce);
let u_val = u.to_arcis_with_pubkey_and_nonce(key, u_nonce);
(t_val + u_val).reveal()
}
```
`EncData` is an advanced optimization with security implications:
* **Nonce uniqueness:** Each (key, nonce) pair must be unique. Reusing nonces compromises security.
* **Silent failures:** Using the wrong key or nonce produces garbage data without error; MPC cannot add runtime validation since that would leak information.
For most use cases, use `Enc` or `Enc`, which handle key management automatically.
## Unsupported types
Arcis does not currently support `HashMap`, `Vec`, or `String` because these types have variable length. Constant-size byte strings, such as `b"hello_world"`, are supported.
The `Enc` type defines encrypted data input as `Enc`, where `Owner` can be either `Mxe` or `Shared`. `Owner` determines which party can decrypt data of type `T`. See [Input/output](/developers/arcis/input-output) for encrypted input and output patterns.
**Storage representation:** All values are stored as 256-bit Curve25519 field elements. A `u8` uses the same storage as a `u128`; integer type bounds are enforced at compile time, not by storage size. Use `Pack` to compress multiple small values into fewer field elements for onchain efficiency.
## What's next?
Working with `Enc` for encrypted inputs and outputs.
MPC constraints and the operation cost model.
# Arcium.toml
Source: https://docs.arcium.com/developers/arcium-toml
Configuration reference for the Arcium CLI tooling suite
`Arcium.toml` is auto-generated by `arcium init` and configures the Arcium CLI tooling. It lives at the root of your project alongside `Anchor.toml`.
Most projects only need to edit this file when changing localnet behavior or adding a cluster offset for devnet or mainnet testing.
## Localnet configuration
The `[localnet]` section controls your local development cluster. All required fields are auto-generated by `arcium init` with sensible defaults.
| Field | Type | Required | Default | Description |
| ----------------------- | ------------------ | -------- | ------------------------------ | --------------------------------------------------- |
| `nodes` | integer | Yes | `2` | Number of MPC nodes. Minimum: 2 |
| `nodes_ips` | array of `[u8; 4]` | No | Sequential from `172.20.0.100` | IPv4 addresses for each node |
| `localnet_timeout_secs` | integer | Yes | `60` | Seconds to wait for localnet startup |
| `backends` | array of string | No | Ignored | Written by the init template but ignored by the CLI |
Use the default values unless you need to test against a larger local cluster or a custom Docker network setup.
## Cluster configuration
The `[clusters.]` sections map network names to cluster offsets. These entries are used by `arcium test --cluster ` to resolve the cluster offset for testing.
| Field | Type | Required | Description |
| -------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `offset` | integer | Yes | Cluster offset for PDA derivation. See [Deployment - Cluster offsets](/developers/deployment#understanding-cluster-offsets) for current values. |
The CLI reads these entries when you run commands such as:
```bash theme={null}
arcium test --cluster devnet
```
For local tests without a `--cluster` argument, the CLI uses the `[localnet]` configuration.
## Full example
```toml theme={null}
[localnet]
nodes = 2
nodes_ips = [
[172, 20, 0, 100],
[172, 20, 0, 101]
]
localnet_timeout_secs = 60
backends = ["Cerberus"] # Ignored by the CLI
[clusters.devnet]
offset = 456
[clusters.mainnet]
offset = 2026
```
## Common mistakes
| Issue | Fix |
| ---------------------------------------------------- | -------------------------------------------------------------------- |
| `arcium test --cluster ` cannot find a cluster | Add `[clusters.]` with an `offset` field |
| Tests derive the wrong cluster account | Check that the offset matches the target network |
| Localnet startup times out | Increase `localnet_timeout_secs` or reduce local resource contention |
| Custom node IPs do not work | Keep `nodes` and `nodes_ips` lengths aligned |
# Computation lifecycle
Source: https://docs.arcium.com/developers/computation-lifecycle
How computations flow from client-side encryption through MPC execution to callback
Before diving into the details of the tooling, it's useful to understand the general architecture of Arcium. The below diagram gives a high-level overview of the lifecycle of a typical interaction with Arcium (we call these "computations").
```mermaid theme={null}
sequenceDiagram
participant Client
participant MXE Program
participant Arcium Program
participant MPC Cluster
Client->>Client: Encrypt params
Client->>MXE Program: Invoke computation with encrypted params
MXE Program->>Arcium Program: Handle & format params and send to Arcium Program
Arcium Program->>Arcium Program: Queue Computation in Cluster's Mempool
MPC Cluster->>Arcium Program: Fetch new computation from mempool
MPC Cluster->>MPC Cluster: Compute using MPC
MPC Cluster->>Arcium Program: Callback with Result
Arcium Program->>Arcium Program: Verify Result
Arcium Program->>MXE Program: Invoke callback instruction with result
MXE Program->>MXE Program: Handle Result
MXE Program->>Client: Notify of completion
```
We have 4 key actors here:
* The client: The party that wants to perform a computation, usually the user of your MXE. This is implemented using the [Arcium TypeScript client library](/developers/js-client-library).
* The MXE Program: Your app. An MXE (MPC eXecution Environment) consists of everything needed to perform computations and is implemented using the [Arcium program tooling](/developers/program):
* A smart contract that is deployed on the blockchain and is used to format and submit computations to Arcium.
* A set of encrypted instructions (we call these "computation definitions") that are used to define what parameters are needed for the computation and what the computation is. Writing these is done using [Arcis](/developers/arcis).
* Some metadata about the MXE, most importantly the MPC cluster we would like to use to compute our computations.
* The Arcium Program: The program in charge of assigning, scheduling, and verifying computations for the various MPC clusters to perform.
* The MPC Cluster: The parties that are performing the client's computations using MPC.
The MXE and its computation definitions are onchain accounts with their own lifecycle. When you no longer need them, you can close them and reclaim rent. See [Account lifecycle and closing](/developers/program/account-lifecycle).
## What's next?
Learn the Arcis framework for encrypted business logic.
How X25519 key exchange and Rescue cipher protect your data.
Queue computations from your Solana program.
# Continuous integration
Source: https://docs.arcium.com/developers/continuous-integration
Build and test Arcium projects in GitHub Actions with a pinned setup-arcium toolchain
The [`arcium-hq/setup-arcium`](https://github.com/arcium-hq/setup-arcium) action installs the Arcium CLI, Anchor, Solana CLI, Node.js, and Yarn so you can build and test an MXE in GitHub Actions.
## Add the workflow
```yaml theme={null}
name: arcium
on:
push:
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: arcium-hq/setup-arcium@v0.13.2
with:
runner-arch-os: x86_64_linux
arcium-version: "0.13.2"
anchor-version: "1.0.2"
solana-cli-version: "3.1.10"
node-version: "24.10.0"
- name: Install dependencies
run: yarn install --frozen-lockfile
shell: bash
- name: Build
run: arcium build
shell: bash
- name: Test
run: |
sudo prlimit --pid $$ --nofile=1048576:1048576
arcium test
shell: bash
```
Pin the action and each tool to versions that match your project. Check the [setup-arcium releases](https://github.com/arcium-hq/setup-arcium/releases) before changing the action version.
## Configure the inputs
| Input | Required | Default | Notes |
| -------------------- | -------- | --------- | ----------------------------------------------------------------------------------------- |
| `runner-arch-os` | yes | None | Use `x86_64_linux` for `ubuntu-latest` or `aarch64_macos` for Apple Silicon macOS runners |
| `arcium-version` | no | `0.13.2` | Match the Arcium dependencies in your project |
| `anchor-version` | no | `1.0.2` | Match the version your project pins |
| `solana-cli-version` | no | `3.1.10` | Use Solana CLI 2.x or newer |
| `node-version` | no | `24.10.0` | Match your project's Node.js version |
## Raise the Linux file limit
`arcium test` starts validator and node processes that can exhaust the default open-file limit on Linux runners. The workflow raises the limit with `prlimit` in the test step. GitHub Actions starts a new shell for each step, so running `prlimit` in an earlier step does not affect the test process.
## What's next?
Deploy your MXE to devnet or mainnet after CI passes.
Local-machine setup for the same toolchain.
# Core concepts
Source: https://docs.arcium.com/developers/core-concepts
Key terminology for Arcium development: MXE, Clusters, Encrypted Instructions, Computation Definitions, offsets, and how they connect
Key terms you'll encounter throughout Arcium development.
Your complete encrypted application composed of three parts:
* A **Solana program** that receives inputs and queues computations
* **Encrypted instructions** with encrypted logic written in Arcis
* An **MXE account** storing onchain metadata like cluster selection and the MXE's public key
When no longer needed, MXE accounts can be closed through the [account lifecycle](/developers/program/account-lifecycle).
A group of Multi-Party Computation (MPC) nodes that execute your encrypted instructions. You choose
which cluster when deploying your MXE. The selection is made at deployment time
but can be changed later via [`arcium migrate-cluster`](/developers/deployment#cluster-migration).
Each cluster has a numeric `cluster_offset` identifier (e.g., `456` on devnet)
that you specify during [deployment](/developers/deployment).
Your encrypted business logic written in **Arcis** (Rust framework) that
executes on encrypted data via MPC. Mark functions with the `#[instruction]`
macro to designate them as encrypted instructions. Examples: vote counting,
order matching, sealed-bid auctions.
The onchain coordinator on Solana that orchestrates MPC computations, routes
work to clusters, and manages callbacks and finalization. You interact with it
via **CPI** (Cross-Program Invocation) from your Solana program.
An onchain account storing compiled MPC bytecode. Created once per
encrypted instruction, it contains the compiled circuit from your Arcis
code and must be initialized via `init_computation_def()` before first use.
When no longer needed, a computation definition has its own [close lifecycle](/developers/program/account-lifecycle).
A single execution instance of an encrypted instruction. You generate a random
`computation_offset` (u64) per invocation to uniquely identify it. The computation
tracks execution status and stores encrypted results until callback.
An MPC execution node that participates in cluster computations. Each node
holds secret shares and collaborates via the MPC protocol. Clusters contain
multiple Arx nodes. The dishonest majority model means privacy holds even
if all but one node are compromised.
```mermaid theme={null}
graph TD
A[MXE Program] -->|queues computation| B[Arcium Program]
A -->|references| C[MXE Account]
B -->|routes to| D[Cluster]
D -->|executes| E[Encrypted Instructions]
D -->|returns result via callback| A
```
## Offset identifiers
Arcium uses numeric offsets to identify onchain resources. You'll encounter these throughout the SDK:
| Offset | Type | Purpose | How it's set |
| -------------------- | ----- | ------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| `cluster_offset` | `u32` | Identifies which cluster your MXE connects to | Chosen at [deployment](/developers/deployment) (e.g., `456` on devnet) |
| `computation_offset` | `u64` | Uniquely identifies a single computation invocation | Generated randomly per invocation (`randomBytes(8)`) |
| `comp_def_offset` | `u32` | Identifies a computation definition (circuit) within your MXE | Via `comp_def_offset("instruction_name")`, which computes `sha256(name)` truncated to LE u32 |
## What's next?
Understand how computations flow from queue to callback
Learn Arcis and write encrypted business logic
# Deployment
Source: https://docs.arcium.com/developers/deployment
Deploy your MXE to Solana with cluster configuration and RPC setup
## Getting started with deployment
Deploy after `arcium build` and `arcium test` pass locally. Use the [continuous integration guide](/developers/continuous-integration) to run both checks on each change. This guide covers program preparation, cluster offsets, RPC setup, migration, and closing accounts.
This guide walks you through deploying to **devnet** first. Once you've validated your MXE on devnet, see [Deploying to Mainnet](#deploying-to-mainnet) for mainnet-specific configuration.
## What you'll need
Before deploying, make sure you have:
* Your MXE built successfully with `arcium build`
* Tests passing locally with `arcium test`
* A Solana keypair with around 2-5 SOL for deployment costs (program deployment and account initialization)
* Access to a reliable RPC endpoint
**RPC reliability is critical for deployment.** Default Solana RPC endpoints can be unreliable and drop transactions, causing deployment failures. Get an API key from [Helius](https://helius.dev), [Triton](https://triton.one), or [QuickNode](https://quicknode.com) before attempting deployment.
## Preparing your program
Before deploying, decide how your program stores computation definitions.
### Handling large circuits with offchain storage
Arcis compiled circuits can be several MB. Uploading the full bytecode onchain can require many transactions and high rent.
For larger circuits, upload the compiled `.arcis` file to public storage and initialize the computation definition with its URL and hash.
**Standard approach (works for small circuits):**
```rust theme={null}
pub fn init_add_together_comp_def(ctx: Context) -> Result<()> {
// This initializes the computation definition account
init_computation_def(ctx.accounts, None)?;
Ok(())
}
```
**Offchain approach (recommended for larger circuits):**
```rust theme={null}
// First, import the types you'll need
use arcium_client::idl::arcium::types::{CircuitSource, OffChainCircuitSource};
use arcium_macros::circuit_hash;
pub fn init_add_together_comp_def(ctx: Context) -> Result<()> {
// Point to your uploaded circuit file
init_computation_def(
ctx.accounts,
Some(CircuitSource::OffChain(OffChainCircuitSource {
source: "https://your-storage.com/path/to/add_together.arcis".to_string(),
hash: circuit_hash!("add_together"),
})),
)?;
Ok(())
}
```
The `circuit_hash!` macro embeds the SHA-256 hash of your compiled circuit at compile time. The hash is read from `build/{circuit_name}.hash`, which is generated automatically during `arcium build`. Arx nodes verify this hash when fetching your circuit to ensure the circuit hasn't been tampered with.
**Important:** Always use `circuit_hash!` for offchain circuits. Don't use a placeholder like `[0u8; 32]` - this will cause verification to fail on Arx nodes.
Offchain circuit flow:
1. Build your project with `arcium build` to generate the circuit files and hashes
2. Upload the `.arcis` files from `build/` folder to your preferred storage service
3. Update your init functions with the public URLs and `circuit_hash!` macro calls
Circuit files must be publicly readable without authentication.
### Note on cluster configuration
When testing locally, you've been using `arciumEnv.arciumClusterOffset` with `getClusterAccAddress()` in your test code. For devnet deployment, you'll use the same pattern with your chosen cluster offset - we'll show you exactly how in the post-deployment section.
## Basic deployment
The `arcium deploy` command handles both deploying your program and initializing the MXE account. Here's the basic command structure:
```bash theme={null}
arcium deploy --cluster-offset --recovery-set-size --keypair-path --rpc-url
```
For a fresh MXE initialization, `--keypair-path` must point to the deployed program's current upgrade authority, including when you use `--skip-deploy`. An immutable program has no upgrade authority and can never initialize a fresh MXE. Deploy a new program that retains its upgrade authority instead.
Let's break down what each parameter does:
### Understanding cluster offsets
The `--cluster-offset` tells your MXE which Arcium cluster it should connect to. Think of clusters as groups of nodes that will perform your encrypted computations. Available offsets:
**Devnet cluster:**
* `456`
**Mainnet cluster:**
* `2026`
### Recovery set size
The `--recovery-set-size` parameter specifies how many nodes form the recovery set that holds encrypted key shares of your MXE's key. This enables key reconstruction when needed, whether due to node failure or cluster migration. This is required. The absolute minimum is `4`; larger clusters may require a larger recovery set, and the CLI prints the required value if the one you pass is too small.
### Choosing your RPC provider
Always pass `--rpc-url `. Solana's default RPC endpoints (`-u d`, `-u m`) can drop transactions during deployment.
**Recommended approach with a reliable RPC:**
```bash theme={null}
arcium deploy --cluster-offset 456 \
--recovery-set-size 4 \
--keypair-path ~/.config/solana/id.json \
--rpc-url
```
**If you must use the default RPC:**
```bash theme={null}
arcium deploy --cluster-offset 456 \
--recovery-set-size 4 \
--keypair-path ~/.config/solana/id.json \
-u d # 'd' for devnet, 'm' for mainnet, 'l' for localnet
```
Just be prepared for potential transaction failures with the default RPC.
## Advanced deployment options
Once you're comfortable with basic deployment, you might want to customize things further.
### Using a custom program address
If you need your program at a specific address (maybe for consistency across deployments), you can provide a program keypair:
```bash theme={null}
arcium deploy --cluster-offset 456 \
--recovery-set-size 4 \
--keypair-path ~/.config/solana/id.json \
--rpc-url \
--program-keypair ./program-keypair.json
```
### Partial deployments
Sometimes you might need to run just part of the deployment process. For instance, if you've already deployed the program but need to reinitialize the MXE account:
```bash theme={null}
# Skip program deployment, only initialize MXE account
arcium deploy --cluster-offset 456 \
--recovery-set-size 4 \
--keypair-path ~/.config/solana/id.json \
--rpc-url \
--skip-deploy
```
Or if you only want to deploy the program without initialization:
```bash theme={null}
# Deploy program only, skip MXE initialization
arcium deploy --cluster-offset 456 \
--recovery-set-size 4 \
--keypair-path ~/.config/solana/id.json \
--rpc-url \
--skip-init
```
## After deployment
### Initialize your computation definitions
Your MXE is deployed, but you still need to initialize the computation definitions. This tells the Arcium Network what encrypted operations your MXE can perform. Computation definitions only need to be initialized once - they persist onchain and don't need to be re-initialized unless you're deploying to a new program address. You can initialize them anytime after deployment completes successfully.
Remember how we mentioned you'd need to update your cluster configuration? Now's the time! You'll need to update your test or client code to derive the cluster account (and the related PDAs) from the cluster offset you selected during deployment.
**Local testing pattern:**
```typescript theme={null}
const arciumEnv = getArciumEnv();
// In your transaction...
.accountsPartial({
computationAccount: getComputationAccAddress(
arciumEnv.arciumClusterOffset,
computationOffset
),
clusterAccount: getClusterAccAddress(arciumEnv.arciumClusterOffset),
mxeAccount: getMXEAccAddress(program.programId),
mempoolAccount: getMempoolAccAddress(arciumEnv.arciumClusterOffset),
executingPool: getExecutingPoolAccAddress(arciumEnv.arciumClusterOffset),
// ... other accounts
})
```
**For devnet deployment:**
```typescript theme={null}
// Use the cluster offset from your deployment (e.g., 456)
const clusterOffset = 456;
// In your transaction...
.accountsPartial({
computationAccount: getComputationAccAddress(clusterOffset, computationOffset),
clusterAccount: getClusterAccAddress(clusterOffset),
mxeAccount: getMXEAccAddress(program.programId),
mempoolAccount: getMempoolAccAddress(clusterOffset),
executingPool: getExecutingPoolAccAddress(clusterOffset),
// ... other accounts
})
```
Make sure to use the same `cluster_offset` value that you used during deployment! This ensures your program talks to the right cluster.
Once you've updated the cluster configuration, you can run the initialization:
```typescript theme={null}
// Now with the correct cluster configured
await initAddTogetherCompDef(program, owner);
```
### Verify everything's working
Let's make sure your deployment succeeded:
```bash theme={null}
solana program show --url
```
To run your tests against the deployed program, configure the cluster offset in your `Arcium.toml`:
```toml theme={null}
[clusters.devnet]
offset =
```
Then run:
```bash theme={null}
arcium test --cluster devnet
```
The CLI reads the cluster offset from `Arcium.toml` and configures the test environment automatically. For RPC configuration, set the cluster and wallet in your `Anchor.toml`:
```toml theme={null}
[provider]
cluster = "devnet"
wallet = "~/.config/solana/id.json"
```
## Deploying to mainnet
Once you've validated your MXE on devnet, you can deploy to Arcium mainnet. The deployment process is the same, with a few key differences:
### Mainnet cluster offset
Use the mainnet cluster offset from the [Understanding Cluster Offsets](#understanding-cluster-offsets) section:
```bash theme={null}
arcium deploy --cluster-offset \
--recovery-set-size 4 \
--keypair-path ~/.config/solana/id.json \
--rpc-url
```
### Mainnet configuration
Configure the mainnet cluster offset in your `Arcium.toml` (see [Understanding Cluster Offsets](#understanding-cluster-offsets) for the current value):
```toml theme={null}
[clusters.mainnet]
offset =
```
To test against your mainnet deployment:
```bash theme={null}
arcium test --cluster mainnet
```
Mainnet deployments require real SOL. There are no airdrops. Ensure your wallet is funded before deploying.
## Common issues and solutions
The examples below use devnet. For mainnet, replace `-u d` with `-u m` and use a mainnet RPC URL.
### Dealing with dropped transactions
If your deployment fails with transaction errors, it's almost always the RPC. Switch to a dedicated provider:
```bash theme={null}
# Instead of this (unreliable):
arcium deploy --cluster-offset 456 \
--recovery-set-size \
--keypair-path ~/.config/solana/id.json \
-u d
# Use this (reliable):
arcium deploy --cluster-offset 456 \
--recovery-set-size \
--keypair-path ~/.config/solana/id.json \
--rpc-url
```
### Running out of SOL
Check your balance before deploying:
```bash theme={null}
solana balance -u devnet
```
If you need more SOL on devnet, request it via airdrop:
```bash theme={null}
solana airdrop 2 -u devnet
```
### Deployment partially failed?
If your deployment was interrupted (e.g., due to a dropped transaction or network issue), use the `--resume` flag to pick up where it left off:
```bash theme={null}
arcium deploy --cluster-offset 456 \
--recovery-set-size 4 \
--keypair-path ~/.config/solana/id.json \
--rpc-url \
--resume
```
This skips already-completed steps and retries from the point of failure. The `--resume` flag is also available on `init-mxe` and `migrate-cluster` for the same purpose. For finer control, you can also skip specific phases: `--skip-deploy` (skip program deployment, only initialize MXE) or `--skip-init` (deploy program only, skip MXE initialization).
## Managing your MXE
### Cluster migration
If you need to move your MXE from one cluster to another, use the `migrate-cluster` command:
```bash theme={null}
arcium migrate-cluster \
--keypair-path \
--cluster-offset \
--rpc-url
```
This reconstructs your MXE's keys on the new cluster using the recovery set configured during deployment. Your MXE will be temporarily unavailable during migration.
MXEs deployed with v0.9.x already have the recovery material needed for `migrate-cluster`; no separate remediation command is required before migrating them.
If a migration is interrupted, resume it with `--resume`:
```bash theme={null}
arcium migrate-cluster \
--keypair-path \
--rpc-url \
--resume
```
To abort an in-progress migration:
```bash theme={null}
arcium migrate-cluster \
--keypair-path \
--rpc-url \
--abort
```
With `--resume`/`--abort` the CLI infers the cluster offset from onchain state; `--cluster-offset` is required only on the initial call.
Do not abort and then re-initialize a migration back to the same target cluster. That opens a denial-of-service surface. If you need to retry, resume instead.
### Closing an MXE or computation definition
When you no longer need an MXE or one of its computation definitions, close the accounts to reclaim rent. Comp defs close in two steps: deactivate, wait 180 slots, then close. MXEs can close only after all user-defined comp defs are closed.
```bash theme={null}
# Comp def: deactivate, wait for TTL, then close
arcium deactivate-computation-definition \
-o \
-p \
-k \
--rpc-url
arcium close-computation-definition \
-o \
-p \
-c \
-k \
--rpc-url
# OnChain circuits also need their raw buffers closed (one call per index)
arcium close-computation-definition-buffers \
-o \
-p \
-i \
-k \
--rpc-url
# MXE
arcium close-mxe \
-p \
-k \
--rpc-url
```
Pass an explicit `--rpc-url`: the CLI defaults to mainnet without it.
For the full state machine, authority rules, error codes, and common mistakes, see [Account lifecycle and closing](/developers/program/account-lifecycle).
## What's next?
After deployment, update your client code to use the correct cluster offset, initialize your computation definitions onchain, and run end-to-end tests on devnet.
Learn patterns and optimizations for efficient encrypted instructions.
Browse runnable MXE projects mapped to the docs concept they demonstrate.
For questions or issues, reach out on [Discord](https://discord.gg/arcium).
# Encryption overview
Source: https://docs.arcium.com/developers/encryption
How encryption works in Arcium: X25519 key exchange, Rescue cipher, and data protection
Encrypted data is passed as an `Enc` generic type. `Owner` specifies who can decrypt the data (`Shared` or `Mxe`), and `T` is the encrypted data type. With `Mxe`, the nodes can collectively decrypt the data under dishonest-majority assumptions. With `Shared`, the data is encrypted with a shared secret between the client and the MXE. The wrapper contains the ciphertext, the nonce, and, for `Shared` ownership, the public key used to encrypt the data.
Encrypted data can be decrypted globally or selectively to a specific user. For global decryption, call the `reveal` method on any variable with a [supported data type](/developers/arcis/types). For selective decryption, use [sealing](/developers/encryption/sealing), which re-encrypts data for a chosen recipient.
Private inputs are encrypted using the arithmetization-oriented symmetric [Rescue cipher](https://eprint.iacr.org/2019/426). Before the cipher runs, the client and cluster perform an [X25519](https://www.rfc-editor.org/rfc/rfc7748.html#page-7) elliptic curve Diffie-Hellman key exchange to derive a shared secret. The Rescue key is derived by hashing the shared secret with the [Rescue-Prime](https://eprint.iacr.org/2020/1143.pdf) hash function, as described in [Section 4](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-56Cr2.pdf), Option 1. This increases the min-entropy of the key.\
Note:
1. Since the X25519 key exchange natively returns shared secrets in the finite field with $p = 2^{255} - 19$ elements, we implemented Rescue over the field $\mathbb{F}_{p}$. States in the context of Rescue are elements of the $m$-dimensional vector space $\mathbb{F}_p^m$, i.e., the Rescue cipher transforms vectors of size $m$ to vectors of the same size.
2. The security level $s$ of the cipher is set to 128 bits.
3. We use the Rescue block cipher in [Counter (CTR) mode](https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38a.pdf) (see Section 6.5), with fixed $m = 5$. The choice $m = 5$ is motivated by the fact that it is the smallest value that attains the minimum of recommended rounds (10), given the fixed finite field and security level. The counters have the form `[nonce, i, 0, 0, 0]`, where `nonce` is a 16-byte random value provided by the user.
4. The hash function used for key derivation is Rescue-Prime over $\mathbb{F}_{2^{255}-19}$, with `rate = 7` and `capacity = 5` (i.e., $m = 12$) and output truncated to 5 field elements. The target security level $s$ is set to 256. According to [Section 2.2](https://eprint.iacr.org/2020/1143.pdf), this offers 256 bits of security against collision, preimage and second-preimage attacks for any field of size at least 102 bits.
The decryption of `input_enc: Enc` can conveniently be obtained by calling `input_enc.to_arcis()` (the nodes do not learn `input`; they convert the ciphertext to secret-shares of `input` by running the Rescue decryption circuit in MPC). If the owner is `Shared`, the MXE and the client perform a key exchange first. Similarly, `owner.from_arcis(output)` encrypts the secret-shared `output` by running the Rescue encryption circuit in MPC.\
Note:
1. After decrypting the user-provided inputs, the MXE increments the `nonce` by 1 and uses it for encrypting the outputs. For the forthcoming interaction with the MXE, a new `nonce` must be provided.
2. The performance will benefit from reducing the number of calls to `owner.from_arcis(..)` (per owner). Ideally, put all data encrypted to `owner` in one struct.
[`EncData`](/developers/arcis/types#advanced-encdata%3Ct%3E) contains only ciphertext, omitting pubkey and nonce. Use for multiple outputs to reduce callback payload size.
## What's next?
Implement encryption in your TypeScript client.
Re-encrypt data between different parties.
# Sealing (re-encryption)
Source: https://docs.arcium.com/developers/encryption/sealing
Re-encrypt data between parties using sealing patterns
Suppose you're Alice and have secret data onchain that you want to share with Bob. You may also want to compute a function on sensitive data and share the result with Bob without revealing the data or result to anyone else.
Arcium can re-encrypt any data to a given public key. This is known as "sealing" in cryptography: restricting data access to specific recipients.
This is useful for a variety of reasons, such as compliance, end-to-end privacy, and more.
```rust theme={null}
#[encrypted]
mod circuits {
use arcis::*;
#[instruction]
pub fn verify_loan_eligibility(
alice_balance: Enc,
min_balance_required: Enc,
loan_officer: Shared
) -> Enc {
let balance = alice_balance.to_arcis();
let threshold = min_balance_required.to_arcis();
// Check if Alice meets minimum balance for loan without revealing her exact balance
let is_eligible = balance >= threshold;
// Re-encrypt the result for the loan officer
loan_officer.from_arcis(is_eligible)
}
}
```
In this example, we have an encrypted function `verify_loan_eligibility` that takes Alice's encrypted balance (encrypted with a shared secret between Alice and the MXE), the minimum balance requirement (encrypted only for the MXE), and a `Shared` type parameter representing the loan officer who will receive the result.
The function checks if Alice meets the minimum balance requirement for loan eligibility without revealing her actual balance to anyone. The boolean result is then re-encrypted specifically for the loan officer using their public key. This way, Alice's financial privacy is preserved: the loan officer only learns whether she's eligible, not her actual balance, and Arx nodes never see plaintext, only secret shares.
## What's next?
Learn how to encrypt data before sending to the MXE.
Call encrypted instructions from your Solana program.
# Examples
Source: https://docs.arcium.com/developers/examples
Browse runnable encrypted applications grouped by complexity and mapped to the Arcium concepts they demonstrate
Browse complete MXE projects in the [Arcium examples repository](https://github.com/arcium-hq/examples). Each project includes its Arcis circuits, Solana program, tests, and setup instructions.
The examples currently target Arcium `0.12.0`, while the latest toolchain is `0.13.2`. Check each project's manifests before copying code into a project on another release.
## Start with the fundamentals
| Example | What it demonstrates | Maps to |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Coinflip](https://github.com/arcium-hq/examples/tree/main/coinflip) | Trustless randomness with `ArcisRNG`; stateless circuit | [Arcis primitives](/developers/arcis/primitives), [Callback type generation](/developers/program/callback-type-generation) |
| Rock Paper Scissors: [player vs player](https://github.com/arcium-hq/examples/tree/main/rock_paper_scissors/against-player), [player vs house](https://github.com/arcium-hq/examples/tree/main/rock_paper_scissors/against-house) | Encrypted async gameplay with hidden moves or an MPC-generated opponent | [Input/output](/developers/arcis/input-output), [Callback accounts](/developers/program/callback-accs), [Arcis primitives](/developers/arcis/primitives) |
## Build stateful applications
| Example | What it demonstrates | Maps to |
| ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| [Voting](https://github.com/arcium-hq/examples/tree/main/voting) | Private ballots with public results via encrypted state accumulation | [Callback type generation](/developers/program/callback-type-generation) |
| [Share medical records](https://github.com/arcium-hq/examples/tree/main/share_medical_records) | Patient-controlled re-encryption between parties | [Sealing](/developers/encryption/sealing) |
| [Sealed-bid auction](https://github.com/arcium-hq/examples/tree/main/sealed_bid_auction) | Encrypted bid comparison supporting first-price and Vickrey mechanisms | [Operations](/developers/arcis/operations) |
## Explore advanced patterns
| Example | What it demonstrates | Maps to |
| ----------------------------------------------------------------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| [Blackjack](https://github.com/arcium-hq/examples/tree/main/blackjack) | Hidden deck state with `Pack` for compact encrypted storage | [Arcis primitives](/developers/arcis/primitives#data-packing), [Types](/developers/arcis/types) |
| [Ed25519 signatures](https://github.com/arcium-hq/examples/tree/main/ed25519) | Threshold signing where the private key never exists in one place | [Arcis primitives](/developers/arcis/primitives#ed25519-signatures), [Input/output](/developers/arcis/input-output) |
## What's next?
Build your first encrypted instruction end-to-end.
Syntax cheatsheet for the patterns these examples use.
# Hello World with Arcium
Source: https://docs.arcium.com/developers/hello-world
Build your first encrypted circuit with Arcis (Rust), a Solana program, and TypeScript tests
## Hello World
The Arcium tooling suite for writing MXEs (MPC eXecution Environments) is built on top of [Anchor](https://www.anchor-lang.com/), so if you're familiar with Anchor, you should find Arcium to be a familiar experience, except that you're using the `arcium` CLI instead of `anchor`.
To initialize a new MXE project, run:
```bash theme={null}
arcium init
```
This will create a new project with the given name and initialize it with a basic structure. The structure is the same as in an Anchor project with two differences, so it is not repeated here (for an explanation of the Anchor project structure, see the [Anchor documentation](https://www.anchor-lang.com/docs/quickstart/local)). The two differences are:
* The `Arcium.toml` file, which contains the configuration for the Arcium tooling suite.
* The `encrypted-ixs` directory. This is where we write all our code that is meant to operate on encrypted data and therefore runs in MPC. This code is written using our own Rust framework called [Arcis](/developers/arcis). This will already be populated with a simple example called `add_together.rs`. Let's take a closer look at it.
### Our first encrypted instruction
```rust theme={null}
use arcis::*;
#[encrypted]
mod circuits {
use arcis::*;
pub struct InputValues {
v1: u8,
v2: u8,
}
#[instruction]
pub fn add_together(input_ctxt: Enc) -> Enc {
let input = input_ctxt.to_arcis();
let sum = input.v1 as u16 + input.v2 as u16;
input_ctxt.owner.from_arcis(sum)
}
}
```
Let's go through it line by line. `use arcis::*;` imports all the necessary types and functions for writing encrypted instructions with Arcis. The `#[encrypted]` attribute marks a module that contains encrypted instructions. Inside this module, we define a struct `InputValues` that contains the two values we want to encrypt and pass to the encrypted instruction.
The `#[instruction]` macro marks the function as an entry point for MPC execution. While you can write helper functions without this attribute, only functions marked with `#[instruction]` will be compiled into individual circuits that can be called onchain.
The function `add_together` takes an encrypted input parameter of type `Enc`. Let's break this down:
* `Enc` is Arcium's encrypted data type
* `Shared` means the data is encrypted with a shared secret between the client and MXE (both can decrypt it)
* `InputValues` is the actual data structure being encrypted (our struct with v1 and v2)
* The alternative to `Shared` is `Mxe`, where only the MXE can decrypt the data
Inside the function:
1. `input_ctxt.to_arcis()` converts the input into a form we can operate on within the MPC environment.
2. We perform the addition operation, casting the u8 values to u16 to prevent overflow.
3. `input_ctxt.owner.from_arcis(sum)` converts the encrypted sum into an encrypted format that can be stored onchain, while maintaining encryption with the shared secret between the client and the MXE.
### Calling it from Solana
Now that we've written our first encrypted instruction, let's see how we can use it from within a Solana program. Our default project already contains a Solana program in the `programs/` directory. Let's take a closer look at it too:
```rust theme={null}
use anchor_lang::prelude::*;
use arcium_anchor::prelude::*;
// This constant identifies our encrypted instruction for onchain operations
// comp_def_offset() generates a unique identifier from the function name
const COMP_DEF_OFFSET_ADD_TOGETHER: u32 = comp_def_offset("add_together");
declare_id!("YOUR_PROGRAM_ID_HERE");
#[arcium_program]
pub mod hello_world {
use super::*;
pub fn init_add_together_comp_def(ctx: Context) -> Result<()> {
init_computation_def(ctx.accounts, None)?;
Ok(())
}
pub fn add_together(
ctx: Context,
computation_offset: u64,
ciphertext_0: [u8; 32],
ciphertext_1: [u8; 32],
pub_key: [u8; 32],
nonce: u128,
) -> Result<()> {
let args = ArgBuilder::new()
.x25519_pubkey(pub_key)
.plaintext_u128(nonce)
.encrypted_u8(ciphertext_0)
.encrypted_u8(ciphertext_1)
.build();
ctx.accounts.sign_pda_account.bump = ctx.bumps.sign_pda_account;
queue_computation(
ctx.accounts,
computation_offset,
args,
vec![AddTogetherCallback::callback_ix(
computation_offset,
&ctx.accounts.mxe_account,
&[]
)?],
1,
0, // cu_price_micro: priority fee in microlamports
0, // callback_cu_limit: compute unit limit for the callback (0 = default)
)?;
Ok(())
}
#[arcium_callback(encrypted_ix = "add_together")]
pub fn add_together_callback(
ctx: Context,
output: SignedComputationOutputs,
) -> Result<()> {
let o = match output.verify_output(
&ctx.accounts.cluster_account,
&ctx.accounts.computation_account
) {
Ok(AddTogetherOutput { field_0 }) => field_0,
Err(e) => {
msg!("Error: {}", e);
return Err(ErrorCode::AbortedComputation.into())
},
};
emit!(SumEvent {
sum: o.ciphertexts[0],
nonce: o.nonce.to_le_bytes(),
});
Ok(())
}
}
```
For brevity, the `InitAddTogetherCompDef`, `AddTogether`, and `AddTogetherCallback` account structs are not included here, but they are automatically generated when you run `arcium init`. Here's a simplified version of what `AddTogether` looks like:
```rust theme={null}
#[derive(Accounts)]
#[instruction(computation_offset: u64)]
pub struct AddTogether<'info> {
#[account(mut)]
pub payer: Signer<'info>,
// ... other required Arcium accounts (see program/ section for full details)
}
```
For the full Solana program flow, see [Invoking computations from your Solana program](/developers/program).
The key things to note here are that every MXE program is identified by the `#[arcium_program]` macro (which replaces Anchor's `#[program]` macro) and that for every encrypted instruction, we generally have three instructions in the Solana program:
* `init_add_together_comp_def`: Initializes the computation definition. This runs once before the first invocation of the encrypted instruction. See [Computation definition accounts](/developers/program/computation-def-accs).
* `add_together`: Invokes the encrypted instruction, builds the arguments, and queues the computation through the Arcium program. See [Invoking computations](/developers/program).
* `add_together_callback`: Runs after the MPC cluster finishes the encrypted instruction and returns the result. See [Invoking computations](/developers/program).
These three instructions mirror the standard [computation lifecycle](/developers/computation-lifecycle).
## Building and testing
Similar to Anchor, encrypted instructions and Solana programs are built with `arcium build`. Tests use the [`@arcium-hq/client` TypeScript library](/developers/js-client-library) by default and run with `arcium test`. Install npm dependencies first with `yarn` or `npm install` in your project directory. By default, tests run against a local cluster. To test against devnet or mainnet, use `arcium test --cluster devnet` after adding the cluster offset to `Arcium.toml`, for example `[clusters.devnet] offset = 456`.
For faster local loops, use `arcium test --test-name ` to run `tests/.ts`. After a full localnet keygen, `arcium test --skip-keygen` or `arcium localnet --skip-keygen` reuses cached MXE keys; for detached runs, call `arcium snapshot-mxe-keygen --rpc-url l` before shutting down the validator.
Let's take a quick look at the default test file. Some helper functions and imports are excluded for brevity, but you can find the complete examples in your generated project:
```typescript theme={null}
describe("Hello World", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.HelloWorld as Program;
const provider = anchor.getProvider();
const arciumEnv = getArciumEnv();
it("Is initialized!", async () => {
const owner = readKpJson(`${os.homedir()}/.config/solana/id.json`);
console.log("Initializing add together computation definition");
const initATSig = await initAddTogetherCompDef(program, owner);
console.log(
"Add together computation definition initialized with signature",
initATSig
);
const privateKey = x25519.utils.randomSecretKey();
const publicKey = x25519.getPublicKey(privateKey);
const mxePublicKey = await getMXEPublicKeyWithRetry(
provider as anchor.AnchorProvider,
program.programId
);
console.log("MXE x25519 pubkey is", mxePublicKey);
const sharedSecret = x25519.getSharedSecret(privateKey, mxePublicKey);
const cipher = new RescueCipher(sharedSecret);
const val1 = BigInt(1);
const val2 = BigInt(2);
const plaintext = [val1, val2];
const nonce = randomBytes(16);
const ciphertext = cipher.encrypt(plaintext, nonce);
const sumEventPromise = awaitEvent("sumEvent");
const computationOffset = new anchor.BN(randomBytes(8), "hex");
const queueSig = await program.methods
.addTogether(
computationOffset,
Array.from(ciphertext[0]),
Array.from(ciphertext[1]),
Array.from(publicKey),
new anchor.BN(deserializeLE(nonce).toString())
)
.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("add_together")).readUInt32LE()
),
})
.rpc({ commitment: "confirmed" });
console.log("Queue sig is ", queueSig);
const finalizeSig = await awaitComputationFinalization(
provider as anchor.AnchorProvider,
computationOffset,
program.programId,
"confirmed"
);
console.log("Finalize sig is ", finalizeSig);
const sumEvent = await sumEventPromise;
const decrypted = cipher.decrypt([sumEvent.sum], new Uint8Array(sumEvent.nonce))[0];
expect(decrypted).to.equal(val1 + val2);
});
});
```
This test demonstrates the complete flow of encrypted computations in Arcium. Here's what each key step does:
* `initAddTogetherCompDef`: Call the `init_add_together_comp_def` instruction to initialize the encrypted instruction definition. (only need to be called once after the program is deployed)
* `getMXEPublicKeyWithRetry`: Fetch the MXE's x25519 public key.
* `x25519.utils.randomSecretKey`: Generate a random private key for the x25519 key exchange.
* `x25519.getPublicKey`: Generate the public key corresponding to the private key we generated above.
* `x25519.getSharedSecret`: Generate the shared secret with the MXE cluster using a x25519 key exchange.
* `cipher = new RescueCipher(sharedSecret)`: Initialize the Rescue cipher. The constructor derives a key from the Rescue-Prime hash function; see [Encryption overview](/developers/encryption).
* `cipher.encrypt`: Encrypt the inputs for the encrypted instruction.
* `awaitEvent`: Wait for the `sumEvent` event to be emitted by the program on finalization of the computation (in the callback instruction).
* `addTogether`: Call the `add_together` instruction to invoke the encrypted instruction.
* `awaitComputationFinalization`: Since waiting for an Arcium computation is not the same as waiting for one Solana transaction (because the MPC cluster must finish the computation and invoke the callback), this function is used, which is provided by the Arcium TypeScript library.
## Ready to Deploy?
Now that you have built and tested your MXE locally, you are probably eager to see it running live! Head over to our [deployment guide](/developers/deployment) where we'll walk you through getting your MXE deployed on Solana. We'll cover everything from choosing the right RPC endpoint to initializing your computation definitions.
## What's next?
Continue with complete applications grouped by complexity.
Understand MXEs, Clusters, and encrypted instructions.
Understand MPC constraints and build more complex circuits.
# Installation overview
Source: https://docs.arcium.com/developers/installation
Install the Arcium toolchain with arcup, including Rust, Solana CLI, and Anchor prerequisites
## Quick install (recommended)
On Mac and Linux, run this single command to install Arcium:
```bash theme={null}
curl --proto '=https' --tlsv1.2 -sSfL https://install.arcium.com/ | bash
```
`arcup` manages Arcium tooling versions, including the CLI and Arx node software. See the [Arcup version manager guide](/developers/installation/arcup) for version-management details.
This script will:
* Check for all required dependencies
* Install Linux build dependencies automatically (if needed)
* Download and install `arcup` for your platform
* Install the latest Arcium CLI, which interacts with the Arcium Network and manages computations
* Install the Arx node software, which performs encrypted computations in the network
### Prerequisites
Before running the installation script, make sure you have these dependencies installed:
* **Rust**: Install from the [Rust installation guide](https://www.rust-lang.org/tools/install)
* **Solana CLI 3.1.10**: Install from the [Solana CLI installation guide](https://docs.solana.com/cli/install-solana-cli-tools), then run `solana-keygen new`
* **Yarn**: Install from the [Yarn installation guide](https://yarnpkg.com/getting-started/install)
* **Anchor 1.0.2**: Install from the [Anchor installation guide](https://www.anchor-lang.com/docs/installation)
* **Docker & Docker Compose**: Install [Docker](https://docs.docker.com/engine/install/) and [Docker Compose](https://docs.docker.com/compose/install/)
The installation script will check for all these dependencies and provide clear instructions if any are missing.
## Manual installation
If you prefer to install manually, use the target-specific binaries below. `arcup` still manages Arcium tooling versions after installation; see the [Arcup version manager guide](/developers/installation/arcup).
Install `arcup`. We currently publish pre-built targets for x86 Linux and Apple Silicon. We do not support Windows at the moment.
* `x86_64_linux`
* `aarch64_macos`
You can install it by replacing `` with the target you want to install, and running the following command:
```bash x86 Linux theme={null}
TARGET=x86_64_linux && curl "https://bin.arcium.com/download/arcup_${TARGET}_0.13.2" -o ~/.cargo/bin/arcup && chmod +x ~/.cargo/bin/arcup
```
```bash Apple Silicon theme={null}
TARGET=aarch64_macos && curl "https://bin.arcium.com/download/arcup_${TARGET}_0.13.2" -o ~/.cargo/bin/arcup && chmod +x ~/.cargo/bin/arcup
```
Install the latest version of the CLI using `arcup`:
```bash theme={null}
arcup install
```
Verify the installation:
```bash theme={null}
arcium --version
```
## Shell completions
Generate tab-completion scripts for your shell:
```bash Bash theme={null}
arcium completions bash >> ~/.bashrc
```
```bash Zsh theme={null}
arcium completions zsh >> ~/.zshrc
```
```bash Fish theme={null}
arcium completions fish > ~/.config/fish/completions/arcium.fish
```
Restart your shell or source the config file to activate completions.
## Issues
Installation might fail due to a variety of reasons. This section contains a list of the most common issues and their solutions, taken from Anchor's installation guide.
### Platform-specific issues
**Windows users:** Arcium is not currently supported on Windows. We recommend using Windows Subsystem for Linux (WSL2) with Ubuntu for the best experience.
**Linux systems:** You may need additional dependencies. On Ubuntu/Debian:
```bash theme={null}
sudo apt-get update && sudo apt-get upgrade && sudo apt-get install -y pkg-config build-essential libudev-dev libssl-dev
```
### Incorrect `$PATH`
Rust binaries, including `arcup` and `arcium`, are installed to the `~/.cargo/bin` directory. Since this directory is required to be in the `PATH` environment variable, Rust installation tries to set it up automatically, but it might fail to do so on some platforms.
To verify that the `PATH` environment variable was set up correctly, run:
```shell theme={null}
which arcium
```
The output should look like (with your username):
```text theme={null}
/home/user/.cargo/bin/arcium
```
**Shell-specific PATH issues:**
If `which arcium` returns nothing, add the cargo bin directory to your PATH:
* **Bash/Zsh:** Add to `~/.bashrc` or `~/.zshrc`:
```bash theme={null}
export PATH="$HOME/.cargo/bin:$PATH"
```
* **Fish:** Add to `~/.config/fish/config.fish`:
```bash theme={null}
set -gx PATH $HOME/.cargo/bin $PATH
```
After editing, restart your terminal or run `source ~/.bashrc` (or equivalent for your shell).
## What's next?
Build your first encrypted instruction end-to-end.
Understand the Arcium architecture before building.
# Arcup version manager
Source: https://docs.arcium.com/developers/installation/arcup
Install and manage Arcium toolchain versions with arcup
The `arcup` version manager installs and manages the Arcium tooling suite, including the Arcium CLI binary and the Arx node Docker image. Use it to install the current release, switch between installed versions, and update components when new releases are available.
The [Quick start](#quick-start) section below covers basic `arcup` onboarding. For the full toolchain setup, see the [installation overview](/developers/installation). For component compatibility rules, see [Inter-component versioning](#inter-component-versioning).
## Quick start
First, check whether you already have a manually installed CLI or Arx node image:
```bash theme={null}
command -v arcium || true
docker image ls "arcium/*"
```
If you no longer need those old installs, remove the specific binary or Docker image before using `arcup`. Then verify the old CLI is gone or ready to be replaced:
```bash theme={null}
arcium --version
docker image ls "arcium/*"
```
Next, install `arcup` on your machine by following the [manual installation steps](/developers/installation#manual-installation). Then run the `arcup install` command:
```bash theme={null}
arcup install # Installs the latest releases of the Arcium components
```
Now verify that everything is installed correctly:
```bash theme={null}
arcium --version # Shows the latest CLI version
arcup version # Shows the currently installed Arcium component versions
docker images # Lists the image for the Arx node
```
You can also install older versions with `arcup install `, remove installed versions with `arcup uninstall`, and switch between installed versions with `arcup use`. See [Available commands](#available-commands) for details.
`arcium` and `arcup` may print a one-line update banner. Set `ARCIUM_NO_UPDATE_CHECK=1` to suppress it in scripts; CI and non-TTY runs are silent automatically.
## Inter-component versioning
Arcium publishes the CLI and Arx node image as a coordinated release. `arcup install ` installs the matching CLI and image for that release, and `arcup update` updates both to the latest coordinated release.
Use the same Arcium release across the CLI, Rust crates, TypeScript packages, and Arx node image. Mixed versions might work, but they are not tested or guaranteed. See [Versioning and compatibility](/developers/versioning) for compatibility across releases and the supported upgrade paths.
## Available commands
```bash theme={null}
install Install the latest (or a specific) version of Arcium components (Arx node and CLI)
update Update all Arcium components (Arx node and CLI) to the latest version
list List all installed versions
version Show currently active version
use Switch to using a specific installed version
uninstall Remove a specific version (alias: rm)
self Manage arcup itself (update, uninstall)
help Print this message or the help of the given subcommand(s)
```
## Self management
The `self` subcommand allows you to manage arcup itself:
```bash theme={null}
# Update arcup to the latest version
arcup self update
# Uninstall arcup and all Arcium components
arcup self uninstall
# Uninstall but keep Docker images
arcup self uninstall --keep-docker
# Skip confirmation prompt
arcup self uninstall --force
# Preview what would be removed (dry run)
arcup self uninstall --dry-run
```
# JavaScript client overview
Source: https://docs.arcium.com/developers/js-client-library
TypeScript SDKs for encrypted computations, network reads, and staking transactions
## Overview
Arcium provides three TypeScript SDKs for interacting with Arcium, deployed MXEs (MPC eXecution Environments), and the staking program.
Client library `@arcium-hq/client`:
* Handles key exchange, encryption, and decryption
* Derives Arcium PDAs and packs circuit data
* Waits for computation finalization
Reader library `@arcium-hq/reader`:
* Reads MXE data
* Views computations for a given MXE
Staking SDK `@arcium-hq/staking`:
* Derives staking program-derived addresses (PDAs)
* Reads typed staking accounts and delegated positions
* Builds staking instructions for a wallet to sign and send
Use the client library to encrypt and pack data, derive PDAs, and wait for finalization. Use the reader library to inspect MXE and network state, and the staking SDK to interact with the staking program. For the full computation flow, see the [computation lifecycle](/developers/computation-lifecycle).
## Installation
Client library:
```bash npm theme={null}
npm install @arcium-hq/client
```
```bash yarn theme={null}
yarn add @arcium-hq/client
```
```bash pnpm theme={null}
pnpm add @arcium-hq/client
```
Reader library:
```bash npm theme={null}
npm install @arcium-hq/reader
```
```bash yarn theme={null}
yarn add @arcium-hq/reader
```
```bash pnpm theme={null}
pnpm add @arcium-hq/reader
```
Staking SDK:
```bash npm theme={null}
npm install @arcium-hq/staking @anchor-lang/core @solana/web3.js
```
```bash yarn theme={null}
yarn add @arcium-hq/staking @anchor-lang/core @solana/web3.js
```
```bash pnpm theme={null}
pnpm add @arcium-hq/staking @anchor-lang/core @solana/web3.js
```
## Read staking accounts
With an Anchor `AnchorProvider`, create a typed staking program client, then read a wallet's primary stake account and delegated positions:
```typescript theme={null}
import {
getPrimaryStakeAccInfo,
getStakingProgram,
getUserStakePositions,
} from "@arcium-hq/staking";
const stakingProgram = getStakingProgram(provider);
const owner = provider.wallet.publicKey;
const primaryStake = await getPrimaryStakeAccInfo(stakingProgram, owner);
const delegatedPositions = await getUserStakePositions(stakingProgram, owner);
```
Single-account readers return `null` when the account does not exist. Instruction builders return a `TransactionInstruction` for your wallet to sign and send.
## API reference
For complete documentation for all three TypeScript SDKs, see the [API reference](https://ts.arcium.com/api).
## Reclaiming computation rent
Every queued computation allocates a Solana account that holds rent. After the computation lifecycle completes, you should reclaim that rent.
### After successful finalization
Once a computation reaches `Finalized` status (after the callback executes), use `claimComputationRent` to close the account and reclaim the rent:
```typescript theme={null}
import { claimComputationRent } from '@arcium-hq/client';
const sig = await claimComputationRent(
provider, // AnchorProvider
clusterOffset, // cluster the computation was queued on
computationOffset, // BN - the offset used when queueing
);
```
The signer must be the original payer who queued the computation. Using a different signer will fail with `InvalidAuthority`.
Computations that remain in `Queued` status expire after 180 slots. Expired computations can be reclaimed using the `reclaimExpiredComputationFee` instruction, which also returns any fees from the fee pool. See the [API reference](https://ts.arcium.com/api) for details.
`claimComputationRent` only closes per-computation accounts. To close long-lived MXE or computation-definition accounts, see [Account lifecycle and closing](/developers/program/account-lifecycle).
## Reading computation fees
The reader library exposes two getters for inspecting a computation's fee, depending on whether its account is still open:
```typescript theme={null}
import { getComputationFee, getComputationFeeFromQueueTx } from '@arcium-hq/reader';
// Live computation: reads the onchain fee from the open account.
// Returns null once the account is closed (rent reclaimed).
const fee = await getComputationFee(
arciumProgram,
mxeProgramId,
computationOffset,
);
// Closed computation: reconstructs the queue-time fee inputs by decoding
// the original queue_computation instruction from its transaction signature.
const queued = await getComputationFeeFromQueueTx(
arciumProgram,
queueTxSignature,
mxeProgramId,
computationOffset,
);
```
`getComputationFee` returns `{ source: 'account', ... }` with the live onchain fee, or `null` when the account has been closed. `getComputationFeeFromQueueTx` returns `{ source: 'queueTx', ... }` with the raw queue-time inputs (not lamport amounts) and requires an archive RPC that retains the original transaction and its inner instructions. See the [API reference](https://ts.arcium.com/api) for the full return shapes.
## Using the client
Prefer a more step-by-step approach? Get started with learning [how to encrypt inputs for encrypted transactions](/developers/js-client-library/encryption).
## What's next?
Step-by-step guide to encrypting data for encrypted transactions.
Await and process computation results.
Complete TypeScript SDK documentation.
# Tracking callbacks
Source: https://docs.arcium.com/developers/js-client-library/callback
Track computation progress and handle callback results
Unlike regular transactions, encrypted computations involve additional steps after your Solana transaction completes:
1. **Your transaction completes** - Encrypted data is submitted and queued in the cluster's mempool
2. **Computation waits in queue** - MPC nodes promote computations from the mempool by leader choice, priority fee, and lock-conflict checks
3. **MPC execution** - When your computation's turn comes, MPC nodes execute it offchain
4. **Callback invocation** - Results are returned via your callback instruction
This means you can't await a transaction completion like normal Solana programs. Instead, you need to wait for the entire computation lifecycle to finish. The Arcium client library provides utilities to handle this:
## Await computation completion with `awaitComputationFinalization`
```typescript theme={null}
// Generate a random 8-byte computation offset
const computationOffset = new anchor.BN(randomBytes(8), "hex");
// `program` is the anchor program client of the MXE we're invoking
// the instruction `ourIx` on (which then invokes a computation under the hood by CPIing into the Arcium program).
// `queueSig` is the signature of said transaction.
const queueSig = await program.methods
.ourIx(
// Computation offset that you provide when invoking the instruction
computationOffset
/* other inputs */
)
.accounts(/* some accounts */)
.rpc();
// Since this is an Arcium computation, we need to wait for it to be finalized
// a little bit differently
const finalizeSig = await awaitComputationFinalization(
// Anchor provider
provider as anchor.AnchorProvider,
// Computation offset that you provide when invoking the instruction
computationOffset,
// Program ID of the MXE
program.programId,
// Solana commitment level, "confirmed" by default
"confirmed"
);
console.log("Computation was finalized with sig: ", finalizeSig);
```
The optional fifth argument, `timeoutMs`, sets the maximum wait time in milliseconds. It defaults to `120000`.
Failed finalization reports a `CircuitFailureReason` such as `OffChainCircuitFetchFailed`, `OffChainCircuitHashMismatch`, `CircuitCUMismatch`, `LocalCircuitFetchFailed`, or `CircuitSerialization`.
## Read and assert the callback result
Finalization tells you the callback has run, but not what it produced. Consume the result the way you would in any Solana program: await an emitted event, or read updated account state. Events are the common path because Arcium callbacks typically relay encrypted payloads for the client to decrypt offchain.
Event-based decryption requires the callback to emit both the ciphertext and the nonce. See the callback-side `emit!` pattern in the [Arcium program guide](/developers/program).
On the client side, register the event listener **before** queuing the computation so it's attached by the time the callback fires. Helpers and imports are omitted for brevity: `awaitEvent` is a test-scaffold wrapper around `program.addEventListener`, `cipher` comes from the encryption setup in [Encrypting inputs](/developers/js-client-library/encryption), and `expect` / `expectedSum` are shown in a test context.
```typescript theme={null}
// Register the listener first to avoid a race where the callback fires
// before you've subscribed.
const sumEventPromise = awaitEvent("sumEvent");
const computationOffset = new anchor.BN(randomBytes(8), "hex");
const queueSig = await program.methods
.ourIx(computationOffset /* other inputs */)
.accounts(/* ... */)
.rpc({ commitment: "confirmed" });
await awaitComputationFinalization(
provider as anchor.AnchorProvider,
computationOffset,
program.programId,
"confirmed"
);
// Callback has fired; resolve the event, decrypt offchain, and assert.
const sumEvent = await sumEventPromise;
const decrypted = cipher.decrypt([sumEvent.sum], new Uint8Array(sumEvent.nonce))[0];
expect(decrypted).to.equal(expectedSum);
```
The two valid orderings are:
* **Event path**: `subscribe → queue → finalize → await event → decrypt → assert`
* **Account path**: `queue → finalize → read account → assert`
## What's next?
Learn how to encrypt data before sending to the MXE.
Deploy your MXE program to devnet or mainnet.
# Encrypting inputs
Source: https://docs.arcium.com/developers/js-client-library/encryption
Encrypt inputs with X25519 key exchange and Rescue, then decrypt encrypted computation outputs.
Let's say we have the following encrypted instruction that adds 2 encrypted `u8`s and returns the result encrypted:
```rust theme={null}
use arcis::*;
#[encrypted]
mod circuits {
use arcis::*;
pub struct InputValues {
v1: u8,
v2: u8,
}
#[instruction]
pub fn add_together(input_ctxt: Enc) -> Enc {
let input = input_ctxt.to_arcis();
let sum = input.v1 as u16 + input.v2 as u16;
input_ctxt.owner.from_arcis(sum)
}
}
```
We want to input the values `x = 42` and `y = 101` into this instruction. To do this, we first have to build the parameters for the encrypted instruction correctly:
```typescript theme={null}
import { RescueCipher, getArciumEnv, x25519 } from "@arcium-hq/client";
import { randomBytes } from "crypto";
// Our encrypted instruction takes two encrypted `u8` values as input, so we need to provide two ciphertext values which are represented as `[u8; 32]` in our Solana program.
const val1 = BigInt(42);
const val2 = BigInt(101);
const plaintext = [val1, val2];
```
Now that we have the inputs, we need to encrypt them. This is done using the `RescueCipher` class with some info about the MPC cluster we want to use:
```typescript theme={null}
// Fetch the MXE x25519 public key
// getMXEPublicKeyWithRetry is a helper that wraps getMXEPublicKey with retries
// See the Hello World tutorial for the full implementation
const mxePublicKey = await getMXEPublicKeyWithRetry(
provider as anchor.AnchorProvider,
program.programId
);
// Generate a random private key for x25519 elliptic curve Diffie-Hellman key exchange.
const privateKey = x25519.utils.randomSecretKey();
// Derive the public key from the private key.
const publicKey = x25519.getPublicKey(privateKey);
// Generate a random nonce for the encryption.
const nonce = randomBytes(16);
// Get the shared secret with the cluster.
const sharedSecret = x25519.getSharedSecret(privateKey, mxePublicKey);
// Initialize the cipher with the shared secret.
const cipher = new RescueCipher(sharedSecret);
// Encrypt the plaintext, and serialize it to a `[u8; 32]` array.
const ciphertext = cipher.encrypt(plaintext, nonce);
```
To decrypt the data, again it follows a similar pattern:
```typescript theme={null}
// Initialize the cipher with the shared secret.
const cipher = new RescueCipher(sharedSecret);
const plaintext = cipher.decrypt(ciphertext, nonce);
```
**Working with packed data?** If your Arcis structs use `Pack`, you'll use generated packers on the client. See [Efficient data packing](/developers/arcis/input-output#efficient-data-packing).
## What's next?
Learn how to await and process computation results.
Complete TypeScript SDK documentation.
# Current limitations
Source: https://docs.arcium.com/developers/limitations
Output size limits, Arcis language constraints, and common error codes when building encrypted applications on Arcium
## Keep callback transactions within the size limit
Solana limits a serialized transaction to 1,232 bytes. Callback output shares this limit with signatures, account keys, instructions, and other callback data, so the usable output size depends on the callback.
If Arx cannot build the callback as a single transaction, the computation fails with `OutputTooLarge`. Keep outputs compact and test the largest expected output with the callback accounts used by your application. Split larger results across separate computations when necessary.
## Arcis language constraints
Arcis compiles Rust-like code into fixed MPC circuits. The circuit shape must be known at compile time, so several standard Rust patterns need different implementations:
| Pattern | Status | Use instead |
| --------------------------------------------------------------- | ------------- | ------------------------------------------------ |
| `Vec`, `String`, `HashMap` | Not supported | Fixed-size arrays, byte arrays, or structs |
| `while`, `loop` | Not supported | `for` loops with fixed bounds |
| `break`, `continue`, early `return` | Not supported | A single exit path with conditional assignments |
| `let ... else` | Not supported | `if let` or `match` |
| `.reveal()` or `.from_arcis()` inside non-constant conditionals | Not supported | Move the reveal or conversion outside the branch |
Arcis supports `if`, `else`, `else if`, `if let`, let chains, `match`, `matches!`, fixed-size arrays, structs, tuples, enums, `Option`, and fixed-bound loops. Enums and `Option` cannot be the input or output of an encrypted instruction, and enum discriminants cannot be set explicitly. See the [Operations guide](/developers/arcis/operations) for the complete support matrix.
## Common errors
| Error or symptom | Likely cause | Fix |
| ------------------------------------ | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| `OutputTooLarge` | The complete callback transaction exceeds Solana's transaction size limit | Return less data or split the result across separate computations |
| Computation never finalizes | Computation definition was not initialized, callback is missing, or cluster configuration is wrong | Initialize the computation definition, check callback registration, and verify the cluster offset |
| Decryption fails | Nonce mismatch, nonce reuse, wrong MXE key, or incorrect ciphertext order | Use a fresh 16-byte nonce per encryption and match the circuit argument order exactly |
| Shared input fails silently | Missing `x25519_pubkey` before the nonce and ciphertext | For `Enc`, pass public key, nonce, then ciphertexts |
| Callback runs but state is unchanged | Callback account was not writable or was omitted from callback accounts | Mark the account writable in both the callback account list and the Anchor account struct |
| Circuit compile failure | Unsupported Rust construct or variable-size data | Replace dynamic constructs with fixed-size Arcis patterns |
## What's next?
Performance tips and common patterns
Complete operation support matrix
Supported and unsupported types
Deploy your MXE to the network
# Node setup
Source: https://docs.arcium.com/developers/node-setup
Run an Arx node: hardware requirements and network registration
## Overview
As a Node Operator, you'll set up your own Arx node to participate in the Arcium Network. This guide walks you through each step of the process.
First, you'll prepare your environment by installing the necessary tools and generating security keys. Then, you'll get your node registered onchain and configure it to run. Finally, you'll connect to other nodes in a cluster and start doing computations.
By the end, you will:
* Install the Arcium tooling
* Generate required keypairs
* Fund accounts with SOL
* Initialize onchain node accounts
* Configure your node
* Join or create a cluster
* Deploy your node with Docker
## Prerequisites
Before starting, ensure you have the following installed:
* **Rust**: Install from [rustup.rs](https://rustup.rs/)
* **Solana CLI 3.1.10**: Install from [Solana's documentation](https://docs.solana.com/cli/install-solana-cli-tools)
* **Docker & Docker Compose**: Install from [Docker's documentation](https://docs.docker.com/get-docker/)
* **OpenSSL**: Install from [OpenSSL's documentation](https://www.openssl.org/source/) (usually pre-installed on macOS/Linux)
* **Git**: For cloning repositories and version control
You'll also need:
* A reliable internet connection
* Basic familiarity with command-line tools
**Recommended system requirements:**
| Resource | Recommendation |
| --------- | ------------------------------------ |
| RAM | 32GB+ |
| CPU | 12+ cores, 2.8GHz+ base |
| Bandwidth | 1 Gbit/s minimum |
| Disk | Minimal (node is not disk-intensive) |
| GPU | Not required |
**Network requirements - open these ports:**
| Port | Protocol | Purpose |
| ---- | --------- | -------------------------- |
| 8001 | TCP & UDP | MPC protocol communication |
| 8002 | TCP & UDP | BLS signature aggregation |
| 8012 | TCP & UDP | TD preprocessing |
| 8013 | TCP & UDP | TD registration |
| 9091 | TCP | Prometheus metrics |
**Windows users:** Arcium doesn't run natively on Windows yet. Use Windows Subsystem for Linux (WSL2) with Ubuntu to follow this guide.
## Step 1: Set up your workspace
Create a dedicated folder for your node setup to keep everything organized:
```bash theme={null}
mkdir arcium-node-setup
cd arcium-node-setup
```
Stay in this directory for all remaining steps. All file paths and Docker commands assume you're working from `arcium-node-setup/`.
You'll also need to know your public IP address for the next steps. Here's a quick way to find it:
```bash theme={null}
curl https://ipecho.net/plain ; echo
```
## Step 2: Install Arcium tooling
The Arcium tooling suite includes the CLI and Arx node software. Install it using the automated installer:
```bash theme={null}
curl --proto '=https' --tlsv1.2 -sSfL https://install.arcium.com/ | bash
```
This script will:
* Check for all required dependencies
* Install `arcup` (Arcium's version manager)
* Install the latest Arcium CLI
* Install the Arx node software
Verify the installation:
```bash theme={null}
arcium --version && arcup --version
```
If you prefer manual installation, see the [installation guide](/developers/installation) for detailed instructions.
## Step 3: Generate required keypairs
Your Arx node needs five different keypairs for secure operation. Create these in your `arcium-node-setup` directory:
### 3.1 Node authority keypair
This Solana keypair identifies your node and handles onchain operations:
```bash theme={null}
solana-keygen new --outfile node-keypair.json --no-bip39-passphrase
```
The `--no-bip39-passphrase` flag creates a keypair without a passphrase for easier automation.
### 3.2 Callback authority keypair
This Solana keypair signs callback computations and must be different from your node keypair for security separation:
```bash theme={null}
solana-keygen new --outfile callback-kp.json --no-bip39-passphrase
```
### 3.3 Identity keypair
This keypair handles node-to-node communication and must be in PKCS#8 format:
```bash theme={null}
openssl genpkey -algorithm Ed25519 -out identity.pem
```
### 3.4 BLS keypair
This keypair is used for BLS (Boneh-Lynn-Shacham) threshold signatures on MPC computation callbacks. Generate it using the Arcium CLI:
```bash theme={null}
arcium gen-bls-key bls-keypair.json
```
This creates a 32-byte private key stored as a JSON array format.
### 3.5 X25519 keypair
This keypair is used for encrypted communication between nodes:
```bash theme={null}
arcium generate-x25519 -o x25519-keypair.json
```
This creates a 32-byte X25519 private key stored in JSON array format.
Keep these keypairs safe and private. Back them up to a secure location outside your VPS - you'll need them to restore your node if something goes wrong. Never share them with anyone.
## Step 4: Fund your accounts
Your node and callback accounts need SOL for transaction fees. Transfer SOL to both accounts and verify balances:
```bash theme={null}
# Check your account addresses
solana address --keypair node-keypair.json
solana address --keypair callback-kp.json
# Verify balances
solana balance "$(solana address --keypair node-keypair.json)"
solana balance "$(solana address --keypair callback-kp.json)"
```
On devnet, you can use `solana airdrop 2 -u devnet` or the [web faucet](https://faucet.solana.com/) to get free SOL for testing.
On mainnet, SOL has real economic value. Ensure you have sufficient funds before proceeding.
## Step 5: Initialize node accounts
Now we'll register your node with the Arcium Network by creating its onchain accounts. This step tells the blockchain about your node and its capabilities.
Always pass `--rpc-url ` to `arcium` commands. `solana config set --url ...` affects Solana CLI commands only; `arcium` defaults to mainnet when `--rpc-url` is omitted.
`init-arx-accs` creates and activates the node's primary stake account unless you skip those steps. The default lockup is 100 epochs. Run the command with `--dry-run` first to inspect the planned transactions.
For guidance on choosing a reliable endpoint, see the [RPC Provider Recommendations](#rpc-provider-recommendations).
Use the `init-arx-accs` command to initialize all required onchain accounts for your node:
```bash theme={null}
arcium init-arx-accs \
--keypair-path node-keypair.json \
--callback-keypair-path callback-kp.json \
--peer-keypair-path identity.pem \
--bls-keypair-path bls-keypair.json \
--x25519-keypair-path x25519-keypair.json \
--amount \
--node-offset \
--ip-address \
--rpc-url
```
### Required parameters:
* `--keypair-path`: Path to your node authority keypair
* `--callback-keypair-path`: Path to your callback authority keypair
* `--peer-keypair-path`: Path to your identity keypair (PEM format)
* `--bls-keypair-path`: Path to your BLS keypair (JSON array format)
* `--x25519-keypair-path`: Path to your X25519 keypair (JSON array format)
* `--amount`: Amount to stake, in whole ARX tokens. For example, `1000` stakes 1,000 ARX.
* `--node-offset`: Your node's unique ID number on the network. Choose a unique, non-zero number. If you get an error during setup saying your number is already taken, pick a different one and try again.
* `--ip-address`: Your node's public IPv4 address
* `--rpc-url`: Solana RPC endpoint (mainnet or devnet)
If successful, you'll see confirmation that your node accounts have been initialized onchain.
## Step 6: Configure your node
The configuration file specifies which network to connect to, how to communicate with other nodes, and various operational settings.
Create a `node-config.toml` file in your `arcium-node-setup` directory:
```toml theme={null}
[node]
offset = # Your node offset from step 5
computations_limit = 10 # Max computations the node executes concurrently
[solana]
endpoint_rpc = "" # e.g., https://api.mainnet-beta.solana.com or https://api.devnet.solana.com
endpoint_wss = "" # Replace with your RPC provider WebSocket URL (e.g., wss://api.mainnet-beta.solana.com)
secondary_endpoint_rpc = "" # Failover RPC, ideally a different provider
secondary_endpoint_wss = "" # Failover WebSocket, ideally a different provider
```
All six fields in the example are required. The config fails to parse if any are missing. The node fails over to the secondary endpoints when the primary RPC lags or stops responding, so point them at a different provider than your primary endpoints.
**Upgrading from v0.7.0?** The `[network]` section, `cluster`, `commitment`, `hardware_claim`, `starting_epoch`, and `ending_epoch` fields have been removed. Update your `node-config.toml` to the simplified format above.
### Trusted dealer config
If you operate a trusted dealer, create a separate `trusted_dealer_config.toml` file. The trusted-dealer process reads this file; a `[dealer]` section in `node-config.toml` is ignored. The config expects every field under `[dealer]` and requires `master_seed_path`:
```toml trusted_dealer_config.toml theme={null}
[dealer]
private_key_path = "/usr/trusted-dealer/identity.json"
master_seed_path = "/usr/trusted-dealer/master_seed.json"
cluster_offsets = [456]
solana_rpc_url = ""
n_peers = 4
local_ip = "0.0.0.0"
rate_limit_initial_tokens = 10000000
rate_limit_tokens_per_second = 100000
rate_limit_max_tokens = 10000000
```
Replace `456` and `4` with your cluster offset and peer count.
`local_addr` was renamed to `local_ip`. Treat `master_seed_path` as the highest-sensitivity host secret; leaking it compromises keyshares.
## Step 7: Cluster operations
Clusters are groups of nodes that collaborate on MPC computations. For background on cluster concepts, see [Clusters overview](/clusters/overview).
Most operators should **join an existing cluster**. Only create your own cluster if you're coordinating a group of nodes.
To join an existing cluster, you must first be **proposed by the cluster authority**. Once proposed, accept the invitation:
```bash theme={null}
arcium join-cluster true \
--keypair-path node-keypair.json \
--node-offset \
--cluster-offset \
--rpc-url
```
**Parameters:**
* `true`: Accept the join request (use `false` to reject)
* `--node-offset`: Your node's unique identifier (chosen during node initialization)
* `--cluster-offset`: The cluster's unique identifier (different from node offset - clusters and nodes have separate ID spaces)
You cannot join a cluster unless the cluster owner has first proposed you using `propose-join-cluster`.
If you want to create a new cluster and invite other nodes:
**1. Create the Cluster**
```bash theme={null}
arcium init-cluster \
--keypair-path node-keypair.json \
--offset \
--max-nodes \
--rpc-url
```
**Parameters:**
* `--offset`: Unique identifier for your cluster (different from your node offset)
* `--max-nodes`: Maximum number of nodes in the cluster
**Optional parameters:**
* `--mempool-size`: Size of the mempool (`Tiny`, `Small`, `Medium`, `Large`). Defaults to `Tiny`
* `--price-per-cu`: Initial price per compute unit in micro-lamports. Defaults to 1
* `--propose-node`: Automatically propose a node to join after creation
**2. Propose Nodes to Join**
After creating the cluster, you must propose each node that should join. Nodes cannot join without being proposed first:
```bash theme={null}
arcium propose-join-cluster \
--keypair-path node-keypair.json \
--cluster-offset \
--node-offset \
--rpc-url
```
**Parameters:**
* `--cluster-offset`: Your cluster's offset
* `--node-offset`: The offset of the node you're inviting
You can also propose a node inline when creating the cluster using `--propose-node `
**3. Wait for Nodes to Accept**
Once proposed, each node must accept the invitation using `join-cluster true` (see Join tab).
### Submit aggregated BLS key (required after cluster is full)
The command will verify that all cluster slots are filled before submitting. If nodes are still pending, it will fail with an error.
Once all nodes have joined the cluster, **all nodes** must aggregate and submit the combined BLS public key. This enables threshold BLS signatures for computation callbacks:
```bash theme={null}
arcium submit-aggregated-bls-key \
--keypair-path node-keypair.json \
--cluster-offset \
--node-offset \
--rpc-url
```
**Parameters:**
* `--keypair-path`: Your node authority keypair (must be a node in the cluster)
* `--cluster-offset`: The cluster's offset
* `--node-offset`: Your node's offset within the cluster
All nodes need to run this command. The CLI will automatically fetch all node BLS public keys from the cluster and aggregate them.
### Activate the cluster (cluster authority only)
After all nodes have submitted the aggregated BLS key, the **cluster authority** must activate the cluster:
```bash theme={null}
arcium activate-cluster \
--keypair-path \
--cluster-offset \
--rpc-url
```
**Parameters:**
* `--keypair-path`: A keypair that has authority over the cluster
* `--cluster-offset`: The cluster's offset
Only the cluster authority can run this command. The cluster must have all BLS keys submitted before activation will succeed.
## Step 8: Deploy your node
Before running Docker, prepare your environment and verify you have all required files:
```bash theme={null}
mkdir -p arx-node-logs private-shares public-inputs
ls node-keypair.json callback-kp.json identity.pem bls-keypair.json x25519-keypair.json node-config.toml
```
Create a `docker-compose.yml` file in your `arcium-node-setup` directory:
```yaml theme={null}
services:
arx-node:
image: arcium/arx-node:v0.13.2
container_name: arx-node
restart: unless-stopped
environment:
NODE_IDENTITY_FILE: /usr/arx-node/node-keys/node_identity.pem
NODE_KEYPAIR_FILE: /usr/arx-node/node-keys/node_keypair.json
CALLBACK_AUTHORITY_KEYPAIR_FILE: /usr/arx-node/node-keys/callback_authority_keypair.json
BLS_PRIVATE_KEY_FILE: /usr/arx-node/node-keys/bls_keypair.json
X25519_PRIVATE_KEY_FILE: /usr/arx-node/node-keys/x25519_keypair.json
ARX_METRICS_HOST: 0.0.0.0
ARX_METRICS_PORT: 9091
ASYNC_MPC_STREAM_WINDOW_MB: 64
ASYNC_MPC_CONN_WINDOW_MB: 512
ASYNC_MPC_CC: bbr
volumes:
- ./node-config.toml:/usr/arx-node/arx/node_config.toml
- ./node-keypair.json:/usr/arx-node/node-keys/node_keypair.json:ro
- ./callback-kp.json:/usr/arx-node/node-keys/callback_authority_keypair.json:ro
- ./identity.pem:/usr/arx-node/node-keys/node_identity.pem:ro
- ./bls-keypair.json:/usr/arx-node/node-keys/bls_keypair.json:ro
- ./x25519-keypair.json:/usr/arx-node/node-keys/x25519_keypair.json:ro
- ./arx-node-logs:/usr/arx-node/logs
- ./private-shares:/usr/arx-node/private-shares
- ./public-inputs:/usr/arx-node/public-inputs
ports:
- "8001:8001/tcp"
- "8001:8001/udp"
- "8002:8002/tcp"
- "8002:8002/udp"
- "8012:8012/tcp"
- "8012:8012/udp"
- "8013:8013/tcp"
- "8013:8013/udp"
- "9091:9091/tcp"
```
**Key configuration:**
* `image: arcium/arx-node:v0.13.2`: pinned version for stability
* `restart: unless-stopped`: auto-restarts on crashes or server reboot
* Volume mounts with `:ro`: read-only access for keypair files
* `ASYNC_MPC_STREAM_WINDOW_MB`, `ASYNC_MPC_CONN_WINDOW_MB`, `ASYNC_MPC_CC`: recommended QUIC transport tuning for MPC throughput (per-stream and per-connection flow-control windows in MB, and the `bbr` congestion controller)
Start the node:
```bash theme={null}
docker compose up -d
docker compose ps
docker compose logs -f arx-node
```
Ensure ports 8001, 8002, 8012, and 8013 are open for TCP and UDP. Port 9091 is TCP-only for metrics. The metrics endpoint has no authentication - restrict access to trusted networks only.
If you prefer using `docker run` directly:
```bash theme={null}
docker run -d \
--name arx-node \
-e NODE_IDENTITY_FILE=/usr/arx-node/node-keys/node_identity.pem \
-e NODE_KEYPAIR_FILE=/usr/arx-node/node-keys/node_keypair.json \
-e CALLBACK_AUTHORITY_KEYPAIR_FILE=/usr/arx-node/node-keys/callback_authority_keypair.json \
-e BLS_PRIVATE_KEY_FILE=/usr/arx-node/node-keys/bls_keypair.json \
-e X25519_PRIVATE_KEY_FILE=/usr/arx-node/node-keys/x25519_keypair.json \
-e ARX_METRICS_HOST=0.0.0.0 \
-e ARX_METRICS_PORT=9091 \
-e ASYNC_MPC_STREAM_WINDOW_MB=64 \
-e ASYNC_MPC_CONN_WINDOW_MB=512 \
-e ASYNC_MPC_CC=bbr \
-v "$(pwd)/node-config.toml:/usr/arx-node/arx/node_config.toml" \
-v "$(pwd)/node-keypair.json:/usr/arx-node/node-keys/node_keypair.json:ro" \
-v "$(pwd)/callback-kp.json:/usr/arx-node/node-keys/callback_authority_keypair.json:ro" \
-v "$(pwd)/identity.pem:/usr/arx-node/node-keys/node_identity.pem:ro" \
-v "$(pwd)/bls-keypair.json:/usr/arx-node/node-keys/bls_keypair.json:ro" \
-v "$(pwd)/x25519-keypair.json:/usr/arx-node/node-keys/x25519_keypair.json:ro" \
-v "$(pwd)/arx-node-logs:/usr/arx-node/logs" \
-v "$(pwd)/private-shares:/usr/arx-node/private-shares" \
-v "$(pwd)/public-inputs:/usr/arx-node/public-inputs" \
-p 8001:8001/tcp -p 8001:8001/udp \
-p 8002:8002/tcp -p 8002:8002/udp \
-p 8012:8012/tcp -p 8012:8012/udp \
-p 8013:8013/tcp -p 8013:8013/udp \
-p 9091:9091/tcp \
arcium/arx-node:v0.13.2
```
## Step 9: Verify node operation
Check that your node is running correctly:
### Check node status
```bash theme={null}
arcium arx-info --rpc-url
```
### Check if node is active
```bash theme={null}
arcium arx-active --rpc-url
```
### Metrics and health
The node exposes Prometheus-compatible metrics on port 9091:
| Endpoint | Description |
| -------------- | --------------------------- |
| `GET /metrics` | Prometheus-format metrics |
| `GET /health` | Health check (returns "OK") |
Verify:
```bash theme={null}
curl -s http://localhost:9091/metrics | grep arx_
curl -s http://localhost:9091/health
```
Scrape with [Prometheus](https://prometheus.io/) and visualize with [Grafana](https://grafana.com/):
```yaml theme={null}
# prometheus.yml snippet
scrape_configs:
- job_name: 'arx-node'
static_configs:
- targets: ['localhost:9091']
```
### Monitor logs
```bash theme={null}
docker compose logs -f arx-node
```
### Extract internal logs
If you need detailed logs for debugging:
```bash theme={null}
docker cp arx-node:/usr/arx-node/logs ./node-logs
ls ./node-logs/ # Shows arx_log__.log
```
## RPC provider recommendations
For better reliability, consider using dedicated RPC providers instead of the default public endpoints.
**Recommended providers:**
* [Helius](https://helius.dev)
* [Triton](https://triton.one)
* [QuickNode](https://quicknode.com)
## Troubleshooting
All 5 environment variables are required. Verify your `docker-compose.yml` includes:
* `NODE_KEYPAIR_FILE`
* `CALLBACK_AUTHORITY_KEYPAIR_FILE`
* `NODE_IDENTITY_FILE`
* `BLS_PRIVATE_KEY_FILE`
* `X25519_PRIVATE_KEY_FILE`
Regenerate the X25519 keypair:
```bash theme={null}
arcium generate-x25519 -o x25519-keypair.json
```
* Verify all 5 keypair files exist and are readable
* Check that `node-config.toml` is valid TOML with all required fields
* Ensure your IP address is accessible from the internet
1. Verify firewall allows inbound TCP and UDP on ports 8001, 8002, 8012, 8013, plus TCP on 9091 if exposing metrics
2. Check RPC endpoint is responsive
3. Verify node is active: `arcium arx-active --rpc-url `
* Verify you have sufficient SOL for transaction fees
* Check that your RPC endpoint is working
* Ensure node offset is unique (try a different number)
* Verify you've been invited by the cluster authority
* Check that cluster has available slots
* Ensure your node is properly initialized
All cluster nodes need valid BLS keys. After all nodes join:
```bash theme={null}
arcium submit-aggregated-bls-key \
--keypair-path node-keypair.json \
--cluster-offset \
--node-offset \
--rpc-url
```
Check the balance and top up your callback account:
```bash theme={null}
solana balance "$(solana address -k callback-kp.json)"
# On devnet:
solana airdrop 2 "$(solana address -k callback-kp.json)" -u devnet
# On mainnet: transfer SOL from your funded wallet
```
* Verify Docker is running
* Check file permissions on mounted volumes
* Ensure ports 8001, 8002, 8012, 8013 (TCP & UDP) and 9091 (TCP) are not already in use
**Need more help?** Join the [Arcium Discord](https://discord.gg/arcium) for community support, or review the [installation troubleshooting guide](/developers/installation#issues).
## What's next?
Once your node is running successfully:
* **Join the community**: Connect with other Node Operators on [Discord](https://discord.gg/arcium)
* **Stay updated**: Keep your Arcium tooling updated with `arcup self update && arcup update`
* **Learn more**: Read the [Arcium Network overview](/getting-started/architecture-overview) or [installation guide](/developers/installation)
# Solana program overview
Source: https://docs.arcium.com/developers/program
Invoke encrypted computations from your Solana program using CPI
Before reading this, we recommend having read the [Computation lifecycle](/developers/computation-lifecycle) section, the [Arcis inputs/outputs](/developers/arcis/input-output) section, and the [Callback type generation](/developers/program/callback-type-generation) guide to understand how output types like `AddTogetherOutput` are automatically generated from encrypted instructions.
## The basics
Let's say we have the following encrypted instruction and want to invoke it from our MXE.
```rust theme={null}
#[encrypted]
mod circuits {
use arcis::*;
pub struct InputValues {
v1: u8,
v2: u8,
}
#[instruction]
pub fn add_together(input_ctxt: Enc) -> Enc {
let input = input_ctxt.to_arcis();
let sum = input.v1 as u16 + input.v2 as u16;
input_ctxt.owner.from_arcis(sum)
}
}
```
To do this, we first need to receive the encrypted parameter of type `InputValues` which contains two encrypted `u8`s, then build the computation arguments using `ArgBuilder`, and finally queue the computation for execution. Additionally, we need to define a callback instruction that will be invoked when the computation is complete. Callback instructions have a few requirements:
1. They must be defined with the `#[arcium_callback(encrypted_ix = "encrypted_ix_name")]` macro.
2. They must have exactly two arguments: `ctx: Context<...>` and `output: SignedComputationOutputs` where `T` is named as `{encrypted_ix_name}Output`.
For passing encrypted arguments, we use the `ArgBuilder` API. If the corresponding argument is `Enc`, then we need to pass the `x25519_pubkey(pub_key)` and `plaintext_u128(nonce)`, before the ciphertext. If the corresponding argument is `Enc`, then we only need to pass the nonce as `plaintext_u128(nonce)` and the ciphertext. Ciphertexts are passed using methods like `encrypted_u8`, `encrypted_u16`, `encrypted_u32`, `encrypted_u64`, `encrypted_u128`, or `encrypted_bool`.
```rust theme={null}
pub fn add_together(
ctx: Context,
computation_offset: u64,
ciphertext_0: [u8; 32],
ciphertext_1: [u8; 32],
pub_key: [u8; 32],
nonce: u128,
) -> Result<()> {
// Build the args the encrypted instruction expects using ArgBuilder
let args = ArgBuilder::new()
.x25519_pubkey(pub_key)
.plaintext_u128(nonce)
.encrypted_u8(ciphertext_0)
.encrypted_u8(ciphertext_1)
.build();
// 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 encrypted instruction expects
args,
// Use callback_ix() helper to generate the callback instruction
vec![AddTogetherCallback::callback_ix(
computation_offset,
&ctx.accounts.mxe_account,
&[] // Empty array = no custom accounts
)?],
1, // Number of transactions needed for callback
0, // cu_price_micro: priority fee in microlamports (0 = no priority fee)
0, // callback_cu_limit: compute unit limit for the callback (0 = default)
)?;
Ok(())
}
// Macro provided by the Arcium SDK to define a callback instruction.
#[arcium_callback(encrypted_ix = "add_together")]
pub fn add_together_callback(
ctx: Context,
output: SignedComputationOutputs,
) -> Result<()> {
let o = match output.verify_output(
&ctx.accounts.cluster_account,
&ctx.accounts.computation_account
) {
Ok(AddTogetherOutput { field_0 }) => field_0,
Err(e) => {
msg!("Error: {}", e);
return Err(ErrorCode::AbortedComputation.into())
},
};
emit!(SumEvent {
sum: o.ciphertexts[0],
nonce: o.nonce.to_le_bytes(),
});
Ok(())
}
```
Let's also have a look at the `Accounts` structs for each of these instructions:
```rust theme={null}
/// Accounts required to invoke the `add_together` encrypted instruction.
/// `add_together` must be the name of the encrypted instruction we're invoking.
#[queue_computation_accounts("add_together", payer)]
#[derive(Accounts)]
#[instruction(computation_offset: u64)]
pub struct AddTogether<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init_if_needed,
space = 9,
payer = payer,
seeds = [&SIGN_PDA_SEED],
bump,
address = derive_sign_pda!(),
)]
pub sign_pda_account: Account<'info, ArciumSignerAccount>,
#[account(
address = derive_mxe_pda!()
)]
pub mxe_account: Box>,
#[account(
mut,
address = derive_mempool_pda!(mxe_account)
)]
/// CHECK: mempool_account, checked by the arcium program.
pub mempool_account: UncheckedAccount<'info>,
#[account(
mut,
address = derive_execpool_pda!(mxe_account)
)]
/// CHECK: executing_pool, checked by the arcium program.
pub executing_pool: UncheckedAccount<'info>,
#[account(
mut,
address = derive_comp_pda!(computation_offset, mxe_account)
)]
/// CHECK: computation_account, checked by the arcium program.
pub computation_account: UncheckedAccount<'info>,
#[account(
address = derive_comp_def_pda!(COMP_DEF_OFFSET_ADD_TOGETHER)
)]
pub comp_def_account: Box>,
#[account(
mut,
address = derive_cluster_pda!(mxe_account)
)]
pub cluster_account: Box>,
#[account(
mut,
address = ARCIUM_FEE_POOL_ACCOUNT_ADDRESS,
)]
pub pool_account: Account<'info, FeePool>,
#[account(
mut,
address = ARCIUM_CLOCK_ACCOUNT_ADDRESS
)]
pub clock_account: Account<'info, ClockAccount>,
pub system_program: Program<'info, System>,
pub arcium_program: Program<'info, Arcium>,
}
```
That's a lot of accounts to remember! Here's what each one does:
**Core MXE accounts:**
* `mxe_account`: Your MXE's metadata and configuration
* `mempool_account`: Queue where computations wait to be processed
* `executing_pool`: Tracks computations currently being executed
* `computation_account`: Stores individual computation data and results
* `comp_def_account`: Definition of your encrypted instruction (circuit)
**Arcium Network accounts:**
* `cluster_account`: The MPC cluster that will process your computation
* `pool_account`: Arcium's fee collection account
* `clock_account`: Network timing information
**System accounts:**
* `payer`: Pays transaction fees and rent
* `sign_pda_account`: PDA signer for the computation
* `system_program`: Solana's system program for account creation
* `arcium_program`: Arcium's core program that orchestrates MPC
These accounts can be reused for any encrypted instruction. Change:
1. `COMP_DEF_OFFSET_ADD_TOGETHER` to match your instruction name
2. The instruction name in the `queue_computation_accounts` macro
Callback accounts are smaller:
```rust theme={null}
#[callback_accounts("add_together")]
#[derive(Accounts)]
pub struct AddTogetherCallback<'info> {
pub arcium_program: Program<'info, Arcium>,
/// COMP_DEF_OFFSET_ADD_TOGETHER is an encrypted instruction specific u32
/// offset which can be calculated with `comp_def_offset("add_together")`, where
/// comp_def_offset is a function provided by the Arcium SDK and `add_together`
/// is the name of the encrypted instruction we're invoking.
#[account(
address = derive_comp_def_pda!(COMP_DEF_OFFSET_ADD_TOGETHER)
)]
pub comp_def_account: Account<'info, ComputationDefinitionAccount>,
#[account(
address = derive_mxe_pda!()
)]
pub mxe_account: Account<'info, MXEAccount>,
/// CHECK: computation_account, checked by arcium program via constraints in the callback context.
pub computation_account: UncheckedAccount<'info>,
#[account(
address = derive_cluster_pda!(mxe_account)
)]
pub cluster_account: Account<'info, Cluster>,
#[account(address = ::arcium_anchor::solana_instructions_sysvar::ID)]
/// CHECK: instructions_sysvar, checked by the account constraint
pub instructions_sysvar: UncheckedAccount<'info>,
}
```
The callback requires fewer accounts. As with the `AddTogether` struct, change the `derive_comp_def_pda!` parameter and the instruction name in the `callback_accounts` macro for each encrypted instruction.
The `callback_ix()` method is a convenient helper generated by the `#[callback_accounts]` macro that automatically creates the proper callback instruction with all required accounts.
To return more than a raw value, check out [input/outputs](/developers/arcis/input-output) for how to handle encrypted data and [callback accounts](/developers/program/callback-accs) for returning additional accounts in the callback.
## What's next?
Pass additional accounts to your callback for storing results.
Encrypt inputs and invoke computations from TypeScript.
Deploy your MXE to Solana devnet.
Close computation definitions and MXEs when retiring them.
Output size limits and language constraints to keep in mind.
# Account lifecycle and closing
Source: https://docs.arcium.com/developers/program/account-lifecycle
Close MXEs and computation definitions to reclaim rent: state machine, authority rules, errors, and common mistakes
Computation definitions and MXEs are onchain accounts that hold rent. When you no longer need them, close them to reclaim that rent. This page covers the close lifecycle, authority rules, and common errors. For a shorter command list, see the [deployment guide](/developers/deployment#closing-an-mxe-or-computation-definition).
This page covers long-lived MXE and computation-definition accounts. To close per-computation accounts after finalization, use [`claimComputationRent`](/developers/js-client-library#reclaiming-computation-rent).
## Computation-definition lifecycle
A computation-definition account moves through the following states:
```mermaid theme={null}
stateDiagram-v2
[*] --> IncompleteOnChain: init_computation_def
(default OnChain)
IncompleteOnChain --> Active: upload finalized
(is_completed = true)
[*] --> Active: init_computation_def
(OffChain)
Active --> Deactivated: deactivate-computation-definition
(irreversible; blocks new queues)
Deactivated --> Closed: close-computation-definition
(after TTL + no in-flight comps)
IncompleteOnChain --> Closed: close-computation-definition
(skips deactivate + TTL)
Closed --> [*]
```
A default `CircuitSource::OnChain` comp def starts as `IncompleteOnChain` (`is_completed = false`) and is not queueable until upload finalization marks it complete. `CircuitSource::OffChain` comp defs initialize directly as `Active`. `CircuitSource::Local` is reserved for internal MXE key-generation and recovery circuits; user initialization and queueing reject it.
The two-step gate exists so computations queued before deactivation can drain safely. Calling `deactivate-computation-definition` blocks new `queue_computation` calls immediately; existing queued computations must finalize or expire before closing succeeds.
## Deactivation TTL
`TTL_SLOTS = 180` (\~72s at 400 ms/slot). The TTL is identical to a Solana transaction's signature lifetime; the constant is shared on purpose. You can close the computation definition any time after `deactivation_slot + 180`.
## Authority
| Circuit source | Who can deactivate / close |
| ------------------------- | ------------------------------------------------------------- |
| `CircuitSource::OnChain` | `upload_auth` (the keypair that uploaded the circuit buffers) |
| `CircuitSource::OffChain` | `mxe.authority` |
`close-mxe` always requires `mxe.authority`. Rent from every close goes back to the signer keypair (`-k`).
## Closing a computation definition
```bash theme={null}
# 1. Mark the computation definition for closure (irreversible: blocks new queues immediately)
arcium deactivate-computation-definition \
-o \
-p \
-k \
--rpc-url
# 2. After the TTL (180 slots, ~72s) has passed and no in-flight comps remain, close it
arcium close-computation-definition \
-o \
-p \
-c \
-k \
--rpc-url
```
`close-computation-definition` is the only close command that needs `--cluster-offset` (`-c`): the program enforces `mxe.cluster == cluster_offset`.
### Incomplete onchain comp defs skip the TTL
If a `CircuitSource::OnChain` upload was aborted before finishing, the comp def remains `Incomplete` and can be closed immediately, without deactivating first. This is the only way out of the two-step gate.
### Raw circuit buffers (onchain circuits only)
`CircuitSource::OnChain` comp defs also store circuit bytes in separate buffer accounts indexed by `raw_circuit_index`. Close each buffer separately with the MXE authority keypair:
```bash theme={null}
arcium close-computation-definition-buffers \
-o \
-p \
-i \
-k \
--rpc-url
```
Buffers can be closed when the parent comp def is `Deactivated` *or* still `Incomplete`. Unlike deactivating or closing the parent `OnChain` comp def, buffer close requires `mxe.authority`. Offchain comp defs store no buffers, so this step does not apply to them. Reserved internal `Local` comp defs close with the MXE.
### Reserved comp defs
Each MXE has reserved internal computation definitions (e.g. `MxeKeygen`, `MxeKeyRecoveryInit`). You cannot close these directly with `close-computation-definition`: attempting it errors with `CannotCloseReservedComputationDefinition`. They are closed atomically by `close-mxe`.
### LUT entries persist across close
Closing a comp def does **not** close its address-lookup-table entry. Re-initializing a comp def at the same offset reuses the existing LUT slot, so close + reinit cycles do not pay rent for a new LUT.
## Closing an MXE
```bash theme={null}
arcium close-mxe \
-p \
-k \
--rpc-url
```
`close-mxe` enforces several preconditions before closing:
1. MXE must not be in `Migration` state: abort or resume the migration first.
2. Keygen must be finalized (`MxeKeygenNotFinalized` otherwise).
3. Every user-defined computation definition must be closed first. Only reserved MXE computation definitions may remain (`MxeKeygen` and, if present, `MxeKeyRecoveryInit`).
4. Signer must be `mxe.authority`.
When the preconditions pass, `close-mxe` closes the MXE account, the reserved keygen comp def, the keygen computation account, the recovery cluster account, and (if present) the key-recovery-init comp def and computation, all in one transaction. Rent for every closed account returns to the signer.
## Error reference
Anchor program errors are reported as `Error Code: ` where the number is `6000 + `. Internal offsets and meanings:
| Internal | Anchor surfaced | Name | Meaning |
| -------- | --------------- | -------------------------------------------- | ------------------------------------------------- |
| 0 | 6000 | `InvalidAuthority` | Signer is not the authority for this account |
| 9 | 6009 | `MxeHasComputationDefinitions` | `close-mxe` blocked: user comp defs remain |
| 10 | 6010 | `MxeKeygenNotFinalized` | `close-mxe` blocked: keygen not yet finalized |
| 306 | 6306 | `ComputationDefinitionDeactivated` | Cannot deactivate twice |
| 307 | 6307 | `ComputationDefinitionNotDeactivated` | Call `deactivate-computation-definition` first |
| 308 | 6308 | `ComputationDefinitionHasActiveComputations` | A pre-deactivation computation is still in flight |
| 309 | 6309 | `ComputationDefinitionTtlNotPassed` | Wait until `deactivation_slot + 180` |
| 310 | 6310 | `CannotCloseReservedComputationDefinition` | Reserved offsets close atomically via `close-mxe` |
| 725 | 6725 | `CannotCloseMxeDuringMigration` | Abort or resume the cluster migration first |
## Common mistakes
* **"`ComputationDefinitionTtlNotPassed` (6309)"**: you're inside the 180-slot window after deactivation. Wait \~72 s and retry.
* **"`ComputationDefinitionHasActiveComputations` (6308)"**: a computation queued before `deactivation_slot` is still in the execpool. Computations themselves expire after 180 slots; wait for it to finalize or expire and retry.
* **"`ComputationDefinitionNotDeactivated` (6307)"**: you skipped `deactivate-computation-definition`. Active comp defs cannot be closed directly (the only exception is `Incomplete` `OnChain` comp defs).
* **"`CannotCloseReservedComputationDefinition` (6310)"**: you targeted a reserved offset (`MxeKeygen`, `MxeKeyRecoveryInit`). Run `close-mxe` instead; reserved offsets close atomically as part of it.
* **"`MxeHasComputationDefinitions` (6009)"**: `close-mxe` requires every user-defined comp def to be closed first. Only reserved MXE computation definitions may remain (`MxeKeygen` and, if present, `MxeKeyRecoveryInit`).
* **"`CannotCloseMxeDuringMigration` (6725)"**: there's a `migrate-cluster` in flight. `--resume` or `--abort` it, then retry.
* **"`InvalidAuthority` (6000)"**: for user-defined comp defs, `OnChain` accepts `upload_auth` while `OffChain` requires `mxe.authority`. `close-mxe` always requires `mxe.authority`.
* **"`InvalidAuthority` (6000)" on buffer close**: `close-computation-definition-buffers` always requires `mxe.authority`, even for `OnChain` comp defs uploaded by a different `upload_auth`.
* **"`close-computation-definition-buffers` did nothing"**: your comp def is `OffChain`. There are no buffer accounts to close; skip the step.
* **"Forgot `--cluster-offset` on `close-computation-definition`"**: only that command needs `-c`; the program rejects mismatches against `mxe.cluster`.
## What's next?
Deploy, migrate, and close MXEs from the CLI.
How comp defs are created and initialized.
# Callback accounts
Source: https://docs.arcium.com/developers/program/callback-accs
Advanced patterns for callback accounts in Arcium programs
Callback accounts define additional accounts used by a computation callback. Use them when a computation result needs to modify an onchain account.
**Prerequisites**: Before diving into callback accounts, make sure you've read:
* [Basic program invocation guide](/developers/program): fundamentals of queuing computations and defining callback instructions
* [Callback type generation](/developers/program/callback-type-generation): how output types like `AddTogetherOutput` are automatically generated from encrypted instructions
* [Arcis inputs/outputs](/developers/arcis/input-output): 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](/developers/program), 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:
```rust theme={null}
#[account]
#[derive(InitSpace)]
pub struct SecretAdditionResult {
pub sum: [u8; 32], // Store the encrypted result as ciphertext
}
pub fn init(ctx: Context) -> 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 all required standard accounts and is less error-prone.
**What callback\_ix() does automatically:**
* Creates a CallbackInstruction with the proper instruction data
* Automatically includes standard accounts: `arcium_program`, `comp_def_account`, `mxe_account`, `computation_account`, `cluster_account`, `instructions_sysvar`
* Accepts custom accounts through the `&[CallbackAccount]` parameter
* Eliminates boilerplate and prevents errors
```rust theme={null}
pub fn add_together(
ctx: Context,
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 encrypted instruction expects using ArgBuilder
let args = ArgBuilder::new()
.x25519_pubkey(pub_key)
.plaintext_u128(nonce)
.encrypted_u8(ciphertext_0)
.encrypted_u8(ciphertext_1)
.build();
// 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 encrypted instruction expects
args,
// Using callback_ix() helper - automatically includes the 6 standard accounts
// (arcium_program, comp_def_account, mxe_account, computation_account, cluster_account, instructions_sysvar) plus our custom account
vec![AddTogetherCallback::callback_ix(
computation_offset,
&ctx.accounts.mxe_account,
&[
CallbackAccount {
pubkey: addition_result_pda,
is_writable: true, // Tells nodes to mark this account as writable in the transaction
}
]
)?],
1, // Number of transactions needed for callback
0, // cu_price_micro: priority fee in microlamports (0 = no priority fee)
0, // callback_cu_limit: compute unit limit for the callback (0 = default)
)?;
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:
```rust theme={null}
pub fn add_together(
ctx: Context,
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 encrypted instruction expects using ArgBuilder
let args = ArgBuilder::new()
.x25519_pubkey(pub_key)
.plaintext_u128(nonce)
.encrypted_u8(ciphertext_0)
.encrypted_u8(ciphertext_1)
.build();
// 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 encrypted instruction expects
args,
// 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_client::ARCIUM_PROGRAM_ID,
is_writable: false,
},
CallbackAccount {
pubkey: derive_comp_def_pda!(COMP_DEF_OFFSET_ADD_TOGETHER),
is_writable: false,
},
CallbackAccount {
pubkey: ctx.accounts.mxe_account.key(),
is_writable: false,
},
CallbackAccount {
pubkey: ctx.accounts.computation_account.key(),
is_writable: false,
},
CallbackAccount {
pubkey: derive_cluster_pda!(ctx.accounts.mxe_account),
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
}
]
}],
1, // Number of transactions needed for callback (1 for simple computations)
0, // cu_price_micro: priority fee in microlamports (0 = no priority fee)
0, // callback_cu_limit: compute unit limit for the callback (0 = default)
)?;
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:
```rust theme={null}
// Macro provided by the Arcium SDK to define a callback instruction.
#[arcium_callback(encrypted_ix = "add_together")]
pub fn add_together_callback(
ctx: Context,
output: SignedComputationOutputs,
) -> Result<()> {
let o = match output.verify_output(
&ctx.accounts.cluster_account,
&ctx.accounts.computation_account
) {
Ok(AddTogetherOutput { field_0 }) => field_0,
Err(e) => {
msg!("Error: {}", e);
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 required for all callbacks
pub arcium_program: Program<'info, Arcium>,
#[account(
address = derive_comp_def_pda!(COMP_DEF_OFFSET_ADD_TOGETHER)
)]
pub comp_def_account: Account<'info, ComputationDefinitionAccount>,
#[account(
address = derive_mxe_pda!()
)]
pub mxe_account: Account<'info, MXEAccount>,
/// CHECK: computation_account, checked by arcium program via constraints in the callback context.
pub computation_account: UncheckedAccount<'info>,
#[account(
address = derive_cluster_pda!(mxe_account)
)]
pub cluster_account: Account<'info, Cluster>,
/// CHECK: instructions_sysvar, checked by the account constraint
#[account(address = ::arcium_anchor::solana_instructions_sysvar::ID)]
pub instructions_sysvar: UncheckedAccount<'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`:
1. Standard accounts are always required first: `arcium_program`, `comp_def_account`, `mxe_account`, `computation_account`, `cluster_account`, `instructions_sysvar`
2. 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 writable
* The account must have `#[account(mut)]` in the callback struct
* Without 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:
1. **Takes required parameters**: `computation_offset` and `&mxe_account` for proper context
2. **Creates a CallbackInstruction** with the proper instruction data
3. **Automatically includes standard accounts** that every callback needs:
* `arcium_program`: The Arcium program that will invoke your callback
* `comp_def_account`: The computation definition account for your encrypted instruction
* `mxe_account`: Your MXE's metadata and configuration
* `computation_account`: The computation being processed
* `cluster_account`: The MPC cluster processing the computation
* `instructions_sysvar`: Solana's instructions sysvar for transaction validation
4. **Accepts custom accounts** through the `&[CallbackAccount]` parameter
### Usage patterns
**Basic usage (no custom accounts):**
```rust theme={null}
vec![AddTogetherCallback::callback_ix(
computation_offset,
&ctx.accounts.mxe_account,
&[]
)?]
```
The empty array indicates no custom accounts needed beyond the standard ones.
**Advanced usage (with custom accounts):**
```rust theme={null}
vec![AddTogetherCallback::callback_ix(
computation_offset,
&ctx.accounts.mxe_account,
&[
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](/developers/program).
For handling different types of encrypted data inputs and outputs, see [Arcis inputs/outputs](/developers/arcis/input-output).
## What's next?
Understand how output types like `AddTogetherOutput` are automatically generated.
Track computation completion from your TypeScript client.
# Callback type generation
Source: https://docs.arcium.com/developers/program/callback-type-generation
Automatic type generation for callback outputs in Arcium programs
## 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:
```rust theme={null}
// You write this encrypted instruction:
#[instruction]
pub fn add_numbers() -> Enc { /* ... */ }
// Arcium generates this for your callback:
pub struct AddNumbersOutput {
pub field_0: SharedEncryptedStruct<1>, // 1 = single u64 value
}
```
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:
```rust theme={null}
#[instruction]
pub fn my_calc() -> Enc { /* ... */ }
```
2. Generate types: `arcium build`
3. Use in callback:
```rust theme={null}
#[arcium_callback(encrypted_ix = "my_calc")]
pub fn my_calc_callback(
ctx: Context,
output: SignedComputationOutputs,
) -> Result<()> {
let o = match output.verify_output(
&ctx.accounts.cluster_account,
&ctx.accounts.computation_account
) {
Ok(MyCalcOutput { field_0 }) => field_0,
Err(_) => return Err(ErrorCode::AbortedComputation.into()),
};
let encrypted_value = o.ciphertexts[0];
// Your logic here
Ok(())
}
```
## Your first generated type: simple addition
Here's a concrete example. Consider this encrypted instruction that adds two numbers:
```rust theme={null}
#[encrypted]
mod circuits {
use arcis::*;
#[instruction]
pub fn add_together(input: Enc) -> Enc {
let (a, b) = input.to_arcis();
let sum = a as u16 + b as u16;
input.owner.from_arcis(sum)
}
}
```
When Arcium sees that your function returns `Enc`, it automatically generates this output struct:
```rust theme={null}
#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct AddTogetherOutput {
pub field_0: SharedEncryptedStruct<1>,
}
```
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:
```rust theme={null}
#[arcium_callback(encrypted_ix = "add_together")]
pub fn add_together_callback(
ctx: Context,
output: SignedComputationOutputs,
) -> Result<()> {
let o = match output.verify_output(
&ctx.accounts.cluster_account,
&ctx.accounts.computation_account
) {
Ok(AddTogetherOutput { field_0 }) => field_0,
Err(_) => return Err(ErrorCode::AbortedComputation.into()),
};
// Access the encrypted result and metadata
emit!(SumEvent {
sum: o.ciphertexts[0], // The encrypted u16 sum
nonce: o.nonce.to_le_bytes(), // Nonce for decryption
});
Ok(())
}
```
## 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:
```rust theme={null}
#[instruction]
pub fn add_together(input: Enc) -> Enc {
// Your function code here
}
```
After running `arcium build`, the `#[callback_accounts("add_together")]` macro reads `build/add_together.idarc` and generates:
```rust theme={null}
// This struct is generated for you - you never write it yourself!
pub struct AddTogetherOutput {
pub field_0: SharedEncryptedStruct<1>,
}
```
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.
```rust theme={null}
// Available after running arcium build
#[arcium_callback(encrypted_ix = "add_together")]
pub fn callback(
ctx: Context