Arcium Docs
arcium.com@ArciumHQ
  • Documentation
  • Developers
  • Intro to Arcium
  • Installation
    • Arcup Version Manager
  • Hello World with Arcium
  • Arcium Computation Lifecycle
  • Encryption
    • Sealing aka re-encryption
  • Arcis
    • Operations
    • Types
    • Input/Output
    • Best practices
  • Invoking a Computation from your Solana program
    • Computation Definition Accounts
    • Callback Accounts
  • JavaScript Client
    • Encrypting inputs
    • Tracking callbacks
  • Callback Server
  • Current Limitations
Powered by GitBook
On this page
  • Hello World
  • Our first encrypted instruction
  • Calling it from Solana
  • Building and testing

Hello World with Arcium

PreviousArcup Version ManagerNextArcium Computation Lifecycle

Last updated 10 days ago

Hello World

The Arcium tooling suite for writing MXEs (MPC eXecution Environments) is built on top of , 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, you can therefore simply run:

arcium init <project-name>

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 we won't repeat it here (for an explanation of the Anchor project structure, see the ). 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 . 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

use arcis_imports::*;

#[encrypted]
mod circuits {
    use arcis_imports::*;

    pub struct InputValues {
        v1: u8,
        v2: u8,
    }

    #[instruction]
    pub fn add_together(input_ctxt: Enc<Shared, InputValues>) -> Enc<Shared, u16> {
        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::*; 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<Shared, InputValues>, which represents the encrypted form of our InputValues struct. The Shared type parameter indicates this is encrypted using a shared secret between the client and the MXE.

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 confidential instruction, let's see how 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:

use anchor_lang::prelude::*;
use arcium_anchor::{
    comp_def_offset, derive_cluster_pda, derive_comp_def_pda, derive_comp_pda, derive_execpool_pda, derive_mempool_pda, derive_mxe_pda, init_comp_def, queue_computation, ARCIUM_CLOCK_ACCOUNT_ADDRESS, ARCIUM_STAKING_POOL_ACCOUNT_ADDRESS, CLUSTER_PDA_SEED, COMP_PDA_SEED,
    COMP_DEF_PDA_SEED, MEMPOOL_PDA_SEED, MXE_PDA_SEED, EXECPOOL_PDA_SEED,
};
use arcium_client::idl::arcium::{
    accounts::{
        ClockAccount, Cluster, ComputationDefinitionAccount, PersistentMXEAccount, StakingPoolAccount
    },
    program::Arcium,
    types::Argument,
    ID_CONST as ARCIUM_PROG_ID,
};
use arcium_macros::{
    arcium_callback, arcium_program, callback_accounts, init_computation_definition_accounts,
    queue_computation_accounts,
};

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<InitAddTogetherCompDef>) -> Result<()> {
        init_comp_def(ctx.accounts, None)?;
        Ok(())
    }

    pub fn add_together(
        ctx: Context<AddTogether>,
        computation_offset: u64,
        ciphertext_0: [u8; 32],
        ciphertext_1: [u8; 32],
        pub_key: [u8; 32],
        nonce: u128,
    ) -> Result<()> {
        let args = vec![
            Argument::EncryptedU8(ciphertext_0),
            Argument::EncryptedU8(ciphertext_1),
            Argument::ArcisPubkey(pub_key),
            Argument::PlaintextU128(nonce),
        ];
        queue_computation(ctx.accounts, computation_offset, args, vec![], None)?;
        Ok(())
    }

    #[arcium_callback(encrypted_ix = "add_together")]
    pub fn add_together_callback(ctx: Context<AddTogetherCallback>, output: ComputationOutputs) -> Result<()> {
        let bytes = if let ComputationOutputs::Bytes(bytes) = output {
            bytes
        } else {
            return Err(ErrorCode::AbortedComputation.into());
        };

        emit!(SumEvent {
            sum: bytes[48..80].try_into().unwrap(),
            nonce: bytes[32..48].try_into().unwrap(),
        });
        Ok(())
    }
}

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 confidential instruction, we generally have three instructions in our solana program:

Building and testing

describe("Hello World", () => {
  // Configure the client to use the local cluster.
  anchor.setProvider(anchor.AnchorProvider.env());
  const program = anchor.workspace.HelloWorld as Program<HelloWorld>;
  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, false);
    console.log(
      "Add together computation definition initialized with signature",
      initATSig
    );

    const privateKey = x25519.utils.randomPrivateKey();
    const publicKey = x25519.getPublicKey(privateKey);
    const mxePublicKey = new Uint8Array([
      // ... add MXE cluster publickey here
    ]);
    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: getComputationAcc(
          program.programId,
          computationOffset
        ),
        clusterAccount: arciumEnv.arciumClusterPubkey,
        mxeAccount: getMXEAccAcc(program.programId),
        mempoolAccount: getMempoolAcc(program.programId),
        executingPool: getExecutingPoolAcc(program.programId),
        compDefAccount: getCompDefAcc(
          program.programId,
          Buffer.from(getCompDefAccOffset("add_together")).readUInt32LE()
        ),
      })
      .rpc({ commitment: "confirmed" });
    console.log("Queue sig is ", queueSig);

    const finalizeSig = await awaitComputationFinalization(
      provider,
      computationOffset,
      program.programId,
      "confirmed"
    );
    console.log("Finalize sig is ", finalizeSig);

    const sumEvent = await sumEventPromise;
    const decrypted = cipher.decrypt([sumEvent.sum], sumEvent.nonce)[0];
    expect(decrypted).to.equal(val1 + val2);
  });
});

Again, here we exclude some helper functions below and the imports at the top for brevity. This test visualizes the general flow of computations throughout Arcium on the client side quite well too:

  • initAddTogetherCompDef: Call the init_add_together_comp_def instruction to initialize the confidential instruction definition. (only need to be called once after the program is deployed)

  • x25519.utils.randomPrivateKey: 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.encrypt: Encrypt the inputs for the confidential 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 confidential instruction.

  • awaitComputationFinalization: Since waiting for an Arcium computation isn't the same as waiting for one Solana transaction (since we need to wait for the MPC cluster to finish the computation and invoke the callback), we wait using this function, which is provided by the Arcium typescript library.

If you would like to deploy your MXE to Solana devnet, you can do so by running the following command

arcium deploy --cluster-offset <one-of-the-offsets-below> --keypair-path <path-to-your-local-keypair> -ud

where cluster-offset is one of 2326510165, 2260723535, 768109697 and keypair-path is just your local keypair which has devnet SOL to deploy the program and initialize MXE account, the -ud flag represents that the deployment should be done on devnet. Once your program is deployed, it is usually a good idea to run the tests by changing the clusterAccount to be fetched using getClusterAcc(cluster_offset) so that the computation definitions are also initialized once.

For the sake of brevity, we don't include the InitAddTogetherCompDef, AddTogether, and AddTogetherCallback account structs here (the generated project will generate these for you). You can read more about them and the invokation of confidential instructions inside solana programs .

init_add_together_comp_def: This is the instruction that initializes the confidential instruction definition. It is used to set up the computation definition and is therefore only called once prior to the first invocation of the confidential instruction. More info on this can be found .

add_together: This is the instruction that invokes the confidential instruction. It takes in the arguments for the confidential instruction and queues it for execution using the Arcium program. More info on this can be found .

add_together_callback: This is the instruction that is called by the MPC cluster when the confidential instruction has finished executing which returns our result. More info on this can be found .

This is due to the general flow of computations throughout Arcium, which you can read more about .

Similar to anchor, we can build the confidential instructions and Solana programs using arcium build. Testing is done using the @arcium-hq/client typescript library (more info on it can be found ) by default and can be run using arcium test (make sure you have installed the npm dependencies prior by running yarn or npm install in your project directory). Let's take a quick look at the default test file as well:

cipher = new RescueCipher(sharedSecret): Initialize the Rescue cipher (the constructor internally performs a HKDF with HMAC based on the Rescue-Prime hash function, you can learn more )

This concludes our introduction of the Arcium tooling suite. Browse our sample projects in the to see Arcium in action. Check out the rest of this documentation to learn more about the different features and capabilities of Arcium. We hope you'll find it useful and look forward to seeing what you build with it!

If you have any questions or feedback, please don't hesitate to reach out to us on !

Anchor
Anchor documentation
Arcis
here
here
here
here
here
here
here
examples repo
Discord