Abstract
This proposal standardizes an implementation-agnostic interface for on-chain verification of Groth16 proofs over committed metrics, enabling predicate checks with minimal disclosure.
It standardizes how a verifier contract (or an MTC deployment that integrates a verifier) accepts a proof and public signals showing that a subject satisfies a predicate over a committed metric (e.g., value ≥ threshold, membership, range/delta) without revealing the underlying value. Proofs are bound to a domain (chainId + contract address) and to the current anchor root used by the credential system to prevent cross-contract/cross-chain replay and substitution. In typical deployments, the anchor root is the current Merkle root committed on-chain for the subject’s metric, which this interface treats as the verification reference.
This interface is designed to be used alongside an address-bound credential system (MultiTrustCredential), but can be integrated by any contract that maintains committed metrics and an anchor root.
Motivation
Applications and circuits need a shared, minimal way to verify that a subject’s metric satisfies a policy (e.g., “score ≥ threshold”) without exposing raw values. A common ABI enables wallets, circuits, verifiers, and indexers to interoperate across ecosystems while keeping personally identifiable information (PII) off-chain. More expressive predicates (e.g., bounded ranges or epoch-based deltas) are common in credit and risk modules, motivating a standardized extension path at the presentation layer.
Specification
The key words “MUST”, “MUST NOT”, “SHOULD”, and “MAY” are to be interpreted as described in RFC 2119 and RFC 8174.
ERC-165 Compliance (normative)
Implementations of this standard MUST implement ERC-165 and report the interface id for IMultiTrustCredentialZK.
Implementations that support IMultiTrustCredentialZKEx MUST also report the interface id for IMultiTrustCredentialZKEx.
The interface id is the XOR of all function selectors defined in this interface.
Implementations MUST NOT change function signatures in a way that alters the interface id.
External Verifier (normative)
pragma solidity ^0.8.22;
/// @notice External circuit verifier (e.g., Groth16 via snarkJS).
/// @dev This EIP's reference profile uses 6 public signals; other profiles SHOULD use IMultiTrustCredentialZKEx.
interface IVerifier {
/// @dev Circuit-specific verification with fixed-length proof/public inputs.
function verifyProof(
uint256[2] calldata a,
uint256[2][2] calldata b,
uint256[2] calldata c,
uint256[6] calldata publicSignals
) external view returns (bool);
}ZK Presentation Interface (normative)
pragma solidity ^0.8.22;
/// @title IMultiTrustCredentialZK
/// @notice Verify a ZK proof against the current MTC Core anchor and the active comparison policy.
/// @dev Additive extension to MTC Core; does not modify Core semantics.
interface IMultiTrustCredentialZK {
/// @return ok True if proof is valid AND bound to the current anchor/policy for (tokenId, metricId).
function proveMetric(
uint256 tokenId,
bytes32 metricId,
uint256[2] calldata a,
uint256[2][2] calldata b,
uint256[2] calldata c,
uint256[6] calldata publicSignals
) external view returns (bool ok);
}Extended ZK Presentation Interface (normative)
This EIP defines an additive extension to the fixed Groth16 [1] interface to support richer predicates such as bounded ranges and epoch-based delta constraints, without breaking the baseline interface.
pragma solidity ^0.8.22;
/// @title IMultiTrustCredentialZKEx
/// @notice Verify extended predicates (e.g., RANGE, DELTA) over MTC metrics.
/// @dev This interface is additive and does not replace IMultiTrustCredentialZK.
interface IMultiTrustCredentialZKEx {
/// @param tokenId Credential token id
/// @param metricId Metric schema id
/// @param predicateType Identifier of the predicate (e.g., RANGE, DELTA)
/// @param proof Opaque proof bytes (verifier-specific)
/// @param publicSignals Variable-length public inputs (predicate-specific)
function provePredicate(
uint256 tokenId,
bytes32 metricId,
bytes32 predicateType,
bytes calldata proof,
uint256[] calldata publicSignals
) external view returns (bool ok);
}The binding requirements defined in this EIP (anchor binding, address binding, comparison mask enforcement, and domain separation) MUST also apply to provePredicate.
Public Signals Mapping (normative)
This EIP fixes the order of publicSignals to match the reference circuit and verifier:
publicSignals[0] = mode // operator bitmask: GT=1, LT=2, EQ=4 (combinable); 0 allowed only for KYC-only proofs (see below)
publicSignals[1] = root // Merkle root (must equal the Core anchor for the metric)
publicSignals[2] = nullifier // circuit-defined nullifier
publicSignals[3] = addr // holder address as field element
publicSignals[4] = threshold // comparator parameter (e.g., threshold or expected value)
publicSignals[5] = leaf // commitment to the underlying value (e.g., Poseidon(score, rand)); the raw value remains privateBinding Requirements (normative)
Implementations of proveMetric MUST guarantee that a successful proof is bound to the current MTC anchor and policy for (tokenId, metricId):
- Implementations MUST obtain the current anchor root for (
tokenId,metricId) from the credential system. For the reference profile, implementations MUST call IMultiTrustCredentialCore.getMetric(tokenId, metricId) (as defined in ERC-8035) and interpret the returned leafFull as anchorRoot, then MUST verifypublicSignals[1] == anchorRoot. If the metric is revoked, per Core specification, getMetric MUST revert; ZK verification MUST NOT succeed for revoked metrics. - Implementations MUST enforce the active comparison mask from Core for the metric schema; CompareMask domain: bitwise subset of
GT=1, LT=2, EQ=4(combinations allowed), with aliases such asGTE=GT|EQandLTE=LT|EQ.
The operator encoded bymodeMUST be permitted by the mask; otherwiseproveMetricMUST revert.mode == 0MAY be used only for "KYC-only" proofs, where the Core comparison mask formetricIdis0(i.e., no comparison operators are permitted). In that caseproveMetricMUST verify membership/inclusion and binding constraints, and MUST revert if the mask is non-zero. - Implementations MUST verify
tokenId == tokenIdOf(address(uint160(publicSignals[3]))), interpretingpublicSignals[3]as a big-endian field element whose lower 160 bits map to the EVM address. If no token exists for that address, the call MUST revert.
Comparison Semantics (normative)
Let value denote the committed metric value proven inside the circuit. Implementations MUST interpret mode as a bitmask selecting any of {GT, LT, EQ}.
The proof MUST satisfy at least one selected relation. For example:
mode = GT|EQmeansvalue ≥ threshold.mode = LT|EQmeansvalue ≤ threshold.mode = GT|LTmeansvalue != threshold.
value and threshold are treated as unsigned 256-bit integers; units/scaling are defined by the metric schema.
When mode == 0 (KYC-only), no comparison is performed; only membership/binding is proven.
Extended Predicate Semantics (normative)
The extended presentation interface defines the following predicate types. Implementations MAY support each predicate type unless otherwise specified:
RANGE
A bounded range predicate proves that a metric value lies within an inclusive range: lower ≤ value ≤ upper
The proof MUST enforce both bounds within a single circuit execution. lower and upper MUST be provided as part of the publicSignals passed to provePredicate.
The exact ordering and encoding of these values is profile-specific.
DELTA (epoch-based)
A delta predicate proves that the absolute change of a metric value between epochs does not exceed a specified threshold: |value(epoch_n) − value(epoch_{n−1})| ≤ Δ
An implementation supporting DELTA predicates MUST bind the proof to an explicit epoch or version identifier and MUST prevent reuse of proofs across epochs (e.g., via epoch-bound nullifiers or statement hashes).
The epoch/version identifier MUST be encoded in the publicSignals passed to provePredicate (profile-specific).
Freshness and Replay Safety
Extended predicates involving epochs or versions MUST ensure freshness by binding the proof to the current anchor and the declared epoch/version. Reuse of proofs from prior epochs MUST be rejected.
Leaf Construction with Domain Separation (normative)
To prevent cross-contract / cross-chain replay without changing the public signals arity, this profile MUST include a domain separator in the tree leaf construction:
domain := keccak256(abi.encode(chainid(), verifierContract)) (reduced mod field)
treeLeaf := Poseidon(leaf, addr, domain)- The circuit MUST compute
treeLeafas above and prove membership in the Merkle tree withroot == publicSignals[1]. - On-chain contracts MUST rely on the proof (and current root equality) and MUST NOT attempt to reconstruct
leafbeyond verifying the proof and anchor binding.
Extensions (non-normative)
Some implementations may support proving multiple metrics, group predicates, or domain-specific predicates in a single proof. Such features are provided as non-normative extensions or via the extended presentation interface defined above.
Proof Verification and Binding (normative)
An implementation that verifies a ZK presentation MUST ensure the proof is bound to the intended context:
- the subject and metric identifiers (e.g.,
tokenId,metricId), - the current anchor root for that metric,
- and the active comparison/predicate policy for that metric.
Implementations MUST reject proofs whose bound context does not match the current on-chain state.
To prevent cross-contract replay, implementations MUST use domain separation in the leaf/statement construction so that the same underlying attribute cannot be presented against unrelated deployments.
Revocation Checks (normative)
Verification MUST fail if the target metric is not currently valid (e.g., revoked, or expired if the implementation supports expiration).
Public Signals Layout (normative)
Implementations MUST validate the expected public input length and indices before evaluating policy checks.
Optional Events
/// @notice Emitted when the verifier address is configured (e.g., during initialize()).
event VerifierSet(address verifier);
/// @notice MAY be emitted when a proof is successfully verified.
event ProofVerified(uint256 tokenId, bytes32 metricId, uint8 mode, uint256 threshold);References
{
"id": "10.1007/978-3-662-49896-5_11",
"citation-key": "10.1007/978-3-662-49896-5_11",
"type": "paper-conference",
"title": "On the Size of Pairing-based Non-interactive Arguments",
"author": [
{ "family": "Groth", "given": "Jens" }
],
"container-title": "Advances in Cryptology – EUROCRYPT 2016",
"collection-title": "Lecture Notes in Computer Science",
"volume": "9666",
"page": "305-326",
"publisher": "Springer Berlin Heidelberg",
"publisher-place": "Berlin, Heidelberg",
"issued": { "date-parts": [[2016]] },
"DOI": "10.1007/978-3-662-49896-5_11",
"URL": "https://link.springer.com/chapter/10.1007/978-3-662-49896-5_11"
}Rationale
- Fixed ABI: A Groth16 tuple is widely supported and keeps calldata minimal; other proving systems MAY adapt behind
IVerifier. - Separation of Concerns: The Core and the presentation layer are separated so deployments can integrate verification without changing Core semantics.
- Binding over leakage: By binding proofs to Core’s current anchor and the policy mask, implementations avoid value disclosure while ensuring policy is enforced. Domain separation in the leaf construction prevents cross-contract replay while preserving the 6-signal layout.
Backwards Compatibility
This interface is compatible with any MTC Core implementation that stores a commitment (leafFull) and a comparison mask policy. For this reference profile, leafFull is interpreted as anchorRoot. Gas costs for pairing precompiles vary by platform; implementations SHOULD document expected ranges.
Reference Implementation
A non-normative reference will be published with example circuits and an adapter verifier contract demonstrating:
- Reading Core’s
getMetricto retrieve the currentleafFull(root) - Enforcing the Core comparison mask against
mode - Mapping
publicSignalsper the order above - Domain-separated leaf construction:
treeLeaf = Poseidon(leaf, addr, keccak256(abi.encode(chainid(), verifierContract)))(where verifierContract is the contract implementing proveMetric / provePredicate, i.e., address(this) in the reference adapter) - Emitting
ProofVerifiedon success (optional)
Security Considerations
-
Substitution & Replay: If proofs are not context-bound, an attacker may reuse a valid proof for a different subject, metric, contract, or after a policy update. Mitigate by binding proofs to the subject/metric context (e.g., tokenId/metricId), the current anchor root, and the active policy, and by using domain separation so the same leaf cannot be reused across unrelated deployments.
-
Revocation: Revocation creates a time-of-check/time-of-use gap if clients or relayers cache old proofs. Implementations typically mitigate this by checking the current on-chain state at verification time and rejecting proofs for revoked metrics.
-
Upgrades / Governance: Changing verifiers or policy parameters can invalidate security assumptions. Operational mitigations include role-gated updates, timelocks, and emitting auditable events so indexers and clients can detect changes.
-
Malleability / Input Validation: Proof verification should fail closed on malformed proofs or unexpected public signal layouts. Defensive parsing and strict length/index checks reduce the risk of bypasses due to decoding or ABI mismatches.
Copyright
Copyright and related rights waived via CC0.
