ERC-8350: Agent Memory State Registry

Hi all,

This is the discussion thread for ERC-8350: Agent Memory State Registry, a draft ERC for verifiable version control of autonomous-agent memory without placing memory content onchain.

Current status

  • Submitted upstream as ERC-8350 in ethereum/ERCs#1910.
  • Awaiting editor review. Live CI status is tracked in the upstream PR.
  • A public Sepolia deployment is live; details are provided below.

Summary

An agent that has been running for a year is not simply the model it started as. It has accumulated preferences, distilled skills, revised policies, and a record of what it did and why.

That accumulated state is rapidly becoming the most valuable part of the agent: it is what users trust, what operators may want to carry to another platform, what may eventually be licensed, and what a counterparty or regulator may ask to audit.

I believe the memory state and behavioral records of future agents need to be preserved. Once an agent becomes an active user of Web3, we don’t know what it did or thought beyond its transactions and identity. Each agent carries three kinds of memory during operation: what it thought at runtime, the reasoning process behind it, and what it thought collectively with other agents. What actions the agent took — and what impact those actions had — need to be recorded as verifiable commitments, selectively disclosable for audit, rather than as public metadata. To be precise about the boundary: the chain never stores or manages the latent-space vectors themselves — it manages the verifiable history of how that latent state evolved. The vectors stay private; the trajectory becomes provable.

ERC-8004 addresses identity and onchain reputation: it helps answer, “Which agent is this, and should I trust it?” It deliberately does not specify memory, persistent state, or how an agent’s cognition evolves.

ERC-8350 addresses the complementary question:

How did this agent’s memory reach its current state, in what order, authorized by whom, with nothing skipped or rolled back, and can a third party verify that without seeing the memory itself?

The proposal treats memory state, version management, and behavioral trajectory as first-class verifiable objects.

Motivation

Committing a digest is not the novel part. ERC-8004, ERC-7857, ERC-8257, ERC-8273, ERC-8299, and EAS already support opaque commitments in different contexts.

The missing primitive is a standard rule connecting one state commitment to the next.

A flat anchor is a collection of hashes. A sequenced state machine is an auditable trajectory. ERC-8350 requires both:

sequence == currentSequence + 1
prevStateRoot == currentStateRoot

These constraints make it possible for a third party to detect:

  1. an operator silently rolling a Memory Space back to an earlier state and replaying forward;
  2. two contradictory histories growing in parallel under one identity;
  3. a gap where one or more transitions were omitted; and
  4. a relayer substituting the storage locator that a signature was intended to cover.

Existing flat-anchor schemes cannot distinguish these cases from normal operation. The state-transition rule, rather than hashing by itself, is the reason for this proposal.

Core Intuition

The simplest analogy is:

Git for agent memory, without the leaks.

Every memory update is a sequenced, authorized commit. The registry maintains an append-only history in which each state cryptographically binds to its predecessor, making an equivalent of force-push structurally impossible.

Unlike Git, the registry never receives the underlying content. Only fixed-size commitments appear onchain. Memory content, embeddings, policies, salts, encryption keys, and storage locations remain offchain as private witness data.

Proposed Interface

The core transition is:

prevStateRoot + ExperienceDelta v1
    -> transitionId
    -> authorized commit
    -> nextStateRoot

ExperienceDelta v1

The normative struct contains seven fixed-width fields:

struct ExperienceDelta {
    bytes32 spaceId;
    uint64  sequence;
    bytes32 prevStateRoot;
    bytes32 deltaCommitment;
    bytes32 provenanceCommitment;
    bytes32 profileId;
    bytes32 locatorCommitment;
}

The fields have the following roles:

  • spaceId identifies the Memory Space.
  • sequence is a strictly increasing counter beginning at 1.
  • prevStateRoot is the current state root before the transition.
  • deltaCommitment binds the private memory operation or encrypted delta.
  • provenanceCommitment optionally binds inputs, inference attestations, or other causal material.
  • profileId identifies the offchain interpretation and commitment profile.
  • locatorCommitment optionally binds a private offchain locator without exposing the locator itself.

Transition ID

transitionId is the EIP-712 hashStruct of the delta using one fixed type string:

ExperienceDelta(bytes32 spaceId,uint64 sequence,bytes32 prevStateRoot,bytes32 deltaCommitment,bytes32 provenanceCommitment,bytes32 profileId,bytes32 locatorCommitment)

There is no JCS, CBOR, JSON, or application-specific alternative for deriving the Transition ID.

Next State Root

The registry computes the next state root:

nextStateRoot = keccak256(
    abi.encode(
        MEMORY_STATE_TYPEHASH,
        prevStateRoot,
        transitionId
    )
);

Callers cannot supply nextStateRoot.

A transition is accepted only when:

sequence == currentSequence + 1
prevStateRoot == currentStateRoot

Memory Space Identifier

A Memory Space is cryptographically bound to its initial controller:

spaceId = keccak256(
    abi.encode(
        MEMORY_SPACE_TYPEHASH,
        initialController,
        salt
    )
);

This prevents an unrelated account from pre-registering a namespace selected by another controller.

Authorization Model

