EIP.tools

EIP.tools

⚠️ DraftStandards Track: ERC

ERC-8299: WYRIWE β€” Input Provenance for AI Inference

An input-provenance commitment scheme and attestation profile for verifiable AI agent inference

Authors
Created2026-05-28
Discussion Linkhttps://ethereum-magicians.org/t/erc-8299-wyriwe-what-you-read-is-what-you-execute/28655
Requires

Markdown

https://raw.githubusercontent.com/ethereum/ERCs/re...
Pull Request#1810PR open

EIP-GPT summary

Contents
AbstractMotivationSpecification1. Triple-Hash Construction2. IDENTITYSENTINEL β€” No-Sanitization Case3. WyriweAttestation Struct4. EIP-712 Domain5. Verification Procedure6. Gateway Query Interface7. ClaimType DiscriminatorRationaleWhy three hashes?Why IPFS CIDs for the sanitization spec?Why EIP-712?Why include agentId and registry?Why IDENTITYSENTINELCID instead of a null value?Relationship to ERC-8126Backwards CompatibilityTest CasesCase 1: Identity transform (no sanitization)Case 2: Sanitization appliedCase 3: Attestation forgery (should fail)Reference ImplementationSecurity ConsiderationsAttestor key compromiseHash collision resistanceAdversarial sanitization specificationSanitization spec CID stabilityReplay and cross-domain attacksInput availabilityCompositionL2 Snapshot commitment (ccip-router)L4 Judgment validator binding (@babyblueviper1)Appendix A β€” Cross-system settlement: GenericCommitRevealSettler integrationHash constructionEncoding convention for judgment attestationsRecommended preflightperiodId conventionWorkflowInvariant checkrecordPointer sub-pathsAppendix B β€” RecordPointer SchemaStruct definitionregistryType valuesMechanism identifier conventionvalidatorId = 0x0 β€” off-registry validatorsRelay retentionProduction mapping β€” reference implementationAppendix C β€” Identity-Transform Specification (IDENTITYSENTINELCID)ReferencesAcknowledgementsCopyright

Abstract

This ERC defines a triple-hash commitment scheme and EIP-712 attestation profile for proving that the input a model received is the input the user intended. It introduces three linked fields β€” raw_input_hash, sanitization_pipeline_hash, and input_hash β€” that together form a verifiable chain of custody for AI inference inputs. A verifier can confirm input integrity using only the committed hashes and the public sanitization specification, without trusting the agent, gateway, or execution environment. This standard occupies the input-provenance layer of the AI inference trust stack, complementing ERC-8004 (agent identity), ERC-8126 (agent verification), TruthAnchorV1 (on-chain proof commitment and anchor layer), and OCP (observation commitment protocol).


Motivation

On-chain AI agent systems built on standards such as ERC-8004, ERC-8126, TruthAnchorV1, and the proof verification layer can attest to which agent is registered, which model ran, and what output was produced β€” but no standard defines how to commit to the input before inference. This creates a trust gap: an agent may sanitize, rewrite, or substitute the user's input between request submission and model execution, leaving no on-chain evidence of the transformation.

ERC-8126 (AI Agent Verification, Final) addresses the question "Is this agent trustworthy?" via risk scores and verification registries. It deliberately leaves the execution receipt layer out of scope β€” what the agent actually processed in a specific invocation. WYRIWE closes that gap.

Without a committed input record:

  • A settlement contract cannot verify that the delivered output corresponds to the funded input.
  • A proof verifier (e.g. an IProofVerifier) cannot confirm the inputHash it receives matches what was originally requested.
  • A dispute resolution mechanism (e.g. a CommitRevealSettler) has no ground truth for what the model was actually asked to do.

WYRIWE (What You Read Is What You Execute) closes this gap by defining a minimal, hash-based commitment that any compliant gateway MUST produce at execution time and that any verifier can check independently.


Specification

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.

1. Triple-Hash Construction

A WYRIWE-compliant execution MUST produce the following three values:

raw_input_hash             = keccak256(raw_user_input)
sanitization_pipeline_hash = keccak256(sanitization_spec_cid_bytes || raw_input_hash)
input_hash                 = keccak256(sanitized_input)

Where:

  • raw_user_input is the exact bytes of the user's input as received, before any transformation.
  • sanitization_spec_cid_bytes is utf8(sanitization_spec_cid): the full IPFS URI string including the ipfs:// scheme prefix (e.g., ipfs://QmccvoM6aRVg...), serialized as UTF-8 bytes. The ipfs:// prefix is part of the preimage and MUST be included.
  • sanitized_input is the exact bytes fed to the model after applying the sanitization pipeline.
  • || denotes byte concatenation.

Verification invariant: Given raw_input_hash, sanitization_pipeline_hash, and the public sanitization specification at sanitization_spec_cid, any verifier MUST be able to confirm that input_hash is the correct output of that pipeline applied to that raw input. No party needs to be trusted to assert this.

2. IDENTITY_SENTINEL β€” No-Sanitization Case

When no sanitization is applied (identity transform), the following MUST hold:

sanitization_pipeline_hash = keccak256(IDENTITY_SENTINEL_CID || raw_input_hash)
input_hash                 = raw_input_hash

IDENTITY_SENTINEL_CID is a stable IPFS reference to the identity-transform specification:

ipfs://QmccvoM6aRVgZ2dtFWvT6Wm3DmTvoAUHHotK7uQufnStVR

The content at this CID is normative and frozen β€” it defines the identity transform (no modification to the input). Implementations MUST pin this CID to ensure long-term verifiability. The content is reproduced in Appendix C for self-containment in the event of IPFS unavailability.

The input_hash == raw_input_hash equality in the no-sanitization case is a provable on-chain claim, not an assumption. Implementations MUST NOT omit sanitization_pipeline_hash even when no sanitization is applied.

3. WyriweAttestation Struct

A WYRIWE attestation is an EIP-712 typed structured data record with the following fields:

struct WyriweAttestation {
    bytes32 agentId;                    // ERC-8004 agent identity anchor
    address registry;                   // ERC-8004 registry address
    bytes32 modelHash;                  // Hash of model weights or manifest
    bytes32 rawInputHash;               // keccak256(raw_user_input)
    bytes32 sanitizationPipelineHash;   // keccak256(sanitization_spec_cid_bytes || raw_input_hash)
    bytes32 inputHash;                  // keccak256(sanitized_input)
    bytes32 outputHash;                 // keccak256(model_output)
    uint256 timestamp;                  // Unix timestamp of execution
}

All fields are REQUIRED. A conforming attestation MUST populate every field. agentId and registry MAY be zero-valued if the execution environment does not implement ERC-8004, but MUST NOT be omitted from the struct.

The agentId is the ERC-8004 registry identity anchor β€” the identifier the registry assigns, paired with registry β€” not a hash of any human-readable name. When the ERC-8004 identity is a uint256 registry id, agentId is its left-padded encoding agentId = bytes32(uint256(id)) (e.g. registry id 860 becomes 0x000...035c). Deriving an identity from a human-readable name is out of scope for this ERC; if an implementation does so, the derivation MUST be declared, and an ENS-shaped name SHOULD use the ERC-137 namehash (the ENS node), never keccak256(utf8(name)).

The canonical EIP-712 type string for WyriweAttestation is:

WyriweAttestation(bytes32 agentId,address registry,bytes32 modelHash,bytes32 rawInputHash,bytes32 sanitizationPipelineHash,bytes32 inputHash,bytes32 outputHash,uint256 timestamp)

Field ordering is as declared in the struct above and is normative. EIP-712 encoding is order-sensitive β€” a type string with fields in any other order produces a different typeHash and MUST NOT be used.

Claim classification: A WyriweAttestation is an attestation-class claim. The corresponding IProofVerifier SHOULD return proofSystem() = "attestation/wyriwe". When consumed in a proof-verification outer claim container, claimType SHOULD be set to Attestation. The EIP-712 type string acts as the on-chain schema discriminator; claimType serves off-chain consumers (indexers, explorers, dispute interfaces) that read the raw signed struct without calling the verifier contract.

Note: modelHash commits to the model weights or manifest β€” what model ran. This is distinct from a TEE codeMeasurement, which commits to the execution environment. WYRIWE operates at the input-provenance layer, not the execution environment layer. For execution environment attestations, see the tee/* proof systems in the proof verification framework.

4. EIP-712 Domain

The EIP-712 domain separator for WYRIWE attestations is:

EIP712Domain({
    name:    "ERC8004AttestationGateway",
    version: "1",
    chainId: block.chainid
})

chainId MUST use the chain ID of the network on which the attestation is produced (block.chainid in Solidity). A hardcoded value is NOT permitted β€” doing so prevents the domain separator from distinguishing attestations produced on different chains and breaks replay protection in multi-chain deployments.

The signature is produced via eth_signTypedData_v4 over the EIP-712 digest of the WyriweAttestation struct, yielding a 65-byte (r, s, v) ECDSA signature recovered against the attestor address. Note: eth_signTypedData_v4 (EIP-712) is distinct from eth_sign β€” using eth_sign applies the \x19Ethereum Signed Message prefix, which would break on-chain ecrecover verification of the 712 digest and MUST NOT be used.

5. Verification Procedure

A verifier MUST execute the following steps to accept a WYRIWE attestation as valid:

  1. Recompute the EIP-712 digest from the attestation struct fields and verify signature against the known attestor address.
  2. Verify rawInputHash == keccak256(raw_user_input) if the raw input is available.
  3. Fetch the sanitization specification at sanitization_spec_cid and apply it to raw_user_input; verify the result hashes to inputHash.
  4. If sanitization_spec_cid == IDENTITY_SENTINEL_CID, verify inputHash == rawInputHash.
  5. Accept the attestation only if all applicable steps pass.

Steps 2–4 are REQUIRED when the corresponding inputs are available. Step 1 is always REQUIRED.

6. Gateway Query Interface

A conforming gateway MUST expose the following HTTP endpoint:

GET /agent/verify/:inputHash

Where :inputHash is the lowercase hex-encoded (no 0x prefix) inputHash value. The response MUST be a JSON object containing the WyriweAttestation fields and the signature. The response MUST use HTTP 200 on success and HTTP 404 when no attestation exists for the given inputHash.


7. ClaimType Discriminator

Compliant claim artifacts MUST carry a claimType field identifying the accountability model of the claim. This field is distinct from proofSystem and MUST NOT be conflated with it:

  • proofSystem belongs to the IProofVerifier contract path and has contract context. It identifies the cryptographic mechanism that authenticated the artifact.
  • claimType belongs inside the signed artifact and travels without that context. It identifies the accountability and dispute model.

An off-chain consumer holding only the raw signed struct MUST be able to determine the accountability model without a registry lookup.

enum ClaimType {
    ReExecution,  // deterministic computation β€” objectively disputable
    Attestation,  // authorized signer certifies a result
    Judgment      // subjective assessment β€” accountable through track record / policy
}

The fields answer different questions:

  • proofSystem answers: what cryptographic mechanism authenticated the artifact?
  • claimType answers: what kind of accountability model backs the claim?

A zk/sp1 proof system may back a ReExecution claim; sig/eip712 is used for both Attestation and Judgment claims. The proof system alone does not determine the accountability model.

claimTypeproofSystem examplesAccountabilityDispute surface
ReExecutionzk/sp1, zk/ezkl, op/oraMathematical / economicObjectively disputable
Attestationsig/eip712, attestation/wyriwe, attestation/multisigSigner authorization / stake / reputationSigner registry or policy
Judgmentsig/eip712, attestation/judgmentTrack record / reputation / policy-defined stakePre-outcome commitment + later outcome evidence

A WyriweAttestation is an Attestation-class claim (claimType = Attestation). A JudgmentExecutionAttestation is a Judgment-class claim (claimType = Judgment). See Section 3 and the Composition section respectively.


Rationale

Why three hashes?

Two hashes (raw and final) are insufficient: they prove the input was transformed but do not commit to which transformation was applied. The sanitization_pipeline_hash commits to both the specification and the raw input, making the transform auditable and reproducible by any third party.

Why IPFS CIDs for the sanitization spec?

Content-addressed references ensure the specification retrieved at verification time is identical to the one applied at execution time. A mutable URL reference would allow the spec to be swapped after the fact, defeating the commitment.

Why EIP-712?

EIP-712 typed structured data signatures are natively verifiable on-chain by Ethereum contracts, wallet UIs, and existing tooling. The structured hash is deterministic and auditable without any off-chain oracle.

Why include agentId and registry?

Linking attestations to an ERC-8004 agent identity makes the attestation attributable β€” not just to a signing key, but to an on-chain registered agent. This is load-bearing for settlement systems (e.g. ERC-8183) that need to associate an output with a specific funded agent.

Why IDENTITY_SENTINEL_CID instead of a null value?

Using a null or zero value for sanitization_pipeline_hash in the no-sanitization case would make it ambiguous whether the field was intentionally omitted or a transform was applied but not committed. The sentinel makes the no-sanitization case explicit, auditable, and verifiable on equal footing with sanitized cases.

Relationship to ERC-8126

ERC-8126 (AI Agent Verification, Final) defines risk scores and verification registries that answer "Is this agent trustworthy?" WYRIWE is complementary, not overlapping: it answers "What did this agent actually process in this invocation?" The two standards compose naturally β€” ERC-8126 establishes agent-level trust, WYRIWE establishes execution-level provenance. A settlement contract may require both: the agent must be verified (ERC-8126) AND the input commitment must match the funded request (WYRIWE).


Backwards Compatibility

This ERC introduces a new standard with no dependencies on or conflicts with existing ERCs beyond the voluntary integration points described in the Specification. It does not modify any existing interface.


Test Cases

Case 1: Identity transform (no sanitization)

raw_user_input             = "transfer 1 ETH to 0xABCD..."
raw_input_hash             = keccak256("transfer 1 ETH to 0xABCD...")
sanitization_spec_cid      = "ipfs://QmccvoM6aRVgZ2dtFWvT6Wm3DmTvoAUHHotK7uQufnStVR"
sanitization_pipeline_hash = keccak256(sanitization_spec_cid_bytes || raw_input_hash)
sanitized_input            = raw_user_input
input_hash                 = raw_input_hash

Expected: input_hash == raw_input_hash β€” verifier confirms identity case.

Case 2: Sanitization applied

raw_user_input             = "transfer 1 ETH to 0xABCD... <script>alert(1)</script>"
raw_input_hash             = keccak256(raw_user_input)
sanitization_spec_cid      = "ipfs://Qm<strip-html-spec-cid>"
sanitized_input            = "transfer 1 ETH to 0xABCD..."
sanitization_pipeline_hash = keccak256(sanitization_spec_cid_bytes || raw_input_hash)
input_hash                 = keccak256(sanitized_input)

Expected: input_hash != raw_input_hash β€” verifier fetches spec, applies strip-HTML transform to raw input, confirms result matches input_hash.

Case 3: Attestation forgery (should fail)

input_hash (claimed) = keccak256("transfer 100 ETH to attacker")

Verifier fetches attestation for this input_hash, recomputes sanitization_pipeline_hash from the claimed sanitized input and the committed spec, finds mismatch with the attested sanitization_pipeline_hash. Attestation rejected.


Reference Implementation

A live WYRIWE-compliant gateway is deployed at:

GET https://gateway.ensub.org/agent/verify/:inputHash

Example query:

https://gateway.ensub.org/agent/verify/758d61f26a44448384e5c4468a0dcb7a2abe456067b0f7b505bc28b9411fe931

Source code: https://github.com/Echo-Merlini/ccip-router

L2 settlement reference node: https://gateway.gen-plasma.com β€” live ccip-router node tracking spec revisions. Runs CommitRevealSettlerV2 (bond/slash, Router+Hybrid gate) and GenericCommitRevealSettler (bytes-generic, Appendix A) against Sepolia. Each spec revision is reviewed against the deployed node before commit; drift found in review is fixed before the revision is pushed. Conformance is tracked at the npm package version β€” ccip-router on npmjs.com mirrors the deployed spec layer.

L4 judgment reference implementation: https://api.babyblueviper.com/ledger β€” live production judgment validator tracking spec revisions. Each revision reviewed against deployed endpoints; /commitment and /outcome sub-paths conform to Appendix A. See Acknowledgements.

External implementations:

  • WyriweVerifier (Jimmy Shi) β€” IProofVerifier implementation of the WYRIWE triple-hash scheme
  • WyriweProofVerifier (mainnet): 0xd8a09d830b27697e1b24e8c9800e562d20318a09
  • WyriweAttestationVerifier (mainnet): referenced in ccip-router npm package

Security Considerations

Attestor key compromise

The signature is only as trustworthy as the attestor's private key. Implementations that use WYRIWE attestations for settlement or dispute resolution SHOULD maintain an on-chain registry of authorised attestor addresses and support key rotation. A compromised attestor key allows forged attestations but does not break the hash commitments β€” a forged attestation for a hash with no matching input still cannot produce a valid preimage.

Hash collision resistance

WYRIWE relies on keccak256 collision resistance. No practical collision attacks against keccak256 are known. If keccak256 is broken, all three hashes are affected equally; the triple-hash construction does not introduce additional collision surface.

Adversarial sanitization specification

WYRIWE proves faithful application of a declared transform, not that the transform is benign. A gateway that publishes a sanitization specification designed to rewrite inputs maliciously (e.g., replacing a transfer amount or destination address) can commit to the application of that spec faithfully while still exploiting the caller. The scheme makes the transform fully auditable and attributable β€” any observer can fetch the spec at the committed CID and verify it was applied correctly β€” but semantic safety of the specification is a consumer responsibility, not a WYRIWE guarantee. Implementations that accept arbitrary sanitization spec CIDs SHOULD enforce an allow-list of approved CIDs. End users SHOULD verify the sanitization spec at the committed CID before trusting a result.

Sanitization spec CID stability

sanitization_pipeline_hash commits to a CID, not the spec content. If the IPFS content at the referenced CID becomes unavailable, step 3 of the verification procedure cannot be completed. Implementations SHOULD pin all referenced sanitization spec CIDs to ensure long-term verifiability.

Replay and cross-domain attacks

The timestamp field in WyriweAttestation is informational and does not prevent replay. Settlement contracts that consume WYRIWE attestations MUST enforce their own replay protection (e.g. by recording consumed inputHash values on-chain). The EIP-712 chainId in the domain separator prevents cross-chain signature replay.

Input availability

WYRIWE commits to the hash of the input, not the input itself. The raw input and sanitized input are not published by this standard. Parties who need to reproduce verification MUST retain the original inputs off-chain. WYRIWE does not define an input storage or retrieval mechanism.


Composition

This section documents known application patterns that reuse WYRIWE's triple-hash shape at adjacent stack layers. The shape β€” commitment = keccak256(abi.encode(inputCommitment, scopeBinding, attestingParty)) β€” is layer-agnostic; only the semantic content of each slot changes.

L2 Snapshot commitment (ccip-router)

The ccip-router's contribution settlement layer reuses the triple-hash directly:

snapshotRoot    = keccak256(abi.encode(rows))
commitmentHash  = keccak256(abi.encode(snapshotRoot, periodId, nodeAddress))

Slot mapping against WYRIWE's L3 scheme:

WYRIWE (L3 input provenance)ccip-router L2 snapshot settlement
rawInputHashsnapshotRoot β€” commitment to contribution rows
sanitizationPipelineHashperiodId β€” temporal scope of the settlement window
inputHash (submitted on-chain)commitmentHash (submitted to CommitRevealSettler)
agentIdnodeAddress β€” the attesting node

The snapshotRoot is derived from rows of (address contributor, uint256 score, uint256 timestamp) sorted by contributor address ascending, ABI-encoded. The commitmentHash is what a node submits during the commit phase; the full snapshot rows are revealed and verified against it during the reveal phase. The commit-reveal scheme closes the "committed β†’ executed" gap by construction: a node can only reveal data whose hash matches its commit.

Reference implementation: POST /contributions/snapshot/freeze in ccip-router v0.6.0.

L4 Judgment validator binding (@babyblueviper1)

The same chain-of-custody question exists one layer up for judgment validators: inputHash commits to the exact proposed action reviewed, but nothing yet binds "action reviewed" to "action executed after verdict." The triple-hash shape maps slot-for-slot:

WYRIWE (L3, input provenance)Judgment validator (L4, one layer up)
rawInputHash β€” what the user submittedrawProposalHash β€” what the agent proposed
sanitizationPipelineHash β€” public spec transforming raw β†’ permittedverdictHash β€” the judgment, including any conditions ("approve IF size halved") = the public spec transforming proposed β†’ permitted
inputHash β€” what the model actually receivedexecutedActionHash β€” what was actually executed
IDENTITY_SENTINEL β€” no-sanitization is a provable claimunconditional approve β€” executed-as-reviewed is a provable claim, not an assumption

Triple-hash construction:

Unlike the base WYRIWE triple-hash (L1–L3), whose algorithm is fixed to keccak256 because that layer exists specifically for an on-chain Solidity verify() call, L4's hash algorithm is deployment-dependent: sha256 for off-chain (relay-anchored) verdicts as published by the reference implementation, or keccak256 when a producer wraps the same fields in an on-chain EIP-712 struct. The construction below is written against the off-chain reference implementation (api.babyblueviper.com/ledger); an on-chain producer substitutes keccak256 and abi.encode for sha256 and JCS.

rawProposalHash    = sha256(proposed_action_artifact)
verdictHash        = sha256(JCS(verdict_preimage))
executedActionHash = sha256(executed_action_record)

rawProposalHash is the sha256 of the exact proposed-action artifact as submitted for review β€” no separate canonicalization step.

verdictHash is the sha256 of the JCS-canonicalized (sorted keys, no insignificant whitespace, literal UTF-8) JSON object of the verdict's named preimage fields β€” not a literal byte-concatenation keccak256(verdict_artifact_ref || rawProposalHash). The exact field set is producer-published on each proof and MUST include the field that pins the proposal (the one carrying rawProposalHash, e.g. artifact_hash) for the anti-replay binding to hold.

The preimage field set MUST be published alongside verdictHash as a named, self-describing field (e.g. a decision_ref_preimage_fields array). Consumers MUST recompute against the field list the producer declares on that proof, and MUST NOT assume a fixed set taken from this specification's examples. Any example field set given here β€” such as {artifact_hash, artifact_type, policy_version, verdict, source_class, vantage_limitation} β€” is illustrative, not exhaustive and not frozen.

This requirement exists because the set grows. In the production reference implementation it has grown twice, from six fields to twelve. A verifier that hardcoded an earlier example would have silently stopped reproducing verdictHash at each addition, and would have reported a mismatch as a bad proof rather than as its own stale assumption. JCS guarantees the preimage bytes recompute identically across languages and implementations.

verdictHash binds to the proposal β€” mirroring the sanitization-pipeline-hash construction β€” making verdict-shopping impossible: a verdict cannot be replayed against a different proposal than the one it judged. The anti-equivocation property is identical across storage backends (IPFS CID, Nostr event ID): the specific signed verdict artifact is pinned by its identifier/hash among the preimage fields, so a validator cannot swap between multiple signed verdicts post-reveal.

Optional preimage fields. Two further fields are defined for the verdict preimage. Both are additive: a preimage omitting them remains conformant, and neither replaces an existing field.

intended_verifier (SHOULD) β€” a CAIP-10 account identifier naming the on-chain verifier instance the verdict is issued for. Producers SHOULD include it whenever the consuming IProofVerifier instance is known at judgment time. It is not a MUST because not every verdict has a single known consumer when it is judged.

Without it, the preimage binds the judging party and the content judged, but nothing about who the judgment was for. A technically valid verdictHash is therefore replayable against any verifier contract willing to accept it β€” not only the one it was produced for. This was surfaced by an independent verifier gate testing the reference implementation against a live MandateGate deployment, not anticipated in the abstract. intended_verifier supplies crypto-level domain separation at the preimage layer, so a verdict judged for one gate does not authenticate at another.

policy_commitment (SHOULD) β€” a commitment to the policy actually in force, published alongside policy_version rather than instead of it:

policy_commitment = sha256(JCS({
  policy_version,
  rubric_sha256,
  conformance_suite_repo,
  conformance_suite_commit
}))

A version string asserts that a policy was in force; it is not itself recomputable, so a consumer must trust the producer's later account of what that version meant. The commitment lets a stranger fetch the rubric text and the conformance vectors at the pinned commit and confirm the hash themselves, independent of that account. policy_version remains useful as a human-readable label; policy_commitment is what a recompute checks against.

EIP-712 struct:

struct JudgmentExecutionAttestation {
    bytes32 agentId;             // ERC-8004 identity of the EXECUTING agent
    address registry;            // ERC-8004 registry address
    bytes32 validatorId;         // ERC-8004 identity of the judgment validator. MUST NOT be omitted.
                                 // Zero value signals an off-registry validator whose identity
                                 // MUST be resolvable from the verdict artifact (e.g. schnorr pubkey in a signed Nostr event).
    bytes32 rawProposalHash;     // 32-byte hash of the proposed-action artifact, pre-review.
                                 // Deployment-dependent: sha256(artifact) off-chain / keccak256(artifact) on-chain.
    bytes32 verdictHash;         // 32-byte hash binding the verdict to rawProposalHash.
                                 // sha256(JCS(verdict_preimage)) off-chain (preimage pins the proposal) /
                                 // keccak256(verdict_artifact_ref || rawProposalHash) on-chain.
    bytes32 executedActionHash;  // 32-byte hash of the executed-action record, revealed at settlement
                                 // (same algorithm as the deployment above).
    uint256 verdictTimestamp;    // verdict issuance β€” the commit, strictly pre-execution
    uint256 executedTimestamp;   // execution β€” the reveal
    string  recordPointer;       // URI to the ledger entry. Standard sub-paths:
                                 //   {recordPointer}/commitment β€” pre-settlement evidence
                                 //     (signed verdict, relay anchor, judgment execution commitment)
                                 //   {recordPointer}/outcome    β€” post-settlement evidence
                                 //     (settlement account, signed outcome digests)
                                 // Empty string if not yet anchored.
}

The recordPointer URI resolves to a record conforming to the RecordPointer schema defined in Appendix B. commitmentProof and outcomeEvidence MUST remain separately resolvable (see design note 4 and Appendix B).

Type string:

JudgmentExecutionAttestation(bytes32 agentId,address registry,bytes32 validatorId,bytes32 rawProposalHash,bytes32 verdictHash,bytes32 executedActionHash,uint256 verdictTimestamp,uint256 executedTimestamp,string recordPointer)

Domain separator: ERC8004AttestationGateway / version "1" / block.chainid β€” same as WyriweAttestation by design. Struct typehash prevents cross-type confusion; attestor address serves as deployment-level identity. A dedicated judgment gateway SHOULD use the same domain name β€” splitting it would fork verifier code paths without adding security.

proofSystem() = "attestation/judgment" β€” returned by the IProofVerifier implementation; lives in the verifier path (contract context). claimType = Judgment β€” carried inside the signed artifact for off-chain consumers (indexers, dispute interfaces) that read the struct without calling the verifier contract. The two fields serve distinct routing purposes and MUST NOT be conflated.

Design notes:

  1. Signature roles. Only one signature is required β€” the executing agent's attestor signs the EIP-712 digest at reveal time. The validator's own signature lives inside the verdict artifact that verdictHash pins, so validator authenticity is carried without a second signature field. This keeps the IProofVerifier layering clean: IProofVerifier authenticates the attestation; the verdict artifact authenticates the judgment.

  2. Commit-reveal invariant. verdictTimestamp < executedTimestamp MUST hold. executedActionHash SHOULD be committed at verdict time — when the post-verdict intent hash is knowable — not post-execution. submitReveal is then called post-execution with settlement evidence linked separately rather than hashed into the record. The verdict artifact is published at commit time (relay-anchored in the reference implementation). The reviewed→executed gap closes by the same argument as WYRIWE's reviewed→input gap: the executor can only reveal an action whose hash matches what was committed and judged. This two-step pattern maps directly to CommitRevealSettler.submitCommit / submitReveal.

  3. Canonicalization as verification step 3. The executed action record never byte-equals the proposal (a fill has a price; a proposal has an intent), so the verdict artifact doubles as the conformance spec the verifier applies β€” exactly the role the sanitization spec CID plays in WYRIWE verification step 3. The unconditional-approve case degenerates to canonical field equality, which is the sentinel.

  4. recordPointer, evidence separability, and verify() semantics. The recordPointer target MUST keep commitment evidence and outcome evidence separately addressable via standard sub-paths: {recordPointer}/commitment returns pre-settlement evidence (signed verdict, relay anchor, judgment execution commitment); {recordPointer}/outcome returns post-settlement evidence (settlement account, signed outcome digests). A verifier MUST be able to check commitment without outcome (pre-settlement) and outcome without re-deriving commitment (post-settlement). A single combined document breaks this invariant: a dispute client cannot prove it did not inspect the outcome before evaluating the commitment.

    For ClaimType.Judgment, verify() = true means the verdict is authentically the validator's and correctly bound to the task inputs. It does NOT mean the judgment is sound, that the action should proceed, or that the verdict has been independently endorsed. A settlement contract that gates directly on the bool has confirmed authenticity β€” it has not endorsed the judgment. Verdict weight lives in the accountability record, not in the bool.

    The three-layer accountability boundary:

    IProofVerifier  β€” authenticates the verdict (EIP-712 binding is valid)
    IAgentVerifier  β€” checks validator authorization for the task
    recordPointer   β€” carries accountability: track record + pre-outcome commitment

    Reference implementation: api.babyblueviper.com/ledger/{n}/commitment and api.babyblueviper.com/ledger/{n}/outcome.

Honesty conventions from the reference implementation (generalise to any producer):

  • Entries predating the wiring carry a partial block with executed_action_hash: null and an explicit "not backfilled by design" status. A commitment you did not make at the time is not one you get to manufacture later.
  • Where a production system records a single timestamp per governance cycle, executedTimestamp is null with an ordering note rather than a fabricated reveal time. The strict verdictTimestamp < executedTimestamp invariant belongs to the on-chain attestation; an off-chain production mapping should record what it actually measured.

Reference implementation: api.babyblueviper.com/ledger β€” live production ledger. Entry /ledger/3 shows a pre-wiring partial block; new entries carry the full judgment_execution block. Running against real capital.

  1. string recordPointer is the correct type for EIP-712. The RecordPointer struct (Appendix B) is the resolved payload schema β€” it is NOT inlined into the signed type. The attestation is signed once and frozen at verdict time; the record it points to is alive and grows over time (outcomeEvidence does not exist when the verdict attestation is signed). Inlining RecordPointer into the EIP-712 struct would require signing a permanently incomplete field. The on-chain anchoring of the verdict artifact (Nostr event ID or IPFS CID in verdictHash) already secures commitment integrity independently of the pointer. The 9-field type string is therefore stable and MUST NOT be changed pre-review.

Appendix A β€” Cross-system settlement: GenericCommitRevealSettler integration

This appendix documents the integration pattern for judgment attestations with GenericCommitRevealSettler as a reference for any L4 producer. First demonstrated in the cross-system settlement of ledger entry 19 at api.babyblueviper.com/ledger/19 β€” commit block 11030402, reveal block 11030403.

Contract: GenericCommitRevealSettler on Sepolia: 0xFe7Ab6d95f7567a311B98D029373d0fc1511aCCe

Bytes-opaque commit/reveal primitive. No bond, no NodeType gate. Verifies preimage binding only β€” usable for contribution snapshots (mesh node settlement), judgment attestations (WYRIWE L4), OCP observations, or any future schema.

Hash construction

commitmentHash = keccak256(abi.encode(record, periodId, committer))

Binding to periodId + committer prevents cross-period and cross-sender replay without the contract knowing the record schema.

Encoding convention for judgment attestations

bytes memory record = abi.encode(
    rawProposalHash,     // bytes32 β€” what was proposed
    verdictHash,         // bytes32 β€” the judgment, binding to rawProposalHash
    executedActionHash,  // bytes32 β€” what was actually executed
    verdictTimestamp     // uint256 β€” verdict issuance, strictly pre-execution
);

Minimal set that closes the reviewed→executed gap. Attestation metadata (agentId, registry, validatorId, recordPointer) is carried in the full struct at settlement time and does not need to be committed.

Before calling submitCommit, verify the hash binding off-chain at zero gas cost:

// eth_call β€” no gas spent
bytes32 expected = computeCommitmentHash(record, periodId, committerAddress);
// verify expected == locally computed hash before submitting

Treat this as a required preflight, not an optional check. If the hashes do not match byte-exact, there is an encoding error β€” fix it before spending gas.

periodId convention

Use the ledger entry number as periodId. Creates a 1:1 binding between ledger entries and settlement periods; makes getCommit(periodId, committer) directly queryable from the entry index.

Workflow

1. record           = abi.encode(rawProposalHash, verdictHash, executedActionHash, verdictTimestamp)
2. commitmentHash   = keccak256(abi.encode(record, periodId, committerAddress))
3. Preflight        : eth_call computeCommitmentHash(record, periodId, committerAddress) β†’ verify match
4. submitCommit(periodId, commitmentHash)
5. Execute the governed action
6. submitReveal(periodId, record)

submitReveal reverts on mismatch. Reveal window: 48 hours from commit. Challenge period: 7 days from reveal.

Invariant check

CommitRecord memory c = getCommit(periodId, committer);
// c.committedAt > 0                          committed
// c.revealedAt  > c.committedAt              temporal ordering holds
// c.recordHash  == keccak256(record)         preimage binding holds

The Revealed event emits the full bytes record β€” any observer can verify the preimage independently from the event log.

recordPointer sub-paths

{recordPointer}/commitment MUST return pre-settlement evidence: signed verdict, relay anchor, commitmentHash, block number of submitCommit. {recordPointer}/outcome MUST return post-settlement evidence: recordHash from getCommit, reveal transaction hash, settlement account. These are separately resolvable β€” a dispute verifier MUST be able to confirm commitment integrity without accessing outcome evidence.


Appendix B β€” RecordPointer Schema

The RecordPointer struct is the resolved payload schema for the string recordPointer field in JudgmentExecutionAttestation. It is NOT part of the EIP-712 signed type β€” the attestation is signed once and frozen at verdict time; the record it points to grows over time as outcomeEvidence accumulates. See design note 5.

Struct definition

struct RecordPointer {
    bytes32 validatorId;      // ERC-8004 identity of the judgment validator.
                              // Zero value: off-registry validator β€” identity MUST resolve from the
                              // verdict artifact itself (e.g. schnorr pubkey of a signed Nostr event).
                              // Consumers MUST reject if resolution fails.
    bytes32 registryType;     // keccak256 of registry type string β€” see table below.
    bytes   registryRef;      // registry-specific locator (contract address, Nostr pubkey, URL, etc.)
    bytes   commitmentProof;  // pre-settlement evidence: signed verdict, relay anchor, commit hash.
                              // SHOULD open with a self-describing mechanism identifier byte prefix
                              // so consumers can select the correct trust model without external context.
    bytes   outcomeEvidence;  // post-settlement evidence: settlement account, outcome digests.
                              // MAY be empty before settlement closes.
                              // SHOULD open with a self-describing mechanism identifier byte prefix.
}

registryType values

Identifier stringkeccak256Description
"evm/registry"β€”On-chain ERC-8004 registry. registryRef = ABI-encoded (address registry, bytes32 agentId).
"nostr/profile"β€”Nostr relay profile. registryRef = UTF-8 encoded npub or hex pubkey.
"offchain/ledger"β€”Off-chain ledger URL. registryRef = UTF-8 encoded base URL.

Producers SHOULD use one of the above identifiers. Custom types are permitted; consumers encountering an unknown registryType SHOULD surface it as unrecognised rather than failing silently.

Mechanism identifier convention

commitmentProof and outcomeEvidence are opaque bytes. Each SHOULD open with a UTF-8 encoded self-describing mechanism identifier followed by a null byte (0x00) separator, so consumers can select the correct trust model without external context:

IdentifierMeaning
"nostr-relay-publication"Nostr event anchor β€” Nostr event ID, relay set, relay_anchor retention status
"onchain-commitment"On-chain commit tx hash and block number (e.g. GenericCommitRevealSettler)
"onchain-settlement+digests"On-chain reveal tx hash + signed outcome digests
"offchain-ledger+digests"Off-chain settlement account URL + signed outcome digests

validatorId = 0x0 β€” off-registry validators

When validatorId is zero, the validator is not registered in an ERC-8004 registry. The validator identity MUST resolve from the verdict artifact itself β€” for example, the schnorr pubkey embedded in a signed Nostr event pinned by verdictHash. Consumers MUST reject the record if resolution fails. Zero validatorId is a valid state, not an error; it describes validators whose identity is carried in the artifact rather than an on-chain registry.

Relay retention

For Nostr-anchored verdicts, relay copies are not guaranteed to persist. NIP-33 parameterized-replaceable events can silently overwrite relay copies β€” naive event-fetching by event ID may fail even when the commitment binding survives intact. Producers SHOULD surface a relay_anchor retention status field within commitmentProof that declares actual relay availability rather than assuming it. Consumers MUST treat an unavailable relay copy as a retrieval failure, not as evidence of non-commitment.

The relay_anchor status SHOULD use one of the following values:

StatusMeaning
"anchored"Original event live on all declared relays.
"replaced_by_newer_same_artifact"Original event overwritten by NIP-33 replacement; surviving event commits to identical artifact hash. Recovery path: consumer verifies the surviving event satisfies the same commitment check.
"unverified"Relay availability could not be confirmed at record publication time.

The replaced_by_newer_same_artifact state distinguishes a recoverable NIP-33 overwrite (same content, different event ID) from an unrecoverable loss. When a producer identifies a surviving equivalent event, consumers can confirm publication timing from the survivor β€” but the survivor's timestamp is strictly later than the original's. A NIP-33 replacement eliminates the relay-attested timestamp of the original event; the survivor proves the verdict existed by the survivor's timestamp, not by the original publication time. verdictHash binding is separate: it ties to the specific original event ID committed on-chain and is not affected by relay churn; the original event ID remains in the on-chain commitment record regardless of relay availability. These are two distinct proof layers β€” relay anchor confirms publication occurred; on-chain verdictHash confirms what was published. This recovery path is anchor-type-specific β€” the mechanism identifier tag ("nostr-relay-publication") scopes what "event id" means within that trust model; other anchor types require their own recovery semantics.

Timestamp hierarchy for pre-outcome commitment evaluation. Consumers evaluating whether a verdict was committed before execution MUST apply the following precedence:

  1. Original relay copy available β€” use the original relay-attested timestamp. Strongest proof; no gap.
  2. On-chain committedAt (settled entry) β€” use the committedAt block timestamp from the settlement contract. Decay-resistant; independent of relay state. Valid even when relay copy is unavailable.
  3. Survivor timestamp (replaced_by_newer_same_artifact) β€” fallback only. The survivor's timestamp is strictly later than the original publication time. Production-observed gap: 15–45 minutes (illustrative; normative content is the hierarchy, not the specific numbers). Consumers MUST NOT treat survivor timestamp as equivalent to original relay-attested timestamp.

Claiming tier-1 timing strength from a tier-3 source is incorrect. For settled entries, tier 2 is always available and SHOULD be preferred over the survivor timestamp regardless of relay state.

Production finding: in the reference implementation, 9 of 18 relay copies were silently replaced via NIP-33 overwrites before the relay_anchor status field was introduced. The commitment bindings survived; naive fetching would have failed. Observed gap between original publication and NIP-33 replacement: 15–45 minutes.

Production mapping β€” reference implementation

Ledger entry at https://api.babyblueviper.com/ledger/19:

RecordPointer fieldValue
validatorId0x0 (off-registry β€” schnorr pubkey resolves from the signed Nostr event)
registryTypekeccak256("offchain/ledger")
registryRefhttps://api.babyblueviper.com/ledger/19
commitmentProofresolves at /ledger/19/commitment β€” Nostr event ID, relay set, relay_anchor retention status
outcomeEvidenceresolves at /ledger/19/outcome β€” settlement account, signed outcome digests (pending until settlement)

Appendix C β€” Identity-Transform Specification (IDENTITY_SENTINEL_CID)

This appendix reproduces the normative content of the identity-transform specification pinned at ipfs://QmccvoM6aRVgZ2dtFWvT6Wm3DmTvoAUHHotK7uQufnStVR. It is included here so the standard is self-contained in the event of IPFS unavailability. The content at the CID MUST match this text exactly. Any discrepancy between the pinned CID and this appendix is an error in the pinned content, not in this document.

{
  "name": "identity-transform",
  "version": "1",
  "description": "The identity sanitization pipeline. No modification is applied to the input. The sanitized output is identical to the raw input byte-for-byte.",
  "transform": "none",
  "input": "raw_user_input (bytes, unmodified)",
  "output": "raw_user_input (bytes, identical to input)",
  "invariants": [
    "sanitized_input == raw_user_input",
    "input_hash == raw_input_hash",
    "sanitization_pipeline_hash == keccak256(IDENTITY_SENTINEL_CID_bytes || raw_input_hash)"
  ],
  "notes": "Use this CID when no sanitization is applied. The input_hash == raw_input_hash equality is a provable on-chain claim. Implementations MUST NOT substitute any other CID to represent the no-sanitization case."
}

The CID was derived from the above JSON content (UTF-8 encoded, no trailing newline) using the IPFS CIDv0 (SHA2-256 multihash) algorithm. Implementations that re-derive the CID from this content MUST produce QmccvoM6aRVgZ2dtFWvT6Wm3DmTvoAUHHotK7uQufnStVR.


References

  • ERC-8004 β€” Verified Node Identity (agent identity layer)
  • ERC-8126 β€” AI Agent Verification (Final)
  • ERC-8183 β€” Agentic Commerce
  • VeriLLM (Ke Wang et al., Gradient, 2025; arXiv:2509.24257) β€” publicly verifiable decentralized LLM inference via Merkle-committed hidden states and VRF-based recompute. Compute-integrity complement to WYRIWE's behavioral-integrity layer.

Acknowledgements

  • Vincent Wu (@TruthAnchor-AI) β€” co-author. Contributions: TruthAnchorV1 layer-boundary definition establishing the proof-commitment / anchor surface as a distinct primitive from OCP (observation commitment); TruthAnchorV1 / AnchorProof canonical event as the anchor layer; separation of AttestationIndex (commitment store) from TruthAnchorV1 (event layer) as composable without either absorbing the other; interoperability path from gateway-produced signed attestation through to IProofVerifier-style settlement consumption.

  • Jimmy Shi β€” first external implementation of WYRIWE: WyriweVerifier wrapping the triple-hash scheme as an IProofVerifier. Co-author contributions include technical corrections to inputHash derivation (not keccak of the two hashes), ATTESTATION_TYPEHASH field names (manifestHashβ†’modelHash, agentId uint256β†’bytes32, timestamp uint64β†’uint256), block.chainid dynamic requirement, and proofSystem = "attestation/wyriwe" taxonomy placement.

  • Damon Zwicker (@damonzwicker) β€” co-author. Contributions: ClaimType enum definition and proofSystem / claimType separation rationale (Section 7); RecordPointer typed schema formalizing the commitmentProof / outcomeEvidence distinction; JudgmentVerificationCompleted event definition; verify() three-layer accountability boundary for ClaimType.Judgment; OCP commitment discipline integration; worked example gist (claimType field mapping, RecordPointer schema, verify() semantics).

  • babyblueviper1 (@babyblueviper1) β€” co-author. Production judgment validator operator. Primary author of the L4 Composition section: JudgmentExecutionAttestation EIP-712 struct and triple-hash construction; slot-for-slot WYRIWE mapping; claimType field concept; verify() semantic clarification (authenticates verdict, does not endorse soundness); recordPointer field and commitment/outcome separability invariant; Nostr relay anchoring as timestamp commitment primitive; codeMeasurement MUST be absent for claimType = Judgment; verdictHash construction clarification (verdict_artifact_ref covers both IPFS CID and Nostr event ID forms); closing the string recordPointer vs inline struct question ("attestation frozen, record alive"); Appendix A verification against deployed code. Production reference implementation at api.babyblueviper.com/ledger β€” running against real capital.


Copyright and related rights waived via CC0.

EIP.tools

EIP.tools

Search, read, and map Ethereum improvement proposals, ERCs, RIPs, and CAIPs from one focused interface.

Farcaster
by @apoorveth