AR.IO LogoAR.IO Documentation

Encrypted Data Storage with Nillion Blind Computing

Store encrypted data permanently on Arweave while controlling access through Nillion's nilDB private storage. This approach ensures your data remains encrypted on Arweave, decryption keys are securely stored in nilDB (Private Storage), and only authorized users can access and decrypt the data.

Arweave provides permanent, immutable storage for encrypted data, while Nillion's nilDB stores decryption keys encrypted and split across multiple nodes. Users can encrypt files using their private keys stored in nilDB, upload the encrypted data to Arweave, and later retrieve and decrypt the data using the same private key—all while maintaining complete control over access permissions.

Prerequisites

Before starting, you'll need to complete Nillion's setup process:

Create Builder and Get Tokens

Create a Test Builder:

  • Visit Nillion Subscription Portal
  • Create a testnet public/private key pair for network access
  • Use two distinct keys: one for network access, one for subscription payments

Get Testnet NIL Tokens:

  • Visit NIL Faucet
  • Fund your account with testnet NIL tokens

Subscribe to nilDB Service

Activate nilDB Subscription:

  • Use your subscription wallet to pay for nilDB service
  • Save your private key in hex format for authentication
  • You'll need this key to access Nillion's Private Storage services

Set Up Development Environment

Create a New Project:

mkdir nillion-arweave-demo
cd nillion-arweave-demo
npm init -y

Install Required Dependencies:

npm install @nillion/secretvaults @nillion/nuc @nillion/blindfold @ardrive/turbo-sdk arweave dotenv @noble/curves consola
npm install --save-dev typescript @types/node ts-node @tsconfig/node20

Set Up TypeScript:

npx tsc --init

Update tsconfig.json:

{
  "extends": "@tsconfig/node20/tsconfig.json",
  "compilerOptions": {
    "outDir": "dist/src",
    "baseUrl": ".",
    "module": "esnext",
    "moduleResolution": "bundler",
    "allowUnusedLabels": false,
    "allowImportingTsExtensions": true,
    "noEmit": true,
    "allowUnreachableCode": false,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedIndexedAccess": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "forceConsistentCasingInFileNames": true,
    "declaration": true,
    "sourceMap": true,
    "resolveJsonModule": true
  },
  "include": ["src"],
  "exclude": ["dist", "bundle", "node_modules"]
}

Configure Environment Variables: Create a .env file with your configuration:

NIL_BUILDER_PRIVATE_KEY=your_hex_private_key_here
NIL_BUILDER_COLLECTION_ID=your_collection_id_here # If it exists, one will be created otherwise
NILCHAIN_URL=http://rpc.testnet.nilchain-rpc-proxy.nilogy.xyz
NILAUTH_URL=https://nilauth.sandbox.app-cluster.sandbox.nilogy.xyz
NILDB_NODES=https://nildb-stg-n1.nillion.network,https://nildb-stg-n2.nillion.network,https://nildb-stg-n3.nillion.network

Understanding the Implementation

Let's build the application step by step, explaining what each part does:

Set Up Project Structure and Imports

First, create the main application file src/index.ts and understand the imports:

import { SecretVaultBuilderClient, SecretVaultUserClient, Uuid } from "@nillion/secretvaults";
import { createWallet, downloadFile, uploadFile } from "./arweave";
import { JWKInterface } from "arweave/node/lib/wallet";
import { logger } from "./logger";
import { Keypair, Command, NucTokenBuilder, Did, NucTokenEnvelope, DelegationBody, InvocationBody } from "@nillion/nuc";
import fs from "fs";
import { createCipheriv, createDecipheriv, createHash, randomBytes } from "crypto";
import { randomUUID } from "node:crypto";
import { SecretKey } from "@nillion/blindfold";
import { bytesToHex } from "@noble/curves/utils";
import "dotenv/config";

What these imports do:

  • Nillion SDKs: @nillion/secretvaults and @nillion/nuc for private storage and access control
  • Arweave: arweave and @ardrive/turbo-sdk for permanent data storage
  • Crypto: Node.js built-in crypto functions for AES-256-GCM encryption
  • Utilities: Helper functions for key conversion and UUID generation

