Skip to main content

Why a separate registry chain?

The Threshold Network needs to confirm that the ciphertext it’s about to decrypt is exactly the one the FHE Engine produced (not a tampered or stale handle). The natural place to anchor that proof is on-chain, but doing it on every host chain would force the network to maintain N RPC paths and pay gas on N chains for every FHE operation. Instead, the coprocessor posts commitments to a single registry chain (currently Arbitrum One), and the Threshold Network only watches that one. This is why the host-chain CTRegistry (which maps temporary → final ciphertext hashes inside one chain’s lifecycle) and CommitmentRegistry (which records the canonical commitment for every produced ciphertext, cross-chain) are deliberately distinct components.

Storage shape

commitments is the source-of-truth lookup. handlesByVersion is an array kept in parallel so paginated enumeration is O(limit) instead of O(total). Storage lives at the ERC-7201 slot derived from cofhe.storage.CommitmentRegistry, so the contract is upgrade-safe.

Version lifecycle

version is an opaque bytes32 tag chosen by the coprocessor when FHE parameters change (see the FHE Engine COMMITMENT_VERSION notes). Every version moves through a small state machine:
Owner-only setVersionStatus(version, newStatus) enforces these transitions and reverts with InvalidVersionTransition otherwise. The transition emits VersionStatusChanged(version, oldStatus, newStatus).

Roles and write surface

Non-poster posts revert with OnlyPosterAllowed(caller). In production, the blockchain-poster service holds the only poster role and signs through OpenZeppelin Relayer.

Writing commitments

Both functions batch-write (version, handle) → commitHash rows and require:
  • version is in Active state — otherwise reverts with VersionNotActive(version).
  • handles.length == commitHashes.length and > 0 — otherwise LengthMismatch / EmptyBatch.
  • Each commitHash != bytes32(0) — otherwise ZeroCommitHash(handle).
The difference is in how duplicates are handled: postCommitments emits CommitmentsPosted(version, batchSize). postCommitmentsSafe emits CommitmentsPostedSafe(version, newlyPosted, skipped) so the off-chain caller can tell whether the round did real work. Both enforce write-once per (version, handle) — a commitment can never be overwritten, only superseded by writing the same handle under a new version.

Reading commitments

The paginated getHandles is the recommended way to enumerate a version — getSize first to compute pages, then getHandles(version, offset, pageSize) in a loop.

Events

Upgrades

The contract is UUPSUpgradeable. _authorizeUpgrade is gated by onlyOwner. The constructor calls _disableInitializers() so the implementation contract itself can never be initialized — initialization happens through the proxy via initialize(initialOwner, initialPoster).

Source