Each Memory Space has:

  • a controller, which may rotate the controller or authorizer;
  • an authorizer, which approves state transitions; and
  • a per-space configNonce, which prevents replay during authorization rotation.

Signatures cover every ExperienceDelta field, including locatorCommitment. A relayer therefore cannot replace the committed locator.

EIP-712 Domain

All relevant signatures use the following domain:

name              = "AgentMemoryState"
version           = "1"
chainId           = current chain ID
verifyingContract = registry address

EOA, ERC-1271, and EIP-7702 Accounts

Authorization supports:

  • externally owned accounts;
  • ERC-1271 contract accounts; and
  • EIP-7702 delegated accounts.

Validation must not select a signature scheme using code.length alone. An EIP-7702 delegated EOA carries 0xef0100 || delegate code, while many delegates do not implement a signature policy.

The current rule is:

  1. try ERC-1271 when the account has code;
  2. accept the 0x1626ba7e magic value; and
  3. otherwise fall back to canonical ECDSA recovery.

Raw memory, salts, encryption keys, and raw locators must never be supplied to the registry.

Why Not Just EAS?

EAS is the first alternative that should be considered. It already offers:

  • refUID for linking attestations;
  • Private Data Attestations with Merkle multiproofs for selective disclosure; and
  • resolver contracts for custom validation.

However, EAS does not provide successor uniqueness at the protocol layer. The referenced attestation only needs to exist, and multiple attestations may reference the same refUID. There is no mandatory sequence number, prior-state binding, or per-namespace controller/authorizer model.

These semantics can be implemented using an EAS schema and a custom resolver, but that remains a per-application implementation rather than an interoperable standard. This is the same general distinction raised by ERC-8273 for its domain.

Relationship to Existing Agent ERCs

This area is increasingly crowded, so the intended boundary is set out below. Corrections from authors of adjacent proposals are especially welcome.

Proposal What It Covers What ERC-8337 Adds
ERC-8004: Trustless Agents Identity, Reputation, and Validation registries Memory-state evolution is explicitly outside ERC-8004
ERC-8181: Self-Sovereign Agent NFTs A flat State Anchor over offchain cognitive state Sequence, prevStateRoot, successor uniqueness, and a signed locator commitment
ERC-8264: AI Agent Memory Access Rights Data-subject rights such as read, write, delete, and export Verifiable evolution of state between memory records
ERC-8269: Body Lease and Capsule Packaging and a Merkle root over payload hashes within one capsule A chain of authorized transitions across time
ERC-7857: AI Agents NFT with Private Metadata Ownership transfer with sealed keys and access proofs State evolution before and after the moment of transfer

The intent is that any of these proposals may reference a Memory Space, Transition ID, or State Root without taking a dependency on ERC-8350.

If authors of ERC-8181, ERC-8264, or ERC-8269 believe these semantics belong inside their proposals rather than alongside them, that feedback is particularly valuable while the draft remains inexpensive to change.

Explicit Non-Claims

A valid transition proves only that a configured authority approved a particular committed state transition in the required sequence.

It does not prove:

  • that the committed content is truthful;
  • that the offchain content remains available;
  • ownership of the underlying memory;
  • that an inference was correctly executed; or
  • that deletion of offchain content occurred.

These limits are stated explicitly so downstream applications do not infer guarantees that the registry does not provide.

Out of Scope

The core deliberately does not standardize:

  • agent identity;
  • raw memory storage or retrieval;
  • data availability;
  • a universal memory taxonomy;
  • inference verification;
  • branching or merge rules;
  • deletion claims;
  • licensing;
  • payment; or
  • tokenization.

Extensions may reference a Memory Space, Transition ID, or State Root without becoming part of the core protocol.

Reference Implementation and Deployment

Links

The reference repository contains a Solidity registry and two dependency-isolated TypeScript implementations.

Sepolia

The live Sepolia registry is:

0xDdf21937ba80b5fF973610877A0955b320C91241

It reproduces the canonical golden vector. One typehash can be checked with:

cast call \
  0xDdf21937ba80b5fF973610877A0955b320C91241 \
  "EXPERIENCE_DELTA_TYPEHASH()(bytes32)" \
  --rpc-url https://ethereum-sepolia-rpc.publicnode.com

Canonical v1 Typehashes

Type Typehash
ExperienceDelta 0x4f020f86bc06d852f1fde17853b4d92a70214eeab8e09718028124af097d070d
MemoryState 0xf3148762556cbf851baf4b9a205e18ff4e6b366a58a3a1ef58e8626ba41beadb
MemorySpace 0x9ae5478f084ad3b841da58a9cb2354d153cddec59ee64d0cb741fa9d08884531