Define Configuration and Types

Set up the application configuration with proper TypeScript types:

export type AppConfig = {
  NILCHAIN_URL: string;
  NILAUTH_URL: string;
  NILDB_NODES: string[];
  NIL_PAYER_PRIVATE_KEY: string;
  NIL_BUILDER_PRIVATE_KEY: string;
  NIL_BUILDER_COLLECTION_ID: string;
};

const config: AppConfig = {
  NILCHAIN_URL: process.env.NILCHAIN_URL || "http://rpc.testnet.nilchain-rpc-proxy.nilogy.xyz",
  NILAUTH_URL: process.env.NILAUTH_URL || "https://nilauth.sandbox.app-cluster.sandbox.nilogy.xyz",
  NILDB_NODES: process.env.NILDB_NODES ? process.env.NILDB_NODES.split(",") : [
    "https://nildb-stg-n1.nillion.network",
    "https://nildb-stg-n2.nillion.network",
    "https://nildb-stg-n3.nillion.network",
  ],
  NIL_BUILDER_PRIVATE_KEY: process.env.NIL_BUILDER_PRIVATE_KEY,
  NIL_BUILDER_COLLECTION_ID: process.env.NIL_BUILDER_COLLECTION_ID,
};

What this does:

  • Type safety: Defines the structure of our configuration object
  • Environment variables: Loads settings from .env file with fallback defaults
  • Network endpoints: Points to Nillion testnet infrastructure
  • Multiple nodes: Uses 3 nilDB nodes for redundancy and security

Create Encryption and Decryption Functions

Implement AES-256-GCM encryption using the user's private key:

// Utility functions
const downloadFile = async (url, location) => {
  try {
    const response = await fetch(url);
    const data = await response.text();
    await fs.promises.writeFile(location, data);
    console.log(`💾 Downloaded file saved to: ${location}`);
  } catch (error) {
    console.error("❌ Download file failed:", error);
  }
};

const encryptContent = (content, encryptionKey) => {
  // Convert hex private key to buffer
  const privateKeyBuffer = Buffer.from(encryptionKey, "hex");
  // Derive a 32-byte encryption key from your private key using SHA-256
  const derivedKey = createHash("sha256").update(privateKeyBuffer).digest();
  // Validate derived key length (should be 32 bytes for AES-256)
  if (derivedKey.length !== 32) {
    throw new Error("Derived encryption key must be exactly 32 bytes for AES-256");
  }
  // Generate secure encryption parameters
  const iv = randomBytes(16);
  // Encrypt using AES-256-GCM
  const cipher = createCipheriv("aes-256-gcm", derivedKey, iv);
  let encrypted = cipher.update(content);
  encrypted = Buffer.concat([encrypted, cipher.final()]);
  // Get the authentication tag (16 bytes for GCM)
  const authTag = cipher.getAuthTag();
  // Combine IV + authTag + encrypted data into a single buffer
  return Buffer.concat([iv, authTag, encrypted]);
};

What this does:

  • Key derivation: Uses SHA-256 to create a 32-byte encryption key from the user's private key
  • AES-256-GCM: Provides both confidentiality and authenticity
  • Random IV: Each encryption uses a unique initialization vector
  • Auth tag: Prevents tampering with the encrypted data
  • Combined format: Stores IV, auth tag, and encrypted data together

Create Decryption Function

Implement the corresponding decryption function:


const decryptContent = (encryptedData, encryptionKey, outputLocation) => {
  // Convert hex private key to buffer
  const privateKeyBuffer = Buffer.from(encryptionKey, "hex");
  // Derive the same 32-byte encryption key using SHA-256
  const derivedKey = createHash("sha256").update(privateKeyBuffer).digest();
  // Validate derived key length
  if (derivedKey.length !== 32) {
    throw new Error("Derived encryption key must be exactly 32 bytes for AES-256");
  }
  // Extract IV, auth tag, and encrypted content
  if (encryptedData.length < 32) {
    throw new Error("Invalid encrypted data: too short");
  }
  const iv = encryptedData.subarray(0, 16);
  const authTag = encryptedData.subarray(16, 32);
  const encrypted = encryptedData.subarray(32);
  // Create decipher
  const decipher = createDecipheriv("aes-256-gcm", derivedKey, iv);
  // Set the authentication tag
  decipher.setAuthTag(authTag);
  // Decrypt the data
  let decrypted = decipher.update(encrypted);
  decrypted = Buffer.concat([decrypted, decipher.final()]);
  console.log("✅ Successfully decrypted!");
  // Save decrypted file
  fs.writeFileSync(outputLocation, decrypted);
  console.log(`💾 Decrypted file saved to: ${outputLocation}`);
  return decrypted;
};

What this does:

  • Recreates the key: Uses the same SHA-256 derivation process
  • Extracts components: Separates IV, auth tag, and encrypted data
  • Validates integrity: The auth tag ensures data hasn't been tampered with
  • Decrypts safely: Only works with the correct private key
  • Saves result: Writes the decrypted data to a file

Create Token Generation Function

Implement delegation token creation for secure access control:

const generateToken = async (parentToken, command, audience, tokenExpirySeconds, privateKey, body) => {
  const token = NucTokenBuilder.extending(parentToken)
    .command(command)
    .audience(audience)
    .expiresAt(Math.floor(Date.now() / 1000) + tokenExpirySeconds);
  if (body) token.body(body);
  return token.build(privateKey);
};

What this does:

  • Delegation tokens: Creates time-limited access tokens for users
  • Command-based: Specifies exactly what operations the token allows
  • Audience-specific: Only works for the intended user
  • Time-limited: Automatically expires after the specified duration
  • Cryptographically signed: Uses the builder's private key for authenticity

Create Utility Files

Create supporting utility files for Arweave operations and logging:

Create src/arweave.ts:

import Arweave from 'arweave';
import { JWKInterface } from 'arweave/node/lib/wallet';
import { logger } from './logger';
import { TurboFactory, ArweaveSigner } from "@ardrive/turbo-sdk";

const ARWEAVE_HOST = "arweave.net";

export async function createWallet() {
  try {
    const arweave = Arweave.init({
      host: ARWEAVE_HOST,
      port: 1984,
      protocol: 'http',
      timeout: 20000,
      logging: true,
    });

    // Generate a new wallet
    const wallet = await arweave.wallets.generate();
    // Get the wallet address
    const address = await arweave.wallets.jwkToAddress(wallet);
    return { wallet, address };
  } catch (error) {
    logger.error('Error creating wallet:', error);
  }
}

export const uploadFile = async (data: string, wallet: JWKInterface) => {
  try {
    const signer = new ArweaveSigner(wallet);
    const turbo = TurboFactory.authenticated({ signer });
    const result = await turbo.upload({
      data,
      dataItemOpts: {
        tags: [
          { name: "Content-Type", value: "text/plain" },
          { name: "Title", value: "My demo file" },
        ],
      },
    });
    return result;
  } catch (error) {
    logger.error('Error uploading securely encrypted file:', error);
  }
}

Create src/logger.ts:

import { createConsola } from 'consola'

export const logger = createConsola({})

What these do:

  • Arweave utilities: Handle wallet creation and file uploads using Turbo
  • Consola logging: Provides a beautiful, consistent logging experience with colors and formatting
  • Error handling: Gracefully handle and log errors

Implement the Main Workflow

Now let's implement the main application logic that ties everything together:

