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 |
Related¶
- Testnet — Sepolia anchoring on the validate path
- Mainnet — the promote pipeline
- Verify it yourself — reproduce and check a certificate end to end
- Message contracts