Bakugo32 — @TMerlini
That resolution is exactly right. commitmentRef staying scheme-neutral at the ERC-8183 layer is the correct boundary. The invariant — commitmentRef cryptographically includes inputHash — is satisfied by OCP’s observation commitment because inputHash is an explicit named field in the commitment construction.
From the OCP side: the observation hash is per-execution, not per-configuration. manifest_hash describes the agent. inputHash describes the specific input processed. Keeping those as separate fields and letting the attestation layer own the outer commitment scheme preserves verifiability at every layer independently.
Vincent — the ERC-8263 anchorProof construction uses proofHash = H(canonical observation bytes). That satisfies Bakugo32’s invariant directly — the OCP observation commitment is what gets anchored, and it includes inputHash explicitly. Worth noting in the Appendix B reference if it isn’t already.
Damon / OCP
@Bakugo32 thanks for the update and for landing on hash-scheme agnosticism. That’s the right boundary.
To confirm from our side: the OCP observation commitment keccak256(abi.encode(agentId, modelHash, inputHash, outputHash, timestamp)) satisfies the §B.x invariant cleanly. inputHash is an explicit named field in the ABI encoding, the binding is independently verifiable from the struct alone, no trust in the verifier required.
@Damonzwicker the per-execution / per-configuration distinction is sharp and worth stating explicitly for anyone reading this thread: in ccip-router, commitmentHash is strictly per-execution, it binds inputHash, outputHash, and timestamp alongside agentId and modelHash. There’s no per-configuration manifest hash in the current implementation; the agent descriptor fields are repeated in every attestation rather than factored out. Whether that’s the right long-term shape or whether a separate stable manifest_hash (agentId + modelHash, invariant across executions) should be anchored once and referenced by commitment is an open question worth carrying into the ERC-8004 thread.
On the IAttestationVerifier path: the natural mapping for ccip-router is attestationHash = commitmentHash, proof = abi.encode(WyriweAttestation, sig). The verifier contract recovers the signer, recomputes commitmentHash from the struct fields, and checks it against attestationHash , no external trust. We’re looking at shipping WyriweAttestationVerifier.sol as the reference implementation. One open question before we do: is the proof parameter in complete() expected to be caller-defined ABI, or is there a fixed encoding convention?
§B.x - Outcome Envelope Field Mapping (non-normative)
Consolidated spec text addressing the open questions from @TMerlini (#290, #293) and @Damonzwicker (#292).
Field mapping
| Outcome Envelope Field | ERC-8183 on-chain field | Set at | Description |
|---|---|---|---|
status |
job.status |
complete() / reject() |
Terminal state |
commitmentRef |
job.reason |
complete() - attested path only |
Input anchor. Hash scheme defined by verifier. |
deliverable |
job.deliverable |
submit() |
Output hash submitted by provider |
evaluator |
job.evaluator |
fund() |
Assigned evaluator address |
Cryptographic binding
commitmentRef is a bytes32 commitment to the agent input. ERC-8183 does not mandate the hash scheme, the exact construction belongs to the attestation layer and is owned by the verifier implementation.
The invariant ERC-8183 requires :
commitmentRefcryptographically includesinputHash, the keccak256 of the sanitized inputs at fund time. This is the WYRIWE anchor : it proves the outcome envelope refers to the same input that was funded, independently of what the agent delivers.
Both the ERC-8263 full-struct hash and the OCP observation commitment (keccak256(abi.encode(agentId, modelHash, inputHash, outputHash, timestamp))) satisfy this invariant. inputHash is an explicit named field in both constructions, making the binding independently verifiable from the struct alone. The ERC-8263 anchorProof construction (proofHash = H(canonical observation bytes)) also satisfies it directly, the OCP observation commitment is what gets anchored, and it includes inputHash explicitly.
Per-execution vs per-configuration
commitmentRef is strictly per-execution : it binds the inputHash for a specific job. Agent configuration (model, pipeline) is a separate concern. §B.x does not conflate the two. The per-configuration anchoring question, whether a stable manifest_hash invariant across executions should be anchored once and referenced, is worth carrying into the ERC-8004 thread rather than resolving here.
On the proof parameter @TMerlini
The bytes calldata proof in the attested complete() overload is caller-defined. ERC-8183 passes it opaquely to IAttestationVerifier.verify(attestationHash, proof), the verifier owns the decoding entirely. There is no fixed encoding convention at the ERC-8183 layer.
For WyriweAttestationVerifier.sol, the natural mapping you described is correct: attestationHash = commitmentHash, proof = abi.encode(WyriweAttestation, sig). The verifier recovers the signer, recomputes commitmentHash from the struct fields, and checks it against attestationHash. That pattern is exactly what IAttestationVerifier is designed for.
On the simple complete() path
commitmentRef is only meaningful via the attested overload. The simple complete(jobId, bytes32 reason) path stores the evaluator’s verdict hash as job.reason, not an input anchor.
@Bakugo32 , thanks for confirming the proof encoding is caller-defined. Shipped WyriweAttestationVerifier as the reference IAttestationVerifier implementation for OCP-committed attestations.
Deployed on Sepolia: 0x9515D6e53D2D45C1CFE6181943ca11C150C2bf61
Mapping:
-
attestationHash=commitmentHash=keccak256(abi.encode(agentId, modelHash, inputHash, outputHash, timestamp)) -
proof=abi.encode(WyriweAttestation, sig)
verify() recovers the signer from the EIP-712 signature, recomputes commitmentHash from the decoded struct fields, and checks both against attestationHash. No external contract calls, fully self-contained. Source in ccip-router, also available via npm (ccip-router@0.1.1).
@Bakugo32, the §B.x framing in the draft reads clean and the §11 protection is explicit. Spot-checking the External References section before the PR moves:
- ERC-8263 link in the draft 404s. Correct: ERC-8263: Onchain Proof Layer for AI Agents
- ERC-8274 link in the draft 404s. Correct: ERC-8274: AI Inference Proof Verification
- ERC-8265 link in the draft 404s; the listed title (“agentic commerce outcome envelopes”) also misnames the spec. Correct URL: ERC-8265: Prepared Transaction Envelope. Title is “Prepared Transaction Envelope” and §11 is “Appendix B: Outcome Reporting (Non-normative).”
Also on PR target: per #283, §B.x PR goes against ERC-8183 spec, not amendment to PR #1753; flagging since your #291 mentioned PR #1753.
Quick update on something I mentioned in my last reply, I referenced WyriweAttestationVerifier (ERC-8183) as the settlement verifier, but we’ve since shipped a replacement.
The core problem with ERC-8183 verify(attestationHash, proof) is that attestationHash is opaque to the caller, you have to trust that the prover computed it correctly from the right inputs. A settlement contract can’t independently verify what went in or what came out; it can only check that the hash was signed. That makes it hard to compose: if you want to slash on a specific bad output, or verify that the same input produced consistent outputs across nodes, you have no surface to do it.
ERC-8274 fixes this by making inputHash and outputHash explicit top-level parameters. The settlement contract supplies them directly — it doesn’t need to trust the prover’s hash construction. metadata carries the authorized signer identity (agentId + registry), and proof carries the cryptographic evidence. The verifier’s job is strictly L4: “did the authorized gateway attest that this input produced this output?” — nothing more.
We moved to ERC-8274 IProofVerifier. The interface changed:
Old (ERC-8183, deprecated):
verify(bytes32 attestationHash, bytes calldata proof) returns (bool)
Deployed at 0x9515D6e53D2D45C1CFE6181943ca11C150C2bf61, still live but no longer the canonical integration point.
New (ERC-8274):
verify(bytes32 inputHash, bytes32 outputHash, bytes calldata metadata, bytes calldata proof) returns (bool)
WyriweProofVerifier deployed on Sepolia: 0x001eFFa0fD1D171b164808644678F3301d8EDC96
Call params:
-
metadata = abi.encode(agentId, registry)— authorized signer identity -
proof = abi.encode(modelHash, rawInputHash, sanitizationPipelineHash, commitmentHash, timestamp, sig)— cryptographic material
The verifier recomputes commitmentHash = keccak256(abi.encode(agentId, modelHash, inputHash, outputHash, timestamp)), reconstructs the EIP-712 digest, and recovers the signer. No external calls.
WYRIWE_PROOF_VERIFIER_ABI is also exported from ccip-router@0.3.0 if you’re integrating via the npm package.
Wanted to make sure you had the right ABI, sorry for the stale reference.
@mike-diamond three corrections applied :
-
ERC-8263 → https://ethereum-magicians.org/t/erc-8263-onchain-proof-layer-for-ai-agents/28577
-
ERC-8274 → https://ethereum-magicians.org/t/erc-8274-ai-inference-proof-verification/28083
-
ERC-8265 → https://ethereum-magicians.org/t/erc-8265-prepared-transaction-envelope/28557, title corrected to “Prepared Transaction Envelope”, §11 referenced as “Appendix B: Outcome Reporting (Non-normative)”
On PR target : confirmed, §B.x goes against the ERC-8183 spec directly. That was the consensus in #283, ready to coordinate on the PR when you are.
@TMerlini the composability gap you identified is real and the move to ERC-8274 makes sense for your use case.
ERC-8183’s position : IAttestationVerifier stays opaque at the settlement layer by design, the settlement contract has no need to inspect inputHash or outputHash directly. The invariant it enforces is commitmentRef → inputHash, not the full proof structure.
The consequence is that WyriweProofVerifier (ERC-8274 IProofVerifier) cannot be passed directly as the verifier parameter in complete(), an adapter contract implementing IAttestationVerifier and delegating internally to IProofVerifier is required. That wrapper pattern works, but it’s an indirection layer. Whether a future ERC-8274-compatible complete() overload belongs in ERC-8183 is worth discussing before the audit, happy to take that up here or in the ERC-8274 thread.
§B.x updated: WyriweAttestationVerifier marked deprecated, WyriweProofVerifier added as current reference with the adapter note.
@Bakugo32 — thanks for the detailed breakdown in #299, and for updating §B.x so quickly.
One point worth surfacing: IProofVerifier is also opaque by design — the interface doesn’t prescribe how verification happens internally, only what the caller declares about the inference. The difference from IAttestationVerifier is that inputHash and outputHash are explicit top-level parameters rather than folded into an opaque attestationHash. The caller supplies them directly; the verifier decides what to do with them.
The relationship between the two interfaces is actually quite tight. commitmentHash in the IAttestationVerifier path is keccak256(abi.encode(agentId, modelHash, inputHash, outputHash, timestamp)) — which means inputHash and outputHash are already in there, just hashed together with the rest. IProofVerifier lifts them out as named parameters and puts the remaining fields (agentId, modelHash, timestamp) into metadata and proof. The adapter is thin precisely because the two are describing the same underlying verification — one just makes the I/O commitments explicit.
The practical benefit of that explicitness is forward compatibility. An IProofVerifier implementation can be swapped between proof backends — ZK, TEE, optimistic attestation — without changing the interface or the settlement contract that calls it. Each backend encodes its cryptographic material differently inside proof, but inputHash and outputHash stay stable as the shared anchor. That’s harder to achieve when the I/O commitments are folded into an opaque attestationHash that the caller can’t decompose. @TMerlini’s WyriweProofVerifier (deployed on Sepolia at 0x001eFFa0fD1D171b164808644678F3301d8EDC96) is a concrete example of this — it implements the EIP-712 / WYRIWE attestation backend today, and a zkML or TEE backend could slot into the same interface without the settlement contract changing at all.
On the complete() overload question: the adapter pattern works today, but whether a native IProofVerifier-compatible path belongs in ERC-8183 feels like the right conversation to have in the ERC-8183 thread before the audit — happy to pick it up there.
— Jimmy
Thanks @JimmyShi22 , helpful to see the commitment formula laid out explicitly: keccak256(abi.encode(agentId, modelHash, inputHash, outputHash, timestamp)) is identical in both paths, which is precisely why the adapter feels like unnecessary indirection.
If the underlying data is the same, the settlement contract doesn’t need an opaque wrapper to enforce it, it just needs an overload that accepts inputHash and outputHash directly and delegates to any IProofVerifier. The adapter pattern adds a deploy step, extra gas, and a composability seam that breaks when someone wants to verify proofs across settlement contracts without redeploying adapters.
On the WyriweProofVerifier point, building on your Sepolia reference (0x001eFFa0fD1D171b164808644678F3301d8EDC96), we’ve also deployed AttestationIndex on mainnet (0xc7BCCD785Fb994e570d0ca10D0F7899d87C82210) anchoring commitmentHash → inputHash on-chain. That’s exactly the settlement verification step ERC-8183 is describing, and it’s wired directly to IProofVerifier, no adapter needed.
+1 to getting the native IProofVerifier path into the spec before the audit phase rather than leaving it as an implementer footnote.
@JimmyShi22 @TMerlini the argument holds. If commitmentHash = keccak256(abi.encode(agentId, modelHash, inputHash, outputHash, timestamp)) is identical in both paths, the adapter is indirection without architectural justification. The native overload is the right call before the audit.
Proposed signature :
function complete(
uint256 jobId,
bytes32 inputHash,
bytes32 outputHash,
address verifier, // IProofVerifier (ERC-8274)
bytes calldata metadata,
bytes calldata proof
) external;
One question before we lock the spec : what should be stored as job.reason on this path ? Options :
-
keccak256(abi.encode(inputHash, outputHash))- compact I/O commitment -
inputHashalone - consistent with the WYRIWE anchor /commitmentRefsemantics -
keccak256(abi.encode(agentId, modelHash, inputHash, outputHash, timestamp))- fullcommitmentHash, mirrors whatWyriweProofVerifiercomputes internally
The third option would make job.reason identical across both attested paths (ERC-8183 IAttestationVerifier and ERC-8274 IProofVerifier), which simplifies the outcome envelope. But it requires the verifier to return commitmentHash rather than just bool - which changes the interface.
Happy to hear how AttestationIndex handles this on the mainnet side before we finalize.
Can answer the AttestationIndex question directly, it’s live on mainnet.
The contract stores two mappings:
mapping(bytes32 => address) public signerOf; // commitmentHash → signer
mapping(bytes32 => bytes32) public commitmentOf; // inputHash → commitmentHash
commitmentHash is the primary key. A consumer that has commitmentHash calls isRecorded(commitmentHash), one call, done. A consumer that only has inputHash needs an extra hop through commitmentOf[inputHash] to get there.
So on job.reason: Option 3. Store commitmentHash. It’s the shortest verification path, it’s the anchor both WYRIWE and OCP produce natively, and it’s the same value regardless of which attestation path got you there. Options 1 and 2 both require downstream reconstruction, unnecessary when the full commitment is already computed.
On the adapter: Bakugo32 is right to remove it. If commitmentHash = keccak256(abi.encode(agentId, modelHash, inputHash, outputHash, timestamp)) is identical across both paths, the adapter is a call boundary with no architectural justification. Native complete() is the correct shape.
One extension worth considering on the verifier interface: isRecorded() returns bool, which is enough for a gate check. But if the verifier returned commitmentHash instead, consumers could verify and get the on-chain anchor reference in a single call, useful for any downstream that wants to surface the proof to a user or chain it into a larger settlement. Not a blocker, but worth flagging before the interface locks.
For reference, the record() call and full contract are in Echo-Merlini/ccip-router , I can share the mainnet address if useful for the spec.
On job.reason: Option 3 looks right — commitmentHash = keccak256(abi.encode(agentId, modelHash, inputHash, outputHash, timestamp)) aligns with the AttestationIndex.signerOf() primary key, so any downstream observer can verify the on-chain anchor in a single call without reconstruction. That consistency across both attested paths is a nice property to lock in before the audit.
On the verifier interface: there might be a case for keeping verify() returning bool and letting the settlement contract derive commitmentHash itself. The new complete() overload already receives metadata and proof as explicit parameters — the inputs needed to reconstruct commitmentHash are right there. The reason this might matter: a zkML backend has no commitmentHash to return, a TEE backend has a different anchor concept, and if verify() returns bytes32 the meaning of that value becomes backend-specific. The interface would lose the neutrality that makes it composable across proof systems.
The commitmentHash derivation belonging in ERC-8183’s settlement logic rather than in IProofVerifier keeps the verifier as a pure cryptographic gate regardless of which consumer is calling it — ERC-8183, ERC-8275’s EscrowV1, or anything else downstream. Happy to be corrected if there’s a case where the settlement contract can’t reconstruct it from what it already holds.
Also — the ccip-router mesh is solving a genuinely important problem: making off-chain agent execution economically accountable without trusting any single operator. The settlement layer question here is exactly the missing piece that makes that possible. Looking forward to seeing it land.
The commitmentHash construction in Option 3 aligns cleanly with the WyriweAttestation struct - agentId, modelHash, inputHash, outputHash, timestamp are the same five fields. The inputHash in that formula is specifically keccak256(sanitized_input) as defined in WYRIWE, which means the commitment already encodes the full input provenance chain (raw → sanitization pipeline → final input) rather than a raw prompt hash. Worth making that explicit in the ERC-8183 spec so the field meaning is unambiguous across implementations.
On verify() returning bool , agreed. Letting the settlement contract derive commitmentHash independently is the right boundary. The verifier stays backend-agnostic and the commitment logic stays in the layer that owns it.
@Bakugo32 @TMerlini — following up on the job.reason and commitmentHash discussion from #302/#303, there’s a design update on the ERC-8274 side that resolves both questions cleanly. Sharing here since it directly affects the complete() interface.
A business layer above IProofVerifier
The v0.2 draft (ERC-8274 v0.2 draft · GitHub) introduces IAgentVerifier as an outer stateful layer that wraps IProofVerifier. Settlement contracts interact only with IAgentVerifier — they never touch metadata or need to know which proof backend is in use.
How complete() fits
The evaluator contract implements IAgentVerifiable (declaring its IAgentVerifier at deployment). The complete() overload becomes:
complete(jobId, agentId, inputHash, outputHash, proof)
No backend-specific encoding at the call site. IAgentVerifier retrieves its stored metadata and calls IProofVerifier.verify() internally.
job.reason resolved
IAgentVerifier.verify() returns (bool valid, bytes32 verificationDigest). The digest is computed from taskId + agentId + inputHash + outputHash + valid + proofProfile and returned regardless of pass or fail — the natural value for job.reason. A VerificationCompleted event carries the full preimage so any observer can independently recompute and confirm the digest (recompute → compare → confirm, per OCP).
(bool valid, bytes32 verificationDigest) =
getAgentVerifier().verify(bytes32(jobId), agentId, inputHash, outputHash, proof);
if (valid) complete(jobId, verificationDigest, "");
else reject(jobId, verificationDigest, "");
WyriweProofVerifier on mainnet requires no changes — it slots in as the inner IProofVerifier directly. Happy to discuss further or adjust based on how the complete() overload is shaping up on your end.
This lands cleanly. The verificationDigest as job.reason is the right solution to the question that was open in the earlier design, it’s deterministic, recomputable by any observer, and derived from the task and agent identifiers rather than being an opaque blob.
The outer/inner split does exactly what it needs to: settlement contracts talk to IAgentVerifier without caring what proof system is underneath, and IProofVerifier implementations like WyriweProofVerifier stay stateless and unchanged. The backward compatibility point matters, the mainnet deployment doesn’t need to move.
One thing worth making explicit in the spec: the VerificationCompleted event containing the full preimage is also the anchor for the ERC-8275 challenge window. Any party initiating a challenge needs to recompute that digest, the event is what makes that possible without trusting the settlement contract’s storage. Worth a sentence in the ERC-8183 spec noting that dependency.
@JimmyShi22 @TMerlini, the design has converged cleanly. Confirming our read and flagging one open question before we code the overload.
Validated :
-
job.reason=verificationDigestfromIAgentVerifier.verify(), deterministic, independently recomputable, consistent across both attested paths. Option 3 is locked. -
complete()overload signature:complete(jobId, agentId, inputHash, outputHash, proof), no backend-specific encoding at the call site. -
verify()returnsbool. JimmyShi22’s argument holds: returningbytes32would make the interface backend-specific.commitmentHashderivation belongs in ERC-8183’s settlement logic, not inIProofVerifier. A zkML or TEE backend has nocommitmentHashto return, keepingverify()a pure cryptographic gate preserves composability. -
inputHash=keccak256(sanitized_input)per WYRIWE, encodes the full provenance chain. Worth a clarifying note in the ERC-8183 spec so the field meaning is unambiguous across implementations.
On ERC-8275 :
VerificationCompleted carrying the full preimage is good event design independent of ERC-8275, an observer should be able to recompute the digest without trusting the settlement contract’s storage. That’s the design principle ERC-8183 is committing to.
The compatibility with ERC-8275’s challenge window is a consequence of that design, not a dependency. We’ll note it informatively in §B.x alongside the other cross-references. ERC-8183 won’t take a normative dependency on a draft standard, same boundary we’ve held on ERC-8263 and ERC-8274.
Open question before we finalize IAgentVerifiable :
If the evaluator contract implements IAgentVerifiable (declaring its IAgentVerifier at deployment), does that mean EvaluatorRegistry needs to store and verify an IAgentVerifier address per evaluator at staking time? Or is IAgentVerifiable a soft declaration, the settlement contract reads it at complete() time without the registry being aware of it? The answer affects whether this is a contract change to EvaluatorRegistry or only to AgentJobManager.
@Bakugo32 Soft declaration is the right model. EvaluatorRegistry handles staking and slashing — it shouldn’t need to know about verifier addresses. Coupling verifier registration to staking time means every new verifier type becomes a registry upgrade, and you end up with a registry that does two unrelated jobs.
The settlement contract (AgentJobManager) reads IAgentVerifiable at complete() time — the evaluator declares which verifier to use as part of the job commitment, and the settlement contract resolves it then. The registry is unaware of it by design.
This is the same separation we use in ERC-8275: arbitrators are a settlement-layer concern (resolved at resolveDispute()), not a registry concern. EvaluatorRegistry stays narrow , staking, slashing, liveness. AgentJobManager owns the verification logic. Neither needs to change when you add a new verifier type.
So: no modifications needed to EvaluatorRegistry. Only AgentJobManager needs to call IAgentVerifiable.verifier() and dispatch to it at complete().
@Bakugo32 @TMerlini — agreed with the soft declaration framing in #309. That’s exactly the right direction.
IAgentVerifiable was designed to carry exactly this semantic: the interface itself is the declaration. getAgentVerifier() is resolved at settlement time by the calling contract — no pre-registration in a separate registry required. External contracts don’t need to manage the verifier at all; IAgentVerifiable owns that responsibility entirely. This also makes implementation simpler on both sides.
The registry stays focused on staking and slashing. New verifier types can be added without registry changes, which matters as the proofSystem taxonomy grows.
Aligns with how ERC-8274 v0.2 is structured: settlement contracts call getAgentVerifier() at complete() time, not at registration time.
Confirmed, and to close the §B.x loop : the VerificationCompleted event with full preimage will be added to the External References section alongside an informative note on ERC-8275 compatibility. No normative dependency on ERC-8275. Implementation underway.