ERC-8274: AI Inference Proof Verification

@JimmyShi22 agreed, and it’s a free win rather than just a simplification: proofSystem() already returns the paradigm string (“tee/nitro”, “zk/sp1”, etc.) and agentProofProfile/proofProfile() already commits the exact-config discriminator within that paradigm (circuit version, enclave measurement, committee pubkey – hashed). classTag/schemeTag in instanceId would have duplicated a distinction the interface already makes elsewhere. My original worry – a consumer having to enumerate every scheme variant just to answer a class-level question – is already answered by proofSystem() today, not by instanceId at all. So instanceId narrows back to exactly what you described: the deployment instance’s canonical per-chain identifier, one job, nothing else.

Pushed the updated PR – dropped classTag/schemeTag from both the spec text and the reference implementation, kept the CAIP-2 chainId change (unaffected by this): feat(erc-8274): self-describing instanceId (classTag||schemeTag) + CAIP-2 chainId by babyblueviper1 · Pull Request #3 · JimmyShi22/ERCs · GitHub

Should be a small, clean diff to review now.

Coming at this from having just shipped the same shape of interface. ERC-8354 (Confidential Agent Policy Verdicts) defines a prover-agnostic verifier boundary, verifyProof(programKey, publicInputs, proof), with a Noir circuit compiled to an UltraHonk verifier that runs on-chain behind it. So the abstraction here holds up in practice, not just on paper.

The IProofVerifier / IAgentVerifier split is the right cut, and it rhymes with something we just landed in the 8354 thread, separating the action-level property (what the proof establishes about a specific claim) from the layer above it (application authorization, and further up, whether the verifier itself is trustworthy). babyblue is in both threads, so the resonance is not an accident.

One framing that might help the OCP discussion. Recompute-and-compare and proof-based verification are orthogonal, not competing. Some backends are publicly recomputable, some are zk proofs, and some are confidential and cannot be recomputed at all. 8354 lives in that last corner, the policy is never revealed, so there is nothing to recompute. A unified verifier interface is stronger if it does not assume recomputability, which makes OCP one profile among several rather than the base layer everything reduces to. IProofVerifier already reads that way to me, which is good.

I left three concrete notes on the PR rather than clutter here.

And a composition note, since it is close to home. A CAPV verdict is itself a verified agent action, so 8354 could sit on top of this, using an 8274 verifier as its backend. Happy to be a test integrator if that is useful.

2 Likes

Not an accident, and worth confirming the composition works in the direction you named. A CAPV verdict sitting on top of an 8274 verifier as its backend is exactly the shape — the CAPV’s verifyProof establishes the action-level property (the confidential policy ran, produced this output), and IAgentVerifier is the layer above that asks whether the verifier itself is trustworthy for this call, which is the same split @helmymekaoui-web just landed over on t/29088 (action-level integrity vs interpreter/verifier-level fidelity, orthogonal, don’t conflate). So the resonance holds structurally, not just because you’re citing the same threads.

Concretely, where I’d want the seam: our own /review verdicts are the thing an IAgentVerifier implementation could wrap — a CAPV verifyProof call succeeding tells a consumer “this confidential policy check ran and returned X,” but it doesn’t tell them whether X was the right judgment call, only that the claimed policy produced it faithfully. That’s a second, separate question (was the policy itself sound for this case) that a signed /review verdict answers independently, sitting beside CAPV rather than inside it — same non-overlapping-layers principle you’re describing for OCP vs proof-verification.

Taking you up on test-integrator: happy to run a real interaction through both — a CAPV-verified confidential decision, with our own independent /review verdict issued on the same input, published to /ledger so the two can be checked against each other after the fact rather than just described. If you have a minimal fixture (the interface shape + a toy policy) I can point a real call at, that’s the concrete next step rather than another round of “the layers compose.”

1 Like

Yes, let’s do it. A real interaction through both is the right test, and it maps cleanly onto the layering. IProofVerifier, or a zk backend behind it, attests the inference, and CAPV sits one level over that, proving the action the agent then takes was permitted by a policy that stays secret. Same action bytes on both sides, two different guarantees, no overlap. You already wired a CAPV composed example once with invinoveritas, so we have a shape to reuse.

Concretely I would run one action end to end. ERC-8274 verifies the model produced the output, CAPV produces a verdict that the resulting action clears a confidential allowlist, the guard consumes it and burns the nullifier. If it holds, that is the two corners meeting on one flow, recomputable inference under you and confidential policy under me.

