Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions DEPLOYMENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Deployment Evidence

Network:

- Chain: Ritual Chain
- Chain ID: `1979`
- RPC: `https://rpc.ritualfoundation.org`
- Explorer: `https://explorer.ritualfoundation.org`

Contract:

- `AIJudge`: `0xde69EcA7E223E52B607AB1113bb358D63C9a388C`
- Deployment transaction: `0x6a5f550beaa89a9cee8b9704bcf71f579ce1253db71015c8e0d097a3e5d94204`

Live smoke test:

- Bounty ID: `3`
- `createBounty`: `0xf861f7493231c6cd2a967d1d22cc44cda6684a374c84a224765459a8e12e13a8`
- `submitCommitment`: `0x62fea65878cc2bc1c222fea28f29469fbfc58dce6b19f2b51fad81c77095a70f`
- `revealAnswer`: `0xcf6098807d9b3eb99d361a186ff77be8888ec0dbdc78f318cb5bd10eaa27ff53`
- `judgeAll`: `0x0f4996e7eec438952174ceac3387e61f75a46cc15a2db5070af1bb0543ad49af`
- `finalizeWinner`: `0xfcaf65f8433bb342dec66fe143871a01beb42f43e6ed27325cbe7291c7117dd1`

Final observed state for bounty `3`:

- `judged`: `true`
- `finalized`: `true`
- `commitmentCount`: `1`
- `revealedCount`: `1`
- `winnerIndex`: `0`
- `remainingReward`: `0`
213 changes: 210 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,212 @@
## Starter for Ritual workshop on 23th June 2026
# Privacy-Preserving AI Bounty Judge

/hardhat -> Where we'll write the smart contract
This repository extends the Ritual AI Bounty Judge workshop with a
commit-reveal bounty flow. Participants no longer publish plaintext answers
during the submission phase, so later participants cannot copy earlier work
before the deadline.

/web -> Where the frontend lives.
The required Solidity track is implemented in `hardhat/contracts/AIJudge.sol`.
It works on any EVM chain for the commit-reveal lifecycle, and uses Ritual's
LLM inference precompile for the optional on-chain AI judging step when
deployed on Ritual Chain.

## Why this matters

The original workshop contract accepted public plaintext answers immediately.
That is simple, but unfair: the first honest participant reveals their solution
to everyone, and later participants can copy, lightly edit, or strategically
wait for better answers. Commit-reveal fixes the submission-phase leak by
storing only a hash until submissions close.

## Bounty lifecycle

1. The owner creates a bounty with a reward, a submission deadline, and a
reveal deadline.
2. During the submission phase, participants submit only a commitment hash with
`submitCommitment(bountyId, commitment)`.
3. During the reveal phase, participants reveal the original `answer` and
`salt` with `revealAnswer(bountyId, answer, salt)`.
4. The contract recomputes the commitment as:

```solidity
bytes32 commitment = keccak256(
abi.encodePacked(answer, salt, msg.sender, bountyId)
);
```

5. Only valid revealed answers are stored in the judged submissions array.
Unrevealed commitments are ignored.
6. After the reveal deadline, the owner calls
`judgeAll(bountyId, llmInput)`.
7. `judgeAll` sends one batched Ritual LLM request for all revealed answers.
It does not call the LLM once per submission.
8. The owner reviews the AI output and finalizes exactly one winner with
`finalizeWinner(bountyId, winnerIndex)`.
9. The contract pays the full reward once, using checks-effects-interactions.

AI output is advisory. The contract never pays directly from raw model output;
the owner must choose a valid revealed-submission index.

## Generating a commitment

Use the exact Solidity packing order and include the participant address and
`bountyId` so another wallet cannot reuse the same answer and salt.

```ts
import { encodePacked, keccak256 } from "viem";

const commitment = keccak256(
encodePacked(
["string", "bytes32", "address", "uint256"],
[answer, salt, participantAddress, bountyId],
),
);
```

The `salt` should be a random `bytes32` value kept private until reveal.

## Participant flow

Submit phase:

```solidity
submitCommitment(bountyId, commitment);
```

Reveal phase:

```solidity
revealAnswer(bountyId, answer, salt);
```

If the answer or salt differs from the committed values, or if a different
wallet reveals, the contract rejects the reveal.

## AI judging

