Skip to content

aptos-labs/ace

Repository files navigation

ACE

ACE is a protocol for access-controlling encrypted data with smart contracts.

⚠️ Prototype: ACE is currently a prototype and not yet ready for production use.

With ACE, dApps can support privacy scenarios like these — without any single party holding a decryption key:

  • Pay-to-decrypt: Alice sells her album as encrypted files; Bob can only decrypt after paying.
  • Time-locked release: A journalist encrypts a story that auto-releases on January 1, 2027. Until then, no one can decrypt it.
  • ZK-gated access: A DeFi protocol gates content to users who can prove age ≥ 18 via a zero-knowledge proof, without revealing the actual age.
  • The general pattern: Encrypt now; let a contract decide who can decrypt later.

This monorepo provides a TypeScript SDK, worker binary, operator CLI, and examples for Aptos and Solana.

Design Overview

Roles

Role Responsibility
App developer Deploys an access-control contract; encrypts data; integrates the SDK for user-side decryption
End user Satisfies the access condition (pays, passes KYC, etc.); requests decryption
Operator Runs a worker node that holds a share of the decryption key

Trust Assumptions

  • Threshold honest majority: decryption requires t-of-n worker shares. No single worker — and no coalition smaller than t — can decrypt alone or learn the plaintext.
  • Contract is truth: workers trust the on-chain view function unconditionally. If it returns true, they release their share. The security of the system reduces to the correctness of your contract and the integrity of the chain.
  • Workers run their own fullnodes (in production): workers that rely on a shared RPC endpoint inherit the trust assumptions of that provider.

Interaction Flow

App developer                 End user                      Operators (n workers)
─────────────────────         ─────────────────────         ─────────────────────
(1) Deploy access-control
    contract on-chain

(2) Encrypt plaintext
    → publish ciphertext
                              (3) Satisfy access condition
                                  (pay, prove identity, …)

                              (4) Submit decryption request
                                  (signature or ZK proof)
                                                            (5) Each worker simulates
                                                                check_permission /
                                                                check_acl on-chain;
                                                                if true, returns an
                                                                encrypted key share

                              (6) SDK collects ≥ t shares,
                                  reconstructs key, decrypts

Steps 1–2 happen once per piece of content. Steps 3–6 happen each time a user decrypts.


Project Structure

Package Description
docs Protocol specifications (cryptography, trust model, protocols, wire formats) — start here for audit
ts-sdk TypeScript SDK (@aptos-labs/ace-sdk)
cli Operator CLI (ace) for node onboarding and management
worker-components Rust worker binaries (HTTP server, DKG/DKR participants)
scenarios Local network setup scripts
examples/tutorial-aptos Step-by-step ACE walkthrough on Aptos testnet — start here
examples/shelby-explorer-acl-aptos ACE ACL module from Shelby Explorer (allowlist / time-lock / pay-to-download)
examples/pay-to-download-solana Pay-to-download example on Solana
examples/zk-kyc Age-gated decryption with Groth16 ZK proofs

App Developer Guide

Your job as an app developer is to deploy a Move contract with a single #[view] function that decides whether a given decryption request is allowed. ACE calls that function on-chain; if it returns true, the key is released. You also use the TypeScript SDK to encrypt and decrypt.

ACE supports two flows depending on what your contract needs to verify:

  • Basic Flow — your contract receives the requestor's Aptos address (extracted from their signature). Good for allowlists, time-locks, and pay-to-download.
  • Custom Flow — your contract receives an arbitrary payload byte string submitted by the requestor. Good for ZK proofs, Merkle witnesses, and other cryptographic credentials.

Account-Type Compatibility (Aptos)

ACE's basic flow supports every cryptographic signature scheme the upstream @aptos-labs/ts-sdk produces — legacy Ed25519, SingleKey (Ed25519, Secp256k1, Secp256r1+WebAuthn passkeys, Keyless, FederatedKeyless), legacy MultiEd25519, and modern MultiKey K-of-N. If your users hold any of these, point them at the basic flow.