async function main() {
  try {
    let collectionId;
    const dataId = randomUUID();

    // Builder Client
    const builderKeypair = Keypair.from(config.NIL_BUILDER_PRIVATE_KEY);
    const builder = await SecretVaultBuilderClient.from({
      keypair: builderKeypair,
      urls: {
        chain: config.NILCHAIN_URL,
        auth: config.NILAUTH_URL,
        dbs: config.NILDB_NODES,
      },
      blindfold: { operation: "store" },
    });
    await builder.refreshRootToken();

    // Check if builder is set up correctly
    const existingProfile = await builder.readProfile();

What this does:

  • Builder initialization: Creates a builder client with your private key
  • Network connection: Connects to Nillion testnet infrastructure
  • Token refresh: Gets a fresh authentication token
  • Profile validation: Ensures the builder is properly set up

Set Up Data Collection

Create or validate the collection for storing user private keys:


    // Create the Owned Collection if it doesn't exist
    if (!config.NIL_BUILDER_COLLECTION_ID) {
      collectionId = randomUUID();
      const schema = {
        "$schema": "http://json-schema.org/draft-07/schema#",
        type: "array",
        items: {
          type: "object",
          properties: {
            _id: { "type": "string", "format": "uuid" },
            private_key: {
              "type": "object",
              "properties": { "%share": { "type": "string" } },
              required: ["%share"]
            }
          },
          required: ["_id", "private_key"]
        }
      };
      const collection = {
        _id: collectionId,
        type: "owned",
        name: "Nillion / Arweave Demo App User Profiles",
        schema,
      };
      const collectionResult = await builder.createCollection(collection);
      console.log(`✅ Created Owned Collection with ID: ${collectionId}`);
      console.log(`Make sure to update your .env file with this NIL_BUILDER_COLLECTION_ID to proceed.`);
      process.exit(0);
    } else {
      if (existingProfile.data.collections.indexOf(config.NIL_BUILDER_COLLECTION_ID) === -1) {
        throw new Error(`Builder does not have collection ${config.NIL_BUILDER_COLLECTION_ID} registered. Please check your .env configuration.`);
      } else {
        console.log(`✅ Builder is set up correctly with DID: ${builder.did}`);
      console.log(`✅ Using Owned Collection ID: ${config.NIL_BUILDER_COLLECTION_ID}`);
      collectionId = config.NIL_BUILDER_COLLECTION_ID;
    }
  }

What this does:

  • Collection creation: Creates a new collection if none exists
  • Schema definition: Defines the structure for storing user private keys
  • Validation: Ensures the builder has access to the specified collection
  • Environment setup: Prompts you to update your .env file with the collection ID

Create User and Store Private Key

Generate a new user and securely store their private key in nilDB:

  // Create user and store its private key in nilDB
    const secretKey = await SecretKey.generate(
      { nodes: config.NILDB_NODES.map(url => ({ url })) },
      { store: true }
    );
    const userKeypair = Keypair.from(bytesToHex(secretKey.material));
    const userDid = userKeypair.toDid().toString();
    const user = await SecretVaultUserClient.from({
      baseUrls: config.NILDB_NODES,
      keypair: userKeypair,
      blindfold: { operation: "store" }
    });

    // Grant write access to the user
    const delegationToken = await generateToken(
      builder.rootToken,
      new Command(["nil", "db", "data", "create"]),
      userKeypair.toDid(),
      3600, // 1 hour
      builder.keypair.privateKey()
    );
    console.log(`🗝️ Delegation token created`);

    // User creates profile with Private Key
    await user.createData(delegationToken, {
      owner: userDid,
      acl: {
        grantee: builder.did.toString(),
        read: false,
        write: false,
        execute: true,
      },
      collection: collectionId,
      data: [
        {
          _id: dataId,
          private_key: { "%allot": userKeypair.privateKey() },
        }
      ],
    });
    console.log(`✅ User profile created: ${userDid}`);

What this does:

  • Secret key generation: Creates a cryptographically secure private key
  • Keypair creation: Converts the secret key to a usable keypair
  • User client: Initializes a user client for nilDB operations
  • Delegation token: Grants the user permission to create data
  • ACL setup: Defines access control - builder can execute queries but not read/write
  • Data storage: Stores the user's private key in the nilDB collection

Encrypt and Upload File

Now let's encrypt a file and upload it to Arweave:

    const wallet = await createWallet();
    console.log(`💼 Arweave wallet created: ${wallet?.address}`);

    // Retrieve the user private key from nilDB
    const retrievedUserKey = await user.readData({
      collection: collectionId,
      document: dataId,
    });

    // Encrypt file contents
    const fileData = fs.readFileSync("test/demo.txt");
    const encrypted = encryptContent(fileData, retrievedUserKey.data.private_key);
    const upload = await uploadFile(encrypted, wallet?.wallet);
    console.log(`✅ File uploaded to Arweave with txId: ${upload.id}`);

    // Download the file and decrypt it
    const downloadFileName = `./test/encrypted_demo_${Date.now()}.txt`;
    console.log(`🕒 Downloading file from Arweave`);
    await downloadFile(`https://arweave.net/${upload.id}`, downloadFileName);
    const decrypted = decryptContent(encrypted, retrievedUserKey.data.private_key, `./test/decrypted_demo_${Date.now()}.txt`);

What this does:

  • Arweave wallet: Creates a new wallet for uploading files
  • Key retrieval: Gets the user's private key from nilDB
  • File encryption: Encrypts the file using the user's private key
  • Arweave upload: Uploads the encrypted file to permanent storage
  • File download: Downloads the file from Arweave
  • Decryption: Decrypts the file using the same private key

Complete the Application

Finish the main function with error handling:

  } catch (error) {
    console.log(error);
    console.error("⚠️ Error setting up builder:", JSON.stringify(error, null, 2));
  }
}

main().catch(console.error);

What this does:

  • Error handling: Catches and logs any errors that occur during execution
  • Application startup: Runs the main function when the script is executed

Create Test File

Before running the demo, create a test file to encrypt:

mkdir test
echo "Hello, this is a test file for Nillion + Arweave encryption demo!" > test/demo.txt

Understanding the Complete Workflow

The main application performs the following steps:

  1. Initialize Builder: Sets up the Nillion Builder client with your private key
  2. Create/Verify Collection: Creates a new collection or uses an existing one for storing private keys
  3. Generate User: Creates a new user with a private key stored securely in nilDB
  4. Create Wallet: Generates an Arweave wallet for uploading data
  5. Encrypt Data: Encrypts the test file using the user's private key
  6. Upload to Arweave: Uploads the encrypted data to Arweave using Turbo
  7. Download & Decrypt: Downloads the encrypted file and decrypts it using the stored private key

Key Security Features:

  • Private keys are stored securely in nilDB with proper access control
  • Data is encrypted using AES-256-GCM with the user's private key
  • Only authorized users can access the decryption keys
  • Encrypted data is permanently stored on Arweave

Running the Complete Demo

Add a start script to your package.json:

{
  "scripts": {
    "start": "ts-node src/index.ts",
    "build": "tsc",
    "dev": "ts-node --watch src/index.ts"
  }
}

Then run the complete workflow:

# Run the complete workflow
npm start

This will execute the entire process:

  1. Initialize the Builder and create a collection
  2. Generate a user and store their private key in nilDB
  3. Encrypt a sample file using the private key
  4. Upload the encrypted file to Arweave
  5. Download the encrypted file from Arweave
  6. Decrypt the file using the stored private key

nilCC

To securely compute the logic, we could take advantage of nilCC, Nillion's Confidential Computing product, that allows you to run application logic inside a TEE.

This way, all the interaction could happen inside the confidential environment with no risk of sensitive information leakage.

nilCC workloads can be easily triggered via its REST API:

curl --location '{endpoint}/api/v1/workloads/create' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--header 'x-api-key: xxx' \
--data '{
          "name": "private-stamement-workload",
          "artifactsVersion": "0.1.2",
          "dockerCompose": "services:\n  private-statements:\n    image: my/workload-logic:v0.2\n    environment:\n      - DELEGATION_TOKENS=${DELEGATION_TOKENS}\n      - COLLECTION_ID=${COLLECTION_ID}\n      - DOCUMENT_ID=${DOCUMENT_ID}\n      - NODE_ENV=production\n    build: .\n    ports:\n      - \"8080:8080\"",
          "envVars": {
            "DOCUMENT_ID": "b05917e6-996c-4e90-a49f-b62fa891da1b",
            "COLLECTION_ID": "ce9b1d1c-8006-4053-a0c8-f46ad711fc26",
            "DELEGATION_TOKENS": "W3sidXJsIjoiaHR0cHM6Ly9uaWxkYi1zdGctbjEubmlsbGlvbi5uZXR3b3JrIiwidG9rZW4iOiJleUpoYkdjaU9pSkZVekkxTmtzaWZRLmV5SnBjM01pT2lKa2FXUTZibWxzT2pBeU5qTmlZMkpsTjJVeU5UZGhNamhrTmpjMk5EWTVNemc0WlRnd05EWmxNelEwTW1JeU5UVm1Zakk0TWpZME9EbGhOalE0TW1ZMk9ESmhNRFpsWWpreU1TSXNJbUYxWkNJNkltUnBaRHB1YVd3Nk1ESmxNemcwTm1NME5UVmtZbU5sWldZNVpXWm1PR0U0TkRFeU4yTXpZbVV4WWprM01UbGhZekExTkRFMVpXWmlaamN5Tnprd1pqTXhabUU1Wmpnd01qZGhJaXdpYzNWaUlqb2laR2xrT201cGJEb3dNall6WW1OaVpUZGxNalUzWVRJNFpEWTNOalEyT1RNNE9HVTRNRFEyWlRNME5ESmlNalUxWm1JeU9ESTJORGc1WVRZME9ESm1Oamd5WVRBMlpXSTVNakVpTENKbGVIQWlPakUzTlRrME56Y3hPRGtzSW1OdFpDSTZJaTl1YVd3dlpHSXZkWE5sY25NdmNtVmhaQ0lzSW1GeVozTWlPbnQ5TENKdWIyNWpaU0k2SWpRMVltUTFPVGxsTmpSbU4yVXdaVEZoWWpnNFpqVXdPVEV5TURRME5UWTFJbjAuQ05lMXlISFlUT3cyZW5lYnh4Nm56VWJaQnpJTzdKVmhYZTBMQVo4aTI3TmJzR0JHMHdBbVVGV2VKbG9oc0FxNHBXREVacGQyamtQaXNGMFZCakNHM0EiLCJwdWJsaWNLZXkiOiIwMmUzODQ2YzQ1NWRiY2VlZjllZmY4YTg0MTI3YzNiZTFiOTcxOWFjMDU0MTVlZmJmNzI3OTBmMzFmYTlmODAyN2EifSx7InVybCI6Imh0dHBzOi8vbmlsZGItc3RnLW4yLm5pbGxpb24ubmV0d29yayIsInRva2VuIjoiZXlKaGJHY2lPaUpGVXpJMU5rc2lmUS5leUpwYzNNaU9pSmthV1E2Ym1sc09qQXlOak5pWTJKbE4yVXlOVGRoTWpoa05qYzJORFk1TXpnNFpUZ3dORFpsTXpRME1tSXlOVFZtWWpJNE1qWTBPRGxoTmpRNE1tWTJPREpoTURabFlqa3lNU0lzSW1GMVpDSTZJbVJwWkRwdWFXdzZNREkxTnpreVpUazJZVFk0WXpCaU4yVm1OemM1TkRrMk1ETXlOMlJqTlRjd056QTBZelprWkRVMk5XTm1NbU5oWTJZeU1EWmlaR00zTW1RMk1USXpaamt3SWl3aWMzVmlJam9pWkdsa09tNXBiRG93TWpZelltTmlaVGRsTWpVM1lUSTRaRFkzTmpRMk9UTTRPR1U0TURRMlpUTTBOREppTWpVMVptSXlPREkyTkRnNVlUWTBPREptTmpneVlUQTJaV0k1TWpFaUxDSmxlSEFpT2pFM05UazBOemN4T0Rrc0ltTnRaQ0k2SWk5dWFXd3ZaR0l2ZFhObGNuTXZjbVZoWkNJc0ltRnlaM01pT250OUxDSnViMjVqWlNJNklqVmhNMlV4WW1aaU9ETm1OVGN5WkdVeE5tRTBOV0U1TlRsaFpETXpPREZrSW4wLjE5Nk9uVWZYT3ZnNDVnbHNUaThkZ09OWkc2R3E2NXMxVlBFa01La1huVEZCUlFmamV1R1JzTkVHUnJYci1obmp1Z1BIeWlpWmJiT0JzMU9ndkYzS053IiwicHVibGljS2V5IjoiMDI1NzkyZTk2YTY4YzBiN2VmNzc5NDk2MDMyN2RjNTcwNzA0YzZkZDU2NWNmMmNhY2YyMDZiZGM3MmQ2MTIzZjkwIn0seyJ1cmwiOiJodHRwczovL25pbGRiLXN0Zy1uMy5uaWxsaW9uLm5ldHdvcmsiLCJ0b2tlbiI6ImV5SmhiR2NpT2lKRlV6STFOa3NpZlEuZXlKcGMzTWlPaUprYVdRNmJtbHNPakF5TmpOaVkySmxOMlV5TlRkaE1qaGtOamMyTkRZNU16ZzRaVGd3TkRabE16UTBNbUl5TlRWbVlqSTRNalkwT0RsaE5qUTRNbVkyT0RKaE1EWmxZamt5TVNJc0ltRjFaQ0k2SW1ScFpEcHVhV3c2TURNd05EQXdNVFU1TW1NelpESmhOR0ZtTkdaa01EUTVaamMxWVRVMk1qTmxNVEE1TXpsaU16ZGpNemhqWXpZMFl6STJORGd3TVdFMU5UWTNZalE1TTJGaUlpd2ljM1ZpSWpvaVpHbGtPbTVwYkRvd01qWXpZbU5pWlRkbE1qVTNZVEk0WkRZM05qUTJPVE00T0dVNE1EUTJaVE0wTkRKaU1qVTFabUl5T0RJMk5EZzVZVFkwT0RKbU5qZ3lZVEEyWldJNU1qRWlMQ0psZUhBaU9qRTNOVGswTnpjeE9Ea3NJbU50WkNJNklpOXVhV3d2WkdJdmRYTmxjbk12Y21WaFpDSXNJbUZ5WjNNaU9udDlMQ0p1YjI1alpTSTZJak5rTXpKaVlXSmhaV1ZsTkRjNU5qY3dNbUUyWW1aa016RTVNV013TURaa0luMC5qYTlzQWpJRUFJb09FM2ZmcnIwV190aXlLNWZWWVhQbVdNWjIxbHlhb0d3eUZoMzQycnFhNldUSHhkT3dVSHVXdVhRR0FCOEZTQnhRWks4NXFLbGQtUSIsInB1YmxpY0tleSI6IjAzMDQwMDE1OTJjM2QyYTRhZjRmZDA0OWY3NWE1NjIzZTEwOTM5YjM3YzM4Y2M2NGMyNjQ4MDFhNTU2N2I0OTNhYiJ9XQ=="
          },
          "publicContainerName": "my-workload",
          "publicContainerPort": 8080,
          "memory": 1024,
          "cpus": 1,
          "disk": 10,
          "gpus": 0,
          "workloadId": "88384328-3038-4a8e-8d45-bbebf6d748d4",
          "creditRate": 1,
          "status": "scheduled",
          "accountId": "some-account"
}'

or the nilCC Workload Manager.

Full instructions can be found here

Additional Resources

For reference and additional examples, you can also check out the official demo repository:

Nillion Arweave Demo

This repository contains the complete working implementation that this guide is based on.

Next Steps

Need Help?

If you're interested in implementing encrypted data storage with Nillion, join our Discord community or explore the Nillion developer resources for detailed implementation guides.

How is this guide?