DEV Community

Cover image for Conclave: Confidential Multi-Agent Consensus on Ethereum Using Fully Homomorphic Encryption
Harish Kotra (he/him)
Harish Kotra (he/him)

Posted on

Conclave: Confidential Multi-Agent Consensus on Ethereum Using Fully Homomorphic Encryption

How we built a protocol where AI agents vote on subjective tasks without ever revealing their individual scores — using FHE, Solidity, and a dark-terminal frontend.


The Problem

When multiple AI agents evaluate the same task — an LLM output, a code review, a creative brief — their individual scores carry valuable signal. But today's on-chain voting has a fundamental flaw: every vote is visible.

This creates three problems:

  1. Anchoring — The first score revealed biases all subsequent voters. If Agent A submits an 87, Agent B is subconsciously pulled toward 87 rather than evaluating independently.

  2. Collusion — Agents can coordinate to manipulate the consensus by adjusting their scores after seeing peers' submissions.

  3. Chilling effects — An agent that wants to submit an honest low score (say, 30) may hesitate if other agents or the round creator can see it and retaliate.

Existing solutions — multi-sigs, DAO voting, commit-reveal schemes — all leak individual votes at some point in the lifecycle. Commit-reveal just delays the reveal; it doesn't prevent it.

What we needed was a way to compute the average of encrypted inputs — and only decrypt the final result.

The Solution: FHE-Based Consensus

Fully Homomorphic Encryption (FHE) lets you perform arithmetic directly on ciphertexts. You can add, subtract, multiply, and divide encrypted numbers — and the result, when decrypted, equals what you'd get by operating on the plaintexts.

Conclave uses Fhenix's CoFHE (Confidential FHE) SDK to implement a multi-agent consensus protocol where:

  • Scores are encrypted on the client side — no one, including the contract, ever sees a plaintext score
  • The contract sums ciphertexts using FHE — the running total stays encrypted throughout
  • Only the final average is decrypted — by a threshold network of FHE nodes, after all agents have committed

Individual scores remain encrypted forever.

Agent 1 ───── encrypt(87) ─────┐
                                ▼
Agent 2 ───── encrypt(92) ───► FHE.sum() ──► FHE.div() ──► avg = 81
                                ▲
Agent 3 ───── encrypt(64) ─────┘
                                        │
                              Threshold Decrypt
                                        │
                                   81/100
Enter fullscreen mode Exit fullscreen mode

The Protocol

Conclave's protocol has five phases, encoded as a Solidity enum:

enum Phase { Voting, Revision, Finalized, Revealed }
Enter fullscreen mode Exit fullscreen mode

1. Create

The round creator specifies:

  • A list of agent addresses (2–50)
  • A task URI (either a URL or inline data:application/json URI)
  • A revisionsEnabled flag

The contract initializes two FHE accumulators — encryptedSum and encryptedCount — both set to FHE.asEuint32(0). These will hold the running total and count in encrypted form.

r.encryptedSum   = FHE.asEuint32(0);
r.encryptedCount = FHE.asEuint32(0);

FHE.allowThis(r.encryptedSum);
FHE.allowThis(r.encryptedCount);
Enter fullscreen mode Exit fullscreen mode

The allowThis calls are CoFHE's permission model — they grant the contract itself the ability to read (and operate on) these encrypted values.

2. Vote

Each agent:

  1. Fetches the task from the URI
  2. Scores it via an LLM (e.g., GPT-4o), producing a plaintext integer 0–100
  3. Encrypts it client-side using @cofhe/sdk:
const [encrypted] = await cofheClient
  .encryptInputs([Encryptable.uint32(BigInt(score))])
  .execute();
Enter fullscreen mode Exit fullscreen mode
  1. Submits the InEuint32 ciphertext to the contract

The contract decrypts nothing. It adds the ciphertext to the encrypted sum using pure FHE arithmetic:

euint32 score = FHE.asEuint32(encryptedScore);
euint32 newSum = FHE.add(r.encryptedSum, score);
r.encryptedSum = newSum;

euint32 newCount = FHE.add(r.encryptedCount, FHE.asEuint32(1));
r.encryptedCount = newCount;
Enter fullscreen mode Exit fullscreen mode

3. Revision (Optional)

If the creator enabled revisions, they can open a revision window after all agents have voted. Agents who already voted can submit a new encrypted score. The contract subtracts the old ciphertext and adds the new one — all in the encrypted domain:

euint32 sumAfterRemoval = FHE.sub(r.encryptedSum, oldScore);
euint32 newSum = FHE.add(sumAfterRemoval, newScore);
r.encryptedSum = newSum;
Enter fullscreen mode Exit fullscreen mode

This is the power of FHE: the contract can update values without ever seeing what those values are.

4. Finalize

Once quorum is met, the creator locks the round. The contract computes the encrypted average:

euint32 avg = FHE.div(r.encryptedSum, r.encryptedCount);
Enter fullscreen mode Exit fullscreen mode

It stores this encrypted average as consensusHandle and moves the phase to Finalized. At this point, no more votes or revisions are accepted.

5. Reveal

The Fhenix threshold network decrypts the average. revealConsensus() is called with the plaintext result and a cryptographic proof:

function revealConsensus(uint256 roundId, euint32 ctHash, uint32 plaintext, bytes calldata signature) external {
    FHE.publishDecryptResult(ctHash, plaintext, signature);
    r.consensusScore = plaintext;
    r.phase = Phase.Revealed;
}
Enter fullscreen mode Exit fullscreen mode

The threshold network ensures that no single party can forge a decryption — the signature proves a quorum of FHE nodes validated the result.


Smart Contract Architecture (353 Lines)

The entire protocol fits in a single Solidity contract. Key design decisions:

Errors, Not Strings

18 custom errors for gas-efficient reverts. This is Solidity best practice — custom errors are cheaper than require with string messages and provide typed data for frontend handling.

error QuorumNotMet(uint256 roundId, uint32 submitted, uint32 required);
error ScoreOutOfRange(uint32 plaintext, uint32 max);
error RevisionNotEnabled(uint256 roundId);
Enter fullscreen mode Exit fullscreen mode

Permission Model

CoFHE requires explicit allow calls for every encrypted value — otherwise it's inaccessible. The contract scopes access tightly:

  • FHE.allowThis() — the contract itself can read and operate on the value
  • FHE.allow(score, msg.sender) — the submitting agent can read their own score later
  • FHE.allowPublic() — only called on the final average, so the threshold network can decrypt it

Without these permits, even the contract deployer cannot decrypt individual votes.

Zero-Knowledge About Individual Inputs

The contract never stores plaintext scores. The mapping storing each agent's vote is:

mapping(uint256 => mapping(address => euint32)) private _agentScoreHandle;
Enter fullscreen mode Exit fullscreen mode

This is an encrypted handle, not a plaintext value. Even a storage-level attack on the blockchain would only yield ciphertexts.


The Agent Runner

The agent/ directory contains a Node.js/TypeScript runner that:

  1. Polls the contract every 30 seconds
  2. Fetches the task from the on-chain URI
  3. Sends it to an OpenAI-compatible LLM with the scoring rubric
  4. Encrypts the score using @cofhe/sdk/node
  5. Submits the encrypted ciphertext via submitVote()

We recently refactored it to accept a --agent flag so each agent runs in its own terminal:

npx ts-node agent/index.ts --agent 1
npx ts-node agent/index.ts --agent 2
npx ts-node agent/index.ts --agent 3
Enter fullscreen mode Exit fullscreen mode

The agent process is stateless — it reads everything it needs from the contract. You can kill and restart any agent, and it will pick up where it left off.

Rate Limiting

Running 3 agents against Infura's free tier quickly hits rate limits. We added exponential backoff with jitter:

async function withRetry<T>(fn: () => Promise<T>, attempt = 1): Promise<T> {
  try {
    return await fn();
  } catch (e: any) {
    if (e?.code === "BAD_DATA" && attempt < 6) {
      const delay = Math.min(1000 * 2^attempt + Math.random() * 1000, 30_000);
      await new Promise((r) => setTimeout(r, delay));
      return withRetry(fn, attempt + 1);
    }
    throw e;
  }
}
Enter fullscreen mode Exit fullscreen mode

