# 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 and Clusters ## 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, output: SignedComputationOutputs, ) -> Result<()> { // AddTogetherOutput was generated by #[callback_accounts] from build/add_together.idarc } ``` ## Behind the scenes: the two-step pipeline Understanding how types flow from your circuit to your Solana program helps explain why `arcium build` must run first. ### Step 1: `arcium build` produces `.idarc` files When you run `arcium build`, the compiler processes each `#[instruction]` function inside your `#[encrypted]` module: 1. **Parse the return type**: The compiler examines your function signature and extracts the return type 2. **Analyze the structure**: It breaks down complex types (tuples, structs, encryption wrappers) into components 3. **Write the interface file**: It serializes the type information to `build/{circuit_name}.idarc` ### Step 2: `#[callback_accounts]` generates Rust structs When you compile your Solana program, the `#[callback_accounts("circuit_name")]` macro: 1. **Reads the `.idarc` file**: Loads the circuit interface from `build/{circuit_name}.idarc` 2. **Generates struct definitions**: Creates typed Rust structs matching the circuit's return type 3. **Injects into scope**: The generated types become available in your program module ### What gets generated For different return types, the pipeline generates different struct patterns: ```rust theme={null} // Your function: #[instruction] pub fn simple() -> Enc // Macro generates: pub struct SimpleOutput { pub field_0: SharedEncryptedStruct<1>, } ``` ```rust theme={null} // Your function: #[instruction] pub fn complex() -> (Enc, Enc) // Macro generates: pub struct ComplexOutput { pub field_0: ComplexOutputStruct0, } pub struct ComplexOutputStruct0 { pub field_0: SharedEncryptedStruct<1>, pub field_1: MXEEncryptedStruct<1>, } ``` ### Why this approach works This two-step approach provides several benefits: * **Type safety**: You get compile-time type checking for encrypted results * **No manual definition**: You don't need to define output structs yourself * **Consistency**: All generated types follow the same predictable patterns * **Automatic updates**: If you change your function's return type, re-run `arcium build` and the structs update automatically The key insight is that these structs exist in your compiled program but not in your source code: `arcium build` produces the interface files, and `#[callback_accounts]` turns them into Rust types during compilation. ## Understanding LEN parameters In our `add_together` example, you saw `SharedEncryptedStruct<1>`. The `` number tells you how many encrypted scalar values are stored inside. The `` number represents the count of individual encrypted scalar values: | Return Type | LEN Value | Why | | -------------------------- | ----------- | --------------------------------- | | `Enc` | 1 | Single scalar | | `Enc` | 2 | Two scalars | | `Enc` | 5 | Five array elements | | `Enc` | field count | Count all scalar fields in struct | **For custom structs**, LEN equals total scalar fields: ```rust theme={null} struct UserProfile { id: u32, // 1 scalar balance: u64, // 1 scalar active: bool, // 1 scalar } // Result: SharedEncryptedStruct<3> ``` ## Type availability and scope ### Where generated types live Generated types are scoped to the module where `#[callback_accounts]` is used. After running `arcium build`, the types become available during program compilation: ```rust theme={null} // In your Solana program: CalculateOutput is generated by #[callback_accounts] // which reads build/calculate.idarc (produced by arcium build from your #[instruction] function) #[callback_accounts("calculate")] pub struct CalculateCallback<'info> { /* ... */ } #[arcium_callback(encrypted_ix = "calculate")] pub fn calculate_callback( ctx: Context, output: SignedComputationOutputs, ) -> Result<()> { // CalculateOutput is available here } ``` ### No import required Unlike external types, you don't need to import generated types. `#[callback_accounts]` injects them directly into your module's namespace: ```rust theme={null} // No need for: use some_crate::CalculateOutput; // The type just exists automatically ``` ### Generated struct properties All generated structs automatically receive standard derives that make them work with Anchor: ```rust theme={null} // Every generated struct gets these automatically: #[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone)] pub struct YourFunctionOutput { // fields... } ``` This is why you can use generated types in Anchor contexts without additional setup. ### Multiple instructions, multiple types Each `#[instruction]` produces its own `.idarc` file, so `#[callback_accounts]` generates a separate output type per circuit: ```rust theme={null} #[instruction] pub fn add() -> Enc // → AddOutput #[instruction] pub fn multiply() -> Enc // → MultiplyOutput #[instruction] pub fn divide() -> Enc // → DivideOutput ``` All generated types coexist in the same module scope without conflicts. ## Generation process When you define an encrypted instruction: 1. `arcium build` compiles your circuit and writes a `.idarc` interface file describing its return type 2. `#[callback_accounts]` reads the `.idarc` file at compile time and generates corresponding Rust structs with predictable names 3. Encryption patterns are detected automatically and produce specialized types 4. The generated types are available in your `#[arcium_callback]` functions ## How the naming works The naming follows predictable patterns: ### Your circuit gets an output struct If your encrypted instruction is called `add_together`, you get a struct called `AddTogetherOutput`. Arcium converts your circuit name to PascalCase and adds "Output" at the end. ### Fields are numbered Since Anchor doesn't support tuple structs (yet), Arcium uses numbered fields instead. So if your function returns multiple values, you'll get `field_0`, `field_1`, `field_2`, and so on. Not the prettiest names, but they're consistent and predictable. ### Complex types get their own structs When your function returns complex nested data (like tuples or custom structs), Arcium generates additional helper structs with a unified naming convention: * All output structs use `{CircuitName}OutputStruct{index}` pattern * Nested structs within outputs use `{ParentName}OutputStruct{parent_index}{field_index}` pattern * The naming ensures uniqueness while maintaining consistency ## Encryption types: Shared vs MXE Arcium automatically detects different encryption patterns and generates the right struct type. Understanding when each type is used helps you predict the generated structs. ### SharedEncryptedStruct\ When your circuit returns `Enc`, Arcium knows this is data that both the client and the MXE can decrypt. It generates a struct that includes everything needed for decryption: ```rust theme={null} pub struct SharedEncryptedStruct { pub encryption_key: [u8; 32], // The shared public key pub nonce: u128, // Random nonce for security pub ciphertexts: [[u8; 32]; LEN], // Your actual encrypted data } ``` The `` part tells you how many encrypted values are packed inside. So `SharedEncryptedStruct<1>` has one encrypted value, `SharedEncryptedStruct<3>` has three, and so on. In your callback, you can access everything you need: ```rust theme={null} let shared_key = result.encryption_key; // For key exchange let nonce = result.nonce; // For decryption let encrypted_value = result.ciphertexts[0]; // Your data ``` ### MXEEncryptedStruct\ For `Enc` data, only the MXE cluster can decrypt it - clients can't. Since there's no shared secret needed, the struct is simpler: ```rust theme={null} pub struct MXEEncryptedStruct { pub nonce: u128, // Still need the nonce pub ciphertexts: [[u8; 32]; LEN], // Your encrypted data } ``` Notice there's no `encryption_key` field here - that's because clients don't get to decrypt MXE data. ```rust theme={null} // Working with MXE-encrypted data let nonce = result.nonce; let encrypted_value = result.ciphertexts[0]; // Note: You can't decrypt this on the client side! ``` ### EncDataStruct\ For encrypted data without key exchange metadata (used when the observer tracks encryption context client-side): ```rust theme={null} // Pattern: Only N Ciphertexts pub struct EncDataStruct { pub ciphertexts: [[u8; 32]; LEN], // Raw encrypted values } ``` **Note**: `EncDataStruct` is used when only ciphertext data is needed without additional metadata. See [`EncData`](/developers/arcis/types#advanced-encdata%3Ct%3E) for when to use this pattern. Most applications use `SharedEncryptedStruct` or `MXEEncryptedStruct` instead. ## Moving to real-world applications Now that you understand the basics with our simple addition example, here's how this works in real applications. The key difference is that real apps often: * **Return multiple values**: Functions return tuples or complex structs instead of single values * **Mix encryption types**: Some data for users (`Shared`), some for MXE only (`Mxe`) * **Handle complex data**: Custom structs with multiple fields instead of simple numbers The type generation system handles all of this automatically - you just need to understand the patterns. ## Real-world examples Here's how this type generation works in actual Arcium applications: ### Simple tuple example Let's start with something in between - a function that returns two related values: ```rust theme={null} #[instruction] pub fn calculate_stats(value: u32) -> (Enc, Enc) { // Calculate both the square and double of a number (value.square(), value * 2) } ``` Since this returns a tuple `(Enc, Enc)`, Arcium generates: ```rust theme={null} pub struct CalculateStatsOutput { pub field_0: CalculateStatsOutputStruct0, // The whole tuple becomes one field } pub struct CalculateStatsOutputStruct0 { pub field_0: SharedEncryptedStruct<1>, // First u32 (the square) pub field_1: SharedEncryptedStruct<1>, // Second u32 (the double) } ``` Notice how tuples get wrapped: the tuple itself becomes `field_0`, and its elements become `field_0`, `field_1`, etc. ### Voting application Now let's look at a more realistic example. The [encrypted voting example](https://github.com/arcium-hq/examples/tree/main/voting) shows a perfect use case. You have poll data that only the MXE should see, and a user's vote that should be shared between the user and the MXE: ```rust theme={null} // Example poll data structure (would be defined in your program) #[derive(AnchorSerialize, AnchorDeserialize)] pub struct PollData { pub vote_count_yes: u32, pub vote_count_no: u32, pub is_active: bool, } #[instruction] pub fn vote( poll_data: Enc, // Poll results stay private vote_choice: Enc // User can verify their vote ) -> (Enc, Enc) { // ... voting logic that maintains privacy } ``` Since this function returns a tuple `(Enc, Enc)`, Arcium generates: ```rust theme={null} pub struct VoteOutput { pub field_0: VoteOutputStruct0, // The whole tuple wraps into one field } pub struct VoteOutputStruct0 { pub field_0: MXEEncryptedStruct<3>, // The updated poll data (vote_count_yes + vote_count_no + is_active = 3) pub field_1: SharedEncryptedStruct<1>, // The vote confirmation (boolean) } ``` Now in your callback, you can work with properly typed data instead of raw bytes: ```rust theme={null} #[arcium_callback(encrypted_ix = "vote")] pub fn vote_callback( ctx: Context, output: SignedComputationOutputs, ) -> Result<()> { let o = match output.verify_output( &ctx.accounts.cluster_account, &ctx.accounts.computation_account ) { Ok(VoteOutput { field_0 }) => field_0, Err(_) => return Err(ErrorCode::AbortedComputation.into()), }; let poll_data = o.field_0; // The updated poll (MXE only) let vote_confirmation = o.field_1; // User's confirmation (shared) // Emit an event with the user's confirmation emit!(VoteEvent { confirmation: vote_confirmation.ciphertexts[0], nonce: vote_confirmation.nonce.to_le_bytes(), }); Ok(()) } ``` ### Coinflip application: back to basics After seeing complex tuples and mixed encryption types, let's look at the simplest possible case. The [coinflip example](https://github.com/arcium-hq/examples/tree/main/coinflip) returns just a single encrypted boolean: ```rust theme={null} #[instruction] pub fn flip() -> Enc { // Generate secure randomness in MPC // Return encrypted result that client can decrypt } ``` Arcium sees this returns `Enc` and creates: ```rust theme={null} pub struct FlipOutput { pub field_0: SharedEncryptedStruct<1>, // Just one boolean } ``` Your callback: ```rust theme={null} #[arcium_callback(encrypted_ix = "flip")] pub fn flip_callback( ctx: Context, output: SignedComputationOutputs, ) -> Result<()> { let o = match output.verify_output( &ctx.accounts.cluster_account, &ctx.accounts.computation_account ) { Ok(FlipOutput { field_0 }) => field_0, Err(_) => return Err(ErrorCode::AbortedComputation.into()), }; // Emit the encrypted result - client will decrypt to see heads/tails emit!(FlipEvent { result: o.ciphertexts[0], nonce: o.nonce.to_le_bytes(), }); Ok(()) } ``` ### Blackjack application From the [blackjack example](https://github.com/arcium-hq/examples/tree/main/blackjack) with complex game state: ```rust theme={null} // Example structures (would be defined in your blackjack program) #[derive(AnchorSerialize, AnchorDeserialize)] pub struct GameState { pub deck_cards: [u8; 52], pub dealer_cards: [u8; 10], pub round_number: u32, pub game_active: bool, pub house_balance: u64, } #[derive(AnchorSerialize, AnchorDeserialize)] pub struct PlayerHand { pub cards: [u8; 10], pub card_count: u8, pub bet_amount: u64, } #[instruction] pub fn player_hit( game_state: Enc, player_hand: Enc ) -> (Enc, Enc, Enc) { // ... game logic } ``` **Generated types**: ```rust theme={null} pub struct PlayerHitOutput { pub field_0: PlayerHitOutputStruct0, } pub struct PlayerHitOutputStruct0 { pub field_0: MXEEncryptedStruct<65>, // Updated game state (deck_cards[52] + dealer_cards[10] + round_number[1] + game_active[1] + house_balance[1] = 65) pub field_1: SharedEncryptedStruct<12>, // Player's new hand (cards[10] + card_count[1] + bet_amount[1] = 12) pub field_2: SharedEncryptedStruct<1>, // Is game over? (boolean) } ``` ## Complex nested structures For more complex outputs with nested data structures: ```rust theme={null} // Define the custom struct used in the circuit #[derive(AnchorSerialize, AnchorDeserialize)] pub struct UserData { pub id: u32, pub active: bool, } #[instruction] pub fn complex_example() -> ( UserData, Enc, (u64, f32), Enc ) { // ... complex logic } ``` **Generated types**: ```rust theme={null} pub struct ComplexExampleOutput { pub field_0: ComplexExampleOutputStruct0, // Entire tuple as single field } pub struct ComplexExampleOutputStruct0 { pub field_0: ComplexExampleOutputStruct00, // UserData pub field_1: SharedEncryptedStruct<1>, // Enc pub field_2: ComplexExampleOutputStruct02, // (u64, f32) tuple pub field_3: MXEEncryptedStruct<1>, // Enc } pub struct ComplexExampleOutputStruct00 { pub field_0: u32, // UserData.id pub field_1: bool, // UserData.active } pub struct ComplexExampleOutputStruct02 { pub field_0: u64, // First tuple element pub field_1: f32, // Second tuple element } ``` **Notice the naming pattern**: We have `ComplexExampleOutputStruct00` and `ComplexExampleOutputStruct02`, but no `ComplexExampleOutputStruct01`. This is because: * `field_0` (UserData) needs a custom struct → `ComplexExampleOutputStruct00` * `field_1` (SharedEncryptedStruct) uses a predefined type → no custom struct needed * `field_2` ((u64, f32) tuple) needs a custom struct → `ComplexExampleOutputStruct02` * `field_3` (MXEEncryptedStruct) uses a predefined type → no custom struct needed Only fields that contain custom structs or tuples get their own generated struct definitions. ## Working with generated types ### Pattern matching Use destructuring to access nested data: ```rust theme={null} let ComplexExampleOutputStruct0 { field_0: user_data, field_1: shared_encrypted, field_2: tuple_data, field_3: mxe_encrypted, } = match output.verify_output( &ctx.accounts.cluster_account, &ctx.accounts.computation_account, ) { Ok(ComplexExampleOutput { field_0 }) => field_0, Err(_) => return Err(ErrorCode::AbortedComputation.into()), }; // Access specific fields let user_id = user_data.field_0; let is_active = user_data.field_1; let shared_value = shared_encrypted.ciphertexts[0]; let timestamp = tuple_data.field_0; ``` ### Error handling Always handle computation failures: ```rust theme={null} let result = match output.verify_output( &ctx.accounts.cluster_account, &ctx.accounts.computation_account, ) { Ok(data) => data, Err(_) => return Err(ErrorCode::AbortedComputation.into()), }; ``` ## Best practices ### 1. Use descriptive variable names ```rust theme={null} // Good let FlipOutput { field_0: coin_result } = result; let is_heads = coin_result.ciphertexts[0]; // Less clear let FlipOutput { field_0 } = result; let result = field_0.ciphertexts[0]; ``` ### 2. Document your circuit interfaces ```rust theme={null} /// Returns (updated_game_state, player_hand, is_game_over) #[instruction] pub fn player_hit(/* ... */) -> (Enc, Enc, Enc) { // ... } ``` ### 3. Handle all computation states ```rust theme={null} let result = match output.verify_output( &ctx.accounts.cluster_account, &ctx.accounts.computation_account, ) { Ok(data) => data, Err(_) => return Err(ErrorCode::AbortedComputation.into()), }; ``` ### 4. Emit events for client tracking ```rust theme={null} emit!(ComputationCompleteEvent { computation_id: ctx.accounts.computation_account.key(), success: true, result_hash: result.ciphertexts[0], // or use a hash function if needed }); ``` ## When things go wrong Here are the most common issues and how to fix them: ### "Type not found" errors ```rust theme={null} // Error: cannot find type `MyCircuitOutput` in this scope output: SignedComputationOutputs ``` This usually means one of two things: 1. **Typo in the circuit name** - Check that `MyCircuit` exactly matches your `#[instruction]` function name (case matters!) 2. **You forgot to rebuild** - Run `arcium build` again after making changes to your encrypted instructions ### "No field found" errors ```rust theme={null} // Error: no field `result` on type `AddTogetherOutput` let value = output.result; ``` Remember, the generated structs use numbered fields like `field_0`, `field_1`, etc. There's no field called `result` unless you specifically named your function that way. Try this instead: ```rust theme={null} let value = output.field_0; // First (and often only) field ``` ### Encryption type mismatches ```rust theme={null} // Error: expected `SharedEncryptedStruct<1>`, found `MXEEncryptedStruct<1>` ``` This happens when your circuit returns `Enc` but your callback expects `Enc` (or vice versa). Double-check your encrypted instruction's return type - it needs to match what you're expecting in the callback. ## Callback not working? Check these: * [ ] Circuit name matches exactly (case sensitive) * [ ] Ran `arcium build` after changing circuit * [ ] Handling `verify_output` Ok/Err * [ ] Using correct field numbers (field\_0, field\_1, etc.) * [ ] Array access within bounds (ciphertexts.len()) ## Finding generated types The best way to see generated types: ```bash theme={null} # First install cargo-expand if you haven't already cargo install cargo-expand # In your program directory cargo expand | grep "YourCircuitOutput" -A 20 ``` This shows exactly what structs were generated for your circuit. You can also search the full output: ```bash theme={null} cargo expand > expanded.rs # Then search expanded.rs for your circuit name ``` ## Array and complex type handling ### Fixed-size arrays When your circuit returns arrays, each element becomes a separate scalar in the LEN count: ```rust theme={null} #[instruction] pub fn process_batch() -> Enc { // Process multiple values at once [result1, result2, result3] } ``` This generates `SharedEncryptedStruct<3>` because the array has 3 elements: ```rust theme={null} pub struct ProcessBatchOutput { pub field_0: SharedEncryptedStruct<3>, // Array of 3 u32s } ``` In your callback, access individual elements: ```rust theme={null} let encrypted_array = result.field_0; let first_element = encrypted_array.ciphertexts[0]; // result1 let second_element = encrypted_array.ciphertexts[1]; // result2 let third_element = encrypted_array.ciphertexts[2]; // result3 ``` ### Nested structures For deeply nested data, LEN counts **all scalar values** at any depth: ```rust theme={null} pub struct Position { pub x: u32, pub y: u32, } pub struct Entity { pub position: Position, // 2 scalars (x, y) pub health: u32, // 1 scalar pub alive: bool, // 1 scalar } // Total: 2 + 1 + 1 = 4 scalars #[instruction] pub fn update_entity() -> Enc { /* ... */ } ``` Result: `SharedEncryptedStruct<4>` because Entity contains 4 total scalar values. ## Migration from v0.1.x If you're upgrading from an older version, the new type generation system replaces manual byte parsing: **Old way (v0.1.x)**: ```rust theme={null} pub fn encrypted_ix_callback(output: ComputationOutputs) -> Result<()> { let bytes = if let ComputationOutputs::Bytes(bytes) = output { bytes } else { return Err(ErrorCode::AbortedComputation.into()); }; let sum = bytes[48..80].try_into().unwrap(); let nonce = bytes[32..48].try_into().unwrap(); // ... } ``` **New way (current)**: ```rust theme={null} pub fn encrypted_ix_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()), }; let sum = o.ciphertexts[0]; let nonce = o.nonce; // ... } ``` For version compatibility and required changes, see [Versioning and compatibility](/developers/versioning) and the [release notes](/developers/release-notes). ## Type generation limitations ### Supported return types The type generation system works with most common Rust types, but has some constraints: **✅ Supported:** * Primitive types: `u8`, `u16`, `u32`, `u64`, `u128`, `i8`, `i16`, `i32`, `i64`, `i128`, `bool` * Fixed-size arrays: `[T; N]` where N is a compile-time constant * Tuples: `(T, U, V)` with any number of elements * Custom structs with supported field types * Nested combinations of the above **❌ Not Supported:** * Dynamic types: `Vec`, `String`, `HashMap` * Reference types: `&T`, `&mut T` (except for input parameters) * Generic types with lifetime parameters * Recursive or self-referencing structs * `Option` or `Result` as return types ### Practical constraints **Size limitations:** * Very large structs (1000+ fields) may impact compilation time * Arrays with thousands of elements create correspondingly large LEN values * Deep nesting (10+ levels) may cause macro expansion issues **Naming conflicts:** ```rust theme={null} // This would create a conflict: #[instruction] pub fn test() -> u32 // → TestOutput #[instruction] pub fn TEST() -> u32 // → TestOutput (same name!) ``` Function names must be unique when converted to PascalCase + "Output". ### Working within constraints If you need unsupported types, consider these patterns: ```rust theme={null} // Instead of Vec, use fixed arrays: #[instruction] pub fn process() -> Enc { /* ... */ } // Instead of Option, use a flag + value: #[instruction] pub fn maybe_compute() -> (Enc, Enc) { // (has_value, value) } // Instead of String, use fixed-size byte arrays: #[instruction] pub fn get_name() -> Enc { /* ... */ } ``` ## Common patterns and performance tips ### Choosing the right encryption type * **Use `Enc`** when users need to decrypt and verify results (votes, game outcomes, personal data) * **Use `Enc`** for internal state that users shouldn't access (system secrets, aggregate statistics, protocol data) ### Performance considerations * **Large arrays**: `[u8; 1000]` becomes `SharedEncryptedStruct<1000>` - consider if you really need all elements encrypted * **Complex nesting**: Deep struct hierarchies increase LEN values - flatten when possible * **Mixed returns**: `(Enc, Enc)` creates separate encrypted structs for optimal access patterns ### Testing your callbacks Test callbacks in two layers: 1. **Rust unit tests**: extract the post-`verify_output()` business logic into a pure function and test that directly. 2. **TypeScript integration tests**: queue a real computation with `arcium test`, wait for finalization with `awaitComputationFinalization(...)`, then assert the callback result via emitted events or updated account state. See [Building and testing](/developers/hello-world#building-and-testing) for the `arcium test` runner and [Tracking callbacks](/developers/js-client-library/callback) for `awaitComputationFinalization`. `verify_output()` performs real BLS signature verification against the cluster's BLS key, so a mocked `SignedComputationOutputs` with a dummy signature always fails verification in a unit test. That's why unit tests cover the pure logic only. Exercise the full verification wiring through the `arcium test` flow instead: queue a computation, `awaitComputationFinalization(...)`, then assert on the emitted event or updated account state. ```rust theme={null} #[cfg(test)] mod tests { use super::*; // Post-verify logic extracted as a pure function: test this, not verify_output(). // The callback relays the encrypted ciphertext + nonce to the client (typically via // an emitted event); the client decrypts offchain. fn extract_flip_event_payload(output: FlipOutput) -> ([u8; 32], u128) { (output.field_0.ciphertexts[0], output.field_0.nonce) } #[test] fn test_event_payload_extraction() { let output = FlipOutput { field_0: SharedEncryptedStruct { encryption_key: [0u8; 32], nonce: 42, ciphertexts: [[1u8; 32]], }, }; let (ciphertext, nonce) = extract_flip_event_payload(output); assert_eq!(nonce, 42); assert_eq!(ciphertext, [1u8; 32]); } } ``` Keep your callback thin: match on `verify_output()`, destructure the result, and delegate to the pure function that holds your business logic. The pure function is what you unit-test; the `arcium test` flow is what exercises the callback end-to-end. ## Quick reference | Return Type | Generated Struct | Access Pattern | | ---------------- | -------------------------- | --------------------------------------- | | `Enc` | `SharedEncryptedStruct<1>` | `result.ciphertexts[0]`, `result.nonce` | | `Enc` | `MXEEncryptedStruct<1>` | `result.ciphertexts[0]`, `result.nonce` | | `(T, U, V)` | `{Circuit}OutputStruct0` | `result.field_0`, `result.field_1` | | Custom struct | `{Circuit}OutputStruct0` | `result.field_0`, `result.field_1` | **Callback pattern:** ```rust theme={null} #[arcium_callback(encrypted_ix = "your_function")] pub fn callback( ctx: Context, output: SignedComputationOutputs, ) -> Result<()> { let o = match output.verify_output( &ctx.accounts.cluster_account, &ctx.accounts.computation_account ) { Ok(YourFunctionOutput { field_0 }) => field_0, Err(_) => return Err(ErrorCode::AbortedComputation.into()), }; // Access o.ciphertexts[0], o.nonce.to_le_bytes(), etc. } ``` *** The callback type generation system automatically handles encrypted computation results, eliminating manual byte parsing and offset tracking. With properly typed structs, you can work directly with structured data and focus on building your applications rather than handling low-level data conversion. These generated types provide type safety and predictable patterns that make working with encrypted computation outputs straightforward and reliable. ## What's next? Pass additional accounts to your callback for storing results. Deploy your MXE program to devnet or mainnet. # Computation definition accounts Source: https://docs.arcium.com/developers/program/computation-def-accs Onchain accounts that store compiled MPC circuits for execution ## Why computation definitions exist When you write an encrypted instruction with Arcis, it compiles into an MPC circuit: a program that MPC nodes can execute securely on encrypted data. Computation definitions tell the nodes which circuit to run when your Solana program queues a computation. Computation Definition Accounts bridge your Solana program and the MPC network. They store the circuit metadata and, for onchain circuit sources, the circuit bytecode needed for execution. ## Computation definition accounts When you define an encrypted instruction using [Arcis](/developers/arcis), the MPC cluster that executes it needs access to the instruction interface, metadata, and circuit source. A `ComputationDefinitionAccount` contains two parts: 1. The encrypted instruction metadata and interface. 2. The circuit source. For onchain sources, this references separate accounts that hold the raw MPC bytecode. The interface stores expected inputs, outputs, required accounts, and execution metadata. This data lives in an account with the seeds `b"ComputationDefinitionAccount", mxe_program_id, comp_def_offset`. The first seed is exported by the Arcium Anchor SDK, the second is your MXE program ID, and the third is an encrypted-instruction-specific offset. `comp_def_offset` is `sha256().slice(0,4)` interpreted as a little-endian `u32`. The `derive_comp_def_pda!` macro computes the `ComputationDefinitionAccount` address for you. For onchain circuit sources, the MPC bytecode is stored in accounts with the seeds `b"ComputationDefinitionRaw", comp_def_acc, i`. The first seed is exported by the Arcium Anchor SDK, the second is the computation definition account, and the third is an index from `0` through the number of accounts needed to store the full bytecode. ## Usage When working locally, the Arcium CLI creates and manages MPC bytecode accounts for you. You still need to create the interface `ComputationDefinitionAccount`, which the Arcium Anchor tooling handles. For an encrypted instruction called `add_together`, define: ```rust theme={null} pub fn init_add_together_comp_def(ctx: Context) -> Result<()> { init_computation_def(ctx.accounts, None)?; Ok(()) } #[init_computation_definition_accounts("add_together", payer)] #[derive(Accounts)] pub struct InitAddTogetherCompDef<'info> { #[account(mut)] pub payer: Signer<'info>, #[account( mut, address = derive_mxe_pda!() )] pub mxe_account: Box>, #[account(mut)] /// CHECK: comp_def_account, checked by arcium program. pub comp_def_account: UncheckedAccount<'info>, #[account(mut, address = derive_mxe_lut_pda!(mxe_account.lut_offset_slot))] /// CHECK: address_lookup_table, checked by arcium program. pub address_lookup_table: UncheckedAccount<'info>, #[account(address = LUT_PROGRAM_ID)] /// CHECK: lut_program is the Address Lookup Table program. pub lut_program: UncheckedAccount<'info>, pub arcium_program: Program<'info, Arcium>, pub system_program: Program<'info, System>, } ``` Call this instruction once before using the encrypted instruction. When you no longer need the computation definition, follow the [account lifecycle](/developers/program/account-lifecycle) to deactivate and close it. ## Offchain circuit sources For larger circuits, you can store the compiled circuit offchain and pass an `OffChainCircuitSource` as the second argument to `init_computation_def`: ```rust theme={null} use arcium_client::idl::arcium::types::{CircuitSource, OffChainCircuitSource}; use arcium_macros::circuit_hash; pub fn init_add_together_comp_def(ctx: Context) -> Result<()> { 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 from `build/{circuit_name}.hash` at compile time; Arx nodes verify it when fetching the circuit. For the full offchain workflow, see [Deployment](/developers/deployment#handling-large-circuits-with-offchain-storage). # Release notes Source: https://docs.arcium.com/developers/release-notes Track compatibility, required actions, and developer-facing changes in coordinated Arcium releases from v0.11 onward. Arcium releases coordinate the CLI, Rust crates, TypeScript packages, and Arx node image. Start with [versioning and compatibility](/developers/versioning) to choose an upgrade path. For every upgrade, synchronize all Arcium components, then rebuild and test: each entry below lists additional release-specific actions. **Upgrade impact** * **Existing applications:** No source changes from v0.13.1. Existing initialized MXEs are unaffected. * **New MXE deployments:** Sign initialization with the deployed program's upgrade authority. Immutable programs cannot initialize a new MXE. * **Node Operators:** Update the Arx node image. **Changes** * **Deployment:** Checks the program upgrade authority before queuing MXE initialization. See the [deployment guide](/developers/deployment). * **Arx node:** Reconnects stale subscriptions, applies the RPC slot buffer to computation-account fetches, tightens minimum-context checks, and supports local circuit loading. * **Node services:** Prevents the rewards server from persisting states that cannot be anchored. **Upgrade impact** * **Existing applications:** No source changes from v0.12.x. **Changes** * **Arcis:** Adds all six range-slicing shapes, forward `RangeFrom` iteration, additional unary operations, and bitwise and shift operations on integer types. See [supported operations](/developers/arcis/operations). * **TypeScript:** Adds `@arcium-hq/staking` with staking account readers, PDA helpers, and instruction builders. * **Performance:** Caches Rescue cipher keys between operations. * **Circuit sources:** Reserves local circuit sources for internal MXE workflows. **Upgrade impact** * **Existing applications:** No source changes from v0.11.x. **Changes** * **Arcis:** Adds the `Default` trait and qualified self paths such as `::item`. See [supported operations](/developers/arcis/operations). **Upgrade impact** * **Existing applications:** No source changes from v0.11.1. **Changes** * **Arcis:** Optimizes AES circuits with a Boyar-Peralta S-box implementation. * **MXE initialization:** Validates recovery peers before submission, rejects duplicate peers in the TypeScript client, and initializes recovery-stake state with `initMxePart2`. * **Program:** Corrects the program epoch duration. **Upgrade impact** * **Protocol and accounts:** Existing state remains compatible. * **Existing applications:** Add `skip_init_mxe_part1` to direct calls to `arcium_client::transactions::init_mxe`. * **Staking CLI scripts:** Pass `--amount` and `--new-balance` values in whole ARX instead of base units. **Changes** * **Rust client:** Adds `skip_init_mxe_part1` to `init_mxe` so initialization can resume without repeating the first transaction. * **CLI:** Converts staking amount inputs from whole ARX to base units and corrects the ARX token mint. * **Staking:** Adds primary-stake modification and minimum-stake checks for execution pools. **Upgrade impact** * **Baseline:** v0.11 is the oldest release covered by the maintained compatibility guidance. Existing MXE, Cluster, and computation-definition accounts remained compatible when upgrading from v0.10. Projects still on v0.10 or older should request a project-specific path in [Arcium Discord](https://discord.gg/arcium). * **Applications upgrading from v0.10:** Had to add `callback_cu_limit` to every `queue_computation` call, then rebuild and redeploy. * **Node Operators upgrading from v0.10:** Had to add fallback RPC and computation-limit settings. **Changes** * **Arcis:** Adds enums and `Option`. See [supported types](/developers/arcis/types). * **Callbacks:** Supports additional instructions. * **TypeScript:** Adds computation fee getters to `@arcium-hq/reader`. # Versioning and compatibility Source: https://docs.arcium.com/developers/versioning Understand coordinated Arcium releases, compatibility guarantees, and the supported path from v0.11 or newer. Arcium publishes the CLI, Rust crates, TypeScript packages, and Arx node image as a coordinated release. ## Choose an upgrade path | Project state | Path | | -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | All Arcium components use the latest release | No upgrade required. | | Every component is v0.11 or newer | Start from the oldest component version, review every subsequent [release note](/developers/release-notes), then synchronize all components to the same release. | | Any component is v0.10 or older | Request a project-specific upgrade path in [Arcium Discord](https://discord.gg/arcium). These versions are outside the maintained documentation baseline. | ## Understand compatibility Starting with v0.11, releases are designed to preserve compatibility at the protocol and account level. Network upgrades should not invalidate existing MXEs, Clusters, or computation-definition accounts. Developer-facing APIs can still change. An upgrade might require source changes before you rebuild, even when deployed accounts remain compatible. The [release notes](/developers/release-notes) identify required application and Node Operator actions for each release. | Layer | Compatibility rule | | ------------------------ | --------------------------------------------------------------------------------------------- | | Arcium components | Use the same release across the CLI, crates, TypeScript packages, and Arx node image. | | Protocol and accounts | Releases from v0.11 onward are designed to keep existing deployments and accounts compatible. | | Developer APIs | Rust, TypeScript, CLI, configuration, and Arcis APIs can change between releases. | | Mixed component versions | They might work, but Arcium does not test or guarantee them. | ## Update a project Each component updates through a different mechanism: | Component | Update mechanism | | ---------------------- | ------------------------------- | | CLI and Arx node image | Arcup | | Rust crates | Cargo manifests and lockfile | | TypeScript packages | `package.json` and its lockfile | 1. Check the active CLI and Arcup versions: ```bash theme={null} arcium --version arcup version ``` Also check Arcium versions in your Cargo manifests, JavaScript package manifest, lockfiles, and Arx node image tag. 2. Read the [release notes](/developers/release-notes) from your oldest component version through the latest release. Apply every required API or configuration change in order. 3. Update the components managed by Arcup: ```bash theme={null} arcup self update arcup update ``` 4. Set Arcium Rust crates and TypeScript packages to the same release. Follow the [installation overview](/developers/installation) for current prerequisites. Node Operators should also follow [Node setup](/developers/node-setup) for the current image and configuration. 5. Rebuild and test before deploying: ```bash theme={null} arcium build cargo check --all arcium test ``` # Architecture overview Source: https://docs.arcium.com/getting-started/architecture-overview How MXEs, arxOS, Arcis, Clusters, and Solana coordinate encrypted computations The Arcium Network uses Multi-Party Computation (MPC) to execute computations on encrypted data across a Cluster of Arx nodes. Arcium Network Architecture 1. **MPC eXecution Environments (MXEs)** [**MXEs**](/multi-party-execution-environments-mxes/overview) group a Solana program, computation definitions, key material, and the Cluster that executes their computations. Each MXE is bound to one Cluster at a time. Its authority can migrate it to another Cluster through key recovery. Each Cluster maintains its own onchain mempool. 2. **arxOS** arxOS is the distributed, encrypted operating system powering Arx nodes and Clusters, responsible for coordinating and executing computations within the Arcium Network. Each node (like a core in a computer) provides computational resources to execute computations defined by MXEs. 3. **Arcis (Arcium's Developer Framework)** **Arcis** is the Rust framework and compiler for writing circuits that run with Arcium's supported MPC protocol. 4. **Clusters of Arx nodes (running arxOS)** [Clusters](/clusters/overview) currently run [Cerberus](/multi-party-execution-environments-mxes/mpc-protocols), a dishonest-majority, detect-and-abort MPC protocol. Clusters of Arx nodes ### Chain-level enforcement Solana stores network configuration, computation accounts, queues, staking state, payments, and callbacks. Arx nodes keep MPC key material and preprocessing data offchain and execute circuits offchain. Read more in [Solana integration: orchestration and execution](/solana-integration-and-multichain-coordination/solana-integration-orchestration-and-execution). Arx nodes stake to activate, join Clusters, receive rewards, and participate in leader selection. See [Staking overview](/staking/overview). # How to use this documentation Source: https://docs.arcium.com/getting-started/how-to-use-this-documentation Recommended reading order and guide to navigating the Arcium documentation This documentation is organized for two main paths: learning how Arcium works and building encrypted applications with Arcium. Recommended reading order: 1. **Start with the basics:** Read [Basic concepts](/introduction/basic-concepts) to understand Multi-Party Computation (MPC), MPC eXecution Environments (MXEs), Arcis, Arx nodes, Clusters, and secret sharing. 2. **Review the architecture:** Read [Architecture overview](/getting-started/architecture-overview) to see how MXEs, arxOS, Arcis, and Clusters fit together. 3. **Explore the network sections:** Use these pages when you need more detail: 1. [MXEs](/multi-party-execution-environments-mxes/overview): computation handling, execution workflows, and security. 2. [Clusters](/clusters/overview): node grouping, keyshare distribution, and scalability. 3. [Arx nodes](/arx-nodes/overview): node management, security protocols, and incentives. 4. [Computations](/computations/computation-tasks): computation request structure, computation units, pricing, and fault handling. 4. **Start building:** Go to [Intro to Arcium](/developers) for the developer path, then follow installation and Hello World. As Arcium evolves, this documentation will be regularly updated with new features and network updates. For unanswered questions, reach out to the team on [Discord](https://discord.com/invite/arcium) or [X](https://x.com/arcium). # Network stakeholders Source: https://docs.arcium.com/getting-started/network-stakeholders Roles in the Arcium Network: Computation Customers, Arx Operators, Delegators, and Recovery Peers Four stakeholder roles commission computations, operate infrastructure, delegate stake, and recover MPC eXecution Environment (MXE) keys: * **Computation Customers:** Create [MXEs](/multi-party-execution-environments-mxes/overview), define computations, and pay execution charges. * **Arx Operators:** Run [Arx nodes](/arx-nodes/overview) that execute encrypted computations in Clusters. * **Third-Party Delegators:** Delegate stake to Arx nodes and receive a share of their rewards. * **Recovery Peers:** Support key recovery for a specific MXE and receive a separate stake-weighted reward share. ## Computation customers Computation Customers integrate Arcium into a Solana program and commission computations: * **Create an MPC eXecution Environment (MXE):** Bind the MXE to a Cluster and define how its Solana program uses computation results. * **Define computations:** Compile Arcis circuits and register their source and CU amount. * **Commission execution:** Provide inputs, callbacks, and an optional priority price. Higher priority improves queue position; applications handle expiry through their lifecycle logic. Persistent state used by the onchain integration lives in Solana accounts. The Arcium program coordinates computation accounts and callbacks, while encrypted execution occurs offchain. ## Arx operators **Arx Operators** run Arx nodes that execute computations with other members of a Cluster. Their responsibilities include: * **Register the Arx node:** Declare its identity and CU capacity. Stake requirements govern activation and Cluster eligibility. * **Stake and join a Cluster:** Meet the activation and Cluster-specific stake requirements, then accept an authority's invitation. * **Protect key material:** Secure the host and the MPC key shares stored by the Arx node. Customers evaluate hosting controls when selecting Cluster members. **Revenue streams:** 1. **Primary stake rewards:** The operator receives the portion attributed to its primary stake. 2. **Delegation fees:** The operator receives its configured percentage fee from delegated rewards. Primary and delegated stake also contribute to the Arx node's leader weight. ## Third-party delegators **Third-Party Delegators** contribute stake to an Arx node. They receive a pro-rata share of its rewards after the Node Operator's fee. Delegators choose operators using offchain information such as operating history and security practices. Program rewards follow stake, while reputation informs delegation decisions. ## Recovery peers **Recovery Peers** participate when an MXE authority initiates key recovery or migration. Each MXE has its own Recovery Peers, separate from the Cluster's Arx nodes. Recovery Peer stake is bound exclusively to the role: the same stake account cannot also back an Arx node. This binding is enforced onchain by the staking program. **Rewards:** When epoch settlement is current, 20% of the computation reward basis accrues to the MXE's Recovery Peer pot. Settlement records each peer's stake-weighted claim. During migration, Recovery Peers help transfer the MXE's key material to its destination Cluster. The MXE authority initiates migration when the application needs to change Clusters. # Overview of the Arcium Network Source: https://docs.arcium.com/index What Arcium is, how encrypted computation works, and key benefits for developers **Computation is broken**. Whoever processes data must access it, exposing it to attacks, exploits, and risky trust assumptions. Traditional cryptography encrypts data at rest and in transit, but data usually has to be exposed during computation. That makes sensitive workloads difficult to build on transparent systems such as public blockchains. Arcium provides **encrypted computation** through the blockchain. It uses Multi-Party Computation (MPC) so applications can compute over encrypted data while keeping inputs hidden from any single node. ## Choose your path Understand MPC eXecution Environments (MXEs) and how encrypted computation works. Set up your environment and write your first encrypted instruction. ## Key benefits Arcium gives applications a way to add encryption without leaving the Solana ecosystem. * **Encrypted inputs and outputs:** Process sensitive data without exposing it to any single node. * **Onchain coordination:** Use Solana for orchestration, finalization, and application integration. * **Developer tooling:** Build encrypted instructions with Arcis, Anchor-compatible Solana programs, and the TypeScript client. Use cases include confidential DeFi, private AI inference, sealed-bid auctions, hidden-information games, and other applications where data must remain confidential during computation. # Basic concepts Source: https://docs.arcium.com/introduction/basic-concepts Core concepts: MPC, MXEs, Arx nodes, Clusters, and secret sharing On this page, we'll provide an overview of concepts core to Arcium, including: 1. **MPC eXecution Environments (MXEs):** Accounts that group a program, computation definitions, key material, and an execution Cluster. 2. **arxOS:** The encrypted, distributed operating system powering Arcium, which lets anyone run encrypted computations. It relies upon other concepts central to Arcium and covered below, including **Arx nodes** and **Clusters.** 3. **Arcis:** The Rust framework and compiler for writing encrypted computations. MXEs, arxOS and Arcis ### Understanding the encrypted supercomputer Arcium describes the network as an "encrypted supercomputer." Arx nodes act as processors, arxOS coordinates their offchain execution, [MXEs](/multi-party-execution-environments-mxes/overview) group application-specific computations, and Arcis provides the developer framework. ### Arcium and Arx nodes The Arcium Network consists of decentralized **Arx nodes** that perform computations on encrypted data. Derived from the Latin word "arx," meaning fortress, each Arx node represents a secure point in the network. However, the true power of Arx nodes comes from their collaboration within the decentralized network. * **Staking:** Arx nodes stake to activate, join Clusters, receive rewards, and participate in leader selection. ### Multi-party computation (MPC) Multi-Party Computation (MPC) is the cryptographic backbone of Arcium. It allows multiple parties to jointly compute functions while keeping their inputs private, preserving data confidentiality throughout the process. * **Secret Sharing** is a key cryptographic method within MPC, used to split data into fragments distributed across Arx nodes. No individual node has access to the full data. ### Trustless execution Cerberus distributes trust across the Cluster. Under the assumption that at least one participating Arx node is honest, it preserves privacy and aborts on detected protocol faults rather than producing a corrupted result. ### Clusters and MXEs (MPC eXecution Environments) **Clusters** are groups of Arx nodes that execute MPC tasks. An **MPC eXecution Environment (MXE)** binds application-specific computation definitions to one active Cluster. Its authority can explicitly migrate it to another Cluster through key recovery. ### Handle faults Cerberus uses a detect-and-abort model. A protocol fault can stop a computation, so applications handle failure, retry, queue expiry, and migration in their lifecycle logic. ### Epochs Timing in the Arcium Network is organized into fixed-duration **Epochs**, which serve as the framework for scheduling computations, distributing rewards, and managing token lock-ups. This consistent timing ensures the efficient and fair execution of tasks across the network. # Key features and use cases Source: https://docs.arcium.com/introduction/key-features-and-use-cases Key capabilities of Arcium's MPC technology and real-world application scenarios Arcium combines encrypted offchain execution with Solana-based coordination. ### Key features The current platform provides: 1. **Encrypted computation:** [MPC eXecution Environments (MXEs)](/multi-party-execution-environments-mxes/overview) run Arcis circuits without reconstructing private inputs on one Arx node. 2. **Detect-and-abort execution:** Cerberus aborts on detected protocol faults rather than producing a corrupted result under its stated trust assumption. 3. **Onchain coordination:** Solana programs manage Cluster configuration, computation queues, staking, charges, rewards, and callbacks. 4. **Developer toolchain:** Arcis, the Arcium CLI, Anchor macros, and TypeScript libraries support Solana application development. Callbacks authenticate the Cluster-signed outcome after the MPC protocol completes. Result integrity follows Cerberus's stated trust model. ### Use cases Potential applications include: 1. **Private DeFi:** Evaluate application logic over encrypted positions, amounts, or strategy inputs. 2. **Sealed-bid auctions:** Compare encrypted bids and reveal only the outcome selected by the application. 3. **Joint analytics:** Compute aggregate results across private datasets without giving one participant all raw inputs. 4. **Private inference:** Evaluate supported model circuits over encrypted inputs. 5. **Institutional workflows:** Process sensitive data across an agreed Cluster without sharing raw inputs with every participant. Practical privacy and performance depend on the circuit, callbacks, surrounding application, and Cluster operations. Teams assess regulatory requirements for the complete application and operating model. # MPC protocols Source: https://docs.arcium.com/multi-party-execution-environments-mxes/mpc-protocols How Cerberus executes encrypted computations and how MPC differs from FHE, TEEs, and ZKPs Multi-Party Computation (MPC) lets multiple participants compute a function without reconstructing the private inputs on one participant. Protocols differ in trust assumptions, fault behavior, communication cost, and supported operations. Arcium's current tooling supports **Cerberus**, a dishonest-majority, detect-and-abort protocol. Cerberus uses secret sharing and authenticated values. Under the assumption that at least one Cluster member is honest, it preserves privacy and aborts when it detects a protocol fault rather than returning a corrupted result. Cerberus uses preprocessing data generated before online execution. This shifts part of the cryptographic work out of the latency-sensitive execution path, but Arx nodes still need sufficient preprocessing material and availability to complete a computation. ## MPC vs. other encryption techniques MPC, fully homomorphic encryption (FHE), Trusted Execution Environments (TEEs), and zero-knowledge proofs (ZKPs) solve different problems. Choose based on the application's trust model and workload rather than treating one technique as universally superior. ### Fully homomorphic encryption (FHE) FHE lets a party evaluate supported functions over ciphertexts. It can reduce interaction during evaluation, but computation cost, key management, and supported operations depend on the scheme and workload. ### Trusted execution environments (TEEs) TEEs isolate computation in hardware-backed environments. They can provide high performance, but their security depends on hardware, firmware, attestation, and operational controls. MPC distributes trust across participants instead of relying on one enclave. ### Zero-knowledge proofs (ZKPs) ZKPs let a prover demonstrate that a statement is true without revealing its private witness. They provide public or designated verification, but do not by themselves let several parties jointly compute over private inputs. ### Secure multi-party computation (MPC) MPC is useful when several parties need to compute over private inputs without giving one party all inputs. It requires interaction among participants, so availability and network communication remain part of the system's liveness assumptions. For a simplified overview, read [ELI5: Multi-Party Computation](https://www.arcium.com/articles/eli5-mpc). # MXE encryption Source: https://docs.arcium.com/multi-party-execution-environments-mxes/mxe-encryption MXE-level encryption protocols, data isolation, and configurable security parameters Encryption of data in the Arcium Network is handled at the MPC eXecution Environment (MXE) level. The Arcium Network lets Computation Customers define encryption protocols for each MXE individually. Each MXE operates independently and does not share state with other MXEs. While external solutions can enable encrypted data persistence across multiple MXEs, Arcium does not provide protocol-level state storage. This ensures that every computation remains isolated, preserving confidentiality and security. Arcium's **configurable encryption** gives customers the flexibility to customize encryption protocols to suit specific use case requirements. When setting up an MXE, users can choose from a range of encryption options, balancing the need for performance with the level of security. For example, * **Lightweight encryption** can be selected for tasks prioritizing speed and efficiency. * **Stronger encryption protocols** are available for handling highly sensitive data requiring enhanced protection. This lets each MXE choose an appropriate security and performance tradeoff. PET Comparison ## Side-channel attack resistance Some confidential execution platforms, such as Trusted Execution Environments (TEEs), rely on hardware to secure data. This approach has exposed them to side-channel attacks, such as [sgx.fail](https://sgx.fail/), which can leak or manipulate data and weaken confidentiality guarantees. In contrast, **Arcium's Cerberus protocol** relies on **computational and information-theoretic security**, ensuring that **even a majority of malicious participants cannot extract or falsify information, provided security assumptions hold**. Some MPC implementations can be susceptible to timing attacks, where attackers infer information from processing-time variations. Arcium mitigates this class of risk with constant-time operations. For implementation details on encrypting data in your application, see the [Encryption overview](/developers/encryption). # MXEs overview Source: https://docs.arcium.com/multi-party-execution-environments-mxes/overview How MXEs group programs, computation definitions, keys, and Clusters for encrypted execution An MPC eXecution Environment (MXE) groups a Solana program, its computation definitions, MPC key material, and the Cluster that executes its computations. Applications can use an MXE for different workload patterns: 1. **One-time computations:** Commission a computation once and discard its transient computation account. 2. **Recurring computations:** Commission the same definition repeatedly with new inputs and persist application state in Solana accounts. Both workload patterns use the same MXE configuration. Computation state is transient; persistent application state lives in Solana accounts used by the MXE's program. Each MXE is bound to one Cluster at a time. Its authority can explicitly initiate [Cluster migration](/clusters/cluster-migration) through key recovery. ### Execution workflow Executing a computation in Arcium's MXEs requires several prerequisites. 1. **Prepare preprocessing data:** Cluster members generate and store the values required by the MPC protocol. 2. **Select the computation:** The program promotes a computation from the Cluster's mempool using leader choice, priority price, and account-lock ordering. 3. **Execute the circuit:** Cluster members run the computation offchain with Cerberus. 4. **Submit the outcome:** The Cluster signs the outcome and invokes the configured callback. Computations that remain queued expire after 180 slots; see [Lifecycle of an Arcium computation](/computations/lifecycle-of-an-arcium-computation). # Multichain expansion Source: https://docs.arcium.com/solana-integration-and-multichain-coordination/multichain-expansion Arcium's roadmap for blockchain-agnostic support beyond Solana Arcium currently operates on Solana. The network is designed so future versions can support additional blockchain ecosystems, but multichain support is still under active research and design. The goal is to let applications on different chains request encrypted computation from the same underlying network without changing the confidentiality model. Solana remains the current coordination layer for live deployments. ## Planned direction Future multichain work is expected to focus on: * **Resource allocation:** Arx nodes should be able to allocate capacity across supported chains based on demand and task priority. * **Cross-chain workflows:** Applications should be able to coordinate workflows that involve multiple chains, while computations remain isolated and verifiable. * **Unified orchestration:** A common orchestration layer should manage computation requests, execution status, and result validation across supported chains. ## Current status This page describes the roadmap, not a production feature. For current deployment and integration instructions, use the [Solana integration guide](/solana-integration-and-multichain-coordination/solana-integration-orchestration-and-execution) and the [deployment guide](/developers/deployment). # Solana integration: orchestration and execution Source: https://docs.arcium.com/solana-integration-and-multichain-coordination/solana-integration-orchestration-and-execution How Arcium uses Solana for onchain coordination, node management, and payment processing The Arcium Network uses Solana to orchestrate and manage computational workflows efficiently. Solana's high throughput and low latency enable Arcium's core processes, such as computation orchestration, node management, and payment systems, to operate onchain. ### Onchain coordination The Arcium Network uses Solana-based programs to coordinate the following key processes: * **Node Management**: Registration, configuration, and scheduling of Arx nodes, ensuring they are ready to process tasks securely. * **Computation Orchestration**: Collecting computations into per-Cluster mempools, assigning tasks to Clusters of Arx nodes, and processing results. * **Network Economics**: Managing charges, rewards, staking, and epoch settlement. Arx nodes execute computations offchain, while Solana coordinates accounts, queues, payments, and callbacks. Each computation is queued in its Cluster's **onchain mempool** and executed by that Cluster's Arx nodes. ### Mempool architecture To efficiently manage computations, **each Cluster maintains its own mempool**, where commissioned computations for that Cluster's MPC eXecution Environments (MXEs) are queued and prioritized for execution. Priority-fee ordering is scoped to the Cluster's mempool, so computations compete only with others queued for the same Cluster. Once a computation enters the mempool: * It awaits execution by the Cluster's Arx nodes. 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). * Queued computations have a 180-slot mempool TTL. If a computation is still `Queued` and has not been promoted for execution after expiry, the payer can reclaim its computation fee. * The Cluster bound to the MXE executes the computation. This keeps queue competition local to the Cluster while combining stake-weighted leader selection with customer-selected priority prices. ### Workflow execution Once queued, computations are: 1. **Collected** in the mempool. 2. **Promoted** for execution by the Cluster bound to the computation's MXE. 3. **Finalized** through the configured success or failure callback. Solana coordinates accounts, queues, payments, and callbacks. Arx nodes execute the MPC computation offchain, and the callback authenticates the Cluster-signed outcome. # Staking overview Source: https://docs.arcium.com/staking/overview Staking mechanics for Arx nodes: activation, delegation, and rewards Staking activates Arx nodes, determines Cluster eligibility, and weights reward distribution and leader selection. ### Operation requirements Three requirements govern an Arx node's participation: 1. **Registration** stores the Arx node's metadata, including its declared CU capacity (`cu_capacity_claim`). Stake requirements govern activation and Cluster eligibility. 2. **Activation** requires the Node Operator to self-delegate at least 1,000 ARX. Additional self-delegation and third-party delegation increase the Arx node's stake. 3. **Cluster eligibility** requires the Arx node's primary stake to meet a floor set by the Cluster's mempool size: | Mempool size | Minimum primary stake | | ------------ | --------------------- | | Tiny | 100 ARX | | Small | 500 ARX | | Medium | 1,000 ARX | | Large | 10,000 ARX | Because activation already requires 1,000 ARX, that is the effective minimum for tiny, small, and medium Clusters. Large Clusters require 10,000 ARX. The program checks these requirements when a Cluster authority invites an Arx node and when the operator reduces its primary stake. ### Delegation flow Once activated, an Arx node can receive additional stake from its Node Operator and third-party delegators. Delegation increases its stake-weighted share of rewards and leader weight. ### Delegation mechanics * **Lockups:** Delegated stake has a minimum lockup of four epochs. Primary stake has a minimum lockup of eight epochs. * **Maturation:** A stake change joins the Arx node's reward and leader totals after two epoch-settlement rotations through the pending-stake queue. * **Rewards:** Each Arx node's share is `pot × node_stake / cluster_total_stake`, where `node_stake = primary_stake + delegated_stake`. * **Recovery Peers:** [Recovery Peers](/getting-started/network-stakeholders#recovery-peers) receive a separate stake-weighted reward share for an MPC eXecution Environment (MXE) they support. * **Delegator payout:** Delegators receive a pro-rata share of their Arx node's rewards after the Node Operator's percentage fee. Epoch settlement records reward claims. Delegators claim accrued rewards to a destination account. A delegator can undelegate after the lockup ends, then withdraw after the removal matures through the settlement queue. ### Epoch-based rewards Rewards are settled by epoch. Withdrawing stake or switching a delegation to another Arx node follows the same maturation process. For computation charges, see [Pricing and incentives](/computations/pricing-and-incentives). ### Fee allocation When epoch settlement is current, computation revenue is allocated as: * **70% Cluster pot:** Distributed stake-weighted to the Cluster's Arx nodes. * **20% Recovery Peer pot:** Distributed stake-weighted to the MXE's [Recovery Peers](/getting-started/network-stakeholders#recovery-peers). * **Protocol fee pool:** Retains the remainder, including integer rounding. 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. Arx nodes and Recovery Peers must settle each epoch to keep their reward pools current and continue accruing their shares. ### Leader weight Leader weights are refreshed during epoch settlement and use each Arx node's primary plus delegated stake. The program applies the D'Hondt method across dispatches. When a valid leader choice is available, it can take one dispatch slot; otherwise priority-fee and account-lock ordering apply. This gives Arx nodes a stake-weighted opportunity to influence execution order without replacing the Cluster's priority-fee market. ### Example workflow To illustrate how the delegation process works: 1. A Node Operator self-delegates stake to activate an Arx node. 2. Third-party delegators contribute additional stake. 3. The Arx node executes computations with its Cluster and accrues a stake-weighted share of the Cluster reward pot. 4. Epoch settlement records claims, which delegators can withdraw to a destination account. **Key points:** * Arx nodes need sufficient stake to activate and join Clusters. * Rewards are distributed stake-weighted across Cluster members and MXE Recovery Peers. * Stake changes have minimum lockups and mature through epoch settlement.