Avalanche · Engineering notes

Bringing confidential compute to Avalanche

How we stood up a full FHE stack on our own infrastructure — and how it becomes a decentralized service for C-Chain and every Avalanche L1.

Most of what happens on-chain today is public by default. For a lot of the things our ecosystem wants to build next — confidential payments, private DeFi, sealed-bid auctions, on-chain games with hidden state — that's a dealbreaker. This is our engineering take on closing that gap natively, on infrastructure we control, with no dependency on any outside operator.

1. What we set out to prove

The enabling technology is Fully Homomorphic Encryption (FHE) — it lets a computer do math on numbers while they stay encrypted. Applied to a chain, that means balances and inputs can live on-chain as ciphertext, and the network can still verify a transaction is valid without ever seeing the values. Zama's FHEVM is the most mature implementation, and it's open source.

So we asked a simple question: can Avalanche run this entire stack itself? Not as a hosted service someone else operates and meters — but built from source, on our own boxes, serving our own chains. The answer is yes, and this site is the proof:

🏔️ What's running here: a confidential USDC — deposit, send with hidden amounts, fully-private transfers, and withdraw — live on Avalanche Fuji, with every component (coprocessor, KMS, gateway, relayer) built from source and self-hosted by us. There is no dependency on Zama running anything on our behalf. The same stack extends to any Avalanche L1.

2. The core idea: math on locked boxes

You don't need cryptography background for the intuition. FHE is like being able to add up the contents of two sealed boxes and get a third sealed box with the correct sum — without ever opening any of them. The chain can update an encrypted balance, check an encrypted amount is affordable, and move encrypted funds, all without learning a single number.

The way users interact with it mirrors a casino:

💵

Cash = public USDC

Ordinary tokens. Balances and payments are fully visible, like everything on C-Chain today.

🎰

Buy chips = deposit

You move public USDC into the confidential pool. The deposit amount is public (you're handing over visible cash) — but once inside, it becomes an encrypted balance.

🔒

Play = confidential transfer

Move value inside the pool. Everyone sees a transfer happened; nobody sees the amount.

💵

Cash out = withdraw

Convert back to public USDC. The amount you take out becomes public again at the boundary.

3. What's private, and what isn't

We're precise about this, because over-promising privacy is how people get hurt. Here's exactly what each action exposes on-chain:

ActionSenderReceiverAmount
Deposit (shield)visiblepublic
Confidential sendvisiblevisiblehidden
Private sendvisiblehiddenhidden
Withdraw (unshield)visiblepublic

Deposit/withdraw amounts are public because they're the on/off-ramp between public and confidential funds — the same boundary every privacy system has. Everything inside the pool is encrypted.

How "private send" hides the receiver

A private transfer doesn't touch only you and the recipient — it touches every member of the pool at once, handing each an encrypted amount. Almost all are encrypted zeros; only the real recipient's is the actual amount. Since every entry is ciphertext, an observer can't tell which one was real — the recipient is hidden in the crowd, and privacy scales with the size of that crowd. (You still sign the transaction, so your own address shows as the sender; hiding that too needs zero-knowledge spend proofs, which we'll come back to.)

4. The stack we run

Here's the system we self-host, component by component. The useful split: the chains remember state and agree on order, the coprocessor does the encrypted math, the KMS guards the key, and the relayer + listeners are the plumbing that connects them.

⛓️

Host chain — Avalanche (Fuji / C-Chain / any L1)

where the app and transactions live

This is the Avalanche chain our confidential token is deployed on and where users transact. It deliberately does not do the FHE math — that's far too heavy for an EVM. Instead the contract records what math to perform and emits it as events ("subtract handle A from B, add to C"), and stores each balance as a handle — a 32-byte pointer to a ciphertext, never a plaintext number. It also holds the ACL: the on-chain record of who may decrypt which handle.

Holds: encrypted balance handles, the ACL, app logic Runs: Avalanche validatorsOur setup: Fuji C-Chain (43113)
🧮

Coprocessor

the engine that does the encrypted math