Each agent also starts with a random 0–5s stagger and desynchronizes its poll interval by ±2.5s.


The Frontend

The frontend is a Next.js 14 App Router application with a visual identity inspired by Linear, Vercel, and Stripe — clean, monochrome, with a single purple accent (#c084fc).

Design System

The palette is deliberately restricted:

  • Base: #040816 (near-black blue)
  • Surfaces: #070B1F, #0A0F2A, #1A1F3A
  • Accent: #c084fc (purple, single use)
  • Success: #22c55e (emerald)
  • Warning: #f59e0b (amber)
  • Borders: thin, 1px solid #1A1F3A

No gradients. No blur effects. No box shadows. The "glass" card is achieved with flat color and a thin border — no backdrop-filter:

.glass {
  background: rgba(10, 15, 42, 0.6);
  border: 1px solid rgba(30, 35, 60, 0.6);
}
Enter fullscreen mode Exit fullscreen mode

Pages

Route Purpose
/ Landing page (hero, protocol explainer, FHE comparison) when disconnected. Switches to round list when wallet connects.
/create 4-step wizard: Task, Agents, Rules, Launch. Supports inline task entry (title, description, rubric) or URL paste.
/round/[id] Mission control. Two-column layout: protocol execution flow on the left, agent telemetry on the right.

Key Components

  • FhePipeline — Vertical stage diagram showing the protocol execution flow with animated timeline connectors
  • AgentList — Participant identity cards showing wallet address, vote status, ciphertext count, and local decrypt
  • ConsensusResult — SVG ring meter with the final score and protocol completion bar
  • RoundCard — Protocol timeline with horizontal phase indicators (Created → Voting → Revision → Finalized → Revealed)
  • VoteForm — Encrypted vote slider with encrypt-then-submit flow
  • ProtocolStrip — Phase status bar at the top of every round detail page

The Consensus Visualization

The landing page features an animated SVG showing the full protocol flow: agent nodes pulse, data packets travel downward into an FHE computation box, then through threshold decryption, and finally the consensus score appears with a scale-in animation.

@keyframes data-packet {
  0%   { opacity: 0; transform: translateY(0); }
  25%  { opacity: 1; }
  75%  { opacity: 1; }
  100% { opacity: 0; transform: translateY(68px); }
}
Enter fullscreen mode Exit fullscreen mode

Testing

52 test cases covering the full protocol spec. Run with:

npx hardhat test
Enter fullscreen mode Exit fullscreen mode

Breakdown:

Group Tests What It Covers
Create Round 4 Quorum validation, duplicates, empty URI, max agents
Submit Vote 6 Encryption path, submission, double-vote rejection, agent-only access
Revision 8 Open, revise, re-revision rejection, disabled-round
Finalize 6 Quorum check, re-finalize rejection, encrypted average
Reveal 6 Threshold decrypt, score bounds, double-reveal
ACL 6 Permission model, allowThis/allow scoping
Full E2E 8 No-revision path, with-revision path, all phases
Truncation 8 Score boundaries, div-by-zero safety

Test Helper: FHE

Testing FHE contracts requires a local CoFHE node. The test helper sets up the environment:

// test/helpers/fhe.ts
import { createCofheConfig, createCofheClient } from "@cofhe/sdk/node";
// ... configures hardhat in-process environment
Enter fullscreen mode Exit fullscreen mode

Deployment

The contract is deployed on Ethereum Sepolia at:

0x0C83824a9800f9ED1e22ec289CB67E065ceA73C2
Enter fullscreen mode Exit fullscreen mode

Deployment via Hardhat:

npx hardhat run scripts/deploy.ts --network sepolia
Enter fullscreen mode Exit fullscreen mode

The deploy script uses dynamic gas pricing — 2 × baseFee + maxPriorityFeePerGas — to avoid "max fee per gas less than block base fee" errors that plagued earlier versions with hardcoded caps.


What We Learned

1. FHE is production-ready (with caveats)

The Fhenix CoFHE SDK works. Client-side encryption is straightforward. The threshold decryption network adds latency (5–15 seconds for decrypt), but it's reliable on Sepolia. The biggest friction point was configuration: the SDK's environment option accepts "node", "hardhat", "web", or "react" — not chain names like "sepolia".

2. Monorepos need explicit Vercel config

The frontend lives in frontend/ but Vercel expects the root to contain the Next.js app. The solution: set Root Directory to frontend/ in the Vercel project settings. vercel.json's rootDirectory property isn't valid when the project is already linked via GitHub — you must use the dashboard.

3. Hydration errors are a wagmi footgun

useAccount() returns different values on the server vs client, causing React hydration mismatches. The fix: use a mounted state guard that renders a static placeholder until useEffect fires on the client.

const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
if (!mounted) return <StaticPlaceholder />;
Enter fullscreen mode Exit fullscreen mode

4. Infura's free tier is aggressive

3 agents × multiple eth_calls per poll × 30s interval = rapid rate limiting. Solution: exponential backoff, staggered startup, desynchronized poll intervals.

5. WASM circular dependencies

CoFHE's WebAssembly workers create circular webpack chunks. They're harmless but noisy. Suppress with:

config.ignoreWarnings = [/Circular dependency.*cofhe_sdk/];
Enter fullscreen mode Exit fullscreen mode

Project Structure

conclave/
├── contracts/
│   └── Conclave.sol           # 353 lines, full protocol
├── frontend/
│   └── src/
│       ├── app/               # 4 routes (layout, home, create, round)
│       ├── components/        # 6 React components
│       ├── hooks/             # 6 custom hooks (useRound, useVote, etc.)
│       ├── lib/               # ABI, chain config, CoFHE client
│       └── types/             # Phase enum, Round interface
├── agent/
│   ├── index.ts               # Agent runner with --agent flag
│   ├── llm.ts                 # OpenAI-compatible scoring
│   └── wallets.ts             # Wallet management
├── test/
│   ├── Conclave.test.ts       # 52 tests
│   └── helpers/
│       └── fhe.ts             # FHE test utilities
└── scripts/
    └── deploy.ts              # Hardhat deployment
Enter fullscreen mode Exit fullscreen mode

Try It Yourself

Prerequisites

  • 3 Sepolia-funded agent wallets (~0.005 ETH each)
  • An OpenAI API key
  • SEPOLIA_RPC_URL (Alchemy recommended over Infura)

Quick Start

git clone https://github.com/harishkotra/conclave
npm install

# Set up .env
cp .env.example .env
# Fill in: SEPOLIA_RPC_URL, AGENT_{1,2,3}_PRIVATE_KEY, LLM_API_KEY

# Deploy the contract (or use the existing one at 0x0C83...)
npm run deploy

# Start the frontend
cd frontend && npm run dev

# In 3 separate terminals, run the agents:
npx ts-node agent/index.ts --agent 1
npx ts-node agent/index.ts --agent 2
npx ts-node agent/index.ts --agent 3

# Create a round on the frontend with those 3 agent addresses
# Watch the protocol execute
Enter fullscreen mode Exit fullscreen mode

Future Work

  • Multi-round scheduling: An auction or queue where rounds are processed in batches
  • Gas optimization: The current contract makes multiple FHE operations per vote; batching could reduce costs
  • Slashing: Penalties for agents that submit scores but fail to revise when the revision phase opens
  • VC (Verifiable Computation) proofs: Prove that the FHE computation was correct without relying on the threshold network
  • Agent reputation: Track historical score deviations from consensus to weight future votes

Conclave is open source. Built with Solidity, Fhenix CoFHE, Next.js, and TypeScript.

Screenshots

Conclave 1

Conclave 2

Conclave 3

Conclave 4

Conclave 5

Conclave 6

Code and more: https://www.dailybuild.xyz/project/155-conclave

Top comments (0)