Aptos's Account Abstraction (AA, Scheme::Abstraction = 4) and Derivable AA (DAA, Scheme::DeriveDomainAbstraction = 5) authenticate via a Move authenticate(signer, AbstractionAuthData): signer function rather than a cryptographic primitive — they have no "public key" the SDK can plug into a basic-flow (pk_scheme, sig_scheme) pair. AA-authenticated dapps should route through the custom flow: your check_acl view plays the same role as authenticate and can re-run the same credential check on the payload bytes (after binding payload to the request via label / enc_pk).

The Contract Interface

This is the most important part. Your view function is the sole access gate — get the signature right.

Basic Flow — fixed signature, three parameters:

#[view]
public fun check_permission(
    label: vector<u8>,     // the domain the ciphertext was encrypted under
    enc_pk: vector<u8>,    // requestor's ephemeral public key for this session
    user_addr: address,    // Aptos address that signed the decryption request
): bool
  • label identifies what is being decrypted — it equals the domain bytes you passed to encrypt. Use it to look up your access records.
  • user_addr is who is asking. Check your payment table, allowlist, etc. against this.
  • enc_pk is the requestor's session key. Most contracts ignore it; it is there if you need to bind external proofs to a specific session (see Custom Flow).

Custom Flow — fixed signature, three parameters:

#[view]
public fun check_acl(
    label: vector<u8>,     // the domain the ciphertext was encrypted under
    enc_pk: vector<u8>,    // requestor's ephemeral public key for this session
    payload: vector<u8>,   // arbitrary bytes submitted by the requestor
): bool
  • label and enc_pk are the same as above.
  • payload is whatever the requestor sends — a Groth16 proof, a Merkle proof, a signed attestation, etc. Your contract is fully responsible for deserializing and verifying it. A ZK proof should bind to enc_pk so that a captured proof cannot be replayed by a different requestor.

The function name can be anything — you pass it to the SDK at encrypt/decrypt time.

SDK Usage

import * as ACE from "@aptos-labs/ace-sdk";
import { AccountAddress } from "@aptos-labs/ts-sdk";

const aceDeployment = new ACE.AceDeployment({
    apiEndpoint: "https://fullnode.mainnet.aptoslabs.com/v1",
    contractAddr: AccountAddress.fromString("0x<ace-contract-addr>"),
});

For testnet, the SDK ships a registry of known deployments — skip the manual setup:

const { aceDeployment, keypairId, chainId } = ACE.knownDeployments.preview20260506;

Encrypt (both flows)

const ciphertext = (await ACE.AptosBasicFlow.encrypt({   // or AptosCustomFlow.encrypt
    aceDeployment,
    keypairId: AccountAddress.fromString("0x<keypair-id>"),
    chainId: 1,
    moduleAddr: AccountAddress.fromString("0xcafe"),
    moduleName: "album_store",
    functionName: "check_permission",
    domain: new TextEncoder().encode("album-001"),        // becomes `label` in your contract
    plaintext: albumData,
})).unwrapOrThrow("encryption failed");

Decrypt — Basic Flow

The requestor signs a challenge message that proves their identity:

const session = ACE.AptosBasicFlow.DecryptionSession.create({
    aceDeployment,
    keypairId: AccountAddress.fromString("0x<keypair-id>"),
    chainId: 1,
    moduleAddr: AccountAddress.fromString("0xcafe"),
    moduleName: "album_store",
    functionName: "check_permission",
    domain: new TextEncoder().encode("album-001"),
    ciphertext,
});

const messageToSign = await session.getRequestToSign();
const plaintext = (await session.decryptWithProof({
    userAddr: bob.accountAddress,
    publicKey: bob.publicKey,
    signature: bob.sign(messageToSign),
})).unwrapOrThrow("decryption failed");

Decrypt — Custom Flow

The requestor builds a payload (e.g., a ZK proof) and supplies an ephemeral keypair that the payload should be bound to:

const { encryptionKey, decryptionKey } = ACE.pke.keygen();
const encPk = new Uint8Array(encryptionKey.toBytes());
const encSk = new Uint8Array(decryptionKey.toBytes());