`judgeAll(uint256 bountyId, bytes calldata llmInput)` is owner-only and can run
only after the reveal deadline. The frontend or caller builds one prompt that
contains the bounty title, rubric, and every valid revealed answer, then
ABI-encodes the Ritual LLM request as `llmInput`.

Ritual documentation describes LLM inference as precompile `0x0802`; contracts
forward pre-encoded request bytes, and the response format decodes to
`(bool hasError, bytes completionData, bytes modelMetadata, string errorMessage, ConvoHistory updatedConvoHistory)`.
Ritual's docs also describe this as TEE-backed delegated execution, with short
precompile results available to the calling transaction and one SPC call per
transaction.

## Finalization and payout

After `judgeAll` succeeds, the owner calls:

```solidity
finalizeWinner(bountyId, winnerIndex);
```

`winnerIndex` must refer to a valid revealed submission. The contract marks the
bounty finalized, zeroes the reward, and then transfers the reward to that one
winner. A second finalization or payout reverts.

## Tests

The Hardhat test suite covers:

- valid commitment submission
- rejection after the submission deadline
- duplicate commitment rejection
- valid reveal with correct answer and salt
- reveal rejection before submission close
- reveal rejection after reveal close
- reveal rejection with wrong answer or salt
- unrevealed commitments excluded from judging
- `judgeAll()` only after reveal deadline
- `judgeAll()` rejection when there are no revealed submissions
- `finalizeWinner()` only after judging
- invalid winner index rejection
- reward paid only once

Run:

```bash
cd hardhat
npm install
npx hardhat test
```

The local tests deploy `contracts/test/MockAIJudge.sol`, which overrides the
Ritual precompile call and returns a mock LLM response. The production contract
still calls Ritual's LLM precompile through `PrecompileConsumer`.

## Deploy

Deploy with Hardhat Ignition:

```bash
cd hardhat
npm install
npx hardhat ignition deploy ignition/modules/AIJudge.ts --network ritual
```

The `ritual` network is configured with chain ID `1979` and RPC
`https://rpc.ritualfoundation.org`. Set `DEPLOYER_PRIVATE_KEY` before deploying
to Ritual Chain. For other EVM chains, the commit-reveal functions still work,
but `judgeAll()` requires either Ritual Chain's LLM precompile or a chain-local
replacement.

## Assumptions and limitations

- Answers are hidden only until reveal in the required EVM commit-reveal track.
- Revealed plaintext answers are stored on-chain and are public.
- The contract caps commitments at `MAX_SUBMISSIONS` to keep batched judging and
on-chain storage bounded.
- `llmInput` is built off-chain so the contract does not construct large prompts
or loop over LLM calls.
- The owner is trusted to validate the AI review and choose a valid winner.

## Architecture note

### A. Required commit-reveal design

Commitments are public on-chain hashes. Plaintext answers stay hidden during the
submission phase because only the hash is submitted. Answers become public
during the reveal phase, and only valid reveals are eligible for judging. This
works on any EVM chain because it depends only on `keccak256`, deadlines, and
ordinary Solidity storage.

Plaintext exists in the participant's local environment before reveal, then
exists publicly on-chain after reveal. The on-chain state contains bounty
metadata, commitments, revealed answers, judging status, AI review bytes, and
the final winner.

### B. Advanced Ritual-native encrypted submissions

Participants would encrypt answers for a Ritual TEE/private execution flow
instead of revealing plaintext directly on-chain. The contract would store
encrypted submissions or references to encrypted submissions during the
submission phase. During `judgeAll()`, a TEE-backed workflow would decrypt all
eligible answers privately and send them to the LLM in one batch.

In that design, plaintext exists on the participant's machine and inside the
attested TEE during judging, but not in public mempool calldata or normal
contract storage before judging. On-chain state should store encrypted
submission references, judging status, the final winner, plus
`revealedAnswersRef` and `revealedAnswersHash` instead of large plaintext answer
arrays. The final revealed-answer bundle can live off-chain; users verify it by
hashing the bundle and comparing it with `revealedAnswersHash`. The system can
either reveal all answers together after judging or publish only a verified
off-chain bundle, depending on the bounty rules.

## Reflection

