ERC-8274: AI Inference Proof Verification

Full draft interfaces: Add ERC: AI Inference Proof Verification Interfaces by JimmyShi22 · Pull Request #1771 · ethereum/ERCs · GitHub


Hi everyone :waving_hand:,

As on-chain AI and verifiable compute continue to gain traction, we are hitting a massive interoperability wall. We have an incredible Cambrian explosion of verification networks on one side (zkML, opML, TEEs, etc.), and a growing list of agentic/AI-focused ERCs on the other.

However, right now, these two sides speak completely different languages.

TL;DR: We are facing an N × M integration nightmare for on-chain AI. Every dApp or protocol that wants to consume verified AI inference currently has to write custom, vendor-specific adapters for each proof system. This draft ERC proposes a unified, standard verifier registry and interface that sits exactly in the middle—allowing verification projects to plug in once, and consumer ERCs to query a single standard interface.

Would love to get the community’s feedback on this architecture, especially from authors of the related ERCs and teams building inference networks.

Here is the breakdown of the problem and the proposed standard:


Motivation

Verification infrastructure already exists, but it is highly fragmented.

Verification Projects: On-chain AI verification is already live across five paradigms:

Paradigm Representative Projects
zkML RISC Zero, SP1, EZKL
opML ORA Protocol
TEE Automata DCAP, Marlin Oyster
Oracle Chainlink Functions, Band Protocol
Multisig / AVS EigenLayer AVS, Lit Protocol

ERCs: A growing set of ERCs explicitly need on-chain inference verification, but none define a common interface for it:

  • ERC-8183 (Agentic Commerce) — evaluator “may verify a ZK proof” before releasing payment, but no verifier interface is defined
  • ERC-8004 (Trustless Agents) — Validation Registry lists zkML/TEE verifiers as examples, but specifies no contract interface
  • ERC-7992 (Verifiable ML) — defines a registry for ZK proofs only, leaving opML, TEE, Oracle, and Multisig uncovered
  • ERC-7007 (Verifiable AIGC Token) — accepts opaque bytes proof with no model identifier or proof-system identifier
  • ERC-8001 (Agent Coordination, Final) — routes multi-agent task results through opaque executionData bytes with no inference verification module

The problem: these two sides cannot talk to each other. Every protocol that needs verified AI inference today must write a separate adapter per proof system — resulting in vendor lock-in and N×M integration complexity. What’s missing is a standard verifier interface that sits in the middle. This ERC provides exactly that:

   Verification Projects           This ERC                ERCs

   zkML (RISC Zero, SP1…) ─┐  ╔══════════════════╗  ┌─ ERC-8183 (Agentic Commerce)
   opML (ORA Protocol)    ─┤  ║  AI Inference    ║  ├─ ERC-8004 (Trustless Agents)
   TEE  (Automata, Marlin)─┼─►║  Proof           ║◄─┼─ ERC-7992 (Verifiable ML)
   Oracle (Chainlink…)    ─┤  ║  Verification    ║  ├─ ERC-7007 (AIGC Token)
   Multisig (EigenLayer…) ─┘  ║  Interfaces      ║  └─ ERC-8001 (Agent Coordination)
                              ╚══════════════════╝

Proposal

This ERC defines two minimal interfaces — one for each side of the integration gap.

For Verification Projects (left side) — implement IProofVerifier:

Wrap your existing verifier logic behind two methods. Any consumer can call verify() without knowing which proof system is underneath.

interface IProofVerifier {
    function verify(
        bytes32 inputHash,       // commitment to the model input (hash scheme is backend-specific)
        bytes32 outputHash,      // commitment to the model output (hash scheme is backend-specific)
        bytes calldata metadata, // backend-specific context: model ID, agent ID, signer registry, etc.
        bytes calldata proof     // backend-specific cryptographic material: ZK proof, EIP-712 signature, TEE attestation
    ) external view returns (bool);

    function name()         external view returns (string memory);
    // Recommended format: "{system}/{variant}" e.g. "zkml/sp1", "oracle/attestation"
    function version()      external view returns (string memory);
    function proofProfile() external view returns (bytes32);
    // SHOULD equal keccak256(abi.encodePacked(name(), version())) for standard backends
}
  • zkML: wrap your existing verify / verifyProof call; proofProfile() returns "zkml/risc0/1" or similar
  • opML: adapter resolves the async request/callback pattern before returning; proofProfile() returns "opml/optimistic/1"
  • TEE: wrap the attestation verification call; proofProfile() returns "tee/nitro/1"
  • Oracle & Multisig: wrap threshold signature or BLS aggregate check; proofProfile() returns "oracle/multisig/1"

For ERCs (right side) — implement IVerificationMethod:

Declare which verifier your contract uses. No central registry, no hub — you hold a direct reference to an IProofVerifier implementation and call it directly.

interface IVerificationMethod {
    function getVerifier() external view returns (IProofVerifier);

    event VerifierUpdated(
        address indexed previousVerifier,
        address indexed newVerifier
    );
}
  • ERC-8183: the evaluator contract implements IVerificationMethod; calls getVerifier().verify(inputHash, outputHash, metadata, proof) internally before calling complete()
  • ERC-8004: agent contracts declare their verifier via getVerifier(); VerifierUpdated events feed downstream reputation and audit systems
  • ERC-7992: existing zkML verifiers register as IProofVerifier adapters; proofProfile() carries the proofSystemId equivalent
  • ERC-7007: replace opaque bytes proof with a call to IProofVerifier.verify(); proofProfile() provides the model/proof-system identifier that was previously missing

The architecture is deliberately flat — the same pattern as ERC-20’s transfer().


Open Questions

  1. Input/Output Encoding — Should inputHash and outputHash be bytes32 hashes or raw bytes? Hashes keep the interface minimal and predictable, but raw bytes would allow backends to inspect content directly and avoid a separate hashing step. The tradeoff: hashes are cheaper on-chain but push the hashing convention off-spec; raw bytes are more flexible but increase calldata cost and open questions about encoding.

  2. Input Segmentation — Should input be a single field, or split into systemPrompt and userPrompt? Many inference backends treat system and user context differently, and some proof systems may need to commit to them independently. Splitting would make the interface more expressive at the cost of added complexity.

  3. Naming Governance — Is self-assigned {system}/{variant}/{version} in proofProfile() sufficient to prevent collisions in practice, or does the ecosystem need a lightweight off-chain name registry to coordinate namespace allocation?

5 Likes

This is a solid attempt at fixing the N×M integration problem—but it’s solving the wrong layer.

What you’re standardizing here is how systems call verification.

What’s missing is more fundamental:

a canonical, system-independent way to verify a claim outside the system that produced it

Right now, everything in this design still assumes:

  • a registry

  • a specific verifier implementation

  • an execution environment to interpret the proof

That means verification is still system-mediated.

If I hand a third party:

modelHash, inputHash, outputHash, proof

there is no standard way for them to verify that claim without:

  • calling verifyInference()

  • trusting a specific verifier contract

  • relying on this registry and its assumptions

So while this reduces integration complexity, it doesn’t address the deeper issue:

verification is not independently reproducible across implementations


The missing layer

This isn’t hypothetical—there is a minimal way to define it.

Verification can be reduced to a portable artifact evaluated by a shared invariant, not a shared interface.

At minimum, that artifact includes:

  • a canonical reference to the data being claimed

  • a deterministic digest of that data (recomputable anywhere)

  • a standardized reference to where that digest is committed

  • a defined extraction rule so different implementations retrieve the same value

So verification reduces to:

recompute → compare → confirm inclusion

No registry.
No adapter.
No system-specific interpretation.


That primitive already exists—I’ve formalized it as a minimal verification layer.

Until something like that is recognized and standardized:

we’re not making verification independent—we’re standardizing access to system-specific verification

This proposal improves interoperability within the verification stack.

But it still leaves open the more important question:

what does it mean to verify something without depending on the system that produced it?

1 Like

Hey @Damonzwicker — really appreciate this framing. You’re right that what I’ve built is “interoperability within the verification stack” rather than “independence from it.” That’s a meaningful distinction I hadn’t articulated cleanly.

Your point about the missing primitive got me thinking about what a redesign along those lines would actually look like. Here’s a rough sketch:

Core idea: replace the registry+interface pattern with a Portable Verification Claim (PVC)

Instead of routing proof bytes through a verifier contract, the standard would define a self-contained claim struct:

struct InferenceClaim {
    bytes32 modelId;       // canonical model identifier
    bytes32 inputDigest;   // deterministic digest of input (recomputable anywhere)
    bytes32 outputDigest;  // deterministic digest of output
    bytes32 claimDigest;   // keccak256(modelId || inputDigest || outputDigest)
    uint256 commitBlock;   // block at which the claim was anchored on-chain
}

And a minimal interface with only three functions:

interface IInferenceClaim {
    function commitClaim(InferenceClaim calldata claim) external returns (bytes32 claimId);

    function verifyClaim(
        bytes32 modelId,
        bytes32 inputDigest,
        bytes32 outputDigest,
        uint256 commitBlock
    ) external view returns (bool valid);

    function getClaimStatus(bytes32 claimId) external view returns (bool exists, uint256 blockNumber);
}

verifyClaim does exactly what you described — recompute → compare → confirm inclusion — with no registry lookup, no verifier contract call, no proof bytes. Anyone with the raw data can independently verify.

The key shift: proof verification moves left — each proof system (zkML, TEE, opML, etc.) is responsible for verifying its own proof before calling commitClaim. The standard layer doesn’t care how you proved it; it only anchors that you did. This dissolves the N×M problem differently — not by building adapters, but by having each system emit a unified InferenceClaim as its output.

Curious whether this aligns with the primitive you had in mind, and whether you see gaps in this approach?

2 Likes

Sorry for the late response - This is a meaningful step forward—especially the move toward portable claims and pushing proof verification upstream.

Where I think it fits is one layer above something more primitive I’ve been working to isolate.

PVC standardizes how inference claims are structured and exchanged. But it still assumes:

  • a schema (modelId, input/output)

  • implicit semantics

  • and a verification path tied to an implementation

That makes claims portable—but verification still system-mediated.

The boundary I’ve been exploring is lower:

a system-independent way to verify that any byte sequence existed—without relying on:

  • a verifier contract

  • a registry

  • or an execution environment

That’s what the Observation Commitment Protocol defines:

data → digest → public commitment
recompute → compare → confirm inclusion

At that level:

  • no schema is required

  • no notion of “inference” is assumed

  • no interface is needed

Just a portable artifact:
(digest + transaction reference)

Verification reduces to:

  • recomputing the digest

  • extracting the committed value from a transaction

  • comparing the two

No contract call. No system dependency.

PVC composes cleanly with this—emit a structured claim, then anchor it as a digest + commitment so it becomes independently verifiable outside the system.

That separates:

  • claim construction (your layer)
    from

  • system-independent verification (this lower boundary)

Framed another way:

ZK proves computation
DA proves availability
This layer proves what actually existed

That last piece is widely assumed—but not actually standardized.

I’ve formalized this as a minimal spec + reference implementation (still refining areas like canonical extraction rules and ledger assumptions):
https://github.com/damonzwicker/observation-commitment-protocol

Feedback welcome on any and all thoughts.

@Damonzwicker Thanks for the detailed breakdown — the layering is clear and I follow the separation you’re drawing between claim construction and system-independent verification.

That said, I want to push on what’s actually new here. Hash anchoring — committing a digest to a public ledger to prove a byte sequence existed — has been around for a while. OpenTimestamps, Chainpoint, and similar projects have been doing exactly this for years.

So I’m genuinely curious: what does OCP define that those don’t already cover? Is it mainly the formalization as a named, citable spec that can be cleanly composed with higher-level protocols like PVC? Or is there a meaningful technical distinction I’m missing — something in the verification model, the portability guarantees, or the ledger assumptions?

Either way I think there’s something interesting here — just trying to understand where the line is between formalizing existing practice and introducing something genuinely new. Happy to dig in further if you can point me to the right part of the spec.

Thanks @JImmyshi22 ! That’s a fair challenge — and an important one.

You’re absolutely right that hash anchoring itself is not new. OpenTimestamps, Chainpoint, and related systems demonstrated years ago that public ledgers can anchor digests and provide timestamp-style proofs.

What I’m trying to isolate with OCP is not the existence of hash commitments, but the definition of a minimal verification boundary that remains independently verifiable outside the originating system.

In many existing approaches, verification is still coupled to:

  • a protocol

  • an indexer

  • a proof format

  • a verifier implementation

  • an API

  • or application-layer semantics

OCP attempts to formalize the lower irreducible layer beneath those dependencies.

The invariant is intentionally narrow:

data → digest → public commitment
recompute → compare → confirm inclusion

At that boundary:

  • no schema is assumed

  • no notion of “AI inference” is required

  • no verifier contract is required

  • no registry is required

  • and verification reduces to recomputation plus ledger-relative inclusion checking

The core property is portability of verification itself.

Meaning: a verification artifact should remain independently verifiable even if the originating application, company, protocol stack, or execution environment no longer exists.

That survivability property is the part I think has not been cleanly isolated as its own primitive layer.

So I’d frame the contribution less as:

“public hash commitments are new”

and more as:

  • separating verification from application semantics

  • defining a minimal invariant for independently reconstructable claims

  • and standardizing a portable verification boundary that higher-level systems can compose around without inheriting system-specific trust assumptions

In that sense, OCP is less a new timestamping mechanism and more an attempt to define the smallest possible boundary for portable, independent verification itself - somewhat analogous to how TCP/IP standardized transport boundaries and HTTP standardized communication boundaries.

Still refining areas like extraction rules, ledger assumptions, and long-term composability, so I genuinely appreciate the pushback. It helps sharpen where the architectural distinction is — and where prior art already exists.

Always open to feedback and thankful and graetful for your perspective!

@Damonzwicker Thanks for the detailed clarification — that really helps me understand the architectural intent behind OCP more clearly.

If I’m reading this right, the goal is to define a verification primitive that sits below any specific implementation layer — meaning it shouldn’t be tied to a particular encoding format (like Solidity ABI), a specific chain, a specific API, or any application-level semantics.

That raises a question I’d love to hear your thoughts on. For OCP to be a truly portable verification boundary, is the spec intended to formally define things like:

  • How data is serialized/encoded before hashing (in a language- and platform-agnostic way)
    • How the hash/digest is computed (which algorithm, and what the canonical input looks like)
    • What exactly gets committed to the public ledger, and in what form
    • What steps a verifier must reproduce to confirm inclusion
    • What counts as a successful verification, and where the boundary conditions are
    • What minimal external assumptions are made (e.g., ledger trust), made explicit rather than implied
      Is that the right way to think about what OCP is actually specifying? Would love to understand better.
2 Likes

Great timing on this, I’ve been working on a companion proposal for ERC-8004 that sits directly upstream of what you’re describing, and I think the two stack cleanly.

Your registry solves “was this inference verifiably run”, routing verify(modelHash, inputHash, outputHash, proof) through a unified interface regardless of whether the proof system is zkML, TEE, or opML. That’s the right abstraction for the verification layer.

What it doesn’t address is “was the input clean before it was hashed.”

On-chain AI agents (ERC-8004) read ENS text records, NFT metadata, and contract return values as LLM context. That data goes into the reasoning loop before inputHash is computed. A poisoned ENS record or NFT attribute, permanent and unremovable on-chain, becomes part of the input that gets hashed and “verified.” The proof system will confirm the inference ran correctly on a compromised input.

The full pipeline looks like this:

[Input trust layer — my proposal]       [Verification layer — your proposal]

on-chain data
     ↓
sanitize() + provenance label           
     ↓                                  
inputHash ──────────────────────────→  verify(modelHash, inputHash, outputHash, proof)
                                               ↓
trustScope check (A2A depth/caps)       IProofVerifier registry
     ↓
manifest_hash (config drift detection)


The three primitives I’ve proposed and deployed live on gateway.ensub.org:

  1. inputSources - manifest field declaring every on-chain data source the agent reads, with per-source sanitization rules. Undeclared sources are rejected. This is what produces a clean inputHash.

  2. trustScope - caps inter-agent trust transitivity, call depth, and capability delegation for A2A call chains. A compromised agent can’t arbitrarily relay poisoned inputs downstream.

  3. Execution attestation - SHA-256(sanitized_input), SHA-256(reply), and SHA-256(manifest_at_call_time) logged fire-and-forget after each action. The manifest hash is essentially the Observation Commitment primitive @damonzwicker describes — data → digest → public commitment applied to agent configuration, making drift detectable without accessing internals.

You can verify the manifest endpoint live:

curl https://gateway.ensub.org/agent/0xe61f5a6783ae09949b9a1b6821b68f89c0d7bb2d/5/.well-known/agent.json
# → { "inputSources": null, "trustScope": { "transitive": false, "maxDepth": 0, "capabilities": [] } }

curl https://gateway.ensub.org/agent/0xe61f5a6783ae09949b9a1b6821b68f89c0d7bb2d/5/attestations
# → [ { "input_hash": "...", "manifest_hash": "...", "action_type": "chat", ... } ]


Full proposal and companion spec draft are in the ERC-8004 thread — search “On-Chain Input Trust Boundaries for ERC-8004” on this forum. Happy to coordinate — particularly interested in whether inputHash in your registry spec should have an OPTIONAL sanitizationPipelineHash field that commits to the preprocessing step used before hashing. That would make the two proposals interoperable end-to-end.

dinamic.eth

2 Likes

Yes — that’s very close to how I’m thinking about it.

But with one important distinction:

OCP is not trying to standardize observation semantics globally.

It’s trying to standardize the minimal verification semantics required for independent recomputation and verification.

So some things belong inside the OCP boundary, while others intentionally remain application-defined.

I think OCP likely does need to explicitly define:

  • the digest algorithm

  • the exact committed value being referenced

  • the extraction rule for obtaining that value from a ledger artifact

  • the verification procedure itself

  • and the minimal trust assumptions being relied upon

Without those, independent verification becomes ambiguous.

But I do not think OCP should universally standardize how all observations are serialized, structured, or interpreted across every application domain.

The observation remains application-defined.

What OCP standardizes is the minimal reproducible verification boundary:

given an observation and declared hashing/commitment rules, can an independent verifier:

  • reproduce the digest

  • extract the committed value

  • and verify equivalence

without relying on the originating system’s API, infrastructure, or execution environment?

Successful verification occurs when:

  1. the verifier deterministically reproduces the declared digest from the observation under the declared rules, and

  2. the reproduced digest matches the committed value extracted from the referenced ledger artifact.

That’s the layer I’m trying to isolate.

So the architectural boundary becomes:

  • applications define observations

  • OCP defines portable verification

That distinction is important because I’m specifically trying to avoid collapsing OCP into:

  • an application framework

  • a universal schema layer

  • a canonical encoding system

  • or a chain-specific verification protocol

The ambition is smaller and lower-level than that.

The TCP/IP analogy is useful here.

TCP/IP didn’t standardize every application payload or internal data structure on the internet.

It standardized the minimal boundary required for independent systems to reliably exchange information.

Similarly, OCP is attempting to standardize the minimal portable verification boundary required for independent systems to reliably verify observations without inheriting the originating system’s full trust stack.

Still refining exactly where those boundaries should sit, so these questions are extremely helpful.

2 Likes

This is extremely thoughtful — and I think you’re identifying exactly the right architectural separation.

I completely agree that:

  • verification of execution correctness

  • verification of input trustworthiness

  • and portability of verification

are distinct concerns.

A proof system can verify that an inference executed correctly on compromised input.

OCP intentionally does not attempt to solve upstream trust formation.

That separation is deliberate.

What OCP attempts to isolate is the narrower question:

“Given some observation, can its correspondence to a public commitment later be independently verified?”

Not:

  • whether the observation was trusted

  • whether sanitization was correct

  • whether provenance policies were sufficient

  • or whether the upstream agent pipeline was secure

Those are critically important problems, but I think they belong above the minimal verification layer rather than inside it.

Your model around:

  • inputSources

  • trustScope

  • sanitization constraints

  • and manifest drift detection

composes very naturally with that separation.

The layering, as I currently see it, is roughly:

  • trust layers constrain what may enter the execution boundary

  • proof systems verify execution correctness

  • OCP provides portable verification of observations and commitments

  • and registries coordinate interoperability across those layers

That architectural decomposition feels correct to me.

I also think your point about manifest hashes is important.

At that stage you’re effectively applying the same primitive recursively to configuration state itself:

configuration → digest → public commitment

which makes configuration drift independently observable without requiring visibility into internal execution state.

On interoperability:

I could absolutely see something like an optional sanitizationPipelineHash or preprocessingCommitment composing cleanly above the core OCP layer.

But I’d be cautious about making that intrinsic to the primitive itself.

My concern is preserving the smallest possible portable verification boundary while allowing richer trust and provenance semantics to compose externally around it.

Otherwise OCP risks collapsing upward into:

  • AI-specific semantics

  • pipeline governance semantics

  • provenance orchestration

  • or generalized agent policy frameworks

which I think are adjacent layers rather than the primitive layer itself.

Taken together, these start to resemble a modular trust architecture composed of separable verification boundaries rather than one monolithic verification system.

And yes — I’d absolutely be interested in coordinating further around interoperability and composition boundaries here.

I think there’s real value in keeping trust formation, execution verification, and portable verification architecturally separable while still allowing them to compose coherently into larger verifiable agent systems.

Really appreciate this response — this is exactly the kind of discussion that helps clarify where the abstraction boundaries should sit.

1 Like

The OCP scope discussion surfaces a real composition problem worth making concrete.

OCP’s goal is the smallest portable verification boundary: given an observation, can its correspondence to a public commitment be independently verified? That’s the right abstraction. The question is where that boundary sits relative to on-chain input preprocessing.

On-chain AI agents (ERC-8004) read ENS text records, NFT metadata, and contract return values before inference runs. That data is sanitized — injection patterns stripped, provenance labels added — before it enters the LLM context. The current inputHash in the verification registry is computed over the already-sanitized value. The raw on-chain artifact, which is independently verifiable against ledger state, is never committed.

This creates a gap:

ledger state (verifiable)

raw_onchain_data → SHA-256 → raw_input_hash

sanitize(raw, pipeline) → SHA-256 → input_hash ← what LLM consumed

pipeline_spec → SHA-256 → sanitization_pipeline_hash

                                         ↓

                            verify(modelHash, input_hash, output_hash, proof)

Without raw_input_hash and sanitization_pipeline_hash as separate commitments, there is no way to verify that input_hash is the result of applying a known transformation to a known on-chain artifact. The proof system confirms the inference ran correctly on whatever was hashed — but not that the hashed value has a verifiable relationship to what is on the ledger.

OCP doesn’t need to understand what sanitization means to close this. It only needs to treat the pipeline spec as another commitment — data → digest → public commitment applied to the preprocessing step. Trust semantics stay in the input trust layer. OCP stays at the verification boundary.

We’ve added raw_input_hash to the attestation log in our live reference implementation so both commitments are now logged per inference:

curl https://gateway.ensub.org/agent/0xe61f5a6783ae09949b9a1b6821b68f89c0d7bb2d/5/attestations?limit=1

# → { “raw_input_hash”: “…”, “input_hash”: “…”, “manifest_hash”: “…”, … }

When no sanitization runs they are identical. When the pipeline fires they diverge — that divergence is the signal that a transformation occurred, and sanitization_pipeline_hash is what makes it verifiable without trusting the runtime.

The modular stack then looks like:

Input trust layer → inputSources + sanitize() + trustScope

Commitment layer → raw_input_hash + sanitization_pipeline_hash + input_hash

Proof layer → verify(modelHash, inputHash, outputHash, proof)

Registry → coordinates interoperability across all three

Each layer makes a commitment. Each is independently verifiable. None absorbs the semantics of the others.

sanitizationPipelineHash as an optional field in the verification registry spec would be the concrete interop point. Happy to draft the schema….ill do it in the next reply!

1 Like

Following the architecture discussion — here is a concrete schema for
sanitizationPipelineHash as an interop point between the input trust layer
and the verification registry.


Pipeline Spec Format

The hash commits to a canonical JSON pipeline spec. Deterministic serialization
(sorted keys, no whitespace) ensures the same pipeline always produces the same
hash regardless of runtime.

{
  "controlCharRanges": ["U+0000-U+0008", "U+000E-U+001F", "U+007F"],
  "maxLength": 500,
  "patternFlags": "case-insensitive word-boundary",
  "patterns": [
    "act as", "disregard", "from now on", "ignore",
    "ignore previous", "override", "pretend you", "roleplay as",
    "you are now", "your new instructions",
    "<|im_end|>", "<|im_start|>", "[INST]", "[SYS]"
  ],
  "provenanceLabel": "[on-chain:ens]",
  "replacementToken": "[redacted]",
  "sourceType": "ens",
  "specVersion": "erc-8004-security/1"
}


Hash computation:

sanitizationPipelineHash = SHA-256(JSON.stringify(spec, Object.keys(spec).sort()))


The patterns array MUST be sorted lexicographically before hashing so
implementations with different insertion order produce identical hashes.


Attestation Log Extension

Additive to the existing ERC-8004 attestation schema:

{
  raw_input_hash:             string,  // SHA-256(raw on-chain artifact)
  sanitization_pipeline_hash: string,  // SHA-256(canonical pipeline spec)
  input_hash:                 string,  // SHA-256(sanitize(raw, pipeline))
  output_hash:                string,  // SHA-256(LLM reply)
  manifest_hash:              string,  // SHA-256(agent config at call time)
}


When no sanitization runs: raw_input_hash === input_hash and
sanitization_pipeline_hash is the hash of the identity pipeline
{ "patterns": [], "maxLength": null, ... }.


Verification Procedure