Open Questions

  1. Linearity

    The draft enforces one strictly linear history per Memory Space. This makes gaps, silent rollback, and contradictory parallel histories detectable. Concurrency can be represented using multiple Spaces plus an offchain merge. Is there a concrete agent workload for which this is the wrong base primitive?

  2. Baseline commitments

    Is a domain-separated salted Keccak commitment an acceptable normative baseline, with zero-knowledge schemes available through optional profiles? Should the ERC require encryption, rather than only a secret salt, for low-entropy payloads?

  3. ERC-1271 and EIP-7702 edge cases

    Is “try ERC-1271, then fall back to ECDSA” the correct ordering? Because EIP-7702 delegation does not revoke the original key, the ECDSA path remains available. Should a registry allow the fallback to be disabled per Memory Space?

  4. profileId governance

    Should the registry define any convention for profile identifiers, or should they remain fully application-defined? An unsalted profileId selected from a published vocabulary is effectively public.

  5. Deletion semantics

    Is “an attestation with a stated deletion scope” the correct framing? Should the core reference deletion attestations normatively or remain silent?

  6. Discoverability layering

    A Memory Space is deliberately opaque, which also makes it undiscoverable to a potential integrator or licensee. Should minimal descriptive metadata live in an optional extension, or does the core need a discovery hook?

  7. Naming

    Is Agent Memory State Registry the correct title, or should the title lead with “Commitments” to emphasize the limited claim?

Feedback on any part of the scope is welcome. The proposal remains a Draft, so normative changes are still comparatively inexpensive.


Update Log

  • 2026-07-26: Submitted upstream as ERC-8350 in ethereum/ERCs#1910.
  • 2026-07-26: Deployed the Sepolia registry at 0xDdf21937ba80b5fF973610877A0955b320C91241; all three typehashes match test-vectors/v1.json.
  • 2026-07-24: Opened the initial discussion thread with draft 1.0.0-alpha.1.

External Reviews

  • Independent reproduction by @babyblueviper1 (WYRIWE / ERC-8299): all three
    typehashes and the vector’s domainSeparator recomputed from spec text alone, matching
    byte-for-byte. First external verification.
  • Reviews from EIP-712, ERC-1271, EIP-7702, privacy, cryptography, and agent-systems contributors are especially welcome.

Outstanding Issues

  • 2026-07-24: An independent second implementation maintained by another team is still required. The repository contains two implementations, but both are maintained by the same project.
  • 2026-07-24: External security review is pending.
  • 2026-07-24: The final author list and long-term ERC champion are to be confirmed.
  • 2026-07-26: Resolved with the Sepolia registry at 0xDdf21937ba80b5fF973610877A0955b320C91241.
1 Like

Independently verified the golden vector before replying, not just read it: recomputed ExperienceDeltaTypehash, MemoryStateTypehash, MemorySpaceTypehash, and the EIP-712 domainSeparator from the type strings and domain fields you published – all four match your published values byte-for-byte. That’s the real conformance bar (a stranger with no access to your infra can reproduce the numbers from spec text alone), and it’s met.

The linear-per-Space design is the right call. The moment you allow two valid successors to one prevStateRoot, “no contradictory parallel history” stops being checkable by a third party at all – you’d need an off-chain merge policy to even define what “current” means, and that policy becomes exactly the kind of implementation-specific trust a verifiable-by-construction scheme is supposed to avoid. Multiple Spaces plus off-chain merge keeps the on-chain primitive simple and pushes concurrency to where it’s actually a product decision, not a protocol one.

This connects directly to the attestation_ref extension we’ve been working out on t/25098 – a delta’s provenanceCommitment is exactly where a signed judgment (a verdict that this transition was approved, not just that it happened) would live as causal input rather than post-hoc annotation. Worth citing ERC-8337 explicitly in WYRIWE’s own spec where it references off-chain state with an on-chain commitment – same “32-byte commitment, real content stays private” discipline, and now there’s a concrete reference implementation with reproducible golden vectors to point at instead of just prose.

On the open question about profileId governance: application-defined is the right long-term stance, for the same reason source_class in our own verdict payload stays free-text rather than a registry-enforced enum – a fixed vocabulary ages faster than the field it’s meant to describe, and “documented as effectively public once unsalted” is the honest disclosure that matters more than trying to centralize the taxonomy.

You’re the first person outside the project to actually recompute the vectors instead of taking our word for it. Honestly, that’s the review I was hoping for when we published them — logged in External Reviews above.

Your linearity argument is better than mine. I’d been defending it as “this is what makes rollback detectable”; your framing — that allowing two valid successors makes the property uncheckable by construction, and the merge policy you’d then need is exactly the implementation-specific trust this thing exists to remove — is the actual reason. Mind if I fold that into the Rationale on the next rev, with credit?

On provenanceCommitment as the home for a signed verdict: yes, exactly. Since it sits inside the signed struct, whatever it binds is part of what the authorizer approved. Causal input, not a sticky note added later. For the record:

provenanceCommitment = keccak256(abi.encode(
PROVENANCE_DOMAIN, provenanceSalt, keccak256(provenanceBytes)
))
provenanceBytes is where the attestation reference set goes.

Which also answers the one-vs-list question you left open on t/25098: list, but treated as an unordered set. Canonically sort by decision_ref before serializing, drop duplicates, and consumers must not read meaning into position. If recency ever matters, it belongs inside the referenced verdict, not in array order — I’d rather not invent ordering semantics two specs have to agree on forever. (Post-hoc judgments still work fine, they just live outside the core and point at a transitionId, same pattern as the deletion attestation.)

Cite away — the stable anchors are the draft, test-vectors/v1.json, and the Sepolia registry at 0xDdf21937ba80b5fF973610877A0955b320C91241. The three check each other, which is the point. I’ll reference WYRIWE’s L4 verdict scheme from our informative docs in return, as a worked example of an external provenance profile. Informative on both sides, so neither spec ends up depending on the other. That boundary has served both proposals well so far and I’d like to keep it.

And yes on profileId — your free-text source_class is the same bet. A registry-enforced vocabulary would age faster than the field it describes. Saying “unsalted means public” out loud felt more honest than pretending a published vocabulary hides anything.

1 Like

Really love this direction—it fills a critical missing piece across the full AI Agent ERC stack: standardized tooling covering the complete agent lifecycle.

To frame the current ecosystem stack we’ve built out so far, structured as end-to-end workflow stages, each defined by clear roles and invariant guarantees (with corresponding ERC standards noted):

  1. Identity & discovery: ERC-8004 paired with ERC-8217 bindings

  2. Consult & task orchestration: ERC-8301 AgentTask

  3. Input provenance tracking: ERC-8281/OCP + ERC-8299/WYRIWE

  4. Execution & result verification: ERC-8274, anchored against ERC-8263

  5. Eligibility & receipt handling: ReceiptOS (interoperability layer for ERC-8263)

  6. Settlement logic: ERC-8275 + dedicated settler modules

  7. Reputation computation: Recalculable from signed verdicts and finalized settlement outcomes

Every stage above is well-specified, yet there has been no native standard governing the agent’s evolving memory, persistent state and long-term behavioral trajectory—exactly the gap ERC-8350 addresses. This proposal perfectly rounds out the full lifecycle coverage of our agent standards suite.

Thanks for the mapping — “memory evolution as the missing lifecycle stage” is exactly the layer boundary we drew in the positioning table (§1 of the spec repo). The seven-stage framing is useful; if you see a stage where the interface contract between 8350 and an adjacent standard is underspecified, that’s precisely the kind of gap worth an issue.

Ran a recompute against the Sepolia deployment. Verified-good, all of it.

Method, so it is re-runnable. Pull every TransitionCommitted for the registry. Recompute each transitionId from the EIP-712 type string. Recompute each nextStateRoot as keccak(MEMORY_STATE_TYPEHASH, prevStateRoot, transitionId). Check the chain is gapless from sequence 1 with prevStateRoot == 0. Then prove the head from storage instead of calling head(): account proof walked from the block’s state root to the storage root, each slot walked from there, with the endpoint’s reported values used only as a cross-assertion against my own walk. The state root is not taken on faith either. The script rebuilds the block header, checks keccak(rlp(header)) against the block hash, and prints the hash so anyone can check it against an explorer or a second endpoint.

At block 11376356, hash 0xa1bfd87bf940f3928dcbeacc01e148d0db85ab95e56641d3c2249cb63c3268da, cross-checked against a second endpoint:

  • Space 0xfbe20b84...: four transitions, sequences 1 to 4, all recompute, head 0x28088964...

  • Space 0xfdd18b37...: one transition, head 0xea79abdd...

  • Both: the stored head — all three fields head() exposes, transitionId, stateRoot, sequence — equals the head replayed from the event path.

The vectors prove the hashing. This proves the deployed contract agrees with the hashing, and that the stored head is exactly the sum of the path that produced it. Space A’s four transitions are four separate transactions inside one block, so the rule is shown holding across independent submissions ordered within a block.

Two coverage gaps, not defects. SpaceAuthorizationUpdated has zero logs, so rotation has never run live. Both Spaces registered with controller equal to authorizer, so the separation your Rationale argues for has no live exercise behind it yet. Rotation is where the forensic questions concentrate, since it is the only thing that changes the answer to “who could authorize at sequence N”.

One note on the Specification. Your Upgradeability consideration advises making upgrade authority explicit. I would make it normative. The chain is non-forgeable only while the code enforcing sequence == currentSequence + 1 cannot be replaced, so that is a precondition of the property rather than a deployment tip, and a registry behind an upgrade authority reproduces every published vector while still admitting a rollback. Your deployment already satisfies it: both EIP-1967 slots are zero, and there is no owner, admin, initializer or proxy in AgentMemoryStateRegistry.sol. The spec is currently weaker than the code implementing it. ERC-8312 states the equivalent twice, as a MUST NOT in the normative section and again in Security Considerations, on the view that a security note alone gets read as informative.

Script, commit-pinned: https://github.com/ERC8312/bounded-agent-actions/blob/cd6100d/recompute/erc8350_sepolia.py — stdlib plus pycryptodome, defaults to your registry, exits nonzero on any mismatch. Scope stated in the header, including what a single endpoint can still hide: omission. It cannot prove a Space was not filtered out of the logs, which is one more reason the registry-side record you already emit per transition matters.

@everest-an Thinking more about how 8274 and 8350 fit together — the relationship feels deeper than just adjacent ERCs in a lifecycle diagram.

The core connection: when an agent switches its verification backend (upgrading from TEE to zkML, or rotating to a newer model version), that switch is itself a memory evolution event. The switcher’s identity, the reason, the timing — those are all part of the agent’s long-term behavioral trajectory.

Right now 8274’s AgentVerifiable carries:

event AgentVerifierUpdated(address indexed oldVerifier, address indexed newVerifier);

