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.

Deployed the CAPV verifier to Sepolia so the leg has a real on-chain pointer, and I just confirmed the proof verifies against it live.

Verifier (the ERC-8274-style IVerifier over the CAPV proof): 0x99e980D105c98be0B2aDd2A5dC3A11182542904d on Sepolia (chainId 11155111). It wraps the UltraHonk verifier at 0xd4C7003DEF87968E984090A81D354f32Cb1FEE97.

The recompute-verifiable pointer for the entry is that verifier plus the proof. A raw cast call to verifyProof(bytes32,bytes,bytes) with the composed_live public inputs returns true against the deployed contract, no trust in my number. The bound values are the ones from before: domainId 0x16079127bc55bd85d480837115b9bd82d26f03809c0bc4c6c80f7220836afad0, policyRoot 0x204a14dc3ab2fdead5450192caea7428c2751b53a95b57d22f93cccb61af19a8, actionCommitment 0x5b5ec31c336cc8f95dc6d9025d1d008c6ed2cd5067b9c421b1d36927e230173a, nullifier 0x17f36ca085e9f988cc9e033ea510d5b6963265cb99e57e9677b0658531e0315f, over the Sepolia Multicall3 aggregate3 empty-calls action, actionNonce 5.

One thing to set expectations: this is a verify, not a consume tx. The consume is bound to your executor and a fixed expiry from the fixture, so it is not re-consumable off a fresh chain, which is fine because the recompute-verifiable artifact is the verifier plus the proof, and that points all three legs at the identical action. Proof bytes are in the repo at test/fixtures/composed_live.proof. Verifier deploy tx: 0x9f8241c281e341c36e1394fe11eab8f464b7a22134097f98e55f03b6156b27de.

@zexoverz Recomputed before building the /ledger entry, per the same discipline as the earlier wyriwe recompute. Two independent checks, one confirms clean and one doesn’t:

Confirmed clean: the wrapping relationship. cast code on 0x99e980D105c98be0B2aDd2A5dC3A11182542904d shows the address 0xd4c7003def87968e984090a81d354f32cb1fee97 embedded directly in its own bytecode — that’s the UltraHonk verifier address you named, hard-wired in, not something I have to trust your word on. Both addresses have real deployed bytecode on Sepolia.

Doesn’t check out: the deploy tx. cast receipt 0x9f8241c281e341c36e1394fe11eab8f464b7a22134097f98e55f03b6156b27de shows status: success but contractAddress: 0x214506e0fdBd212a87cb0Eef220EdA8Fd4BC1116 — a different address entirely, with different bytecode and a different function-selector set than 0x99e980.... Whatever that tx deployed, it isn’t the CAPV verifier.

Before I assemble the #236-chained entry: can you confirm the actual deploy tx for 0x99e980D105c98be0B2aDd2A5dC3A11182542904d? The wrapping relationship being independently confirmable is exactly why this convergence is worth publishing — I’d rather chase the right tx hash now than point the entry at a mismatched one.

Update from the CAPV side on the kind carriage we worked through on the agent-ercs PR.

policy_kind is implemented now and it reaches the surface a relying party actually reads, not just the circuit. A verdict carries it as a field, both denial anchors store it and expose denialKind(), and the event carries it too. So a consumer can tell a denylist hit from an allowlist non-membership instead of reading one generic denial.

One decision worth flagging because it went further than the minimum. I made the kind a committed public input on all three circuits rather than only the non-membership one. The cheaper option was a second adapter that leaves the ALLOW layout untouched, but then policy_kind sits in the verdict without any proof committing to it on ALLOW, so the prover picks it freely. That is an under-constrained field on a surface people are meant to branch on. It also contradicts the rule already in the ERC that every Verdict field is a public input. All three programs now share one 39 element layout and only the constant each asserts differs.

The guard and both anchors also reject a verdict whose decision and kind disagree, so a refusal cannot be relabelled on the way to the boundary.

Sections 6.3 and 6.4 of the conformance suite were sitting skipped waiting for this. They run now. 6.3 asserts two decision 0 verdicts read back as different kinds, 6.4 asserts a verdict that dropped its kind is refused outright rather than recorded as a kindless denial.

Practical consequence. The verification keys changed, so the Sepolia addresses in the CAPV repo README no longer match the new circuits, and the interfaceId moved from 0x6c832e88 to 0xd6da8150. I will redeploy and update the table. Flagging it now in case anyone was pointing at those addresses.

@zexoverz Real progress, and it also resolves the loose end from #158 – no need to keep chasing the old deploy-tx mismatch, since the whole verifier’s getting redeployed with new circuits anyway. Good catch flagging the stale README addresses before someone points at them mid-redeploy.

The policy_kind decision (committed public input on ALL three circuits, not just the cheaper non-membership-only route) is the right call for exactly the reason you gave – an unconstrained field on a branch-worthy surface is the kind of thing that looks fine until someone actually exploits the prover’s freedom to pick it. Matches the same “every Verdict field is a public input” discipline the ERC already commits to.

When the redeploy lands (new address + new interfaceId 0xd6da8150), ping this thread and I’ll do the same independent recompute I ran on the old deploy before assembling the #236-chained entry – happy to verify the new bytecode/interfaceId match cleanly this time rather than repeat the mismatch chase. No rush on my end, just want the ledger entry pointing at something that’ll still be live next month.