Off-chain, deterministic, no trust in runtime required:

1. Fetch raw_onchain_data from ledger (ENS resolver, NFT contract, etc.)
2. Compute SHA-256(raw_onchain_data) → confirm matches raw_input_hash
3. Resolve pipeline spec from sanitization_pipeline_hash
   (via agent manifest endpoint or a spec registry keyed by hash)
4. Apply pipeline deterministically to raw_onchain_data
5. Compute SHA-256(result) → confirm matches input_hash
6. Pass input_hash to proof system:
   verify(modelHash, input_hash, outputHash, proof)


Steps 1–5 are fully independent of the runtime. Any verifier with access
to the ledger and the pipeline spec can reproduce them without trusting
the agent or its gateway.


Optional Verification Registry Interface

interface IInputProvenanceVerifier {
    /// @notice Returns true if sanitize(raw, pipeline) === sanitized,
    ///         where each argument is a SHA-256 commitment.
    /// @dev    Verification is deterministic and can be computed off-chain.
    ///         On-chain implementation is optional — commitments alone
    ///         are sufficient for off-chain auditors.
    function verifyInputProvenance(
        bytes32 rawInputHash,
        bytes32 sanitizationPipelineHash,
        bytes32 inputHash
    ) external view returns (bool);
}


This interface is intentionally separate from IProofVerifier — it sits
upstream of inference verification and has no dependency on the proof system.
Registries MAY implement it; runtimes that publish all three commitments in
their attestation log make it redundant for off-chain verifiers.


Full Stack Composition

Input trust layer    →  inputSources + sanitize() + trustScope
Commitment layer     →  raw_input_hash + sanitization_pipeline_hash + input_hash
Proof layer          →  verify(modelHash, inputHash, outputHash, proof)
Registry             →  coordinates interoperability across all three


Each layer makes a commitment. Each is independently verifiable.
None absorbs the semantics of the others.


What Is Already Live

raw_input_hash and input_hash are both logged per inference in the
reference implementation:

curl https://gateway.ensub.org/agent/0xe61f5a6783ae09949b9a1b6821b68f89c0d7bb2d/5/attestations?limit=1
# → { "raw_input_hash": "...", "input_hash": "...", "manifest_hash": "...", ... }


sanitization_pipeline_hash is the next field pending agreement on the
canonical spec format above. The pipeline spec itself is fully defined in
the ERC-8004 companion draft (§1.3).


Open Questions for Coordination

  1. Should sanitizationPipelineHash be an optional field in the existing
    verify() call, or a separate verifyInputProvenance() interface?

  2. Should the pipeline spec be resolvable via a on-chain registry keyed by
    hash, or is the agent manifest endpoint sufficient as the resolution
    mechanism?

  3. Should specVersion reference the ERC number once the companion standard
    is assigned, or use a URI-based versioning scheme?

1 Like

Went ahead and implemented sanitization_pipeline_hash on our end — the canonical spec format from post is now live in the reference implementation. Three commitments logged per inference:

curl https://gateway.ensub.org/agent/0xe0454dfa17a57a84c3e0e2dbfda5318cbbe91e2c/11/attestations?limit=1

# → { “raw_input_hash”: “72169d…”, “sanitization_pipeline_hash”: null, “input_hash”: “72169d…” }

raw === input with sanitization_pipeline_hash: null signals no transformation occurred, a verifier can skip the pipeline step. When sanitization fires the two diverge and the pipeline hash tells the verifier which spec was applied.

Waiting on your assessment of the three open questions in post #12 before locking the format. Happy to adjust the canonical spec structure if anything needs tightening.

1 Like

The pipeline spec format you landed in post #12 is the cleanest input trust decomposition I’ve seen proposed in this thread — the sorted-keys canonicalization detail especially. That’s the kind of thing that looks obvious in retrospect and isn’t.

Three open questions, three answers:

1. verifyInputProvenance() vs. optional field in verify()?

Separate interface. These are verifying fundamentally different things — transformation correctness vs. execution correctness. An auditor checking input provenance has no reason to touch the proof system. Bundling them conflates two failure modes that should be independently observable. Your post #12 stack already makes this argument — the interface design should reflect it.

2. On-chain registry vs. agent manifest endpoint?

Manifest endpoint is sufficient. The commitment is the security property — sanitization_pipeline_hash binds the verifier to a specific spec regardless of where that spec is resolved from. A global on-chain registry adds permissionless discoverability across agents that don’t share infrastructure, but that’s a coordination problem we don’t have yet. Introduce it when the use case exists, not before.

3. ERC number vs. URI-based versioning?

URI-based. erc-8004-security/1 is stable before ERC assignment, portable after, and doesn’t couple semantic versioning to a governance process. Lock to an ERC number once the number exists and the interface is final — not before.


The OCP boundary

sanitization_pipeline_hash and verifyInputProvenance() belong above the OCP boundary, not inside it. The primitive does one thing:

observation → digest → commitment → verify inclusion

The provenance pipeline composes before that boundary:

raw observation → sanitize() → transformed observation → digest → commitment

The full stack:

trust layer       →  inputSources + sanitize()
provenance layer  →  verifyInputProvenance()
OCP primitive     →  digest → commitment → verify inclusion
proof layer       →  verify(modelHash, inputHash, outputHash, proof)
registry          →  coordinates interoperability

Each layer commits. Each verifies independently. None absorbs the semantics of the others. This boundary needs to stay hard as the ERC matures — the moment OCP standardizes sanitization semantics it stops being a primitive and starts being an application.


On the identity pipeline sentinel — recommend fixed hash over null

null creates a branch every verifier has to handle forever. A canonical fixed hash for the identity case (no sanitization) makes verifier logic uniform — the same code path runs regardless of whether sanitization occurred.

Proposed canonical identity spec:

{
  "patterns": [],
  "maxLength": null,
  "replacementToken": null,
  "controlCharRanges": [],
  "specVersion": "erc-8004-security/1"
}

Hashed deterministically (sorted keys, no whitespace) this produces a stable sentinel value every implementation can precompute. If you agree on the spec structure I’ll publish the hash and we can lock.

1 Like

The five-layer decomposition is the clearest framing of this problem we’ve seen — it maps
directly onto what the reference implementation had to solve independently, which is a
good sign the architecture is converging.

Let us be precise about where we stand against each layer, implement what was missing,
and state our position on where verifyInputProvenance() sits.