The host chain writes down instructions; the coprocessor carries them out. It watches the chain, picks up the symbolic FHE operations, performs the real fully-homomorphic computation on the ciphertexts, and writes the encrypted results back. It's the most resource-hungry part of the stack (CPU + lots of RAM for keys), and it's several specialized workers:

tfhe-worker — the core FHE ops (add, subtract, compare, conditional-select) on ciphertexts. The heavy lifter.
sns-worker — "switch-and-squash": reshapes a ciphertext into the form the KMS needs to threshold-decrypt it later.
zkproof-worker — verifies the zero-knowledge proof on every encrypted input (that it's well-formed and came from that user).
host-listener / poller — watches the host chain (live subscription, or HTTP polling when the RPC can't stream).
transaction-sender — posts results/attestations back on-chain and pays their gas.

Every result it produces is signed so the rest of the protocol can trust it came from an authorized operator. In our benchmarking this caps around ~20 TPS per instance on CPU (no GPU path in v0.11 yet), which is exactly why we scale it horizontally rather than vertically.

Holds: FHE evaluation keys (RAM), ciphertexts + compute history Without it: transactions land but balances never update
🔑

KMS — Key Management Service

the vault; the only thing that can decrypt

The KMS holds the FHE secret key — the one thing that can turn a ciphertext back into a number. It decrypts only when the gateway confirms the requester is authorized by the ACL. Two modes: user-decryption (reveal to one person — the "Reveal" button) and public-decryption (make a value public — used at withdraw). Every decryption is signed so apps can verify it.

In this demo it's a single node holding the whole key — fine for a testnet proof. In production we'd split the key across many nodes so no single operator ever holds it; decryption then requires a quorum. That's the single most important hardening step, and §6 is about how we'd run it across our validator set.

Holds: the FHE secret key (or a share of it) Without it: nothing can ever be decrypted
🗂️

Gateway chain

the coordination layer / switchboard

A blockchain separate from the host chain that acts as the protocol's source of truth for everything cross-cutting. It's deliberately not the host chain because it's meant to be a shared hub: many host chains (Fuji plus other L1s) plug into one gateway, so key management and decryption logic live in one place rather than being copied onto every chain. It runs:

GatewayConfig — registry of allowed host chains + their addresses. This is what we call to "onboard" a new chain.
Decryption — where decryption requests are submitted, coordinated, and the KMS's signed responses verified.
KMS & input verification — the valid KMS / coprocessor signer sets and required thresholds.

In our setup this is its own node (chain 54321), kept local — it is not the host chain, and it's the piece that most naturally becomes its own Avalanche L1.

Holds: host-chain registry, decryption requests, signer sets
🔐 Where the ACL lives — a detail teams always ask. The access-control list (who may decrypt which handle) is a contract on the host chain (Fuji), not the gateway — we verified there's no ACL code on the gateway at all. Your contract's FHE.allow(handle, you) writes the permission there, and it's the single source of truth. On a decryption request, the gateway/KMS side reads that ACL across chains (our kms-connector is configured per host chain with its RPC + ACL address). Miss that config and decryption fails with "No ACL contract config found for chain id …" — a real gotcha we hit. When the host and gateway are the same chain (§7), there's no cross-chain read at all.
📨

Relayer

the front door (HTTP API)

What apps actually talk to. It serves the FHE public keys (so a browser can encrypt), turns inputs into handles + proofs, and forwards decryption requests to the gateway/KMS. It holds no secrets and no special trust — it's a convenience and scaling layer. Anyone can run one, you can run many, and the worst a bad relayer can do is refuse service.

Holds: nothing sensitiveRuns: anyone — can be many
👂

Listeners, connectors & storage

the wiring

The host-listener/poller and gw-listener watch the chains and feed events to the coprocessor; the kms-connector bridges the KMS to the gateway (posts signed decryptions, reads ACL). None are trust-critical on their own. Rounding it out: object storage (MinIO/S3) for the large ciphertext + key blobs that don't fit on-chain, and Postgres for the coprocessor's and relayer's working state.

5. The lifecycle of a transaction

Every confidential action has the same shape: encrypt → submit → compute → (later) decrypt.

1
Encrypt: the app locks the amount into a ciphertext and attaches a zero-knowledge proof it's well-formed.
2
Submit: the transaction lands on the host chain; the contract updates balances with encrypted math, never seeing the numbers.
3
Compute: the listener notices it, the coprocessor runs the FHE operations, and the new encrypted balances are stored.
4
Decrypt (only on request): to read your own balance you sign a request; the relayer + KMS decrypt it just for you. The chain stays encrypted.

We built an interactive view that walks each action through these phases, station by station:

▶ Open the interactive transaction flow

6. Making it ours: decentralizing on an Avalanche L1

What's running today is centralized — one box, one key holder, one coprocessor. That's the right scope for a proof of concept, but it means users are trusting us. The whole point of doing this on Avalanche is to spread that trust across the validator set we already operate. One rule guides the design:

Needs agreement on order → it's a chain.   Needs distributed trust or compute → it's a job validators run.   Just convenience or storage → it stays a service.

What becomes what

PieceToday (our box)Decentralized form
Gateway chainsingle nodeBecomes an Avalanche L1 — validators run its consensus
Host chainFujiAlready ours — C-Chain or any L1
KMS1 node, whole key🔑 Validator job — key split across validators (threshold MPC)
Coprocessor1 instance🧮 Validator job — many operators compute + co-sign
Relayer / storage / DBsingle🟡 Stay services — can be many; not consensus-critical

So exactly one thing becomes a new chain — the gateway. The KMS and coprocessor don't become chains; they become the two big jobs our validators perform. Everything else is plumbing.

How we'd ship it

1

Launch an Avalanche L1 as the gateway

Spin up a new L1 (Subnet) with a validator set and deploy the gateway contracts (GatewayConfig, Decryption, KMS/input verification) on it. Repoint the coprocessor's gw-listener, the kms-connector, and the relayer at this L1 instead of our dev node.

2

Run the KMS in threshold mode across validators

Rebuild the KMS in its threshold (MPC) configuration, one node per validator/committee member, and run the distributed key-generation ceremony so the secret key is born already split — no one ever holds it whole. Register the signer set + quorum in the gateway and in each host chain's KMS verifier.

3

Run multiple coprocessors with co-signing

Have validators with the hardware run the FHE coprocessor; register their signers with a threshold so a result is only accepted when enough agree. Compute stays off-chain (too heavy for a VM) — what's decentralized is who runs it and the requirement that they agree.

4

Keep the edge as managed services

Relayers, object storage (S3), and databases (RDS) run as ordinary services — one or many. They hold no secrets, so they don't need consensus.

🎯 Why this is the right shape for us: privacy stops depending on trusting any single operator — no one decrypts alone (it takes a validator quorum), coordination is secured by Avalanche consensus, and the trust model is exactly the validator set the ecosystem already relies on. It's Zama's "gateway + KMS operators + coprocessors" topology, unified under an Avalanche L1 we own end to end.

7. Should Fuji itself be the gateway?

There's a tempting shortcut: skip the dedicated gateway L1 and deploy the gateway contracts directly on Fuji C-Chain. Fuji is already secured by our full validator set, so there's no new chain to bootstrap. It's a real option — but it changes the trade-offs, and it hits two audiences differently.

For Fuji-native confidential apps (host = gateway)

For onboarding other Avalanche L1s (Fuji as the shared gateway)

⚖️ Our rule of thumb: for a single Fuji-native app or an early rollout, Fuji-as-gateway is simplest — one chain, top-tier security, no cross-chain decryption headaches. To serve many L1s at scale, a dedicated gateway L1 isolates all that coordination load and gas off C-Chain and scales far better. The pragmatic middle path: a dedicated gateway L1 secured by a subset of Avalanche validators, with Fuji and other L1s plugging in as host chains.

8. Where this goes

The proof is done: the full stack runs on our infrastructure and serves real confidential transactions on Fuji. The path from here is incremental and each step stands on its own:

The strategic takeaway is the simple one: confidential compute can be a native Avalanche capability that we own outright — no external operator in the critical path, extensible to the whole ecosystem.

Built on Zama's open-source FHEVM, self-hosted by Avalanche. This is a testnet proof of concept on Fuji using mock USDC.