Deploy to Sepolia
TestnetThis tutorial moves an E3 program from the local stack to the Sepolia testnet. It is for developers who have a working program from the default template. On Sepolia, you use the Interfold contracts that the Interfold team deployed and the ciphernodes that operators run. You deploy only your program contract. At the end, your program is on Sepolia, the owner of the Interfold contract can register it, and your server and client use the testnet.
Do not submit real private data on Sepolia. The insecure-64 preset is not secure. The public
key, decryption, and DKG verifiers of the Sepolia deployment check the proofs. The ciphertext
verifier wraps MockOpenVmReceiptVerifier, which accepts every receipt. See Check the protocol
ciphertext verifier. The test contracts are MockUSDC,
the Faucet, MockE3Program, and MockOpenVmReceiptVerifier. The manifest marks Sepolia with
mocks: true.
E3 requests on Sepolia are paused. requestsPaused() on the Interfold contract returns true,
and request reverts with RequestsPaused. The owner of the Interfold contract unpauses requests
after operators that run the 0.20.0-dev.3 release (protocol_version 9, node_generation 2)
register. Until then, you can deploy your program but you cannot run an E3.
Sepolia protocol contracts
These addresses come from deployments/manifest.json (chain ID 11155111), except BondedVotes. The
Contract Addresses page has the full list for each network.
| Contract | Address | Deploy block | Role |
|---|---|---|---|
Interfold | 0xb35EE9ed8BE5967daabBA8c2e817C52D0b57ac26 | 12050035 | Requests, stages, fees, and outputs of each E3 |
CiphernodeRegistry | 0x895392D5782E78768A35B1aDfc5b0BD1faB259e4 | 12050030 | CiphernodeRegistryOwnable: committees and committee keys |
BondingRegistry | 0x8f42eD45556bC8fF8949c37c7644Af9Cf321108F | 12050022 | Operator bonds and tickets |
SlashingManager | 0xa82F589B29BD5c2392b7127Cba8727Af2bbEd84D | 12050027 | Slashing of ciphernodes that break protocol rules |
MockUSDC (fee token) | 0xE2e534F7Cb95777eD870CDCaCb80B1E9E2aD555B | 11856062 | Test USDC, 6 decimals. Anyone can mint it. It pays E3 fees. |
InterfoldToken (FOLD) | 0x3192Ee1FeE48159807058aA78038cf80cB3450Cb | 11873112 | Governance and utility token. Operators bond FOLD. |
InterfoldTicketToken (tFOLD) | 0xE7cCF95F36131340bdAB122C4cc09787D28217e5 | 12050025 | Non-transferable operator tickets for sortition |
E3RefundManager | 0x272f3A25E30990A7C30b7B90a240e7afe4365349 | 12050041 | Refunds for failed E3s |
NodeReleaseRegistry | 0xd6d7D4839985BFEB5d6440A1ECe594e288C128e6 | 12050048 | The ciphernode releases that can join new E3s |
BondedVotes | 0xfbd932BD297e9C7261A384925E78371C7B1FEc7a | 12050190 | Voting power of locked and bonded FOLD |
BfvPkVerifierRouter | 0xA5a95C7aBdFdC814f4E724743CFD20Cc50ba7E5B | 12050098 | Verifies the committee public key proofs |
BfvDecryptionVerifierRouter | 0x2e66AE65d8eF5399b231113aFE4E06a175F0A20E | 12050099 | Verifies the decryption proofs |
OpenVmBfvCiphertextVerifier | 0x86A4C3CF4026599ed521738cd534f1C0354f77fA | 12050706 | Ciphertext verifier for BFV. It wraps a mock. |
MockE3Program | 0xc4C83c3C94e9360C136f3cd022925b4979BD580b | 12050024 | Test E3 program. The deployment registers it. |
Faucet | 0x1B4340A65BB669267BF5DE37385B139987C3F09c | 11873271 | Test faucet. It sends 1000 FOLD and 1000 USDC |
MockUSDC is a test token. Anyone can call its mint function. The deployment uses the same token
as the ticket collateral.
What changes on Sepolia
Locally, pnpm dev:all deploys its own Interfold contracts, and the deployer owns them. On Sepolia,
the Interfold team owns the protocol contracts. This changes four steps of the local flow.
| Step | Local (pnpm dev:all) | Sepolia |
|---|---|---|
| Program registration | deployTemplate calls registerE3Program. | registerE3Program is onlyOwner. The owner of the Interfold contract must call it. |
| Protocol ciphertext verifier | deployTemplate calls setCiphertextVerifier with your receipt identity. | setCiphertextVerifier is onlyOwner. The Sepolia verifier accepts every receipt. |
| Data availability | The coordination server stores the output (mode: mock_http). | Ciphernodes read the output from Avail (mode: avail). |
| Ciphernodes | Five local nodes from scripts/dev_ciphernodes.sh. | The nodes of registered operators. Sortition needs enough of them for the committee size. |
The first two rows mean that you cannot run deployTemplate on Sepolia unchanged. The owner-only
calls revert for any other signer. The last two rows decide if an E3 of your program can complete.
The procedure below covers each row.
Before you start
- A project from the default template. See Build a Complete E3 Program.
- A deployer account with Sepolia ETH. Use any public Sepolia faucet.
- A requester account with Sepolia USDC. See Get Sepolia USDC.
- A Sepolia RPC endpoint. Hardhat uses HTTPS. The server and the client can use HTTPS or WebSocket.
- Foundry (
cast) for the checks.
Set the Sepolia variables
The Hardhat configuration of the template reads RPC_URL, PRIVATE_KEY, and MNEMONIC from .env
through dotenv. Create .env in the project root. The template .gitignore excludes it.
RPC_URL=https://<your Sepolia HTTPS RPC>
PRIVATE_KEY=0x<deployer private key>Set the same values in your shell for the cast commands, and add the Interfold address:
source .env
INTERFOLD_ADDRESS=0xb35EE9ed8BE5967daabBA8c2e817C52D0b57ac26Get Sepolia USDC
E3 fees are paid in the fee token. On Sepolia, this is MockUSDC at
0xE2e534F7Cb95777eD870CDCaCb80B1E9E2aD555B. It is a test USDC with 6 decimals, and anyone can mint
it. To get 1000 USDC, run interfold faucet --chain sepolia. You can also call faucet() on the
Faucet contract, or call mint on MockUSDC. Then check your balance:
cast call 0xE2e534F7Cb95777eD870CDCaCb80B1E9E2aD555B "balanceOf(address)(uint256)" <your address> --rpc-url $RPC_URLThe balance has 6 decimals. Requesters do not need FOLD. Only operators bond FOLD. The Faucet also
sends 1000 FOLD. One faucet call provides enough FOLD for the 100 FOLD Sepolia bond. FOLD transfers
are restricted before the token generation event, and the Interfold team mints FOLD to operators.
Record the Interfold address for the deploy script
The template deploy helpers read addresses from deployed_contracts.json, one object for each
Hardhat network name (readDeploymentArgs). Add a sepolia object next to localhost:
{
"localhost": { "...": "..." },
"sepolia": {
"Interfold": {
"address": "0xb35EE9ed8BE5967daabBA8c2e817C52D0b57ac26",
"blockNumber": 12050035
}
}
}Add the Sepolia chain to interfold.config.yaml
interfold print-env reads addresses from interfold.config.yaml. Add a sepolia entry to
chains. The deploy script adds e3_program to it.
chains:
# - name: 'localhost' ... (keep the local entry)
- name: 'sepolia'
rpc_url: 'wss://<your Sepolia WebSocket RPC>'
chain_id: 11155111
contracts:
interfold:
address: '0xb35EE9ed8BE5967daabBA8c2e817C52D0b57ac26'
deploy_block: 12050035
ciphernode_registry:
address: '0x895392D5782E78768A35B1aDfc5b0BD1faB259e4'
deploy_block: 12050030
bonding_registry:
address: '0x8f42eD45556bC8fF8949c37c7644Af9Cf321108F'
deploy_block: 12050022
fee_token:
address: '0xE2e534F7Cb95777eD870CDCaCb80B1E9E2aD555B'
deploy_block: 11856062
faucet:
address: '0x1B4340A65BB669267BF5DE37385B139987C3F09c'
deploy_block: 11873271Write a Sepolia deploy script
Create scripts/deploy-sepolia.ts. It is deploy/default.ts without the two owner-only calls. Like
the CRISP deploy script, it deploys PoseidonT3 when the network has no record for it. It deploys
an OpenVmReceiptVerifier for the guest that interfold program compile built (see
Compute Provider), so run that first.
import {
compiledOpenVmEnvironment,
deployOpenVmReceiptVerifier,
getDeploymentChain,
readDeploymentArgs,
storeDeploymentArgs,
updateE3Config,
} from '@interfold/contracts/scripts'
import hre from 'hardhat'
async function main() {
const { MyProgram__factory: MyProgramFactory } = await import('../types/factories/contracts')
const { ensureTemplateCwd, INTERFOLD_CONFIG_FILE, TEMPLATE_ROOT } = await import(
'./template-paths'
)
ensureTemplateCwd()
const { ethers } = await hre.network.connect()
const [owner] = await ethers.getSigners()
const chain = getDeploymentChain(hre)
const interfoldAddress = readDeploymentArgs('Interfold', chain)?.address
if (!interfoldAddress)
throw new Error(`No Interfold record for ${chain} in deployed_contracts.json`)
let poseidonT3Address = readDeploymentArgs('PoseidonT3', chain)?.address
if (!poseidonT3Address || (await ethers.provider.getCode(poseidonT3Address)) === '0x') {
const poseidonT3 = await ethers.deployContract('PoseidonT3')
await poseidonT3.waitForDeployment()
poseidonT3Address = await poseidonT3.getAddress()
storeDeploymentArgs(
{ address: poseidonT3Address, blockNumber: await ethers.provider.getBlockNumber() },
'PoseidonT3',
chain,
)
}
// The receipt verifier for the guest in .interfold/caches/openvm/prover.json, unless OPENVM_*
// settings name another. It deploys the Halo2 verifier from the checked artifact first.
const { receipt: verifier } = await deployOpenVmReceiptVerifier(
ethers,
compiledOpenVmEnvironment(TEMPLATE_ROOT),
)
const programId = await verifier.imageId()
const factory = await ethers.getContractFactory(
MyProgramFactory.abi,
MyProgramFactory.linkBytecode({
'npm/[email protected]/PoseidonT3.sol:PoseidonT3': poseidonT3Address,
}),
owner,
)
const program = await factory.deploy(interfoldAddress, await verifier.getAddress(), programId)
await program.waitForDeployment()
const programAddress = await program.getAddress()
storeDeploymentArgs(
{ address: programAddress, blockNumber: await ethers.provider.getBlockNumber() },
'MyProgram',
chain,
)
updateE3Config(chain, INTERFOLD_CONFIG_FILE, { MyProgram: 'e3_program' })
console.log(`MyProgram deployed at ${programAddress}`)
}
main().catch((error) => {
console.error(error)
process.exit(1)
})If your MyProgram constructor has more arguments (for example an input verifier), deploy them
first and add them to factory.deploy.
Deploy the program
Compile, then run the script on the sepolia network of hardhat.config.ts:
pnpm compile
pnpm exec hardhat run scripts/deploy-sepolia.ts --network sepoliaThe script prints MyProgram deployed at 0x.... It records the address in deployed_contracts.json
and adds e3_program to the sepolia entry of interfold.config.yaml.
Get the program registered
Check the registration and the owner of the Interfold contract:
cast call $INTERFOLD_ADDRESS "e3Programs(address)(bool)" <MyProgram address> --rpc-url $RPC_URL
cast call $INTERFOLD_ADDRESS "owner()(address)" --rpc-url $RPC_URLThe first call returns false until the owner calls registerE3Program. Send the program address
to the Interfold team and ask for registration. The task interfold:enableE3 in
packages/interfold-contracts (pnpm e3:enable --e3-address <address> --network sepolia) makes the
call when its signer is the owner. For any other signer, it stops with
Submit registerE3Program through the owner Safe.
At registration, the Interfold contract checks with ERC-165 that the program supports IE3Program
and IE3ProgramDataAvailability. The template does. A request for an unregistered program reverts
with E3ProgramNotAllowed.
Check the protocol ciphertext verifier
Every output proof must pass the ciphertext verifier that the owner set for BFV. On Sepolia, this is
OpenVmBfvCiphertextVerifier at 0x86A4C3CF4026599ed521738cd534f1C0354f77fA. It wraps
MockOpenVmReceiptVerifier at 0x226436d8A72357c7992896B37f0A2E00dCb6A341, which accepts every
receipt. Read the verifier address:
cast call $INTERFOLD_ADDRESS "getCiphertextVerifier(bytes32)(address)" $(cast keccak "fhe.rs:BFV") --rpc-url $RPC_URLRead the receipt identity and the receipt verifier of that address:
cast call <verifier address> "imageId()(bytes32)" --rpc-url $RPC_URL
cast call <verifier address> "openVmVerifier()(address)" --rpc-url $RPC_URLThe verifier records a receipt identity, but the mock receipt verifier does not check the seal
against it. The verifier decodes the proof envelope,
abi.encode(bytes seal, bytes32 paramsHash, bytes32 inputRoot), and requires the parameter hash of
the E3. Thus an OpenVM proof passes the protocol check on Sepolia when its parameter hash matches,
and so does a development-runner proof. Your program must still pass MyProgram.verify, which
checks the OpenVM receipt for real.
Do not rely on this behavior on mainnet. A non-mock ciphertext verifier checks every proof against its pinned identity.
The receipt verifier in your program affects only MyProgram.verify. It does not affect the
protocol verifier. The Interfold contract copies the current verifier into each E3 at request time,
so a later change applies only to new requests. See
Verify the Compute Provider.
Replace the local data-availability check
Sepolia ciphernodes read the output ciphertext from Avail (data_availability.mode: avail in the
Sepolia configurations). They use the block and leaf index that verifyDataAvailability returns.
The template version returns zero for both, so a Sepolia committee cannot read your output.
Delegate the check to AvailVectorXDataAvailabilityVerifier from @interfold/contracts, as
CRISPProgram does:
IDataAvailabilityVerifier public immutable dataAvailabilityVerifier;
function verifyDataAvailability(
bytes32 expectedContentHash,
bytes calldata proof
) external view override returns (IDataAvailabilityVerifier.DataReference memory receipt) {
receipt = dataAvailabilityVerifier.verifyDataAvailability(expectedContentHash, proof);
if (receipt.contentHash != expectedContentHash) revert DataAvailabilityHashMismatch(expectedContentHash, receipt.contentHash);
}Set dataAvailabilityVerifier in the constructor. Deploy the verifier with the Sepolia bridge and
VectorX addresses from AVAIL_VECTORX.sepolia in @interfold/contracts/scripts. Add
@interfold/contracts/contracts/verifiers/AvailVectorXDataAvailabilityVerifier.sol to
npmFilesToBuild in hardhat.config.ts so that Hardhat compiles it.
The coordination server must then publish the output bytes to Avail and pass the VectorX proof as
availabilityProof to publishCiphertextOutput. The template server does not do this. The
CRISP server (opens in a new tab) does, and
docs/architecture/data-availability.md (opens in a new tab)
describes the flow.
Point the server at Sepolia
In server/index.ts, change the chain of the SDK from hardhat to sepolia:
import { sepolia } from 'viem/chains'
// in createPrivateSDK()
sdkInstance = InterfoldSDK.create({
rpcUrl: RPC_URL,
privateKey: PRIVATE_KEY as `0x${string}`,
contracts: {
interfold: INTERFOLD_CONTRACT as `0x${string}`,
ciphernodeRegistry: CIPHERNODE_REGISTRY_CONTRACT as `0x${string}`,
feeToken: FEE_TOKEN_CONTRACT as `0x${string}`,
},
chain: sepolia,
thresholdBfvParamsPresetName: 'INSECURE_THRESHOLD_64',
})runProgram reads the InputPublished events from block 0. Some RPC providers limit the block
range of a log query. If yours does, set fromBlock to the deploy block of your program.
Start the program server and the coordination server. interfold print-env prints RPC_URL,
INTERFOLD_ADDRESS, REGISTRY_ADDRESS, BONDING_REGISTRY_ADDRESS, E3_PROGRAM_ADDRESS, and
FEE_TOKEN_ADDRESS for the chain:
interfold program startexport $(interfold print-env --chain sepolia)
export PRIVATE_KEY=0x<server key> CHAIN_ID=11155111
pnpm tsx ./server/index.tsThe server account pays gas for publishCiphertextOutput, so it needs Sepolia ETH.
Point the client at Sepolia
The client already includes the sepolia chain in its wagmi configuration. Start it with the
Sepolia addresses:
cd client
export $(interfold print-env --vite --chain sepolia)
pnpm devThe client requests E3s with paramSet: 0, and the SDK uses INSECURE_THRESHOLD_64 by default
(VITE_THRESHOLD_BFV_PARAMS_PRESET_NAME). Keep the two values in agreement. Sepolia accepts the
param sets 0 (insecure-64) and 2 (secure-8192) when registered. Do not submit private data with
the insecure preset. Version-7 mainnet requests use index 2; index 1 remains for older E3s. The SDK
does not include secure-8192 proof circuits. Compile matching circuits for secure proofs; see
SDK encryption proofs.
Request an E3
Open http://localhost:3000, connect a wallet on Sepolia with fee tokens, and request an E3. Each
stage waits for Sepolia blocks, so an E3 takes longer than on the local chain, where anvil makes
one block each second. To follow the E3 from a script, listen to the same events as the template:
await sdk.onInterfoldEvent(InterfoldEventType.E3_REQUESTED, (event) =>
console.log('requested', event.data.e3Id),
)
await sdk.onInterfoldEvent(RegistryEventType.COMMITTEE_PUBLIC_KEY_CHUNK_PUBLISHED, (event) =>
console.log('key chunk', event.data.e3Id),
)
await sdk.onInterfoldEvent(InterfoldEventType.CIPHERTEXT_OUTPUT_REFERENCE_PUBLISHED, (event) =>
console.log('output', event.data.e3Id),
)
await sdk.onInterfoldEvent(InterfoldEventType.PLAINTEXT_OUTPUT_PUBLISHED, (event) =>
console.log('result', event.data.plaintextOutput),
)Deploy scripts and tasks
| Location | Command | What it does |
|---|---|---|
| Template | pnpm dev:all | Runs scripts/deploy-local.ts on localhost: deployInterfold(true, false), then deployTemplate(). |
| Template | pnpm hardhat ciphernode:admin-add --ciphernode-address <address> --network localhost | Adds a ciphernode with admin privileges. It needs account impersonation (Anvil or Hardhat), so it works only on a local chain. |
packages/interfold-contracts | pnpm e3:enable --e3-address <address> --network <name> | Registers an E3 program. The signer must be the owner of the Interfold contract. |
packages/interfold-contracts | pnpm run deploy --network <name> | Deploys the protocol contracts (scripts/run.ts, deployInterfold). This is for the protocol owner. |
| CRISP repository | pnpm deploy:contracts --network sepolia | Deploys CRISP and its verifiers, and wires them only when the signer owns the Interfold contract. |
Outside the Interfold monorepo, the deploy scripts come from the published @interfold/contracts
package. With ENABLE_ZK_VERIFICATION=true, the deploy of the BFV ZK verifiers reads the
verification key hashes of the BFV aggregation circuits. In the monorepo, it reads them from
dist/circuits. Outside the monorepo, set INTERFOLD_CIRCUITS_DIR to a release circuit directory,
in the layout <preset>/<committee>/default/.... The circuits must come from the release of the
package version, for example the circuits/ directory of circuits-<version>.tar.gz from the
GitHub release. Without INTERFOLD_CIRCUITS_DIR, the deploy stops with an error.
The Sepolia deployment has these test contracts: MockUSDC, the Faucet, MockE3Program, and
MockOpenVmReceiptVerifier. The public key, decryption, and DKG verifiers are real.
deployInterfold can still deploy mock contracts on chain ID 11155111. On other chains it refuses
mocks unless ALLOW_MOCKS_ON_PRODUCTION=true is set.
Troubleshooting
| Error or symptom | Cause | Fix |
|---|---|---|
OwnableUnauthorizedAccount from the deploy | The script calls registerE3Program or setCiphertextVerifier. | Use scripts/deploy-sepolia.ts, which leaves out the owner-only calls. |
E3ProgramNotAllowed on request | The program is not registered. | Ask the owner of the Interfold contract to call registerE3Program. |
ERC20InsufficientAllowance on request | The fee token allowance is lower than the fee. | Call approveFeeToken(fee) with the fee from getE3Quote, and wait for the receipt. |
FeeExceedsMaximum on request | The fee changed after the quote. | Get a new quote and request again. |
InvalidInputDeadlineStart on request | inputWindow[0] is earlier than the block time. | Compute the window with calculateInputWindow(publicClient, duration). |
| No committee key | Sortition did not select a full committee, or the DKG did not finish. | Read the stage with getE3Stage. If it is Failed, request a new E3. |
InvalidOutput on output publication | The protocol verifier or MyProgram.verify rejected the compute proof. | Check that the service's guest has the identity your MyProgram names. |
RequestsPaused on request | Sepolia requests are paused. | Wait until the owner unpauses requests. requestsPaused() returns false then. |
| The E3 does not finish after the output | The committee cannot read the output from Avail. | Replace the local data-availability check, and publish the output to Avail. |