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.
| 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 |
- Threshold honest majority: decryption requires
t-of-nworker shares. No single worker — and no coalition smaller thant— 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.
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.
| 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 |
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
payloadbyte string submitted by the requestor. Good for ZK proofs, Merkle witnesses, and other cryptographic credentials.
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).
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
): boollabelidentifies what is being decrypted — it equals thedomainbytes you passed toencrypt. Use it to look up your access records.user_addris who is asking. Check your payment table, allowlist, etc. against this.enc_pkis 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
): boollabelandenc_pkare the same as above.payloadis 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 toenc_pkso 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.
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",
});Start a full ACE network locally (3 workers + Aptos localnet):
cd scenarios
pnpm install
pnpm run-local-network-foreverWait for the ACE local network is READY banner. The network writes contractAddr and keypairId to /tmp/ace-localnet-config.json.
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 installAll 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 newThe 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 profileFullnodes (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.
- Aptos: See Run a public fullnode
- Solana: See Setup a Solana RPC node
| 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 |
Apache 2.0