Layers 1–3 — live

  • Layer 1 (Trust): inputSources and sanitizationSpec in the agent manifest. Seven
    on-chain source types. Spec at ipfs://QmaYUmRWD33jsmvH9TkMY1TwDgPcZ2zcx5S9BAqCqbUdTr

  • Layer 2 (Provenance): Three-hash commitment logged per inference — verifiable at
    https://gateway.ensub.org/admin/attestations

  • Layer 3 (OCP): Deterministic sorted-key SHA-256(JSON.stringify(spec)) — any
    verifier can reconstruct the hash independently from the IPFS spec


Sentinel hash — shipped

The null issue was a real gap. We’ve implemented the fixed sentinel as proposed.

The identity spec is published at:
ipfs://QmTst97dG8i9tFrutdetqMbVhSHqJGJaxMmPzWCcVVTWDU

{
  "controlCharExclusions": null,
  "controlCharRanges":     null,
  "maxLength":             null,
  "patternFlags":          null,
  "patterns":              [],
  "provenanceLabel":       null,
  "replacementToken":      null,
  "sourceType":            "identity",
  "specVersion":           "erc-8004-security/identity",
  "transformation":        "none"
}


Sentinel hash (SHA-256 of sorted keys):
8116eec29078e8f57c07077d5e8080a35bde73036581df3abb93755d1b1a16ea

logExecution() now always writes a hash — never null. Identity pipeline gets the
sentinel. Sanitization pipeline gets the spec hash. A verifier always has a positive
commitment to check against.


verifyInputProvenance() sits at Layer 2 — our position

We’ve implemented it as a live endpoint and want to state the reasoning clearly, open
for pushback.

Layer 5 coordinates across execution verification backends — zkML vs opML vs TEE.
Those are alternatives, a consumer picks one. verifyInputProvenance() is not a
backend, it’s a prerequisite. You don’t choose between “verify input provenance” and
“run zkML.” Input provenance runs upstream of every backend, regardless of which one
is chosen.

The sentinel makes this concrete: it represents “identity pipeline applied” — a Layer 2
concept. If verifyInputProvenance() belonged at Layer 5, the sentinel would need to be
registered as a verification method alongside zkML/opML, which conflates input
preparation with execution verification.

The clean layering:

Layer 2 — verifyInputProvenance()   always runs, backend-agnostic
               ↓
Layer 4 — verify()                  runs against whichever backend
               ↓
Layer 5 — Registry                  coordinates which backend Layer 4 uses


Layer 5 is horizontal, it coordinates across backends at the same level.
verifyInputProvenance() is vertical, it runs upstream of everything.

We’ve shipped a callable implementation:

GET https://gateway.ensub.org/verify/input-provenance
  ?rawInputHash=<hex>
  &sanitizationPipelineHash=<hex|sentinel>
  &inputHash=<hex>