const payload: Uint8Array = buildMyPayload(encPk, ...); // bind proof to encPk

const plaintext = await ACE.AptosCustomFlow.decrypt({
    ciphertext,
    label: new TextEncoder().encode("my-label"),
    encPk,
    encSk,
    payload,
    aceDeployment,
    keypairId: AccountAddress.fromString("0x<keypair-id>"),
    chainId: 1,
    moduleAddr: AccountAddress.fromString("0xcafe"),
    moduleName: "my_verifier",
    functionName: "check_acl",
});

Local Development

Start a full ACE network locally (3 workers + Aptos localnet):

cd scenarios
pnpm install
pnpm run-local-network-forever

Wait for the ACE local network is READY banner. The network writes contractAddr and keypairId to /tmp/ace-localnet-config.json.


Operator Guide

Joining the ACE network requires coordination with the admin (who controls the ACE contract) and the existing committee (who votes to admit you).

Operator                              Admin / existing committee
────────────────────────────────      ─────────────────────────────────
                                      (1) Admin shares a deployment blob:
                                          { rpcUrl, aceAddr, rpcApiKey?,
                                            gasStationKey? }

(2) `pnpm dev node new` — paste blob;
    wizard generates keys, prints
    a docker/gcloud command to
    start the worker, registers
    on-chain

(3) Share account address with admin

                                      (4) `pnpm dev proposal new` — proposes
                                          adding the new node to the
                                          committee

                                      (5) Each committee member:
                                          `pnpm dev proposal review`
                                          until threshold is reached

(6) Node joins the committee and
    participates in the next DKG

Install

The CLI isn't on npm yet. Clone the repo and install dependencies:

git clone git@github.com:aptos-labs/ace.git
cd ace
pnpm install

All CLI commands below run as pnpm dev <subcommand> from the cli/ directory:

cd cli
pnpm dev <subcommand>

To update later: git pull && pnpm install.

Onboard a new node

pnpm dev node new

The guided wizard asks for the deployment blob from the admin, generates node keys, prints the docker run or gcloud run deploy command to start the worker, and registers your node on-chain. At the end it prints your account address — send this to the admin.

Useful commands

pnpm dev network-status [-w]              # committee, epoch, active proposals, contract version
pnpm dev node status    [-w]              # your node's registration and key state
pnpm dev proposal new                     # propose a committee change (committee members or admin)
pnpm dev proposal review [-s <session>]   # review and vote on a proposal (interactive TUI)
pnpm dev node edit                        # update image, API key, or gas station key
pnpm dev node log [--since <t>] [--until <t>] [-w]   # stream or query node logs
pnpm dev node ls                          # list saved node profiles
pnpm dev node delete <alias>              # delete a saved node profile
pnpm dev node default <alias>             # set the default node profile

# Admin (deployment) side
pnpm dev deployment new                   # full deployment wizard (requires tagged clean commit)
pnpm dev deployment update-contracts      # republish all 11 packages at NEXT_RELEASE version
pnpm dev deployment edit                  # edit RPC URL, API keys, alias of a deployment profile
pnpm dev deployment ls                    # list deployment profiles
pnpm dev deployment delete <alias>        # delete a deployment profile (local-only)
pnpm dev deployment default <alias>       # set the default deployment profile

Fullnodes (optional for testing, recommended for production)

⚠️ Workers that rely on a shared RPC endpoint inherit its trust assumptions — a malicious provider could return false permission results.


Examples

Example Flow Chain Description
tutorial-aptos Basic Aptos Step-by-step tutorial — a minimal pay-to-download marketplace; demonstrates per-item domain-binding. One faucet click for Alice and go.
shelby-explorer-acl-aptos Basic Aptos ACE ACL module from Shelby Explorer — allowlist / time-lock / pay-to-download
pay-to-download-solana Basic Solana Pay-to-download with Anchor programs
zk-kyc Custom Aptos Age-gated decryption with Groth16 ZK proofs

License

Apache 2.0

About

Access-controlling encrypted data with smart contracts.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors