view contract. It checks that enough selected nodes signed the payload under the stated registry version, and optionally that the payload is fresh. Your contract owns source and threshold policy, replay protection and value decoding.
Install
The interface, result codes and consumer library live inmolpha/evm-verifier (Apache-2.0). Add it as a Foundry dependency:
chivchyn is the release tag that matches the deployed verifier.
remappings.txt
>=0.8.4 <0.9.0 and import nothing outside the package. The Verifier contract itself (Solidity 0.8.31, Cancun) is only needed if you run the repository’s tests. Pin a deployed verifier address from Deployments.
Verify an attestation
1
Accept the attestation as calldata
Your function takes an
IVerifier.Attestation calldata: the signed payload plus its aggregate signature. A relayer, keeper or user submits it in the transaction that uses the value.2
Check source, threshold, freshness and signature
MolphaLib.requireValid checks that sourceId matches yours and that signaturesRequired is at least your minimum, then calls verify with your maxAge. It reverts on the first failure.3
Guard against replay
verify is view, so the same attestation verifies any number of times. latest.acceptNewer accepts only a strictly newer timestamp.4
Decode the value and act
value is an opaque bytes32. Decode it with your source’s encoding, for example MolphaLib.asInt256.examples/SettleOnWord.sol in the repository, which also has LatestStore.sol and StructuredPayload.sol. To get a sourceId, see Feeds.
Without the library, call verify and apply the same checks yourself:
signaturesRequired as a minimum (>=), never as equality. sourceId does not commit the threshold: whoever requests the attestation chooses it. The full list of consumer duties is in the consumer checklist.
For unit tests, src/test-utils/MockVerifier.sol exposes a verify with a configurable result (setResult, setResultFor) and no cryptography.
Build the arguments off-chain
buildEvmVerifierArgs maps the Attestation returned by sdk.gateway.requestSignedData (see the backend quickstart) onto the on-chain structs. It has no viem or ethers dependency. maxAge is required: seconds, 0 to disable freshness.
eth_call, encodeEvmVerifyCalldata({ attestation, maxAge }) returns the calldata and parseEvmVerifyResult also accepts the 64-byte return data.
Consumer library
MolphaLib (src/consumer/MolphaLib.sol) packages the checks verify leaves to you. It holds no storage and runs the cheap calldata comparisons before calling the verifier.
Checks run in this order:
The replay guards revert with
NotNewer(last, actual) and AlreadyConsumed(key). With NO_MAX_AGE, acceptNewer enforces ordering but not recency, so the first attestation a contract accepts can be arbitrarily old.
Reference
Structs
Defined inIVerifier. Payload field order is the order of the signed message.
i of signersBitmap maps to 0-based node index i in that registry version. Indices can change between versions, so an index is valid for one version only.
verify
0x67e2907b. Both structs are static, so calldata is the selector followed by nine 32-byte words: value, sourceId, registryVersion, signaturesRequired, timestamp, signature, commitment, signersBitmap, maxAge.
maxAge is in seconds and is compared against timestamp / 1000 and block.timestamp. An attestation passes while block.timestamp - timestamp / 1000 <= maxAge; 0 skips the check. Calldata that does not ABI-decode (for example a signaturesRequired word above 255) reverts in the decoder instead of returning a code.
Checks run in this order and stop at the first failure:
- Calldata guards:
signaturesRequired,signersBitmap,signatureorcommitmentis zero,signature >= Q, or fewer set bits thansignaturesRequired→R_MALFORMED. - Freshness, only when
maxAge != 0: timestamp in a future second →R_MALFORMED; older thanmaxAge→R_STALE. - Registry version lifetime →
R_BAD_REGISTRY_VERSION,R_NOT_YET_ACTIVEorR_VERSION_EXPIRED. - Signers outside the derived selection set →
R_BAD_QUORUM. Selection math is on Cryptography. - Aggregate key is the point at infinity →
R_BAD_AGGREGATE. - Schnorr check fails →
R_BAD_SIGNATURE.
Result codes
Defined insrc/libs/VerifyCodes.sol. The numbering is shared with the Starknet verifier.
Registry versions
Every registry change publishes a new version whoseactivatesAt is the block timestamp of that transaction. A version accepts payloads whose timestamp / 1000 lies in [activatesAt(v), activatesAt(v + 1) + 60]; the latest version has no upper bound. The test uses the payload timestamp, not the current block time, so a round signed inside that window keeps verifying with maxAge = 0. Pass the exact registryVersion the nodes signed under. See Registry versions.
Useful reads: getRegistryVersion(), activatesAt(uint256), retiredAt(uint256), isLatestVersion(uint256), getTotalNodes(), isNode(address), nodeStatus(address) (0 never registered, 1 active, 2 retired) and redundancyBuffer(). Version-keyed reads revert with InvalidRegistryVersion for a version that was never published.
Admin functions
These belong to the verifier’s owner (SoladyOwnable); other callers revert with Unauthorized(). Each call publishes a new registry version and emits RegistryAdvanced(newVersion, newRoot, op).
Gas
Measured withtest/gas/Verifier.gas.t.sol (VerifierGasTest, redundancy buffer 2, maxAge = 0, Solidity 0.8.31, Cancun, IR, 1M optimizer runs); re-run for your configuration:
Totals include calldata gas and the 21,000 base. Cost grows with the number of signers, not the registry size:
verify reads only the signers’ keys and does one EC addition per signer.
Report vulnerabilities through SECURITY.md, not public issues. Trust assumptions are on Security model.
Next steps
Smart contract quickstart
Request an attestation and verify it end to end.
Deployments
Verifier addresses per network.
Consumer checklist
Freshness, replay and threshold rules for every chain.