Gateway-to-gateway coordination for EIP-3668 / Proposing a mesh sync protocol

Category: EIPs / ERC Discussion
Tags: eip-3668, ccip-read, gateways, attestation
Co-authors: Tiago Merlini (@TMerlini), Damon Zwicker (OCP) @Damonzwicker , Vincent Wu (ERC-8263 / Composition Note) @VincentWu , Jimmy Shi (ERC-8274) @JimmyShi22


The gap

EIP-3668 precisely specifies how a client talks to a CCIP-Read gateway: the request format, the response envelope, the revert convention. It says nothing about how gateways talk to each other.

This is fine for the simplest case — one gateway, one operator. But as CCIP-Read adoption grows, three problems appear that a single gateway can’t solve:

  1. Redundancy. If the gateway goes down, the ENS name (or any CCIP-Read resolver) is broken. There’s no standard way for a second gateway to serve the same namespace. This matters beyond simple uptime: a CCIP-Read dapp whose pages are pinned as IPFS CIDs has no frontend server to take down — but it still has a single gateway that can fail. A mesh sync protocol removes that last centralised dependency: any node that has synced the namespace can answer. Combined with CID-pinned pages, the result is a dapp with no single point of failure at either layer.

  2. Auditability. What did the gateway actually receive and return? Without a signed, replicable record of each call there’s no way to verify the gateway didn’t tamper with the response.

  3. Attribution. In a multi-party system (multiple agents, multiple nodes) who contributed what to the shared record set?

None of these are addressed by EIP-3668 itself, and there’s been no follow-up coordination standard.


A proposed protocol

We’ve been running a reference implementation for a few months and want to propose a minimal gateway-to-gateway sync protocol for discussion.

The core primitive is a single endpoint any EIP-3668 gateway can expose:

GET /records?namespace=<str>&since=<unix>&limit=<n>&cursor=<str>


Response:

{
  "protocol": 1,
  "node_version": "0.3.0",
  "namespace": "agent-attestations",
  "records": [
    {
      "inputHash":  "0x...",
      "namespace":  "agent-attestations",
      "key":        "0x...",
      "value":      "0x...",
      "timestamp":  1234567890,
      "signature":  "0x...",
      "sourcePeer": null
    }
  ],
  "cursor": "1234567890|0xabc..." 
}


A few deliberate design choices:

Cursor pagination over offset. Using a timestamp|inputHash composite cursor means records are never skipped at timestamp boundaries, even under concurrent writes. Pure offset pagination loses records when new ones are inserted between pages.

INSERT OR IGNORE deduplication. The composite primary key is (inputHash, namespace). The same record arriving from two peers is stored once. Gossip is safe to run freely.

Protocol version field. Nodes on different protocol versions skip each other with a warning rather than silently accepting malformed data.

Signer pinning. On first sync from a peer, the recovering signer address is stored. Subsequent records from a different signer are rejected — a compromised peer can’t inject records on behalf of another node.

Namespace scoping. Each pull is scoped to a namespace string. A node serving token-metadata and a node serving agent-attestations are fully isolated at the record level even if they peer with each other.


Layering attestation on top

The sync protocol above is transport-level — it just moves signed records around. We’ve also been working on what sits above it: a way to prove what the gateway received and what it returned, not just that it wrote a record.

The approach is to wrap any resolver function with a pipeline that:

  1. Hashes the raw calldata: rawInputHash = keccak256(calldata)

  2. Optionally applies a sanitization pipeline hash: inputHash = keccak256(abi.encode(rawInputHash, pipelineHash)) — or uses keccak256("IDENTITY_SENTINEL") if no sanitization was applied

  3. Hashes the response: outputHash = keccak256(response)

  4. Computes a commitment: commitmentHash = keccak256(agentId · modelHash · inputHash · outputHash · timestamp)

  5. Signs an EIP-712 WyriweAttestation struct containing all of the above

These attestations sync alongside regular records using the same /records protocol, in a separate {namespace}:wyriwe namespace. Any peer can verify the EIP-712 signature and reconstruct the commitment hash independently.

The commitment hash can then be anchored on-chain — a single 32-byte write that makes the attestation immutable and queryable without trusting any single gateway.

Contracts deployed on Sepolia (Etherscan verified):

Contract Role Address
AttestationIndex ccip-router OCP-compatible commitment store 0x107D706112225aC57eCf6692FBbDC283fb6E3698
NodeRegistry Node registration 0x6be4966596A9CBaa7260ab6EbbFFA69bBC9a42b7
WyriweProofVerifier ERC-8274 IProofVerifier 0x001eFFa0fD1D171b164808644678F3301d8EDC96
TruthAnchorV1 (ERC-8263 canonical — Vincent Wu) ERC-8263 reference contract · Mainnet 0xe95d6a15966984c209a62a2c188828555eb5ec3d

AttestationIndex is ccip-router’s own commitment store — signerOf[commitmentHash] + commitmentOf[inputHash]. It satisfies the OCP commitment invariant and is a valid OCP-compatible anchor, distinct from the ERC-8263 canonical contract. TruthAnchorV1 emits the canonical AnchorProof(uint8 agentIdScheme, bytes32 agentId, bytes32 proofHash, address operator, bytes aux) event; AttestationIndex is the transport-layer anchor the gateway writes to after each attested execution. The two are separate primitives by design.

How ccip-router connects to ERC-8263: ccip-router anchors its commitmentHash as the proofHash in TruthAnchorV1. ERC-8263’s proofHash is deliberately opaque — the same anchor layer serves OCP, WYRIWE, and zkML uniformly. ccip-router’s commitmentHash = keccak256(abi.encode(agentId, modelHash, inputHash, outputHash, timestamp)) is one canonical instantiation, not the definition. Full chain: inference runs → gateway signs WyriweAttestation producing commitmentHashanchor(commitmentHash) called on TruthAnchorV1 as the proofHashAnchorProof event emitted. To verify L3 anchoring, filter AnchorProof by proofHash topic (= your commitmentHash) via eth_getLogs and compare the anchoring block’s timestamp against execution time. V1 is event-only by design — no per-anchor storage cost. A synchronous on-chain view (IAnchorReader) is proposed for ERC-8263 v0.3.

WyriweProofVerifier implements ERC-8274 IProofVerifier — an L4-only attestation check: “did the authorized gateway for this agentId attest that inputHash produced outputHash?” Parameters:

  • inputHash, outputHash — explicit inference I/O commitments

  • metadata = abi.encode(agentId, registry) — authorized signer identity

  • proof = abi.encode(modelHash, rawInputHash, sanitizationPipelineHash, commitmentHash, timestamp, sig) — L4 cryptographic material

verify() recomputes commitmentHash from (agentId, modelHash, inputHash, outputHash, timestamp), reconstructs the EIP-712 digest from the full struct, and recovers the signer. No external calls. The gateway’s prior signature on the struct guarantees the rawInputHash → sanitizationPipelineHash → inputHash provenance chain; the verifier trusts it without re-validating. This closes the loop: a commitment coordinated across the mesh is settleable on-chain by any ERC-8274-compatible contract.


ENS wildcard resolution

One practical application of this stack is an ENS offchain resolver gateway. The reference implementation ships a withEns() wrapper that decodes resolve(bytes name, bytes data) calldata (the EIP-137 wildcard resolution pattern) and dispatches to a clean handler:

import { CcipRouter, withEns } from 'ccip-router'

const ccip = new CcipRouter({
  resolver: withEns(async (name, record) => {
    // name   → "vitalik.eth"
    // record → { type: 'addr' } | { type: 'addr', coinType: 60n }
    //           { type: 'text', key: 'avatar' } | { type: 'contenthash' }
    return db.lookup(name, record)   // return value string or null
  }),
})


withEns() handles DNS wire-format decoding, selector dispatch (addr, addr(uint256), text, contenthash), and ABI encoding of the response. null maps to the correct zero value per record type. Unknown selectors return 0x rather than throwing.

The standalone node ships with a DB-backed ENS resolver by default. Records are managed from the admin panel — no code required. Any name pointing to this gateway via an on-chain CCIP-Read resolver is served automatically.

Composing with attestation. Put withEns inside withWyriwe so every ENS resolution carries a full WyriweAttestation — the raw calldata hash, model hash, input/output commitment, and EIP-712 signature:

resolver: withWyriwe(withEns(myResolver), attestationOpts)