Separate note. I went GitHub - trustless-ai/agent-ercs: ERCs and base implementations for the trustless AI agent stack · GitHub and read agent-ercs and I like it a lot :slight_smile:. @babyblueviper1 @JimmyShi22 .

CAPV already sits in that dependency graph, it requires ERC-8004 and composes with ERC-8312, and the verify/ category does not have a confidential primitive yet. I would love to contribute there and become a member, whether that is CAPV slotting into verify/ as the confidential option or just helping on the existing verify/ ERCs. Point me at where it is useful.

1 Like

Wired up my side so this is easy to run. There is a composed harness in the CAPV repo, confidential-agent-policy-verdicts, with a walkthrough in COMPOSED_RUN.md. @babyblueviper1

Two levels. ComposedVerify.t.sol runs the shape with a mock so you can see it in one command, no setup. ComposedLiveRun.t.sol calls a real deployed IProofVerifier, you just pass ERC8274_VERIFIER, RPC_URL, and the inference proof as env vars and it forks your chain and calls the real thing. No code change on your side, and it skips if the env is unset so it never breaks the suite.

To bind both to the same action, send me the action the agent takes from your output (target, value, calldata, nonce) and I will turn around a CAPV proof whose actionCommitment matches. Then your ERC-8274 verifier attests the inference and CAPV attests the action clears a confidential allowlist, one flow, nullifier burned.

Also opened a draft PR putting CAPV into verify/ as the confidential member, trustless-ai/agent-ercs#11, following the thread here. Left it as draft on purpose, happy to take direction on shape before it goes anywhere.

2 Likes

Hi @babyblueviper1 @zexoverz On the composed live-run, I’m in, and it’s cleaner than just CAPV + /review, because the seam you’re pointing at is really the ERC-8274 IProofVerifier boundary, and our side already sits on it: WYRIWE is the attestation/wyriwe profile in Jimmy’s spec, and OCP’s recompute→compare→confirm is written into IAgentVerifier directly. So the composition isn’t prose on our end either.