Bounty metadata, deadlines, reward amount, commitments, judging status, and the
final winner should be public so participants can verify the process. Plaintext
answers should stay hidden during submission because early disclosure creates an
unfair copying advantage. In the advanced Ritual-native design, plaintext should
also stay hidden until private judging is complete. AI should score and rank
submissions against the rubric because it can apply the same evaluation prompt
to the full batch. A human bounty owner should finalize the payout because model
output can be malformed, manipulated, or wrong. The contract should enforce
objective rules like deadlines, reveal validity, winner index validity, and
single payout. The overall design should make the process auditable without
exposing private answers too early.
7 changes: 7 additions & 0 deletions hardhat/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Copy to .env and fill in private values.

DEPLOYER_PRIVATE_KEY=

# Optional Sepolia config.
SEPOLIA_RPC_URL=
SEPOLIA_PRIVATE_KEY=
75 changes: 42 additions & 33 deletions hardhat/README.md
Original file line number Diff line number Diff line change
@@ -1,57 +1,66 @@
# Sample Hardhat 3 Project (`node:test` and `viem`)
# Hardhat: Privacy-Preserving AI Bounty Judge

This project showcases a Hardhat 3 project using the native Node.js test runner (`node:test`) and the `viem` library for Ethereum interactions.
This package contains the Solidity implementation and tests for the
commit-reveal AI bounty judge.

To learn more about Hardhat 3, please visit the [Getting Started guide](https://hardhat.org/docs/getting-started#getting-started-with-hardhat-3). To share your feedback, join our [Hardhat 3](https://hardhat.org/hardhat3-telegram-group) Telegram group or [open an issue](https://github.com/NomicFoundation/hardhat/issues/new) in our GitHub issue tracker.
## Contract

## Project Overview
- `contracts/AIJudge.sol` is the production contract.
- `contracts/utils/PrecompileConsumer.sol` contains the Ritual precompile helper.
- `contracts/test/MockAIJudge.sol` is test-only and mocks the Ritual LLM result.

This example project includes:
Required public entrypoints:

- A simple Hardhat configuration file.
- Foundry-compatible Solidity unit tests.
- TypeScript integration tests using [`node:test`](nodejs.org/api/test.html), the new Node.js native test runner, and [`viem`](https://viem.sh/).
- Examples demonstrating how to connect to different types of networks, including locally simulating OP mainnet.
```solidity
function submitCommitment(uint256 bountyId, bytes32 commitment) external;

## Usage
function revealAnswer(
uint256 bountyId,
string calldata answer,
bytes32 salt
) external;

### Running Tests
function judgeAll(uint256 bountyId, bytes calldata llmInput) external;

To run all the tests in the project, execute the following command:
function finalizeWinner(uint256 bountyId, uint256 winnerIndex) external;
```

## Run tests

```shell
```bash
npm install
npx hardhat test
```

You can also selectively run the Solidity or `node:test` tests:
## Compile

```shell
npx hardhat test solidity
npx hardhat test nodejs
```bash
npx hardhat compile
```

### Make a deployment to Sepolia

This project includes an example Ignition module to deploy the contract. You can deploy this module to a locally simulated chain or to Sepolia.
## Deploy

To run the deployment to a local chain:
Local simulated deployment:

```shell
npx hardhat ignition deploy ignition/modules/Counter.ts
```bash
npx hardhat ignition deploy ignition/modules/AIJudge.ts
```

To run the deployment to Sepolia, you need an account with funds to send the transaction. The provided Hardhat configuration includes a Configuration Variable called `SEPOLIA_PRIVATE_KEY`, which you can use to set the private key of the account you want to use.
Ritual Chain deployment:

You can set the `SEPOLIA_PRIVATE_KEY` variable using the `hardhat-keystore` plugin or by setting it as an environment variable.
```bash
npx hardhat ignition deploy ignition/modules/AIJudge.ts --network ritual
```

To set the `SEPOLIA_PRIVATE_KEY` config variable using `hardhat-keystore`:
Put the deployer key in a local, ignored `hardhat/.env` file before running the
command.

```shell
npx hardhat keystore set SEPOLIA_PRIVATE_KEY
```
The Ritual network configuration uses:

After setting the variable, you can run the deployment with the Sepolia network:
- Chain ID: `1979`
- RPC: `https://rpc.ritualfoundation.org`
- Currency: `RITUAL`

```shell
npx hardhat ignition deploy --network sepolia ignition/modules/Counter.ts
```
`judgeAll()` depends on Ritual's LLM precompile when using the production
contract. The commit-reveal lifecycle itself uses ordinary EVM features and can
be deployed to any EVM chain.
Loading