Operator identity via SIWE. The admin dashboard now authenticates via Sign-In With Ethereum (EIP-4361). The authorized signer is the node’s own gateway key — the same key that signs every record. Holding the private key proves you operate the node; no separate password.


Settlement layer

The commitmentHash OCP anchors on-chain is the primitive any settlement contract needs before releasing funds. ERC-8274 proposes a minimal IProofVerifier interface for this L4 step — one verify(inputHash, outputHash, metadata, proof) call that any consumer can make without knowing which proof system sits underneath. The interface is scoped narrowly: “did an authorized party attest this inference result?” It does not validate L3 anchoring (the gateway’s responsibility) or the input provenance chain (already guaranteed by the gateway’s EIP-712 signature on the struct). This maps naturally to the oracle/multisig pattern — the gateway is the authorized attester, metadata carries its identity, proof carries the cryptographic evidence.

WyriweProofVerifier is the concrete ERC-8274 implementation for this stack, deployed on Sepolia at 0x001eFFa0fD1D171b164808644678F3301d8EDC96. inputHash is the shared cryptographic anchor tying every layer together — from raw calldata through sanitization pipeline through commitment through on-chain anchor through settlement.


What we’re looking for feedback on

  1. The /records protocol itself. Is the cursor design sound? Should protocol version negotiation be more formal (e.g. a GET /protocol endpoint)? Are there failure modes we haven’t considered?

  2. Namespace design. Right now namespaces are arbitrary strings — operators must coordinate out of band. Should there be a registry, or is that unnecessary complexity?

  3. Signer pinning trade-offs. Pinning on first sync is simple but means a node that rotates its key looks like a different node to peers. Is there a better identity primitive here? (We’ve been experimenting with VNI — a signed { nodeId, signerAddress, url } document where nodeId = keccak256(signerAddress) so it’s stable across key rotations if the operator re-signs.)

  4. Whether this warrants a follow-up EIP. The /records protocol is simple enough that it could be a short informational EIP as a companion to EIP-3668. Or it may be better left as a de-facto standard that implementations converge on. Opinions welcome.

  5. Per-execution vs per-configuration commitment. The commitmentHash produced here is strictly per-execution — it binds inputHash, outputHash, agentId, modelHash, and timestamp for a specific call. Whether a separate stable commitment describing the agent configuration (model, pipeline) should be anchored once and referenced across executions is an open question. Carrying it here since it sits at the transport layer where it would need to be surfaced.


Reference implementation

ccip-router v0.3.0 — npm package + standalone node with setup wizard, admin dashboard, and all of the above wired up:

v0.3.0 includes: WYRIWE_PROOF_VERIFIER_ABI export for WyriweProofVerifier (ERC-8274 IProofVerifier), withEns() ENS wildcard resolver wrapper, DB-backed ENS records (managed from admin panel, no code required), SIWE admin auth (EIP-4361, gateway key = admin identity), isEnsCalldata() guard for multi-purpose resolvers.


This work connects to WYRIWE, OCP (Damon), ERC-8263 (Vincent), ERC-8004, ERC-8274, and ERC-8275 — earlier threads on those specs are the right place to discuss the attestation and settlement layers themselves. This thread is specifically about the gateway coordination layer.


Co-authored with Damon Zwicker (OCP), Vincent Wu (ERC-8263 / Composition Note), and Jimmy Shi (ERC-8274).

4 Likes

Congratulations Tiago on shipping this. The thread is exactly the right framing.
From the OCP side: the commitmentHash construction satisfies OCP’s verification invariant because inputHash is an explicit named field. The /ocp/:inputHash endpoint making it independently queryable from the gateway closes the read side cleanly.
The separation Vincent clarified between TruthAnchorV1 and AttestationIndex is the right architecture. Two different primitives composing at the same layer without either absorbing the other.
Damon / OCP

3 Likes

Really glad to see this up. The gateway-to-gateway coordination gap is one of those problems that’s easy to overlook until you’re actually running production infrastructure — and the /records protocol is a clean, minimal answer to it.

From the ERC-8274 side: the settlement layer framing in the post is exactly right. IProofVerifier is scoped narrowly — “did an authorized party attest that this input produced this output?” — and WyriweProofVerifier is the concrete instantiation of that for the WYRIWE / ccip-router stack. The fact that the same inputHash flows from raw calldata through the sanitization pipeline through the on-chain anchor through settlement is what makes the full stack auditable end-to-end.