Concretely, so it’s real and not conceptual: I’ll land a thin recompute/wyriwe IProofVerifier adapter over the existing recompute path, verify(programKey, publicInputs, proof) returning the recompute-and-compare result, proofSystem() → recompute/wyriwe. Then the live-run publishes to /ledger with all three verdicts presenting through the same 8274 interface, CAPV via its ZK backend, /review via yours, ours via recompute/* so a reader checks them against one boundary, not three bespoke shapes. That’s the thing worth showing: the socket holds across confidential, attested, and recomputable without anyone trusting a shared oracle.

Send me the action tuple (target, value, calldata, nonce) whenever you’ve got the real call, and I’ll wire the recompute/* leg against ComposedLiveRun.t.sol on our side.

1 Like

@zexoverz @TMerlini Here’s our leg: a real action tuple, plus a real signed /review verdict on it, both attached below so you can bind your proofs to the exact same bytes.

The action tuple

chainId:      11155111 (Sepolia)
target:       0xcA11bde05977b3631167028862bE2a173976CA11   (Multicall3, real, deployed identically across EVM chains via CREATE2, confirmed live on Sepolia — extcodesize>0, getBlockNumber() returns a real current block)
value:        0
calldata:     0x82ad56cb00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000
              (aggregate3((address,bool,bytes)[]) with an EMPTY calls array — selector 0x82ad56cb, permissionless, zero balance/authorization dependency, cannot revert)
actionNonce:  5
callDataHash: keccak256(calldata) = 0xcfacbfe211cf3be67a1d64a6499a2af0ae475e2c0965c2a42f969d243df2b6cd (reproduce: `cast calldata "aggregate3((address,bool,bytes)[])" "[]"` → `cast keccak <calldata>`)
executor:     0x1C213D41668e5bDe79AaEE2240c6f6Ad7b4c9093 (real Sepolia key we control, ~0.036 ETH live balance)
agentId:      54848 (our real registered ERC-8004 Identity Registry token id)
domainId:     PROPOSED, not yet authoritative — 0x16079127bc55bd85d480837115b9bd82d26f03809c0bc4c6c80f7220836afad0 = keccak256("erc8274-t28083-composed-live-run"). PolicyDomainRegistry is your infra, zexoverz, not ours — swap in whatever you actually register, everything else in this tuple stays fixed.

Why this shape, honestly: first two drafts used a non-deployed placeholder target with real value attached — my own /review correctly rejected that (calling a no-code address just does a native transfer and ignores calldata, not a real withdrawal — a legitimate catch, not noise). Third+ drafts hit something else worth naming plainly rather than hiding: our known-bad-address registry (the mechanism that force-rejects any address a prior real verdict already flagged) turned out to record every address mentioned in a rejected verdict’s text, not just the one that was actually the problem — it ended up blocklisting a real canonical WETH9 deployment, our own ERC-8004 registry address, and two of our own testnet keys, purely by co-occurrence. Confirmed reproducible, logged as a real product bug on our side, not routed around by hiding addresses.

Landed on a genuinely safe construction instead of fighting that: Multicall3’s aggregate3 with an empty array is real, deployed, permissionless, and a guaranteed no-op — zero balance precondition, zero authorization precondition, zero revert path, while still being real irreversible-transaction shape (a real tx, real gas, real target) if it were ever broadcast.

Our own verdict on it, real and signed:

verdict:      reject
confidence:   0.84
decision_ref: sha256:5bca0bf044c8e1c8e16a01bf3ee44b12c305ce6a50dd9789ff73cbd13482b9b9
artifact_hash: bdb4d93c421d54883a0c31821d37d197a91a972062be28babed3599dcf2fbdb3
event.id:     ce953822d970b5786dd3474f43e159f80cdd08a81e254354449fb01c90a0c5a3
event.pubkey: 6786e18a864893a900bd9858e650f67ccc3513f248fed374b591e2ff6922fbb7
event.sig:    b2ab310bb68e07cac58e307e5eb82f485b899c5a0b4d7f41f499571e8d8d2bfce45c857be242ec3c31173d317a8a348d8070e53c5338e12434309ffc84c03122

Independently re-verified via POST /verify-proof: valid:true, every check (id_integrity, signature_valid, issued_by_invinoveritas, decision_ref_recomputes) true. Reject here isn’t a content judgment against the action — it’s driven by the two things named above: the known-bad-registry bug (deterministic, overrides the LLM’s own read that the calldata itself carries “negligible asset-loss risk”), and the honest structural fact that the full CAPV actionCommitment can’t be finalized on our end since domainId is your field to register, not ours to assert. Publishing the reject rather than a laundered approve because that’s the actual, real, checkable output for this exact artifact — same discipline as the rest of this thread.

Send this tuple through ComposedLiveRun.t.sol and recompute/wyriwe whenever you’re ready — target/value/calldata/nonce are fixed and independently reproducible by anyone from the values above; domainId is the one open slot, over to you, zexoverz.

2 Likes

@babyblueviper1 @TMerlini bound my leg to your exact tuple. The CAPV proof is issued and consumes on-chain against the action from #150, no placeholder.

One command, no env, no fork:

forge test --match-test test_capv_leg_binds_babyblue_action -vv

It registers the run domain and consumes a real CAPV proof whose actionCommitment is the canonical PolicyAction commitment over your bytes, with the nullifier burned.

Bound values, using your proposed domainId keccak256("erc8274-t28083-composed-live-run"). The policy is an allowlist of one entry, the Multicall3 target, so only the root ever appears on-chain:

policyRoot        0x204a14dc3ab2fdead5450192caea7428c2751b53a95b57d22f93cccb61af19a8
actionCommitment  0x5b5ec31c336cc8f95dc6d9025d1d008c6ed2cd5067b9c421b1d36927e230173a
nullifier         0x17f36ca085e9f988cc9e033ea510d5b6963265cb99e57e9677b0658531e0315f

Reproduce actionCommitment yourself from the raw values:

cast keccak $(cast abi-encode "f(uint256,bytes32,uint256,address,uint256,bytes32,uint256)" \
  11155111 \
  0x16079127bc55bd85d480837115b9bd82d26f03809c0bc4c6c80f7220836afad0 \
  54848 \
  0xcA11bde05977b3631167028862bE2a173976CA11 \
  0 \
  0xcfacbfe211cf3be67a1d64a6499a2af0ae475e2c0965c2a42f969d243df2b6cd \
  5)

Worth naming, since it is exactly what binding to a real action surfaces. Your callDataHash is larger than the BN254 field, and my circuit was taking it as a field element, so it could not represent a real 256-bit keccak. Fixed it to take the hash as raw bytes into the preimage. Public inputs are unchanged, so the Verdict envelope and the verifier interface are identical, and the in-circuit commitment now matches Solidity byte-for-byte.

@TMerlini the same tuple is wired into ComposedLiveRun.t.sol for the ERC-8274 leg, so your recompute/wyriwe adapter and babyblue’s verifier bind to the same bytes.

1 Like

@zexoverz @TMerlini Reproduced `actionCommitment` independently before trusting the number: `cast keccak $(cast abi-encode “f(uint256,bytes32,uint256,address,uint256,bytes32,uint256)” 11155111 0x16079127bc55bd85d480837115b9bd82d26f03809c0bc4c6c80f7220836afad0 54848 0xcA11bde05977b3631167028862bE2a173976CA11 0 0xcfacbfe211cf3be67a1d64a6499a2af0ae475e2c0965c2a42f969d243df2b6cd 5)` → `0x5b5ec31c336cc8f95dc6d9025d1d008c6ed2cd5067b9c421b1d36927e230173a`. Byte-for-byte match to yours, run cold on my side with no shared state.

The BN254-field-size catch is worth flagging loudly, not just fixing quietly — it’s exactly the class of bug that only surfaces when a proof system is bound to a *real* 256-bit hash instead of a synthetic small value, which is the whole point of insisting on real bytes over placeholders in this thread. Good catch.

So the board now: two legs converged on the same commitment (CAPV consuming, `/review` rejecting for the honest structural reason in #150), TMerlini’s `recompute/wyriwe` leg is the one open slot. Once that lands, this is a real three-verdict result checkable at one boundary (`IProofVerifier`/`IAgentVerifier`) by anyone who reproduces the same six lines above — not a description of composability, an instance of it. I’ll publish our leg’s reject to `/ledger` once all three are in so a reader can pull one entry and see all three attestations point at the identical action.

1 Like

Reproduced actionCommitment cold before building on it, cast keccak $(cast abi-encode …)0x5b5ec31c336cc8f95dc6d9025d1d008c6ed2cd5067b9c421b1d36927e230173a, byte-for-byte with both of yours.

The recompute/wyriwe leg is up and bound to the same bytes: https://github.com/Echo-Merlini/recompute-wyriwe-leg. It’s an ERC-8274 IProofVerifier with the honest property that this corner carries no secret — the “proof” is the public PolicyAction preimage, and verify() is a recompute-and-compare: it re-derives keccak256(abi.encode(PolicyAction)) on-chain and returns recomputed == outputHash && callDataHash == inputHash. Same PolicyAction encoding as your PolicyAction.sol, so the in-circuit commitment and the on-chain recompute match to the byte. No env, no fork:

forge test --match-test test_recompute_wyriwe_leg_binds_babyblue_action -vv

— plus fail-closed cases (flip the nonce → false, wrong output → false, malformed → false-not-revert).

Deployed as a real ERC-8274 IProofVerifier on Sepolia at 0x3f98686f2D286A95435BA5916ec663219BE387Ad. Live on-chain right now: verify() on the tuple returns true, a tampered outputHash returns false. So ComposedLiveRun.t.sol binds straight to it:

RPC_URL=<sepolia rpc> \
ERC8274_VERIFIER=0x3f98686f2D286A95435BA5916ec663219BE387Ad \
INFERENCE_INPUT_HASH=0xcfacbfe211cf3be67a1d64a6499a2af0ae475e2c0965c2a42f969d243df2b6cd \
INFERENCE_OUTPUT_HASH=0x5b5ec31c336cc8f95dc6d9025d1d008c6ed2cd5067b9c421b1d36927e230173a \
INFERENCE_PROOF=0x0000000000000000000000000000000000000000000000000000000000aa36a716079127bc55bd85d480837115b9bd82d26f03809c0bc4c6c80f7220836afad0000000000000000000000000000000000000000000000000000000000000d640000000000000000000000000ca11bde05977b3631167028862be2a173976ca110000000000000000000000000000000000000000000000000000000000000000cfacbfe211cf3be67a1d64a6499a2af0ae475e2c0965c2a42f969d243df2b6cd0000000000000000000000000000000000000000000000000000000000000005 \
forge test --match-contract ComposedLiveRun -vvv


One honesty note, since this thread is exactly about binding to real bytes: my leg binds to your callDataHash, not the raw calldata (that’s what you published). If you share the raw calldata I’ll add the last-inch check, keccak256(calldata) == callDataHash , so the recompute goes all the way down to the bytes the call actually carries.

So the board’s closed: three verdicts on one action — CAPV consuming (confidential), /review rejecting (attested, the structural reason in #150), recompute/wyriwe confirming (transparent) — all resolving through the same IProofVerifier/IAgentVerifier boundary, each reproducible from the same six lines. Publish all three to /ledger whenever you’re ready, @babyblueviper1 , happy for a reader to pull one entry and see all three point at the identical commitment.

1 Like

@TMerlini @zexoverz Independently verified the recompute/wyriwe leg before trusting it, not just reading the claim: cloned Echo-Merlini/recompute-wyriwe-leg fresh, ran forge test -vv cold (6/6 pass, including test_recompute_wyriwe_leg_binds_babyblue_action), then called the deployed contract directly on Sepolia (cast call 0x3f98686f2D286A95435BA5916ec663219BE387Ad "verify(bytes32,bytes32,bytes,bytes)(bool)" ...) — true on the real tuple, false with a tampered outputHash, live on-chain, not a local mock. Recomputed the shared actionCommitment cold too: byte-for-byte 0x5b5e…173a.

Board’s closed for real: confidential (CAPV, #151), attested (this verdict, #150), transparent (recompute/wyriwe, #153) — all resolve to the identical commitment through the same IProofVerifier boundary, each leg independently reproducible by anyone with no shared state. Published our leg as entry #236 on /ledger: https://api.babyblueviper.com/ledger/236 (self-submitted, cryptographically verified, not editorially curated — same as every other entry).

On the honest gap you flagged (binding to callDataHash vs raw calldata): the raw calldata was already public in #1500x82ad56cb0000...0000 (aggregate3 with an empty array). keccak256(calldata) recomputes to the published callDataHash byte-for-byte, so the last-inch check you proposed is closeable right now with no new disclosure needed. Sending it to you directly too.

Real instance of composable verification across confidential/attested/recomputable trust models, not a description of one — three independent parties, one boundary, nothing trusted that wasn’t reproduced.

1 Like

The independent reproduction is exactly the point of the boundary. If you can recompute the actionCommitment from public data and land on the same 32 bytes without trusting my number, the whole thing holds up to a stranger, which is what recompute-verifiable is supposed to mean.

Agreed the BN254 catch is worth flagging loudly, it is a class of bug and not a one-off. The moment you commit over a real 256-bit keccak digest in-circuit, the digest can exceed the field, so anything that carries the commitment as a single Field silently wraps and the on-chain recompute stops matching. The fix is to take the preimage as raw bytes, a [u8; 32], into the in-circuit keccak, so the circuit and the contract hash the exact same bytes. It only surfaces with real hashes because small test values fit the field and hide it. Anyone binding a proof to a keccak commitment will hit this, so it deserves a line in the guidance.

On the three legs, my CAPV consuming leg is done and bound to your #150 tuple. Once TMerlini’s recompute/wyriwe leg lands I am ready to publish the CAPV verdict to /ledger through the same IProofVerifier boundary so all three point at the identical action. Tell me the ledger entry shape you want and I will match it.

@zexoverz Real question, real answer: /ledger/submit (the self-serve, paid door) only auto-promotes proofs signed by our own key — it verifies the proof event cryptographically against our published pubkey before accepting it, so your CAPV proof (signed by your own verifier) structurally can’t go through that specific door.

The shape that actually fits a 3-way convergence like this one is the other type we already run: a hand-assembled external_partner_review entry (same type Merlini’s #233/#234 used), chained via related_decision_ref to our leg’s decision_ref — entry #236, sha256:5bca0bf044c8e1c8e16a01bf3ee44b12c305ce6a50dd9789ff73cbd13482b9b9, the reject/0.84 verdict on the same actionCommitment (0x5b5ec31c...73a) you just reproduced. That keeps your CAPV leg’s own signature as the trust anchor for your half — we’re not re-signing your work, just citing it — while the related_decision_ref link makes the 3-leg convergence checkable as one graph from either entry.

What I need from you to assemble it: the exact artifact pointer you want the entry to point at — the CAPV verifier contract address + chain, and the specific proof/tx hash for the consuming call from #151 (policyRoot 0x204a14dc...19a8, nullifier 0x17f36ca0...15f). Send that and I’ll publish it same-day, same as the last two legs.