→ { valid: bool, transformation: "identity"|"sanitized", reason: string, spec: ipfs:// }


Example — identity pipeline (sentinel):

GET .../verify/input-provenance
  ?rawInputHash=abc123
  &sanitizationPipelineHash=8116eec29078e8f57c07077d5e8080a35bde73036581df3abb93755d1b1a16ea
  &inputHash=abc123

→ { "valid": true, "transformation": "identity", "sentinelSpec": "ipfs://QmTst97..." }


The on-chain version follows the same interface, same inputs, same return shape. The
execution environment changes, the verification logic doesn’t.


Layer 4 — partial, honest

Hashes computed, stored, internally verifiable. Not EIP-712 signed — the gateway is
still the trust root. Third-party portability is the next concrete task.


Layer 5 — not implemented as a trustless contract

The gateway is a centralized coordinator today. The AgentIdentityRegistryFactory at
0xc2bb6502a7d8ee3cdb2f96508d6cdf426aa2858f (mainnet) is trustless but coordinates
agent identities, not verification methods. The IPFS-published spec is content-addressed
and publicly resolvable, the closest thing we have to a decentralized spec registry.

A trustless Layer 5 is on the roadmap once Layer 4 (EIP-712 signing) is solid. Building
Layer 5 before Layer 4 produces a registry over unverifiable attestations.

Open for pushback on the Layer 2 positioning and the sentinel spec format.


  • Manifest: https://gateway.ensub.org/agent/0xe61f5a6783ae09949b9a1b6821b68f89c0d7bb2d/5/.well-known/agent.json

  • Attestations: https://gateway.ensub.org/admin/attestations

  • verifyInputProvenance: https://gateway.ensub.org/verify/input-provenance

  • Sanitization spec: ipfs://QmaYUmRWD33jsmvH9TkMY1TwDgPcZ2zcx5S9BAqCqbUdTr

  • Identity sentinel spec: ipfs://QmTst97dG8i9tFrutdetqMbVhSHqJGJaxMmPzWCcVVTWDU

  • Source: github.com/Echo-Merlini/ens-dynamic-kit *closed for release audit

Tiago / Dinamic.eth

2 Likes

Tiago —

The implementation you shipped in this post is the most complete response in this thread. The honest Layer 4/5 status especially — building Layer 5 over unverifiable attestations would have been the wrong move and you called it correctly.

Sentinel hash confirmed.

Computed independently against your published identity spec using sorted-key SHA-256:

canonical: {"controlCharExclusions":null,"controlCharRanges":null,"maxLength":null,"patternFlags":null,"patterns":[],"provenanceLabel":null,"replacementToken":null,"sourceType":"identity","specVersion":"erc-8004-security/identity","transformation":"none"}

hash: 8116eec29078e8f57c07077d5e8080a35bde73036581df3abb93755d1b1a16ea

Matches. The sentinel is locked. Two independent implementations, same result — that’s the confirmation that matters.

Layer 2 positioning — confirmed.

Your reasoning is correct. verifyInputProvenance() is vertical — it runs upstream of every backend regardless of which one is chosen. verify() is horizontal — it coordinates across backends at the same level. They are sequential, not alternatives. The clean layering you documented holds:

Layer 2 — verifyInputProvenance()   always runs, backend-agnostic
               ↓
Layer 4 — verify()                  runs against whichever backend
               ↓
Layer 5 — Registry                  coordinates which backend Layer 4 uses

No pushback on the positioning. It’s right.

Where OCP sits in this stack

For clarity on the record — OCP is the Layer 3 primitive underneath all of this. Its job is narrow and stays narrow:

observation → digest → commitment → verify inclusion

It doesn’t touch sanitization semantics, pipeline specs, or provenance policy. Those belong at Layer 2 as you’ve implemented them. OCP provides the commitment mechanism Layer 2 and Layer 4 both rely on — the ledger-verifiable proof that a specific hash was committed at a specific moment, verifiable from raw chain data with no SDK dependency.

The zero-dependency reference verifier is live against Base Sepolia if useful for testing:

What’s locked

  • Sentinel hash: 8116eec29078e8f57c07077d5e8080a35bde73036581df3abb93755d1b1a16ea
  • Identity spec: ipfs://QmTst97dG8i9tFrutdetqMbVhSHqJGJaxMmPzWCcVVTWDU
  • verifyInputProvenance() at Layer 2, upstream of all backends
  • URI-based versioning: erc-8004-security/identity
  • Separate interfaces: verifyInputProvenance() and verify() are not bundled

What’s next

Layer 4 EIP-712 signing is the right next step on your end. On ours it’s the Solana appendix — stress-testing whether the OCP proof envelope holds across a non-EVM architecture. If it does, the primitive is chain-agnostic in practice, not just in spec.

Looking forward to seeing Layer 4 land.

Damon / OCP

1 Like

Yes, Layer 4 EIP-712 signing is next on our end. That’s the concrete commitment.

The current state: hashes are computed and stored per inference, the three-hash
commitment is live and verifiable at /admin/attestations, and verifyInputProvenance()
is callable. What’s missing is the gateway’s signing key over the commitment — without
that, the attestations are gateway-internal and not third-party portable. EIP-712 signing
closes that gap.

The work: define a typed struct over
(raw_input_hash, sanitization_pipeline_hash, input_hash, output_hash, manifest_hash, agentId, registry, timestamp), sign it with the gateway’s private key on every
logExecution() call, expose the signature via the attestation endpoint. Any verifier
can then check the signature against the gateway’s public key without trusting the
gateway’s database.

That’s what makes the attestations portable enough to anchor on EAS.


The independent sentinel hash verification is the kind of confirmation that matters.
Deterministic by construction, if two implementations compute the same SHA-256 over the
same sorted-key JSON, the spec is doing its job. That’s the OCP principle applied to our
own spec process.

The vertical vs horizontal clarification is the clearest framing of the layering we’ve
seen. Saving it verbatim for the spec documentation:

verifyInputProvenance() runs vertically — upstream of all backends, always.
verify() runs horizontally — coordinates across backends at the same level.

They’re sequential, not alternative. That resolves the ambiguity completely.


On the OCP Base Sepolia reference verifier

A zero-dependency reference verifier on Base Sepolia is the right move at this stage,
it gives implementers something to test against before Layer 4 (EIP-712 signing) locks
in the portable attestation format. We’ll integrate against it when we build out the
EIP-712 signing pass on logExecution().

One question for you: does the Base Sepolia verifier currently accept the
sanitization_pipeline_hash field as part of the OCP commitment, or is it working
against a simpler input hash structure? Asking because we want to make sure the
three-hash scheme (raw_input_hash / sanitization_pipeline_hash / input_hash) is what
we build the EIP-712 signing around — if OCP is moving to a different structure at Layer 3
we should align before Layer 4 lands.


On the Solana appendix

The chain-agnostic claim for OCP is the interesting stress test. Solana’s account model
means the commitment structure will need to be re-examined — sorted-key SHA-256 of JSON
is portable but the verification primitive on-chain isn’t. If the appendix surfaces
differences in how the commitment is anchored across chains, that’s useful input for
whether Layer 3 should specify the commitment format only (chain-agnostic) or also the
verification interface (necessarily chain-specific).

Our position: Layer 3 should specify the commitment format. Layer 5 handles the
verification interface variation across execution environments.

Looking forward to the appendix.

One honest note: we’re aware we’ve been all over the place in these threads, KYA,
ERC-8217, ERC-8265, TAP, ERC-8183, and now our own ERC-8004 factory thread. It’s not
noise for its own sake. We’re running a single production system that happens to sit at
the intersection of all of them, and each thread has touched something we’ve already
had to solve or are actively building. Layer 4 is where we focus next.

1 Like

Tiago —

Glad the vertical/horizontal framing landed clearly enough to go into the spec verbatim. That’s the right place for it.

On your question — what the Base Sepolia verifier accepts

To be direct: the current OCP verifier works against a single committed digest. It does not know about sanitization_pipeline_hash, raw_input_hash, or the three-hash scheme. Those live at Layer 2. What OCP receives at Layer 3 is the final input_hash — the observation after your pipeline has run. That’s the only thing OCP commits and verifies.

The flow as OCP sees it:

Layer 2 produces:   input_hash = SHA-256(sanitize(raw, pipeline))
Layer 3 commits:    input_hash → on-chain digest → proof envelope
Layer 3 verifies:   recompute input_hash → confirm inclusion in tx

OCP never sees raw_input_hash or sanitization_pipeline_hash. Those are Layer 2 commitments, verifiable via verifyInputProvenance(). OCP is downstream of that step.

So for EIP-712 signing: the struct you’re signing over —
(raw_input_hash, sanitization_pipeline_hash, input_hash, output_hash, manifest_hash, agentId, registry, timestamp) — is a Layer 2/4 attestation. OCP anchors input_hash from that struct at Layer 3. The two are complementary, not overlapping.

This means your EIP-712 work doesn’t need to wait on OCP’s structure — OCP’s interface to your system is a single bytes32 digest. Build Layer 4 around the full struct you’ve defined. When you’re ready to anchor on EAS, the input_hash field is what gets committed via OCP.

On your architectural position

Layer 3 should specify the commitment format. Layer 5 handles the verification interface variation across execution environments.

Agreed. That’s the right separation. The proof envelope specifies the commitment format — chain-agnostic, self-describing. The verification interface is necessarily chain-specific and belongs at Layer 5. The Solana appendix will test whether that boundary holds in practice.

On the honest note

It reads as a single production system at the intersection of multiple active standards efforts — that’s not noise, that’s a vantage point most people in these threads don’t have. Layer 4 is the right focus. Looking forward to seeing it land.

Damon / OCP

1 Like

Good to have it confirmed from your side. Rather than just proposing the Layer 4 spec, we went ahead and shipped it. Here’s the live reference first, then the formal proposal.


Live attestation - verifiable now:

GET https://gateway.ensub.org/agent/0xe61f5a6783ae09949b9a1b6821b68f89c0d7bb2d/5/attestations?limit=1


{
  "registry":                   "0xe61f5a6783ae09949b9a1b6821b68f89c0d7bb2d",
  "agent_id":                   "5",
  "action_type":                "chat",
  "raw_input_hash":             "a50f50918f5886e410ced5cc5d44ad0d35dc324fcf70d3e6d833580e4d3a3da7",
  "sanitization_pipeline_hash": "8116eec29078e8f57c07077d5e8080a35bde73036581df3abb93755d1b1a16ea",
  "input_hash":                 "a50f50918f5886e410ced5cc5d44ad0d35dc324fcf70d3e6d833580e4d3a3da7",
  "output_hash":                "48f2d477e7f3afbe9a970a6eb617a7f4b416a380982df4868f8fd592fd57f32c",
  "manifest_hash":              "0a56e06bbff01de7c1955dc902fadcd4f7b893bc6fc260f9253d779c220a569e",
  "l4_signature":               "0x61db3d3be8e7450ca951fc429d431fbf112f2fbd758ddc5cb0f8737724ac1e9c4c09f7fd13c8f8c15f2610c6796bfb63c5995c5e81df9a012d2cb39a6ae36cee1c"
}


Two things worth highlighting in this example:

  1. sanitization_pipeline_hash equals the identity sentinel
    (8116eec2...1a16ea) , the canonical hash of the no-op pipeline spec.
    This means no transformation was applied, so raw_input_hash == input_hash.
    A verifier can confirm this without fetching the pipeline spec.

  2. The signature is over the EIP-712 struct below, signed by
    0x85Fa13511D170FBe173761b63D7f8DD4A6f6Bf1A. Verify via:
    registry.getAgentWallet(5) on 0xe61f5a6783ae09949b9a1b6821b68f89c0d7bb2d (mainnet).


Layer 4: EIP-712 Inference Attestation , Formal Proposal

EIP-712 Domain

{
  "name":              "KYA-L4",
  "version":           "1",
  "chainId":           1,
  "verifyingContract": "<ERC-8004 registry address>"
}


Typed Struct

struct InferenceAttestation {
  bytes32 raw_input_hash;             // SHA-256 of input before sanitization
  bytes32 sanitization_pipeline_hash; // SHA-256 of canonical pipeline spec
  bytes32 input_hash;                 // SHA-256 of input after sanitization
  bytes32 output_hash;                // SHA-256 of model output
  bytes32 manifest_hash;              // SHA-256 of agent manifest (model, provider, trust config)
  uint256 agentId;                    // ERC-8004 token ID
  address registry;                   // ERC-8004 registry contract
  uint64  timestamp;                  // Unix timestamp of execution (seconds)
}


Field semantics

Field Layer Description
raw_input_hash L2 Digest of unmodified input as received
sanitization_pipeline_hash L2 Digest of the pipeline spec applied. MUST be the identity sentinel when no transformation runs
input_hash L2 Digest of input after pipeline. Equals raw_input_hash when sentinel is used
output_hash L4 Digest of model response
manifest_hash L4 Digest of { id, model, provider, input_sources, trust_scope }
agentId L4 Links attestation to ERC-8004 on-chain identity
registry L4 Resolves signer: registry.getAgentWallet(agentId)
timestamp L4 Execution time for replay detection

Signer resolution

address expected  = registry.getAgentWallet(agentId);
address recovered = ecrecover(eip712Digest, v, r, s);
require(recovered == expected, "invalid attestation");


No trusted third party. Verifier only needs the registry address and the public EIP-712 struct.

Relationship to other layers

L2 → produces raw_input_hash, sanitization_pipeline_hash, input_hash
L3 → commits input_hash on-chain (OCP never sees L2 intermediates — confirmed)
L4 → signs over all L2 hashes + output_hash + manifest_hash + identity
L5 → uses manifest_hash to route to appropriate verifier (zkML / opML / TEE)


L3 and L4 are fully decoupled. An operator can produce a L4 signature without
submitting to L3, submit to L3 without signing, or do both. The live example
above is L4-only, L3 anchoring is the natural next step.

Identity sentinel

When no sanitization is applied, sanitization_pipeline_hash MUST be:

0x8116eec29078e8f57c07077d5e8080a35bde73036581df3abb93755d1b1a16ea


SHA-256 of the canonical identity pipeline spec. When present: input_hash == raw_input_hash and verifiers skip sanitization verification entirely.


Happy to PR this as a formal Layer 4 section. Implementation is live, every
agent execution on gateway.ensub.org now produces a signed attestation
retrievable at /{registry}/{agentId}/attestations. The signer
(0x85Fa13511D170FBe173761b63D7f8DD4A6f6Bf1A) is registered on-chain as the
hot wallet for agent 5, verifiable right now via getAgentWallet(5) on the
registry.

Tiago / dinamic.eth

1 Like

Tiago —

Verified independently. Just now:

curl "https://gateway.ensub.org/agent/0xe61f5a6783ae09949b9a1b6821b68f89c0d7bb2d/5/attestations?limit=1"
{
  "raw_input_hash":             "9ead1a0a102b70ba1403b8ccb691c39075f0bb7ba4205ab17e2f5ec7c51f8163",
  "sanitization_pipeline_hash": "8116eec29078e8f57c07077d5e8080a35bde73036581df3abb93755d1b1a16ea",
  "input_hash":                 "9ead1a0a102b70ba1403b8ccb691c39075f0bb7ba4205ab17e2f5ec7c51f8163",
  "output_hash":                "204db556b51c2e107acff7bc8940d864b8d3d1833e9a54529a9cd777b7fe1347",
  "manifest_hash":              "0a56e06bbff01de7c1955dc902fadcd4f7b893bc6fc260f9253d779c220a569e",
  "l4_signature":               "0x83e206012a88ae4935d08f8a2ea1427abbbf7b7c..."
}

Sentinel hash present: 8116eec2...1a16ea :check_mark:
Identity pipeline confirmed: raw_input_hash == input_hash :check_mark:
Signature present :check_mark:

This is live, independently verifiable, right now. No trust in the gateway required — the sentinel is deterministic, the signature resolves on-chain via getAgentWallet(5), and the manifest hash is stable across calls.

On the PR

Yes. Submit it. The formal Layer 4 proposal is well-specified — the typed struct is clean, the field semantics table maps correctly to the layer architecture, and the signer resolution pattern is exactly right. The only thing that makes a spec real is a live implementation that matches it. You have that.

On L3 anchoring

L3 and L4 being fully decoupled is the correct design and you called it correctly. The natural next step is anchoring input_hash from the L4 attestation via OCP on Base Sepolia — that connects the signed attestation to a ledger-verifiable commitment. When you’re ready to do that pass, the reference verifier and contract are live at:

Contract: 0x0963Fd33DF80c94360F2DC22e5c09517AeE7ED5c (Base Sepolia)

The input_hash field from your L4 struct is the value that goes into record(bytes32). One transaction, one proof envelope, verifiable from raw chain data with no SDK.

Damon / OCP