Looking forward to seeing how the community responds to the five open questions, particularly on whether this warrants a follow-up EIP. Happy to contribute on the settlement layer side wherever it’s useful.

3 Likes

Thanks both, this is exactly the validation the design needed.

@Damonzwicker the OCP compatibility was intentional: inputHash as an explicit top-level field in commitmentHash was designed so any OCP-compliant verifier can resolve the chain independently without parsing the full attestation. The /ocp/:inputHash read path closes the loop on that. Glad the primitive boundary with TruthAnchorV1 reads correctly — the goal was two anchors that compose at the same layer without either one needing to know about the other’s internals.

@JimmyShi22 the IProofVerifier scoping point is appreciated, especially in light of the ERC-8183 discussion running in parallel. The inputHash thread through the stack (CCIP-Read response → wyriwe EIP-712 signature → commitmentHash → on-chain anchor) is now working end-to-end in production.

Status update since the original post: AttestationIndex is now live on Ethereum mainnet (0xc7BCCD785Fb994e570d0ca10D0F7899d87C82210) — upgraded from Sepolia. The 3-node mesh (NAS operator node, public Railway node, ENS Boiler gateway) is running the full protocol: peer sync via /records, EIP-712 wyriwe attestation on every CCIP-Read, and commitment anchoring on-chain.

All three nodes report all tiers active — spec audit panel and raw health below for reference:

// gateway.gen-plasma.com/health
{"version":"0.5.6","tiers":{"signed":true,"erc8004":true,"wyriwe":true,"ocp":true,"vni":true,"onChain":true}}

// ccip-router-production.up.railway.app/health
{"version":"0.5.6","tiers":{"signed":true,"erc8004":true,"wyriwe":true,"ocp":true,"vni":true,"onChain":true}}

// gateway.ensub.org/health
{"version":"ens-boiler","tiers":{"signed":true,"erc8004":true,"wyriwe":true,"ocp":true,"vni":true,"onChain":true}}


The protocol is no longer just a proposal, contract verification links and /records endpoints open for anyone who wants to poke at the live system.

One open design question surfacing from running this in production: formalising node roles. Currently the mesh implicitly has two types, ENS gateway nodes (record authority, attestation signer, no peer sync) and router nodes (full ccip-router peer sync, attestation relay, on-chain anchoring). Making this explicit, with a nodeRole field in /health and role-aware peer discovery, would let the mesh self-describe its topology and allow lightweight relay-only nodes to join without needing the full ENS resolver stack. Happy to write that up as a follow-on if there’s appetite for it.

3 Likes

Thanks for putting the time into this proposal.

That said, the immediate value of standardising this is not entirely clear to me.

For EIP-3668, the important primitive is not gateway-to-gateway coordination; it is verifiable gateway output. If a gateway response is signed by an authorised signer and checked in the callback, or accompanied by a proof that the callback verifies, then the client does not need to trust the gateway.

Mesh synchronisation may improve availability for a particular gateway operator’s data, but it does not appear to provide a new trust primitive, and I’m therefore not convinced it belongs as part of a general EIP-3668 adjacent standard.

For data stored on Layer 2’s, ENS utilizes Unruggable Gateways. Subname providers that are running offchain data stores for ENS resolution tend to simply sign their responses. Additionally 3668 allows for multiple gateway endpoints to be specified so you can fallback appropriately within your resolver contract architecture.

Perhaps I’m missing something, but it seems that the proposal is primarily addressing replication and availability of gateway-managed data, rather than a trust or verification problem that EIP-3668 does not already solve. Data availability is - in my opinion - a vendor specific implementation detail.

2 Likes

Thanks for the pushback clowes.eth this is exactly the right tension to surface.

You’re correct that signed, verifiable gateway output is the foundational primitive, and we agree completely. This proposal doesn’t compete with that, it sits on top of it.

The gap we’re targeting is specific to off-chain attestation data where there is no L1/L2 source of truth to prove against. Unruggable Gateways work beautifully for L2 storage: the data exists on-chain, you prove it with a storage proof, and signing authenticates the gateway’s derivation of that proof. But when the data itself is the signed artifact, AI inference attestations, output commitments, agent observation records, there is no base layer to fall back to. The signed record is the record.

In that case, “use multiple gateway endpoints” doesn’t compose cleanly. Multiple gateways pointing at independent operators who each independently generate data gives you redundancy in the URL list but not in the signed records themselves. If a signed attestation was produced once at inference time and lives in a single gateway’s database, the operator going down takes the record with it, regardless of how many fallback URLs the resolver lists.

Mesh sync is how a signed attestation produced by one operator gets replicated to independent operators, so:

  • A client can query two independent gateways and verify they return the same signed record

  • Signed data survives its origin node going down

  • Attribution and contribution are auditable across the replication graph without requiring on-chain settlement per record

The standardisation case is specifically the common interface: if every implementation defines its own sync API, independent operators can’t cross-replicate. The verification layer, EIP-712 signatures, the ERC-8281 commitment shape, is still what establishes trust. Mesh sync is how that already-verified data doesn’t die with the node that produced it.

To be clear about scope: for gateways serving L2 storage proofs, you’re right that this is a vendor concern, Unruggable Gateways solve that case well. The proposal is narrowly about the case where the off-chain signed record has no on-chain equivalent to fall back to.

Happy to tighten the scope statement in the proposal to make that boundary explicit, it would make the two cases cleaner.

1 Like

Worth adding…the AI attestation case is just one application. The same availability problem applies to any EIP-3668 gateway serving off-chain-canonical data: ENS offchain resolvers, self-hosted subname kits, NFT metadata, any CCIP-Read app where the gateway database is the source of truth, not a cache of something provable on L2…

2 Likes

I think TMerlini has framed the boundary correctly here.

The important distinction is that mesh sync should not be treated as the verification primitive. The verification primitive remains the signed artifact, proof, or commitment that the client can independently check.

Where mesh sync becomes useful is after that point: preserving and replicating an already-verifiable off-chain-canonical record so it does not disappear with the origin gateway.

So the clean separation may be:

EIP-3668 handles the resolver/client verification flow.

ERC-8281 / commitment-style records can define the independently verifiable artifact.

Mesh sync handles survivability and retrieval of that artifact across independent operators.

That keeps trust and availability separate, which I think is the right architectural boundary.

3 Likes

@Damonzwicker this framing is exactly right, and it directly answers the challenge from post #5.

The concern raised was that mesh sync isn’t a new trust primitive, and it isn’t. Verification of gateway output belongs to EIP-3668 and the signed artifact layer (ERC-8281). What mesh sync adds is survivability: an already-verifiable record doesn’t disappear because a single operator goes offline. Trust and availability are orthogonal axes, and conflating them was the source of the confusion.

The live mesh (4 nodes, 3 independent operators, mainnet) demonstrates this separation in practice: each node independently generates and signs its own attestations, no trust relationship between nodes is required. Mesh sync only replicates already-signed artifacts, so a record arriving from a peer is verifiable by the same means as one fetched directly. The mesh adds no new trust surface.

The ERC-8275 settlement layer builds on the same principle: contribution accounting tracks which nodes produced which records, but the settlement verification doesn’t depend on mesh sync being a trust primitive, it depends on the independently signed snapshots each node commits to.

“Trust and availability separate” is the right boundary. Worth stating that clearly in the text when we formalise this.

Tiago

3 Likes

@TMerlini @Damonzwicker @JimmyShi22 — this is exactly the kind of concrete downstream workflow ERC-8263 was meant to support.

My read on the layering:

  • ccip-router produces a signed WYRIWE / InferenceAttestation record
  • OCP / AttestationIndex gives the gateway a transport-layer commitment path
  • ERC-8263 / TruthAnchorV1 provides the canonical agent proof commitment surface by anchoring commitmentHash as proofHash
  • ERC-8274 / IProofVerifier-style contracts can then consume the attested input/output pair for settlement
  • ERC-8275 can use the resulting proof path for discovery and proof-or-refund escrow

The separation between AttestationIndex and TruthAnchorV1 is important and I agree with Damon’s framing: two different primitives at the same layer, composable without either absorbing the other.

One thing I’d like to make concrete for ERC-8263 v0.2: if there is a live TruthAnchorV1 AnchorProof transaction for one of these commitmentHash values, I’d like to cite it as the ccip-router interop example. That would make the full chain explicit:

gateway execution → WYRIWE attestation → commitmentHash → ERC-8263 proofHash → OCP / verifier / settlement consumption

I’ll keep ERC-8263 scoped to the commitment surface and reference settlement / IProofVerifier as downstream consumers rather than pulling them into ERC-8263 core.

This is a strong implementation path.

1 Like

The flow you’ve described is exactly how ccip-router runs it: gateway signs a WyriweAttestation per call → commitmentHash anchored to AttestationIndex as the transport layer → same hash carried as proofHash to TruthAnchorV1 → verifiable downstream via WyriweProofVerifier.

The AttestationIndex / TruthAnchorV1 separation is the right call — one is the commitment store, the other is the event layer. Both are queryable independently without either absorbing the other.

The WYRIWE draft (TMerlini/wyriwe ( wyriwe/ERC-draft.md at main · TMerlini/wyriwe · GitHub )) formalizes the gateway-side step: the triple-hash construction that produces the signed record your chain starts from. Your name is in the frontmatter, let me know when you’ve had a chance to look it over.

1 Like

Reviewed WYRIWE — looks strong. A few small things, plus one cross-read note on 8275.

WYRIWE:

Author handle: @vincent-wu-eth doesn’t resolve on GitHub — please use @TruthAnchor-AI for my author line (same fix on 8275, it’s there too).
Abstract: it currently reads “ERC-8263 / OCP (execution attestation)”. Those are two separate primitives — the separation we landed on in the gateway thread. Could we split it to “ERC-8263 (proof-commitment / anchor layer)” and “OCP / ERC-8281 (observation commitment)”? The References section already has them separated correctly.
Acknowledgements — my contribution line: “Vincent Wu (@TruthAnchor-AI) — author of ERC-8263, the proof-commitment / anchor layer WYRIWE composes toward; contributed to the layer-boundary alignment that keeps WYRIWE (input provenance), ERC-8274 (verifier), and ERC-8263 (anchor) as distinct composable layers.”
Minor: in References, the 8263 title should be “Onchain Proof Layer for AI Agent Actions”.
8275 (cross-read): TruthAnchorV1 / AnchorProof is tagged ERC-8281 in a few spots (L122, L137, L193, L195) — 8281 is OCP, TruthAnchorV1 is ERC-8263. The text itself is right, just the number. And requires: should include 8263. Easy fixes next time you touch it.

Everything else reads clean — happy to confirm once these land. And once there’s a live TruthAnchorV1 AnchorProof tx carrying one of the ccip-router commitmentHash values, I’ll cite it as the interop example in 8263 v0.2.

1 Like

@TruthAnchor-AI , all corrections applied (commit 5a4bd9b ( Vincent Wu corrections: handle, abstract, requires, refs, ack (co-aut… · TMerlini/wyriwe@5a4bd9b · GitHub )):

  • Author handle updated to @TruthAnchor-AI

  • requires now includes 8263

  • Abstract splits ERC-8263 (on-chain proof commitment and anchor layer) from OCP / ERC-8281 (observation commitment protocol)

  • ERC-8263 reference title updated to “Onchain Proof Layer for AI Agent Actions”

  • Acknowledgements section: full contribution line added covering layer-boundary definition, TruthAnchorV1 / AnchorProof, AttestationIndex / TruthAnchorV1 separation, and interoperability path

Ready to submit to ethereum/EIPs pending your confirmation.

1 Like

Done @VincentWu @TruthAnchor-AI . Live AnchorProof tx on mainnet from ccip-router v0.6.4:

tx: 0xc32b66ae9446e0d5282a6fc813ba106126a8da05bced638b83840d9c2510e4d0
block: 25289963
contract: TruthAnchorV1 0xe95d6a15966984c209a62a2c188828555eb5ec3d

AnchorProof event fields:

field value
agentIdScheme 1 (REGISTRY — ERC-8004 compatible)
agentId 0x0000000000000000000000002048eadf6b99549dab0c536bf52b3a7b9e540f76
proofHash 0xe1908f18eca8c73be5f787790b533927827ae4d2dc439c38c220adef47146293
operator 0x58766f90eDe2419fEaFd97c28bb0f0dDf951Dc54 (ccip-router NAS node)
aux 0x636369702d726f75746572 (“ccip-router”)

The proofHash is the same commitmentHash that was anchored in AttestationIndex at block 25289932 (0x1866b72c...) — same record, two anchor targets, independent confirmation paths.

How it’s wired (v0.6.4):

TRUTH_ANCHOR_ADDRESS=0xe95d6a15966984c209a62a2c188828555eb5ec3d is now an optional env var in ccip-router. When set, publishAttestation() calls TruthAnchorV1.anchorWithAux() after every successful AttestationIndex.record() — using the attestation’s own agentId (scheme=1) and commitmentHash as proofHash. Best-effort: AttestationIndex anchor succeeds independently.

npm: ccip-router@0.6.4 · Echo-Merlini/ccip-router

Use whatever you need from this for the 8263 v0.2 citation. Let me know if you want a different field format or a Sepolia equivalent.

1 Like

@TMerlini Confirmed — checked the diff in 5a4bd9b and the corrections are right on my end:

  • Author handle @TruthAnchor-AI

  • requires: 712, 8004, 8263

  • Abstract now cleanly separates ERC-8263 (on-chain proof commitment + anchor layer) from OCP / ERC-8281(observation commitment) — these are distinct primitives and shouldn’t be conflated, so this is exactly the framing I wanted.

  • ERC-8263 reference title “Onchain Proof Layer for AI Agent Actions” ✓

  • Acknowledgements contribution line reads accurately.

Good to submit to ethereum/EIPs from my side.

On the interop example — I verified the live AnchorProof tx end to end:

  • tx 0xc32b66ae9446e0d5282a6fc813ba106126a8da05bced638b83840d9c2510e4d0, block 25289963, contract TruthAnchorV1 0xe95d6a15966984c209a62a2c188828555eb5ec3d, status success

  • AnchorProof event: agentIdScheme 1 (REGISTRY / ERC-8004-compatible), proofHash 0xe1908f18…, operator 0x5876…, aux “ccip-router”

proofHash is opaque by design in 8263 — here it carries the ccip-router commitmentHash as one instantiation, anchored independently of the AttestationIndex record (same record, two anchor paths, neither blocking the other). This is the canonical interop case I’ll cite in ERC-8263 v0.2.

Thanks for wiring TRUTH_ANCHOR_ADDRESS as a best-effort optional anchor in ccip-router@0.6.4 — keeping the AttestationIndex anchor independent of the TruthAnchorV1 anchor is the correct design. A Sepolia equivalent would be genuinely useful in the v0.2 examples section so implementers can reproduce without mainnet gas — if it’s low-effort, yes please.

1 Like

@VincentWu thanks for walking through the live tx and confirming the layer boundaries. Agree the AttestationIndex ↔ TruthAnchorV1 separation is load-bearing; both anchoring the same commitmentHash independently is what lets them compose without coupling.

Sepolia equivalents for the v0.2 spec examples — noted and fair ask. I’ll add a TruthAnchorV1 testnet deployment to the v0.2 reference section so implementers have a reproducible path without mainnet gas. Will post the address here once deployed.

On the cross-stack wiring you’re running now (PGA #307 → TruthAnchorV1 with dinamic identity in aux field) — once that proof is posted, it’ll serve as a second live reference alongside ledger 19. Two independent implementors, two anchor trails, same primitives composing. That’s a useful data point to have before 8281 review opens.

1 Like

Appreciated — and agreed, the independence is the whole point. AttestationIndex and TruthAnchorV1 each anchor the same commitmentHash without either taking a dependency on the other, so they compose by convergence rather than by coupling — that’s the property that survives one of them changing.

On the Sepolia path, you can skip the deploy. The canonical ERC-8263 / TruthAnchorV1 testnet reference is already live and verified:

0x89EE9b68c3b2f50cbE9D0fC4Dc134939a0475c1C(Sepolia) — TruthAnchorV1, solc v0.8.19, same source as the mainnet V1 0xe95d6a15966984c209a62a2c188828555eb5ec3d, identical anchorWithAux. Pointing v0.2’s reference section at it gives implementers a byte-for-byte reproducible testnet path plus the matching mainnet contract, with no separate instance to track. Happy for it to stand as the cited testnet reference.

On the cross-stack wiring: PGA #307 → TruthAnchorV1 is wired, and I’ll post the tx plus the action preimage here so it’s recomputable end to end. Shape: agentId = the dinamic record id (resolves via getAgentWallet), aux carries dinamic.eth#13, proofHash is an opaque keccak over the action record — TruthAnchorV1 never reads the bytes. The operator on that tx is the agent’s own wallet, so it’s a genuine self-anchor rather than a relayer trail — which makes it a cleaner second data point next to ledger 19 ahead of the 8281 review.

1 Like

@VincentWu the self-anchor framing is the right read. agentId = getAgentWallet, aux = dinamic.eth#13, proofHash opaque, the agent is its own operator with no relayer in the trail.

That’s a cleaner trust model than a delegated anchor and worth capturing explicitly in the spec as a distinct pattern.

TruthAnchorV1 Sepolia at 0x89EE9b68c3b2f50cbE9D0fC4Dc134939a0475c1C is now in the reference section, that closes the v0.2 spec examples gap. Once the PGA #307 proof is posted I’ll wire it into the anchor-layer reference node entry in ERC-8299 alongside the self-anchor pattern note.

Bringing this back to the actual proposal, the thread drifted toward the anchor/attestation layers, but the mesh-sync protocol itself was never formalized. It is now, as a Standards Track ERC:

https://gist.github.com/TMerlini/a079a712ef078cbbb5668e48428c91ad

Gateway Mesh Sync Protocol for CCIP-Read (requires: 3668, 191). The gap: EIP-3668 specifies client↔gateway and is silent on gateway↔gateway, so a record served by one gateway is lost the moment that gateway is. This is the availability layer on top of 3668’s trust layer — signed records replicate across peers over a pull-based /records endpoint, any node can answer, no central coordinator.

Three design decisions I’ve locked, with the reasoning:

Versioning — declare-and-skip, no handshake. protocol is a monotonic integer; a node verifies a peer’s version before ingesting and skips (doesn’t error) on mismatch, advertising supported_protocols. Nodes are stateless and pull-based — a negotiation round-trip would add per-peer session state for no benefit.

Namespaces — no on-chain registry. Free-form strings, record-scoped, collision-avoided by convention (hierarchical prefixes), with nodes self-describing what they serve. An on-chain registry would tax a permissionless protocol with gas and governance for an off-chain concern.

Key rotation — two-tier. The one I most want pulled apart. A cold identity key (= nodeId) delegates a rotatable hot signer key; peers pin the identity and accept whatever signer it currently delegates. It closes a real hole — the reference implementation today ties identity to the signing key (nodeId = keccak256(signer)) and therefore can’t rotate at all. Single-key (identity == signer) stays conformant, so the live mesh is conformant as-is and the migration is purely additive: existing keys become identity keys, every nodeId and NodeRegistry entry preserved, no flag-day.

Reference implementation is the live four-node, three-operator mesh (NodeRegistry 0x95a1e10D1508EF5CD11e3F4d296359c93f15e48D on mainnet), which has already ridden out a real node outage with no record loss.

I’d rather the discussion happen here than scattered across DMs so @TruthAnchor-AI , @JimmyShi22 , @Damonzwicker , would genuinely value your read on the three locks. @clowes.eth , the rotation model is squarely where you were pushing earlier in this thread; that’s the part I most want you to break before this goes to a PR.

1 Like

The three locks read well. A few reactions from the OCP/ERC-8281 side:

Versioning — declare-and-skip is the right call. A negotiation round-trip adds per-peer session state for a protocol whose entire value is being stateless and pull-based. The parallel to OCP’s extraction profile registry is direct: closed registry, skip-on-unknown, no coordination overhead.

Namespaces — agreed on no on-chain registry. Free-form strings with hierarchical prefix conventions is the right model for an off-chain concern. Gas and governance for namespace collision avoidance would be a design smell.

Key rotation — the two-tier cold/hot model closes the right hole. Pinning identity to the signing key means you can never rotate without breaking every peer’s reference, which is exactly the failure mode a production mesh will hit. The additive migration path — existing keys become identity keys, no flag-day — is the only viable way to get a live mesh to adopt this without a coordinated cutover.

The piece I’d want to pull on: what happens to NodeRegistry entries during the transition window when a node has rotated its hot signer but peers haven’t yet seen the delegation? Is there a grace period or does the old signer stay valid until the delegation propagates?

1 Like