What the check proves
A commitment iskeccak256 over a ciphertext’s stored bytes. Those bytes are deterministic. The same inputs, the same keys, and the same FHE parameters produce the same ciphertext, byte for byte, on your machine as on ours.
So the audit is a recomputation:
- Read the operation and its input handles from the host chain.
- Fetch the input ciphertexts from the public archive.
- Fetch the public key material the network computes with.
- Re-run the operation with those inputs.
- Hash your result and compare it to the recorded commitment.
What you need
Pending values
Three values are not published yet. Everything else on this page works today.
You need one CommitmentRegistry address, not one per chain. A handle is derived from the ciphertext bytes, or from the operation and its input handles, and neither derivation mixes in a chain id. So handles are global, one registry records them all, and your host chain never appears in the lookup.
Step 1 works today and does not depend on any of the three. The computation graph and every input commitment come from the host chain alone.
How a ciphertext is committed
Every ciphertext in CoFHE, whether an encrypted input or a computed result, is stored in one format: the tfhe-rs value, compressed withcompress(), then written with tfhe’s safe_serialize. The commitment is keccak256 over exactly those bytes. The security zone is not in the preimage.
A handle is not the commitment, and the two relate differently depending on where the ciphertext came from.
That difference is why the registry exists. A result’s handle is fixed onchain the moment the task is created, so it cannot carry a hash of bytes nobody has computed yet.
Read the computation graph
The TaskManager publishes every operation it schedules.TaskCreated carries the result handle, the operation name, and up to three input handles.
Operation names come from
Utils.functionIdToString in ICofhe.sol, which also defines the type constants. Byte 30 holds the type in its low seven bits. Its high bit is set when the value was trivially encrypted, so mask the byte with 0x7f before you compare it to those constants. Here every handle ends in 05 00, which is EUINT64_TFHE in security zone 0. The same type, trivially encrypted, ends 85 00 and matches no constant until you mask it.
An input handle is either another task’s result, in which case you follow it and recurse, or an encrypted input. Encrypted inputs terminate the graph, and each one has its commitment onchain:
Read the commitment
getCommitment takes the version tag and the handle, and returns the recorded hash. bytes32(0) means nothing is posted for that pair.
Active. Read the status before you trust a match:
0 for Unset, 1 for Active, 2 for Deprecated, and 3 for Revoked. A status only moves forward: Active to Deprecated or Revoked, and Deprecated to Revoked. Deprecated means the version was superseded and its commitments stay readable. Revoked means the opposite. Do not trust a match under a revoked version.
A zero answer is not proof of tampering, and it has two ordinary causes.
The first is timing: a result that is still being computed has no commitment yet, because the coprocessor posts the commitment after the compute stage finishes.
The second is the version. Version 1 is still Active and holds the bulk of the earlier history. A handle committed before the cutover resolves only under version 1, and reads as zero under version 2. Retry the lookup under version 1 before you conclude anything.
To audit in bulk rather than one handle at a time, enumerate the version. getSize reports the total, and getHandles pages through it, clamping offset + limit at the total and returning an empty array once offset runs past the end.
limit small. See CommitmentRegistry for the write rules, the version state machine, and the rest of the read surface.
Fetch the inputs
The ZK Verifier archives every input it verifies, together with the proof that covered it. This store holds inputs and proofs only, not the keys. Objects are laid out by chain and security zone, sharded by the first four hex characters of the handle:0x prefix, and {shard} is its first four characters. For the verified input above, on Arbitrum Sepolia in zone 0, that is cofhe/v1/chain/421614/security-zone/0/verified-inputs/1b2c/1b2cd55ff9b0cbef294dfdc4cfb80ea6450bf0a272e45f9b3715a27bed350500.
Each object is a bincode-encoded record holding the ciphertext bytes and its metadata. The metadata carries the handle, the proof it belongs to, the submitting account, the security zone, the chain, the index within the proof, and the type. Take the ciphertext bytes out of that record. They are the same safe_serialize of a compressed value as everything else, so keccak256 over them reproduces the commitment you read from InputVerified. Check that before going further.
The metadata also gives you the proof id, which is where you find the matching object under verification-proofs. Verifying that proof is a separate check, and it tells you the input was well formed rather than that the computation was honest.
Fetch the public key material
Recomputation needs three artifacts, all public and all specific to a security zone. They live in the key store, which is separate from the verified input archive and has its own base URL.
A manifest sits alongside them at
keys/versionized/{zone}/public-material, carrying a SHA-256 digest of each artifact. Check the artifacts you downloaded against it.
Two of the three are also served over HTTP by the coprocessor, at the CoFHE URL your chain’s SDK config already holds. Both take a security zone and return hex:
Neither HTTP endpoint serves the evaluation key, and you cannot compute without it. Only the key store has it. Until that base URL is published, this is the step that blocks a full recomputation.
Re-run the operation
Start from a crate that pins tfhe-rs to the version the network runs:Cargo.toml
src/main.rs
getCommitment gave you for the result handle, the coprocessor computed honestly for that task. Walk the graph up from the encrypted inputs and you have checked the whole computation.
Parameters the keyset uses
You do not need these to recompute, because the evaluation key carries them. They are here so you can confirm the keyset you downloaded is the one you expect:When the hashes do not match
A mismatch is a serious claim, and the first ones are almost always a setup problem rather than a dishonest coprocessor. Work through these before you conclude anything:- The type. Mask byte 30 of the handle with
0x7f, and confirm it matches the type you deserialized as. Reading aeuint32handle as aCompressedFheUint64gives bytes that hash to something else. - The security zone. Byte 31 names the zone. The evaluation key, the public key, and the CRS are all per zone, so a keyset from the wrong zone produces a plausible result with the wrong bytes.
- The serialized form. Hash
safe_serializeof the compressed value. The expanded value and the compressed one are the same ciphertext and different bytes. The size limit you pass is only a guard, and never reaches the hash. - The version and the task. Confirm
getVersionStatusreturnsActive, and thatgetCommitmentreturned a non-zero hash. A result still in the compute stage has no commitment yet. - The tfhe-rs version. Confirm your pin is
=1.5.1, the version the network runs.
Where this check runs in production
Teecryptor makes the same comparison inside its enclave on every decryption request. It hashes the ciphertext bytes it is about to decrypt and checks them against the commitment anchored onchain. Your audit and its gate read the same record.Source
CommitmentRegistry.sol, the registry contract.TaskManager.sol, which emitsTaskCreatedandInputVerified.ICofhe.sol, for operation names and type constants.