Skip to content

[DOCS] cofhe-components/verify-commitments: recompute a ciphertext and check its commitment - #81

Merged
haimbj1 merged 6 commits into
mainfrom
docs/DOC-62-verify-commitments
Sep 16, 2026
Merged

haimbj1 merged 6 commits into
mainfrom
docs/DOC-62-verify-commitments

Conversation

@haimbj1

@haimbj1 haimbj1 commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Adds a Deep Dive page that shows a reader how to audit CoFHE without trusting it.

DOC-62.

Why

We claim every ciphertext is anchored in the CommitmentRegistry, and that the decryption path enforces the check. A reader had no way to test that claim. The accountability story stayed theoretical.

What the page does

It documents the audit as a recomputation, not a byte fetch:

  1. Read the operation and its input handles from TaskCreated on the host chain.
  2. Fetch the input ciphertexts from the verified input archive.
  3. Fetch the key material the network computes with.
  4. Re-run the operation yourself with tfhe-rs.
  5. Hash your result and compare it to the commitment recorded onchain.

A match means the coprocessor ran the operation the contract asked for. The reader never learns a plaintext, so the computation is auditable while the data stays confidential.

Three values we are waiting on

The page is written in full, with these marked as pending in one table.

  • CommitmentRegistry address. One address, not one per chain: handles are derived from ciphertext bytes, or from the operation and its input handles, and neither mixes in a chain id. So a single registry records every host chain and the lookup never mentions your chain. The contract, its interface, and the version tag are all settled; only the deployment target is open.
  • Verified input archive base URL. Holds the inputs and their proofs. Object layout is documented in full.
  • Key store base URL. A separate store, holding the evaluation key, the public key, the CRS, and the digest manifest.

There is no way for a reader to discover the registry address today. It is not in the storage key, not in the SDK chain config, and not reachable onchain. The poster reads it from config, either inline or from a file the deployer writes. If we add it to the SDK chain config alongside coFheUrl, the page can resolve it the same way it already resolves the CoFHE URL, and it stops being a hardcoded value we have to maintain here.

Worth flagging beyond the values: GetNetworkPublicKey and GetCrs do not serve the evaluation key, and you cannot recompute without it. It only lives in the key store. Both stores currently refuse anonymous reads, so publishing the URLs is necessary but not sufficient. Someone has to open read access, or a reader following the page gets a 403.

Everything else works today

Verified live:

  • Both cast logs commands, against Arbitrum Sepolia. The example handles and commitments in the page are real events.
  • Both HTTP endpoints, through the CoFHE URL that @cofhe/sdk@0.7.1 already ships for the chain.
  • The Rust sample compiles against tfhe-rs 1.5.1. It also documents the dependency pin you need, because a fresh resolve picks a tfhe-zk-pok that fails to build.

Checks

lint-docs.py, vale, check-versions.py --docs, mint validate, and mint broken-links --check-anchors all clean.

@mintlify

mintlify Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
fhenix 🟢 Ready View Preview Sep 15, 2026, 3:21 PM


| Ciphertext | Handle | Where its commitment lives |
| --- | --- | --- |
| Encrypted input | The commitment with its last two bytes replaced by metadata: byte 30 carries the type, byte 31 carries the security zone | `InputVerified` on the host chain, and the registry |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Byte 30 isn't only the type — its top bit is the trivial-encrypt flag.

fhe-engine src/types.rs:31-34:

TRIVIAL_ENCRYPT_AND_TYPE_BYTE = 30
TYPE_MASK = 0x7f
TRIVIAL_ENCRYPT_FLAG = 0x80

So byte 30 is trivial_flag | type. A trivially-encrypted euint64 handle ends 85 00, not 05 00, and matches no ICofhe constant. This bites readers at line 98, where you tell them to read byte 30 against those constants.

Suggest: "byte 30 carries the type in its low 7 bits, with the high bit set when the value was trivially encrypted; mask with 0x7f before comparing to the ICofhe constants."

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in both places. The handle table now says byte 30 carries the type and the trivial-encrypt flag. The text at line 98 says byte 30 holds the type in its low seven bits, with the high bit set when the value was trivially encrypted, so mask with 0x7f before comparing to the ICofhe constants.

I added your 85 00 example there, since that is where it bites: EUINT64_TFHE = 5 in ICofhe.sol:89, so 0x80 | 0x05. The term links to the Trivial Encryption page on first mention.

$VERSION 0 100 --rpc-url $REGISTRY_RPC
```

Keep `limit` small. See [CommitmentRegistry](/deep-dive/cofhe-components/commitment-registry) for the write rules, the version state machine, and the rest of the read surface.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing read: getVersionStatus(bytes32).

CommitmentRegistry.sol:26 has enum VersionStatus { Unset, Active, Deprecated, Revoked }, and teecryptor's own note on that enum says commitments under a Revoked version must not be trusted. Right now the page tells the reader to look up version 2 (hard-coded) and trust a match — but a matching hash under a revoked version proves nothing.

Worth one more cast call here:

cast call $REGISTRY "getVersionStatus(bytes32)(uint8)" $VERSION --rpc-url $REGISTRY_RPC

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added. "Read the commitment" now carries a getVersionStatus(bytes32)(uint8) call after the version-tag warning, with the four values and the forward-only transitions from setVersionStatus (Unset->Active, Active->Deprecated or Revoked, Deprecated->Revoked), and the line that a match under a revoked version proves nothing.

One thing I deliberately did not write: teecryptor probes the status once at boot and only logs a warning when it is not Active (src/main.rs:708-723). It does not re-check per request. So the page tells the auditor to read the status, and stops short of claiming the enclave enforces it.


Compression is deterministic. It is a modulus switch whose noise-reduction step picks its candidate by a fixed measure, with no randomness. The same inputs give the same bytes on every run and every machine.

If the value this returns equals what `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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The page never says what to do when the hashes don't match, and line 20 ("A mismatch means it did not") points straight at fraud.

