Skip to content

Certify contract

The on-chain half of certification. eth_certify holds Certify.sol — the contract the validate and promote workers submit transactions to — plus certify_cli, a Python CLI for deploying, certifying, and verifying outside the platform.

Repo Beneficial-AI-Foundation/eth_certify (public)
License MIT
Toolchain Foundry (forge) + uv

Deployed addresses

Network Address Explorer
Mainnet 0x4f2a70eC878E9Adae88FF0c7528ebEbf83dFD83c Etherscan
Sepolia 0x125721f8a45bbABC60aDbaaF102a94d9cae59238 Sepolia Etherscan

These are what CERTIFY_ADDRESS (Sepolia) and MAINNET_CERTIFY_ADDRESS point at. See Configuration and environment variables.

What the contract does

Certify.sol is event-only — it stores no certification state. Each call emits a log entry; the chain's log history is the record. That keeps gas low and makes verification a matter of reading logs rather than calling a getter.

uint8 public constant SCHEMA_VERSION = 2;
address public immutable AUTHORIZED_CERTIFIER;

event Certified(
    bytes32 indexed identifierHash,
    bytes32 indexed contentHash,
    address indexed sender,
    string  identifier,
    bytes32 commitHash,
    string  description,
    uint8   schemaVersion,
    uint256 timestamp
);

function certify(
    string calldata identifier,
    bytes32 contentHash,
    bytes32 commitHash,
    string calldata description
) external onlyAuthorized;

function hashIdentifier(string calldata id) external pure returns (bytes32);
function hashContent(bytes calldata content) external pure returns (bytes32);
Parameter Meaning
identifier Project identifier, e.g. owner/repo. Indexed on-chain as keccak256(identifier).
contentHash Merkle root: keccak256(results_hash \|\| specs_hash [\|\| proofs_hash]). This is the commitment to what was verified.
commitHash Git commit SHA as bytes32 — a 20-byte SHA-1, zero-padded.
description Free-form, e.g. "72/72 verified".

hashIdentifier and hashContent are pure convenience helpers for off-chain callers — you can compute both locally instead.

The repo README's Contract API section is stale

eth_certify's README documents certifyWebsite(...), hashUrl(...), and events WebsiteCertified / VerificationProgressCertified. None of those exist in the current src/Certify.sol, which has exactly one entrypoint (certify) and one event (Certified). Read the Solidity source, not the README table.

Who can certify

certify() is guarded by onlyAuthorized, which reverts with UnauthorizedCertifier(caller, expected) unless msg.sender == AUTHORIZED_CERTIFIER. That address is immutable — set at deploy time and unchangeable.

On mainnet, the authorized certifier is the BAIF Gnosis Safe, not an individual key:

Role Address Type
Contract deployer 0xA8A1F614F4b86A7F7AA0F498C3a72aBbD51067d3 EOA (MetaMask)
Certification sender 0x8EAb4dB55DCEfb6D8bF76e1C6132d48D2048ef0e Gnosis Safe
Certify contract 0x4f2a70eC878E9Adae88FF0c7528ebEbf83dFD83c Contract

The deployer's role was one-time; the ongoing authority is the Safe. Full reasoning: doc/trust_model.md.

This constrains how the promote worker can submit

The promote worker takes a MAINNET_PRIVATE_KEY. A plain EOA signing with that key can only certify if that EOA is the contract's AUTHORIZED_CERTIFIER. Where the authorized certifier is a Safe, direct EOA submission reverts and the transaction must go through the Safe — which is what the mainnet/certify-mainnet.sh helper and certify_cli --safe … --execute exist for.

Before enabling MAINNET_CERTIFY_ENABLED=1, read AUTHORIZED_CERTIFIER() off the deployed contract and confirm it matches the key or Safe you intend to submit with. Getting this wrong costs a reverted transaction, not a bad anchor.

Using certify_cli directly

git clone https://github.com/Beneficial-AI-Foundation/eth_certify.git && cd eth_certify
forge install
uv sync
cp .env.example .env    # RPC URLs, keys, CERTIFY_ADDRESS
Command Purpose
uv run python3 -m certify_cli deploy --network mainnet Deploy a contract instance
uv run python3 -m certify_cli certify --network sepolia Certify from certify.conf (EOA)
uv run python3 -m certify_cli certify --network mainnet --safe 0x... --execute Certify via Gnosis Safe
uv run python3 -m certify_cli verify --rpc-url https://... --contract 0x... Verify an existing certification

Config split: .env holds secrets (MAINNET_RPC_URL, MAINNET_PRIVATE_KEY, SEPOLIA_RPC_URL, SEPOLIA_PRIVATE_KEY, CERTIFY_ADDRESS); certify.conf holds what to certify (CERTIFY_SOURCE, CERTIFY_DESCRIPTION).

Idempotency

Both workers scan Certified events for a matching identifierHash + contentHash before submitting, so a replayed job does not double-anchor. Both identifierHash and contentHash are indexed, which is what makes that scan cheap.

Path Default on scan failure
Sepolia (validate) Fail-open — submit anyway. Override with ETH_CERTIFY_DEDUP_REQUIRED=1.
Mainnet (promote) Fail-closed — retry the job rather than risk a duplicate. Override with MAINNET_CERTIFY_DEDUP_REQUIRED=0.

Set *_DEDUP_FROM_BLOCK to the contract's deploy block. Public RPCs cap eth_getLogs ranges, so an earliest scan fails and — under fail-closed — forces endless retries.

Run at most one promote_processor when mainnet is enabled: the scan-then-submit sequence is a check-then-act race under at-least-once delivery.

Further reading in eth_certify

Doc Topic
doc/trust_model.md Address roles and the trust chain
doc/mainnet_deployment.md Mainnet deployment guide
doc/safe_integration.md Gnosis Safe integration
doc/computing_hashes_locally.md Reproducing the content hash yourself
doc/badge.md Certification badge