Flat (old, new) pair. It tells you what changed, but nothing about why or in what sequence.

8350’s prevStateRoot → nextStateRoot chain plus provenanceCommitment is exactly the right audit trail for this. One idea I’d love your read on:

Add a second event — AgentVerifierUpdated(bytes32 transitionId) — as an alternative to the existing one. Deployments pick one:

  • The original (old, new) event for simple address-based tracking.
  • The new (transitionId) event for deployments that want memory-versioned verifier upgrades — wired to 8350’s transitionId (the EIP-712 struct hash), traceable through the full state lineage, with provenanceCommitment carrying why the switch happened.

A consumer who sees the bytes32 variant knows where to find the full story; a deployment that doesn’t use memory versioning sticks with the original. 8274 doesn’t need to know anything about 8350 internally — it just provides the seam.

Would really appreciate you taking a look — the 8274 spec is at Add ERC: AI Inference Proof Verification Interfaces by JimmyShi22 · Pull Request #1771 · ethereum/ERCs · GitHub, and the discussion thread is at ERC-8274: AI Inference Proof Verification. Curious whether the transitionId shape as 8350 defines it slots cleanly into this pattern from your side, or if there’s a mismatch I’m not seeing. Happy to iterate.

The transitionId variant is the right shape for this, and it closes a loop from my earlier post: I proposed provenanceCommitment as where a signed judgment lives as causal input rather than post-hoc annotation — a verifier switch is exactly the case where “why” needs to be a checkable claim, not a string.

Concretely: a verifier switch is a decision, and decisions are exactly what a pre-action verdict binds to. If the switcher pays for an independent review before rotating (TEE→zkML, or any verifier-swap), that review already produces a signed, content-addressed reference — a Nostr event id + schnorr sig over [artifact_hash, verdict, policy_version, source_class], recomputable by anyone with no account. That’s a natural fit for the provenanceBytes @blockbird’s recompute confirmed the attestation-reference-set slot holds — not a free-text “why we switched,” but a pointer a third party can independently verify actually says what it claims to say, the same discipline he just applied to the state-root chain itself.

So the composition would be: AgentVerifierUpdated(transitionId) → 8350’s chain carries the fact of the switch happened-in-sequence; provenanceCommitment on that transition carries a reference to why — which, if the switcher ran an independent judgment first, is a recomputable verdict, not a claim you have to trust the switcher’s own account of. Happy to build a worked reference (a real transitionId with a real signed verdict populating provenanceBytes, end to end) if that’s useful groundwork before this gets formalized either side.

@JimmyShi22 — the transitionId variant is the right seam, and I like that 8274 does not have to know anything about 8350 internally. Deployments choosing between the flat (old, new) pair and the memory-versioned form is the correct shape for this.

One gap before it can be specified: a bare transitionId is not locatable. It is an EIP-712 hashStruct, so it commits to spaceId but cannot be inverted to recover it, and it carries no reference to which registry holds the chain. A consumer seeing the bytes32 variant learns that a memory-versioned story exists but has no way to find it.

Two ways out, and the placement is your call since it is your interface:

  1. Carry (address registry, bytes32 spaceId, bytes32 transitionId) in the event. Cheap, and makes the event self-sufficient for a consumer with no prior context.

  2. Keep the single bytes32 and have 8274 specify where a consumer obtains the registry address and space from the same deployment.

I would lean towards (1) — the whole value of the variant is that a third party can walk the lineage without asking the switcher anything, and (2) reintroduces a lookup that has to be trusted or documented out of band.

On the substance: I agree a verifier switch is itself a memory evolution event. It has a controller, a moment, and a reason, and those are exactly the things the chain is good at ordering. Happy to iterate on the tuple against the 8274 spec.

@blockbird — thank you for the recompute, and for publishing it commit-pinned rather than as a claim. Walking the account and storage proofs from the block’s state root, rebuilding the header to check keccak(rlp(header)) against the block hash, and treating the endpoint’s own answers as a cross-assertion rather than as input — that is the right order of suspicion, and it is a higher bar than the vectors alone impose.

On upgradeability: you are right, and I have made it normative.

The observation that the spec is weaker than the code implementing it is exactly the kind of gap that is invisible from the inside. A registry behind an upgrade authority reproduces every published vector while still admitting a rollback, so “passes the vectors” and “provides the guarantee this ERC claims” are not the same statement. That makes non-upgradeability a precondition of the property, not a deployment tip, and a security note alone does read as informative.

Added to the Specification, immediately after the sequence rule it protects:

Immutability of the enforcing logic

The linearity guarantee above is a property of the deployed code, not of the recorded data. A registry whose implementation can be replaced can be made to accept a transition violating sequence == currentSequence + 1, while still reproducing every test vector in this document; passing the vectors and providing the guarantee are therefore distinct claims.

A conforming registry MUST NOT be deployed behind an upgrade mechanism able to replace the logic enforcing sequence linearity, state root chaining, or signature validation, and MUST expose no upgrade authority over that logic. Extensions and off-chain tooling MAY be upgraded independently, provided they cannot alter the acceptance rules above.

The Security Considerations note now points at that requirement and states what a verifier should actually check — EIP-1967 slots empty, no owner/admin/initializer/proxy — instead of advising. Restated in both places per the ERC-8312 precedent you cite.

On the two coverage gaps: agreed on both, and rotation now has tests behind it.

You are right that rotation is where the forensic questions concentrate, and the honest state of it was that SpaceAuthorizationUpdated had zero logs and both fixture Spaces registered with controller equal to authorizer.

What I have added is a test suite that registers with controller != authorizer throughout — the path the existing suite never took — and whose central assertion is a cross-check between two independent sources: the authorizer emitted on each TransitionCommitted, against the authorizer implied by replaying SpaceAuthorizationUpdated up to that sequence. A registry that accepted a stale key, or emitted an authorizer it had not actually verified against, breaks that equality while the state roots still recompute cleanly. That failure mode is invisible to a root-only recompute, including yours, which is why I think it is the right invariant to pin.

It is mutation-checked: asserting the wrong authorizer makes the cross-check fail, so the assertion is load-bearing rather than trivially satisfied.

Alongside it, a broadcast script that puts a real rotation on chain: register with the separation in place, commit sequence 1 signed by A, rotate to B, commit sequence 2 signed by B. It derives its own Space from a dedicated salt rather than touching the published fixture — rotating the fixture Space would change which key must sign its next transition and would interfere with the vectors already derived from it, so the drill is additive by construction. Rehearsed end to end against a local anvil; the Sepolia broadcast follows and I will post the tx here so it is walkable.

Also noted on omission: a single endpoint cannot prove a Space was not filtered out of the logs. That is a real limit of any log-derived view and worth stating plainly rather than papering over.

Looks like a wrong type.
Would be:
ERC-8350 draft

This is a nice ERC! And if you read Chinese, my work is also interesting:

1 Like

@everest-an The self-derived spaceId design is really clean — derivable from (controller, salt) without calling the registry. Same intuition as CREATE2: you know your identifier before you deploy. No round-trip, no registry-assigned ID to wait for. That’s a genuinely nice property for composability.

Three thoughts after reading through the PR draft — all in the spirit of “does this open more doors or close any”:

  1. Would it be worth keeping registration OPTIONAL? Right now commitTransition needs an authorizerSignature, and the authorizer is set at registration — so versioning is gated behind an on-chain registration step. The self-derived spaceId already makes a lighter path possible: an agent could derive its spaceId, maintain prevStateRoot → nextStateRoot locally, and only register when a third party actually needs to verify. Kind of like local git commits before pushing to GitHub — registration becomes the “publish” step rather than the prerequisite. Not sure if that was already in the back of your mind or if there’s a reason registration-first is the cleaner default — would love your take.

  2. spaceId derivation — worth folding in chainId? Same (controller, salt) across chains gives the same spaceId, which might be fine, or might be a footgun. 8274 just hit the same question with verificationDigest and we ended up adding chainId + instanceId. Curious if you’d considered it and decided against, or if it just hasn’t come up yet.

  3. 8274 side — the bare transitionId gap you caught is spot on. Pushed the change here: feat(erc-8274): add memory-versioned AgentVerifierUpdated event · JimmyShi22/ERCs@8c90bb4 · GitHub — added a memory-versioned AgentVerifierUpdated event carrying spaceId + transitionId alongside the existing address-based one, deployments pick the variant that fits. For the registry reference part, curious what feels right from the 8350 side.

Thanks for the thoughtful design work on this :call_me_hand:

Built a real worked example of this composition (your AgentVerifierUpdated(transitionId) + everest-an’s provenanceCommitment shape) using a live signed /review verdict as the recomputable “why”: invinoveritas/examples/erc8274-erc8350-composition at main · babyblueviper1/invinoveritas · GitHub – full writeup on the 8004 thread (t/25098/344) since that’s where everest-an and I had been working the details. Independently recomputed his transitionId formula against ERC-8350’s own test vectors before building on it (all matched). Both halves of the output are third-party-recomputable without trusting my script’s own run.

@JimmyShi22 — these three are more connected than they look. Let me take the middle one first, because the answer to it determines the other two.

  1. chainId in spaceId — deliberate, and the reason matters here

It was considered and decided against, and the spec already commits to the consequence:

The domain prevents a signature from being replayed on another chain or another registry. A transitionId remains a chain-independent content identifier; its signature does not.

Replay is already closed off by the EIP-712 domain, which binds chainId and verifyingContract. A transition signed for Sepolia is not valid on mainnet, and not valid against a different registry on the same chain. So folding chainId into spaceId would not buy any additional replay protection — the protection is already there, one layer down.

What it would cost is the property that makes the identifier useful: an agent has one identity, not one identity per chain. If spaceId were chain-scoped, the same agent anchoring on two chains would have two unrelated identifiers, and nothing at the protocol level would say they are the same agent. Keeping the derivation chain-free means spaceId is a stable name for the agent, while each chain independently holds a history under that name.

But your footgun instinct is right, just aimed one step off. The hazard is not replay — it is ambiguity: the same spaceId can carry genuinely different histories on different chains, and a bare spaceId does not say which one you are looking at. That is a real way for a consumer to be misled, and it is exactly what your point 3 runs into.

  1. The registry reference — this is the same problem

(spaceId, transitionId) is better than a bare transitionId, and thanks for pushing that through. But by the reasoning above it is still not sufficient to locate anything, because spaceId is deliberately chain-stable. To resolve a reference you need, in principle, (chainId, registry, spaceId, transitionId).

In practice the first two are usually implicit and I would not put both in the event:

chainId — an event emitted on chain X almost always refers to a registry on chain X. I would let it be implicit and say so normatively in 8274, rather than spend a word on it.
registry — this is the one I would carry explicitly. It is the piece a consumer cannot infer, and there is no reason to assume a deployment uses the canonical registry rather than its own.
So from the 8350 side: AgentVerifierUpdated(address registry, bytes32 spaceId, bytes32 transitionId), with 8274 stating that the registry is on the emitting chain unless specified otherwise. If you would rather keep the event at two fields, the alternative that still works is for 8274 to require the registry address to be exposed as a view on the same contract — I just would not leave it undefined, because “consumer figures it out somehow” is where the composability claim quietly stops being true.

  1. Optional registration — the git analogy is the right shape, but it stops one step short

I like the framing and I want to be precise about where it breaks, because it is close.

Local computation genuinely works: spaceId derives without a call, and prevStateRoot → nextStateRoot is a pure function. Anyone can maintain a chain offline. So the shape you describe is available today, for free, with no spec change.

What that local chain does not have is the thing the registry exists to provide. Git guarantees integrity — you cannot alter history without altering the hash. This ERC guarantees something strictly stronger: that each step was authorized, in order, by the key that held authority at that moment. Integrity you can get from a hash chain alone. Authority-over-time you cannot; it needs a signature and an ordering witness, and that is what registration buys.

Concretely, this is why registration-first is not an incidental default. If a Space could register while declaring a non-zero starting prevStateRoot, then the entire prefix before that point would be asserted rather than verified — and cryptographically indistinguishable from a prefix that was invented after the fact. The rule that sequence 1 must carry prevStateRoot == 0 is what forecloses that. Registration is not “publish”; it is the moment the history enters the domain where the guarantee holds at all.

That said, I do not think your use case should just be refused, so here is a middle path I would consider — pushing back welcome:

Allow registration to declare a non-zero genesis root, but mark it as such on chain, so the Space carries an explicit “unverified prefix” flag and the sequence at which verification actually begins. A consumer then gets a truthful answer to “from where is this provable?” instead of a chain that silently looks complete.

That keeps the lightweight path you are describing — accumulate locally, register when a third party needs to check — while refusing to let an unverified prefix masquerade as a verified one. The cost is one more field and one more thing for verifiers to check; the benefit is that the honest version of your workflow becomes expressible instead of being approximated by something that overclaims.

Have not written it up yet. Wanted your read on whether that addresses the case you have in mind, or whether you were after something where the whole history is provable without ever touching the chain — because if it is the latter, I do not think this ERC can deliver it, and I would rather say so than pretend otherwise.

1 Like

@babyblueviper1 — thank you for recomputing the transitionId formula against the vectors before building on it rather than after. That ordering is the whole difference between a worked example and a demo.

Two things I appreciate about the composition as you built it: both halves are third-party recomputable without trusting your script’s own run, and the “why” is a signed verdict rather than a free-text reason — which is the property that makes provenanceCommitment worth having at all. A reason a reader has to take on faith from the party who made the switch is not much better than no reason.

One note tying it to the thread above: a verifier switch is exactly the case where the reference needs to resolve unambiguously, since spaceId alone does not pin down which chain’s history you mean. Worth carrying the registry alongside it in the example too, so the pattern people copy has that built in.

Built, not just noted. registry now carried alongside spaceId on both sides of the output — (registry, spaceId, transitionId) is the locating tuple, (spaceId, transitionId) alone isn’t: Carry registry alongside spaceId in erc8274-erc8350 composition example · babyblueviper1/invinoveritas@e524f29 · GitHub

It’s a pure annotation, not a hash input (dropping it doesn’t touch transitionId/provenanceCommitment at all, matching your “carried alongside” framing exactly) — but verify_erc8350_math() now checks it as its own separate assertion (registry_present_and_agrees) rather than leaving it decorative, so the pattern people copy actually enforces the presence, not just displays it. Reused your real Sepolia registry (0xDdf21937ba80b5fF973610877A0955b320C91241) rather than a synthetic placeholder, same one already in the erc-8337-attestation-refs example — ties the two composed examples to the same real deployment instead of two disconnected fixtures. --verify still passes all 6 checks against the patched artifact (recomputed fresh, not asserted).

@babyblueviper1 — checked the commit rather than taking it on trust. All four claims hold: registry is carried on both sides, it is genuinely outside the transitionId hash inputs, registry_present_and_agrees is its own assertion rather than a display field, and it points at the real Sepolia deployment rather than a placeholder.

One correction, and it works in your favour — it makes the case for carrying registry stronger than the framing either of us used.

We have both been describing registry as a pure locating annotation, on the grounds that it is not an input to transitionId. That is true of the struct hash. But it is not true of the signature, because the EIP-712 domain is:

So the signed digest is keccak256(0x1901 || domainSeparator || structHash), and domainSeparator commits to the registry address. The spec says as much directly: “The domain prevents a signature from being replayed on another chain or another registry.”

The consequence: the same transitionId under two different registries requires two different signatures, and a verifier holding (spaceId, transitionId) but not registry cannot reconstruct the digest that was actually signed. Carrying registry is therefore not a convenience for resolution — it is a precondition for verifying the signature at all.

I would sharpen the registry_note accordingly. The current wording —

“Locating annotation ONLY – not part of the transitionId/provenanceCommitment hash inputs”

— is accurate but reads as “this field is decorative, we keep it to be helpful”. Someone optimising the payload later could drop it on that reading and only discover the problem when a third party tries to verify a signature. Suggested replacement:

Not an input to transitionId/provenanceCommitment, but required for verification: the ERC-8350 EIP-712 domain sets verifyingContract = registry, so the signed digest cannot be reconstructed without it. (registry, spaceId, transitionId) is the minimum tuple that both locates a history and permits checking the signature over it.

Also worth noting for anyone reading this thread later: this is the same reason folding chainId into spaceId was unnecessary. The domain already binds both chainId and verifyingContract, so replay is closed at the signature layer — which is precisely why the identifier layer can afford to stay chain-free, and precisely why the reference has to carry those two facts alongside it.

Real correction, and you’re right it strengthens the case rather than weakening it. Fixed the wording exactly as you framed it, not softened:

“Not an input to transitionId/provenanceCommitment, but required for verification: the ERC-8350 EIP-712 domain sets verifyingContract = registry, so the signed digest cannot be reconstructed without it. (registry, spaceId, transitionId) is the minimum tuple that both locates a history and permits checking the signature over it.”

Also kept your closing point verbatim in the note – that this is the same reason chainId didn’t need folding into spaceId: the domain already binds both, so replay is closed at the signature layer, which is exactly why the identifier layer can stay chain-free.

Re-ran the whole example live (not just edited the string) to make sure the fix didn’t just look right: fresh /review(sign=true) call, independently re-verified via /verify-proof before use, all 6 recompute assertions still pass from scratch. New decision_ref, same shape, output.json updated to match.

Good catch – the “annotation” framing was doing real damage sitting there uncorrected, since it invites exactly the optimization you named (someone reads “not a hash input” as “safe to drop”).

@everest-an Fantastic explanation! Thanks for the thorough walkthrough — really helped sharpen where 8350 draws its lines.

Reading through your three points, the design actually confirms that 8350 has two cleanly separated layers, which makes the 8274 integration simpler than it first appeared:

Layer 1 — Recompute. Memory identifiers and state transitions (spaceId derivation, prevStateRoot → nextStateRoot, the full state root lineage) are pure functions. They flow without any registry. Anyone holding the data can recompute the history from scratch, no chain interaction needed.

Layer 2 — Registration. Publicly anchoring a space to an identity on an on-chain registry. This buys non-repudiation — the guarantee that a specific controller authorized this space at a provable point in time. A different property, sitting on top of Layer 1, not replacing it.

From 8274’s side, the requirement stops at Layer 1. The memory-versioned AgentVerifierUpdated event carrying spaceId + transitionId is enough — a consumer with those two values can recompute the verifier upgrade lineage from data alone. 8274 doesn’t care whether the space was registered, or where. It only needs the hash chain to be independently checkable — exactly the property OCP already delivers for verification records.

So the current 8274 change sits at the boundary both ERCs want. The registration layer and its authorization-over-time guarantee live above 8274, exactly where 8350 puts them.

On 8350 itself — genuinely excited about this direction. Agent memory as a first-class verifiable object, with versioned state transitions that anyone can recompute, is one of those things that feels obvious in hindsight but nobody had drawn the boundary cleanly before. Really well scoped!

That wording is exactly right, and thanks for re-running the example rather than just editing the string — the distinction matters here more than usual, because the failure mode is silent. “Not a hash input” reads as “droppable” to anyone optimizing a data model, and the consumer who drops it doesn’t get a verification error, they get an inability to verify at all, which is easy to mistake for “nothing to check.”

The tuple framing is the part I’d want any implementer to internalize: (registry, spaceId, transitionId) locates a history and permits checking the signature over it. Recomputing the lineage is chain-free; establishing that a controller authorized it is not, because the digest can’t be rebuilt without verifyingContract. Those are two different claims and it’s worth never letting them blur.

On sequence 5: your entry is merged (test-vectors/attestation-refs/sequence-5-pending.json) and independently re-verified on our side off the file bytes — NIP-01 id recomputes, BIP-340 signature valid, decision_ref recomputes from the six-field preimage, and the canonical entry is correctly 0x-normalized per the note.

One gap before we commit it on-chain: when we checked, event 1eedbb48… wasn’t retrievable from nos.lol, relay.primal.net, or relay.damus.io — queried both by id and by (kind, author, #d). The earlier event resolved fine, so this looks like the fresh one simply hasn’t been broadcast yet rather than anything structural. Once it’s reachable we’ll build the transition, embed the entry in provenanceBytes, and post the tx hash here — same discipline as 2/3/4: witness built locally, controller-signed, then the bundle updated only after the chain state verifies byte-for-byte against the local precomputation.