Realistically the first several mismatches anyone hits will be their own setup: wrong tfhe-rs patch version, wrong security zone's keyset, wrong EUINT* type for the handle, or hashing expanded bytes instead of the stored compressed form. A short checklist before "the coprocessor lied" would save readers a lot of grief and save us the reports:

  • confirm byte 30's type matches the type you deserialized as
  • confirm the keyset zone matches byte 31
  • confirm you hashed safe_serialize(compress(result)), not the expanded value
  • confirm the version is Active and the task actually finished

...then, if it still mismatches, here's where to report it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added ## When the hashes do not match, with your four checks plus the tfhe-rs pin. Line 20 now links to it instead of pointing straight at fraud.

I left out the "where to report it" line. There is no channel we can point a reader at today, and inventing one fails the STYLE.md rule on unverifiable claims. It goes in as soon as there is one.

use tfhe::{CompressedFheUint64, CompressedServerKey};
use tiny_keccak::{Hasher, Keccak};

const SIZE_LIMIT: u64 = 1 << 32;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: the network uses 1 << 30 (1 GB) — rust-common's SAFE_SERIALIZATION_SIZE_LIMIT, which is what every cofhe service passes to safe_serialize/safe_deserialize.

The limit is only a guard and isn't part of the serialized bytes, so the hash is unaffected either way. But there's no reason for the sample to diverge from the constant the network actually uses — suggest 1 << 30.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to 1 << 30, matching rust-common/src/safe_serde.rs:13. I also added a line to the new checklist saying the limit never reaches the hash, so nobody chases it as a mismatch cause.

…ut archive are separate, and one registry serves every chain

## Where this check runs in production

[Teecryptor](/deep-dive/cofhe-components/teecryptor) makes the same comparison inside its enclave on every decryption request. It hashes the ciphertext bytes it is about to decrypt and refuses any that do not match the commitment anchored onchain. Your audit and its gate read the same record.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Teecryptor's baked testnet and staging policies both set warning_instead_of_enforcement, so the gate logs a mismatch and still decrypts (src/env_policy.rs, testnet_policy_is_commitment_warn_only). A public page must not claim an enforcement we do not run yet; say the gate runs in warn-only mode during rollout.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed. env_policy.rs bakes warning_instead_of_enforcement = true for testnet and staging, the test at line 275 pins it, and main.rs:224 says failures are logged but the decrypt is allowed.

The section no longer claims a refusal. It says the gate hashes and checks, and a Warning underneath says the gate runs warn-only during rollout: a mismatch is logged and the decryption still proceeds, moving to enforcement once handles under the earlier version have aged out.


The answer is `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. A result that is still being computed has no commitment yet, because the coprocessor posts the commitment after the compute stage finishes.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+The answer is 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. A result that is still being computed has no commitment yet, because the coprocessor posts the commitment after the compute stage finishes.
+
+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.
+
+```bash

Version 1 is still Active on the testnet registry and holds most of the history, so pre-cutover handles return zero under version 2. This paragraph names only "still computing" as a cause of zero, which sends readers to the wrong conclusion. Add the pre-cutover case and tell them to retry under version 1.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, and worse than you stated. I queried the registry: version 1 is Active with 34,474,546 commitments, version 2 Active with 50,229,974. So a pre-cutover handle is a normal, common case, not an edge case.

The paragraph now gives two causes for a zero. Timing, as before, and 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 the reader is told to retry under version 1 before concluding anything.


| Value | Why it is missing |
| --- | --- |
| CommitmentRegistry address | The registry chain is not fixed yet. The contract, its interface, and the version tag are settled; only the deployment target is open. |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Teecryptor bakes a deployed registry address per environment, so the deployment target is decided, not open. Reword this as a publication decision we have not made, or publish the testnet address and let the page work end to end.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, the target is decided: the address is baked per environment via include_str!, and the env-var path is #[cfg(feature = "mock")] only.

Reworded as a publication decision rather than an open deployment target: the registry is deployed and the coprocessor already writes to it, the contract and interface and version tag are settled, and what is open is publishing the address on this page.

We are holding the address until Haim confirms which one to publish, so the page still ships with it pending and the three values stay in the table.

Comment thread docs.json Outdated
"deep-dive/cofhe-components/acl",
"deep-dive/cofhe-components/plaintext-storage",
"deep-dive/cofhe-components/commitment-registry",
"deep-dive/cofhe-components/verify-commitments",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indent this to 14 spaces to match the nine sibling entries.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, 8 to 14 spaces, matching the nine siblings.

…on 1 zero case, and the designed archify diagram
…reatment, zone-filled header band per component
@haimbj1
haimbj1 merged commit f4de9f1 into main Sep 16, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants