Enclave is now The Interfold.
Build
Tutorials
Deploy to Sepolia

Deploy to Sepolia

Testnet

This 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.

ContractAddressDeploy blockRole
Interfold0xb35EE9ed8BE5967daabBA8c2e817C52D0b57ac2612050035Requests, stages, fees, and outputs of each E3
CiphernodeRegistry0x895392D5782E78768A35B1aDfc5b0BD1faB259e412050030CiphernodeRegistryOwnable: committees and committee keys
BondingRegistry0x8f42eD45556bC8fF8949c37c7644Af9Cf321108F12050022Operator bonds and tickets
SlashingManager0xa82F589B29BD5c2392b7127Cba8727Af2bbEd84D12050027Slashing of ciphernodes that break protocol rules
MockUSDC (fee token)0xE2e534F7Cb95777eD870CDCaCb80B1E9E2aD555B11856062Test USDC, 6 decimals. Anyone can mint it. It pays E3 fees.
InterfoldToken (FOLD)0x3192Ee1FeE48159807058aA78038cf80cB3450Cb11873112Governance and utility token. Operators bond FOLD.
InterfoldTicketToken (tFOLD)0xE7cCF95F36131340bdAB122C4cc09787D28217e512050025Non-transferable operator tickets for sortition
E3RefundManager0x272f3A25E30990A7C30b7B90a240e7afe436534912050041Refunds for failed E3s
NodeReleaseRegistry0xd6d7D4839985BFEB5d6440A1ECe594e288C128e612050048The ciphernode releases that can join new E3s
BondedVotes0xfbd932BD297e9C7261A384925E78371C7B1FEc7a12050190Voting power of locked and bonded FOLD
BfvPkVerifierRouter0xA5a95C7aBdFdC814f4E724743CFD20Cc50ba7E5B12050098Verifies the committee public key proofs
BfvDecryptionVerifierRouter0x2e66AE65d8eF5399b231113aFE4E06a175F0A20E12050099Verifies the decryption proofs
OpenVmBfvCiphertextVerifier0x86A4C3CF4026599ed521738cd534f1C0354f77fA12050706Ciphertext verifier for BFV. It wraps a mock.
MockE3Program0xc4C83c3C94e9360C136f3cd022925b4979BD580b12050024Test E3 program. The deployment registers it.
Faucet0x1B4340A65BB669267BF5DE37385B139987C3F09c11873271Test 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.

StepLocal (pnpm dev:all)Sepolia
Program registrationdeployTemplate calls registerE3Program.registerE3Program is onlyOwner. The owner of the Interfold contract must call it.
Protocol ciphertext verifierdeployTemplate calls setCiphertextVerifier with your receipt identity.setCiphertextVerifier is onlyOwner. The Sepolia verifier accepts every receipt.
Data availabilityThe coordination server stores the output (mode: mock_http).Ciphernodes read the output from Avail (mode: avail).
CiphernodesFive 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.

.env
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=0xb35EE9ed8BE5967daabBA8c2e817C52D0b57ac26

Get 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_URL

The 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:

deployed_contracts.json
{
  "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.

interfold.config.yaml
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: 11873271

Write 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.

scripts/deploy-sepolia.ts
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 sepolia

The 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_URL

The 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_URL

Read 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_URL

The 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:

contracts/MyProgram.sol
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:

server/index.ts
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 start
export $(interfold print-env --chain sepolia)
export PRIVATE_KEY=0x<server key> CHAIN_ID=11155111
pnpm tsx ./server/index.ts

The 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 dev

The 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

LocationCommandWhat it does
Templatepnpm dev:allRuns scripts/deploy-local.ts on localhost: deployInterfold(true, false), then deployTemplate().
Templatepnpm hardhat ciphernode:admin-add --ciphernode-address <address> --network localhostAdds a ciphernode with admin privileges. It needs account impersonation (Anvil or Hardhat), so it works only on a local chain.
packages/interfold-contractspnpm e3:enable --e3-address <address> --network <name>Registers an E3 program. The signer must be the owner of the Interfold contract.
packages/interfold-contractspnpm run deploy --network <name>Deploys the protocol contracts (scripts/run.ts, deployInterfold). This is for the protocol owner.
CRISP repositorypnpm deploy:contracts --network sepoliaDeploys 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 symptomCauseFix
OwnableUnauthorizedAccount from the deployThe script calls registerE3Program or setCiphertextVerifier.Use scripts/deploy-sepolia.ts, which leaves out the owner-only calls.
E3ProgramNotAllowed on requestThe program is not registered.Ask the owner of the Interfold contract to call registerE3Program.
ERC20InsufficientAllowance on requestThe fee token allowance is lower than the fee.Call approveFeeToken(fee) with the fee from getE3Quote, and wait for the receipt.
FeeExceedsMaximum on requestThe fee changed after the quote.Get a new quote and request again.
InvalidInputDeadlineStart on requestinputWindow[0] is earlier than the block time.Compute the window with calculateInputWindow(publicClient, duration).
No committee keySortition 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 publicationThe protocol verifier or MyProgram.verify rejected the compute proof.Check that the service's guest has the identity your MyProgram names.
RequestsPaused on requestSepolia requests are paused.Wait until the owner unpauses requests. requestsPaused() returns false then.
The E3 does not finish after the outputThe committee cannot read the output from Avail.Replace the local data-availability check, and publish the output to Avail.

Next steps